PHP Strongest one way encryption/hashing method

user1763295 picture user1763295 · Apr 21, 2013 · Viewed 22.3k times · Source

I have a site on which people can sign up and I need to encrypt their password. I've researched it but I can't find any methods that won't be beat by the power of a modern GPU.

So I have come to the good people of StackOverflow to ask, what is the strongest method of encryption possible, I have done my best to stop people getting their hands on the database but I want to be as sure as possible that their data would be fine if the database was somehow stolen.

Other things I wonder are, would be it be safer to just randomize the characters in the password somehow but in a way where you would be able to randomize them again for login?

Edit: I have used the implementation of bcrypt by Andrew Moore (How do you use bcrypt for hashing passwords in PHP?) and come up with this:

public static function Encrypt($Str,$Salt)
{
    $bcrypt = new \bcrypt();
    return $bcrypt->hash(SERVER_SALT . md5($Str) . $Salt);
}

if anyone sees anything wrong with it or any weaknesses please tell me.

Answer

LSerni picture LSerni · Apr 21, 2013

For passwords, you can't beat bcrypt. Here is a link on SO: How do you use bcrypt for hashing passwords in PHP? . Its main advantage: it is inherently slow (as well as secure). While a normal user will use it once, and will not appreciate the difference between one tenth and one millionth of a second, a cracker will discover that instead of finding the password in four days, he now needs twelve centuries (except that forcing Blowfish even at one million attempts per second does not require four days: barring implementation errors and as yet unforeseen crypto breakthroughs, the heat death of the Universe will still come first).

For data, I'd rely on the database engine itself; MySQL supports AES, and that's quite good. Yes, with enough GPUs one could beat it, and since to crack the code within the century one would need some four billions of them, he'd probably get a good quantity discount too. But I wouldn't worry.

As for the "randomization" of the password, it would serve no purpose. If someone were to resort to bruteforcing your password, he would have the same probability of finding the randomized sequence or the non-randomized one. It might be of some help if one were to use a dictionary attack (i.e., not trying everything from AAAAA to ZZZZZ, but trying ABRAHAM, ACCORDION, ... up to, say, ZYGOTE). But even then, an attack against the server would change nothing (the server receives the non-randomized password!), and an attack against the stored hash would be better nullified by salting, which bcrypt automatically does: i.e., storing the hash of $76X.J:MICKEYMOUSE instead of the hash of MICKEYMOUSE, together with the needed overhead to handle the extra '$76X.J:' on input. And while the randomization would have to be fixed, the '$76X.J:' sequence is completely different on every write -- good luck figuring that out!

Salting

