为什么bcrypt密码哈希可能不安全?
Why Bcrypt Can Be Unsafe for Password Hashing?

原始链接: https://blog.enamya.me/posts/bcrypt-limitation

## bcrypt 的隐藏 72 字节限制 尽管 bcrypt 长期以来被广泛使用,并且是一种强大的密码哈希算法,但它有一个鲜为人知的限制:它只处理密码的前 72 个字节。这源于它基于 Blowfish 密码,而 Blowfish 密码具有这种内在限制。 如果密码超过 72 字节,bcrypt 会有效地忽略多余的部分,可能导致碰撞和安全漏洞——正如 Okta 最近发生的一起事件所证明的那样。这个限制适用于*字节*,而不是字符,这意味着包含多字节字符(如表情符号)的密码可能会更快地达到限制。 现代 Python 的 bcrypt 包 (v5.0.0+) 现在会对超过 72 字节的密码引发错误,但其他实现方式各不相同——有些会截断,有些会报错,有些则提供选项来控制此行为。 为了未来的安全性,请考虑使用 Argon2 等替代方案,或者在应用 bcrypt *之前* 将密码哈希为固定大小的摘要(如 SHA-256)。虽然 bcrypt 仍然适用于典型的 72 字节以下密码,但了解此限制至关重要。

## Bcrypt 密码哈希:潜在弱点 一篇 Hacker News 的讨论强调了 bcrypt 中可能存在的安全漏洞,bcrypt 是一种密码哈希算法,尽管于 1999 年开发,但至今仍被广泛使用。核心问题是 bcrypt 的 72 字节限制,它会静默截断超过此长度的密码。 虽然 72 个字符似乎足够,但使用像表情符号这样的多字节字符会迅速耗尽此限制,*降低*密码的有效熵。这是因为在截断的字节空间内,可能的组合更少。一些人认为这并非主要问题,理由是 bcrypt 经过验证的良好记录以及攻击者不太可能针对基于表情符号的密码。 然而,讨论强调,像 Argon2、scrypt 和 yescrypt 这样的现代替代方案是专门为密码哈希设计的,解决了这些限制。共识倾向于改进文档,并可能在库中添加异常以防止静默截断,以及减少对通用哈希函数在密码安全方面依赖。
相关文章

原文

TL;DR: bcrypt ignores any bytes after the first 72 bytes, this is due to bcrypt being based on the Blowfish cipher which has this limitation.

bcrypt has been a commonly used password hashing algorithm for decades, it’s slow by design, includes built-in salting, and has protected countless systems from brute-force attacks.

But despite its solid reputation, it also has a few hidden limitations worth knowing about.

Let’s take a look at this code:

import bcrypt

password_1 = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"
password_2 = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"
hashed_password1 = bcrypt.hashpw(password_1, bcrypt.gensalt())
if bcrypt.checkpw(password_2, hashed_password1):
    print("Good password")
else:
    print("Bad password")

The code takes a string (as bytes) starting with 72 a’s and ending with 1, hashes it using bcrypt.hashpw, and then checks the same string ending with 2 this time against the hashed password.

The output should be Bad password, right ?

Let’s run the code, and see the output:

bcrypt.checkpw returns True

bcrypt.checkpw returns True

The code shows Good password, but why ????

Turns out that bcrypt is based on the Blowfish cipher, which only encrypts the first 72 bytes, this limitation is therefore inherited by bcrypt.

The Blowfish algorithm uses an 18-element P-box, where each element is a 32-bit (4-byte) subkey. Therefore, the total P-box size is 18 * 4 bytes (72 bytes).

This means that if the password is longer than 72 bytes, the bcrypt algorithm will only encrypt the first 72 bytes, and the rest will be ignored.

bcrypt’s 72-byte limit applies to bytes, not characters. This means that passwords with non-ASCII characters (like emojis or accented letters) may reach the limit even sooner, since UTF-8 encoding can use more than 1 byte per character.

If you don’t want to worry about this limitation, you have several alternatives:

  • Use a different algorithm, like Argon2, which does not have this limitation (Awarded the Password Hashing Competition in 2015).

  • Hash the password into a digest -less than 72 bytes- first (SHA-256, SHA-512, etc.), and then hash the digest using bcrypt. See the following example:

Hash with SHA-512 before bcrypt

Hash with SHA-512 before bcrypt

  • You can always enforce the password length to be less than or equal to 72 bytes by yourself, but it’s not recommended to do so.

However, since version 5.0.0, Python’s bcrypt package began raising errors when hashing passwords longer than 72 bytes, this commit introduces the change: Raise ValueError if password is longer than 72 bytes

bcrypt.hashpw raises an error since version 5.0.0

bcrypt.hashpw raises an error since version 5.0.0

This limitation is handled in different ways in other languages and libraries.

  • Go raises an error when the password is longer than 72 bytes
  • OpenBSD’s bcrypt implementation truncates the password if it’s longer than 72 bytes
  • Rust’s bcrypt truncates the password by default, but offers non_truncating methods to raise BcryptError::Truncation error if the password is longer than 72 bytes.
  • Spring Security’s base class BCrypt offers the method hashpw with for_check flag (weird name, right ?) to raise IllegalArgumentException if for_check = false, while I havent found a way to pass a similar flag to the BCryptPasswordEncoder class.

In 2024, Okta -a major security service provider- had a security incident due to this limitation, they announced that the incident was caused by using bcrypt to hash cache keys for their AD/LDAP delegated authentication feature, which allowed attackers to use new usernames with old cached keys to authenticate to the service and gain access to user accounts.

To summarize, bcrypt is still fine for typical passwords <72 bytes, but consider other options for future-proof security.


I discovered this limitation in @devhammed’s tweet, thanks to him for sharing this information.

联系我们 contact @ memedata.com