If you allow users to log in to your system, you need to hash their
passwords with a cryptographically secure mechanism. That means not MD5 or
basic Unix crypt(3)
(though credit goes to OpenBSD for allowing the use of Blowfish with their
crypt(3)
.
It's obvious why hashing passwords is necessary: storing passwords in plain text offers an attack vector by which you can inadvertently expose private user data to the wild world. Even if an attacker somehow gets access to hashed passwords, extracting the plain text password (or its cryptographic equivalent) from the hash is an expensive operation.
Yet the quality of the hash matters. Faster computers can perform brute force attacks against simpler hashing functions. DES and RSA aren't sufficient. MD5 is not sufficient. Even SHA-1—which worked really well until a couple of years ago—isn't sufficient. The modern consensus seems to prefer the Blowfish algorithm as sufficiently secure.
Yet I had deployed code which used SHA-1 to hash sensitive user information.
Upgrading passwords in place is reasonably simple, though. (Here's the part where posting code about a security issue has the potential to expose a really silly bug, in which case thank you for helping me fix my code.)
I have a user model built with DBIx::Class. Its check_password()
function (slightly misnamed, but misnamed to integrate with other components) verifies that the given plaintext password hashes to the stored hash value. If so—and if the user has the is_active
flag set to a true value—the login can continue. Otherwise, the login fails.
I'd previously extracted the hashing function into a single module which
exports a hash()
function. This turned out to be a wise approach;
it's one and only one place in the code where I can change the underlying
hashing mechanism.
That abstraction meant that switching new users to use Blowfish could happen automatically. Upgrading existing users took a little more work:
sub check_password
{
my ($self, $attempt) = @_;
my $password = $self->password;
# upgrade existing passwords to Bcrypt passwords
if (old_hash($attempt) eq $password)
{
# crypt with blowfish
$password = hash($attempt);
$self->update({ password => $password });
return $self->is_active;
}
return unless hash($attempt, $password) eq $password;
return $self->is_active;
}
When a user attempts to log in, first compare with the old hashing mechanism. If that matches, update the user's password in the database with the new hashing mechanism. Otherwise, use the new hashing mechanism.
This code could be even easier; Crypt::Eksblowfish::Bcrypt returns a hashed password encoded in Base-64 with a special string prepended which contains the password hashing settings. Those magic characters never appear in the encoding mechanism I used for SHA-1 passwords, so only encoded passwords which start with that sequence can use the new hash. Everything else should attempt to match against the old hash.
One drawback of this technique is that it makes what would normally be a read-only operation (compare passwords) perform a write (update passwords), but this is a temporary workaround. When all of the users have logged in (and had their passwords update transparently), I can remove this workaround. Detecting that is easy: check the first few characters of each password for the special prefix.
This technique isn't particularly original or difficult, but it's worked very well for me so far. Better yet, my users haven't noticed at all. They get more security for free with no work on their part. That's a good tradeoff.
The only thing that I can think of is why always do the old check first? I know that this is 'temp' code but it seems that if you can 'find' new passwords by prefix then you could just use that do decide what hash to match and then you would only do the hash once in the case of 'updated' passwords.
You're right. I found and fixed a bug in the actual code, and your approach made that easier.
I thought about doing things the right way when I first wrote the code, but somehow convinced myself that it wasn't worth the additional effort. Perhaps I hadn't realized then that the prefix approach would work.
Here's something that has always bugged me. I'm sure it must be a solved problem, but I have been unsuccessful in finding the proper practice. It's a little bit of a tangent, but I hope you'll indulge me.
So your user sets his password. The client hashes the password, and sends it to your server. You store the user ID, hashed password, and maybe the salt value. Haha, you think, I'm safe from hackers now.
Haven't you just substituted hashed-passwords for passwords?
If someone hacks into your database and grabs user IDs, salts, and hashed-passwords, can't they just use those to log in? Their client, instead of hashing a plaintext password, sends the hashed password. Or say you don't get hacked, and someone eavesdrops on a user's unencrypted internet conversation. He'd get the hashed password and be able to log in with that, no?
If the solution is "force https" or "have the client encrypt a packet containing the userid & hashed password", then isn't hashing the password redundant?
The client doesn't send hashed passwords—or if the client sends a hashed password, you have to hash it again on the server.
Eavesdropping and securing stored passwords are different problems.
Hash it again.... well shoot, that makes sense. <slaps forehead> Thanks.
What would be the disadvantage of taking the unsalted hashes that you have stored in your database and creating digests based on them instead of waiting for users to supply the plain text version again?
That's a reasonable approach too. In my case, I didn't want to modify data in place, but it would have worked.
The only downside is that detecting whether a user has an upgraded password is slightly more difficult in this case. My code used a really stupid start-of-string comparison to see if the salt is present, but that was a short-lived hack I wouldn't have left in place for more than a week.
Thanks for the reply. For clarity, I was imagining running the first bit of code against all users in the datastore in a one-off upgrade, and upgrading the app with the second bit of code thereafter. That would mean no need to determine which digest method was in use per request.
What concerned me was would hashing the hash present a weakness? However, I think that the improved salting, encryption algorithm and iterations that Authen::Passphrase introduces makes it sufficiently difficult to crack.
That said, I am not a cryptographer. It'd be good if we could find one or two to consider our approaches and maybe give us ideas for how to do graceful upgrades on existing data.
Thanks agaim