Skip to content

Commit 5ca25c2

Browse files
committed
Add Yescrypt support to crypt() and password API
1 parent 56bd057 commit 5ca25c2

25 files changed

+4086
-5
lines changed

README.REDIST.BINS

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
19. xxHash (ext/hash/xxhash)
2020
20. Lexbor (ext/dom/lexbor/lexbor) see ext/dom/lexbor/LICENSE
2121
21. Portions of libcperciva (ext/hash/hash_sha_{ni,sse2}.c) see the header in the source file
22+
22. yescrypt (ext/standard/yescrypt) see the header in the source files
2223

2324
3. pcre2lib (ext/pcre)
2425

ext/standard/config.m4

+33-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ AS_VAR_IF([PHP_EXTERNAL_LIBCRYPT], [no], [
8989
crypt_sha256.c
9090
crypt_sha512.c
9191
php_crypt_r.c
92+
yescrypt/yescrypt-opt.c
93+
yescrypt/yescrypt-common.c
94+
yescrypt/sha256.c
9295
"])
9396
], [
9497
AC_SEARCH_LIBS([crypt], [crypt],
@@ -206,6 +209,33 @@ int main(void) {
206209
[ac_cv_crypt_blowfish=no],
207210
[ac_cv_crypt_blowfish=no])])
208211
212+
AC_CACHE_CHECK([for Yescrypt crypt], [ac_cv_crypt_yescrypt],
213+
[AC_RUN_IFELSE([AC_LANG_SOURCE([[
214+
#ifdef HAVE_UNISTD_H
215+
#include <unistd.h>
216+
#endif
217+
218+
#ifdef HAVE_CRYPT_H
219+
#include <crypt.h>
220+
#endif
221+
222+
#include <stdlib.h>
223+
#include <string.h>
224+
225+
int main(void) {
226+
char answer[128];
227+
char *encrypted;
228+
char salt[] = "\$y\$j9T\$fFqB7ZKMpdoOep2IXlKMuBnGplYOF/\$";
229+
230+
strcpy(answer, salt);
231+
strcpy(&answer[sizeof(salt) - 1], "YUbFz9cPA2OISKzl1FhXHQP556fm3v7K1PBuIcVwyL/");
232+
encrypted = crypt("rasmuslerdorf", salt);
233+
return !encrypted || strcmp(encrypted, answer);
234+
}]])],
235+
[ac_cv_crypt_yescrypt=yes],
236+
[ac_cv_crypt_yescrypt=no],
237+
[ac_cv_crypt_yescrypt=no])])
238+
209239
AC_CACHE_CHECK([for SHA512 crypt], [ac_cv_crypt_sha512],
210240
[AC_RUN_IFELSE([AC_LANG_SOURCE([[
211241
#ifdef HAVE_UNISTD_H
@@ -260,7 +290,7 @@ int main(void) {
260290
[ac_cv_crypt_sha256=no],
261291
[ac_cv_crypt_sha256=no])])
262292
263-
if test "$ac_cv_crypt_blowfish" = "no" || test "$ac_cv_crypt_des" = "no" || test "$ac_cv_crypt_ext_des" = "no" || test "$ac_cv_crypt_md5" = "no" || test "$ac_cv_crypt_sha512" = "no" || test "$ac_cv_crypt_sha256" = "no"; then
293+
if test "$ac_cv_crypt_blowfish" = "no" || test "$ac_cv_crypt_yescrypt" = "no" || test "$ac_cv_crypt_des" = "no" || test "$ac_cv_crypt_ext_des" = "no" || test "$ac_cv_crypt_md5" = "no" || test "$ac_cv_crypt_sha512" = "no" || test "$ac_cv_crypt_sha256" = "no"; then
264294
AC_MSG_FAILURE([Cannot use external libcrypt as some algo are missing.])
265295
fi
266296
@@ -396,6 +426,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([
396426
crc32.c
397427
credits.c
398428
crypt.c
429+
yescrypt/yescrypt-config.c
399430
css.c
400431
datetime.c
401432
dir.c
@@ -456,6 +487,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([
456487
[-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1])
457488

458489
PHP_ADD_BUILD_DIR([$ext_builddir/libavifinfo])
490+
PHP_ADD_BUILD_DIR([$ext_builddir/yescrypt])
459491

460492
PHP_ADD_MAKEFILE_FRAGMENT
461493
PHP_INSTALL_HEADERS([ext/standard/])

ext/standard/config.w32

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \
3838
streamsfuncs.c http.c flock_compat.c hrtime.c", false /* never shared */,
3939
'/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');
4040
ADD_SOURCES("ext/standard/libavifinfo", "avifinfo.c", "standard");
41+
ADD_SOURCES("ext/standard/yescrypt", "yescrypt-opt.c yescrypt-common.c yescrypt-config.c sha256.c", "standard");
4142
PHP_STANDARD = "yes";
4243
ADD_MAKEFILE_FRAGMENT();
4344
PHP_INSTALL_HEADERS("", "ext/standard");

ext/standard/crypt.c

+41
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#if PHP_USE_PHP_CRYPT_R
2828
# include "php_crypt_r.h"
2929
# include "crypt_freesec.h"
30+
# include "yescrypt/yescrypt.h"
3031
#else
3132
# ifdef HAVE_CRYPT_H
3233
# if defined(CRYPT_R_GNU_SOURCE) && !defined(_GNU_SOURCE)
@@ -76,6 +77,19 @@ PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const ch
7677
return NULL;
7778
}
7879

80+
if (salt[0] == '$' && (salt[1] == 'y' || salt[1] == '7') && salt[2] == '$') {
81+
/* Reference yescrypt can handle NUL bytes in the password, but sytem crypt cannot.
82+
* Return NULL for both cases for consistency. */
83+
if (zend_char_has_nul_byte(password, (size_t) pass_len)) {
84+
return NULL;
85+
}
86+
87+
/* Neither reference yescrypt nor system crypt can handle NUL bytes in the salt. */
88+
if (zend_char_has_nul_byte(salt, (size_t) salt_len)) {
89+
return NULL;
90+
}
91+
}
92+
7993
/* Windows (win32/crypt) has a stripped down version of libxcrypt and
8094
a CryptoApi md5_crypt implementation */
8195
#if PHP_USE_PHP_CRYPT_R
@@ -138,6 +152,33 @@ PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const ch
138152
ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
139153
return result;
140154
}
155+
} else if (salt[0] == '$' && (salt[1] == 'y' || salt[1] == '7') && salt[2] == '$') {
156+
yescrypt_local_t local;
157+
uint8_t buf[PREFIX_LEN + 1 + HASH_LEN + 1]; /* prefix, '$', hash, NUL */
158+
159+
if (yescrypt_init_local(&local)) {
160+
return NULL;
161+
}
162+
163+
uint8_t *hash = yescrypt_r(
164+
NULL,
165+
&local,
166+
(const uint8_t *) password,
167+
(size_t) pass_len,
168+
(const uint8_t *) salt,
169+
NULL /* no key */,
170+
buf,
171+
sizeof(buf)
172+
);
173+
174+
if (yescrypt_free_local(&local) || !hash) {
175+
ZEND_SECURE_ZERO(buf, sizeof(buf));
176+
return NULL;
177+
}
178+
179+
result = zend_string_init((const char *) hash, strlen((const char *) hash), false);
180+
ZEND_SECURE_ZERO(buf, sizeof(buf));
181+
return result;
141182
} else if (salt[0] == '_'
142183
|| (IS_VALID_SALT_CHARACTER(salt[0]) && IS_VALID_SALT_CHARACTER(salt[1]))) {
143184
/* DES Fallback */

ext/standard/password.c

+211-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#ifdef HAVE_ARGON2LIB
3131
#include "argon2.h"
3232
#endif
33+
#include "yescrypt/yescrypt.h"
3334

3435
#ifdef PHP_WIN32
3536
#include "win32/winutil.h"
@@ -151,7 +152,8 @@ static bool php_password_bcrypt_needs_rehash(const zend_string *hash, zend_array
151152
return old_cost != new_cost;
152153
}
153154

154-
static bool php_password_bcrypt_verify(const zend_string *password, const zend_string *hash) {
155+
/* Password verification using the crypt() API, works for both bcrypt and yescrypt. */
156+
static bool php_password_crypt_verify(const zend_string *password, const zend_string *hash) {
155157
int status = 0;
156158
zend_string *ret = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1);
157159

@@ -224,12 +226,215 @@ static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_a
224226
const php_password_algo php_password_algo_bcrypt = {
225227
"bcrypt",
226228
php_password_bcrypt_hash,
227-
php_password_bcrypt_verify,
229+
php_password_crypt_verify,
228230
php_password_bcrypt_needs_rehash,
229231
php_password_bcrypt_get_info,
230232
php_password_bcrypt_valid,
231233
};
232234

235+
/* yescrypt implementation */
236+
237+
static void php_password_yescrypt_expect_long(const char *parameter_name) {
238+
if (!EG(exception)) {
239+
zend_value_error("Parameter \"%s\" cannot be converted to int", parameter_name);
240+
}
241+
}
242+
243+
static zend_string *php_password_yescrypt_hash(const zend_string *password, zend_array *options) {
244+
zend_long block_count = PHP_PASSWORD_YESCRYPT_DEFAULT_BLOCK_COUNT;
245+
zend_long block_size = PHP_PASSWORD_YESCRYPT_DEFAULT_BLOCK_SIZE;
246+
zend_long parallelism = PHP_PASSWORD_YESCRYPT_DEFAULT_PARALLELISM;
247+
zend_long time = PHP_PASSWORD_YESCRYPT_DEFAULT_TIME;
248+
249+
if (UNEXPECTED(ZEND_LONG_INT_OVFL(ZSTR_LEN(password)))) {
250+
zend_value_error("Password is too long");
251+
return NULL;
252+
}
253+
254+
if (options) {
255+
bool failed;
256+
const zval *option;
257+
258+
option = zend_hash_str_find(options, ZEND_STRL("block_count"));
259+
if (option) {
260+
block_count = zval_try_get_long(option, &failed);
261+
if (UNEXPECTED(failed)) {
262+
php_password_yescrypt_expect_long("block_count");
263+
return NULL;
264+
}
265+
266+
if (block_count < 4 || block_count > UINT32_MAX) {
267+
zend_value_error("Parameter \"block_count\" must be between 4 and %u", UINT32_MAX);
268+
return NULL;
269+
}
270+
}
271+
272+
option = zend_hash_str_find(options, ZEND_STRL("block_size"));
273+
if (option) {
274+
block_size = zval_try_get_long(option, &failed);
275+
if (UNEXPECTED(failed)) {
276+
php_password_yescrypt_expect_long("block_size");
277+
return NULL;
278+
}
279+
280+
if (block_size < 1) {
281+
zend_value_error("Parameter \"block_size\" must be greater than 0");
282+
return NULL;
283+
}
284+
}
285+
286+
option = zend_hash_str_find(options, ZEND_STRL("parallelism"));
287+
if (option) {
288+
parallelism = zval_try_get_long(option, &failed);
289+
if (UNEXPECTED(failed)) {
290+
php_password_yescrypt_expect_long("parallelism");
291+
return NULL;
292+
}
293+
294+
if (parallelism < 1) {
295+
zend_value_error("Parameter \"parallelism\" must be greater than 0");
296+
return NULL;
297+
}
298+
}
299+
300+
option = zend_hash_str_find(options, ZEND_STRL("time"));
301+
if (option) {
302+
time = zval_try_get_long(option, &failed);
303+
if (UNEXPECTED(failed)) {
304+
php_password_yescrypt_expect_long("time");
305+
return NULL;
306+
}
307+
308+
if (time < 0) {
309+
zend_value_error("Parameter \"time\" must be greater than or equal to 0");
310+
return NULL;
311+
}
312+
}
313+
314+
if ((uint64_t) block_size * (uint64_t) parallelism >= (1U << 30)) {
315+
zend_value_error("Parameter \"block_size\" * parameter \"parallelism\" must be less than 2**30");
316+
return NULL;
317+
}
318+
}
319+
320+
zend_string *salt = php_password_get_salt(NULL, Z_UL(16), options);
321+
if (UNEXPECTED(!salt)) {
322+
return NULL;
323+
}
324+
ZSTR_VAL(salt)[ZSTR_LEN(salt)] = 0;
325+
326+
uint8_t prefix_buffer[PREFIX_LEN + 1];
327+
yescrypt_params_t params = {
328+
.flags = YESCRYPT_DEFAULTS,
329+
.N = block_count, .r = block_size, .p = parallelism, .t = time,
330+
.g = 0, .NROM = 0
331+
};
332+
uint8_t *prefix = yescrypt_encode_params_r(
333+
&params,
334+
(const uint8_t *) ZSTR_VAL(salt),
335+
ZSTR_LEN(salt),
336+
prefix_buffer,
337+
sizeof(prefix_buffer)
338+
);
339+
340+
zend_string_release_ex(salt, false);
341+
342+
if (UNEXPECTED(prefix == NULL)) {
343+
return NULL;
344+
}
345+
346+
return php_crypt(
347+
ZSTR_VAL(password),
348+
/* This cast is safe because we check that the password length fits in an int at the start. */
349+
(int) ZSTR_LEN(password),
350+
(const char *) prefix_buffer,
351+
/* The following cast is safe because the prefix buffer size is always below INT_MAX. */
352+
(int) strlen((const char *) prefix_buffer),
353+
true
354+
);
355+
}
356+
357+
static bool php_password_yescrypt_valid(const zend_string *hash) {
358+
const char *h = ZSTR_VAL(hash);
359+
/* Note: $7$-style is longer */
360+
return (ZSTR_LEN(hash) >= 3 /* "$y$" */ + 3 /* 3 parameters that must be encoded */ + 2 /* $salt$ */ + HASH_LEN
361+
&& ZSTR_LEN(hash) <= PREFIX_LEN + 1 + HASH_LEN)
362+
&& (h[0] == '$') && h[1] == 'y' && (h[2] == '$');
363+
}
364+
365+
static bool php_password_yescrypt_needs_rehash(const zend_string *hash, zend_array *options) {
366+
zend_long block_count = PHP_PASSWORD_YESCRYPT_DEFAULT_BLOCK_COUNT;
367+
zend_long block_size = PHP_PASSWORD_YESCRYPT_DEFAULT_BLOCK_SIZE;
368+
zend_long parallelism = PHP_PASSWORD_YESCRYPT_DEFAULT_PARALLELISM;
369+
zend_long time = PHP_PASSWORD_YESCRYPT_DEFAULT_TIME;
370+
371+
if (!php_password_yescrypt_valid(hash)) {
372+
/* Should never get called this way. */
373+
return true;
374+
}
375+
376+
yescrypt_params_t params = { .p = 1 };
377+
const uint8_t *src = yescrypt_parse_settings((const uint8_t *) ZSTR_VAL(hash), &params, NULL);
378+
if (!src) {
379+
return true;
380+
}
381+
382+
if (options) {
383+
const zval *option;
384+
385+
option = zend_hash_str_find(options, ZEND_STRL("block_count"));
386+
if (option) {
387+
block_count = zval_get_long(option);
388+
}
389+
390+
option = zend_hash_str_find(options, ZEND_STRL("block_size"));
391+
if (option) {
392+
block_size = zval_get_long(option);
393+
}
394+
395+
option = zend_hash_str_find(options, ZEND_STRL("parallelism"));
396+
if (option) {
397+
parallelism = zval_get_long(option);
398+
}
399+
400+
option = zend_hash_str_find(options, ZEND_STRL("time"));
401+
if (option) {
402+
time = zval_get_long(option);
403+
}
404+
}
405+
406+
return block_count != params.N || block_size != params.r || parallelism != params.p || time != params.t;
407+
}
408+
409+
static int php_password_yescrypt_get_info(zval *return_value, const zend_string *hash) {
410+
if (!php_password_yescrypt_valid(hash)) {
411+
/* Should never get called this way. */
412+
return FAILURE;
413+
}
414+
415+
yescrypt_params_t params = { .p = 1 };
416+
const uint8_t *src = yescrypt_parse_settings((const uint8_t *) ZSTR_VAL(hash), &params, NULL);
417+
if (!src) {
418+
return FAILURE;
419+
}
420+
421+
add_assoc_long(return_value, "block_count", (zend_long) params.N);
422+
add_assoc_long(return_value, "block_size", (zend_long) params.r);
423+
add_assoc_long(return_value, "parallelism", (zend_long) params.p);
424+
add_assoc_long(return_value, "time", (zend_long) params.t);
425+
426+
return SUCCESS;
427+
}
428+
429+
const php_password_algo php_password_algo_yescrypt = {
430+
"yescrypt",
431+
php_password_yescrypt_hash,
432+
php_password_crypt_verify,
433+
php_password_yescrypt_needs_rehash,
434+
php_password_yescrypt_get_info,
435+
php_password_yescrypt_valid,
436+
};
437+
233438

234439
#ifdef HAVE_ARGON2LIB
235440
/* argon2i/argon2id shared implementation */
@@ -427,6 +632,10 @@ PHP_MINIT_FUNCTION(password) /* {{{ */
427632
return FAILURE;
428633
}
429634

635+
if (FAILURE == php_password_algo_register("y", &php_password_algo_yescrypt)) {
636+
return FAILURE;
637+
}
638+
430639
#ifdef HAVE_ARGON2LIB
431640
if (FAILURE == php_password_algo_register("argon2i", &php_password_algo_argon2i)) {
432641
return FAILURE;

0 commit comments

Comments
 (0)