Skip to content

Commit 39af57c

Browse files
committed
initial implementation
1 parent 08d7fcc commit 39af57c

File tree

8 files changed

+70
-46
lines changed

8 files changed

+70
-46
lines changed

NEWS

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ PHP NEWS
138138
while COW violation flag is still set). (alexandre-daubois)
139139
. Added form feed (\f) in the default trimmed characters of trim(), rtrim()
140140
and ltrim(). (Weilin Du)
141-
. Fixed bug GH-21673 Reject NUL bytes in bcrypt passwords passed to
142-
password_verify(). (Weilin Du)
141+
. Fixed bug GH-21673 (the bundled bcrypt implementation no longer truncates
142+
passwords at NUL bytes, and password_hash()/password_verify() no longer
143+
reject them). (Weilin Du)
143144
. Invalid mode values now throw in array_filter() instead of being silently
144145
defaulted to 0. (Jorg Sowa)
145146
. Fixed bug GH-21058 (error_log() crashes with message_type 3 and

ext/standard/crypt.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const ch
129129

130130
memset(output, 0, PHP_MAX_SALT_LEN + 1);
131131

132-
crypt_res = php_crypt_blowfish_rn(password, salt, output, sizeof(output));
132+
crypt_res = php_crypt_blowfish_rn(password, pass_len, salt, output, sizeof(output));
133133
if (!crypt_res) {
134134
ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
135135
return NULL;

ext/standard/crypt_blowfish.c

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -530,10 +530,10 @@ static void BF_swap(BF_word *x, int count)
530530
*(ptr - 1) = R; \
531531
} while (ptr < &data.ctx.S[3][0xFF]);
532532

533-
static void BF_set_key(const char *key, BF_key expanded, BF_key initial,
534-
unsigned char flags)
533+
static void BF_set_key(const char *key, size_t key_len, BF_key expanded,
534+
BF_key initial, unsigned char flags)
535535
{
536-
const char *ptr = key;
536+
size_t key_pos = 0;
537537
unsigned int bug, i, j;
538538
BF_word safety, sign, diff, tmp[2];
539539

@@ -559,8 +559,7 @@ static void BF_set_key(const char *key, BF_key expanded, BF_key initial,
559559
* information - that is, we mostly use fixed-cost bitwise operations instead
560560
* of branches or table lookups. (One conditional branch based on password
561561
* length remains. It is not part of the bug aftermath, though, and is
562-
* difficult and possibly unreasonable to avoid given the use of C strings by
563-
* the caller, which results in similar timing leaks anyway.)
562+
* difficult and possibly unreasonable to avoid here.)
564563
*
565564
* For actual implementation, we set an array index in the variable "bug"
566565
* (0 means no bug, 1 means sign extension bug emulation) and a flag in the
@@ -577,13 +576,20 @@ static void BF_set_key(const char *key, BF_key expanded, BF_key initial,
577576

578577
sign = diff = 0;
579578

579+
/*
580+
* bcrypt cycles over the password bytes plus a trailing NUL terminator.
581+
* The explicit length keeps embedded NUL bytes significant while
582+
* preserving the historical behavior for ordinary C strings.
583+
*/
580584
for (i = 0; i < BF_N + 2; i++) {
581585
tmp[0] = tmp[1] = 0;
582586
for (j = 0; j < 4; j++) {
587+
unsigned char c = key_pos < key_len ? (unsigned char) key[key_pos] : 0;
588+
583589
tmp[0] <<= 8;
584-
tmp[0] |= (unsigned char)*ptr; /* correct */
590+
tmp[0] |= c; /* correct */
585591
tmp[1] <<= 8;
586-
tmp[1] |= (BF_word_signed)(signed char)*ptr; /* bug */
592+
tmp[1] |= (BF_word_signed)(signed char)c; /* bug */
587593
/*
588594
* Sign extension in the first char has no effect - nothing to overwrite yet,
589595
* and those extra 24 bits will be fully shifted out of the 32-bit word. For
@@ -592,10 +598,9 @@ static void BF_set_key(const char *key, BF_key expanded, BF_key initial,
592598
*/
593599
if (j)
594600
sign |= tmp[1] & 0x80;
595-
if (!*ptr)
596-
ptr = key;
597-
else
598-
ptr++;
601+
key_pos++;
602+
if (key_pos > key_len)
603+
key_pos = 0;
599604
}
600605
diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */
601606

@@ -636,7 +641,7 @@ static const unsigned char flags_by_subtype[26] =
636641
{2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
637642
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0};
638643

639-
static char *BF_crypt(const char *key, const char *setting,
644+
static char *BF_crypt(const char *key, size_t key_len, const char *setting,
640645
char *output, int size,
641646
BF_word min)
642647
{
@@ -679,7 +684,7 @@ static char *BF_crypt(const char *key, const char *setting,
679684
}
680685
BF_swap(data.binary.salt, 4);
681686

682-
BF_set_key(key, data.expanded_key, data.ctx.P,
687+
BF_set_key(key, key_len, data.expanded_key, data.ctx.P,
683688
flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a']);
684689

685690
memcpy(data.ctx.S, BF_init_state.S, sizeof(data.ctx.S));
@@ -800,10 +805,10 @@ static int _crypt_output_magic(const char *setting, char *output, int size)
800805
* The performance cost of this quick self-test is around 0.6% at the "$2a$08"
801806
* setting.
802807
*/
803-
char *php_crypt_blowfish_rn(const char *key, const char *setting,
804-
char *output, int size)
808+
char *php_crypt_blowfish_rn(const char *key, size_t key_len,
809+
const char *setting, char *output, int size)
805810
{
806-
const char *test_key = "8b \xd0\xc1\xd2\xcf\xcc\xd8";
811+
static const char test_key[] = "8b \xd0\xc1\xd2\xcf\xcc\xd8";
807812
const char *test_setting = "$2a$00$abcdefghijklmnopqrstuu";
808813
static const char * const test_hashes[2] =
809814
{"i1D709vfamulimlGcq0qq3UvuUasvEa\0\x55", /* 'a', 'b', 'y' */
@@ -819,7 +824,7 @@ char *php_crypt_blowfish_rn(const char *key, const char *setting,
819824

820825
/* Hash the supplied password */
821826
_crypt_output_magic(setting, output, size);
822-
retval = BF_crypt(key, setting, output, size, 16);
827+
retval = BF_crypt(key, key_len, setting, output, size, 16);
823828
save_errno = errno;
824829

825830
/*
@@ -838,17 +843,17 @@ char *php_crypt_blowfish_rn(const char *key, const char *setting,
838843
}
839844
memset(buf.o, 0x55, sizeof(buf.o));
840845
buf.o[sizeof(buf.o) - 1] = 0;
841-
p = BF_crypt(test_key, buf.s, buf.o, sizeof(buf.o) - (1 + 1), 1);
846+
p = BF_crypt(test_key, sizeof(test_key) - 1, buf.s, buf.o, sizeof(buf.o) - (1 + 1), 1);
842847

843848
ok = (p == buf.o &&
844849
!memcmp(p, buf.s, 7 + 22) &&
845850
!memcmp(p + (7 + 22), test_hash, 31 + 1 + 1 + 1));
846851

847852
{
848-
const char *k = "\xff\xa3" "34" "\xff\xff\xff\xa3" "345";
853+
static const char k[] = "\xff\xa3" "34" "\xff\xff\xff\xa3" "345";
849854
BF_key ae, ai, ye, yi;
850-
BF_set_key(k, ae, ai, 2); /* $2a$ */
851-
BF_set_key(k, ye, yi, 4); /* $2y$ */
855+
BF_set_key(k, sizeof(k) - 1, ae, ai, 2); /* $2a$ */
856+
BF_set_key(k, sizeof(k) - 1, ye, yi, 4); /* $2y$ */
852857
ai[0] ^= 0x10000; /* undo the safety (for comparison) */
853858
ok = ok && ai[0] == 0xdb9c59bc && ye[17] == 0x33343500 &&
854859
!memcmp(ae, ye, sizeof(ae)) &&

ext/standard/crypt_blowfish.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
#ifndef _CRYPT_BLOWFISH_H
1818
#define _CRYPT_BLOWFISH_H
1919

20-
extern char *php_crypt_blowfish_rn(const char *key, const char *setting,
21-
char *output, int size);
20+
#include <stddef.h>
21+
22+
extern char *php_crypt_blowfish_rn(const char *key, size_t key_len,
23+
const char *setting, char *output, int size);
2224

2325
#endif

ext/standard/password.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,6 @@ static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_a
181181
zval *zcost;
182182
zend_long cost = PHP_PASSWORD_BCRYPT_COST;
183183

184-
if (zend_str_has_nul_byte(password)) {
185-
zend_value_error("Bcrypt password must not contain null character");
186-
return NULL;
187-
}
188-
189184
if (options && (zcost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) {
190185
cost = zval_get_long(zcost);
191186
}
@@ -620,10 +615,6 @@ PHP_FUNCTION(password_verify)
620615
ZEND_PARSE_PARAMETERS_END();
621616

622617
algo = php_password_algo_identify(hash);
623-
if (algo == &php_password_algo_bcrypt && zend_str_has_nul_byte(password)) {
624-
RETURN_FALSE;
625-
}
626-
627618
RETURN_BOOL(algo && (!algo->verify || algo->verify(password, hash)));
628619
}
629620
/* }}} */

ext/standard/tests/password/password_bcrypt_errors.phpt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,7 @@ try {
1414
} catch (ValueError $exception) {
1515
echo $exception->getMessage() . "\n";
1616
}
17-
18-
try {
19-
var_dump(password_hash("null\0password", PASSWORD_BCRYPT));
20-
} catch (ValueError $e) {
21-
echo $e->getMessage(), "\n";
22-
}
2317
?>
2418
--EXPECT--
2519
Invalid bcrypt cost parameter specified: 3
2620
Invalid bcrypt cost parameter specified: 32
27-
Bcrypt password must not contain null character
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
--TEST--
2-
password_verify() rejects bcrypt passwords containing null bytes
2+
password_* handles bcrypt passwords containing null bytes
3+
--SKIPIF--
4+
<?php
5+
$setting = '$2y$05$CCCCCCCCCCCCCCCCCCCCC.';
6+
if (crypt("foo\0bar", $setting) === crypt("foo", $setting)) {
7+
die("skip bcrypt backend truncates NUL bytes");
8+
}
9+
?>
310
--FILE--
411
<?php
5-
$hash = password_hash("foo", PASSWORD_BCRYPT);
12+
$password = "foo\0bar";
13+
$hash = password_hash($password, PASSWORD_BCRYPT);
614

15+
var_dump($hash !== false);
16+
var_dump(password_verify($password, $hash));
717
var_dump(password_verify("foo", $hash));
8-
var_dump(password_verify("foo\0bar", $hash));
9-
var_dump(password_verify("\0foo", $hash));
18+
var_dump(password_verify("foo\0baz", $hash));
1019
?>
1120
--EXPECT--
1221
bool(true)
22+
bool(true)
1323
bool(false)
1424
bool(false)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
crypt() bcrypt distinguishes passwords containing null bytes
3+
--SKIPIF--
4+
<?php
5+
$setting = '$2y$05$CCCCCCCCCCCCCCCCCCCCC.';
6+
if (crypt("foo\0bar", $setting) === crypt("foo", $setting)) {
7+
die("skip bcrypt backend truncates NUL bytes");
8+
}
9+
?>
10+
--FILE--
11+
<?php
12+
$setting = '$2y$05$CCCCCCCCCCCCCCCCCCCCC.';
13+
$hash = crypt("foo\0bar", $setting);
14+
15+
var_dump($hash === crypt("foo\0bar", $hash));
16+
var_dump($hash === crypt("foo", $hash));
17+
var_dump($hash === crypt("foo\0baz", $hash));
18+
?>
19+
--EXPECT--
20+
bool(true)
21+
bool(false)
22+
bool(false)

0 commit comments

Comments
 (0)