Password hashes and salts
One of the biggest mistakes novice programmers do is storing plain text passwords in a database. That is a great security risk easily compared (if not greater) to SQL injection or session hijacking. This security risk is common to all server side languages because it is mainly related to how you store the data in databases. Solution is very easy – use hashes.
What is a hash? A hash is the result of a one-way mathematical process which creates a unique string. The string is next to impossible to decode in order to recover the original text. This makes it an ideal way of storing sensible data in databases – especially passwords.
There are many hashing algorithms, most widely used are MD5 (128-bit) and sha1 (160-bit). Researchers found out that MD5 and also SHA1 are less secure than previously thought so if security is your top priority, you should look for some more advanced algorithms (256-bit or 512-bit). But for usual applications SHA1 is still very secure.
Example: Simple user authorization using sha1 hash
Let’s create a simple table for users:
CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL, PRIMARY KEY (id) ) ENGINE = INNODB;
When user registers, you ought to save only a hash of the password in the database:
-
/* Add user to the database */
-
-
$username = $_POST['username'];
-
$password = $_POST['password'];
-
-
$user = 'root';
-
$pass = '';
-
$conn = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
-
-
$stmt = $conn->prepare('INSERT INTO users (username, password_hash) VALUES (?, sha1(?))');
-
$stmt->execute(array($username, $password));
When a user attempts to log in, you just need to compare a hash of the user entered password to the hash stored in the database:
-
/* Check user credentials */
-
-
$username = $_POST['username'];
-
$password = $_POST['password'];
-
-
$user = 'root';
-
$pass = '';
-
$conn = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
-
-
$stmt = $conn->prepare('SELECT * FROM users WHERE username = ?');
-
$stmt->execute(array($username));
-
$user = $stmt->fetchObject();
-
-
if ($user != false && sha1($password) == $user->password_hash) {
-
// user has logged in successfully
-
}
Salt improvement
Even though storing only hashed passwords in the database is already very safe, there is a weakness which can be exploited. If an attacker gains access to the database, he will be able to say that users with the same password hash must also have the same password. This information would be still useless but the attacker can use the ‘ brute forcing’ (generate numerous potential password hashes – either randomly generated or from a dictionary – and compare them with hashes in the database) to decode user passwords.
Strong passwords (containing letters, numbers and other symbols) are secure even against the ‘brute forcing’ attacks but users often like short passwords containing only letters or only letters and numbers. This weakness can be overcome by using a ’salt’. Salt is a randomly generated string which is prepended to a plain text password before using a hasing algorithm. You need a function for creating a random string:
-
function randomString($length = 32, $chars = '1234567890abcdef') {
-
// length of character list
-
$charsLength = (strlen($chars) - 1);
-
-
// start our string
-
$string = $chars{rand(0, $charsLength)};
-
-
// generate random string
-
for ($i = 1; $i < $length; $i = strlen($string)) {
-
// grab a random character from our list
-
$r = $chars{rand(0, $charsLength)};
-
// make sure the same two characters don't appear next to each other
-
if ($r != $string{$i - 1}) {
-
$string .= $r;
-
} else {
-
$i–;
-
}
-
}
-
-
// return the string
-
return $string;
-
}
User registration will now look a little differently:
-
/* Add user to the database */
-
-
$username = $_POST['username'];
-
$password = $_POST['password'];
-
$salt = randomString(40);
-
-
$user = 'root';
-
$pass = '';
-
$conn = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
-
-
$stmt = $conn->prepare('INSERT INTO users (username, password_hash) VALUES (?, CONCAT(?, sha1(CONCAT(?, ?))))');
-
$stmt->execute(array($username, $salt, $salt, $password));
And when a user attempts to log in:
-
/* Check user credentials */
-
-
$username = $_POST['username'];
-
$password = $_POST['password'];
-
-
$user = 'root';
-
$pass = '';
-
$conn = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
-
-
$stmt = $conn->prepare('SELECT * FROM users WHERE username = ?');
-
$stmt->execute(array($username));
-
$user = $stmt->fetchObject();
-
-
$password_hash = substr($user->password_hash, 0, 40) . sha1(substr($user->password_hash, 0, 40) . $password);
-
if ($user != false && $password_hash == $user->password_hash) {
-
// user has logged in successfully
-
}
