PHP Password Encryption

In your role as a PHP developer, it’s crucial to handle password storage with the utmost security.

Consider the following imperatives:

  • Web-based attacks frequently target user passwords;
  • In the event of theft, passwords should be irretrievable by attackers;
  • Contemporary privacy laws mandate the safeguarding of sensitive information, such as passwords, with non-compliance potentially leading to penalties;
  • Clients anticipate and rely on password protection as a fundamental aspect of PHP security;
  • Proficiency in secure practices enhances your professional standing and fosters client confidence.

In PHP, after implementing secure password hashing techniques, the next critical step is to apply robust email validation methods to maintain data integrity and security.

Is it safe to use MD5 and SHA for hashing?

The concise response: No. Historically, MD5 or SHA1 hashing was commonplace for storing passwords, like so:

/* User's password. */
$password = 'my secret password';
/* MD5 hash to be saved in the database. */
$hash = md5($password);

However, this technique is not safe enough.

For two reasons:

Yet, such methods are no longer robust due to:

  • The inadequacy of MD5 and SHA algorithms against current computational capabilities;
  • The susceptibility of simple, unsalted hashes to “rainbow tables” and brute-force attacks.

A stolen MD5 or SHA hash can lead to rapid password recovery. In essence, these hashes are barely more secure than storing passwords in plain text. The recommended alternative is the secure hashing function: password_hash().

Understanding password_hash()

The password_hash() function generates a secure hash, utilized as follows:

/* User's password. */
$password = 'my secret password';
/* Secure password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);

The security of the password_hash() result lies in:

  • Employing robust hashing algorithms;
  • Incorporating a unique salt to each hash, thwarting rainbow table and brute-force methods.

Upon obtaining a secure hash, you’re set to store it in your database. We’ll explore how to accomplish this with subsequent examples.

Implementing password_hash() in PHP

You’ll begin by establishing a ‘users’ database table.

Consider a basic “accounts” table from an authentication guide:

Columns include:

  • account_id: Auto-incremented primary key;
  • account_name: User’s chosen username;
  • account_passwd: Stores password hash;
  • SQL schema definition:

Use the following SQL statement to construct the table, which is compatible with PhpMyAdmin for setting up in your development environment:

CREATE TABLE `accounts` (
  `account_id` int(10) UNSIGNED NOT NULL,
  `account_name` varchar(255) NOT NULL,
  `account_passwd` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `accounts`
  ADD PRIMARY KEY (`account_id`);
ALTER TABLE `accounts`
  MODIFY `account_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;

Note: Ensure the password field is defined as a varchar. Varchar refers to a text field that allows for variable length. 

This is necessary because the length of the hash generated by password_hash() may vary, which will be explained in further detail subsequently. Next, establish a connection to your database from the PHP script. For those unfamiliar, below is a straightforward PDO connection script ready for immediate use.

Modify the connection details to align with your specific settings:

/* Host name of the MySQL server. */
$host = 'localhost';
/* MySQL account username. */
$user = 'myUser';
/* MySQL account password. */
$passwd = 'myPasswd';
/* The default schema you want to use. */
$schema = 'mySchema';
/* The PDO object. */
$pdo = NULL;
/* Connection string, or "data source name". */
$dsn = 'mysql:host=' . $host . ';dbname=' . $schema;
/* Connection inside a try/catch block. */
try
{  
   /* PDO object creation. */
   $pdo = new PDO($dsn, $user,  $passwd);
   
   /* Enable exceptions on errors. */
   $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e)
{
   /* If there is an error, an exception is thrown. */
   echo 'Database connection failed.';
   die();
}

You are now prepared to include a new user in the table. Here’s a complete example, with “pdo.php” being the script containing the earlier database connection snippet:

/* Include the database connection script. */
include 'pdo.php';
/* Username. */
$username = 'John';
/* Password. */
$password = 'my secret password';
/* Secure password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);
/* Insert query template. */
$query = 'INSERT INTO accounts (account_name, account_passwd) VALUES (:name, :passwd)';
/* Values array for PDO. */
$values = [':name' => $username, ':passwd' => $hash];
/* Execute the query. */
try
{
  $res = $pdo->prepare($query);
  $res->execute($values);
}
catch (PDOException $e)
{
  /* Query error. */
  echo 'Query error.';
  die();
}

Important: In this example, the validation steps have been skipped, including:

  • Checking the username and password length;
  • Ensuring there are no invalid characters;
  • Verifying if the username already exists;
  • And other relevant validation checks.

Validation is not within the scope of this tutorial, but it is essential to validate your input variables.

Modifying an Existing User’s Password in a Database System

This section outlines the procedure for updating the password of an existing user in a database.

Initially, acquire the new password input and generate its corresponding hash utilizing the password_hash() function:

/* New password. */
$password = $_POST['password'];
/* Remember to validate the password. */
/* Create the new password hash. */
$hash = password_hash($password, PASSWORD_DEFAULT);

