Passwords shouldn't be limited to 72 characters
Current behavior
Due to bcrypt passwords are limited to 72 bytes/characters.
Expected behavior
Possibly pre-hash the password before putting it into bcrypt. See downstream issue https://github.com/tootsuite/mastodon/issues/13152 for details why it is a problem in Mastodon.
Note: I have no knowledge of Ruby, I'm just creating this issue.
72 Bytes actually. UTF-8 character encodes into 1~4 bytes
Maximum password length should be limited because of computation time of password hash. It takes 15-150 seconds of uninterruptible bcrypt hash calculation if password have ~1024 characters length.
With https://github.com/heartcombo/devise-encryptable you can use other encryption tools, such as argon2, which will hash up to a 4Gb input string (not recommended, it might let out the magic smoke!).
Anyone who is (or is considering) pre-hashing should be aware that OWASP considers it a dangerous practice that should be avoided.
@drakmail The bcrypt algorithm actually ignores the characters after the first 72 bytes, so there is no way to hash a 1024 character password with bcrypt. Perhaps you meant another algorithm?
@pboling when user sends a long password (tens of thousands characters) the rails server hangs for several seconds consuming 100% CPU
@pboling when user sends a long password (tens of thousands characters) the rails server hangs for several seconds consuming 100% CPU
Are you using the bcrypt-ruby gem? That's the default in Rails' Gemfile. Starting at 512 bytes it should raise BCrypt::Errors::InvalidHash, for instance:
3.1.0 (mbb)[1] » BCrypt::Password.create('a'*512)
BCrypt::Errors::InvalidHash: invalid hash
So bcrypt shouldn't return a hash for a password containing 1024 characters. The hang-up you're describing is most likely related to ruby itself, in my IRB console, this does cause a (temporary) hang up:
# only do this if you don't mind hangups.
3.1.0 (mbb)[2] » 'a'*99999999999999
Back to the main point; the limit of bcrypt is 72 bytes, which effectively means a password containing only a's of 72 bytes is equal to 73 bytes of a's ad infinitum. To illustrate this:
3.1.0 (mbb)[3] » b72 = BCrypt::Password.create('a'*72) # 72 bytes of a's
=> "$2a$12$g2IH91mSXl6Xww/RfvVI6.l3g9fHL.KtD5Xg4bhoVE5l6cx3hQRrC"
3.1.0 (mbb)[4] » b72 == 'a'*73 # <-- not the same password
=> true
3.1.0 (mbb)[5] » b72 == 'a'*74 # <-- not the same password
=> true
3.1.0 (mbb)[6] » b72 == 'a'*100 # <-- not the same password
=> true
In the linked thread in a mastodon repository, a case is made for seemingly small passwords exceeding 72 bytes. It's a valid case and point. Truncating passwords should be avoided. So, in case anyone also hit this issue, here are a couple of ways to handle this issue:
- You can refuse passwords longer than 72 bytes. This isn't a great way.
- Use a different algorithm, i.e. Argon2i or Argon2id.
- Use BCrypt for passwords up to 72 bytes and switch over to Argon2i(d) for passwords longer. (bcrypt has been around way longer than argon2 and has proven itself; so thats something to keep in mind.)
In any case, password fields should have a limit to avoid bad actors from DoS'ing your service and this limit should be enforced before you hash the password. Especially when using a slow algorithm (i.e. bcrypt, argon, etc.)