I've search SO and I know there are a lot of “Remember Me” questions but I couldn't find one specifically about using Blowfish for generating security tokens.
I'm implementing a login system to use cookie-based “Remember Me” functionality and all the tutorials I've read either don't look secure (e.g. just an unsalted md5 hash based on the time) or look overly complicated.
I am not building sites for a bank or anything like that but just want a reasonable level of security.
The system I am proposing is to simply create a 128 char random string for the user name and a second 128 char string for a login token. The raw strings would be stored in a cookie and unsalted sha1'd versions would go in the row of their user account in the database.
I guess I could even regenerate the strings on every page load.
To me this offers decent security (I think!) because:
My questions are:
Many thanks.
Well, there are two things that blowfish can mean. The Cipher (two way encryption algorithm) or the Hash (one way hashing algorithm, specifically called bcrypt). But more on that in a bit.
So let's look at your assumed advantages.
A hacker can't target a specific account by knowing their user name.
Even if they knew the username, it wouldn't matter. The 128 bit random string is large enough that you really don't need to worry about an 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 unless you are concerned about an attacker trying to break into your system in the next trillion years, 128 bit is plenty strong enough...
To forge the login cookie ... would have to guess 2 x 128 char strings
We've already shown that guessing 1 is not going to happen. So adding a second is not necessary (it may not be bad, but it's not needed)
If the database is hacked creating a rainbow table for 128 char strings is too difficult
Yes. However that's not what you should be concerned about. What you should be concerned about is brute forcing. But more on that later...
So, to answer your actual questions:
Does this sound reasonably secure.
Reasonably, sure. Over-complicated: definitely. There are simpler ways of dealing with this...
Is it worth using Blowfish instead of sha1.
No. Blowfish is for derivations (meaning where you want a proof of work). In this case you want to generate a MAC (machine authentication code). So I wouldn't use either Blowfish or SHA1. I would use SHA256 or SHA512...
Is there an advantage to running sha1 1000 times.
No
The better way
The better approach that I recommend is to store the cookie with three parts.
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 validate:
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's very important that the SECRET_KEY
be a cryptographic secret (generated by something like /dev/random
and/or derived from a high-entropy input). Also, GenerateRandomToken()
needs to be a strong random source (mt_rand()
is not nearly strong enough. Use a library or mcrypt with DEV_URANDOM)...
And timingSafeCompare is to prevent timing attacks. Something like this:
/**
* 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's 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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With