Skip to content

Commit 55277a6

Browse files
charlesportwoodiicmb69
authored andcommitted
RFC: Argon2 Password Hash Enhancements Implementation of Argon2id per RFC https://wiki.php.net/rfc/argon2_password_hash_enhancements
- m4 and Windows configure scripts now forces Argon2 reference library version >= 20161029 - Implementation tested against 20161029 and 20171227 for Argon2id support - Updates Argon2 ext/standard/password/tests to run tests for both Argon2i and Argon2id
1 parent 3f96f01 commit 55277a6

10 files changed

+125
-29
lines changed

UPGRADING

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ readline:
150150
options has been added to readline_info(). These options are only available
151151
if PHP is linked against libreadline (rather than libedit).
152152

153+
Standard:
154+
. The -–with-password-argon2[=dir] configure argument now provides support for
155+
both Argon2i and Argon2id hashes in the password_hash(), password_verify(),
156+
password_get_info(), and password_needs_rehash() functions. Passwords may be
157+
hashed and verified using the PASSWORD_ARGON2ID constant.
158+
Support for both Argon2i and Argon2id in the password_* functions now requires
159+
PHP be linked against libargon2 reference library >= 20161029.
160+
(RFC: https://wiki.php.net/rfc/argon2_password_hash_enhancements).
161+
153162
========================================
154163
3. Changes in SAPI modules
155164
========================================
@@ -311,6 +320,9 @@ PGSQL:
311320
. Requires Postgres 9.6
312321
- PGSQL_DIAG_SEVERITY_NONLOCALIZED
313322

323+
Standard:
324+
. PASSWORD_ARGON2ID
325+
314326
========================================
315327
11. Changes to INI File Handling
316328
========================================

ext/standard/config.m4

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -434,18 +434,11 @@ if test "$PHP_PASSWORD_ARGON2" != "no"; then
434434
PHP_ADD_LIBRARY_WITH_PATH(argon2, $ARGON2_DIR/$PHP_LIBDIR)
435435
PHP_ADD_INCLUDE($ARGON2_DIR/include)
436436

437-
AC_CHECK_LIB(argon2, argon2_hash, [
438-
LIBS="$LIBS -largon2"
439-
AC_DEFINE(HAVE_ARGON2LIB, 1, [ Define to 1 if you have the <argon2.h> header file ])
440-
], [
441-
AC_MSG_ERROR([Problem with libargon2.(a|so). Please verify that Argon2 header and libaries are installed])
442-
])
443-
444437
AC_CHECK_LIB(argon2, argon2id_hash_raw, [
445438
LIBS="$LIBS -largon2"
446-
AC_DEFINE(HAVE_ARGON2ID, 1, [ Define to 1 if Argon2 library has support for Argon2ID])
439+
AC_DEFINE(HAVE_ARGON2LIB, 1, [ Define to 1 if you have the <argon2.h> header file ])
447440
], [
448-
AC_MSG_RESULT([not found])
441+
AC_MSG_ERROR([Problem with libargon2.(a|so). Please verify that Argon2 header and libaries >= 20161029 are installed])
449442
])
450443
fi
451444

ext/standard/config.w32

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ ARG_WITH("password-argon2", "Argon2 support", "no");
66
if (PHP_PASSWORD_ARGON2 != "no") {
77
if (CHECK_LIB("argon2_a.lib;argon2.lib", null, PHP_PASSWORD_ARGON2)
88
&& CHECK_HEADER_ADD_INCLUDE("argon2.h", "CFLAGS")) {
9-
AC_DEFINE('HAVE_ARGON2LIB', 1);
10-
if (CHECK_FUNC_IN_HEADER("argon2.h", "argon2id_hash_raw", PHP_PHP_BUILD + "\\include", "CFLAGS")) {
11-
AC_DEFINE('HAVE_ARGON2ID', 1);
9+
if (!CHECK_FUNC_IN_HEADER("argon2.h", "argon2id_hash_raw", PHP_PHP_BUILD + "\\include", "CFLAGS")) {
10+
ERROR("Please verify that Argon2 header and libaries >= 20161029 are installed");
1211
}
12+
AC_DEFINE('HAVE_ARGON2LIB', 1);
1313
} else {
1414
WARNING("Argon2 not enabled; libaries and headers not found");
1515
}

ext/standard/password.c

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ PHP_MINIT_FUNCTION(password) /* {{{ */
4545
REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT", PHP_PASSWORD_BCRYPT, CONST_CS | CONST_PERSISTENT);
4646
#if HAVE_ARGON2LIB
4747
REGISTER_LONG_CONSTANT("PASSWORD_ARGON2I", PHP_PASSWORD_ARGON2I, CONST_CS | CONST_PERSISTENT);
48+
REGISTER_LONG_CONSTANT("PASSWORD_ARGON2ID", PHP_PASSWORD_ARGON2ID, CONST_CS | CONST_PERSISTENT);
4849
#endif
4950

5051
REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_DEFAULT_COST", PHP_PASSWORD_BCRYPT_COST, CONST_CS | CONST_PERSISTENT);
@@ -66,6 +67,8 @@ static zend_string* php_password_get_algo_name(const php_password_algo algo)
6667
#if HAVE_ARGON2LIB
6768
case PHP_PASSWORD_ARGON2I:
6869
return zend_string_init("argon2i", sizeof("argon2i") - 1, 0);
70+
case PHP_PASSWORD_ARGON2ID:
71+
return zend_string_init("argon2id", sizeof("argon2id") - 1, 0);
6972
#endif
7073
case PHP_PASSWORD_UNKNOWN:
7174
default:
@@ -81,6 +84,10 @@ static php_password_algo php_password_determine_algo(const zend_string *hash)
8184
return PHP_PASSWORD_BCRYPT;
8285
}
8386
#if HAVE_ARGON2LIB
87+
if (len >= sizeof("$argon2id$")-1 && !memcmp(h, "$argon2id$", sizeof("$argon2id$")-1)) {
88+
return PHP_PASSWORD_ARGON2ID;
89+
}
90+
8491
if (len >= sizeof("$argon2i$")-1 && !memcmp(h, "$argon2i$", sizeof("$argon2i$")-1)) {
8592
return PHP_PASSWORD_ARGON2I;
8693
}
@@ -159,6 +166,21 @@ static zend_string* php_password_make_salt(size_t length) /* {{{ */
159166
}
160167
/* }}} */
161168

169+
#if HAVE_ARGON2LIB
170+
static void extract_argon2_parameters(const php_password_algo algo, const zend_string *hash,
171+
zend_long *v, zend_long *memory_cost,
172+
zend_long *time_cost, zend_long *threads) /* {{{ */
173+
{
174+
if (algo == PHP_PASSWORD_ARGON2ID) {
175+
sscanf(ZSTR_VAL(hash), "$%*[argon2id]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, v, memory_cost, time_cost, threads);
176+
} else if (algo == PHP_PASSWORD_ARGON2I) {
177+
sscanf(ZSTR_VAL(hash), "$%*[argon2i]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, v, memory_cost, time_cost, threads);
178+
}
179+
180+
return;
181+
}
182+
#endif
183+
162184
/* {{{ proto array password_get_info(string $hash)
163185
Retrieves information about a given hash */
164186
PHP_FUNCTION(password_get_info)
@@ -186,13 +208,15 @@ PHP_FUNCTION(password_get_info)
186208
break;
187209
#if HAVE_ARGON2LIB
188210
case PHP_PASSWORD_ARGON2I:
211+
case PHP_PASSWORD_ARGON2ID:
189212
{
190213
zend_long v = 0;
191214
zend_long memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST;
192215
zend_long time_cost = PHP_PASSWORD_ARGON2_TIME_COST;
193216
zend_long threads = PHP_PASSWORD_ARGON2_THREADS;
194217

195-
sscanf(ZSTR_VAL(hash), "$%*[argon2i]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, &v, &memory_cost, &time_cost, &threads);
218+
extract_argon2_parameters(algo, hash, &v, &memory_cost, &time_cost, &threads);
219+
196220
add_assoc_long(&options, "memory_cost", memory_cost);
197221
add_assoc_long(&options, "time_cost", time_cost);
198222
add_assoc_long(&options, "threads", threads);
@@ -252,6 +276,7 @@ PHP_FUNCTION(password_needs_rehash)
252276
break;
253277
#if HAVE_ARGON2LIB
254278
case PHP_PASSWORD_ARGON2I:
279+
case PHP_PASSWORD_ARGON2ID:
255280
{
256281
zend_long v = 0;
257282
zend_long new_memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST, memory_cost = 0;
@@ -270,7 +295,7 @@ PHP_FUNCTION(password_needs_rehash)
270295
new_threads = zval_get_long(option_buffer);
271296
}
272297

273-
sscanf(ZSTR_VAL(hash), "$%*[argon2i]$v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, &v, &memory_cost, &time_cost, &threads);
298+
extract_argon2_parameters(algo, hash, &v, &memory_cost, &time_cost, &threads);
274299

275300
if (new_time_cost != time_cost || new_memory_cost != memory_cost || new_threads != threads) {
276301
RETURN_TRUE;
@@ -303,7 +328,16 @@ PHP_FUNCTION(password_verify)
303328
switch(algo) {
304329
#if HAVE_ARGON2LIB
305330
case PHP_PASSWORD_ARGON2I:
306-
RETURN_BOOL(ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_i));
331+
case PHP_PASSWORD_ARGON2ID:
332+
{
333+
argon2_type type;
334+
if (algo == PHP_PASSWORD_ARGON2ID) {
335+
type = Argon2_id;
336+
} else if (algo == PHP_PASSWORD_ARGON2I) {
337+
type = Argon2_i;
338+
}
339+
RETURN_BOOL(ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), type));
340+
}
307341
break;
308342
#endif
309343
case PHP_PASSWORD_BCRYPT:
@@ -470,13 +504,19 @@ PHP_FUNCTION(password_hash)
470504
break;
471505
#if HAVE_ARGON2LIB
472506
case PHP_PASSWORD_ARGON2I:
507+
case PHP_PASSWORD_ARGON2ID:
473508
{
474509
zval *option_buffer;
475510
zend_string *salt, *out, *encoded;
476511
size_t time_cost = PHP_PASSWORD_ARGON2_TIME_COST;
477512
size_t memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST;
478513
size_t threads = PHP_PASSWORD_ARGON2_THREADS;
479-
argon2_type type = Argon2_i;
514+
argon2_type type;
515+
if (algo == PHP_PASSWORD_ARGON2ID) {
516+
type = Argon2_id;
517+
} else if (algo == PHP_PASSWORD_ARGON2I) {
518+
type = Argon2_i;
519+
}
480520
size_t encoded_len;
481521
int status = 0;
482522

@@ -485,7 +525,7 @@ PHP_FUNCTION(password_hash)
485525
}
486526

487527
if (memory_cost > ARGON2_MAX_MEMORY || memory_cost < ARGON2_MIN_MEMORY) {
488-
php_error_docref(NULL, E_WARNING, "Memory cost is outside of allowed memory range", memory_cost);
528+
php_error_docref(NULL, E_WARNING, "Memory cost is outside of allowed memory range");
489529
RETURN_NULL();
490530
}
491531

@@ -494,7 +534,7 @@ PHP_FUNCTION(password_hash)
494534
}
495535

496536
if (time_cost > ARGON2_MAX_TIME || time_cost < ARGON2_MIN_TIME) {
497-
php_error_docref(NULL, E_WARNING, "Time cost is outside of allowed time range", time_cost);
537+
php_error_docref(NULL, E_WARNING, "Time cost is outside of allowed time range");
498538
RETURN_NULL();
499539
}
500540

@@ -503,7 +543,7 @@ PHP_FUNCTION(password_hash)
503543
}
504544

505545
if (threads > ARGON2_MAX_LANES || threads == 0) {
506-
php_error_docref(NULL, E_WARNING, "Invalid number of threads", threads);
546+
php_error_docref(NULL, E_WARNING, "Invalid number of threads");
507547
RETURN_NULL();
508548
}
509549

@@ -517,10 +557,8 @@ PHP_FUNCTION(password_hash)
517557
memory_cost,
518558
threads,
519559
(uint32_t)ZSTR_LEN(salt),
520-
ZSTR_LEN(out)
521-
#if HAVE_ARGON2ID
522-
, type
523-
#endif
560+
ZSTR_LEN(out),
561+
type
524562
);
525563

526564
encoded = zend_string_alloc(encoded_len - 1, 0);

ext/standard/php_password.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ typedef enum {
4343
PHP_PASSWORD_BCRYPT,
4444
#if HAVE_ARGON2LIB
4545
PHP_PASSWORD_ARGON2I,
46+
PHP_PASSWORD_ARGON2ID,
4647
#endif
4748
} php_password_algo;
4849

ext/standard/tests/password/password_get_info_argon2.phpt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
--TEST--
2-
Test normal operation of password_get_info() with Argon2
2+
Test normal operation of password_get_info() with Argon2i and Argon2id
33
--SKIPIF--
44
<?php
55
if (!defined('PASSWORD_ARGON2I')) die('skip password_get_info not built with Argon2');
6+
if (!defined('PASSWORD_ARGON2ID')) die('skip password_get_info not built with Argon2');
67
?>
78
--FILE--
89
<?php
910

1011
var_dump(password_get_info('$argon2i$v=19$m=65536,t=3,p=1$SWhIcG5MT21Pc01PbWdVZw$WagZELICsz7jlqOR2YzoEVTWb2oOX1tYdnhZYXxptbU'));
12+
var_dump(password_get_info('$argon2id$v=19$m=1024,t=2,p=2$Zng1U1RHS0h1aUJjbGhPdA$ajQnG5s01Ws1ad8xv+1qGfXF8mYxxWdyul5rBpomuZQ'));
1113
echo "OK!";
1214
?>
1315
--EXPECT--
@@ -26,4 +28,19 @@ array(3) {
2628
int(1)
2729
}
2830
}
31+
array(3) {
32+
["algo"]=>
33+
int(3)
34+
["algoName"]=>
35+
string(8) "argon2id"
36+
["options"]=>
37+
array(3) {
38+
["memory_cost"]=>
39+
int(1024)
40+
["time_cost"]=>
41+
int(2)
42+
["threads"]=>
43+
int(2)
44+
}
45+
}
2946
OK!
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
--TEST--
2-
Test normal operation of password_hash() with argon2
2+
Test normal operation of password_hash() with Argon2i and Argon2id
33
--SKIPIF--
44
<?php
55
if (!defined('PASSWORD_ARGON2I')) die('skip password_hash not built with Argon2');
6+
if (!defined('PASSWORD_ARGON2ID')) die('skip password_hash not built with Argon2');
67
--FILE--
78
<?php
89

@@ -11,8 +12,12 @@ $password = "the password for testing 12345!";
1112
$hash = password_hash($password, PASSWORD_ARGON2I);
1213
var_dump(password_verify($password, $hash));
1314

15+
$hash = password_hash($password, PASSWORD_ARGON2ID);
16+
var_dump(password_verify($password, $hash));
17+
1418
echo "OK!";
1519
?>
1620
--EXPECT--
1721
bool(true)
22+
bool(true)
1823
OK!
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
--TEST--
2-
Test error operation of password_hash() with argon2
2+
Test error operation of password_hash() with Argon2i and Argon2id
33
--SKIPIF--
44
<?php
55
if (!defined('PASSWORD_ARGON2I')) die('skip password_hash not built with Argon2');
6+
if (!defined('PASSWORD_ARGON2ID')) die('skip password_hash not built with Argon2');
67
?>
78
--FILE--
89
<?php
910
var_dump(password_hash('test', PASSWORD_ARGON2I, ['memory_cost' => 0]));
1011
var_dump(password_hash('test', PASSWORD_ARGON2I, ['time_cost' => 0]));
1112
var_dump(password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]));
13+
var_dump(password_hash('test', PASSWORD_ARGON2ID, ['memory_cost' => 0]));
14+
var_dump(password_hash('test', PASSWORD_ARGON2ID, ['time_cost' => 0]));
15+
var_dump(password_hash('test', PASSWORD_ARGON2ID, ['threads' => 0]));
1216
?>
1317
--EXPECTF--
1418
Warning: password_hash(): Memory cost is outside of allowed memory range in %s on line %d
@@ -17,5 +21,14 @@ NULL
1721
Warning: password_hash(): Time cost is outside of allowed time range in %s on line %d
1822
NULL
1923