Following this, execute an update operation on the database table that corresponds to user accounts. Specifically, target the row that matches the account ID of the user in question, replacing the existing password hash with the newly generated one.

It is presupposed that the $accountId variable is already set to the relevant user’s account ID for identification purposes.

/* Include the database connection script. */
include 'pdo.php';
/* ID of the account to edit. */
$accountId = 1;
/* Update query template. */
$query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
/* Values array for PDO. */
$values = [':passwd' => $hash, ':id' => $accountId];
/* Execute the query. */
try
{
  $res = $pdo->prepare($query);
  $res->execute($values);
}
catch (PDOException $e)
{
  /* Query error. */
  echo 'Query error.';
  die();
}

Implementing Password Validation Using password_verify()

To authenticate the password submitted by a user, the password_verify() function is employed. This function requires two parameters:

  • The user-submitted password to be authenticated;
  • The hashed version of the original password, generated previously using password_hash().

When the input password matches the hashed password, password_verify() yields a true result, indicating successful authentication.

Example usage is as follows:

/* Include the database connection script. */
include 'pdo.php';
/* Login status: false = not authenticated, true = authenticated. */
$login = FALSE;
/* Username from the login form. */
$username = $_POST['username'];
/* Password from the login form. */
$password = $_POST['password'];
/* Remember to validate $username and $password. */
/* Look for the username in the database. */
$query = 'SELECT * FROM accounts WHERE (account_name = :name)';
/* Values array for PDO. */
$values = [':name' => $username];
/* Execute the query */
try
{
  $res = $pdo->prepare($query);
  $res->execute($values);
}
catch (PDOException $e)
{
  /* Query error. */
  echo 'Query error.';
  die();
}
$row = $res->fetch(PDO::FETCH_ASSOC);
/* If there is a result, check if the password matches using password_verify(). */
if (is_array($row))
{
  if (password_verify($password, $row['account_passwd']))
  {
    /* The password is correct. */
    $login = TRUE;
  }
}

Direct hash comparisons for password validation are ineffective due to password_hash() creating salted hashes. Salting involves appending a random string (“salt”) to each hash, safeguarding against advanced brute-force techniques like rainbow table and dictionary attacks. Consequently, hashes of the same password differ.

The following PHP code demonstrates this concept:

$password = 'my password';
echo password_hash($password, PASSWORD_DEFAULT);
echo '<br>';
echo password_hash($password, PASSWORD_DEFAULT);

This results in two distinct hashes for the identical password. Note that password_verify() is compatible exclusively with hashes from password_hash(), not MD5 or SHA hashes.

Optimizing password_hash() Security

Graphic of a laptop with a lock symbol denoting SSL encryption

Enhancing the security of password_hash() involves:

  • Incrementing Bcrypt Cost: This raises the computational intensity of the hashing process, thereby fortifying the hash against brute-force attacks;
  • Algorithmic Updates: Periodically upgrading to more advanced hashing algorithms aligns password security with current cryptographic standards.

Optimizing Bcrypt Cost Parameter in password_hash()

Bcrypt, the default algorithm in password_hash(), utilizes a “cost” parameter, defaulting to 10. Increasing this value heightens computational complexity, thus enhancing hash security but also increasing processing time. Balancing security with server performance is key. Custom cost settings in password_hash() are adjustable as follows:

/* Password. */
$password = 'my secret password';
/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];
/* Create the hash. */
$hash = password_hash($password, PASSWORD_DEFAULT, $options);

What should be the ideal cost value to use A reasonable compromise would be to set the cost value in a way that allows your server to generate the hash in approximately 100 milliseconds.

Here’s a straightforward test to determine this value:

/* 100 ms. */
$time = 0.1;
/* Initial cost. */
$cost = 10;
/* Loop until the time required is more than 100ms. */
do
{
  /* Increase the cost. */
  $cost++;
  
  /* Check how much time we need to create the hash. */
  $start = microtime(true);
  password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
  $end = microtime(true);
}
while (($end - $start) < $time);
echo 'Cost found: ' . $cost;

Once you’ve determined your desired cost value, you can apply it consistently whenever you use the password_hash() function, as demonstrated in the earlier example.

Keeping Hashes Updated with password_needs_rehash()

To comprehend this process, let’s examine how password_hash() operates.

password_hash() accepts three parameters:

  • The password to be hashed;
  • The chosen hashing algorithm;
  • An array of configuration options for the algorithm.

PHP provides various hashing algorithms, but the default option is commonly preferred. You can specify the default algorithm using the PASSWORD_DEFAULT constant, as demonstrated in previous examples.

 As of June 2020, Bcrypt serves as the default algorithm. However, PHP reserves the flexibility to modify the default algorithm in the future, should a more secure option emerge. In such a scenario, the PASSWORD_DEFAULT constant will be updated to point to the new algorithm. Consequently, all newly generated hashes will utilize the new algorithm.

Now, what if you need to update your old hashes, originally created with the previous algorithm, to align with the new one? This is where the password_needs_rehash() function becomes essential. This function serves the purpose of checking whether a hash has been generated using a specific algorithm and set of parameters.

