Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I store my users' passwords safely?

People also ask

Where should I keep all of my passwords?

Try using a desktop application like KeePassXC. It stores encrypted versions of all your passwords into an encrypted digital vault that keeps you secure with a master password, a key file, or both.


The easiest way to get your password storage scheme secure is by using a standard library.

Because security tends to be a lot more complicated and with more invisible screw up possibilities than most programmers could tackle alone, using a standard library is almost always easiest and most secure (if not the only) available option.


The new PHP password API (5.5.0+)

If you are using PHP version 5.5.0 or newer, you can use the new simplified password hashing API

Example of code using PHP's password API:

<?php
// $hash is what you would store in your database
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT, ['cost' => 12]);

// $hash would be the $hash (above) stored in your database for this user
$checked = password_verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

(In case you are still using legacy 5.3.7 or newer you can install ircmaxell/password_compat to have access to the build-in functions)


Improving upon salted hashes: add pepper

If you want extra security, the security folks now (2017) recommend adding a 'pepper' to the (automatically) salted password hashes.

There is a simple, drop in class that securely implements this pattern, I recommend: Netsilik/PepperedPasswords (github).
It comes with a MIT License, so you can use it however you want, even in proprietary projects.

Example of code using Netsilik/PepperedPasswords:

<?php
use Netsilik/Lib/PepperedPasswords;

// Some long, random, binary string, encoded as hexadecimal; stored in your configuration (NOT in your Database, as that would defeat the entire purpose of the pepper).
$config['pepper'] = hex2bin('012345679ABCDEF012345679ABCDEF012345679ABCDEF012345679ABCDEF');

$hasher = new PepperedPasswords($config['pepper']);

// $hash is what you would store in your database
$hash = $hasher->hash($_POST['password']);

// $hash would be the $hash (above) stored in your database for this user
$checked = $hasher->verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}


The OLD standard library

Please note: you should not be needing this anymore! This is only here for historical purposes.

Take a look at: Portable PHP password hashing framework: phpass and make sure you use the CRYPT_BLOWFISH algorithm if at all possible.

Example of code using phpass (v0.2):

<?php
require('PasswordHash.php');

$pwdHasher = new PasswordHash(8, FALSE);

// $hash is what you would store in your database
$hash = $pwdHasher->HashPassword( $password );

// $hash would be the $hash (above) stored in your database for this user
$checked = $pwdHasher->CheckPassword($password, $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

PHPass has been implemented in some quite well known projects:

  • phpBB3
  • WordPress 2.5+ as well as bbPress
  • the Drupal 7 release, (module available for Drupal 5 & 6)
  • others

The good thing is that you do not need to worry about the details, those details have been programmed by people with experience and reviewed by many folks on the internet.

For more information on password storage schemes, read Jeff`s blog post: You're Probably Storing Passwords Incorrectly

Whatever you do if you go for the 'I'll do it myself, thank you' approach, do not use MD5 or SHA1 anymore. They are nice hashing algorithm, but considered broken for security purposes.

Currently, using crypt, with CRYPT_BLOWFISH is the best practice.
CRYPT_BLOWFISH in PHP is an implementation of the Bcrypt hash. Bcrypt is based on the Blowfish block cipher, making use of it's expensive key setup to slow the algorithm down.


Your users will be much safer if you used parameterized queries instead of concatenating SQL statements. And the salt should be unique for each user and should be stored along with the password hash.


A better way would be for each user to have a unique salt.

The benefit of having a salt is that it makes it harder for an attacker to pre-generate the MD5 signature of every dictionary word. But if an attacker learns that you have a fixed salt, they could then pre-generate the MD5 signature of every dictionary word prefixed by your fixed salt.

A better way is each time a user changes their password, your system generate a random salt and store that salt along with the user record. It makes it a bit more expensive to check the password (since you need to look up the salt before you can generate the MD5 signature) but it makes it much more difficult for an attacker to pre-generate MD5's.


With PHP 5.5 (what I describe is available to even earlier versions, see below) around the corner I'd like to suggest to use its new, built-in solution: password_hash() and password_verify(). It provides several options in order to achieve the level of password security you need (for example by specifying a "cost" parameter through the $options array)

<?php
var_dump(password_hash("my-secret-password", PASSWORD_DEFAULT));

$options = array(
    'cost' => 7, // this is the number of rounds for bcrypt
    // 'salt' => 'TphfsM82o1uEKlfP9vf1f', // you could specify a salt but it is not recommended
);
var_dump(password_hash("my-secret-password", PASSWORD_BCRYPT, $options));
?>

will return

string(60) "$2y$10$w2LxXdIcqJpD6idFTNn.eeZbKesdu5y41ksL22iI8C4/6EweI7OK."
string(60) "$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d."

As you might see, the string contains the salt as well as the cost that was specified in the options. It also contains the algorithm used.

Therefore, when checking the password (for example when the user logs in), when using the complimentary password_verify() function it will extract the necessary crypto parameters from the password hash itself.

When not specifying a salt, the generated password hash will be different upon every call of password_hash() because the salt is generated randomly. Therefore comparing a previous hash with a newly generated one will fail, even for a correct password.

Verifying works like this:

var_dump(password_verify("my-secret-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));
var_dump(password_verify("wrong-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));

var_dump(password_verify("my-secret-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));
var_dump(password_verify("wrong-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));

I hope that providing these built-in functions will soon provide better password security in case of data theft, as it reduces the amount of thought the programmer has to put into a proper implementation.

There is a small library (one PHP file) that will give you PHP 5.5's password_hash in PHP 5.3.7+: https://github.com/ircmaxell/password_compat