Ok, let's go over a few points here
What you have in $salt
is not a salt. It's deterministic (meaning that there is no randomness in there at all). If you want a salt, use either mcrypt_create_iv($size, MCRYPT_DEV_URANDOM)
or some other source of actual random entropy. The point is that it should be both unique and random. Note that it doesn't need to be cryptographically secure random... At absolute worst, I'd do something like this:
function getRandomBytes($length) {
$bytes = '';
for ($i = 0; $i < $length; $i++) {
$bytes .= chr(mt_rand(0, 255));
}
return $bytes;
}
As @Anony-Mousse indicated, never feed the output of one hash function into another without re-appending the original data back to it. Instead, use a proper iterative algorithm such as PBKDF2, PHPASS or CRYPT_BLOWFISH ($2a$).
My suggestion would be to use crypt
with blowfish, as it's the best available for PHP at this time:
function createBlowfishHash($password) {
$salt = to64(getRandomBytes(16));
$salt = '$2a$10$' . $salt;
$result = crypt($password, $salt);
}
And then verify using a method like this:
function verifyBlowfishHash($password, $hash) {
return $hash == crypt($password, $hash);
}
(note that to64
is a good method defined here). You could also use str_replace('+', '.', base64_encode($salt));
...
I'd also suggest you read the following two:
Edit: To Answer the Migration Question
Ok, so I realize that my answer did not address the migration aspect of the original question. So here's how I would solve it.
First, build a temporary function to create a new blowfish hash from the original md5 hash, with a random salt and a prefix so that we can detect this later:
function migrateMD5Password($md5Hash) {
$salt = to64(getRandomBytes(16));
$salt = '$2a$10$' . $salt;
$hash = crypt($md5Hash, $salt);
return '$md5' . $hash;
}
Now, run all the existing md5 hashes through this function and save the result in the database. We put our own prefix in so that we can detect the original password and add the additional md5 step. So now we're all migrated.
Next, create another function to verify passwords, and if necessary update the database with a new hash:
function checkAndMigrateHash($password, $hash) {
if (substr($hash, 0, 4) == '$md5') {
// Migrate!
$hash = substr($hash, 4);
if (!verifyBlowfishHash(md5($password), $hash) {
return false;
}
// valid hash, so let's generate a new one
$newHash = createBlowfishHash($password);
saveUpdatedPasswordHash($newHash);
return true;
} else {
return verifyBlowfishHash($password, $hash);
}
}
This is what I would suggest for a few reasons:
- It gets the
md5()
hashes out of your database immediately.
- It eventually (next login for each user) updates the hash to a better alternative (one that's well understood).
- It's pretty easy to follow in code.
To answer the comments:
A salt doesn't need to be random - I direct you to RFC 2898 - Password Based Cryptography. Namely, Section 4.1. And I quote:
If there is no concern about interactions between multiple uses
of the same key (or a prefix of that key) with the password-
based encryption and authentication techniques supported for a
given password, then the salt may be generated at random and
need not be checked for a particular format by the party
receiving the salt. It should be at least eight octets (64
bits) long.
Additionally,
Note. If a random number generator or pseudorandom generator is not
available, a deterministic alternative for generating the salt (or
the random part of it) is to apply a password-based key derivation
function to the password and the message M to be processed.
A PseudoRandom Generator is available, so why not use it?
Is your solution the same as bcrypt? I can't find much documentation on what bcrypt actually is? - I'll assume that you already read the bcrypt Wikipedia Article, and try to explain it better.
BCrypt is based off the Blowfish block cipher. It takes the key schedule setup algorithm from the cipher, and uses that to hash the passwords. The reason that it is good, is that the setup algorithm for Blowfish is designed to be very expensive (which is part of what makes blowfish so strong of a cypher). The basic process is as follows:
A 18 element array (called P boxes, 32 bits in size) and 4 2-dimensional arrays (called S boxes, each with 256 entries of 8 bits each) are used to setup the schedule by initializing the arrays with predetermined static values. Additionally, a 64 bit state is initialized to all 0's.
The key passed in is XOred with all 18 P boxes in order (rotating the key if it's too short).
The P boxes are then used to encrypt the state that was previously initialized.
The ciphertext produced by step 3 is used to replace P1 and P2 (the first 2 elements of the P array).
Step 3 is repeated, and the result is put in P3 and P4. This continues until P17 and P18 are populated.
That's the key derivation from the Blowfish Cipher. BCrypt modifies that to this:
The 64 bit state is initialized to an encrypted version of the salt.
Same
The P boxes are then used to encrypt the (state xor part of the salt) that was previously initialized.
Same
Same
The resulting setup is then used to encrypt the password 64 times. That's what's returned by BCrypt.
The point is simple: It's a very expensive algorithm that takes a lot of CPU time. That's the real reason that it should be used.
I hope that clears things up.