From dae59a45a7f16f75f409cfd3f0cd8df2d06fb5d5 Mon Sep 17 00:00:00 2001 From: Rafael Molina Date: Fri, 6 Nov 2026 14:31:18 -0300 Subject: [PATCH] ext/sodium: Accept an optional tweak for ipcrypt ND and NDX Callers can reproduce a ciphertext from the ipcrypt draft vectors. Omitting the tweak still generates a random one. --- NEWS | 5 +++ UPGRADING | 5 +++ ext/sodium/libsodium.c | 42 ++++++++++++++++++------ ext/sodium/libsodium.stub.php | 4 +-- ext/sodium/libsodium_arginfo.h | 10 ++++-- ext/sodium/tests/crypto_ipcrypt.phpt | 8 +++++ ext/sodium/tests/crypto_ipcrypt_nd.phpt | 34 +++++++++++++++++++ ext/sodium/tests/crypto_ipcrypt_pfx.phpt | 15 +++++++++ 8 files changed, 108 insertions(+), 15 deletions(-) diff --git a/NEWS b/NEWS index 78f010b74ce7..889dea6ec362 100644 --- a/NEWS +++ b/NEWS @@ -142,6 +142,11 @@ PHP NEWS . Fixed bug GH-24315 (Harden SoapClient cross-origin redirect handling). (Ilia Alshanetsky) +- Sodium: + . sodium_crypto_ipcrypt_nd_encrypt() and sodium_crypto_ipcrypt_ndx_encrypt() + accept an optional tweak. Omitting it still generates a random one. + (rafamolina1) + - SPL: . Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when __serialize() removes an element). (David Carlier) diff --git a/UPGRADING b/UPGRADING index a1e30018cd69..1b5b89d5cace 100644 --- a/UPGRADING +++ b/UPGRADING @@ -105,6 +105,11 @@ PHP 8.7 UPGRADE NOTES 5. Changed Functions ======================================== +- Sodium: + . sodium_crypto_ipcrypt_nd_encrypt() and sodium_crypto_ipcrypt_ndx_encrypt() + accept an optional $tweak argument. Omitting it keeps the previous + behavior of generating a random tweak. + - Standard: . stream_select() no longer fails once a watched descriptor exceeds FD_SETSIZE. The descriptor set now grows on demand on both POSIX (bitset) diff --git a/ext/sodium/libsodium.c b/ext/sodium/libsodium.c index 07c88d4497b0..63ab82065e8f 100644 --- a/ext/sodium/libsodium.c +++ b/ext/sodium/libsodium.c @@ -4193,13 +4193,16 @@ PHP_FUNCTION(sodium_crypto_ipcrypt_nd_encrypt) size_t ip_len; unsigned char *key; size_t key_len; + char *tweak = NULL; + size_t tweak_len = 0; unsigned char bin[crypto_ipcrypt_ND_INPUTBYTES]; - unsigned char tweak[crypto_ipcrypt_ND_TWEAKBYTES]; + unsigned char tweak_buf[crypto_ipcrypt_ND_TWEAKBYTES]; + const unsigned char *tweak_ptr; unsigned char encrypted[crypto_ipcrypt_ND_OUTPUTBYTES]; char hex_out[crypto_ipcrypt_ND_OUTPUTBYTES * 2 + 1]; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", - &ip, &ip_len, &key, &key_len) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss|s!", + &ip, &ip_len, &key, &key_len, &tweak, &tweak_len) == FAILURE) { sodium_remove_param_values_from_backtrace(EG(exception)); RETURN_THROWS(); } @@ -4207,12 +4210,20 @@ PHP_FUNCTION(sodium_crypto_ipcrypt_nd_encrypt) zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES bytes long"); RETURN_THROWS(); } + if (tweak == NULL) { + randombytes_buf(tweak_buf, sizeof tweak_buf); + tweak_ptr = tweak_buf; + } else if (tweak_len != crypto_ipcrypt_ND_TWEAKBYTES) { + zend_argument_error(sodium_exception_ce, 3, "must be SODIUM_CRYPTO_IPCRYPT_ND_TWEAKBYTES bytes long"); + RETURN_THROWS(); + } else { + tweak_ptr = (const unsigned char *) tweak; + } if (sodium_ip2bin(bin, ip, ip_len) != 0) { zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address"); RETURN_THROWS(); } - randombytes_buf(tweak, sizeof tweak); - crypto_ipcrypt_nd_encrypt(encrypted, bin, tweak, key); + crypto_ipcrypt_nd_encrypt(encrypted, bin, tweak_ptr, key); sodium_bin2hex(hex_out, sizeof hex_out, encrypted, sizeof encrypted); RETURN_STRINGL(hex_out, crypto_ipcrypt_ND_OUTPUTBYTES * 2); } @@ -4269,13 +4280,16 @@ PHP_FUNCTION(sodium_crypto_ipcrypt_ndx_encrypt) size_t ip_len; unsigned char *key; size_t key_len; + char *tweak = NULL; + size_t tweak_len = 0; unsigned char bin[crypto_ipcrypt_NDX_INPUTBYTES]; - unsigned char tweak[crypto_ipcrypt_NDX_TWEAKBYTES]; + unsigned char tweak_buf[crypto_ipcrypt_NDX_TWEAKBYTES]; + const unsigned char *tweak_ptr; unsigned char encrypted[crypto_ipcrypt_NDX_OUTPUTBYTES]; char hex_out[crypto_ipcrypt_NDX_OUTPUTBYTES * 2 + 1]; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", - &ip, &ip_len, &key, &key_len) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss|s!", + &ip, &ip_len, &key, &key_len, &tweak, &tweak_len) == FAILURE) { sodium_remove_param_values_from_backtrace(EG(exception)); RETURN_THROWS(); } @@ -4283,12 +4297,20 @@ PHP_FUNCTION(sodium_crypto_ipcrypt_ndx_encrypt) zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES bytes long"); RETURN_THROWS(); } + if (tweak == NULL) { + randombytes_buf(tweak_buf, sizeof tweak_buf); + tweak_ptr = tweak_buf; + } else if (tweak_len != crypto_ipcrypt_NDX_TWEAKBYTES) { + zend_argument_error(sodium_exception_ce, 3, "must be SODIUM_CRYPTO_IPCRYPT_NDX_TWEAKBYTES bytes long"); + RETURN_THROWS(); + } else { + tweak_ptr = (const unsigned char *) tweak; + } if (sodium_ip2bin(bin, ip, ip_len) != 0) { zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address"); RETURN_THROWS(); } - randombytes_buf(tweak, sizeof tweak); - crypto_ipcrypt_ndx_encrypt(encrypted, bin, tweak, key); + crypto_ipcrypt_ndx_encrypt(encrypted, bin, tweak_ptr, key); sodium_bin2hex(hex_out, sizeof hex_out, encrypted, sizeof encrypted); RETURN_STRINGL(hex_out, crypto_ipcrypt_NDX_OUTPUTBYTES * 2); } diff --git a/ext/sodium/libsodium.stub.php b/ext/sodium/libsodium.stub.php index a9b6d345c66b..a87c9f225204 100644 --- a/ext/sodium/libsodium.stub.php +++ b/ext/sodium/libsodium.stub.php @@ -871,13 +871,13 @@ function sodium_crypto_ipcrypt_decrypt(string $encrypted_ip, #[\SensitiveParamet function sodium_crypto_ipcrypt_nd_keygen(): string {} -function sodium_crypto_ipcrypt_nd_encrypt(string $ip, #[\SensitiveParameter] string $key): string {} +function sodium_crypto_ipcrypt_nd_encrypt(string $ip, #[\SensitiveParameter] string $key, ?string $tweak = null): string {} function sodium_crypto_ipcrypt_nd_decrypt(string $ciphertext_hex, #[\SensitiveParameter] string $key): string {} function sodium_crypto_ipcrypt_ndx_keygen(): string {} -function sodium_crypto_ipcrypt_ndx_encrypt(string $ip, #[\SensitiveParameter] string $key): string {} +function sodium_crypto_ipcrypt_ndx_encrypt(string $ip, #[\SensitiveParameter] string $key, ?string $tweak = null): string {} function sodium_crypto_ipcrypt_ndx_decrypt(string $ciphertext_hex, #[\SensitiveParameter] string $key): string {} diff --git a/ext/sodium/libsodium_arginfo.h b/ext/sodium/libsodium_arginfo.h index ac6f90ef6035..886ed57b58ce 100644 --- a/ext/sodium/libsodium_arginfo.h +++ b/ext/sodium/libsodium_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit libsodium.stub.php instead. - * Stub hash: 50b3cf45af139d4d38f0f46a152b5b1e2662dfd4 */ + * Stub hash: 80e109d22f563766f7c7417c8479ab30ad3837c8 */ #include "zend_attributes.h" #include "zend_constants.h" @@ -502,7 +502,11 @@ ZEND_END_ARG_INFO() #define arginfo_sodium_crypto_ipcrypt_nd_keygen arginfo_sodium_crypto_ipcrypt_keygen -#define arginfo_sodium_crypto_ipcrypt_nd_encrypt arginfo_sodium_crypto_ipcrypt_encrypt +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_sodium_crypto_ipcrypt_nd_encrypt, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, ip, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, tweak, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_sodium_crypto_ipcrypt_nd_decrypt, 0, 2, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, ciphertext_hex, IS_STRING, 0) @@ -511,7 +515,7 @@ ZEND_END_ARG_INFO() #define arginfo_sodium_crypto_ipcrypt_ndx_keygen arginfo_sodium_crypto_ipcrypt_keygen -#define arginfo_sodium_crypto_ipcrypt_ndx_encrypt arginfo_sodium_crypto_ipcrypt_encrypt +#define arginfo_sodium_crypto_ipcrypt_ndx_encrypt arginfo_sodium_crypto_ipcrypt_nd_encrypt #define arginfo_sodium_crypto_ipcrypt_ndx_decrypt arginfo_sodium_crypto_ipcrypt_nd_decrypt diff --git a/ext/sodium/tests/crypto_ipcrypt.phpt b/ext/sodium/tests/crypto_ipcrypt.phpt index f398d21c70fc..2a3ffec27819 100644 --- a/ext/sodium/tests/crypto_ipcrypt.phpt +++ b/ext/sodium/tests/crypto_ipcrypt.phpt @@ -31,6 +31,12 @@ var_dump(sodium_crypto_ipcrypt_decrypt($enc6, $key)); $gen_key = sodium_crypto_ipcrypt_keygen(); var_dump(strlen($gen_key) === SODIUM_CRYPTO_IPCRYPT_KEYBYTES); +/* Known answer: draft-denis-ipcrypt A.1 vector 3 */ +$kat_key = sodium_hex2bin("2b7e151628aed2a6abf7158809cf4f3c"); +$kat = sodium_crypto_ipcrypt_encrypt("192.0.2.1", $kat_key); +var_dump($kat); +var_dump(sodium_crypto_ipcrypt_decrypt($kat, $kat_key)); + /* Error: wrong key length */ try { sodium_crypto_ipcrypt_encrypt("192.168.1.1", "short"); @@ -58,6 +64,8 @@ bool(true) bool(true) string(3) "::1" bool(true) +string(39) "1dbd:c1b9:fff1:7586:7d0b:67b4:e76e:4777" +string(9) "192.0.2.1" sodium_crypto_ipcrypt_encrypt(): Argument #2 ($key) must be SODIUM_CRYPTO_IPCRYPT_KEYBYTES bytes long sodium_crypto_ipcrypt_encrypt(): Argument #1 ($ip) must be a valid IP address sodium_crypto_ipcrypt_decrypt(): Argument #2 ($key) must be SODIUM_CRYPTO_IPCRYPT_KEYBYTES bytes long diff --git a/ext/sodium/tests/crypto_ipcrypt_nd.phpt b/ext/sodium/tests/crypto_ipcrypt_nd.phpt index fed36bbda444..d95c1153f95c 100644 --- a/ext/sodium/tests/crypto_ipcrypt_nd.phpt +++ b/ext/sodium/tests/crypto_ipcrypt_nd.phpt @@ -41,6 +41,20 @@ try { echo $e->getMessage() . "\n"; } +/* Known answer: draft-denis-ipcrypt A.3 vector 1 */ +$nd_kat_key = sodium_hex2bin("0123456789abcdeffedcba9876543210"); +$nd_tweak = sodium_hex2bin("08e0c289bff23b7c"); +$nd_kat = sodium_crypto_ipcrypt_nd_encrypt("0.0.0.0", $nd_kat_key, $nd_tweak); +var_dump($nd_kat); +var_dump(sodium_crypto_ipcrypt_nd_decrypt($nd_kat, $nd_kat_key)); + +/* ND error: wrong tweak length */ +try { + sodium_crypto_ipcrypt_nd_encrypt("192.168.1.1", $nd_key, "short"); +} catch (SodiumException $e) { + echo $e->getMessage() . "\n"; +} + /* ND error: bad ciphertext hex length */ try { sodium_crypto_ipcrypt_nd_decrypt("tooshort", $nd_key); @@ -72,6 +86,20 @@ try { echo $e->getMessage() . "\n"; } +/* Known answer: draft-denis-ipcrypt A.4 vector 1 */ +$ndx_kat_key = sodium_hex2bin("0123456789abcdeffedcba98765432101032547698badcfeefcdab8967452301"); +$ndx_tweak = sodium_hex2bin("21bd1834bc088cd2b4ecbe30b70898d7"); +$ndx_kat = sodium_crypto_ipcrypt_ndx_encrypt("0.0.0.0", $ndx_kat_key, $ndx_tweak); +var_dump($ndx_kat); +var_dump(sodium_crypto_ipcrypt_ndx_decrypt($ndx_kat, $ndx_kat_key)); + +/* NDX error: wrong tweak length */ +try { + sodium_crypto_ipcrypt_ndx_encrypt("10.0.0.1", $ndx_key, "short"); +} catch (SodiumException $e) { + echo $e->getMessage() . "\n"; +} + /* NDX error: bad ciphertext hex */ try { sodium_crypto_ipcrypt_ndx_decrypt("tooshort", $ndx_key); @@ -88,6 +116,9 @@ string(11) "192.168.1.1" string(3) "::1" sodium_crypto_ipcrypt_nd_encrypt(): Argument #2 ($key) must be SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES bytes long sodium_crypto_ipcrypt_nd_encrypt(): Argument #1 ($ip) must be a valid IP address +string(48) "08e0c289bff23b7cb349aadfe3bcef56221c384c7c217b16" +string(7) "0.0.0.0" +sodium_crypto_ipcrypt_nd_encrypt(): Argument #3 ($tweak) must be SODIUM_CRYPTO_IPCRYPT_ND_TWEAKBYTES bytes long sodium_crypto_ipcrypt_nd_decrypt(): Argument #1 ($ciphertext_hex) must be a valid hex-encoded ciphertext bool(true) bool(true) @@ -95,4 +126,7 @@ string(8) "10.0.0.1" bool(true) string(7) "fe80::1" sodium_crypto_ipcrypt_ndx_encrypt(): Argument #2 ($key) must be SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES bytes long +string(64) "21bd1834bc088cd2b4ecbe30b70898d782db0d4125fdace61db35b8339f20ee5" +string(7) "0.0.0.0" +sodium_crypto_ipcrypt_ndx_encrypt(): Argument #3 ($tweak) must be SODIUM_CRYPTO_IPCRYPT_NDX_TWEAKBYTES bytes long sodium_crypto_ipcrypt_ndx_decrypt(): Argument #1 ($ciphertext_hex) must be a valid hex-encoded ciphertext diff --git a/ext/sodium/tests/crypto_ipcrypt_pfx.phpt b/ext/sodium/tests/crypto_ipcrypt_pfx.phpt index 73a82537aa49..2d83eca0e3cb 100644 --- a/ext/sodium/tests/crypto_ipcrypt_pfx.phpt +++ b/ext/sodium/tests/crypto_ipcrypt_pfx.phpt @@ -33,6 +33,17 @@ var_dump(filter_var($enc6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false); $gen_key = sodium_crypto_ipcrypt_pfx_keygen(); var_dump(strlen($gen_key) === SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES); +/* Known answer: draft-denis-ipcrypt A.2.1 vector 3 */ +$kat_key = sodium_hex2bin("0123456789abcdeffedcba98765432101032547698badcfeefcdab8967452301"); +$kat = sodium_crypto_ipcrypt_pfx_encrypt("192.0.2.1", $kat_key); +var_dump($kat); +var_dump(sodium_crypto_ipcrypt_pfx_decrypt($kat, $kat_key)); + +/* Same /24 stays in the same encrypted /24 (A.2.2) */ +$pfx_key = sodium_hex2bin("2b7e151628aed2a6abf7158809cf4f3ca9f5ba40db214c3798f2e1c23456789a"); +var_dump(sodium_crypto_ipcrypt_pfx_encrypt("10.0.0.47", $pfx_key)); +var_dump(sodium_crypto_ipcrypt_pfx_encrypt("10.0.0.129", $pfx_key)); + /* PFX error: wrong key length */ try { sodium_crypto_ipcrypt_pfx_encrypt("10.0.0.1", "short"); @@ -61,6 +72,10 @@ bool(true) string(3) "::1" bool(true) bool(true) +string(14) "100.115.72.131" +string(9) "192.0.2.1" +string(14) "19.214.210.244" +string(13) "19.214.210.80" sodium_crypto_ipcrypt_pfx_encrypt(): Argument #2 ($key) must be SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES bytes long sodium_crypto_ipcrypt_pfx_encrypt(): Argument #1 ($ip) must be a valid IP address sodium_crypto_ipcrypt_pfx_decrypt(): Argument #2 ($key) must be SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES bytes long