More Secure Passwords with PHP
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
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
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
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 make 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
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:
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:
$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:
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.
$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
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:
$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:
$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:
SELECT count(*) FROM users WHERE email=? AND password=sha1(concat(salt,?))
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
// 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:
crypt($password,$dbpassword)==$dbpassword
This gives us the following test:
// 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 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.
// 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
Links