I recently started getting BCrypt "invalid hash" errors when I tried to save a record with a password, so I looked into the BCrypt code, and found the following method for validating passwords:
def valid_hash?(h)
h =~ /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/
end
which is called by the initialize method:
def initialize(raw_hash)
if valid_hash?(raw_hash)
self.replace(raw_hash)
@version, @cost, @salt, @checksum = split_hash(self)
else
raise Errors::InvalidHash.new("invalid hash")
end
end
The following passwords:
"PassiveForbearenceFox"
"VindictivePurpleAlligator12345"
"LostBlueLizard!@#$1234"
all return "Invalid Hash" errors.
I dug deeper, and found that the regex is checking the hashed password, but the hash that is getting passed has a stream of /x00s after it.
For example, if I use the password "LostBlueLizard!@#1234", the hash that is passed to challenge the regex is:
"$2a$11$NcmldbbyCDfumGYALgYhfuIQt2FZ8gpbVCQfuiVlwjhCtkD2ndDFy\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\...
and so on, for farther than I could scroll.
If I remove the \x00s and pass in $2a$11$NcmldbbyCDfumGYALgYhfuIQt2FZ8gpbVCQfuiVlwjhCtkD2ndDFy, it passes the regex.
Why does the hash have those characters? I can't prove they weren't there before, but my passwords were always accepted until recently.
Also, it's actually the raw_hash that's getting input into the initialize method that's the hash with all the \x00s. So somewhere there's code that generates the faulty hash and passes it to initialize, but I searched the entire gem's codebase for the term "initialize" and there were no results like that, so I can't find where the hash is actually generated.
I tracked the issue further, to the BCrypt::Engine.create method, which calls __bc_crypt, which is what returns the faulty hash. __bc_crypt is actually from the C source file ext/mri/bcrypt_ext.c, which is beyond my ability to investigate.
I updated BCrypt from 3.1.11 to 3.1.13, and now it works fine. I'm not sure whether it was a bug or not, but the point is it works now.