A Basic Lesson in Password Hashing

Posted by Sean on Jul 01, 2009 under PHP

In the world of the web, lots of sites are popping up requiring users to login. When you need to do so, there's a bit more security than you might realize. You might be making a simple To-Do list, and might think:

Security? Pfft, I'm not too worried about people's to-do lists being stolen.

But what you didn't account for, is that all those username/password combinations a hacker just made off with? Yea, those are the same login's to important stuff, like e-mail , or bank accounts . Yikes!

Worst Case Scenario

Most users of the web don't think this hard about their own security, and even those that do, it's too complicated to remember a unique username and password for every single web-site you visit . And most of those users that don't know much about security also don't realize the need for using complicated passwords. So if you're collecting usernames and passwords, you need to design for the worst possible scenario:

Password: password1

Just Hash It All

So hopefully, the first thing you're thinking is that you shouldn't store your passwords in plain text. Good idea. You were thinking a hash, right? Because 2-way encryptions has it's own security problems. Once a user discovers the encryption key, they can decrypt every password you have. So let's hash everything.

There's plenty of debate about the best hashing algorithms, so for sake of simplicity, I'll just use md5*. But we're not going to use the straight output of the hash. That's like storing plain text. Instead, we'll make sure we use a nice salt.

Not any salt. Nothing like "NaCLS4lt". No no, that also makes it too easy for hackers to precompute. Instead, we're going to generate a random salt for every single password. So even if they manage to crack just one password, they haven't gained the salt for the rest of the hashes.

Use a random salt when producing the hash

$salt = substr(md5(uniqid(rand(), true)), 0, $saltLen); 
 

We grab a nice, random string that gets hashed, and this garbled text becomes our salt.

$hash = md5($salt . $plain); 
 

Optionally, here you could do some manipulation, like splitting the password in half, or adding some salt to the end. The same goes for this entire process. The more you can mix it up without trying to come up with your own hashing algorithm, the more non-standard your passwords become.

Now then, we need this salt for later use, or else we'll never be able to regenerate this hash when a user logs in! We're not ganna store it in a seperate database field called salt. That gives it straight away to the hacker. Instead, we attach it to the hash and make it seem like the whole long thing is the password.

Many sites will suggest simply concatenating them together, $salt . $hash . However, I figure, with such a constant location, while the hacker does have to deal with random salts, he doesn't have to worry about the location of it.

Insert the salt in the middle of the hash

So we take something that is constant with the user, but different enough to allow variations for storing the salt. I'll use simply the length of the password in plain text. If the password is 11 characters long, I insert the salt at character 11 of the hash. This way, it's different than a password that is 8 characters in length.

return substr($hash,0,$saltStart).$salt.substr($hash,$saltStart+1);

Now, this function you've been building up, all you need to do is add an optional argument for a hash. When a user tries to login, you look up the hash from the database, and supply their password attempt and hash to this function. Now check if the hash is supplied, and if so, calculate the position of the salt in the hash, grab the salt, and use that for your md5($salt.$hash) part. If the function returns a hash that equals the hash in the database, you have the correct password.

*I don't claim to be a cryptographer, so use at your own risk.

Interested in PHP? Subscribe to my articles for free.