If you implement salting yourself (as shown above, with bcrypt you don't need to), you can do it by generating a unique random sequence and storing it either in the password field or in a second field. For example

user    password
alice   d9c06f88:6c14d6d313d7cbcb132b5c63650682c4

Then, upon receipt of the password by Alice ("mickeymouse"), you would look in the database to see whether a user called alice exists. If it does, recover the salt (here d9c06f88) and the hash. If it does not, set a "BAD" flag and fetch a fixed salt and hash (e.g. 12345678:0000000...).

In MySQL this can be done using UNION:

SELECT password_fields FROM users WHERE user=? AND hash=?
UNION SELECT
    '12345789' as salt,
    'ffffffffffffffffffffffff' as hash,
    'fake' as user
LIMIT 1;

will retrieve either the correct data or an incorrect set in roughly the same time (this prevents someone to guess which names exist and which do not by timing the time needed to answer "wrong user or password").

Then concatenate the salt and the password, and generate the hash of d9c06f88:mickeymouse. If it does not match, or the "BAD" flag is set, reject the password (instead of using a 'bad' flag, you can repeat the test for user match that MySQL already did: you can initialize the user name by replacing the last character with an invalid one, to ensure it will never match a real user).

Security through noninformation

The added twist of selecting a fixed string is useful because you want the three cases, "user does not exist", "user exists but password is incorrect" and "user exists and password is correct" to be as similar (same complexity, same expense in calculations) as possible.

This way, an attacker will be less likely to tell what happened: was the user incorrect? Was the password wrong? And so on. If the times were different enough (say two queries for valid users, one for invalid users), an attacker with care, time and a good stopwatch could determine statistically whether a given username is present or not.

In the case of the user, even the name comparison will be

 johnsmith     against   johnsmith            if johnsmith exists
 johnsmith     against   johnsmit?            if johnsmith does not exist

so telling what happened from the other end of a HTTP(s) connection won't be easy.

For the same reason, you will not return two different errors for "Bad user" and "Bad password", but always a "Bad user or password"; maybe with the option, for the user, to receive an email to his/her registered email to remind him/her of the username. And of course, you want the system to not send such an email before 24 hours have passed from sending a similar email to the same user, to prevent the system from being exploited to harass someone with spurious "recovery emails".

If you do send the email, you will wait until a preset time has expired (say, 3 seconds), and then inform users that if the username was present, then they should check their inbox (and the spam folder for good measure).

There would have been a time for such a password

A convenient way to improve server security against bruteforcing is to implement a delay on password authentication, and maybe (if you're really paranoid) a CAPTCHA-ed lockout after some X wrong attempts.

You don't want to CAPTCHA the first attempts because users take a dim view of CAPTCHAs.

Delayed lockout is often implemented with sleep (or equivalent), or using a "lock out until manually reset by the admin" strategy. Both methods are bad. The lockout feature can be used to create a denial of service attack, either by locking out an user, or creating lots of server threads stopped in "password authentication delay" state (they won't use CPU, but they will still use memory, sockets and other resources).

In some cases this can happen unwittingly. There seems to be some idiot who uses my bank, and every couple of months I get a SMS saying "WRONG PIN ENTERED IN YOUR HOME BANK SYSTEM". Then I have to log in from my phone, to reset the unsuccessful attempt counter; because if the idiot doesn't realize that he can't get in his account because he entered my account number, for three times, it is MY account that gets locked and I have to go physically to the bank and beg them to reset my access. This, let me tell you, is a major pain in the nether regions, and even knowing it's not their fault I still resent my bank. You don't want to engender such feelings in your users.

And it is best to move the burden on the client:

(very pseudo code)
login = FAIL
if in SECURITY LOCKOUT MODE for this account
    if a session is open and contains a last-attempt time
        if at least DELAY seconds have elapsed since last-attempt
            check the password
            if it is correct
                login = OK
                zero password counter, exit lockout mode.
                # An "innocent" user enters with no lockout! Yay!
            end
        else
            # "Early knocker". Either a bruteforcing robot
            # or a too clever user in a hurry. But we can't
            # tell them apart.
        end
    else
        # No session. Either a sessionless bruteforcing robot
        # or a unaware, innocent user. Again we can't tell them
        # apart. So we bounce both.

        # But a genuine user will assume he has mistyped the password,
        # or anyway will read the warning page, and will login after ONE
        # round of delay.

        # Users with password saved in browser will just click
        # "login" again and be logged in.

        # A robot will find itself delayed and ALL ITS PASSWORDS IGNORED
        # every single time. Even if it finds the right password... it will
        # not work.
    end
else
    check the password
    if it is correct
        # Good user, first attempt, fast login.
        login = OK
    else
        # Beginning to doubt this is a good user...
        increase password counter
        if it is > MAX_ATTEMPTS
            enter SECURITY LOCKOUT MODE for this account
        end
    end
end
if login is not OK
    generate a page with HTTP_REFRESH time of DELAY+1 seconds
    and a session ID, saying "User or password unknown,
    or you tried to login before HH:MM:SS (DELAY seconds).
    The page might also contain a Javascript timer, just in
    case. The delay is 1s more than necessary as a safety
    margin.
end