Well, there are two things that blowfish can mean. Cipher (two-way encryption algorithm) or Hash (one-way hashing algorithm, specially called bcrypt). But a little about that.
So let's look at your perceived benefits.
A hacker cannot target a specific account knowing its username.
Even if they knew the username, it would not matter. The 128-bit random string is large enough, and you really don't need to worry about the attacker guessing it. Let's do some math.
2^128 == 3 * 10^38 possible combinations Assuming there are 50 million servers on the internet, And each server can do 100,000,000,000 guesses per second (to your server) 3e38 / 50,000,000 / 100,000,000,000 == 6 * 10^19 seconds Which when converted to years is: 1,902,587,519,025 To have a 50% chance of guessing it, the attacker would need to hit 1/2: Years to 50% chance of guessing: 951,293,759,512
So, if you are not worried that the attacker is trying to penetrate your system in the next trillion years, 128 bits is a lot strong enough ...
To fake the login cookie ... you had to guess 2 x 128 char strings
We have already shown that guessing 1 will not happen. Therefore, adding a second one is not required (this may not be bad, but it is not necessary)
If the database is hacked, creating a rainbow table for 128 char rows is too complicated
Yes. However, this is not something you should worry about. What bothers you is cruel coercion. But more on that later ...
So, to answer your current questions:
Does that sound reasonable.
Reasonable, of course. Complex: definitely. There are simpler ways to deal with this ...
Should I use Blowfish instead of sha1.
No. Blowfish is for derivations (which means you want proof of work). In this case, you want to generate a MAC (machine authentication code). Therefore, I would not use either Blowfish or SHA1. I would use SHA256 or SHA512 ...
Is there any advantage in working sha1 1000 times.
No
The best way
The best approach I recommend is to save a three-part cookie.
function onLogin($user) { $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit storeTokenForUser($user, $token); $cookie = $user . ':' . $token; $mac = hash_hmac('sha256', $cookie, SECRET_KEY); $cookie .= ':' . $mac; setcookie('rememberme', $cookie); }
Then to check:
function rememberMe() { $cookie = isset($COOKIE['rememberme']) ? $COOKIE['rememberme'] : ''; if ($cookie) { list ($user, $token, $mac) = explode(':', $cookie); if ($mac !== hash_hmac('sha256', $user . ':' . $token, SECRET_KEY)) { return false; } $usertoken = fetchTokenByUserName($user); if (timingSafeCompare($usertoken, $token)) { logUserIn($user); } } }
Now it is very important that SECRET_KEY be a cryptographic secret (generated by something like /dev/random and / or derived from a high entropy input). In addition, GenerateRandomToken() must be a strong random source ( mt_rand() not strong enough. Use a library or mcrypt with DEV_URANDOM) ...
And timingSafeCompare - prevent synchronization attacks . Something like that:
/** * A timing safe equals comparison * * To prevent leaking length information, it is important * that user input is always used as the second parameter. * * @param string $safe The internal (safe) value to be checked * @param string $user The user submitted (unsafe) value * * @return boolean True if the two strings are identical. */ function timingSafeCompare($safe, $user) { // Prevent issues if string length is 0 $safe .= chr(0); $user .= chr(0); $safeLen = strlen($safe); $userLen = strlen($user); // Set the result to the difference between the lengths $result = $safeLen - $userLen; // Note that we ALWAYS iterate over the user-supplied length // This is to prevent leaking length information for ($i = 0; $i < $userLen; $i++) { // Using % here is a trick to prevent notices // It safe, since if the lengths are different // $result is already non-0 $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i])); } // They are only identical strings if $result is exactly 0... return $result === 0; }