Storing Passwords Safely
- 1 Storing passwords securely and safely
- 2 Don't store the actual password typed in by the user. Use hashing instead.
- 3 Don't hash the password alone - use a salt, and a different one for each password
- 4 Don't use a simple hash function like md5 - use password_verify() and password_hash(), or a cryptographic hash function such as bcrypt
- 5 Don't implement your own hashing - use a library or the built in password functions
- 6 Important sidenote
- 7 Further reading
Storing passwords securely and safely
One of the really common things you do when building web apps is storing passwords for user accounts. Here's what you need to know:
- Don't store the actual password typed in by the user. Use hashing instead.
- But actually, don't hash the password alone - use a salt, and a different one for each password
- And don't use a simple hash function like md5 - if you are using PHP 5.5.0 or greater (which you really should be) you should use password_hash() and password_verify() directly, otherwise use the compatibility library for those functions.
- Don't ever implement your own password hashing - use a library or the built in password functions.
I'll go into each of these in detail. Please note that the sections below on how hashes work are for reference only and should not be copy and pasted into any script. As mentioned, you should never ever write your own hashing in a real world situation and you really should only be using password_verify() and password_hash(), or a library which uses these functions.
Don't store the actual password typed in by the user. Use hashing instead.
Doing this is incredibly bad because:
- if your password file is disclosed people can log in using those passwords
- people re-use passwords, so an attack on your site can lead to you being responsible for other sites being hacked
So, if you're not storing passwords, how do you check user logins? The most obvious answer is to store a simple hash of the password, and this is what most beginners to PHP would use. A hash is a one-way process - the idea is that if you hash the string 'abc', you'll get something like '5eb63bbbe01eeed093cb22bb8f5acdc3', and there's no simple function to go backwards. Hashes are used for lots of things in computer science, and there are lots of different hash algorithms, some more secure than others.
To check that a user's password matches the hash you have on file, you don't 'decrypt' the hash. Instead, you run the hash algorithm again, and check that the results match. Hashes are deterministic - they always return the same result for the same input.
So, in pseudocode, where you might do the following;
Instead, you might try something like:
This won't solve the problem though, as it has many inherent flaws (this is why this is provided in pseudocode rather than PHP, to discourage copy and paste). To find out why this is bad, and for better solutions, keep reading!
Don't hash the password alone - use a salt, and a different one for each password
One approach to breaking hashed passwords is to pre-compute dictionaries of hashes for a sensible set of possible passwords. That is, since we know that lots of people use 'password123' as their password, we can pre-compute the hash of that password, and in fact all short words or strings.
Salting is a straightforward way of making dictionary attacks useless - it still allows brute forcing, but it helps a lot. A salt is a random string that's added to the user's password before it's hashed, and is then stored with the resulting hash. When you come back to check the password later, you take the same salt, add it to the password again, and rehash.
There are few things you should note about salts:
- The salt should be different for every hash - never use a single salt for your whole application!
- It's most convenient to store the salt with the hash - many hashing libraries will actually give you back both the hash and the salt packed together in one string by concatenation.
- Salts should really be generated by a cryptographically secure PRNG (pseudo-random number generator) - this is not something you should attempt yourself. As with most security functions, it is best left to tried and tested, peer reviewed existing functionality.
Don't use a simple hash function like md5 - use password_verify() and password_hash(), or a cryptographic hash function such as bcrypt
There are lots of hash functions and they have quite different features. md5 is probably the most well-known, but it's actually a terrible hash for use with passwords - the thing is that md5 is designed to be fast. This is actually a major failing in a password hash, because the faster the hash, the faster you can brute force it.
In modern PHP you should use password_hash() and password_verify(), which are available as built in functions since PHP 5.5 which has been around since mid-2013. These have tried and tested security, and are future-proof as they allow you to re-hash your passwords (increase the length of time taken to verify them) as and when required.
Using password_hash() and password_verify() - RECOMMENDED
To use password_hash() and password_verify(), pass the password provided by the user and the password from your database to the password_verify() function. To create a new password for a user, use the password_hash() function.
For example, to verify a password:
And to create a new password in the database:
As time progresses, computers will become more powerful and the possibility of someone using end-user hardware to break your passwords will increase. To prevent this, you should periodically re-hash your passwords whenever PHP says you should, you should check this when users log in, for example:
Using the compatibility library for 5.3
There is a compatibility library available for PHP 5.3 and above, which implements the password_hash and password_verify functions on older PHP. You can use these to add the functionality to older PHP if for some reason you are forced to use an older version of PHP.
For example, first install it via composer:
Then, you can simply autoload it and it will operate exactly like the built-in functions:
Using bcrypt (discouraged)
A good cryptographic hash is bcrypt, or CRYPT_BLOWFISH. It's designed for just this purpose - it's very slow, and you can turn the knob to make it slower by adjusting its cost. This means that as computers become faster, you can rebuild your hashes to become slower and slower to process. It sounds silly to have functions be deliberately slow, but the point is that when you're using it to check a password, generally it doesn't matter if the hash takes a tenth of a second or whatever to finish - you're only doing it once. But for someone brute forcing millions of passwords, that's suddenly very hard for them.
Here's how to use bcrypt in PHP. Remember, don't actually use this in your code. Use password_hash and password_verify, and consider this just for educational purposes!
Don't implement your own hashing - use a library or the built in password functions
Ignoring everything above, the problem with writing your own security software is that you're likely to have bugs in it. In general, it's a better idea to use code written by someone else who's a security professional and that's been reviewed and used in practice.
If you are unable to use the built in password_verify and password_hash functions, or the compatibility library (WHY? PHP 5.3 has been out since 2009!) You could use phpass, which has been well-tested and is used by a wide variety of open source apps. It implements bcrypt as above, with various other nice features. Please note however that as of 2017 even the authors of phpass recommend that instead of using their library, you should use password_verify and password_hash wherever possible.
Here's the equivalent code using phpass:
If your website uses passwords at all, you should consider only allowing access to your website over SSL. If you do not, you are introducing a weak link in the chain and as previously mentioned people re-use their passwords, so if your site is the only one not using SSL, it will become the target for sniffing of plaintext passwords "off the wire" by attackers.
SSL is free and easy to obtain via letsencrypt, so there really is no excuse to avoid it these days. This is not directly related to securely storing passwords, but if you disregard this important note, you will severely weaken the security of any other technical measures you put into effect, no matter how securely your passwords are stored.