Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is Blowfish necessary for security token in PHP remember me system?

Tags:

php

hash

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:

  1. A hacker can't target a specific account but knowing their user name (they would need to know the 128 char string)
  2. To forge the login cookie and gain access to the account someone would have to guess 2 x 128 char strings.
  3. If the database is hacked creating a rainbow table for 128 char strings is too difficult.
  4. I can safely remember the user name only with storing the actual user name in the cookie.

My questions are:

  1. Does this sounds reasonably secure? Should I be using strings longer than 128 chars?
  2. Is it worth using Blowfish instead of sha1? (I.e. am I right in saying the rainbow table is too difficult to produce?)
  3. If not, is there any advantage to running sha1 say 1,000 times?

Many thanks.

like image 865
texelate Avatar asked Feb 16 '23 04:02

texelate


1 Answers

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.

  1. 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...

  2. 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)

  3. 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:

  1. Does this sound reasonably secure.

    Reasonably, sure. Over-complicated: definitely. There are simpler ways of dealing with this...

  2. 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...

  3. 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;
}
like image 195
ircmaxell Avatar answered Feb 22 '23 22:02

ircmaxell