24+
Warning: password_hash(): Invalid number of threads in %s on line %d
25+
NULL
26+
27+
Warning: password_hash(): Memory cost is outside of allowed memory range in %s on line %d
28+
NULL
29+
30+
Warning: password_hash(): Time cost is outside of allowed time range in %s on line %d
31+
NULL
32+
2033
Warning: password_hash(): Invalid number of threads in %s on line %d
2134
NULL
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
--TEST--
2-
Test normal operation of password_needs_rehash() with argon2
2+
Test normal operation of password_needs_rehash() with Argon2i and Argon2id
33
--SKIPIF--
44
<?php
55
if (!defined('PASSWORD_ARGON2I')) die('skip password_needs_rehash not built with Argon2');
6+
if (!defined('PASSWORD_ARGON2ID')) die('skip password_hash not built with Argon2');
67
?>
78
--FILE--
89
<?php
@@ -12,11 +13,21 @@ var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I));
1213
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['memory_cost' => 1<<17]));
1314
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['time_cost' => 4]));
1415
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['threads' => 4]));
16+
17+
$hash = password_hash('test', PASSWORD_ARGON2ID);
18+
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID));
19+
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID, ['memory_cost' => 1<<17]));
20+
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID, ['time_cost' => 4]));
21+
var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID, ['threads' => 4]));
1522
echo "OK!";
1623
?>
1724
--EXPECT--
1825
bool(false)
1926
bool(true)
2027
bool(true)
2128
bool(true)
22-
OK!
29+
bool(false)
30+
bool(true)
31+
bool(true)
32+
bool(true)
33+
OK!
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
--TEST--
2-
Test normal operation of password_verify() with argon2
2+
Test normal operation of password_verify() with Argon2i and Argon2id
33
--SKIPIF--
44
<?php
55
if (!defined('PASSWORD_ARGON2I')) die('skip password_verify not built with Argon2');
6+
if (!defined('PASSWORD_ARGON2ID')) die('skip password_hash not built with Argon2');
67
?>
78
--FILE--
89
<?php
910

