From 149845b62e9ff40a3b5eaa9ac8f09567473c926e Mon Sep 17 00:00:00 2001 From: Andrey Andreev Date: Mon, 9 Nov 2026 22:15:43 +0200 Subject: [PATCH] Multiple Argon2-related bugfixes and cleanups (libargon2, openssl) --- ext/openssl/openssl_pwhash.c | 48 ++- ext/standard/password.c | 273 ++++++++++++------ .../password/argon2_malformed_hashes.inc | 37 +++ .../argon2_malformed_hashes_unsafe.inc | 12 + .../password/password_get_info_argon2.phpt | 36 ++- .../password_get_info_argon2_error.phpt | 26 ++ .../tests/password/password_hash_argon2.phpt | 24 +- .../password/password_hash_error_argon2.phpt | 81 +++--- .../password_needs_rehash_argon2.phpt | 56 +++- .../password_needs_rehash_argon2_compat.phpt | 82 ++++++ .../password_needs_rehash_argon2_error.phpt | 84 ++++++ .../password_needs_rehash_argon2_threads.phpt | 27 ++ .../password/password_verify_argon2.phpt | 24 -- .../password_verify_argon2_error.phpt | 48 +++ .../password_verify_argon2_extensive.phpt | 70 +++++ .../password_verify_argon2_minimal.phpt | 28 ++ 16 files changed, 757 insertions(+), 199 deletions(-) create mode 100644 ext/standard/tests/password/argon2_malformed_hashes.inc create mode 100644 ext/standard/tests/password/argon2_malformed_hashes_unsafe.inc create mode 100644 ext/standard/tests/password/password_get_info_argon2_error.phpt create mode 100644 ext/standard/tests/password/password_needs_rehash_argon2_compat.phpt create mode 100644 ext/standard/tests/password/password_needs_rehash_argon2_error.phpt create mode 100644 ext/standard/tests/password/password_needs_rehash_argon2_threads.phpt delete mode 100644 ext/standard/tests/password/password_verify_argon2.phpt create mode 100644 ext/standard/tests/password/password_verify_argon2_error.phpt create mode 100644 ext/standard/tests/password/password_verify_argon2_extensive.phpt create mode 100644 ext/standard/tests/password/password_verify_argon2_minimal.phpt diff --git a/ext/openssl/openssl_pwhash.c b/ext/openssl/openssl_pwhash.c index 5fe5bb5cf1bb..9e3147086a25 100644 --- a/ext/openssl/openssl_pwhash.c +++ b/ext/openssl/openssl_pwhash.c @@ -38,7 +38,7 @@ #define PHP_OPENSSL_ITERLIMIT_MIN 1u #define PHP_OPENSSL_ITERLIMIT_MAX UINT32_MAX #define PHP_OPENSSL_THREADS_MIN 1u -#define PHP_OPENSSL_THREADS_MAX UINT32_MAX +#define PHP_OPENSSL_THREADS_MAX 255 #define PHP_OPENSSL_ARGON_VERSION 0x13 @@ -87,7 +87,7 @@ static inline zend_result get_options(zend_array *options, uint32_t *memlimit, u static bool php_openssl_argon2_compute_hash( const char *algo, - uint32_t version, uint32_t memlimit, uint32_t iterlimit, uint32_t threads, + uint32_t version, uint32_t memlimit, uint32_t iterlimit, uint32_t laneslimit, const char *pass, size_t pass_len, const unsigned char *salt, size_t salt_len, unsigned char *hash, size_t hash_len) @@ -95,30 +95,35 @@ static bool php_openssl_argon2_compute_hash( OSSL_PARAM params[7], *p = params; EVP_KDF *kdf = NULL; EVP_KDF_CTX *kctx = NULL; - uint32_t oldthreads; bool ret = false; - oldthreads = OSSL_get_max_threads(NULL); - if (OSSL_set_max_threads(NULL, threads) != 1) { - goto fail; + OSSL_LIB_CTX *libctx = OSSL_LIB_CTX_new(); + if (libctx == NULL) { + return false; + } + + uint32_t ossl_threads = (OSSL_set_max_threads(libctx, laneslimit) == 1) + ? laneslimit : MIN(laneslimit, OSSL_get_max_threads(libctx)); + if (ossl_threads == 0) { + ossl_threads = 1; } + p = params; - *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &threads); - *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &threads); + *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &ossl_threads); + *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &laneslimit); *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iterlimit); *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memlimit); *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, (void *)salt, salt_len); *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, (void *)pass, pass_len); *p++ = OSSL_PARAM_construct_end(); - if ((kdf = EVP_KDF_fetch(NULL, algo, NULL)) == NULL) { + if ((kdf = EVP_KDF_fetch(libctx, algo, NULL)) == NULL) { goto fail; } if ((kctx = EVP_KDF_CTX_new(kdf)) == NULL) { goto fail; } if (EVP_KDF_derive(kctx, hash, hash_len, params) != 1) { - zend_value_error("Unexpected failure hashing password"); goto fail; } @@ -127,7 +132,7 @@ static bool php_openssl_argon2_compute_hash( fail: EVP_KDF_free(kdf); EVP_KDF_CTX_free(kctx); - OSSL_set_max_threads(NULL, oldthreads); + OSSL_LIB_CTX_free(libctx); return ret; } @@ -151,6 +156,7 @@ static zend_string *php_openssl_argon2_hash(const zend_string *password, zend_ar if (!php_openssl_argon2_compute_hash(algo, version, memlimit, iterlimit, threads, ZSTR_VAL(password), ZSTR_LEN(password), salt, PHP_OPENSSL_SALT_SIZE, hash, PHP_OPENSSL_HASH_SIZE)) { + zend_value_error("Unexpected failure hashing password"); return NULL; } @@ -175,13 +181,13 @@ static int php_openssl_argon2_extract( const char *p; char *hash64, *salt64; - if (!digest || (ZSTR_LEN(digest) < sizeof("$argon2id$"))) { + if (!digest || ZSTR_LEN(digest) < 49 || ZSTR_LEN(digest) > 198) { return FAILURE; } p = ZSTR_VAL(digest); if (!memcmp(p, "$argon2i$", strlen("$argon2i$"))) { p += strlen("$argon2i$"); - } else if (!memcmp(p, "$argon2id$", strlen("$argon2id$"))) { + } else if (!memcmp(p, "$argon2id$v=19$", strlen("$argon2id$v=19$"))) { p += strlen("$argon2id$"); } else { return FAILURE; @@ -189,7 +195,14 @@ static int php_openssl_argon2_extract( if (sscanf(p, "v=%" PRIu32 "$m=%" PRIu32 ",t=%" PRIu32 ",p=%" PRIu32, version, memlimit, iterlimit, threads) != 4) { return FAILURE; + } else if ((*version != 0x10 && *version != 0x13) || + *memlimit < PHP_OPENSSL_MEMLIMIT_MIN || *memlimit > PHP_OPENSSL_MEMLIMIT_MAX || + *iterlimit < PHP_OPENSSL_ITERLIMIT_MIN || *iterlimit > PHP_OPENSSL_ITERLIMIT_MAX || + *threads < PHP_OPENSSL_THREADS_MIN || *threads > PHP_OPENSSL_THREADS_MAX || + *memlimit < *threads * 8) { + return FAILURE; } + if (salt && hash) { /* start of param */ p = strchr(p, '$'); @@ -222,12 +235,17 @@ static bool php_openssl_argon2_verify(const zend_string *password, const zend_st zend_string *salt, *hash, *new; bool ret = false; - if ((ZSTR_LEN(password) >= UINT32_MAX) || (ZSTR_LEN(digest) >= UINT32_MAX)) { + if (ZSTR_LEN(password) >= UINT32_MAX || ZSTR_LEN(digest) < 49 || ZSTR_LEN(digest) > 198) { return false; } if (FAILURE == php_openssl_argon2_extract(digest, &version, &memlimit, &iterlimit, &threads, &salt, &hash)) { return false; } + if (ZSTR_LEN(salt) < 8 || ZSTR_LEN(salt) > 48 || ZSTR_LEN(hash) < 12 || ZSTR_LEN(hash) > 64) { + zend_string_free(salt); + zend_string_free(hash); + return false; + } new = zend_string_alloc(ZSTR_LEN(hash), 0); if (php_openssl_argon2_compute_hash(algo, version, memlimit, iterlimit, threads, @@ -266,7 +284,7 @@ static bool php_openssl_argon2_needs_rehash(const zend_string *hash, zend_array } // Algo already checked in pasword_needs_rehash implementation - return (version != new_version) || + return (version < new_version) || (iterlimit != new_iterlimit) || (memlimit != new_memlimit) || (threads != new_threads); diff --git a/ext/standard/password.c b/ext/standard/password.c index 9d6bd6f2003d..0d9875ab0bba 100644 --- a/ext/standard/password.c +++ b/ext/standard/password.c @@ -244,138 +244,239 @@ const php_password_algo php_password_algo_bcrypt = { #ifdef HAVE_ARGON2LIB /* argon2i/argon2id shared implementation */ -static int extract_argon2_parameters(const zend_string *hash, - zend_long *v, zend_long *memory_cost, - zend_long *time_cost, zend_long *threads) /* {{{ */ -{ - const char *p = ZSTR_VAL(hash); - if (!hash || (ZSTR_LEN(hash) < sizeof("$argon2id$"))) { - return FAILURE; +typedef struct php_password_argon2_ops { + argon2_version v; + uint32_t m; + uint32_t t; + uint8_t p; + uint8_t salt_len; + uint8_t digest_len; +} php_password_argon2_ops; + +static bool php_password_argon2_get_options(const zend_array *options, php_password_argon2_ops *ops, const bool silent) { + zval *zv; + zend_long zl; + *ops = (php_password_argon2_ops) { + .v = ARGON2_VERSION_13, /* Don't know the implications of a future version */ + .m = PHP_PASSWORD_ARGON2_MEMORY_COST, + .t = PHP_PASSWORD_ARGON2_TIME_COST, + .p = PHP_PASSWORD_ARGON2_THREADS, + .salt_len = 16, + .digest_len = 32 + }; + + if (options && (zv = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { + zl = zval_get_long(zv); + if (zl >= ARGON2_MIN_MEMORY && zl <= ARGON2_MAX_MEMORY) { + ops->m = zl; + } else if (!silent) { + zend_value_error("Memory cost is outside of allowed memory range"); + return false; + } } - if (!memcmp(p, "$argon2i$", sizeof("$argon2i$") - 1)) { - p += sizeof("$argon2i$") - 1; - } else if (!memcmp(p, "$argon2id$", sizeof("$argon2id$") - 1)) { - p += sizeof("$argon2id$") - 1; - } else { - return FAILURE; + + if (options && (zv = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { + zl = zval_get_long(zv); + if (zl >= ARGON2_MIN_TIME && zl <= ARGON2_MAX_TIME) { + ops->t = zl; + } else if (!silent) { + zend_value_error("Time cost is outside of allowed time range"); + return false; + } } - sscanf(p, "v=" ZEND_LONG_FMT "$m=" ZEND_LONG_FMT ",t=" ZEND_LONG_FMT ",p=" ZEND_LONG_FMT, - v, memory_cost, time_cost, threads); + if (options && (zv = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { + zl = zval_get_long(zv); + /* ARGON2_MAX_LANES is a theoretical KDF value; 255 is specified by PHC strings. */ + if (zl >= ARGON2_MIN_LANES && zl <= 255) { + ops->p = zl; + } else if (!silent) { + zend_value_error("Invalid number of threads"); + return false; + } + } - return SUCCESS; + return true; } -/* }}} */ -static int php_password_argon2_get_info(zval *return_value, const zend_string *hash) { - zend_long v = 0; - zend_long memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; - zend_long time_cost = PHP_PASSWORD_ARGON2_TIME_COST; - zend_long threads = PHP_PASSWORD_ARGON2_THREADS; +static inline bool php_password_argon2_parse_prefix(const char **ptr, argon2_version *v) { + argon2_type type; + const char *h = *ptr; + if (memcmp(h, "$argon2id$", sizeof("$argon2id$")-1) == 0) { + type = Argon2_id; + h += sizeof("$argon2id$")-1; + } else if (memcmp(h, "$argon2i$", sizeof("$argon2i$")-1) == 0) { + type = Argon2_i; + h += sizeof("$argon2i$")-1; + } else { + return false; + } - extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads); + if (memcmp(h, "v=19$", 5) == 0) { + *v = ARGON2_VERSION_13; + h += 5; + /* 1.0 hashes didn't tag the version, but it could be set by future implementers */ + } else if (memcmp(h, "v=16$", 5) == 0) { + *v = ARGON2_VERSION_10; + h += 5; + } else if (h[0] == 'm') { + *v = ARGON2_VERSION_10; + } else { + return false; + } - add_assoc_long(return_value, "memory_cost", memory_cost); - add_assoc_long(return_value, "time_cost", time_cost); - add_assoc_long(return_value, "threads", threads); + *ptr = h; + /* Argon2id was introduced by version 1.3 */ + return !(type == Argon2_id && *v == ARGON2_VERSION_10); +} - return SUCCESS; +static inline bool php_password_argon2_parse_param(const char **ptr, const char param, const uint32_t maxval, zend_ulong *outval) { + const char *h = *ptr; + if (h[0] != param || h[1] != '=' || h[2] < '1' || h[2] > '9') { + return false; + } + + char *dptr = NULL; + zend_ulong val = ZEND_STRTOUL(h + 2, &dptr, 10); + if (*dptr != (param == 'p' ? '$' : ',') || val > maxval) { + return false; + } + *outval = val; + *ptr = (const char *)dptr + 1; + return true; } -static bool php_password_argon2_needs_rehash(const zend_string *hash, zend_array *options) { - zend_long v = 0; - zend_long new_memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST, memory_cost = 0; - zend_long new_time_cost = PHP_PASSWORD_ARGON2_TIME_COST, time_cost = 0; - zend_long new_threads = PHP_PASSWORD_ARGON2_THREADS, threads = 0; - zval *option_buffer; +static bool php_password_argon2_parse(const zend_string *hash, php_password_argon2_ops *ops) { + /* Min/max allowed by PHC strings. keyid and data params excluded as unsupported. */ + if (ZSTR_LEN(hash) < 49 || ZSTR_LEN(hash) > 198) { + return false; + } - if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { - new_memory_cost = zval_get_long(option_buffer); + argon2_version v; + zend_ulong m, t, p; + const char *hashptr = ZSTR_VAL(hash); + // Don't use ARGON2_MAX_* values here; it gets weird on 32bit builds + if (!php_password_argon2_parse_prefix(&hashptr, &v) || + !php_password_argon2_parse_param(&hashptr, 'm', UINT32_MAX, &m) || + !php_password_argon2_parse_param(&hashptr, 't', UINT32_MAX, &t) || + !php_password_argon2_parse_param(&hashptr, 'p', 255, &p)) { + return false; } - if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { - new_time_cost = zval_get_long(option_buffer); + if (m < p * 8) { + return false; } - if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { - new_threads = zval_get_long(option_buffer); + const char *end = ZSTR_VAL(hash) + ZSTR_LEN(hash); + uint8_t s_len = 0, d_len = 0; + for (bool is_salt = true; hashptr < end; hashptr++) { + char c = *hashptr; + if (c == '+' || (c >= '/' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + (is_salt) ? s_len++ : d_len++; + } else if (c == '$' && is_salt) { + is_salt = false; + } else { + return false; + } } - extract_argon2_parameters(hash, &v, &memory_cost, &time_cost, &threads); + if (s_len < 11 || s_len > 64 || s_len % 4 == 1 || d_len < 16 || d_len > 86 || d_len % 4 == 1) { + return false; + } - return (new_time_cost != time_cost) || - (new_memory_cost != memory_cost) || - (new_threads != threads); + if (ops != NULL) { + *ops = (php_password_argon2_ops) { + .v = v, .m = m, .t = t, .p = p, + /* B64 length to binary size */ + .salt_len = (3 * s_len) / 4, + .digest_len = (3 * d_len) / 4 + }; + } + + return true; } -static zend_string *php_password_argon2_hash(const zend_string *password, zend_array *options, argon2_type type) { - zval *option_buffer; - zend_string *salt, *out, *encoded; - size_t time_cost = PHP_PASSWORD_ARGON2_TIME_COST; - size_t memory_cost = PHP_PASSWORD_ARGON2_MEMORY_COST; - size_t threads = PHP_PASSWORD_ARGON2_THREADS; - size_t encoded_len; - int status = 0; +static bool php_password_argon2_valid(const zend_string *hash) { + return php_password_argon2_parse(hash, NULL); +} - if (options && (option_buffer = zend_hash_str_find(options, "memory_cost", sizeof("memory_cost")-1)) != NULL) { - memory_cost = zval_get_long(option_buffer); +static int php_password_argon2_get_info(zval *return_value, const zend_string *hash) { + php_password_argon2_ops ops; + if (!php_password_argon2_parse(hash, &ops)) { + return FAILURE; } - if (memory_cost > ARGON2_MAX_MEMORY || memory_cost < ARGON2_MIN_MEMORY) { - zend_value_error("Memory cost is outside of allowed memory range"); - return NULL; + add_assoc_long(return_value, "memory_cost", ops.m); + add_assoc_long(return_value, "time_cost", ops.t); + /* Note: threads is a misnomer here, but it was voted-in as is. */ + add_assoc_long(return_value, "threads", ops.p); + + return SUCCESS; +} + +static bool php_password_argon2_needs_rehash(const zend_string *hash, zend_array *options) { + php_password_argon2_ops new, old; + + if (!php_password_argon2_get_options(options, &new, true)) { + return false; } - if (options && (option_buffer = zend_hash_str_find(options, "time_cost", sizeof("time_cost")-1)) != NULL) { - time_cost = zval_get_long(option_buffer); + if (!php_password_argon2_parse(hash, &old)) { + return true; } - if (time_cost > ARGON2_MAX_TIME || time_cost < ARGON2_MIN_TIME) { - zend_value_error("Time cost is outside of allowed time range"); - return NULL; + /* Keeping this separate as overriding to other conditions. */ + if (old.v > ARGON2_VERSION_NUMBER) { + return false; } - if (options && (option_buffer = zend_hash_str_find(options, "threads", sizeof("threads")-1)) != NULL) { - threads = zval_get_long(option_buffer); + /* If anything except m,t,p differs, only rehash if it would be an upgrade */ + return new.v > old.v || new.salt_len > old.salt_len || new.digest_len > old.digest_len || + new.m != old.m || new.t != old.t || new.p != old.p; +} + +static zend_string *php_password_argon2_hash(const zend_string *password, zend_array *options, const argon2_type type) { + if (ZSTR_LEN(password) > ARGON2_MAX_PWD_LENGTH) { + zend_value_error("Password is too long"); + return NULL; } - if (threads > ARGON2_MAX_LANES || threads == 0) { - zend_value_error("Invalid number of threads"); + php_password_argon2_ops ops; + if (!php_password_argon2_get_options(options, &ops, false)) { return NULL; } - if (!(salt = php_password_get_salt(NULL, Z_UL(16), options))) { + zend_string *salt; + if (!(salt = php_password_get_salt(NULL, ops.salt_len, options))) { return NULL; } - out = zend_string_alloc(32, 0); - encoded_len = argon2_encodedlen( - time_cost, - memory_cost, - threads, - (uint32_t)ZSTR_LEN(salt), - ZSTR_LEN(out), + uint8_t encoded_len = argon2_encodedlen( + ops.t, + ops.m, + ops.p, + ops.salt_len, + ops.digest_len, type ); - encoded = zend_string_alloc(encoded_len - 1, 0); - status = argon2_hash( - time_cost, - memory_cost, - threads, + zend_string *encoded = zend_string_alloc(encoded_len - 1, 0); + const int status = argon2_hash( + ops.t, + ops.m, + ops.p, ZSTR_VAL(password), ZSTR_LEN(password), ZSTR_VAL(salt), ZSTR_LEN(salt), - ZSTR_VAL(out), - ZSTR_LEN(out), + NULL, + ops.digest_len, ZSTR_VAL(encoded), encoded_len, type, - ARGON2_VERSION_NUMBER + ops.v ); - zend_string_release_ex(out, 0); zend_string_release_ex(salt, 0); if (status != ARGON2_OK) { @@ -391,7 +492,7 @@ static zend_string *php_password_argon2_hash(const zend_string *password, zend_a /* argon2i specific methods */ static bool php_password_argon2i_verify(const zend_string *password, const zend_string *hash) { - return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_i); + return ARGON2_OK == argon2i_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password)); } static zend_string *php_password_argon2i_hash(const zend_string *password, zend_array *options) { @@ -404,13 +505,13 @@ const php_password_algo php_password_algo_argon2i = { php_password_argon2i_verify, php_password_argon2_needs_rehash, php_password_argon2_get_info, - NULL, + php_password_argon2_valid, }; /* argon2id specific methods */ static bool php_password_argon2id_verify(const zend_string *password, const zend_string *hash) { - return ARGON2_OK == argon2_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password), Argon2_id); + return ARGON2_OK == argon2id_verify(ZSTR_VAL(hash), ZSTR_VAL(password), ZSTR_LEN(password)); } static zend_string *php_password_argon2id_hash(const zend_string *password, zend_array *options) { @@ -423,7 +524,7 @@ const php_password_algo php_password_algo_argon2id = { php_password_argon2id_verify, php_password_argon2_needs_rehash, php_password_argon2_get_info, - NULL, + php_password_argon2_valid, }; #endif diff --git a/ext/standard/tests/password/argon2_malformed_hashes.inc b/ext/standard/tests/password/argon2_malformed_hashes.inc new file mode 100644 index 000000000000..7a172a5e0c79 --- /dev/null +++ b/ext/standard/tests/password/argon2_malformed_hashes.inc @@ -0,0 +1,37 @@ + '$argon2i$m=65536,t=2,p=1c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ', + 'v1.0 missing $ post-salt' => '$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ', + 'v1.0 missing salt' => '$argon2i$m=65536,t=2,p=1$$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ', + 'v1.3 missing $ pre-salt' => '$argon2i$v=19$m=65536,t=2,p=1c29tZXNhbHQ$wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA', + 'v1.3 missing $ post-salt' => '$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQwWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA', + 'v1.3 missing salt' => '$argon2i$v=19$m=65536,t=2,p=1$$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ', + // Other permutations + 'invalid version' => '$argon2i$v=18$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'parameter version' => '$argon2i$v=19,m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'impossible argon2id v1.0' => '$argon2id$v=16$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'missing version argon2id' => '$argon2id$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'unsupported param keyid' => '$argon2id$v=19$m=65536,t=4,p=1,keyid=MQ$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'unsupported param data' => '$argon2id$v=19$m=65536,t=4,p=1,data=MQ$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'memory < 8KiB' => '$argon2id$v=19$m=7,t=1,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'memory < (8KiB * threads)' => '$argon2id$v=19$m=15,t=1,p=2$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'time 0' => '$argon2id$v=19$m=8,t=0,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads 0' => '$argon2id$v=19$m=8,t=1,p=0$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads > 255' => '$argon2id$v=19$m=2048,t=1,p=256$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'salt too short' => '$argon2id$v=19$m=65536,t=4,p=1$AAAAAAAAAA$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'salt too long' => '$argon2id$v=19$m=65536,t=4,p=1$FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'salt (length % 4 == 1)' => '$argon2id$v=19$m=65536,t=4,p=1$FFFFFFFFFFFFg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'salt invalid b64' => '$argon2id$v=19$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUV..$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'digest too short' => '$argon2id$v=19$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$AAAAAAAAAAAAAAw', + 'digest too long' => '$argon2id$v=19$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFw', + 'digest (length % 4 == 1)' => '$argon2id$v=19$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFw', + 'digest invalid b64' => '$argon2id$v=19$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w==', + '$ after digest' => '$argon2id$v=19$m=65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w$', + 'memory with space' => '$argon2id$v=19$m= 65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'memory with +sign' => '$argon2id$v=19$m=+65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'time with space' => '$argon2id$v=19$m=65536,t= 4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'time with +sign' => '$argon2id$v=19$m=65536,t=+4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads with space' => '$argon2id$v=19$m=65536,t=4,p= 1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads with +sign' => '$argon2id$v=19$m=65536,t=4,p=+1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', +]; diff --git a/ext/standard/tests/password/argon2_malformed_hashes_unsafe.inc b/ext/standard/tests/password/argon2_malformed_hashes_unsafe.inc new file mode 100644 index 000000000000..794208bd833c --- /dev/null +++ b/ext/standard/tests/password/argon2_malformed_hashes_unsafe.inc @@ -0,0 +1,12 @@ + (2^32)-1 KiB' => '$argon2id$v=19$m=4294967296,t=1,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'memory uint overflow' => '$argon2id$v=19$m=92233720368547758070,t=1,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'time > (2^32)-1' => '$argon2id$v=19$m=8,t=4294967296,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'time uint overflow' => '$argon2id$v=19$m=8,t=92233720368547758070,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads > 255' => '$argon2id$v=19$m=2048,t=1,p=256$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads uint overflow' => '$argon2id$v=19$m=8,t=1,p=92233720368547758070$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'memory with -sign' => '$argon2id$v=19$m=-65536,t=4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'time with -sign' => '$argon2id$v=19$m=65536,t=-4,p=1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', + 'threads with -sign' => '$argon2id$v=19$m=65536,t=4,p=-1$ABCDEFGHIJKLMNOPQRSTUg$VWXYZabcdefghijklmnopqrstuvxyz0123456789+/w', +]; diff --git a/ext/standard/tests/password/password_get_info_argon2.phpt b/ext/standard/tests/password/password_get_info_argon2.phpt index 1965d103db1c..ffa6bb1fad92 100644 --- a/ext/standard/tests/password/password_get_info_argon2.phpt +++ b/ext/standard/tests/password/password_get_info_argon2.phpt @@ -2,17 +2,39 @@ Test normal operation of password_get_info() with Argon2i and Argon2id --SKIPIF-- --FILE-- 'v=19', + 'params' => sprintf('m=%s,t=%s,p=%s', PASSWORD_ARGON2_DEFAULT_MEMORY_COST, PASSWORD_ARGON2_DEFAULT_TIME_COST, PASSWORD_ARGON2_DEFAULT_THREADS), + 'salt' => substr($b64alphabet, 0, 22), + 'digest' => substr($b64alphabet, -43), +]; + +$valid = [ + 'optional version 1.0 excluded' => sprintf(substr($template, 3), 'argon2i', $defaults['params'], $defaults['salt'], $defaults['digest']), + 'optional version 1.0 included' => sprintf($template, 'argon2i', 'v=16', $defaults['params'], $defaults['salt'], $defaults['digest']), + // Also tests min param values + 'shortest possible hash' => sprintf(substr($template, 3), 'argon2i', 'm=8,t=1,p=1', substr($b64alphabet, 0, 11), substr($b64alphabet, -16)), + // Also tests theoretical max param values and that all B64 characters are accepted + 'longest possible hash' => sprintf($template, 'argon2id', 'v=19', 'm=4294967295,t=4294967295,p=255', $b64alphabet, str_repeat('F', 86)), +]; + +foreach ($valid as $name => $hash) { + echo sprintf("%s: %s\n", $name, (password_get_info($hash)['algoName'] === 'unknown') ? 'REJECTED' : 'OK'); +} + +echo "OK!" ?> ---EXPECT-- +--EXPECTF-- array(3) { ["algo"]=> string(7) "argon2i" @@ -43,4 +65,8 @@ array(3) { int(2) } } +optional version 1.0 excluded: OK +optional version 1.0 included: OK +shortest possible hash: OK +longest possible hash: OK OK! diff --git a/ext/standard/tests/password/password_get_info_argon2_error.phpt b/ext/standard/tests/password/password_get_info_argon2_error.phpt new file mode 100644 index 000000000000..cc663534d915 --- /dev/null +++ b/ext/standard/tests/password/password_get_info_argon2_error.phpt @@ -0,0 +1,26 @@ +--TEST-- +Test error operation of password_get_info() with Argon2i and Argon2id +--SKIPIF-- + +--FILE-- + $test) { + if (password_get_info($test)['algoName'] !== 'unknown') { + $ok = false; + echo sprintf("\n%s (%s) was NOT REJECTED!", $test, $k); + continue; + } +} +echo ($ok) ? " OK\n" : "\n"; +echo "OK!" +?> +--EXPECTF-- +Verifying that %d invalid hashes are rejected ... OK +OK! diff --git a/ext/standard/tests/password/password_hash_argon2.phpt b/ext/standard/tests/password/password_hash_argon2.phpt index b179ee202318..84648d2a32d4 100644 --- a/ext/standard/tests/password/password_hash_argon2.phpt +++ b/ext/standard/tests/password/password_hash_argon2.phpt @@ -2,41 +2,39 @@ Test normal operation of password_hash() with Argon2i and Argon2id --SKIPIF-- --FILE-- ---EXPECT-- +--EXPECTF-- bool(true) -string(7) "argon2i" bool(true) -string(7) "argon2i" +$argon2i$v=19$m=%d,t=%d,p=%d$%s$%s bool(true) -string(7) "argon2i" +$argon2i$v=19$m=%d,t=%d,p=%d$%s$%s bool(true) -string(8) "argon2id" +$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s bool(true) -string(8) "argon2id" +$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s bool(true) -string(8) "argon2id" OK! diff --git a/ext/standard/tests/password/password_hash_error_argon2.phpt b/ext/standard/tests/password/password_hash_error_argon2.phpt index 0323057a7efc..defd0e70581a 100644 --- a/ext/standard/tests/password/password_hash_error_argon2.phpt +++ b/ext/standard/tests/password/password_hash_error_argon2.phpt @@ -2,52 +2,53 @@ Test error operation of password_hash() with Argon2i and Argon2id --SKIPIF-- --FILE-- 0]); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; -} - -try { - password_hash('test', PASSWORD_ARGON2I, ['time_cost' => 0]); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; -} +$stringify = function(array $options): string { + $output = ''; + foreach ($options as $k => $v) { + $output .= ",{$k}={$v}"; + } + return ltrim($output, ","); +}; -try { - password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; -} +$options = [ + ["memory_cost" => 0], + ["time_cost" => 0], + ["threads" => 0], + ["threads" => 256], // openssl accepts it and somehow computes super fast?! blind hard-coded 1? + ["memory_cost" => 7], + ["memory_cost" => ($threads = 2)*8-1, "threads" => $threads] // openssl errors, but does attempt hashing and the error is different +]; -try { - password_hash('test', PASSWORD_ARGON2ID, ['memory_cost' => 0]); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; -} +foreach ([PASSWORD_ARGON2I, PASSWORD_ARGON2ID] as $algo) { + foreach ($options as $o) { + try { + $result = password_hash('test', $algo, $o); + } catch (ValueError $exception) { + $result = 'error: '.$exception->getMessage(); + } -try { - password_hash('test', PASSWORD_ARGON2ID, ['time_cost' => 0]); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; + echo sprintf("%s %s: %s\n", $algo, $stringify($o), $result); + } } -try { - password_hash('test', PASSWORD_ARGON2ID, ['threads' => 0]); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; -} +echo "OK!" +// Note: libsodium always triggers an error about not supporting threads!=1 ?> ---EXPECT-- -Memory cost is outside of allowed memory range -Time cost is outside of allowed time range -Invalid number of threads -Memory cost is outside of allowed memory range -Time cost is outside of allowed time range -Invalid number of threads +--EXPECTF-- +argon2i memory_cost=0: error: Memory cost is outside of allowed memory range +argon2i time_cost=0: error: Time cost is outside of allowed time range +argon2i threads=0: error: %s +argon2i threads=256: error: %s +argon2i memory_cost=7: error: Memory cost is outside of allowed memory range +argon2i memory_cost=15,threads=2: error: %s +argon2id memory_cost=0: error: Memory cost is outside of allowed memory range +argon2id time_cost=0: error: Time cost is outside of allowed time range +argon2id threads=0: error: %s +argon2id threads=256: error: %s +argon2id memory_cost=7: error: Memory cost is outside of allowed memory range +argon2id memory_cost=15,threads=2: error: %s +OK! diff --git a/ext/standard/tests/password/password_needs_rehash_argon2.phpt b/ext/standard/tests/password/password_needs_rehash_argon2.phpt index 8a6e7a8b271b..49298ee88a47 100644 --- a/ext/standard/tests/password/password_needs_rehash_argon2.phpt +++ b/ext/standard/tests/password/password_needs_rehash_argon2.phpt @@ -2,29 +2,53 @@ Test normal operation of password_needs_rehash() with Argon2i and Argon2id --SKIPIF-- --FILE-- getMessage(); + } +}; -$hash = password_hash('test', PASSWORD_ARGON2I); -var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I)); -var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST * 2])); -var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST + 1])); +$options = sprintf('m=%d,t=%d,p=%d', PASSWORD_ARGON2_DEFAULT_MEMORY_COST, PASSWORD_ARGON2_DEFAULT_TIME_COST, PASSWORD_ARGON2_DEFAULT_THREADS); +$template = '$%s'.sprintf('$v=19$%s$rmuWsgW7EVRE+0rxKhCFUw$3XoT9PdOvdvimSBt07k/eAC0X6+cZWz7aCfnfepbD2I', $options); -$hash = password_hash('test', PASSWORD_ARGON2ID); -var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID)); -var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID, ['memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST * 2])); -var_dump(password_needs_rehash($hash, PASSWORD_ARGON2ID, ['time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST + 1])); +foreach (['argon2i', 'argon2id'] as $algo) { + $hash = sprintf($template, $algo); + $old = password_get_info($hash); + + echo sprintf("%s unchanged: %s\n", $old['algo'], $result($hash, $old['algo'], $old['options'])); + + $new_algo = ($algo === 'argon2i') ? 'argon2id' : 'argon2i'; + echo sprintf("%s to %s: %s\n", $algo, $new_algo, $result($hash, $new_algo, $old['options'])); + + // Don't test threads here as the libsodium implementation errors out; separate test for that + foreach (['memory_cost', 'time_cost'] as $o) { + $new_options = $old['options']; + $new_options[$o] = $old['options'][$o] + 1; + echo sprintf("%s %s + 1: %s\n", $old['algo'], $o, $result($hash, $old['algo'], $new_options)); + $new_options[$o] = $old['options'][$o] - 1; + echo sprintf("%s %s - 1: %s\n", $old['algo'], $o, $result($hash, $old['algo'], $new_options)); + } +} echo "OK!"; ?> --EXPECT-- -bool(false) -bool(true) -bool(true) -bool(false) -bool(true) -bool(true) +argon2i unchanged: keep +argon2i to argon2id: rehash +argon2i memory_cost + 1: rehash +argon2i memory_cost - 1: rehash +argon2i time_cost + 1: rehash +argon2i time_cost - 1: rehash +argon2id unchanged: keep +argon2id to argon2i: rehash +argon2id memory_cost + 1: rehash +argon2id memory_cost - 1: rehash +argon2id time_cost + 1: rehash +argon2id time_cost - 1: rehash OK! diff --git a/ext/standard/tests/password/password_needs_rehash_argon2_compat.phpt b/ext/standard/tests/password/password_needs_rehash_argon2_compat.phpt new file mode 100644 index 000000000000..c8518d257067 --- /dev/null +++ b/ext/standard/tests/password/password_needs_rehash_argon2_compat.phpt @@ -0,0 +1,82 @@ +--TEST-- +Test interoperability of password_needs_rehash() with Argon2i and Argon2id +--SKIPIF-- + +--FILE-- + sprintf('$argon2i$%s$%s$%s', $defaults, $salt, $digest), + 'argon2i explicit v1.0, default params' => sprintf('$argon2i$v=16$%s$%s$%s', $defaults, $salt, $digest), + 'argon2i implicit v1.0, modified params' => sprintf('$argon2i$%s$%s$%s', $defaults, $salt, $digest), + 'argon2i explicit v1.0, modified params' => sprintf('$argon2i$v=16$%s$%s$%s', $defaults, $salt, $digest), +]; + +foreach (['argon2i', 'argon2id'] as $algo) { + // Should be considered configuration changes, but larger sizes shouldn't trigger a rehash to avoid downgrades + $hashes[$algo.' shorter salt, default params'] = sprintf($template, $algo, $defaults, $salt_s, $digest); + $hashes[$algo.' longer salt, default params'] = sprintf($template, $algo, $defaults, $salt_l, $digest); + $hashes[$algo.' shorter digest, default params'] = sprintf($template, $algo, $defaults, $salt, $digest_s); + $hashes[$algo.' longer digest, default params'] = sprintf($template, $algo, $defaults, $salt, $digest_l); + // All should trigger rehash due to modified options + $hashes[$algo.' shorter salt, modified params'] = sprintf($template, $algo, $modified, $salt_s, $digest); + $hashes[$algo.' longer salt, modified params'] = sprintf($template, $algo, $modified, $salt_l, $digest); + $hashes[$algo.' shorter digest, modified params'] = sprintf($template, $algo, $modified, $salt, $digest_s); + $hashes[$algo.' longer digest, modified params'] = sprintf($template, $algo, $modified, $salt, $digest_l); +} + +foreach ($hashes as $scenario => $hash) { + // Don't use password_get_info() to avoid cascading bug effects with unusual formats + $algo = explode('$', $hash, 3)[1]; + echo sprintf("%s: %s\n", $scenario, $result($hash, $algo, [])); +} + +echo "OK!"; +?> +--EXPECT-- +argon2i implicit v1.0, default params: rehash +argon2i explicit v1.0, default params: rehash +argon2i implicit v1.0, modified params: rehash +argon2i explicit v1.0, modified params: rehash +argon2i shorter salt, default params: rehash +argon2i longer salt, default params: keep +argon2i shorter digest, default params: rehash +argon2i longer digest, default params: keep +argon2i shorter salt, modified params: rehash +argon2i longer salt, modified params: rehash +argon2i shorter digest, modified params: rehash +argon2i longer digest, modified params: rehash +argon2id shorter salt, default params: rehash +argon2id longer salt, default params: keep +argon2id shorter digest, default params: rehash +argon2id longer digest, default params: keep +argon2id shorter salt, modified params: rehash +argon2id longer salt, modified params: rehash +argon2id shorter digest, modified params: rehash +argon2id longer digest, modified params: rehash +OK! diff --git a/ext/standard/tests/password/password_needs_rehash_argon2_error.phpt b/ext/standard/tests/password/password_needs_rehash_argon2_error.phpt new file mode 100644 index 000000000000..604ff752b5a8 --- /dev/null +++ b/ext/standard/tests/password/password_needs_rehash_argon2_error.phpt @@ -0,0 +1,84 @@ +--TEST-- +Test error operation of password_needs_rehash() with Argon2i and Argon2id +--SKIPIF-- + +--FILE-- +getMessage(); + } +}; + +$template = '$%s$v=19$%s$rmuWsgW7EVRE+0rxKhCFUw$3XoT9PdOvdvimSBt07k/eAC0X6+cZWz7aCfnfepbD2I'; +// Keep p=1 to not trigger libsodium errors, as it only supports threads=1 +$defaults = sprintf('m=%d,t=%d,p=1', PASSWORD_ARGON2_DEFAULT_MEMORY_COST, PASSWORD_ARGON2_DEFAULT_TIME_COST); +$modified = sprintf('m=%d,t=%d,p=1', PASSWORD_ARGON2_DEFAULT_MEMORY_COST-1, PASSWORD_ARGON2_DEFAULT_TIME_COST-1); + +foreach (['argon2i', 'argon2id'] as $algo) { + $hash = sprintf($template, $algo, $defaults); + echo sprintf("%s defaults with invalid memory_cost: %s\n", $algo, $result($hash, $algo, ['memory_cost' => 7])); + echo sprintf("%s defaults with invalid time_cost: %s\n", $algo, $result($hash, $algo, ['time_cost' => 0])); + echo sprintf("%s defaults with invalid threads: %s\n", $algo, $result($hash, $algo, ['threads' => 0])); + $hash = sprintf($template, $algo, $modified); + echo sprintf("%s non-defaults with invalid memory_cost: %s\n", $algo, $result($hash, $algo, ['memory_cost' => 7])); + echo sprintf("%s non-defaults with invalid time_cost: %s\n", $algo, $result($hash, $algo, ['time_cost' => 0])); + echo sprintf("%s non-defaults with invalid threads: %s\n", $algo, $result($hash, $algo, ['threads' => 0])); +} + +/* libargon2 output: +argon2i defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2i defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2i defaults with invalid threads: error: A thread value other than 1 is not supported by this implementation +argon2i non-defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2i non-defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2i non-defaults with invalid threads: error: A thread value other than 1 is not supported by this implementation +argon2id defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2id defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2id defaults with invalid threads: error: A thread value other than 1 is not supported by this implementation +argon2id non-defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2id non-defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2id non-defaults with invalid threads: error: A thread value other than 1 is not supported by this implementation +*/ + +/* openssl output: +argon2i defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2i defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2i defaults with invalid threads: error: Invalid number of threads +argon2i non-defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2i non-defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2i non-defaults with invalid threads: error: Invalid number of threads +argon2id defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2id defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2id defaults with invalid threads: error: Invalid number of threads +argon2id non-defaults with invalid memory_cost: error: Memory cost is outside of allowed memory range +argon2id non-defaults with invalid time_cost: error: Time cost is outside of allowed time range +argon2id non-defaults with invalid threads: error: Invalid number of threads +*/ + +// Output based on logic that *if* password_needs_rehash() doesn't trigger exceptions, it would fallback +// to a default for each invalid value. +// Initial libargon2 implementation blindly returned true on any diff though. If you're not reading this +// during a PR review and chasing a breaking change, look at the commit history for this file. It's the first one. +// With each Argon2 provider acting differently, this is effectively undefined behavior yet. +echo "OK!"; +?> +--EXPECT-- +argon2i defaults with invalid memory_cost: keep +argon2i defaults with invalid time_cost: keep +argon2i defaults with invalid threads: keep +argon2i non-defaults with invalid memory_cost: rehash +argon2i non-defaults with invalid time_cost: rehash +argon2i non-defaults with invalid threads: rehash +argon2id defaults with invalid memory_cost: keep +argon2id defaults with invalid time_cost: keep +argon2id defaults with invalid threads: keep +argon2id non-defaults with invalid memory_cost: rehash +argon2id non-defaults with invalid time_cost: rehash +argon2id non-defaults with invalid threads: rehash +OK! diff --git a/ext/standard/tests/password/password_needs_rehash_argon2_threads.phpt b/ext/standard/tests/password/password_needs_rehash_argon2_threads.phpt new file mode 100644 index 000000000000..e4677187a44a --- /dev/null +++ b/ext/standard/tests/password/password_needs_rehash_argon2_threads.phpt @@ -0,0 +1,27 @@ +--TEST-- +Test threads option of password_needs_rehash() with Argon2i and Argon2id +--SKIPIF-- + +--FILE-- + $threads+1]) ? 'rehash' : 'keep'); + echo sprintf("%s threads - 1: %s\n", $algo, password_needs_rehash($hash, $algo, ['threads' => $threads-1]) ? 'rehash' : 'keep'); +} + +echo "OK!"; +?> +--EXPECT-- +argon2i threads + 1: rehash +argon2i threads - 1: rehash +argon2id threads + 1: rehash +argon2id threads - 1: rehash +OK! diff --git a/ext/standard/tests/password/password_verify_argon2.phpt b/ext/standard/tests/password/password_verify_argon2.phpt deleted file mode 100644 index 43db2926ecf4..000000000000 --- a/ext/standard/tests/password/password_verify_argon2.phpt +++ /dev/null @@ -1,24 +0,0 @@ ---TEST-- -Test normal operation of password_verify() with Argon2i and Argon2id ---SKIPIF-- - ---FILE-- - ---EXPECT-- -bool(true) -bool(false) -bool(true) -bool(false) -OK! diff --git a/ext/standard/tests/password/password_verify_argon2_error.phpt b/ext/standard/tests/password/password_verify_argon2_error.phpt new file mode 100644 index 000000000000..824152e3ce92 --- /dev/null +++ b/ext/standard/tests/password/password_verify_argon2_error.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test error operation of password_verify() with Argon2i and Argon2id +--SKIPIF-- + +--FILE-- + $test) { + $time_s = hrtime(true); + $return = password_verify('password', $test); + $time_e = hrtime(true); + $time_t = $time_e - $time_s; + if ($return !== false) { + echo sprintf("\ntrue returned for %s (%s) with password 'password'\n", $test, $k); + break; + } elseif ($time_t >= $min_compute_time) { + $ok = false; + echo sprintf( + "\n%s (%s) took longer to reject than a minimal valid hash", + $test, $k + ); + } +} +echo ($ok) ? " OK\n" : "\n"; +echo "OK!"; +?> +--EXPECTF-- +Verifying that %d invalid hashes are rejected ... OK +OK! diff --git a/ext/standard/tests/password/password_verify_argon2_extensive.phpt b/ext/standard/tests/password/password_verify_argon2_extensive.phpt new file mode 100644 index 000000000000..866e1e8e36ef --- /dev/null +++ b/ext/standard/tests/password/password_verify_argon2_extensive.phpt @@ -0,0 +1,70 @@ +--TEST-- +Test normal operation of password_verify() with Argon2i and Argon2id -- extensive +--SKIPIF-- + +--FILE-- + '$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ'], +// ['hash' => '$argon2i$m=256,t=2,p=1$c29tZXNhbHQ$/U3YPXYsSb3q9XxHvc0MLxur+GP960kN9j7emXX8zwY'], +// ['hash' => '$argon2i$m=256,t=2,p=2$c29tZXNhbHQ$tsEVYKap1h6scGt5ovl9aLRGOqOth+AMB+KwHpDFZPs'], +// ['hash' => '$argon2i$m=65536,t=1,p=1$c29tZXNhbHQ$gWMFUrjzsfSM2xmSxMZ4ZD1JCytetP9sSzQ4tWIXJLI'], +// ['hash' => '$argon2i$m=65536,t=4,p=1$c29tZXNhbHQ$8hLwFhXm6110c03D70Ct4tUdBSRo2MaUQKOh8sHChHs'], +// ['hash' => '$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ$6ckCB0tnVFMaOgvlGeW69ASzDOabPwGsO/ISKZYBCaM', 'password' => 'differentpassword'], +// ['hash' => '$argon2i$m=65536,t=2,p=1$ZGlmZnNhbHQ$eaEDuQ/orvhXDLMfyLIiWXeJFvgza3vaw4kladTxxJc'], +// ['hash' => '$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ$b2G3seW+uPzerwQQC+/E1K50CLLO7YXy0JRcaTuswRo', 'expect' => false], // password is 'passwore' + // Version 1.3 + ['hash' => '$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA'], + ['hash' => '$argon2i$v=19$m=256,t=2,p=1$c29tZXNhbHQ$iekCn0Y3spW+sCcFanM2xBT63UP2sghkUoHLIUpWRS8'], + ['hash' => '$argon2i$v=19$m=256,t=2,p=2$c29tZXNhbHQ$T/XOJ2mh1/TIpJHfCdQan76Q5esCFVoT5MAeIM1Oq2E'], + ['hash' => '$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQ$0WgHXE2YXhPr6uVgz4uUw7XYoWxRkWtvSsLaOsEbvs8'], + ['hash' => '$argon2i$v=19$m=65536,t=4,p=1$c29tZXNhbHQ$qqlT1YrzcGzj3xrv1KZKhOMdf1QXUjHxKFJZ+IF0zls'], + ['hash' => '$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$FK6NoBr+qHAMI1jc73xTWNkCEoK9iGY6RWL1n7dNIu4', 'password' => 'differentpassword'], + ['hash' => '$argon2i$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQ$sDV8zPvvkfOGCw26RHsjSMvv7K2vmQq/6cxAcmxSEnE'], + ['hash' => '$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$8iIuixkI73Js3G1uMbezQXD0b8LG4SXGsOwoQkdAQIM', 'expect' => false], // password is 'passwore' + ['hash' => '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc'], + ['hash' => '$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4'], + ['hash' => '$argon2id$v=19$m=256,t=2,p=2$c29tZXNhbHQ$bQk8UB/VmZZF4Oo79iDXuL5/0ttZwg2f/5U52iv1cDc'], + ['hash' => '$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQ$9qWtwbpyPd3vm1rB1GThgPzZ3/ydHL92zKL+15XZypg'], + ['hash' => '$argon2id$v=19$m=65536,t=4,p=1$c29tZXNhbHQ$kCXUjmjvc5XMqQedpMTsOv+zyJEf5PhtGiUghW9jFyw'], + ['hash' => '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$C4TWUs9rDEvq7w3+J4umqA32aWKB1+DSiRuBfYxFj94', 'password' => 'differentpassword'], + ['hash' => '$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQ$vfMrBczELrFdWP0ZsfhWsRPaHppYdP3MVEMIVlqoFBw'], + // --- END upstream test vectors --- +]; + +echo sprintf("Verifying %d valid hashes ...", count($verify)); +$ok = true; +foreach ($verify as $k => $test) { + $password = $test['password'] ?? 'password'; + // Please don't try to combine these into a single expression; it hides unexpected values upon failure + $expect = $test['expect'] ?? true; + $return = password_verify($password, $test['hash']); + if ($return !== $expect) { + $ok = false; + echo sprintf( + "\n%s returned for (index %d) %s with password '%s'", + var_export($return, true), $k, $test['hash'], $password + ); + } +} +echo ($ok) ? " OK\n" : "\n"; +echo "OK!"; +?> +--EXPECTF-- +Verifying %d valid hashes ... OK +OK! diff --git a/ext/standard/tests/password/password_verify_argon2_minimal.phpt b/ext/standard/tests/password/password_verify_argon2_minimal.phpt new file mode 100644 index 000000000000..85d531197452 --- /dev/null +++ b/ext/standard/tests/password/password_verify_argon2_minimal.phpt @@ -0,0 +1,28 @@ +--TEST-- +Test normal operation of password_verify() with Argon2i and Argon2id -- minimal +--SKIPIF-- + +--FILE-- + '$argon2i$v=19$m=12288,t=3,p=1$bzFZemY2Z2xwdDIxc0F1cg$Dsogk3TkrXQxb9+vcZb8/8/IcHy71faoZtFk9Ej76AA', 'password' => 'password'], + ['hash' => '$argon2id$v=19$m=47104,t=1,p=1$amxxbjAxZUV2NlQ3U2JzdQ$oac38S6CrOni1SC9RR5PaMiBLAyFHjkYgUAIVVVpU1k', 'password' => 'password'], +]; + +foreach ($minimal as $test) { + var_dump(password_verify($test['password'], $test['hash'])); + var_dump(password_verify(strtoupper($test['password']), $test['hash'])); +} +echo "OK!"; +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) +OK!