xref: /PHP-8.4/ext/openssl/openssl_pwhash.c (revision 32c5ce34)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Remi Collet <remi@php.net>                                  |
14    +----------------------------------------------------------------------+
15 */
16 
17 #ifdef HAVE_CONFIG_H
18 # include "config.h"
19 #endif
20 
21 #include "php.h"
22 #include "ext/standard/php_password.h"
23 #include "php_openssl.h"
24 
25 #if defined(HAVE_OPENSSL_ARGON2)
26 #include "Zend/zend_attributes.h"
27 #include "openssl_pwhash_arginfo.h"
28 #include <ext/standard/base64.h>
29 #include "ext/random/php_random_csprng.h"
30 #include <openssl/params.h>
31 #include <openssl/core_names.h>
32 #include <openssl/kdf.h>
33 #include <openssl/thread.h>
34 #include <openssl/rand.h>
35 
36 #define PHP_OPENSSL_MEMLIMIT_MIN  8u
37 #define PHP_OPENSSL_MEMLIMIT_MAX  UINT32_MAX
38 #define PHP_OPENSSL_ITERLIMIT_MIN 1u
39 #define PHP_OPENSSL_ITERLIMIT_MAX UINT32_MAX
40 #define PHP_OPENSSL_THREADS_MIN   1u
41 #define PHP_OPENSSL_THREADS_MAX   UINT32_MAX
42 
43 #define PHP_OPENSSL_ARGON_VERSION 0x13
44 
45 #define PHP_OPENSSL_SALT_SIZE     16
46 #define PHP_OPENSSL_HASH_SIZE     32
47 #define PHP_OPENSSL_DIGEST_SIZE  128
48 
get_options(zend_array * options,uint32_t * memlimit,uint32_t * iterlimit,uint32_t * threads)49 static inline zend_result get_options(zend_array *options, uint32_t *memlimit, uint32_t *iterlimit, uint32_t *threads)
50 {
51 	zval *opt;
52 
53 	*iterlimit = PHP_OPENSSL_PWHASH_ITERLIMIT;
54 	*memlimit  = PHP_OPENSSL_PWHASH_MEMLIMIT;
55 	*threads   = PHP_OPENSSL_PWHASH_THREADS;
56 
57 	if (!options) {
58 		return SUCCESS;
59 	}
60 	if ((opt = zend_hash_str_find(options, "memory_cost", strlen("memory_cost")))) {
61 		zend_long smemlimit = zval_get_long(opt);
62 
63 		if ((smemlimit < 0) || (smemlimit < PHP_OPENSSL_MEMLIMIT_MIN) || (smemlimit > (PHP_OPENSSL_MEMLIMIT_MAX))) {
64 			zend_value_error("Memory cost is outside of allowed memory range");
65 			return FAILURE;
66 		}
67 		*memlimit = smemlimit;
68 	}
69 	if ((opt = zend_hash_str_find(options, "time_cost", strlen("time_cost")))) {
70 		zend_long siterlimit = zval_get_long(opt);
71 		if ((siterlimit < PHP_OPENSSL_ITERLIMIT_MIN) || (siterlimit > PHP_OPENSSL_ITERLIMIT_MAX)) {
72 			zend_value_error("Time cost is outside of allowed time range");
73 			return FAILURE;
74 		}
75 		*iterlimit = siterlimit;
76 	}
77 	if ((opt = zend_hash_str_find(options, "threads", strlen("threads"))) && (zval_get_long(opt) != 1)) {
78 		zend_long sthreads = zval_get_long(opt);
79 		if ((sthreads < PHP_OPENSSL_THREADS_MIN) || (sthreads > PHP_OPENSSL_THREADS_MAX)) {
80 			zend_value_error("Invalid number of threads");
81 			return FAILURE;
82 		}
83 		*threads = sthreads;
84 	}
85 	return SUCCESS;
86 }
87 
php_openssl_argon2_compute_hash(const char * algo,uint32_t version,uint32_t memlimit,uint32_t iterlimit,uint32_t threads,const char * pass,size_t pass_len,const unsigned char * salt,size_t salt_len,unsigned char * hash,size_t hash_len)88 static bool php_openssl_argon2_compute_hash(
89 	const char *algo,
90 	uint32_t version, uint32_t memlimit, uint32_t iterlimit, uint32_t threads,
91 	const char *pass, size_t pass_len,
92 	const unsigned char *salt, size_t salt_len,
93 	unsigned char *hash, size_t hash_len)
94 {
95 	OSSL_PARAM params[7], *p = params;
96 	EVP_KDF *kdf = NULL;
97 	EVP_KDF_CTX *kctx = NULL;
98 	uint32_t oldthreads;
99 	bool ret = false;
100 
101 	oldthreads = OSSL_get_max_threads(NULL);
102 	if (OSSL_set_max_threads(NULL, threads) != 1) {
103 		goto fail;
104 	}
105 	p = params;
106 	*p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &threads);
107 	*p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES,	&threads);
108 	*p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iterlimit);
109 	*p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memlimit);
110 	*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, (void *)salt, salt_len);
111 	*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, (void *)pass, pass_len);
112 	*p++ = OSSL_PARAM_construct_end();
113 
114 	if ((kdf = EVP_KDF_fetch(NULL, algo, NULL)) == NULL) {
115 		goto fail;
116 	}
117 	if ((kctx = EVP_KDF_CTX_new(kdf)) == NULL) {
118 		goto fail;
119 	}
120 	if (EVP_KDF_derive(kctx, hash, hash_len, params) != 1) {
121 		zend_value_error("Unexpected failure hashing password");
122 		goto fail;
123 	}
124 
125 	ret = true;
126 
127 fail:
128 	EVP_KDF_free(kdf);
129 	EVP_KDF_CTX_free(kctx);
130 	OSSL_set_max_threads(NULL, oldthreads);
131 
132 	return ret;
133 }
134 
php_openssl_argon2_hash(const zend_string * password,zend_array * options,const char * algo)135 static zend_string *php_openssl_argon2_hash(const zend_string *password, zend_array *options, const char *algo)
136 {
137 	uint32_t iterlimit, memlimit, threads, version = PHP_OPENSSL_ARGON_VERSION;
138 	zend_string *digest = NULL, *salt64 = NULL, *hash64 = NULL;
139 	unsigned char hash[PHP_OPENSSL_HASH_SIZE+1], salt[PHP_OPENSSL_SALT_SIZE+1];
140 
141 	if ((ZSTR_LEN(password) >= UINT32_MAX)) {
142 		zend_value_error("Password is too long");
143 		return NULL;
144 	}
145 	if (get_options(options, &memlimit, &iterlimit, &threads) == FAILURE) {
146 		return NULL;
147 	}
148 	if (RAND_bytes(salt, PHP_OPENSSL_SALT_SIZE) <= 0) {
149 		return NULL;
150 	}
151 
152 	if (!php_openssl_argon2_compute_hash(algo, version, memlimit, iterlimit, threads,
153 			ZSTR_VAL(password), ZSTR_LEN(password),	salt, PHP_OPENSSL_SALT_SIZE, hash, PHP_OPENSSL_HASH_SIZE)) {
154 		return NULL;
155 	}
156 
157 	hash64 = php_base64_encode_ex(hash, PHP_OPENSSL_HASH_SIZE, PHP_BASE64_NO_PADDING);
158 
159 	salt64 = php_base64_encode_ex(salt, PHP_OPENSSL_SALT_SIZE, PHP_BASE64_NO_PADDING);
160 
161 	digest = zend_string_alloc(PHP_OPENSSL_DIGEST_SIZE, 0);
162 	ZSTR_LEN(digest) = snprintf(ZSTR_VAL(digest), ZSTR_LEN(digest), "$%s$v=%d$m=%u,t=%u,p=%u$%s$%s",
163 		algo, version, memlimit, iterlimit, threads, ZSTR_VAL(salt64), ZSTR_VAL(hash64));
164 
165 	zend_string_release(salt64);
166 	zend_string_release(hash64);
167 
168 	return digest;
169 }
170 
php_openssl_argon2_extract(const zend_string * digest,uint32_t * version,uint32_t * memlimit,uint32_t * iterlimit,uint32_t * threads,zend_string ** salt,zend_string ** hash)171 static int php_openssl_argon2_extract(
172 	const zend_string *digest, uint32_t *version, uint32_t *memlimit, uint32_t *iterlimit,
173 	uint32_t *threads, zend_string **salt, zend_string **hash)
174 {
175 	const char *p;
176 	char *hash64, *salt64;
177 
178 	if (!digest || (ZSTR_LEN(digest) < sizeof("$argon2id$"))) {
179 		return FAILURE;
180 	}
181 	p = ZSTR_VAL(digest);
182 	if (!memcmp(p, "$argon2i$", strlen("$argon2i$"))) {
183 		p += strlen("$argon2i$");
184 	} else if (!memcmp(p, "$argon2id$", strlen("$argon2id$"))) {
185 		p += strlen("$argon2id$");
186 	} else {
187 		return FAILURE;
188 	}
189 	if (sscanf(p, "v=%" PRIu32 "$m=%" PRIu32 ",t=%" PRIu32 ",p=%" PRIu32,
190 			version, memlimit, iterlimit, threads) != 4) {
191 		return FAILURE;
192 	}
193 	if (salt && hash) {
194 		/* start of param */
195 		p = strchr(p, '$');
196 		if (!p) {
197 			return FAILURE;
198 		}
199 		/* start of salt */
200 		p = strchr(p+1, '$');
201 		if (!p) {
202 			return FAILURE;
203 		}
204 		salt64 = estrdup(p+1);
205 		/* start of hash */
206 		hash64 = strchr(salt64, '$');
207 		if (!hash64) {
208 			efree(salt64);
209 			return FAILURE;
210 		}
211 		*hash64++ = 0;
212 		*salt = php_base64_decode((unsigned char *)salt64, strlen(salt64));
213 		*hash = php_base64_decode((unsigned char *)hash64, strlen(hash64));
214 		efree(salt64);
215 	}
216 	return SUCCESS;
217 }
218 
php_openssl_argon2_verify(const zend_string * password,const zend_string * digest,const char * algo)219 static bool php_openssl_argon2_verify(const zend_string *password, const zend_string *digest, const char *algo)
220 {
221 	uint32_t version, iterlimit, memlimit, threads;
222 	zend_string *salt, *hash, *new;
223 	bool ret = false;
224 
225 	if ((ZSTR_LEN(password) >= UINT32_MAX) || (ZSTR_LEN(digest) >= UINT32_MAX)) {
226 		return false;
227 	}
228 	if (FAILURE == php_openssl_argon2_extract(digest, &version, &memlimit, &iterlimit, &threads, &salt, &hash)) {
229 		return false;
230 	}
231 
232 	new = zend_string_alloc(ZSTR_LEN(hash), 0);
233 	if (php_openssl_argon2_compute_hash(algo, version, memlimit, iterlimit, threads,
234 			ZSTR_VAL(password), ZSTR_LEN(password),	(unsigned char *)ZSTR_VAL(salt),
235 			ZSTR_LEN(salt), (unsigned char *)ZSTR_VAL(new), ZSTR_LEN(new))) {
236 		ret = (php_safe_bcmp(hash, new) == 0);
237 	}
238 
239 	zend_string_release(new);
240 	zend_string_release(salt);
241 	zend_string_release(hash);
242 
243 	return ret;
244 }
245 
php_openssl_argon2i_verify(const zend_string * password,const zend_string * digest)246 static bool php_openssl_argon2i_verify(const zend_string *password, const zend_string *digest)
247 {
248 	return php_openssl_argon2_verify(password, digest, "argon2i");
249 }
250 
php_openssl_argon2id_verify(const zend_string * password,const zend_string * digest)251 static bool php_openssl_argon2id_verify(const zend_string *password, const zend_string *digest)
252 {
253 	return php_openssl_argon2_verify(password, digest, "argon2id");
254 }
255 
php_openssl_argon2_needs_rehash(const zend_string * hash,zend_array * options)256 static bool php_openssl_argon2_needs_rehash(const zend_string *hash, zend_array *options)
257 {
258 	uint32_t version, iterlimit, memlimit, threads;
259 	uint32_t new_version = PHP_OPENSSL_ARGON_VERSION, new_iterlimit, new_memlimit, new_threads;
260 
261 	if (FAILURE == get_options(options, &new_memlimit, &new_iterlimit, &new_threads)) {
262 		return true;
263 	}
264 	if (FAILURE == php_openssl_argon2_extract(hash, &version, &memlimit, &iterlimit, &threads, NULL, NULL)) {
265 		return true;
266 	}
267 
268 	// Algo already checked in pasword_needs_rehash implementation
269 	return (version != new_version) ||
270 		(iterlimit != new_iterlimit) ||
271 		(memlimit != new_memlimit) ||
272 		(threads != new_threads);
273 }
274 
php_openssl_argon2_get_info(zval * return_value,const zend_string * hash)275 static int php_openssl_argon2_get_info(zval *return_value, const zend_string *hash)
276 {
277 	uint32_t v, threads;
278 	uint32_t memory_cost;
279 	uint32_t time_cost;
280 
281 	if (FAILURE == php_openssl_argon2_extract(hash, &v, &memory_cost, &time_cost, &threads, NULL, NULL)) {
282 		return FAILURE;
283 	}
284 	add_assoc_long(return_value, "memory_cost", memory_cost);
285 	add_assoc_long(return_value, "time_cost", time_cost);
286 	add_assoc_long(return_value, "threads", threads);
287 
288 	return SUCCESS;
289 }
290 
291 
php_openssl_argon2i_hash(const zend_string * password,zend_array * options)292 static zend_string *php_openssl_argon2i_hash(const zend_string *password, zend_array *options)
293 {
294 	return php_openssl_argon2_hash(password, options, "argon2i");
295 }
296 
297 static const php_password_algo openssl_algo_argon2i = {
298 	"argon2i",
299 	php_openssl_argon2i_hash,
300 	php_openssl_argon2i_verify,
301 	php_openssl_argon2_needs_rehash,
302 	php_openssl_argon2_get_info,
303 	NULL,
304 };
305 
php_openssl_argon2id_hash(const zend_string * password,zend_array * options)306 static zend_string *php_openssl_argon2id_hash(const zend_string *password, zend_array *options)
307 {
308 	return php_openssl_argon2_hash(password, options, "argon2id");
309 }
310 
311 static const php_password_algo openssl_algo_argon2id = {
312 	"argon2id",
313 	php_openssl_argon2id_hash,
314 	php_openssl_argon2id_verify,
315 	php_openssl_argon2_needs_rehash,
316 	php_openssl_argon2_get_info,
317 	NULL,
318 };
319 
PHP_FUNCTION(openssl_password_hash)320 PHP_FUNCTION(openssl_password_hash)
321 {
322 	zend_string *password, *algo, *digest;
323 	zend_array *options = NULL;
324 
325 	ZEND_PARSE_PARAMETERS_START(2, 3)
326 		Z_PARAM_STR(algo)
327 		Z_PARAM_STR(password)
328 		Z_PARAM_OPTIONAL
329 		Z_PARAM_ARRAY_HT(options)
330 	ZEND_PARSE_PARAMETERS_END();
331 
332 	if (strcmp(ZSTR_VAL(algo), "argon2i") && strcmp(ZSTR_VAL(algo), "argon2id")) {
333 		zend_argument_value_error(1, "must be a valid password openssl hashing algorithm");
334 		RETURN_THROWS();
335 	}
336 
337 	digest = php_openssl_argon2_hash(password, options, ZSTR_VAL(algo));
338 	if (!digest) {
339 		if (!EG(exception)) {
340 			zend_throw_error(NULL, "Password hashing failed for unknown reason");
341 		}
342 		RETURN_THROWS();
343 	}
344 
345 	RETURN_NEW_STR(digest);
346 }
347 
PHP_FUNCTION(openssl_password_verify)348 PHP_FUNCTION(openssl_password_verify)
349 {
350 	zend_string *password, *algo, *digest;
351 
352 	ZEND_PARSE_PARAMETERS_START(3, 3)
353 		Z_PARAM_STR(algo)
354 		Z_PARAM_STR(password)
355 		Z_PARAM_STR(digest)
356 	ZEND_PARSE_PARAMETERS_END();
357 
358 	if (strcmp(ZSTR_VAL(algo), "argon2i") && strcmp(ZSTR_VAL(algo), "argon2id")) {
359 		zend_argument_value_error(1, "must be a valid password openssl hashing algorithm");
360 		RETURN_THROWS();
361 	}
362 
363 	RETURN_BOOL(php_openssl_argon2_verify(password, digest, ZSTR_VAL(algo)));
364 }
365 
PHP_MINIT_FUNCTION(openssl_pwhash)366 PHP_MINIT_FUNCTION(openssl_pwhash)
367 {
368 	zend_string *argon2i = ZSTR_INIT_LITERAL("argon2i", 1);
369 
370 	if (php_password_algo_find(argon2i)) {
371 		/* Nothing to do. Core or sodium has registered these algorithms for us. */
372 		zend_string_release(argon2i);
373 		return SUCCESS;
374 	}
375 	zend_string_release(argon2i);
376 
377 	register_openssl_pwhash_symbols(module_number);
378 
379 	if (FAILURE == php_password_algo_register("argon2i", &openssl_algo_argon2i)) {
380 		return FAILURE;
381 	}
382 	if (FAILURE == php_password_algo_register("argon2id", &openssl_algo_argon2id)) {
383 		return FAILURE;
384 	}
385 
386 	return SUCCESS;
387 }
388 #endif /* HAVE_OPENSSL_ARGON2 */
389