1011
var_dump(password_verify('test', '$argon2i$v=19$m=65536,t=3,p=1$OEVjWWs2Z3YvWlNZQ0ZmNw$JKin7ahjmh8JYvMyFcXri0Ss/Uvd3uYpD7MG6C/5Cy0'));
1112

1213
var_dump(password_verify('argon2', '$argon2i$v=19$m=65536,t=3,p=1$OEVjWWs2Z3YvWlNZQ0ZmNw$JKin7ahjmh8JYvMyFcXri0Ss/Uvd3uYpD7MG6C/5Cy0'));
14+
15+
var_dump(password_verify('test', '$argon2id$v=19$m=1024,t=2,p=2$WS90MHJhd3AwSC5xTDJpZg$8tn2DaIJR2/UX4Cjcy2t3EZaLDL/qh+NbLQAOvTmdAg'));
16+
var_dump(password_verify('argon2id', '$argon2id$v=19$m=1024,t=2,p=2$WS90MHJhd3AwSC5xTDJpZg$8tn2DaIJR2/UX4Cjcy2t3EZaLDL/qh+NbLQAOvTmdAg'));
1317
echo "OK!";
1418
?>
1519
--EXPECT--
1620
bool(true)
1721
bool(false)
22+
bool(true)
23+
bool(false)
1824
OK!

0 commit comments

Comments
 (0)