[[more-secure-passwords-with-php]] More Secure Passwords with PHP ------------------------------ [source,javascript] ---- function thest() { alert('hello'); } ---- Posted on Wednesday, 4th September 2013 by admin Update 2: This article has been superceeded by a more to-the-point article which focuses more on using the newer functions. Update 1: PHP 5.5 implements new password functions to simplify working with passwords. Even if you are using an earlier version of PHP, you should look at these functions. You can learn more at the end of this article. Website owners who maintain user details have a great responsibility. Apart from keeping the database safe and sound, you will have to ensure that user passwords are kept safe and secure. A separate article will discuss protecting your database from SQL injection attacks using prepared statements. This article will discuss making passwords harder to crack. Many sites, some which should have known better, have had their user databases compromised. This is bad enough, but if the miscreants get their evil hands on user passwords, this may allow them to attack accounts held on other sites. [[for-users]] For Users ~~~~~~~~~ From a user point of view, these are the practices you need to follow: * Have a different password for each online account. Use some sort of secure password safe to help keep track of all of your passwords. * Always change your password if you have learned that the website has been compromised. * Check the password policy and recovery procedure. If the password is limited in length or in which characters you may use, or if passwords can be recovered, then this is a sign that the password is stored insecurely. If so, complain or consider taking your business elsewhere. At least be very very careful. On that last point, a password policy which enforces adding complexity is a good thing. A password policy which forces simplification is a bad thing. And password recovery, as you will see later, should be impossible in a secure system. [[password-storage]] Password Storage ~~~~~~~~~~~~~~~~ To put it simply, passwords should never be stored. That is, in any helpful format. Instead, a hashed copy of the password should be stored. Hashing is similar to encryption in that it produces a scrambled version of the original. Where it differs is that the process is irreversible. You cannot unhash a string. Encryption, on the other hand, is designed to be reversible given the correct key, and is useful for transmitting messages in secret. The point to hashing is that the process is There are many ways of hashing strings, but, in general, you need a method which: * can be implemented in PHP * hasn’t been cracked * takes long enough to slow down a cracker without blowing all of your resources Most modern algorithms available in PHP are OK, though MD5 has apparently been cracked. However, to complicate things, we also have to consider brute force. [[salts]] Salts ~~~~~ Hashing algorithms such as SHA have not been cracked, but they are popular enough that tables exist of potential passwords and their hashes. Such tables are often referred to as rainbow tables. This is one reason why a good password is not short and contains a mixture of upper and lower case, numbers and other special characters: it is less likely to have appeared in such a table. Character substitutions, such as `p@55w0rd` are passé, and have already been included. Passwords are also subject to brute force attacks. That is, given enough power, storage and time, it should be possible to try every combination until something matches. Modern computers have enough speed and power to reduce the time required to just a few seconds. Again, a good password makes things harder, but not impossible. A salt is an artificial string added to your password. This adds to the length and complexity of the password, and makes it less likely to make an appearance in an existing list. It should be a random string, and not reused for other passwords. It doesn’t have to be secret, as, by itself, it reveals no useful information. [[user-registration]] User Registration ~~~~~~~~~~~~~~~~~ Typically a user does the registration, but what follows also applies if the registration is performed by the administrator. First, there is a database table which includes the user name, hashed password, and a few more details, usually including the email address. If you ensure that the email address is unique, you can use the email address as a user name, simplifying the process. It is also a good idea to have a password expiry date. This is not so much as to force password changes, but to allow for temporary passwords. The SQL to create such a table would be something like this: [source,sql] ------------------------------------------------- CREATE TABLE users ( id INT UNSIGNED PRIMARY KEY, email VARCHAR(64) UNIQUE INDEX, passwordhash CHAR(40), -- for use with sha1() passwordexpires DATETIME, -- more fields ); ------------------------------------------------- The `passwordhash` field is a character field fixed to 40 characters, which the output you get from the `sha1()` function. The PHP to add a new user to the table would look as follows: [source,php] ----------------------------------------------------------- $sql='INSERT INTO users(email,password,etc) VALUES(?,?,?)'; $prepared=$pdo->prepare($sql); $data=array($email,sha1($password),$etc); $prepared->;execute($data); ----------------------------------------------------------- This uses a prepared statement which protects against SQL injection. The etc data is, of course, a place holder for the rest of the user data. Using the above technique, you have stored a hashed version of the password which cannot be reversed. It might, however, be susceptible to rainbow or brute force attacks, in which case you might prefer to include a salt. For the table, add a salt field: [source,sql] ------------------------------------------------- CREATE TABLE users ( -- etc passwordhash CHAR(40), -- for use with sha1() passwordsalt CHAR(20), -- or whatever length -- etc ) ------------------------------------------------- For the PHP, concatenate the salt and add the salt and the hashed string to the table. [source,php] --------------------------------------------------------------------- $salt='....................'; // put in some real random text here $sql='INSERT INTO users(email,password,salt,etc) VALUES(?,?,?)'; $prepared=$pdo->prepare($sql); $data=array($email,sha1($salt.$password),$salt,$etc); $prepared->execute($data); --------------------------------------------------------------------- [[authentication]] Authentication ~~~~~~~~~~~~~~ The main purpose of passwords is, of course, authentication. That is, to allow a user to log in using their user name and password. Since the password is not to be stored directly, you will need to compare not the passwords, but the hashed version. Without the complication of salting, the following PHP would do the job: [source,php] --------------------------------------------------------------- $email='…'; // From login $password='…'; // From login $sql='SELECT count(*) FROM users WHERE email=? AND password=?'; $prepared=$pdo->prepare($sql); $data=array($email,sha1($password)); $prepared->execute($data); if($prepared->fetchColumn()) { // successful } else { // no good } --------------------------------------------------------------- Note that the password is hashed before it is compared. In this way the original password need not be stored in order to verify the match. If you’re using a salt, it gets a bit more complicated. You first need to read the individual salt from the table, and then include the salt when hashing the password for comparison. This involves reading the record twice, once for the salt, and again for the comparison: [source,php] ------------------------------------------------------------------- $email='…'; // From login $password='…'; // From login // get salt $sql='SELECT salt FROM users WHERE email=?'; $prepared=$pdo->prepare($sql); $data=array($email); $prepared->execute($data); $salt=$prepared->fetchColumn(); // salt & check password $sql='SELECT count(*) FROM users WHERE email=? AND password=?'; $prepared=$pdo->prepare($sql); $data=array($email,sha1($salt.$password)); $prepared->execute($data); if($prepared->fetchColumn()) { // successful } else { // no good } ------------------------------------------------------------------- It might be possible to simplify the query query to a single SELECT statement by relying on the database’s own `sha1()` function, but that is less flexible. It might look like this: [code,sql] -------------------------------------------------------------------------- SELECT count(*) FROM users WHERE email=? AND password=sha1(concat(salt,?)) -------------------------------------------------------------------------- [[using-php-crypt]] Using PHP `crypt()` ~~~~~~~~~~~~~~~~~~~ Some of the many improvements in newer versions of PHP include password security. The crypt() function has been around for a long function, but as of 5.3 it adds some improvements. The `crypt()` function allows you to choose from a number of alternative hash algorithms. One particular algorithm, known for some reason as the blowfish algorithm, offers a better hashing, as well a variable cost. There are possibly even better algorithms available, but this will do for the purposes. The cost of the blowfish algorithm is an interesting property. It indicates the number of iterations required to complete the hash. Increasing the cost means that it will take longer, possibly much longer, to compute hash, and can, if high enough, take a number of seconds to complete. The idea is that for a legitimate login, the extra time is barely noticeable, for for someone trying to brute force the password, it could take millions of seconds, which could be a matter of months. In PHP, the `crypt()` function requires the password, of course, and possibly a salt, which is the whole point to this exercise. The salt is a string of 29 characters, which is a combination of 7 characters of instruction and 22 characters of (hopefully) random text, from a range of 64 acceptable characters. The instruction characters encode the blowfish variation as well as the cost. To add a password to the database: * Generate a salt * Use `crypt()` to generate the hash * Add the result to the database [source,php] ------------------------------------------------------------------------------------ // Get the user’s password // Create Initial Salt $salt=''; $chars='./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for($i=0;$i<22;$i++) $salt.=$chars[rand(0,63)]; $salt=sprintf('$2a$10$%s',$salt); // preferably '$2y$10$…' if PHP >= 5.3.7 // Generate the Hash $dbpassword=crypt($password,$salt); // Add to the database … ------------------------------------------------------------------------------------ The interesting thing about this method is that if you later pass the hashed password as the salt parameter, you will get get the same result as the as the hashed password. That is, you can test: [code,php] ----------------------------------------- crypt($password,$dbpassword)==$dbpassword ----------------------------------------- This gives us the following test: [source,php] --------------------------------------------------- // Get password from database // Test by re-hashing: if(crypt($password,$dbpassword)==$dbpassword) { // ok } else { // no good } --------------------------------------------------- [[php-5.5-password-functions]] PHP 5.5 Password Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~ PHP 5.5 adds a number of password functions to simplify password hashing and checking. Even if you haven’t yet got 5.5 on your server, it is still helpful to know a little about them, and see how we can use something similar in our code. `$hash = password_hash($password,$algorithm[,$options]);`:: This function takes the password, and hashes it according to the given algorithm code. The algorithm code is an integer, with some pre-defined constants to make them more memorable. To be compatible with the method in this discussion, use `PASSWORD_BCRYPT`. `$result = password_verify($password,$hash);`:: Returns a boolean indicating whether the password matches the hash. Since the hash includes all of the relevant details, no more information is necessary. You can implement your own simple versions of the above functions as follows. This will be compatible to the password hashes above. [source,php] ------------------------------------------------------------------------ // See http://php.net/manual/en/function.password-hash.php#113490 if(!function_exists('password_hash')) { define('PASSWORD_BCRYPT',1); define('PASSWORD_DEFAULT',1); function password_hash($password,$algorithm=PASSWORD_BCRYPT) { $salt = base64_encode(mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)); $salt = str_replace('+', '.', $salt); return crypt($password, '$2y$10$'.$salt.'$'); } } if(!function_exists('password_verify')) { function password_verify($password,$hash) { return crypt($password,$hash)==$hash; } } ------------------------------------------------------------------------ Further Reading ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------