diff --git a/ext/hash/hash.c b/ext/hash/hash.c index a950d824700f..ad0ea14cb3c8 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -487,13 +487,32 @@ static inline void php_hash_hmac_round(unsigned char *final, const php_hash_ops ops->hash_final(final, context); } +/* Computes HMAC(key, data) using the given hash algorithm and writes the + * result into `digest`, which must be at least ops->digest_size bytes. + * Intended for internal (C-level) use by other extensions; avoids the + * zval/zend_string overhead of the userland hash_hmac() API. */ +PHPAPI void php_hash_hmac(const php_hash_ops *ops, const unsigned char *key, size_t key_len, + const unsigned char *data, size_t data_len, unsigned char *digest) +{ + void *context = php_hash_alloc_context(ops); + unsigned char *K = emalloc(ops->block_size); + + php_hash_hmac_prep_key(K, ops, context, key, key_len); + php_hash_hmac_round(digest, ops, context, K, data, data_len); + + php_hash_string_xor_char(K, K, 0x6A, ops->block_size); + php_hash_hmac_round(digest, ops, context, K, digest, ops->digest_size); + + ZEND_SECURE_ZERO(K, ops->block_size); + efree(K); + php_hash_free_context(ops, context); +} + static void php_hash_do_hash_hmac( zval *return_value, zend_string *algo, char *data, size_t data_len, char *key, size_t key_len, bool raw_output, bool isfilename ) /* {{{ */ { zend_string *digest; - unsigned char *K; const php_hash_ops *ops; - void *context; php_stream *stream = NULL; ops = php_hash_fetch_ops(algo); @@ -514,16 +533,16 @@ static void php_hash_do_hash_hmac( } } - context = php_hash_alloc_context(ops); - - K = emalloc(ops->block_size); digest = zend_string_alloc(ops->digest_size, 0); - php_hash_hmac_prep_key(K, ops, context, (unsigned char *) key, key_len); - if (isfilename) { + void *context = php_hash_alloc_context(ops); + unsigned char *K = emalloc(ops->block_size); char buf[1024]; ssize_t n; + + php_hash_hmac_prep_key(K, ops, context, (unsigned char *) key, key_len); + ops->hash_init(context, NULL); ops->hash_update(context, K, ops->block_size); while ((n = php_stream_read(stream, buf, sizeof(buf))) > 0) { @@ -536,20 +555,17 @@ static void php_hash_do_hash_hmac( zend_string_efree(digest); RETURN_FALSE; } - ops->hash_final((unsigned char *) ZSTR_VAL(digest), context); - } else { - php_hash_hmac_round((unsigned char *) ZSTR_VAL(digest), ops, context, K, (unsigned char *) data, data_len); - } - - php_hash_string_xor_char(K, K, 0x6A, ops->block_size); - php_hash_hmac_round((unsigned char *) ZSTR_VAL(digest), ops, context, K, (unsigned char *) ZSTR_VAL(digest), ops->digest_size); + php_hash_string_xor_char(K, K, 0x6A, ops->block_size); + php_hash_hmac_round((unsigned char *) ZSTR_VAL(digest), ops, context, K, (unsigned char *) ZSTR_VAL(digest), ops->digest_size); - /* Zero the key */ - ZEND_SECURE_ZERO(K, ops->block_size); - efree(K); - php_hash_free_context(ops, context); + ZEND_SECURE_ZERO(K, ops->block_size); + efree(K); + php_hash_free_context(ops, context); + } else { + php_hash_hmac(ops, (unsigned char *) key, key_len, (unsigned char *) data, data_len, (unsigned char *) ZSTR_VAL(digest)); + } if (raw_output) { ZSTR_VAL(digest)[ops->digest_size] = 0; diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h index 69902f1484ed..a6656acc1bfa 100644 --- a/ext/hash/php_hash.h +++ b/ext/hash/php_hash.h @@ -159,6 +159,8 @@ PHP_HASH_API hash_spec_result php_hash_serialize(const php_hashcontext_object *c PHP_HASH_API hash_spec_result php_hash_unserialize(php_hashcontext_object *context, zend_long magic, const zval *zv); PHP_HASH_API hash_spec_result php_hash_serialize_spec(const php_hashcontext_object *context, zval *zv, const char *spec); PHP_HASH_API hash_spec_result php_hash_unserialize_spec(php_hashcontext_object *hash, const zval *zv, const char *spec); +PHP_HASH_API void php_hash_hmac(const php_hash_ops *ops, const unsigned char *key, size_t key_len, + const unsigned char *data, size_t data_len, unsigned char *digest); static inline void *php_hash_alloc_context(const php_hash_ops *ops) { /* Zero out context memory so serialization doesn't expose internals */ diff --git a/ext/hash/php_hash_sha.h b/ext/hash/php_hash_sha.h index e2c6eac30b4c..5dd39d14c006 100644 --- a/ext/hash/php_hash_sha.h +++ b/ext/hash/php_hash_sha.h @@ -103,4 +103,6 @@ PHP_HASH_API void PHP_SHA512_224InitArgs(PHP_SHA512_CTX *, ZEND_ATTRIBUTE_UNUSED #define PHP_SHA512_224Update PHP_SHA512Update PHP_HASH_API void PHP_SHA512_224Final(unsigned char[28], PHP_SHA512_CTX *); +extern PHP_HASH_API const php_hash_ops php_hash_sha256_ops; + #endif /* PHP_HASH_SHA_H */ diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index 7edfcf1da1f5..6a534ea6317f 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -457,5 +457,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([ PHP_ADD_BUILD_DIR([$ext_builddir/libavifinfo]) +PHP_ADD_EXTENSION_DEP([standard], [hash]) + PHP_ADD_MAKEFILE_FRAGMENT PHP_INSTALL_HEADERS([ext/standard/]) diff --git a/ext/standard/password.c b/ext/standard/password.c index a28ceb7e0ced..f2790c26a185 100644 --- a/ext/standard/password.c +++ b/ext/standard/password.c @@ -24,6 +24,8 @@ #include "zend_interfaces.h" #include "info.h" #include "ext/random/php_random_csprng.h" +#include "ext/hash/php_hash.h" +#include "ext/hash/php_hash_sha.h" #include "password_arginfo.h" #ifdef HAVE_ARGON2LIB #include "argon2.h" @@ -228,6 +230,254 @@ const php_password_algo php_password_algo_bcrypt = { php_password_bcrypt_valid, }; +/* bcrypt-sha256 implementation. + * + * Plain bcrypt truncates passwords at 72 bytes and, on some implementations, + * at the first NUL byte. To avoid both quirks, the password is first run + * through HMAC-SHA256 keyed with the salt; the 32-byte digest is base64 + * encoded (44 ASCII bytes, no NUL, well under 72) and *that* is what gets + * bcrypt hashed. This mirrors passlib's bcrypt_sha256 (format version 2). + * SHA256 is taken from the hash extension. */ + +#define PHP_PASSWORD_BCRYPT_SHA256_PREFIX "$bcrypt-sha256$v=2,t=2b,r=" + +static bool php_password_b64char(unsigned char c) +{ + return c == '.' || c == '/' || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9'); +} + +/* Validate a bcrypt-sha256 hash and, on success, extract its cost, salt and + * digest. Layout: + * $bcrypt-sha256$v=2,t=2b,r=$$ (82 or 83 bytes) + */ +static bool php_password_bcrypt_sha256_parse(const zend_string *hash, + zend_long *cost, const char **salt, const char **digest) +{ + if (!zend_string_starts_with_literal(hash, PHP_PASSWORD_BCRYPT_SHA256_PREFIX)) { + return false; + } + + const char *p; + int c; + { + const char *h = ZSTR_VAL(hash); + size_t len = ZSTR_LEN(hash); + + if (len < 82 || len > 83) { + return false; + } + p = h + strlen(PHP_PASSWORD_BCRYPT_SHA256_PREFIX); + + if (*p < '1' || *p > '9') { + return false; + } + c = *p - '0'; + p++; + if (*p >= '0' && *p <= '9') { + c = c * 10 + (*p - '0'); + p++; + } + if (c < 4 || c > 31 || *p != '$') { + return false; + } + p++; + + if (len - (size_t)(p - h) != 54) { + return false; + } + } + + for (size_t i = 0; i < 22; i++) { + if (!php_password_b64char((unsigned char) p[i])) { + return false; + } + } + if (p[22] != '$') { + return false; + } + for (size_t i = 0; i < 31; i++) { + if (!php_password_b64char((unsigned char) p[23 + i])) { + return false; + } + } + + *cost = c; + *salt = p; + *digest = p + 23; + return true; +} + +static bool php_password_bcrypt_sha256_valid(const zend_string *hash) +{ + zend_long cost; + const char *salt, *digest; + return php_password_bcrypt_sha256_parse(hash, &cost, &salt, &digest); +} + +static int php_password_bcrypt_sha256_get_info(zval *return_value, const zend_string *hash) +{ + zend_long cost; + const char *salt, *digest; + + if (!php_password_bcrypt_sha256_parse(hash, &cost, &salt, &digest)) { + return FAILURE; + } + add_assoc_long(return_value, "cost", cost); + return SUCCESS; +} + +static bool php_password_bcrypt_sha256_needs_rehash(const zend_string *hash, zend_array *options) +{ + zend_long cost; + const char *salt, *digest; + + if (!php_password_bcrypt_sha256_parse(hash, &cost, &salt, &digest)) { + return true; + } + + zend_long new_cost = PHP_PASSWORD_BCRYPT_SHA256_COST; + if (options) { + zval *znew_cost = zend_hash_str_find(options, "cost", strlen("cost")); + if (znew_cost != NULL) { + new_cost = zval_get_long(znew_cost); + } + } + + return cost != new_cost; +} + +static zend_string *php_password_bcrypt_sha256_hash(const zend_string *password, zend_array *options) +{ + zend_long cost = PHP_PASSWORD_BCRYPT_SHA256_COST; + + if (options) { + zval *zcost = zend_hash_str_find(options, "cost", strlen("cost")); + if (zcost != NULL) { + cost = zval_get_long(zcost); + } + } + if (cost < 4 || cost > 31) { + zend_value_error("Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost); + return NULL; + } + + zend_string *salt = php_password_get_salt(NULL, 22, options); + if (!salt) { + return NULL; + } + + /* Pre-hash with HMAC-SHA256 (key = the 22-char salt as ASCII bytes) and + * base64-encode the 32-byte digest. The resulting 44-byte string is what is + * handed to bcrypt, so any NUL bytes or length beyond 72 in the original + * password are neutralized. */ + zend_string *key; + { + unsigned char mac[32]; + php_hash_hmac(&php_hash_sha256_ops, (const unsigned char *) ZSTR_VAL(salt), ZSTR_LEN(salt), + (const unsigned char *) ZSTR_VAL(password), ZSTR_LEN(password), mac); + + key = php_base64_encode(mac, sizeof(mac)); + ZEND_SECURE_ZERO(mac, sizeof(mac)); + } + + zend_string *setting; + { + char setting_prefix[16]; + size_t prefix_len = snprintf(setting_prefix, sizeof(setting_prefix), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost); + setting = zend_string_concat2(setting_prefix, prefix_len, ZSTR_VAL(salt), ZSTR_LEN(salt)); + } + + zend_string *raw = php_crypt(ZSTR_VAL(key), (int) ZSTR_LEN(key), ZSTR_VAL(setting), (int) ZSTR_LEN(setting), 1); + zend_string_release_ex(setting, 0); + + ZEND_SECURE_ZERO(ZSTR_VAL(key), ZSTR_LEN(key)); + zend_string_release_ex(key, 0); + + if (!raw || ZSTR_LEN(raw) < 60) { + if (raw) { + zend_string_free(raw); + } + zend_string_release_ex(salt, 0); + return NULL; + } + + /* Relabel the $2y$ result into the bcrypt-sha256 format. The digest is the + * last 31 characters of the 60-byte bcrypt output. */ + zend_string *result = zend_strpprintf(0, "$bcrypt-sha256$v=2,t=2b,r=%" ZEND_LONG_FMT_SPEC "$%s$%s", + cost, ZSTR_VAL(salt), ZSTR_VAL(raw) + (ZSTR_LEN(raw) - 31)); + zend_string_release_ex(salt, 0); + zend_string_free(raw); + + return result; +} + +static bool php_password_bcrypt_sha256_verify(const zend_string *password, const zend_string *hash) +{ + zend_long cost; + const char *salt, *digest; + + if (!php_password_bcrypt_sha256_parse(hash, &cost, &salt, &digest)) { + return false; + } + + zend_string *key; + { + unsigned char mac[32]; + php_hash_hmac(&php_hash_sha256_ops, (const unsigned char *) salt, 22, + (const unsigned char *) ZSTR_VAL(password), ZSTR_LEN(password), mac); + + key = php_base64_encode(mac, sizeof(mac)); + ZEND_SECURE_ZERO(mac, sizeof(mac)); + } + + zend_string *setting; + { + char setting_prefix[16]; + size_t prefix_len = snprintf(setting_prefix, sizeof(setting_prefix), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost); + setting = zend_string_concat2(setting_prefix, prefix_len, salt, 22); + } + + zend_string *raw = php_crypt(ZSTR_VAL(key), (int) ZSTR_LEN(key), ZSTR_VAL(setting), (int) ZSTR_LEN(setting), 1); + zend_string_release_ex(setting, 0); + + ZEND_SECURE_ZERO(ZSTR_VAL(key), ZSTR_LEN(key)); + zend_string_release_ex(key, 0); + + if (!raw || ZSTR_LEN(raw) < 60) { + if (raw) { + zend_string_free(raw); + } + return false; + } + + /* Constant-time comparison of the 31-byte digests. The salt portion of the + * bcrypt output may differ from the stored salt (bcrypt re-encodes the 128-bit + * salt into 22 base64 chars, and the 4 unused padding bits can change the last + * character), but both encodings decode to the same salt bytes, so the digest + * is identical. */ + zend_string *raw_digest = zend_string_init(ZSTR_VAL(raw) + (ZSTR_LEN(raw) - 31), 31, 0); + zend_string *stored_digest = zend_string_init(digest, 31, 0); + + bool ret = (php_safe_bcmp(raw_digest, stored_digest) == 0); + + zend_string_release_ex(raw_digest, 0); + zend_string_release_ex(stored_digest, 0); + zend_string_free(raw); + + return ret; +} + +const php_password_algo php_password_algo_bcrypt_sha256 = { + "bcrypt-sha256", + php_password_bcrypt_sha256_hash, + php_password_bcrypt_sha256_verify, + php_password_bcrypt_sha256_needs_rehash, + php_password_bcrypt_sha256_get_info, + php_password_bcrypt_sha256_valid, +}; #ifdef HAVE_ARGON2LIB /* argon2i/argon2id shared implementation */ @@ -425,6 +675,10 @@ PHP_MINIT_FUNCTION(password) /* {{{ */ return FAILURE; } + if (FAILURE == php_password_algo_register("bcrypt-sha256", &php_password_algo_bcrypt_sha256)) { + return FAILURE; + } + #ifdef HAVE_ARGON2LIB if (FAILURE == php_password_algo_register("argon2i", &php_password_algo_argon2i)) { return FAILURE; diff --git a/ext/standard/password.stub.php b/ext/standard/password.stub.php index b4758ef54b12..d9e02cc6b392 100644 --- a/ext/standard/password.stub.php +++ b/ext/standard/password.stub.php @@ -9,6 +9,12 @@ * @cvalue PHP_PASSWORD_BCRYPT_COST */ const PASSWORD_BCRYPT_DEFAULT_COST = UNKNOWN; +const PASSWORD_BCRYPT_SHA256 = "bcrypt-sha256"; +/** + * @var int + * @cvalue PHP_PASSWORD_BCRYPT_SHA256_COST + */ +const PASSWORD_BCRYPT_SHA256_DEFAULT_COST = UNKNOWN; #ifdef HAVE_ARGON2LIB const PASSWORD_ARGON2I = "argon2i"; diff --git a/ext/standard/password_arginfo.h b/ext/standard/password_arginfo.h index 2a7b19f6108a..a865953eb85d 100644 --- a/ext/standard/password_arginfo.h +++ b/ext/standard/password_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit password.stub.php instead. - * Stub hash: f61df8d477588718e0eb1b055e5a3e138e6bcad3 */ + * Stub hash: 98c86e35c451499dcfad85d839af20cfe03f7930 */ #include "zend_constants.h" @@ -8,6 +8,8 @@ static void register_password_symbols(int module_number) REGISTER_STRING_CONSTANT("PASSWORD_DEFAULT", "2y", CONST_PERSISTENT); REGISTER_STRING_CONSTANT("PASSWORD_BCRYPT", "2y", CONST_PERSISTENT); REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_DEFAULT_COST", PHP_PASSWORD_BCRYPT_COST, CONST_PERSISTENT); + REGISTER_STRING_CONSTANT("PASSWORD_BCRYPT_SHA256", "bcrypt-sha256", CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("PASSWORD_BCRYPT_SHA256_DEFAULT_COST", PHP_PASSWORD_BCRYPT_SHA256_COST, CONST_PERSISTENT); #if defined(HAVE_ARGON2LIB) REGISTER_STRING_CONSTANT("PASSWORD_ARGON2I", "argon2i", CONST_PERSISTENT); REGISTER_STRING_CONSTANT("PASSWORD_ARGON2ID", "argon2id", CONST_PERSISTENT); diff --git a/ext/standard/php_password.h b/ext/standard/php_password.h index 6442b782c256..f682511ffaf3 100644 --- a/ext/standard/php_password.h +++ b/ext/standard/php_password.h @@ -21,6 +21,7 @@ PHP_MSHUTDOWN_FUNCTION(password); #define PHP_PASSWORD_DEFAULT PHP_PASSWORD_BCRYPT #define PHP_PASSWORD_BCRYPT_COST 12 +#define PHP_PASSWORD_BCRYPT_SHA256_COST PHP_PASSWORD_BCRYPT_COST #ifdef HAVE_ARGON2LIB /** @@ -44,6 +45,7 @@ typedef struct _php_password_algo { } php_password_algo; extern const php_password_algo php_password_algo_bcrypt; +extern const php_password_algo php_password_algo_bcrypt_sha256; #ifdef HAVE_ARGON2LIB extern const php_password_algo php_password_algo_argon2i; extern const php_password_algo php_password_algo_argon2id; diff --git a/ext/standard/tests/password/password_bcrypt_sha256_errors.phpt b/ext/standard/tests/password/password_bcrypt_sha256_errors.phpt new file mode 100644 index 000000000000..987903c00806 --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_errors.phpt @@ -0,0 +1,23 @@ +--TEST-- +Test error operation of password_hash() with bcrypt-sha256 +--FILE-- + 3]); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +try { + password_hash("foo", PASSWORD_BCRYPT_SHA256, ["cost" => 32]); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +echo "OK!"; +?> +--EXPECT-- +ValueError: Invalid bcrypt cost parameter specified: 3 +ValueError: Invalid bcrypt cost parameter specified: 32 +OK! diff --git a/ext/standard/tests/password/password_bcrypt_sha256_get_info.phpt b/ext/standard/tests/password/password_bcrypt_sha256_get_info.phpt new file mode 100644 index 000000000000..fe1e7f9d8696 --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_get_info.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test password_get_info() with bcrypt-sha256 +--FILE-- + 4]))); + +// Malformed hash is reported as unknown +var_dump(password_get_info('$bcrypt-sha256$v=2,t=2b,r=12$bad-salt!!$Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2')); + +echo "OK!"; +?> +--EXPECT-- +array(3) { + ["algo"]=> + string(13) "bcrypt-sha256" + ["algoName"]=> + string(13) "bcrypt-sha256" + ["options"]=> + array(1) { + ["cost"]=> + int(12) + } +} +array(3) { + ["algo"]=> + string(13) "bcrypt-sha256" + ["algoName"]=> + string(13) "bcrypt-sha256" + ["options"]=> + array(1) { + ["cost"]=> + int(4) + } +} +array(3) { + ["algo"]=> + NULL + ["algoName"]=> + string(7) "unknown" + ["options"]=> + array(0) { + } +} +OK! diff --git a/ext/standard/tests/password/password_bcrypt_sha256_hash.phpt b/ext/standard/tests/password/password_bcrypt_sha256_hash.phpt new file mode 100644 index 000000000000..b1f28db3ea3d --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_hash.phpt @@ -0,0 +1,21 @@ +--TEST-- +Test normal operation of password_hash() with bcrypt-sha256 +--FILE-- + 4]); +var_dump(preg_match('#^\$bcrypt-sha256\$v=2,t=2b,r=4\$[./A-Za-z0-9]{22}\$[./A-Za-z0-9]{31}$#', $h4) === 1); +var_dump(password_verify("foo", $h4)); +var_dump(password_verify("bar", $h4)); + +// New algorithm is advertised by password_algos() +var_dump(in_array(PASSWORD_BCRYPT_SHA256, password_algos())); + +echo "OK!"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(true) +OK! diff --git a/ext/standard/tests/password/password_bcrypt_sha256_hash_slow.phpt b/ext/standard/tests/password/password_bcrypt_sha256_hash_slow.phpt new file mode 100644 index 000000000000..9831e9d1cfc7 --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_hash_slow.phpt @@ -0,0 +1,21 @@ +--TEST-- +Test normal operation of password_hash() with bcrypt-sha256 +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(false) +OK! diff --git a/ext/standard/tests/password/password_bcrypt_sha256_needs_rehash.phpt b/ext/standard/tests/password/password_bcrypt_sha256_needs_rehash.phpt new file mode 100644 index 000000000000..ebb18d11769c --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_needs_rehash.phpt @@ -0,0 +1,25 @@ +--TEST-- +Test password_needs_rehash() with bcrypt-sha256 +--FILE-- + 5]); + +// Same cost -> no rehash +var_dump(password_needs_rehash($h5, PASSWORD_BCRYPT_SHA256, ["cost" => 5])); +// Different cost -> rehash +var_dump(password_needs_rehash($h5, PASSWORD_BCRYPT_SHA256, ["cost" => 8])); +// Different algorithm -> rehash +var_dump(password_needs_rehash($h5, PASSWORD_BCRYPT)); +// Unrecognized hash -> rehash +var_dump(password_needs_rehash("", PASSWORD_BCRYPT_SHA256)); + +echo "OK!"; +?> +--EXPECT-- +bool(false) +bool(true) +bool(true) +bool(true) +OK! diff --git a/ext/standard/tests/password/password_bcrypt_sha256_passlib.phpt b/ext/standard/tests/password/password_bcrypt_sha256_passlib.phpt new file mode 100644 index 000000000000..c7537dc2408c --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_passlib.phpt @@ -0,0 +1,120 @@ +--TEST-- +Test password_verify() with bcrypt-sha256 using passlib test vectors (v2 format) +--FILE-- + '$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe', + + # ascii + "password" => '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # unicode / utf8 + $upass => '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6', + + # test >72 chars is hashed correctly -- under bcrypt these hash the same. + str_repeat('abc123', 12) => '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue', + str_repeat('abc123', 12) . 'qwr' => '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.', + str_repeat('abc123', 12) . 'xyz' => '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S', +]; + +$bad = [ + # bad char in otherwise correct hash + # \/ + '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # unsupported version (for this format) + '$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # unrecognized version + '$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # unrecognized bcrypt variant + '$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # unsupported bcrypt variant + '$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + '$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # rounds zero-padded + '$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS', + + # config string w/ $ added + '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$', +]; + +echo "# Good hashes\n"; +foreach ($good as $password => $hash) { + var_dump(password_verify($password, $hash)); + var_dump(password_verify($password . "x", $hash)); +} + +echo "# Bad hashes\n"; +foreach ($bad as $hash) { + var_dump(password_verify('password', $hash)); +} + +?> +--EXPECT-- +# Good hashes +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +# Bad hashes +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) diff --git a/ext/standard/tests/password/password_bcrypt_sha256_verify.phpt b/ext/standard/tests/password/password_bcrypt_sha256_verify.phpt new file mode 100644 index 000000000000..8ccd709e8283 --- /dev/null +++ b/ext/standard/tests/password/password_bcrypt_sha256_verify.phpt @@ -0,0 +1,53 @@ +--TEST-- +Test password_verify() with bcrypt-sha256 (reference interop + edge cases) +--FILE-- + 5]); +var_dump(password_verify($long, $hl)); // true +var_dump(password_verify(str_repeat("a", 199), $hl)); // false (no 72-byte truncation) + +// A NUL byte in the password is allowed (the pre-hash neutralizes the NUL quirk). +$nul = "foo\x00bar"; +$hn = password_hash($nul, PASSWORD_BCRYPT_SHA256, ['cost' => 5]); +var_dump(password_verify($nul, $hn)); // true +var_dump(password_verify("foo\x00baz", $hn)); // false + +// A malformed hash (invalid characters in the salt) is rejected. +var_dump(password_verify("password", '$bcrypt-sha256$v=2,t=2b,r=12$bad-salt!!$Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2')); + +echo "OK!"; +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(false) +OK!