Introduction

Password hashing is the most basic security you will need to do if your application accepts passwords from users. If you do not hash your passwords, the passwords stored in your application's database can be stolen and can be used to compromise user's information as well as your system.

What is hashing?

In simple terms, hashing means converting a plain text into a fixed size string of characters called hash value or simply hash. Hashing is done by using a hash function, also called hashing algorithm. For a particular hashing function, the resultant hash is same size (fixed size) irregardless of the plain text size.

The most important property of the hash function is that it is one-way i.e. you can generate as hash from plain text and not the reverse.  

However, brute force (computing hashes for all possible string combinations) can be used to 'guess' the plain text from which as a hash was obtained from. With sufficient computing power, it should be quick to make a guess!

Hashing algorithm/function illustration
Hashing algorithm 

Examples of hash functions

MD5 and SHA1 are common examples of hashing algorithms, which have been implemented in PHP language. However, with increased computing power, they have been found unsuitable for hashing passwords because brute force can be used to reverse hashes to obtain original plain text.

How can one hash passwords in PHP?

Proper hashing should achieve the following:

  1. Hashing should be one-way i.e. should be very difficult, nearly impossible or computationally and prohibitively expensive to reverse a hash.
  2. Even when the plain texts are similar, the hash values should be different. This has two advantages;- i) Users using a similar password in an application have different hashes and ii) Protects password database from rainbow attacks.

We have earlier explained the first point.

In the second point, how do we ensure that similar plain text produces different hashes? By using a salt or commonly known as cryptographic salt.

How does applying a salt protect the password database from rainbow attacks?

What is a cryptographic salt?

A salt is a random text, which is appended to the plain text password before it is hashed. It makes it difficult for attackers to determine plain text for hashes using pre-calculated hashes corresponding to plain text. A salt also ensures that users, who chose the same password end up having different hashes.

Before the password is hashed, a cryptographic salt is added

How is a rainbow table used to attack password databases?

A rainbow table can contain millions or even billions of records of pairs of pre-computed  plain text and hashes. To understand how rainbow tables are used to attack applications, which do not use cryptographic salts, study the table a, b & c below. Note that the hashes are not generated from any particular hashing algorithm. We have used them for illustration purposes.  

Table a: Rainbow table containing pairs of plain text password and pre-computed hash values. This table belongs to the attackers and can contains billions of passwords by generating possible the from charater combination.

Plain text password Password hash
slim 326997b9a035
bac2019! 9c0f64de05
mimir c5bf015828
pa55w0rd 79545f8627
tak b6b10869d965
qwerty2020 6733aafb7f
eric1978 59bf70a9c
manu 0e709553a25
champion7 de059c0f64
and more... ...

Table b: Passwords database for an application, which does not use cryptographic salt before hashing.

username password
erick b99d2222aee879b1
kbruce 9c0f64de05
jdoe 5e360c7bf22ad31f
erick b99d2222aee879b1
and more... ...

Table c: Passwords database for an application, which adds cryptographic salt to password before hashing.

username salt password
erick sab99d22 b99222ad2ee879b1
kbruce sade05 9c0f64de05
jdoe sad31f d31fc5e360a7bf22
kbruce desa05 de059c0f64
and more...... ....... .......

Observation and discussion.

From Table b, It can be observed that, when users use a similar password, their hashes will be the same (see user with username erick). On the other hand, when a cryptographic salt is used (see Table c), users with a similar password  (see user kbruce) end up having different hashes.

To use the rainbow table, every hash in the password table is looked for in the rainbow table, and if found, it's plain text is read in the left column. For instance, 9c0f64de05, which is in Table b, can be found in Table a. Thus, the password for kbruce in Table b is bac2019!

However, even though 9c0f64de05 in Table c can be found in the rainbow table (Table a), the password for kbruce in Table c can NEVER be bac2019! because there is  salt (desa05) applied before hashing - a salt makes it impossible for an attacker to determine user passwords.

Instead of you scratching your head deciding which hashing algorithm to use and how to generate cryptographic salt, PHP has an easy way to hashing passwords and comparing hashes to plain text passwords - password_hash and password_verify.

How to use password_hash and password_verify.

password_hash() creates a new password hash using a strong one-way hashing algorithm and returns the hash value. password_hash() is compatible with crypt().

It generates the cryptographic salt under the hood.

To compare plain text password with the stored hash, we use password_verify ().

Prerequisites

  • PHP, Apache Web server and MySQL/MariaDB, all bundled in XAMPP.
  • Set up Postman.  
  • Some knowledge on PHP's PDO database interface. If you have never used PDO, read the article here.  
  • A code editor of your choice, I love Visual Studio Code (VS Code).

How to go about it

step 1:

Set up your project in xampp's htdocs folder as shown below.

htdocs
    article3
    	-util.php
        -db.php
        -user.php
        -index.php
Project directory structure 

step 2:

Create a database in MySQL database, say, article3, and run the following SQL query to create table user.

