Commit 8a907a9bd8b for php.net
commit 8a907a9bd8bd2a5bee98f0a574e413874d12ff99
Author: Frank Denis <124872+jedisct1@users.noreply.github.com>
Date: Fri Feb 27 13:11:24 2026 +0100
ext/sodium: Add support for libsodium 1.0.21 APIs (#20960)
Add support for new functions present in recent libsodium versions:
- Functions for IP address encryption:
- sodium_crypto_ipcrypt_*
- sodium_bin2ip/sodium_ip2bin helpers
- Extendable output functions:
- SHAKE128/SHAKE256
- TurboSHAKE128/TurboSHAKE256
diff --git a/NEWS b/NEWS
index 3ea44074336..fd2f2fa82ec 100644
--- a/NEWS
+++ b/NEWS
@@ -104,6 +104,9 @@ PHP NEWS
. Fixed GH-20532 (socket_addrinfo_lookup gives the error code with a new
optional parameter). (David Carlier)
+- Sodium:
+ . Added support for libsodium 1.0.21 IPcrypt and XOF APIs. (jedisct1)
+
- SPL:
. DirectoryIterator key can now work better with filesystem supporting larger
directory indexing. (David Carlier)
diff --git a/ext/sodium/libsodium.c b/ext/sodium/libsodium.c
index 7d36fbb8928..bb11a6ef911 100644
--- a/ext/sodium/libsodium.c
+++ b/ext/sodium/libsodium.c
@@ -3913,3 +3913,917 @@ PHP_FUNCTION(sodium_crypto_core_ristretto255_sub)
RETURN_NEW_STR(r);
}
#endif
+
+#ifdef crypto_ipcrypt_KEYBYTES
+PHP_FUNCTION(sodium_crypto_ipcrypt_keygen)
+{
+ unsigned char key[crypto_ipcrypt_KEYBYTES];
+
+ if (zend_parse_parameters_none() == FAILURE) {
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_keygen(key);
+ RETURN_STRINGL((const char *) key, sizeof key);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_encrypt)
+{
+ char *ip;
+ size_t ip_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char bin[crypto_ipcrypt_BYTES];
+ unsigned char encrypted[crypto_ipcrypt_BYTES];
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
+ &ip, &ip_len, &key, &key_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ if (sodium_ip2bin(bin, ip, ip_len) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address");
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_encrypt(encrypted, bin, key);
+ if (sodium_bin2ip(ip_out, sizeof ip_out, encrypted) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_decrypt)
+{
+ char *encrypted_ip;
+ size_t encrypted_ip_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char bin[crypto_ipcrypt_BYTES];
+ unsigned char decrypted[crypto_ipcrypt_BYTES];
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
+ &encrypted_ip, &encrypted_ip_len, &key, &key_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ if (sodium_ip2bin(bin, encrypted_ip, encrypted_ip_len) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address");
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_decrypt(decrypted, bin, key);
+ if (sodium_bin2ip(ip_out, sizeof ip_out, decrypted) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_nd_keygen)
+{
+ unsigned char key[crypto_ipcrypt_ND_KEYBYTES];
+
+ if (zend_parse_parameters_none() == FAILURE) {
+ RETURN_THROWS();
+ }
+ randombytes_buf(key, sizeof key);
+ RETURN_STRINGL((const char *) key, sizeof key);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_nd_encrypt)
+{
+ char *ip;
+ size_t ip_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char bin[crypto_ipcrypt_ND_INPUTBYTES];
+ unsigned char tweak[crypto_ipcrypt_ND_TWEAKBYTES];
+ 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) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_ND_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ 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);
+ sodium_bin2hex(hex_out, sizeof hex_out, encrypted, sizeof encrypted);
+ RETURN_STRINGL(hex_out, crypto_ipcrypt_ND_OUTPUTBYTES * 2);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_nd_decrypt)
+{
+ char *ciphertext_hex;
+ size_t ciphertext_hex_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char encrypted[crypto_ipcrypt_ND_OUTPUTBYTES];
+ unsigned char decrypted[crypto_ipcrypt_ND_INPUTBYTES];
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
+ &ciphertext_hex, &ciphertext_hex_len, &key, &key_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_ND_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ if (ciphertext_hex_len != crypto_ipcrypt_ND_OUTPUTBYTES * 2) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid hex-encoded ciphertext");
+ RETURN_THROWS();
+ }
+ if (sodium_hex2bin(encrypted, sizeof encrypted, ciphertext_hex, ciphertext_hex_len,
+ NULL, NULL, NULL) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid hex-encoded ciphertext");
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_nd_decrypt(decrypted, encrypted, key);
+ if (sodium_bin2ip(ip_out, sizeof ip_out, decrypted) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_ndx_keygen)
+{
+ unsigned char key[crypto_ipcrypt_NDX_KEYBYTES];
+
+ if (zend_parse_parameters_none() == FAILURE) {
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_ndx_keygen(key);
+ RETURN_STRINGL((const char *) key, sizeof key);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_ndx_encrypt)
+{
+ char *ip;
+ size_t ip_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char bin[crypto_ipcrypt_NDX_INPUTBYTES];
+ unsigned char tweak[crypto_ipcrypt_NDX_TWEAKBYTES];
+ 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) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_NDX_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ 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);
+ sodium_bin2hex(hex_out, sizeof hex_out, encrypted, sizeof encrypted);
+ RETURN_STRINGL(hex_out, crypto_ipcrypt_NDX_OUTPUTBYTES * 2);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_ndx_decrypt)
+{
+ char *ciphertext_hex;
+ size_t ciphertext_hex_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char encrypted[crypto_ipcrypt_NDX_OUTPUTBYTES];
+ unsigned char decrypted[crypto_ipcrypt_NDX_INPUTBYTES];
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
+ &ciphertext_hex, &ciphertext_hex_len, &key, &key_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_NDX_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ if (ciphertext_hex_len != crypto_ipcrypt_NDX_OUTPUTBYTES * 2) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid hex-encoded ciphertext");
+ RETURN_THROWS();
+ }
+ if (sodium_hex2bin(encrypted, sizeof encrypted, ciphertext_hex, ciphertext_hex_len,
+ NULL, NULL, NULL) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid hex-encoded ciphertext");
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_ndx_decrypt(decrypted, encrypted, key);
+ if (sodium_bin2ip(ip_out, sizeof ip_out, decrypted) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_pfx_keygen)
+{
+ unsigned char key[crypto_ipcrypt_PFX_KEYBYTES];
+
+ if (zend_parse_parameters_none() == FAILURE) {
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_pfx_keygen(key);
+ RETURN_STRINGL((const char *) key, sizeof key);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_pfx_encrypt)
+{
+ char *ip;
+ size_t ip_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char bin[crypto_ipcrypt_PFX_BYTES];
+ unsigned char encrypted[crypto_ipcrypt_PFX_BYTES];
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
+ &ip, &ip_len, &key, &key_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_PFX_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ if (sodium_ip2bin(bin, ip, ip_len) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address");
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_pfx_encrypt(encrypted, bin, key);
+ if (sodium_bin2ip(ip_out, sizeof ip_out, encrypted) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_crypto_ipcrypt_pfx_decrypt)
+{
+ char *encrypted_ip;
+ size_t encrypted_ip_len;
+ unsigned char *key;
+ size_t key_len;
+ unsigned char bin[crypto_ipcrypt_PFX_BYTES];
+ unsigned char decrypted[crypto_ipcrypt_PFX_BYTES];
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
+ &encrypted_ip, &encrypted_ip_len, &key, &key_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (key_len != crypto_ipcrypt_PFX_KEYBYTES) {
+ zend_argument_error(sodium_exception_ce, 2, "must be SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES bytes long");
+ RETURN_THROWS();
+ }
+ if (sodium_ip2bin(bin, encrypted_ip, encrypted_ip_len) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address");
+ RETURN_THROWS();
+ }
+ crypto_ipcrypt_pfx_decrypt(decrypted, bin, key);
+ if (sodium_bin2ip(ip_out, sizeof ip_out, decrypted) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_bin2ip)
+{
+ unsigned char *bin;
+ size_t bin_len;
+ char ip_out[46];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
+ &bin, &bin_len) == FAILURE) {
+ RETURN_THROWS();
+ }
+ if (bin_len != 16) {
+ zend_argument_error(sodium_exception_ce, 1, "must be 16 bytes long");
+ RETURN_THROWS();
+ }
+ if (sodium_bin2ip(ip_out, sizeof ip_out, bin) == NULL) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ RETURN_STRING(ip_out);
+}
+
+PHP_FUNCTION(sodium_ip2bin)
+{
+ char *ip;
+ size_t ip_len;
+ unsigned char bin[16];
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "s",
+ &ip, &ip_len) == FAILURE) {
+ RETURN_THROWS();
+ }
+ if (sodium_ip2bin(bin, ip, ip_len) != 0) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a valid IP address");
+ RETURN_THROWS();
+ }
+ RETURN_STRINGL((const char *) bin, sizeof bin);
+}
+#endif
+
+#ifdef crypto_xof_shake128_STATEBYTES
+PHP_FUNCTION(sodium_crypto_xof_shake128)
+{
+ zend_string *out;
+ unsigned char *msg;
+ zend_long out_len;
+ size_t msg_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls",
+ &out_len, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ if (crypto_xof_shake128((unsigned char *) ZSTR_VAL(out), (size_t) out_len,
+ msg, (unsigned long long) msg_len) != 0) {
+ zend_string_efree(out);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake128_init)
+{
+ crypto_xof_shake128_state state_tmp;
+ zend_string *state;
+ zend_long domain = -1;
+ bool domain_is_null = 1;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(domain, domain_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ memset(&state_tmp, 0, sizeof state_tmp);
+ if (domain_is_null) {
+ if (crypto_xof_shake128_init(&state_tmp) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ } else {
+ if (domain < 0x01 || domain > 0x7f) {
+ zend_argument_error(sodium_exception_ce, 1, "must be between 0x01 and 0x7f");
+ RETURN_THROWS();
+ }
+ if (crypto_xof_shake128_init_with_domain(&state_tmp, (unsigned char) domain) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ }
+ state = zend_string_alloc(sizeof state_tmp, 0);
+ memcpy(ZSTR_VAL(state), &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(state)[sizeof state_tmp] = 0;
+ RETURN_STR(state);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake128_update)
+{
+ crypto_xof_shake128_state state_tmp;
+ zval *state_zv;
+ unsigned char *msg;
+ unsigned char *state;
+ size_t msg_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zs",
+ &state_zv, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_shake128_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_shake128_update(&state_tmp, msg, (unsigned long long) msg_len) != 0) {
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ RETURN_TRUE;
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake128_squeeze)
+{
+ crypto_xof_shake128_state state_tmp;
+ zval *state_zv;
+ zend_string *out;
+ unsigned char *state;
+ zend_long out_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zl",
+ &state_zv, &out_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_shake128_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 2, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_shake128_squeeze(&state_tmp, (unsigned char *) ZSTR_VAL(out), (size_t) out_len) != 0) {
+ zend_string_efree(out);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake256)
+{
+ zend_string *out;
+ unsigned char *msg;
+ zend_long out_len;
+ size_t msg_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls",
+ &out_len, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ if (crypto_xof_shake256((unsigned char *) ZSTR_VAL(out), (size_t) out_len,
+ msg, (unsigned long long) msg_len) != 0) {
+ zend_string_efree(out);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake256_init)
+{
+ crypto_xof_shake256_state state_tmp;
+ zend_string *state;
+ zend_long domain = -1;
+ bool domain_is_null = 1;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(domain, domain_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ memset(&state_tmp, 0, sizeof state_tmp);
+ if (domain_is_null) {
+ if (crypto_xof_shake256_init(&state_tmp) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ } else {
+ if (domain < 0x01 || domain > 0x7f) {
+ zend_argument_error(sodium_exception_ce, 1, "must be between 0x01 and 0x7f");
+ RETURN_THROWS();
+ }
+ if (crypto_xof_shake256_init_with_domain(&state_tmp, (unsigned char) domain) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ }
+ state = zend_string_alloc(sizeof state_tmp, 0);
+ memcpy(ZSTR_VAL(state), &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(state)[sizeof state_tmp] = 0;
+ RETURN_STR(state);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake256_update)
+{
+ crypto_xof_shake256_state state_tmp;
+ zval *state_zv;
+ unsigned char *msg;
+ unsigned char *state;
+ size_t msg_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zs",
+ &state_zv, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_shake256_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_shake256_update(&state_tmp, msg, (unsigned long long) msg_len) != 0) {
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ RETURN_TRUE;
+}
+
+PHP_FUNCTION(sodium_crypto_xof_shake256_squeeze)
+{
+ crypto_xof_shake256_state state_tmp;
+ zval *state_zv;
+ zend_string *out;
+ unsigned char *state;
+ zend_long out_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zl",
+ &state_zv, &out_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_shake256_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 2, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_shake256_squeeze(&state_tmp, (unsigned char *) ZSTR_VAL(out), (size_t) out_len) != 0) {
+ zend_string_efree(out);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake128)
+{
+ zend_string *out;
+ unsigned char *msg;
+ zend_long out_len;
+ size_t msg_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls",
+ &out_len, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ if (crypto_xof_turboshake128((unsigned char *) ZSTR_VAL(out), (size_t) out_len,
+ msg, (unsigned long long) msg_len) != 0) {
+ zend_string_efree(out);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake128_init)
+{
+ crypto_xof_turboshake128_state state_tmp;
+ zend_string *state;
+ zend_long domain = -1;
+ bool domain_is_null = 1;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(domain, domain_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ memset(&state_tmp, 0, sizeof state_tmp);
+ if (domain_is_null) {
+ if (crypto_xof_turboshake128_init(&state_tmp) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ } else {
+ if (domain < 0x01 || domain > 0x7f) {
+ zend_argument_error(sodium_exception_ce, 1, "must be between 0x01 and 0x7f");
+ RETURN_THROWS();
+ }
+ if (crypto_xof_turboshake128_init_with_domain(&state_tmp, (unsigned char) domain) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ }
+ state = zend_string_alloc(sizeof state_tmp, 0);
+ memcpy(ZSTR_VAL(state), &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(state)[sizeof state_tmp] = 0;
+ RETURN_STR(state);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake128_update)
+{
+ crypto_xof_turboshake128_state state_tmp;
+ zval *state_zv;
+ unsigned char *msg;
+ unsigned char *state;
+ size_t msg_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zs",
+ &state_zv, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_turboshake128_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_turboshake128_update(&state_tmp, msg, (unsigned long long) msg_len) != 0) {
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ RETURN_TRUE;
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake128_squeeze)
+{
+ crypto_xof_turboshake128_state state_tmp;
+ zval *state_zv;
+ zend_string *out;
+ unsigned char *state;
+ zend_long out_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zl",
+ &state_zv, &out_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_turboshake128_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 2, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_turboshake128_squeeze(&state_tmp, (unsigned char *) ZSTR_VAL(out), (size_t) out_len) != 0) {
+ zend_string_efree(out);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake256)
+{
+ zend_string *out;
+ unsigned char *msg;
+ zend_long out_len;
+ size_t msg_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls",
+ &out_len, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ if (crypto_xof_turboshake256((unsigned char *) ZSTR_VAL(out), (size_t) out_len,
+ msg, (unsigned long long) msg_len) != 0) {
+ zend_string_efree(out);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake256_init)
+{
+ crypto_xof_turboshake256_state state_tmp;
+ zend_string *state;
+ zend_long domain = -1;
+ bool domain_is_null = 1;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(domain, domain_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ memset(&state_tmp, 0, sizeof state_tmp);
+ if (domain_is_null) {
+ if (crypto_xof_turboshake256_init(&state_tmp) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ } else {
+ if (domain < 0x01 || domain > 0x7f) {
+ zend_argument_error(sodium_exception_ce, 1, "must be between 0x01 and 0x7f");
+ RETURN_THROWS();
+ }
+ if (crypto_xof_turboshake256_init_with_domain(&state_tmp, (unsigned char) domain) != 0) {
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ }
+ state = zend_string_alloc(sizeof state_tmp, 0);
+ memcpy(ZSTR_VAL(state), &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(state)[sizeof state_tmp] = 0;
+ RETURN_STR(state);
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake256_update)
+{
+ crypto_xof_turboshake256_state state_tmp;
+ zval *state_zv;
+ unsigned char *msg;
+ unsigned char *state;
+ size_t msg_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zs",
+ &state_zv, &msg, &msg_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_turboshake256_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_turboshake256_update(&state_tmp, msg, (unsigned long long) msg_len) != 0) {
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ RETURN_TRUE;
+}
+
+PHP_FUNCTION(sodium_crypto_xof_turboshake256_squeeze)
+{
+ crypto_xof_turboshake256_state state_tmp;
+ zval *state_zv;
+ zend_string *out;
+ unsigned char *state;
+ zend_long out_len;
+ size_t state_len;
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "zl",
+ &state_zv, &out_len) == FAILURE) {
+ sodium_remove_param_values_from_backtrace(EG(exception));
+ RETURN_THROWS();
+ }
+ ZVAL_DEREF(state_zv);
+ if (Z_TYPE_P(state_zv) != IS_STRING) {
+ zend_argument_error(sodium_exception_ce, 1, "must be a reference to a state");
+ RETURN_THROWS();
+ }
+ sodium_separate_string(state_zv);
+ state = (unsigned char *) Z_STRVAL(*state_zv);
+ state_len = Z_STRLEN(*state_zv);
+ if (state_len != sizeof (crypto_xof_turboshake256_state)) {
+ zend_argument_error(sodium_exception_ce, 1, "must have a correct state length");
+ RETURN_THROWS();
+ }
+ if (out_len <= 0 || out_len > ZSTR_MAX_LEN) {
+ zend_argument_error(sodium_exception_ce, 2, "must be a positive integer");
+ RETURN_THROWS();
+ }
+ out = zend_string_alloc((size_t) out_len, 0);
+ memcpy(&state_tmp, state, sizeof state_tmp);
+ if (crypto_xof_turboshake256_squeeze(&state_tmp, (unsigned char *) ZSTR_VAL(out), (size_t) out_len) != 0) {
+ zend_string_efree(out);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ zend_throw_exception(sodium_exception_ce, "internal error", 0);
+ RETURN_THROWS();
+ }
+ memcpy(state, &state_tmp, sizeof state_tmp);
+ sodium_memzero(&state_tmp, sizeof state_tmp);
+ ZSTR_VAL(out)[out_len] = 0;
+ RETURN_NEW_STR(out);
+}
+#endif
diff --git a/ext/sodium/libsodium.stub.php b/ext/sodium/libsodium.stub.php
index 4bf6fade913..9fda5f4ddd7 100644
--- a/ext/sodium/libsodium.stub.php
+++ b/ext/sodium/libsodium.stub.php
@@ -801,4 +801,170 @@ function sodium_base642bin(#[\SensitiveParameter] string $string, int $id, strin
*/
function sodium_crypto_scalarmult_base(#[\SensitiveParameter] string $secret_key): string {}
+#ifdef crypto_ipcrypt_KEYBYTES
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_BYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_BYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_KEYBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_KEYBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_ND_KEYBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_ND_TWEAKBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_ND_TWEAKBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_ND_INPUTBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_ND_INPUTBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_ND_OUTPUTBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_ND_OUTPUTBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_NDX_KEYBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_NDX_TWEAKBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_NDX_TWEAKBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_NDX_INPUTBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_NDX_INPUTBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_NDX_OUTPUTBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_NDX_OUTPUTBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_PFX_KEYBYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_ipcrypt_PFX_BYTES
+ */
+const SODIUM_CRYPTO_IPCRYPT_PFX_BYTES = UNKNOWN;
+
+function sodium_crypto_ipcrypt_keygen(): string {}
+
+function sodium_crypto_ipcrypt_encrypt(string $ip, #[\SensitiveParameter] string $key): string {}
+
+function sodium_crypto_ipcrypt_decrypt(string $encrypted_ip, #[\SensitiveParameter] string $key): string {}
+
+function sodium_crypto_ipcrypt_nd_keygen(): string {}
+
+function sodium_crypto_ipcrypt_nd_encrypt(string $ip, #[\SensitiveParameter] string $key): 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_decrypt(string $ciphertext_hex, #[\SensitiveParameter] string $key): string {}
+
+function sodium_crypto_ipcrypt_pfx_keygen(): string {}
+
+function sodium_crypto_ipcrypt_pfx_encrypt(string $ip, #[\SensitiveParameter] string $key): string {}
+
+function sodium_crypto_ipcrypt_pfx_decrypt(string $encrypted_ip, #[\SensitiveParameter] string $key): string {}
+
+function sodium_bin2ip(string $bin): string {}
+
+function sodium_ip2bin(string $ip): string {}
+#endif
+
+#ifdef crypto_xof_shake128_STATEBYTES
+/**
+ * @var int
+ * @cvalue crypto_xof_shake128_BLOCKBYTES
+ */
+const SODIUM_CRYPTO_XOF_SHAKE128_BLOCKBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_shake128_STATEBYTES
+ */
+const SODIUM_CRYPTO_XOF_SHAKE128_STATEBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_shake256_BLOCKBYTES
+ */
+const SODIUM_CRYPTO_XOF_SHAKE256_BLOCKBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_shake256_STATEBYTES
+ */
+const SODIUM_CRYPTO_XOF_SHAKE256_STATEBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_turboshake128_BLOCKBYTES
+ */
+const SODIUM_CRYPTO_XOF_TURBOSHAKE128_BLOCKBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_turboshake128_STATEBYTES
+ */
+const SODIUM_CRYPTO_XOF_TURBOSHAKE128_STATEBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_turboshake256_BLOCKBYTES
+ */
+const SODIUM_CRYPTO_XOF_TURBOSHAKE256_BLOCKBYTES = UNKNOWN;
+/**
+ * @var int
+ * @cvalue crypto_xof_turboshake256_STATEBYTES
+ */
+const SODIUM_CRYPTO_XOF_TURBOSHAKE256_STATEBYTES = UNKNOWN;
+
+function sodium_crypto_xof_shake128(int $length, string $message): string {}
+
+function sodium_crypto_xof_shake128_init(?int $domain = null): string {}
+
+function sodium_crypto_xof_shake128_update(string &$state, string $message): true {}
+
+function sodium_crypto_xof_shake128_squeeze(string &$state, int $length): string {}
+
+function sodium_crypto_xof_shake256(int $length, string $message): string {}
+
+function sodium_crypto_xof_shake256_init(?int $domain = null): string {}
+
+function sodium_crypto_xof_shake256_update(string &$state, string $message): true {}
+
+function sodium_crypto_xof_shake256_squeeze(string &$state, int $length): string {}
+
+function sodium_crypto_xof_turboshake128(int $length, string $message): string {}
+
+function sodium_crypto_xof_turboshake128_init(?int $domain = null): string {}
+
+function sodium_crypto_xof_turboshake128_update(string &$state, string $message): true {}
+
+function sodium_crypto_xof_turboshake128_squeeze(string &$state, int $length): string {}
+
+function sodium_crypto_xof_turboshake256(int $length, string $message): string {}
+
+function sodium_crypto_xof_turboshake256_init(?int $domain = null): string {}
+
+function sodium_crypto_xof_turboshake256_update(string &$state, string $message): true {}
+
+function sodium_crypto_xof_turboshake256_squeeze(string &$state, int $length): string {}
+#endif
+
class SodiumException extends Exception {}
diff --git a/ext/sodium/libsodium_arginfo.h b/ext/sodium/libsodium_arginfo.h
index 0af7528eec7..1b291e9a233 100644
Binary files a/ext/sodium/libsodium_arginfo.h and b/ext/sodium/libsodium_arginfo.h differ
diff --git a/ext/sodium/tests/crypto_ipcrypt.phpt b/ext/sodium/tests/crypto_ipcrypt.phpt
new file mode 100644
index 00000000000..f398d21c70f
--- /dev/null
+++ b/ext/sodium/tests/crypto_ipcrypt.phpt
@@ -0,0 +1,63 @@
+--TEST--
+Check for libsodium ipcrypt
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_IPCRYPT_KEYBYTES')) print "skip libsodium without ipcrypt (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+$key = str_repeat("\x01", SODIUM_CRYPTO_IPCRYPT_KEYBYTES);
+
+/* Basic encrypt/decrypt roundtrip with IPv4 */
+$enc = sodium_crypto_ipcrypt_encrypt("192.168.1.1", $key);
+var_dump(sodium_crypto_ipcrypt_decrypt($enc, $key));
+
+/* Deterministic: same input produces same output */
+$enc2 = sodium_crypto_ipcrypt_encrypt("192.168.1.1", $key);
+var_dump($enc === $enc2);
+
+/* Different key produces different output */
+$key2 = str_repeat("\x02", SODIUM_CRYPTO_IPCRYPT_KEYBYTES);
+$enc3 = sodium_crypto_ipcrypt_encrypt("192.168.1.1", $key2);
+var_dump($enc !== $enc3);
+
+/* IPv6 roundtrip */
+$enc6 = sodium_crypto_ipcrypt_encrypt("::1", $key);
+var_dump(sodium_crypto_ipcrypt_decrypt($enc6, $key));
+
+/* Keygen produces correct length */
+$gen_key = sodium_crypto_ipcrypt_keygen();
+var_dump(strlen($gen_key) === SODIUM_CRYPTO_IPCRYPT_KEYBYTES);
+
+/* Error: wrong key length */
+try {
+ sodium_crypto_ipcrypt_encrypt("192.168.1.1", "short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: invalid IP */
+try {
+ sodium_crypto_ipcrypt_encrypt("not_an_ip", $key);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: wrong key length for decrypt */
+try {
+ sodium_crypto_ipcrypt_decrypt("::1", "short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+string(11) "192.168.1.1"
+bool(true)
+bool(true)
+string(3) "::1"
+bool(true)
+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
new file mode 100644
index 00000000000..fed36bbda44
--- /dev/null
+++ b/ext/sodium/tests/crypto_ipcrypt_nd.phpt
@@ -0,0 +1,98 @@
+--TEST--
+Check for libsodium ipcrypt non-deterministic (nd) and extended (ndx)
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_IPCRYPT_KEYBYTES')) print "skip libsodium without ipcrypt (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+/* ND: non-deterministic encrypt/decrypt roundtrip */
+$nd_key = sodium_crypto_ipcrypt_nd_keygen();
+var_dump(strlen($nd_key) === SODIUM_CRYPTO_IPCRYPT_ND_KEYBYTES);
+
+$ct = sodium_crypto_ipcrypt_nd_encrypt("192.168.1.1", $nd_key);
+var_dump(strlen($ct) === SODIUM_CRYPTO_IPCRYPT_ND_OUTPUTBYTES * 2);
+$pt = sodium_crypto_ipcrypt_nd_decrypt($ct, $nd_key);
+var_dump($pt);
+
+/* ND is non-deterministic: two encryptions of the same IP differ */
+$ct2 = sodium_crypto_ipcrypt_nd_encrypt("192.168.1.1", $nd_key);
+var_dump($ct !== $ct2);
+$pt2 = sodium_crypto_ipcrypt_nd_decrypt($ct2, $nd_key);
+var_dump($pt2);
+
+/* ND: IPv6 roundtrip */
+$ct6 = sodium_crypto_ipcrypt_nd_encrypt("::1", $nd_key);
+var_dump(sodium_crypto_ipcrypt_nd_decrypt($ct6, $nd_key));
+
+/* ND error: wrong key length */
+try {
+ sodium_crypto_ipcrypt_nd_encrypt("192.168.1.1", "short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* ND error: invalid IP */
+try {
+ sodium_crypto_ipcrypt_nd_encrypt("bad", $nd_key);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* ND error: bad ciphertext hex length */
+try {
+ sodium_crypto_ipcrypt_nd_decrypt("tooshort", $nd_key);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* NDX: keygen, encrypt, decrypt roundtrip */
+$ndx_key = sodium_crypto_ipcrypt_ndx_keygen();
+var_dump(strlen($ndx_key) === SODIUM_CRYPTO_IPCRYPT_NDX_KEYBYTES);
+
+$ct_ndx = sodium_crypto_ipcrypt_ndx_encrypt("10.0.0.1", $ndx_key);
+var_dump(strlen($ct_ndx) === SODIUM_CRYPTO_IPCRYPT_NDX_OUTPUTBYTES * 2);
+$pt_ndx = sodium_crypto_ipcrypt_ndx_decrypt($ct_ndx, $ndx_key);
+var_dump($pt_ndx);
+
+/* NDX is non-deterministic */
+$ct_ndx2 = sodium_crypto_ipcrypt_ndx_encrypt("10.0.0.1", $ndx_key);
+var_dump($ct_ndx !== $ct_ndx2);
+
+/* NDX: IPv6 */
+$ct_ndx6 = sodium_crypto_ipcrypt_ndx_encrypt("fe80::1", $ndx_key);
+var_dump(sodium_crypto_ipcrypt_ndx_decrypt($ct_ndx6, $ndx_key));
+
+/* NDX error: wrong key length */
+try {
+ sodium_crypto_ipcrypt_ndx_encrypt("10.0.0.1", "short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* NDX error: bad ciphertext hex */
+try {
+ sodium_crypto_ipcrypt_ndx_decrypt("tooshort", $ndx_key);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+bool(true)
+bool(true)
+string(11) "192.168.1.1"
+bool(true)
+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
+sodium_crypto_ipcrypt_nd_decrypt(): Argument #1 ($ciphertext_hex) must be a valid hex-encoded ciphertext
+bool(true)
+bool(true)
+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
+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
new file mode 100644
index 00000000000..1ee51ef1f0e
--- /dev/null
+++ b/ext/sodium/tests/crypto_ipcrypt_pfx.phpt
@@ -0,0 +1,65 @@
+--TEST--
+Check for libsodium ipcrypt prefix-preserving (pfx)
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_IPCRYPT_KEYBYTES')) print "skip libsodium without ipcrypt (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+$key = str_repeat("\x02", SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES);
+
+/* PFX encrypt/decrypt roundtrip with IPv4 */
+$enc = sodium_crypto_ipcrypt_pfx_encrypt("10.0.0.1", $key);
+var_dump(sodium_crypto_ipcrypt_pfx_decrypt($enc, $key));
+
+/* PFX is deterministic */
+$enc2 = sodium_crypto_ipcrypt_pfx_encrypt("10.0.0.1", $key);
+var_dump($enc === $enc2);
+
+/* PFX: IPv4 encrypted output looks like IPv4 */
+var_dump(filter_var($enc, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false);
+
+/* PFX: IPv6 roundtrip */
+$enc6 = sodium_crypto_ipcrypt_pfx_encrypt("::1", $key);
+var_dump(sodium_crypto_ipcrypt_pfx_decrypt($enc6, $key));
+
+/* PFX: IPv6 encrypted output is valid IPv6 */
+var_dump(filter_var($enc6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false);
+
+/* PFX: keygen produces correct length */
+$gen_key = sodium_crypto_ipcrypt_pfx_keygen();
+var_dump(strlen($gen_key) === SODIUM_CRYPTO_IPCRYPT_PFX_KEYBYTES);
+
+/* PFX error: wrong key length */
+try {
+ sodium_crypto_ipcrypt_pfx_encrypt("10.0.0.1", "short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* PFX error: invalid IP */
+try {
+ sodium_crypto_ipcrypt_pfx_encrypt("not_an_ip", $key);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* PFX error: wrong key length for decrypt */
+try {
+ sodium_crypto_ipcrypt_pfx_decrypt("10.0.0.1", "short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+string(8) "10.0.0.1"
+bool(true)
+bool(true)
+string(3) "::1"
+bool(true)
+bool(true)
+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
diff --git a/ext/sodium/tests/crypto_xof_shake128.phpt b/ext/sodium/tests/crypto_xof_shake128.phpt
new file mode 100644
index 00000000000..cb075b7ec28
--- /dev/null
+++ b/ext/sodium/tests/crypto_xof_shake128.phpt
@@ -0,0 +1,104 @@
+--TEST--
+Check for libsodium XOF SHAKE128
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_XOF_SHAKE128_STATEBYTES')) print "skip libsodium without XOF support (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+/* One-shot: NIST test vector SHAKE128("") */
+$out = sodium_crypto_xof_shake128(32, "");
+var_dump(bin2hex($out));
+
+/* One-shot: SHAKE128("abc") */
+$out = sodium_crypto_xof_shake128(32, "abc");
+var_dump(bin2hex($out));
+
+/* Variable output length */
+$out16 = sodium_crypto_xof_shake128(16, "abc");
+$out64 = sodium_crypto_xof_shake128(64, "abc");
+var_dump(strlen($out16));
+var_dump(strlen($out64));
+var_dump(bin2hex($out16) === substr(bin2hex($out64), 0, 32));
+
+/* Streaming matches one-shot */
+$state = sodium_crypto_xof_shake128_init();
+sodium_crypto_xof_shake128_update($state, "abc");
+$sq = sodium_crypto_xof_shake128_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* Multi-part update matches one-shot */
+$state = sodium_crypto_xof_shake128_init();
+sodium_crypto_xof_shake128_update($state, "a");
+sodium_crypto_xof_shake128_update($state, "bc");
+$sq = sodium_crypto_xof_shake128_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* Multiple squeezes concatenated match single squeeze */
+$state = sodium_crypto_xof_shake128_init();
+sodium_crypto_xof_shake128_update($state, "abc");
+$sq1 = sodium_crypto_xof_shake128_squeeze($state, 16);
+$sq2 = sodium_crypto_xof_shake128_squeeze($state, 16);
+var_dump(bin2hex($sq1 . $sq2));
+
+/* Domain separation produces different output */
+$state_d = sodium_crypto_xof_shake128_init(0x42);
+sodium_crypto_xof_shake128_update($state_d, "abc");
+$sq_d = sodium_crypto_xof_shake128_squeeze($state_d, 32);
+var_dump(bin2hex($sq_d));
+var_dump(bin2hex($sq_d) !== bin2hex($sq));
+
+/* Error: zero length */
+try {
+ sodium_crypto_xof_shake128(0, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: negative length */
+try {
+ sodium_crypto_xof_shake128(-1, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: domain out of range (0x00) */
+try {
+ sodium_crypto_xof_shake128_init(0);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: domain out of range (0x80) */
+try {
+ sodium_crypto_xof_shake128_init(0x80);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: bad state */
+try {
+ $bad = "not_a_state";
+ sodium_crypto_xof_shake128_update($bad, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+string(64) "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26"
+string(64) "5881092dd818bf5cf8a3ddb793fbcba74097d5c526a6d35f97b83351940f2cc8"
+int(16)
+int(64)
+bool(true)
+string(64) "5881092dd818bf5cf8a3ddb793fbcba74097d5c526a6d35f97b83351940f2cc8"
+string(64) "5881092dd818bf5cf8a3ddb793fbcba74097d5c526a6d35f97b83351940f2cc8"
+string(64) "5881092dd818bf5cf8a3ddb793fbcba74097d5c526a6d35f97b83351940f2cc8"
+string(64) "f3a3a89c329e644a7d2351744d9a28c953698b64102e912085ce1f6d79fa311e"
+bool(true)
+sodium_crypto_xof_shake128(): Argument #1 ($length) must be a positive integer
+sodium_crypto_xof_shake128(): Argument #1 ($length) must be a positive integer
+sodium_crypto_xof_shake128_init(): Argument #1 ($domain) must be between 0x01 and 0x7f
+sodium_crypto_xof_shake128_init(): Argument #1 ($domain) must be between 0x01 and 0x7f
+sodium_crypto_xof_shake128_update(): Argument #1 ($state) must have a correct state length
diff --git a/ext/sodium/tests/crypto_xof_shake256.phpt b/ext/sodium/tests/crypto_xof_shake256.phpt
new file mode 100644
index 00000000000..3e691671332
--- /dev/null
+++ b/ext/sodium/tests/crypto_xof_shake256.phpt
@@ -0,0 +1,80 @@
+--TEST--
+Check for libsodium XOF SHAKE256
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_XOF_SHAKE256_STATEBYTES')) print "skip libsodium without XOF support (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+/* One-shot: NIST test vector SHAKE256("") */
+$out = sodium_crypto_xof_shake256(32, "");
+var_dump(bin2hex($out));
+
+/* One-shot: SHAKE256("abc") */
+$out = sodium_crypto_xof_shake256(32, "abc");
+var_dump(bin2hex($out));
+
+/* Variable output length */
+$out16 = sodium_crypto_xof_shake256(16, "abc");
+$out64 = sodium_crypto_xof_shake256(64, "abc");
+var_dump(strlen($out16));
+var_dump(strlen($out64));
+var_dump(bin2hex($out16) === substr(bin2hex($out64), 0, 32));
+
+/* Streaming matches one-shot */
+$state = sodium_crypto_xof_shake256_init();
+sodium_crypto_xof_shake256_update($state, "abc");
+$sq = sodium_crypto_xof_shake256_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* Multi-part update */
+$state = sodium_crypto_xof_shake256_init();
+sodium_crypto_xof_shake256_update($state, "a");
+sodium_crypto_xof_shake256_update($state, "bc");
+$sq = sodium_crypto_xof_shake256_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* Domain separation */
+$state_d = sodium_crypto_xof_shake256_init(0x42);
+sodium_crypto_xof_shake256_update($state_d, "abc");
+$sq_d = sodium_crypto_xof_shake256_squeeze($state_d, 32);
+var_dump(bin2hex($sq_d));
+
+/* Error: zero length */
+try {
+ sodium_crypto_xof_shake256(0, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: bad state */
+try {
+ $bad = "not_a_state";
+ sodium_crypto_xof_shake256_update($bad, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: squeeze bad length */
+try {
+ $state = sodium_crypto_xof_shake256_init();
+ sodium_crypto_xof_shake256_update($state, "abc");
+ sodium_crypto_xof_shake256_squeeze($state, 0);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+string(64) "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f"
+string(64) "483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739"
+int(16)
+int(64)
+bool(true)
+string(64) "483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739"
+string(64) "483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739"
+string(64) "1259d63c872d50fee4500685419f489966971c0a77e2058fab0e48dfb12d24bc"
+sodium_crypto_xof_shake256(): Argument #1 ($length) must be a positive integer
+sodium_crypto_xof_shake256_update(): Argument #1 ($state) must have a correct state length
+sodium_crypto_xof_shake256_squeeze(): Argument #2 ($length) must be a positive integer
diff --git a/ext/sodium/tests/crypto_xof_turboshake.phpt b/ext/sodium/tests/crypto_xof_turboshake.phpt
new file mode 100644
index 00000000000..1f404140cc0
--- /dev/null
+++ b/ext/sodium/tests/crypto_xof_turboshake.phpt
@@ -0,0 +1,112 @@
+--TEST--
+Check for libsodium XOF TurboSHAKE128 and TurboSHAKE256
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_XOF_TURBOSHAKE128_STATEBYTES')) print "skip libsodium without XOF support (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+/* TurboSHAKE128 one-shot */
+$out = sodium_crypto_xof_turboshake128(32, "");
+var_dump(bin2hex($out));
+$out = sodium_crypto_xof_turboshake128(32, "abc");
+var_dump(bin2hex($out));
+
+/* TurboSHAKE256 one-shot */
+$out = sodium_crypto_xof_turboshake256(32, "");
+var_dump(bin2hex($out));
+$out = sodium_crypto_xof_turboshake256(32, "abc");
+var_dump(bin2hex($out));
+
+/* TurboSHAKE128 streaming matches one-shot */
+$state = sodium_crypto_xof_turboshake128_init();
+sodium_crypto_xof_turboshake128_update($state, "abc");
+$sq = sodium_crypto_xof_turboshake128_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* TurboSHAKE256 streaming matches one-shot */
+$state = sodium_crypto_xof_turboshake256_init();
+sodium_crypto_xof_turboshake256_update($state, "abc");
+$sq = sodium_crypto_xof_turboshake256_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* TurboSHAKE128 multi-part update */
+$state = sodium_crypto_xof_turboshake128_init();
+sodium_crypto_xof_turboshake128_update($state, "a");
+sodium_crypto_xof_turboshake128_update($state, "bc");
+$sq = sodium_crypto_xof_turboshake128_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* TurboSHAKE256 multi-part update */
+$state = sodium_crypto_xof_turboshake256_init();
+sodium_crypto_xof_turboshake256_update($state, "a");
+sodium_crypto_xof_turboshake256_update($state, "bc");
+$sq = sodium_crypto_xof_turboshake256_squeeze($state, 32);
+var_dump(bin2hex($sq));
+
+/* TurboSHAKE128 domain separation */
+$state_d = sodium_crypto_xof_turboshake128_init(0x42);
+sodium_crypto_xof_turboshake128_update($state_d, "abc");
+$sq_d = sodium_crypto_xof_turboshake128_squeeze($state_d, 32);
+var_dump(bin2hex($sq_d));
+
+/* TurboSHAKE256 domain separation */
+$state_d = sodium_crypto_xof_turboshake256_init(0x42);
+sodium_crypto_xof_turboshake256_update($state_d, "abc");
+$sq_d = sodium_crypto_xof_turboshake256_squeeze($state_d, 32);
+var_dump(bin2hex($sq_d));
+
+/* Variable output length */
+$out16 = sodium_crypto_xof_turboshake128(16, "abc");
+$out64 = sodium_crypto_xof_turboshake128(64, "abc");
+var_dump(strlen($out16));
+var_dump(strlen($out64));
+var_dump(bin2hex($out16) === substr(bin2hex($out64), 0, 32));
+
+/* TurboSHAKE128 error: zero length */
+try {
+ sodium_crypto_xof_turboshake128(0, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* TurboSHAKE256 error: bad state */
+try {
+ $bad = "not_a_state";
+ sodium_crypto_xof_turboshake256_update($bad, "test");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* TurboSHAKE128 error: domain out of range */
+try {
+ sodium_crypto_xof_turboshake128_init(0);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+try {
+ sodium_crypto_xof_turboshake256_init(0x80);
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+string(64) "1e415f1c5983aff2169217277d17bb538cd945a397ddec541f1ce41af2c1b74c"
+string(64) "dcf1646dfe993a8eb6b782d1faaca6d82416a5dcf1de98ee3c6dbc5e1dc63018"
+string(64) "367a329dafea871c7802ec67f905ae13c57695dc2c6663c61035f59a18f8e7db"
+string(64) "63824b1431a7372e85edc022c9d7afdd027472fcfa33c887d6f5aaf8dc5d4db6"
+string(64) "dcf1646dfe993a8eb6b782d1faaca6d82416a5dcf1de98ee3c6dbc5e1dc63018"
+string(64) "63824b1431a7372e85edc022c9d7afdd027472fcfa33c887d6f5aaf8dc5d4db6"
+string(64) "dcf1646dfe993a8eb6b782d1faaca6d82416a5dcf1de98ee3c6dbc5e1dc63018"
+string(64) "63824b1431a7372e85edc022c9d7afdd027472fcfa33c887d6f5aaf8dc5d4db6"
+string(64) "3f7566fb02630888cba2af090aaf544ac6e85484d8662335b0c0fd6b3c83ea1b"
+string(64) "0137a7e8cca0b99e9bdd557a7caac1f21d65bc2a2ccbde1e1f8c702352a2bf30"
+int(16)
+int(64)
+bool(true)
+sodium_crypto_xof_turboshake128(): Argument #1 ($length) must be a positive integer
+sodium_crypto_xof_turboshake256_update(): Argument #1 ($state) must have a correct state length
+sodium_crypto_xof_turboshake128_init(): Argument #1 ($domain) must be between 0x01 and 0x7f
+sodium_crypto_xof_turboshake256_init(): Argument #1 ($domain) must be between 0x01 and 0x7f
diff --git a/ext/sodium/tests/sodium_bin2ip.phpt b/ext/sodium/tests/sodium_bin2ip.phpt
new file mode 100644
index 00000000000..ab72b4b297a
--- /dev/null
+++ b/ext/sodium/tests/sodium_bin2ip.phpt
@@ -0,0 +1,69 @@
+--TEST--
+Check for libsodium bin2ip/ip2bin helpers
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_IPCRYPT_KEYBYTES')) print "skip libsodium without ipcrypt (requires >= 1.0.21)";
+?>
+--FILE--
+<?php
+/* IPv4 roundtrip */
+$bin = sodium_ip2bin("192.168.1.1");
+var_dump(strlen($bin));
+var_dump(bin2hex($bin));
+var_dump(sodium_bin2ip($bin));
+
+/* IPv6 roundtrip */
+$bin6 = sodium_ip2bin("::1");
+var_dump(bin2hex($bin6));
+var_dump(sodium_bin2ip($bin6));
+
+/* IPv4-mapped IPv6 preserved as IPv4 */
+$bin4 = sodium_ip2bin("10.0.0.1");
+var_dump(bin2hex($bin4));
+var_dump(sodium_bin2ip($bin4));
+
+/* Full IPv6 address */
+$bin_full = sodium_ip2bin("fe80::1");
+var_dump(bin2hex($bin_full));
+var_dump(sodium_bin2ip($bin_full));
+
+/* ip2bin -> bin2ip roundtrip for various addresses */
+$addrs = ["0.0.0.0", "255.255.255.255", "127.0.0.1", "::ffff:192.168.0.1", "2001:db8::1"];
+foreach ($addrs as $addr) {
+ $result = sodium_bin2ip(sodium_ip2bin($addr));
+ echo "$addr => $result\n";
+}
+
+/* Error: bin2ip wrong length */
+try {
+ sodium_bin2ip("short");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+
+/* Error: ip2bin invalid address */
+try {
+ sodium_ip2bin("not_an_ip");
+} catch (SodiumException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+int(16)
+string(32) "00000000000000000000ffffc0a80101"
+string(11) "192.168.1.1"
+string(32) "00000000000000000000000000000001"
+string(3) "::1"
+string(32) "00000000000000000000ffff0a000001"
+string(8) "10.0.0.1"
+string(32) "fe800000000000000000000000000001"
+string(7) "fe80::1"
+0.0.0.0 => 0.0.0.0
+255.255.255.255 => 255.255.255.255
+127.0.0.1 => 127.0.0.1
+::ffff:192.168.0.1 => 192.168.0.1
+2001:db8::1 => 2001:db8::1
+sodium_bin2ip(): Argument #1 ($bin) must be 16 bytes long
+sodium_ip2bin(): Argument #1 ($ip) must be a valid IP address