Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 34 additions & 18 deletions ext/hash/hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions ext/hash/php_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 2 additions & 0 deletions ext/hash/php_hash_sha.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
2 changes: 2 additions & 0 deletions ext/standard/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -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/])
254 changes: 254 additions & 0 deletions ext/standard/password.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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=<cost>$<salt:22>$<digest:31> (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
Comment thread
Sjord marked this conversation as resolved.
* 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 */
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions ext/standard/password.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading
Loading