PHP 7.2 and Argon2 Password Hashing

|8 min read|
Teklog
Teklog


Introduction

PHP 7.2 version appeared for the first time on 30th of November 2017, Time goes fast and more than a half year later, on 21st of June 2018, PHP announced 7.2.7 patch release. Among all the new features introduced in PHP 7.2, there is one, namely Argon2 Password Hashing, which will be the topic of our today's post. Let's explore this great addition together and verify whether Argon2 is a good contender to Bcrypt or not.

As always, all the other features of PHP 7.2 can be found under Request For Changes wiki page.

Assumptions

Try it yourself

Clone blogging-phplatest repository

git clone https://github.com/tekmi/blogging-phplatest
cd blogging-phplatest

Run docker container, based on the php 7.2.7-cli-stretch image.

docker run -it --rm \
-v "$PWD":/usr/src/php-7.2 \
-w /usr/src/php-7.2 \
php:7.2.7-cli-stretch \
php php-7.2/argon2_password_hash/index.php

Explanation

Argon2 comes in two flavors, Argon2d and Argon2i. The former one is recommended primarily for cryptocurrencies and backend servers, while the latter for password hashing and password-based derivation. Due to this difference, RFC implementing Argon2 in PHP doesn't provide Argon2d flavor - therefore no unnecessary PASSWORD_ARGON2 constant.

You can verify it by exploring Argon2 related named constants that come with PHP 7.2 version.

  • PASSWORD_ARGON2I which shall be used to create new password hashes using Argon2 algorithm and its value is integer 2. If you compare it to PASSWORD_DEFAULT(which is integer 1), you may infer it's not the default hashing algorithm. Like it or not, Bcrypt is still the default one. To prove it, check out PASSWORD_BCRYPT named constant, which is integer 1. There were some talks to make Argon2 default algorithm in PHP 7.4, but apparently this won't happen anymore.
  • Three named constants that define some sane defaults for Argon2, namely PASSWORD_ARGON2_DEFAULT_MEMORY_COSTPASSWORD_ARGON2_DEFAULT_TIME_COST and PASSWORD_ARGON2_DEFAULT_THREADS. Reading the RFC, looks like the sane defaults have been revised and now are 1024 bytes, 2 and 2 respectively.

If somehow you cannot use those variables, make sure you had compiled your PHP --with-password-argon2 , since this ensures libargon2 have been included. Docker image being used here has it compiled - feel free to double check it by running:

docker run -it --rm \
-v "$PWD":/usr/src/php-7.2 \
-w /usr/src/php-7.2 \
php:7.2.7-cli-stretch \
-r "phpinfo();" | grep argon2

What interested me was why Argon2i Algorithm gained so much traction and if Bcrypt is still the valid option.

Reading OWASP cheatsheets about password hashing, looks like there are three other viable contenders, namely PBKDF2, scrypt and bcrypt. While OWASP prefers Argon2, looks like Paragonie is fine with default Bcrypt. Any of the aforementioned algorithms are good and still considered secure, but when it comes to my preference:

  • I like the fact that Argon2 won the Password Hashing Competition  in July 2015, beating other contenders, including Catena, Lyra2, Makwa and yescrypt.
  • Bcrypt was invented in 1999 and PHP made it default password hashing algorithm since version 5.5.0 (around June 2013), so time to switch to something newer.
  • In general, I would not rely on PASSWORD_DEFAULT constant in PHP and always explicitly state which hashing algorithm to use - less problems with PHP updates.

Having said all of this, let's see how to use Argon2 new algorithm to our advantage by exploring the password_hash function.

In the basic form, Argon2 password hashing works with following defaults (memory cost: 1024, time cost: 2, threads: 2) and can be invoked as shown below.

$hashed = password_hash("tekmi", PASSWORD_ARGON2I);

If you don't like the defaults, Argon2 accepts additional options, that can be passed as third parameter to password_hash function. Scan PHP Predefined Password Hashing Constant page in order to learn more. In the example below you can see all of them adjusted at once.

$hashed = password_hash("tekmi", PASSWORD_ARGON2I, [
 'memory_cost' => 2 ** 12,
 'time_cost' => 10,
 'threads' => 20
]);

Be warned that tweaking up those values too much, can drastically increase the execution time of your script.


To give you an example, with params (memory cost: 1048576, time cost: 1000, threads: 50), this call took 440 seconds before it was finished! Moreover Memory usage was 1.014Gib, CPU was at 391.38% and the number of PIDS oscillated around 50.


If you specify too crazy values, you can additionally come across one of the following errors.

