Skip to content

Commit 2dc37a9

Browse files
committed
Make password salt generation algo-agnostic and isolate bcrypt-specific logic
- Fixes a bug where the libargon2 implementation uses less entropy than intended (still safe, but below baseline) - Aligned bcrypt salt encoding more closely to bcrypt's variant (previously, it relied on php_crypt() doing the error correction) - Stricter bcrypt sanity checks, and slightly optimized code
1 parent 785f714 commit 2dc37a9

2 files changed

Lines changed: 61 additions & 54 deletions

File tree

‎ext/standard/password.c‎

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -46,60 +46,25 @@ void php_password_algo_unregister(const char *ident) {
4646
zend_hash_str_del(&php_password_algos, ident, strlen(ident));
4747
}
4848

49-
static int php_password_salt_to64(const char *str, const size_t str_len, const size_t out_len, char *ret) /* {{{ */
50-
{
51-
size_t pos = 0;
52-
zend_string *buffer;
53-
if ((int) str_len < 0) {
54-
return FAILURE;
55-
}
56-
buffer = php_base64_encode((unsigned char*) str, str_len);
57-
if (ZSTR_LEN(buffer) < out_len) {
58-
/* Too short of an encoded string generated */
59-
zend_string_release_ex(buffer, 0);
60-
return FAILURE;
61-
}
62-
for (pos = 0; pos < out_len; pos++) {
63-
if (ZSTR_VAL(buffer)[pos] == '+') {
64-
ret[pos] = '.';
65-
} else if (ZSTR_VAL(buffer)[pos] == '=') {
66-
zend_string_free(buffer);
67-
return FAILURE;
68-
} else {
69-
ret[pos] = ZSTR_VAL(buffer)[pos];
70-
}
71-
}
72-
zend_string_free(buffer);
73-
return SUCCESS;
74-
}
75-
/* }}} */
76-
7749
static zend_string* php_password_make_salt(size_t length) /* {{{ */
7850
{
79-
zend_string *ret, *buffer;
51+
zend_string *salt;
8052

81-
if (length > (INT_MAX / 3)) {
53+
// 64 ought to be enough for anybody
54+
if (length > 64) {
8255
zend_value_error("Length is too large to safely generate");
8356
return NULL;
8457
}
8558

86-
buffer = zend_string_alloc(length * 3 / 4 + 1, 0);
87-
if (FAILURE == php_random_bytes_throw(ZSTR_VAL(buffer), ZSTR_LEN(buffer))) {
59+
salt = zend_string_alloc(length, 0);
60+
if (FAILURE == php_random_bytes_throw(ZSTR_VAL(salt), ZSTR_LEN(salt))) {
8861
zend_value_error("Unable to generate salt");
89-
zend_string_release_ex(buffer, 0);
62+
zend_string_free(salt);
9063
return NULL;
9164
}
9265

93-
ret = zend_string_alloc(length, 0);
94-
if (php_password_salt_to64(ZSTR_VAL(buffer), ZSTR_LEN(buffer), length, ZSTR_VAL(ret)) == FAILURE) {
95-
zend_value_error("Generated salt too short");
96-
zend_string_release_ex(buffer, 0);
97-
zend_string_release_ex(ret, 0);
98-
return NULL;
99-
}
100-
zend_string_release_ex(buffer, 0);
101-
ZSTR_VAL(ret)[length] = 0;
102-
return ret;
66+
ZSTR_VAL(salt)[length] = 0;
67+
return salt;
10368
}
10469
/* }}} */
10570

@@ -113,6 +78,44 @@ static zend_string* php_password_get_salt(zval *unused_, size_t required_salt_le
11378

11479
/* bcrypt implementation */
11580

81+
static bool php_password_bcrypt_salt_to64(const char *in, const size_t in_len, const size_t out_len, char *out) /* {{{ */
82+
{
83+
if (in_len != 16 || out_len != 22) {
84+
zend_value_error("Unexpected bcrypt salt size");
85+
return false;
86+
}
87+
88+
zend_string *buffer = php_base64_encode_ex((const unsigned char*) in, in_len, PHP_BASE64_NO_PADDING);
89+
if (!buffer) {
90+
return false;
91+
}
92+
93+
if (ZSTR_LEN(buffer) != out_len || ZSTR_VAL(buffer)[out_len] == '=') {
94+
zend_string_free(buffer);
95+
return false;
96+
}
97+
98+
char *str = ZSTR_VAL(buffer);
99+
for (size_t i = 0; i < out_len; i++) {
100+
if (str[i] == '+') out[i] = '.';
101+
else out[i] = str[i];
102+
}
103+
zend_string_free(buffer);
104+
105+
// Bcrypt handles the last byte differently.
106+
// It's more complicated than this, but easy with the fixed length.
107+
switch (out[21]) {
108+
case 'A': out[21] = '.'; break;
109+
case 'Q': out[21] = 'O'; break;
110+
case 'g': out[21] = 'e'; break;
111+
case 'w': out[21] = 'u'; break;
112+
default: break;
113+
}
114+
115+
return true;
116+
}
117+
/* }}} */
118+
116119
static bool php_password_bcrypt_get_cost(const zend_string *hash, zend_long *cost) {
117120
const char *h = ZSTR_VAL(hash);
118121

@@ -185,8 +188,7 @@ static bool php_password_bcrypt_verify(const zend_string *password, const zend_s
185188
}
186189

187190
static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_array *options) {
188-
char hash_format[10];
189-
size_t hash_format_len;
191+
char salt64[22];
190192
zend_string *result, *hash, *salt;
191193
zval *zcost;
192194
zend_long cost = PHP_PASSWORD_BCRYPT_COST;
@@ -205,25 +207,26 @@ static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_a
205207
return NULL;
206208
}
207209

208-
hash_format_len = snprintf(hash_format, sizeof(hash_format), "$2y$%02" ZEND_LONG_FMT_SPEC "$", cost);
209-
if (!(salt = php_password_get_salt(NULL, Z_UL(22), options))) {
210+
if (!(salt = php_password_get_salt(NULL, Z_UL(16), options))) {
210211
return NULL;
211212
}
212-
ZSTR_VAL(salt)[ZSTR_LEN(salt)] = 0;
213213

214-
hash = zend_string_concat2(hash_format, hash_format_len, ZSTR_VAL(salt), ZSTR_LEN(salt));
214+
if (!php_password_bcrypt_salt_to64(ZSTR_VAL(salt), ZSTR_LEN(salt), sizeof(salt64), salt64)) {
215+
zend_string_free(salt);
216+
return NULL;
217+
}
215218

216-
zend_string_release_ex(salt, 0);
219+
zend_string_free(salt);
220+
hash = strpprintf(0, "$2y$%02" ZEND_LONG_FMT_SPEC "$%s", cost, salt64);
217221

218-
/* This cast is safe, since both values are defined here in code and cannot overflow */
222+
// This cast is safe, since both values are defined here in code and cannot overflow
219223
result = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1);
220-
zend_string_release_ex(hash, 0);
221-
224+
zend_string_free(hash);
222225
if (!result) {
223226
return NULL;
224227
}
225228

226-
if (ZSTR_LEN(result) < 13) {
229+
if (ZSTR_LEN(result) != 60) {
227230
zend_string_free(result);
228231
return NULL;
229232
}

‎ext/standard/tests/password/password_hash_argon2.phpt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ foreach ($algos as $algo) {
2424
var_dump(password_get_info($hash)['algo']);
2525
}
2626

27+
// Bug where libargon2 implementation unintentionally used less entropy through already base64-encoded salt
28+
var_dump(strspn(base64_decode(explode("$", $hash)[4]), "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") !== 16);
29+
2730
echo "OK!";
2831
?>
2932
--EXPECT--
@@ -39,4 +42,5 @@ bool(true)
3942
string(8) "argon2id"
4043
bool(true)
4144
string(8) "argon2id"
45+
bool(true)
4246
OK!

0 commit comments

Comments
 (0)