CREATE TABLE user (
	id int PRIMARY KEY AUTO_INCREMENT,
    full_name varchar (32) NOT NULL,
    username varchar (32) NOT NULL,
    password varchar (255),---Notice the password field size
    UNIQUE(username)
)

If you are using PostgreSQL, use the the query below instead

CREATE TABLE users(
	id serial PRIMARY KEY,
    full_name varchar (32) NOT NULL,
    username varchar (32) NOT NULL,
    password varchar (255),---Notice the password field size
    UNIQUE(username)
);

step 3:

Insert this code in util.php file. The static variables will be accessed using the class Util and not an object

<?php
    class Util{

        //About DB
        //your databae name here
        static $DB_NAME = "article3";
        //your database user here
        static $DB_USER = "root";
        //password for database user here
        static $DB_USER_PASS = "";
        //machine that is holding your database
        static $SERVER_NAME = "localhost";
    }
?>

step 4:

Add the following code in the db.php. We are using PDO to connect to the database.

<?php
    include_once './util.php';	
    class DBConnector {
        var $pdo;
        function __construct(){
                $dsn= "mysql:host=". Util::$SERVER_NAME . ";dbname=" . Util::$DB_NAME ."";
                $options = [ 
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_EMULATE_PREPARES => false,
                        //Notice here that we read data from tables and is presented as an associative array
                        //indexed by names of column
                        PDO::ATTR_DEFAULT_FETCH_MODE =>PDO::FETCH_ASSOC
                        ];
                try{
                        $this->pdo = new PDO($dsn, Util::$DB_USER, Util::$DB_USER_PASS, $options);				
                }catch (PDOException $e){
                        echo $e->getMessage();
                }			
        }
        public function connectToDB(){
                return $this->pdo;
        } 			
        public function closeDB(){
                $this->pdo = null;
        }
    }
?>

If you are using PostgreSQL database, change the following line:

$dsn= "mysql:host=". Util::$SERVER_NAME . ";dbname=" . Util::$DB_NAME ."";

to:

$dsn= "pgsql:host=". Util::$SERVER_NAME . ";dbname=" . Util::$DB_NAME ."";

Notice that we have only changed the database dialect from mysql to pgsql but you will have to enable PDO_PGSQL driver for PostgreSQL database in php.ini file (see here).

step 5:

Add the following code in the user.php file. In this class, we have a class User with two main methods register and login and three properties, username, password and fullName. Notice how we have used password_hash() in register function and password_verify() in login function.  

<?php
    class User {
        //properties 
        protected $username;
        protected $password;
        protected $fullName;
        //class constructor 
        function __construct($user, $pass){
            $this->username =$user;
            $this->password = $pass;
        }
        //full name setter 
        public function setFullName ($name){
        	$this->fullName = $name;
        }
        //full name getter
        public function getFullName (){
        	return $this->fullName;
        }

        /**
        * Create a new user
        * @param Object PDO Database connection handle
        * @return String success or failure message
        */
        public function register ($pdo){
            $passwordHash = password_hash($this->password,PASSWORD_DEFAULT);
            try {
                $stmt = $pdo->prepare ('INSERT INTO user (full_name,username,password) VALUES(?,?,?)');
                $stmt->execute([$this->getFullName(),$this->username,$passwordHash]);
                return "Registration was successiful";
            } catch (PDOException $e) {
            	return $e->getMessage();
            }            
        }
        /**
        * Check if a user entered a correct username and password
        * @param Object PDO Database connection handle
        * @return String success or failure message
        */
        public function login ($pdo){
            try {
                $stmt = $pdo->prepare("SELECT password FROM user WHERE username=?");
                $stmt->execute([$this->username]);
                $row = $stmt->fetch();
                if($row == null){
                	return "This account does not exist";
                }
                if (password_verify($this->password,$row['password'])){
                	return "Correct blah blah....";
                }
                return "Your username or password is not correct";
            } catch (PDOException $e) {
            	return $e->getMessage();
            }
        }
    }
?>

step 6:

Add the following code in the index.php. This is the entry point of HTTP requests to our app.

<?php
    include_once 'user.php';
    include_once 'db.php';

    $con = new DBConnector();
    $pdo = $con->connectToDB();

    $event = $_POST['event'];
    if($event == "register"){
        //register
        $fullName = $_POST['fullName'];
        $username = $_POST['username'];
        $password = $_POST['password'];
        $user = new User($username, $password);
        $user->setFullName($fullName);
        echo $user->register($pdo);
    }else {
        //login
        $username = $_POST['username'];
        $password = $_POST['password'];
        $user = new User($username, $password);
        echo $user->login($pdo);
    } 
?>

step 7:

Test your app using postman.

  1. User registration.
User registration

2. User login

User login

3. User login with wrong password

User login with wrong password

Link to PHP's password hashing functions documentation

Conclusion

I hope you enjoyed reading and doing!

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.