- Warning: password_hash(): Memory cost is outside of allowed memory range in /usr/src/php-7.2/php-7.2/argon2_password_hash/index.php on line 20
- Warning: password_hash(): Time cost is outside of allowed time range in /usr/src/php-7.2/php-7.2/argon2_password_hash/index.php on line 20
- Warning: password_hash(): Invalid number of threads in /usr/src/php-7.2/php-7.2/argon2_password_hash/index.php on line 20
- Warning: password_hash(): Memory cost is too small in /usr/src/php-7.2/php-7.2/argon2_password_hash/index.php on line 20

You may tinker with this by uncommenting a few lines in index.php script and re-run container with memory limited to 1 GiB, as shown below.

docker run -it --rm \
-v "$PWD":/usr/src/php-7.2 \
-w /usr/src/php-7.2 \
--memory='1g' --cpus='.5' \
php:7.2.7-cli-stretch \
php php-7.2/argon2_password_hash/index.php

I couldn't find much information about those limits, so by trial and error, I figured out some of them, e.g: threads limit 30000000, time limit 900000000000, and surprisingly memory limit 4294967296, even if my container was limited to only 1 GiB memory.

By now we successfully hashed our password 'tekmi', which shall resemble something like $argon2i$v=19$m=1024,t=2,p=2$YU9GN280WU1icm0zVC9sRg$oCiQ7N4liAGYrkFxW9gTDY/gBKXIHizVEKcZcz72tqw or $argon2i$v=19$m=1024,t=2,p=2$VTBsd0dLeVQydm9PdWlmQQ$t8AFo0XU1zGR0hLgzJFxUQkZ3YQL49pqwbAcdhRYn84. As you see, the same password with the same Argon2 defaults, will always result in different string.

Additionally, if you study PHP Password Hashing functions page, you can see three other functions. Let's start with password_get_info, which will give you details of the hashed password, as shown below.

$hashed = password_hash("tekmi", PASSWORD_ARGON2I);
$info = password_get_info($hashed);
var_dump($info);

// result
array(3) {
  ["algo"]=>
  int(2)
  ["algoName"]=>
  string(7) "argon2i"
  ["options"]=>
  array(3) {
    ["memory_cost"]=>
    int(1024)
    ["time_cost"]=>
    int(2)
    ["threads"]=>
    int(2)
  }
}

If you call it with hash that doesn't make sense, you get unknown algorithm name and algorithm value is integer 0, as shown below.

$info = password_get_info('SomeRandomString'); 
var_dump($info);

// result
array(3) {
 ["algo"]=>
 int(0)
 ["algoName"]=>
 string(7) "unknown"
 ["options"]=>
 array(0) {
 }
}

After this part is clear, let's explore password_needs_rehash function, which can detect if given hash shall be rehashed. This can happen if you hashed your password with default Argon2 options, saved this hash in your database and later on decided to adjust your code to introduce different Argon2 options (e.g:  time_cost, memory_cost or threads). Below snippet presents such situation.

$hashed = password_hash("tekmi", PASSWORD_ARGON2I); 
$needsRehashing1 = password_needs_rehash($hashed, PASSWORD_ARGON2I);
$needsRehashing2 = password_needs_rehash($hashed, PASSWORD_ARGON2I, ['time_cost' => 3]);
var_dump([$needsRehashing1, $needsRehashing2]);

// result
array(2) {
 [0] => bool(false)
 [1] => bool(true)
}

And finally onto the last function, password_verify, which can verify if given hash matches given password. Let's see it in action in the snippet below.

$hashed = password_hash("tekmi", PASSWORD_ARGON2I);
$verified1 = password_verify('tekmi', $hashed);
$verified2 = password_verify('otherPassword', $hashed);
var_dump([$verified1, $verified2]);

// result
array(2) {
 [0] => bool(true)
 [1] => bool(false)
}

How did we do it until now

PHP from version 5.5.0 decided to integrate Bcrypt algorithm, making it the default password hashing algorithm. So this was and still is the viable and secure option, if one doesn't like or cannot take advantage of Argon2.

Summary

It's great to see PHP evolving and incorporating new things into the codebase. It ensures me that the language is vivid and manages to keep up with the latest trends - therefore I wanted to thank all the PHP contributors for their heavy and dedicated work.

If you are about to start a new project or you are still hashing passwords with MD5 or SHA, consider trying out the new brand Argon2 algorithms. If you have old project though, which works perfectly with Bcrypt, don't rush and update all your codebase - the situation you are in is totally fine and shall be fine for years to come - in the end Bcrypt has been battle tested and has established high security standards.

In the upcoming post, I would explore another great addition to PHP 7.2, namely Libsodium, so see you there.