For instance:

/* Password. */
$password = 'my secret password';
/* Set the "cost" parameter to 10. */
$options = ['cost' => 10];
/* Create the hash. */
$hash = password_hash($password, PASSWORD_DEFAULT, $options);
/* Now, change the cost. */
$options['cost'] = 12;
/* Check if the hash needs to be created again. */
if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options))
{
  echo 'You need to rehash the password.';
}

If the current default hashing algorithm doesn’t match the one used for hash generation, password_needs_rehash() will return true. This function also verifies if there are differences in the options parameter, which is useful for updating hashes when parameters like the Bcrypt cost change. Here’s an example demonstrating how to automatically assess and update a password hash when a remote user logs in:

/* Include the database connection script. */
include 'pdo.php';
/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];
/* Login status: false = not authenticated, true = authenticated. */
$login = FALSE;
/* Username from the login form. */
$username = $_POST['username'];
/* Password from the login form. */
$password = $_POST['password'];
/* Remember to validate $username and $password. */
/* Look for the username in the database. */
$query = 'SELECT * FROM accounts WHERE (account_name = :name)';
/* Values array for PDO. */
$values = [':name' => $username];
/* Execute the query */
try
{
  $res = $pdo->prepare($query);
  $res->execute($values);
}
catch (PDOException $e)
{
  /* Query error. */
  echo 'Query error.';
  die();
}
$row = $res->fetch(PDO::FETCH_ASSOC);
/* If there is a result, check if the password matches using password_verify(). */
if (is_array($row))
{
  if (password_verify($password, $row['account_passwd']))
  {
    /* The password is correct. */
    $login = TRUE;
	
	/* Check if the hash needs to be created again. */
    if (password_needs_rehash($row['account_passwd'], PASSWORD_DEFAULT, $options))
    {
      $hash = password_hash($password, PASSWORD_DEFAULT, $options);
      
      /* Update the password hash on the database. */
      $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
      $values = [':passwd' => $hash, ':id' => $row['account_id']];
      
      try
      {
        $res = $pdo->prepare($query);
        $res->execute($values);
      }
      catch (PDOException $e)
      {
        /* Query error. */
        echo 'Query error.';
        die();
      }
    }
  }
}

Automated Conversion of MD5-based Hashes to password_hash()

Illustration of a secure user authentication system with privacy icons

In this example, a script will be discussed for the automated conversion of outdated MD5-based hashes to more secure password_hash() generated hashes.

The process involves the following steps:

  • Verification of the user’s password using the password_verify() function during login;
  • In the event of a failed login, a check is made to determine if the stored hash in the database corresponds to an MD5 hash of the password;
  • If the stored hash is indeed an MD5 hash, it is automatically updated with a new hash generated by the password_hash() function.

Below is the script for this automated conversion process:

/* Include the database connection script. */
include 'pdo.php';
/* Set the "cost" parameter to 12. */
$options = ['cost' => 12];
/* Login status: false = not authenticated, true = authenticated. */
$login = FALSE;
/* Username from the login form. */
$username = $_POST['username'];
/* Password from the login form. */
$password = $_POST['password'];
/* Remember to validate $username and $password. */
/* Look for the username in the database. */
$query = 'SELECT * FROM accounts WHERE (account_name = :name)';
/* Values array for PDO. */
$values = [':name' => $username];
/* Execute the query */
try
{
  $res = $pdo->prepare($query);
  $res->execute($values);
}
catch (PDOException $e)
{
  /* Query error. */
  echo 'Query error.';
  die();
}
$row = $res->fetch(PDO::FETCH_ASSOC);
/* If there is a result, check if the password matches using password_verify(). */
if (is_array($row))
{
  if (password_verify($password, $row['account_passwd']))
  {
    /* The password is correct. */
    $login = TRUE;
    
    /* You can also use password_needs_rehash() here, as shown in the previous example. */
  }
  else
  {
    /* Check if the database contains the MD5 hash of the password. */
    if (md5($password) == $row['account_passwd'])
    {
      /* The password is correct. */
      $login = TRUE;
      
      /* Update the database with a new, secure hash. */
      $hash = password_hash($password, PASSWORD_DEFAULT, $options);
      $query = 'UPDATE accounts SET account_passwd = :passwd WHERE account_id = :id';
      $values = [':passwd' => $hash, ':id' => $row['account_id']];
      
      try
      {
        $res = $pdo->prepare($query);
        $res->execute($values);
      }
      catch (PDOException $e)
      {
        /* Query error. */
        echo 'Query error.';
        die();
      }
    }
  }
}

Conclusion

In this tutorial, you have acquired practical knowledge on the application of password_hash() and password_verify() functions to create secure password hashes. Additionally, you’ve gained an understanding of the inherent security risks associated with MD5.

Furthermore, you’ve learned how to bolster the security of your password hashes by appropriately configuring the Bcrypt cost parameter and implementing an automated password rehashing mechanism when necessary.

Leave a Reply

Your email address will not be published. Required fields are marked *