17 Comments

  1. Gravatar
    ellisgl Jul 01, 2009

    To make the passwords even "saltier", rehash each time a user logs into the system. Basically use the time they logged in and store the login time some where.

  2. Gravatar
    Simon Paarlberg Jul 02, 2009

    Please don't take this the wrong way Sean. It's nothing personal, heck, I don't even know you.. But what your describing here is what is known in security circles as "Security through obscurity" and is a big BIG no no!
    I agree with you that you should hash your users passwords, since they most likely use the same password for more important stuff also.
    Oh well, back to the topic of hashing; The reason why your described approach is bad, is because you encourage users to perhaps make already sound hashing more insecure by including homemade algorithms with data into the hashed string. In your example it's pretty harmless, but if a user don't think the approach through, a security risk could easily be introduced. So please Sean and others, don't make your own hashing/scrambling algorithms. Use already known algorithms. Use the SHA-1 or even better, the stronger SHA hashing algorithms to generate that hash. They are still the most secure algorithms around. Remember: use the right tools for the right job and don't throw in obscure stuff to fool the hackers. They _will_ find flaws and loopholes in your logic, because that's what they love to do.
    If you think I'm full of it, please read Graff & van Wyks Secure Coding - principles & practices for a better explanation than mine :-)

  3. Not sure it is bulletproof from a theory of information point of view, but it seems nice. :)

  4. @Simon Paarlberg: Thanks for your input and concern. I wholeheartedly agree with you. I try to point out above, perhaps I'll edit the post and add a little section about that.

    without trying to come up with your own hashing algorithm

    I'm not suggesting anyone come up with their own hash. I mention using an already proven hashing algorithm, and store a salt inside in a non-consistent location. I did my research, and everything I suggest comes from security experts.

  5. We should notice that md5() is not secure.
    There is a lot of service which decrypt this algorithm. (just take a look to "decrypt md5" on google )

  6. @stf: That's true, md5 by itself is not enough anymore. Not because there's an algorithm to reverse md5. But because people have built tables of plain text passwords and their hashes. By uniquely salting, you're greatly lowering the chance of it existing in a table.

    Also, my example shows adding the salt onto the hash, so it's not longer simply an md5 string. Take any hash you create using my example, and see what these "decrypt md5" sites tell you.

  7. Gravatar
    Andrew Jul 02, 2009

    Since we're talking about MD5 security, I took out 10 random md5 hashes out of my user database, and checked them on these md5 decrypt websites. (I didn't look at the username - only took out 10 random MD5 hashes, just too keep that clear)

    How many of them was I able to extract??

    10 out of 10.

    That's because 99% of the users out there tend to use:
    1. Passwords with number only
    2. Passwords out of a dictionary
    3. Birthdates
    4. Combination of a dictionary word with numbers

    They rarely use something like "vc7MKc39cmK". It's just too hard to remember. And boring to type in every time you do a login.

    Using a salt and those tricks in the article are a very good practice. I even recommend take it alot further. You could use many different hashing algorithms depending on the first letter of the salt. The ideas are endless, but I recommend the "Don't try this at home" before reading some good security books from experts.

    Security algorithm is only as strong as your knowledge on it.

  8. Simon and STF are right about this one. Look at their points in this order:
    1. MD5 isn't secure.
    2. The "stuff the salt in" part of your algo is security by obscurity. That's the part that is "your own hashing algorithm".

    This is only as safe as MD5 plus the secret obscurity sauce. You'd be much better off using a stronger hash algo and not trying so hard to improve the security of the algo you use.

  9. Gravatar
    Casper Jul 03, 2009

    Sean, I'm sorry to say: the trick your pulling here is utter BS.

    In the world of web applications, no-one should never have access to a database containing md5 hashed passwords. If someone gained access to the database of file you wrote the md5 password to, the same guy probably could read other sensitive data you are trying to protect too, drop your tables in the process, or better: set a new md5 string.

    Furthermore, adding some obfuscating scheme to md5 hashes requires code. If I already own your database, I will continue and try to own your filesystem too.

    STF: Your comment is even more lame. Building a dictionary with hashes doesn't make hashing passwords insecure.

    This entire article just doesn't make sense.

  10. @Casper: There are people that will hack more useless applications, where changing one's password or dropping tables is of no benefit to them. They know that if they can get a big username/password list, many of those will be useful on a bank web-site as well.

    Also, in many applications, the database and the code lie on two different servers, so it is possible to have gained access to the database server without the code, however, we have to assume they were able to get that too. Even with the code, all it lets them see is that we're using a random salt with the hash, and that the salt must be extracted from the hash. Since it's location is variable, that leaves him with lots of possibilities to try.

    Ultimately, we've made it a huge pain to learn a single password, and all the work that goes into one password, can't be used for any other, since all the salts are random.

  11. It feels really nice to see how you are trying to help people.

    When amateurs are facing lots of problems due to the hackers, its great the nice people like you are there to help.

    Thanks a lot, man.

    Best regards,
    Kushal.

  12. Gravatar
    Marsh Ray Jul 29, 2009

    Attempting to hide the salt adds no real security. In fact your scheme could inadvertantly weaken it.
    Let's say an attacker has one of your salted hashes. If he can somehow identify the salt within the hash, he now knows the length of the plaintext pasword, a major advance.

    Now how would he be able to distinguish the two apart?

      $salt = substr(md5(uniqid(rand(), true)), 0, $saltLen);
    

    Looks like any weakness in your uniqid() or rand() functions would open that vulnerability. For example, the system-supplied rand() functions are generally not cryptographic-quality RNGs, normally they're very simple pseudorandom number generators seeded with the current time the first time they're used.

    It's entirely plausible that the attacker can figure out when your server was last booted, or possibly force you to reboot it at a known time. He can also figure out what time your server's clock is set to, what Linux distro you're running, and so on. Everything about what goes into seeding rand().

    Given this, your scheme is probably worse than a separate salt.

  13. Cool,

    it's a good Idea to hash the pass word with MD5, but as you know there aare many encoding techniques why do you think that MD5 is the best

    Thanks for writing, most people don't bother.

  14. @software developer: I wouldn't say MD5 is the best. As you can tell from the comments, many people zealously hate it. I used it in the examples because it's easy for people to understand, more people know how to use MD5 than bcrypt or blowfish, etc.

  15. Gravatar
    DannyB Aug 26, 2009

    Store passwords as two hashes in database. SHA1 and MD5. That is, the database stores:

    • userPasswordSalt
    • * SHA1(userPasswordSalt+password)+MD5(userPasswordSalt+password)

    Rather than storing the password, the database stores concatenated hash functions of the password and salt. Why the salt?

    To make it difficult to dictionary or rainbow table attack passwords if table of password hashes is stolen from database. Why two hash functions?

    You might be able to find a plaintext password for either hash using John The Ripper or Rainbow Tables, but you aren't likely to find a plaintext that generates BOTH hashes unless it was what the user entered. This applies to both an attack on stolen password table, and to a packet capture of salt+hashed passwords crossing the network. Now if we can just get users to use good passwords!

    Why SHA1 and MD5 instead of other hash functions? JavaScript implementations are readily available. When logging in, the browser AJAX requests the two salts from the server
    by sending only the user name. Whether or not the user name is on file in the DB, two salts are returned: userLoginSalt and userPasswordSalt. When logging in, the browser computes hashes of the password. Thus, only the salts and hashes ever traverse the network. Thus, the login is protected against packet sniffers even without SSL.

    To prevent replay attacks, where the packet sniffer guy just replays the same salts and hashes over the network is why the userLoginSalt is returned along with the userPasswordSalt. The JavaScript code in the login page then computes:

    SHA1( userLoginSalt + SHA1(userPasswordSalt+password)+MD5(userPasswordSalt+password) ) + MD5( userLoginSalt + SHA1(userPasswordSalt+password)+MD5(userPasswordSalt+password) )

    Since the server already has SHA1(userPasswordSalt+password)+MD5(userPasswordSalt+password) and userPasswordSalt on file in the database, the server can compute the same salted hashes and compare. Every time the login page AJAX's the server for the salts, the "userLoginSalt" value is different (per session). There are ways to implement this so that no session need be created prior to a successful login. This helps avoid DDOS attacks against the login page that create sessions
    which consume server resources.

  16. @DannyB:
    okay Danny, i'm a hacker... i gain access to your users db table. I can see a field with hashes, all hash is 72 chars long. if i'm not an idiot, is takes second to find out, that it's an md5 and sha1 hash concatenated, since md5's length is 32 chars (128 bits), sha1's 40 (160 bits).

  17. I agree with the salt for the passwords but you can go too nuts and end up with a whole class just around the password hashing ! More bulk.

Add a Comment

Search

Categories

Treats

See all »