From 9d50bdae44446194b2dd778fa4f560561af6919b Mon Sep 17 00:00:00 2001 From: slowsats <116310285+slowsats@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:40:18 -0400 Subject: [PATCH 1/4] feat: add crypto_hash, crypto_hash_sha256 and crypto_hash_sha512 Co-Authored-By: Claude Fable 5 --- README.md | 6 ++ cpp/react-native-libsodium.cpp | 99 ++++++++++++++++++++ example/src/components/TestResults.tsx | 3 + example/src/tests/crypto_hash_sha256_test.ts | 28 ++++++ example/src/tests/crypto_hash_sha512_test.ts | 23 +++++ example/src/tests/crypto_hash_test.ts | 24 +++++ src/lib.native.ts | 69 ++++++++++++++ src/lib.ts | 8 ++ 8 files changed, 260 insertions(+) create mode 100644 example/src/tests/crypto_hash_sha256_test.ts create mode 100644 example/src/tests/crypto_hash_sha512_test.ts create mode 100644 example/src/tests/crypto_hash_test.ts diff --git a/README.md b/README.md index 749bf57..a387c79 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ import { crypto_generichash_KEYBYTES, crypto_generichash_KEYBYTES_MIN, crypto_generichash_KEYBYTES_MAX, + crypto_hash, + crypto_hash_BYTES, + crypto_hash_sha256, + crypto_hash_sha256_BYTES, + crypto_hash_sha512, + crypto_hash_sha512_BYTES, crypto_secretbox_easy, crypto_secretbox_KEYBYTES, crypto_secretbox_keygen, diff --git a/cpp/react-native-libsodium.cpp b/cpp/react-native-libsodium.cpp index a97e74b..ddb2d37 100644 --- a/cpp/react-native-libsodium.cpp +++ b/cpp/react-native-libsodium.cpp @@ -207,6 +207,9 @@ namespace ReactNativeLibsodium jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_pwhash_BYTES_MIN", static_cast(crypto_pwhash_BYTES_MIN)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_generichash_KEYBYTES_MIN", static_cast(crypto_generichash_KEYBYTES_MIN)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_generichash_KEYBYTES_MAX", static_cast(crypto_generichash_KEYBYTES_MAX)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_BYTES", static_cast(crypto_hash_BYTES)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha256_BYTES", static_cast(crypto_hash_sha256_BYTES)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha512_BYTES", static_cast(crypto_hash_sha512_BYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_sign_SEEDBYTES", static_cast(crypto_sign_SEEDBYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_auth_BYTES", static_cast(crypto_auth_BYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_auth_KEYBYTES", static_cast(crypto_auth_KEYBYTES)); @@ -1526,6 +1529,102 @@ namespace ReactNativeLibsodium jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_generichash", std::move(jsi_crypto_generichash)); + auto jsi_crypto_hash = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_hash"), + 1, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_hash"; + + std::string messageArgumentName = "message"; + unsigned int messageArgumentPosition = 0; + JsiArgType messageArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[messageArgumentPosition], messageArgumentName, true); + + std::vector hash(crypto_hash_BYTES); + int result = -1; + + if (messageArgType == JsiArgType::string) + { + std::string messageString = arguments[messageArgumentPosition].asString(runtime).utf8(runtime); + result = crypto_hash(hash.data(), reinterpret_cast(messageString.data()), messageString.length()); + } + else + { + auto messageArrayBuffer = arguments[messageArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + result = crypto_hash(hash.data(), messageArrayBuffer.data(runtime), messageArrayBuffer.length(runtime)); + } + + throwOnBadResult(functionName, runtime, result); + return arrayBufferAsObject(runtime, hash); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash", std::move(jsi_crypto_hash)); + + auto jsi_crypto_hash_sha256 = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_hash_sha256"), + 1, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_hash_sha256"; + + std::string messageArgumentName = "message"; + unsigned int messageArgumentPosition = 0; + JsiArgType messageArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[messageArgumentPosition], messageArgumentName, true); + + std::vector hash(crypto_hash_sha256_BYTES); + int result = -1; + + if (messageArgType == JsiArgType::string) + { + std::string messageString = arguments[messageArgumentPosition].asString(runtime).utf8(runtime); + result = crypto_hash_sha256(hash.data(), reinterpret_cast(messageString.data()), messageString.length()); + } + else + { + auto messageArrayBuffer = arguments[messageArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + result = crypto_hash_sha256(hash.data(), messageArrayBuffer.data(runtime), messageArrayBuffer.length(runtime)); + } + + throwOnBadResult(functionName, runtime, result); + return arrayBufferAsObject(runtime, hash); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha256", std::move(jsi_crypto_hash_sha256)); + + auto jsi_crypto_hash_sha512 = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_hash_sha512"), + 1, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_hash_sha512"; + + std::string messageArgumentName = "message"; + unsigned int messageArgumentPosition = 0; + JsiArgType messageArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[messageArgumentPosition], messageArgumentName, true); + + std::vector hash(crypto_hash_sha512_BYTES); + int result = -1; + + if (messageArgType == JsiArgType::string) + { + std::string messageString = arguments[messageArgumentPosition].asString(runtime).utf8(runtime); + result = crypto_hash_sha512(hash.data(), reinterpret_cast(messageString.data()), messageString.length()); + } + else + { + auto messageArrayBuffer = arguments[messageArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + result = crypto_hash_sha512(hash.data(), messageArrayBuffer.data(runtime), messageArrayBuffer.length(runtime)); + } + + throwOnBadResult(functionName, runtime, result); + return arrayBufferAsObject(runtime, hash); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha512", std::move(jsi_crypto_hash_sha512)); + auto jsi_crypto_kdf_hkdf_sha256_extract = jsi::Function::createFromHostFunction( jsiRuntime, jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_kdf_hkdf_sha256_extract"), diff --git a/example/src/components/TestResults.tsx b/example/src/components/TestResults.tsx index 83c6257..aa15784 100644 --- a/example/src/components/TestResults.tsx +++ b/example/src/components/TestResults.tsx @@ -15,6 +15,9 @@ import '../tests/crypto_box_open_easy_test'; import '../tests/crypto_box_seal_test'; import '../tests/crypto_box_seed_keypair_test'; import '../tests/crypto_generichash_test'; +import '../tests/crypto_hash_sha256_test'; +import '../tests/crypto_hash_sha512_test'; +import '../tests/crypto_hash_test'; import '../tests/crypto_kdf_derive_from_key_test'; import '../tests/crypto_kdf_keygen_test'; import '../tests/crypto_pwhash_test'; diff --git a/example/src/tests/crypto_hash_sha256_test.ts b/example/src/tests/crypto_hash_sha256_test.ts new file mode 100644 index 0000000..a1a77eb --- /dev/null +++ b/example/src/tests/crypto_hash_sha256_test.ts @@ -0,0 +1,28 @@ +import { + crypto_hash_sha256, + crypto_hash_sha256_BYTES, + to_hex, +} from 'react-native-libsodium'; +import { expect, test } from '../utils/testRunner'; + +test('crypto_hash_sha256', () => { + // FIPS 180-4 SHA-256 test vector for "abc" + const abcHash = + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'; + + expect(crypto_hash_sha256('abc').length).toEqual(crypto_hash_sha256_BYTES); + expect(to_hex(crypto_hash_sha256('abc'))).toBe(abcHash); + expect(to_hex(crypto_hash_sha256(new Uint8Array([97, 98, 99])))).toBe( + abcHash + ); + expect(crypto_hash_sha256('abc', 'hex')).toBe(abcHash); + + // FIPS 180-4 SHA-256 test vector for the two-block message + expect( + to_hex( + crypto_hash_sha256( + 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq' + ) + ) + ).toBe('248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1'); +}); diff --git a/example/src/tests/crypto_hash_sha512_test.ts b/example/src/tests/crypto_hash_sha512_test.ts new file mode 100644 index 0000000..d1d832c --- /dev/null +++ b/example/src/tests/crypto_hash_sha512_test.ts @@ -0,0 +1,23 @@ +import { + crypto_hash, + crypto_hash_sha512, + crypto_hash_sha512_BYTES, + to_hex, +} from 'react-native-libsodium'; +import { expect, test } from '../utils/testRunner'; + +test('crypto_hash_sha512', () => { + // FIPS 180-4 SHA-512 test vector for "abc" + const abcHash = + 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'; + + expect(crypto_hash_sha512('abc').length).toEqual(crypto_hash_sha512_BYTES); + expect(to_hex(crypto_hash_sha512('abc'))).toBe(abcHash); + expect(to_hex(crypto_hash_sha512(new Uint8Array([97, 98, 99])))).toBe( + abcHash + ); + expect(crypto_hash_sha512('abc', 'hex')).toBe(abcHash); + + // crypto_hash is SHA-512, so both must produce the same digest + expect(crypto_hash_sha512('abc')).toEqual(crypto_hash('abc')); +}); diff --git a/example/src/tests/crypto_hash_test.ts b/example/src/tests/crypto_hash_test.ts new file mode 100644 index 0000000..fc5e4b6 --- /dev/null +++ b/example/src/tests/crypto_hash_test.ts @@ -0,0 +1,24 @@ +import { crypto_hash, crypto_hash_BYTES, to_hex } from 'react-native-libsodium'; +import { expect, test } from '../utils/testRunner'; + +test('crypto_hash', () => { + // FIPS 180-4 SHA-512 test vector for "abc" + const abcHash = + 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'; + + expect(crypto_hash('abc').length).toEqual(crypto_hash_BYTES); + expect(to_hex(crypto_hash('abc'))).toBe(abcHash); + expect(to_hex(crypto_hash(new Uint8Array([97, 98, 99])))).toBe(abcHash); + expect(crypto_hash('abc', 'hex')).toBe(abcHash); + + // FIPS 180-4 SHA-512 test vector for the two-block message + expect( + to_hex( + crypto_hash( + 'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu' + ) + ) + ).toBe( + '8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909' + ); +}); diff --git a/src/lib.native.ts b/src/lib.native.ts index bfa7ea8..e0d26fe 100644 --- a/src/lib.native.ts +++ b/src/lib.native.ts @@ -52,6 +52,9 @@ declare global { var jsi_crypto_generichash_KEYBYTES: number; var jsi_crypto_generichash_KEYBYTES_MIN: number; var jsi_crypto_generichash_KEYBYTES_MAX: number; + var jsi_crypto_hash_BYTES: number; + var jsi_crypto_hash_sha256_BYTES: number; + var jsi_crypto_hash_sha512_BYTES: number; var jsi_crypto_sign_SEEDBYTES: number; var jsi_crypto_pwhash_SALTBYTES: number; var jsi_crypto_pwhash_ALG_DEFAULT: number; @@ -149,6 +152,9 @@ declare global { message: string | ArrayBuffer, key?: ArrayBuffer | null | undefined ): ArrayBuffer; + function jsi_crypto_hash(message: string | ArrayBuffer): ArrayBuffer; + function jsi_crypto_hash_sha256(message: string | ArrayBuffer): ArrayBuffer; + function jsi_crypto_hash_sha512(message: string | ArrayBuffer): ArrayBuffer; function jsi_crypto_pwhash( keyLength: number, password: string | ArrayBuffer, @@ -221,6 +227,9 @@ export const crypto_generichash_KEYBYTES_MIN = global.jsi_crypto_generichash_KEYBYTES_MIN; export const crypto_generichash_KEYBYTES_MAX = global.jsi_crypto_generichash_KEYBYTES_MAX; +export const crypto_hash_BYTES = global.jsi_crypto_hash_BYTES; +export const crypto_hash_sha256_BYTES = global.jsi_crypto_hash_sha256_BYTES; +export const crypto_hash_sha512_BYTES = global.jsi_crypto_hash_sha512_BYTES; export const crypto_sign_SEEDBYTES = global.jsi_crypto_sign_SEEDBYTES; export const crypto_pwhash_SALTBYTES = global.jsi_crypto_pwhash_SALTBYTES; export const crypto_pwhash_ALG_DEFAULT = global.jsi_crypto_pwhash_ALG_DEFAULT; @@ -676,6 +685,60 @@ export function crypto_generichash( return convertToOutputFormat(result, outputFormat); } +export function crypto_hash( + message: string | Uint8Array, + outputFormat?: Uint8ArrayOutputFormat | null +): Uint8Array; +export function crypto_hash( + message: string | Uint8Array, + outputFormat: StringOutputFormat +): string; +export function crypto_hash( + message: string | Uint8Array, + outputFormat: OutputFormat +): unknown { + const messageParam = + typeof message === 'string' ? message : toArrayBuffer(message); + const result = global.jsi_crypto_hash(messageParam); + return convertToOutputFormat(result, outputFormat); +} + +export function crypto_hash_sha256( + message: string | Uint8Array, + outputFormat?: Uint8ArrayOutputFormat | null +): Uint8Array; +export function crypto_hash_sha256( + message: string | Uint8Array, + outputFormat: StringOutputFormat +): string; +export function crypto_hash_sha256( + message: string | Uint8Array, + outputFormat: OutputFormat +): unknown { + const messageParam = + typeof message === 'string' ? message : toArrayBuffer(message); + const result = global.jsi_crypto_hash_sha256(messageParam); + return convertToOutputFormat(result, outputFormat); +} + +export function crypto_hash_sha512( + message: string | Uint8Array, + outputFormat?: Uint8ArrayOutputFormat | null +): Uint8Array; +export function crypto_hash_sha512( + message: string | Uint8Array, + outputFormat: StringOutputFormat +): string; +export function crypto_hash_sha512( + message: string | Uint8Array, + outputFormat: OutputFormat +): unknown { + const messageParam = + typeof message === 'string' ? message : toArrayBuffer(message); + const result = global.jsi_crypto_hash_sha512(messageParam); + return convertToOutputFormat(result, outputFormat); +} + export function crypto_pwhash( keyLength: number, password: string | Uint8Array, @@ -899,6 +962,12 @@ export default { crypto_generichash_KEYBYTES, crypto_generichash_KEYBYTES_MIN, crypto_generichash_KEYBYTES_MAX, + crypto_hash, + crypto_hash_BYTES, + crypto_hash_sha256, + crypto_hash_sha256_BYTES, + crypto_hash_sha512, + crypto_hash_sha512_BYTES, crypto_kdf_derive_from_key, crypto_kdf_CONTEXTBYTES, crypto_kdf_KEYBYTES, diff --git a/src/lib.ts b/src/lib.ts index 1a90fc1..f750b3a 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -170,6 +170,8 @@ export const ready = new Promise(async (resolve) => { crypto_generichash_keygen = lib.crypto_generichash_keygen; crypto_generichash_update = lib.crypto_generichash_update; crypto_hash = lib.crypto_hash; + crypto_hash_sha256 = lib.crypto_hash_sha256; + crypto_hash_sha512 = lib.crypto_hash_sha512; crypto_kdf_derive_from_key = lib.crypto_kdf_derive_from_key; crypto_kdf_keygen = lib.crypto_kdf_keygen; crypto_kx_client_session_keys = lib.crypto_kx_client_session_keys; @@ -290,6 +292,8 @@ export const ready = new Promise(async (resolve) => { crypto_generichash_KEYBYTES_MAX = lib.crypto_generichash_KEYBYTES_MAX; crypto_generichash_KEYBYTES_MIN = lib.crypto_generichash_KEYBYTES_MIN; crypto_hash_BYTES = lib.crypto_hash_BYTES; + crypto_hash_sha256_BYTES = lib.crypto_hash_sha256_BYTES; + crypto_hash_sha512_BYTES = lib.crypto_hash_sha512_BYTES; crypto_kdf_BYTES_MAX = lib.crypto_kdf_BYTES_MAX; crypto_kdf_BYTES_MIN = lib.crypto_kdf_BYTES_MIN; crypto_kdf_CONTEXTBYTES = lib.crypto_kdf_CONTEXTBYTES; @@ -423,6 +427,8 @@ export let crypto_generichash_init = sodium.crypto_generichash_init; export let crypto_generichash_keygen = sodium.crypto_generichash_keygen; export let crypto_generichash_update = sodium.crypto_generichash_update; export let crypto_hash = sodium.crypto_hash; +export let crypto_hash_sha256 = sodium.crypto_hash_sha256; +export let crypto_hash_sha512 = sodium.crypto_hash_sha512; export let crypto_kdf_derive_from_key = sodium.crypto_kdf_derive_from_key; export let crypto_kdf_keygen = sodium.crypto_kdf_keygen; export let crypto_kx_client_session_keys = sodium.crypto_kx_client_session_keys; @@ -548,6 +554,8 @@ export let crypto_generichash_KEYBYTES_MAX = export let crypto_generichash_KEYBYTES_MIN = sodium.crypto_generichash_KEYBYTES_MIN; export let crypto_hash_BYTES = sodium.crypto_hash_BYTES; +export let crypto_hash_sha256_BYTES = sodium.crypto_hash_sha256_BYTES; +export let crypto_hash_sha512_BYTES = sodium.crypto_hash_sha512_BYTES; export let crypto_kdf_BYTES_MAX = sodium.crypto_kdf_BYTES_MAX; export let crypto_kdf_BYTES_MIN = sodium.crypto_kdf_BYTES_MIN; export let crypto_kdf_CONTEXTBYTES = sodium.crypto_kdf_CONTEXTBYTES; From fa4eb84082098841c4eaf2853f15aac517a73f40 Mon Sep 17 00:00:00 2001 From: slowsats <116310285+slowsats@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:43:38 -0400 Subject: [PATCH 2/4] feat: add memzero Co-Authored-By: Claude Fable 5 --- README.md | 1 + cpp/react-native-libsodium.cpp | 37 ++++++++++++++++++++++++++ example/src/components/TestResults.tsx | 1 + example/src/tests/memzero_test.ts | 17 ++++++++++++ src/lib.native.ts | 21 +++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 example/src/tests/memzero_test.ts diff --git a/README.md b/README.md index a387c79..3dc05de 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ import { crypto_sign_SEEDBYTES, crypto_sign_verify_detached, from_base64, + memzero, randombytes_buf, randombytes_uniform, to_base64, diff --git a/cpp/react-native-libsodium.cpp b/cpp/react-native-libsodium.cpp index ddb2d37..4850c9a 100644 --- a/cpp/react-native-libsodium.cpp +++ b/cpp/react-native-libsodium.cpp @@ -358,6 +358,43 @@ namespace ReactNativeLibsodium jsiRuntime.global().setProperty(jsiRuntime, "jsi_to_hex", std::move(jsi_to_hex)); + auto jsi_memzero = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_memzero"), + 3, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "memzero"; + + std::string bufferArgumentName = "buffer"; + unsigned int bufferArgumentPosition = 0; + validateIsArrayBuffer(functionName, runtime, arguments[bufferArgumentPosition], bufferArgumentName, true); + + std::string offsetArgumentName = "offset"; + unsigned int offsetArgumentPosition = 1; + validateIsNumber(functionName, runtime, arguments[offsetArgumentPosition], offsetArgumentName, true); + + std::string lengthArgumentName = "length"; + unsigned int lengthArgumentPosition = 2; + validateIsNumber(functionName, runtime, arguments[lengthArgumentPosition], lengthArgumentName, true); + + auto bufferArrayBuffer = arguments[bufferArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + double offset = arguments[offsetArgumentPosition].asNumber(); + double length = arguments[lengthArgumentPosition].asNumber(); + + if (offset < 0 || length < 0 || offset + length > bufferArrayBuffer.length(runtime)) + { + throw jsi::JSError(runtime, "invalid offset or length"); + } + + // wipe the ArrayBuffer's actual backing memory so the change is + // visible to the caller's Uint8Array view + sodium_memzero(bufferArrayBuffer.data(runtime) + static_cast(offset), static_cast(length)); + return jsi::Value::undefined(); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_memzero", std::move(jsi_memzero)); + auto jsi_randombytes_buf = jsi::Function::createFromHostFunction( jsiRuntime, jsi::PropNameID::forUtf8(jsiRuntime, "jsi_randombytes_buf"), diff --git a/example/src/components/TestResults.tsx b/example/src/components/TestResults.tsx index aa15784..c72a263 100644 --- a/example/src/components/TestResults.tsx +++ b/example/src/components/TestResults.tsx @@ -30,6 +30,7 @@ import '../tests/crypto_sign_keypair_test'; import '../tests/crypto_sign_seed_keypair_test'; import '../tests/crypto_sign_verify_detached_test'; import '../tests/from_base64_test'; +import '../tests/memzero_test'; import '../tests/randombytes_buf_test'; import '../tests/randombytes_uniform_test'; import '../tests/to_base64_test'; diff --git a/example/src/tests/memzero_test.ts b/example/src/tests/memzero_test.ts new file mode 100644 index 0000000..1ab85d6 --- /dev/null +++ b/example/src/tests/memzero_test.ts @@ -0,0 +1,17 @@ +import { memzero } from 'react-native-libsodium'; +import { expect, test } from '../utils/testRunner'; + +test('memzero', () => { + const bytes = new Uint8Array([222, 173, 190, 239]); + memzero(bytes); + expect(bytes).toEqual(new Uint8Array([0, 0, 0, 0])); + + // only the bytes of the view must be wiped, not the whole underlying buffer + const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + memzero(buffer.subarray(2, 6)); + expect(buffer).toEqual(new Uint8Array([1, 2, 0, 0, 0, 0, 7, 8])); + + expect(() => memzero([0, 1, 2, 3] as unknown as Uint8Array)).toThrow( + 'Only Uint8Array instances can be wiped' + ); +}); diff --git a/src/lib.native.ts b/src/lib.native.ts index e0d26fe..da99063 100644 --- a/src/lib.native.ts +++ b/src/lib.native.ts @@ -86,6 +86,11 @@ declare global { variant: base64_variants ): string; function jsi_to_hex(input: string | ArrayBuffer): string; + function jsi_memzero( + buffer: ArrayBuffer, + offset: number, + length: number + ): void; function jsi_randombytes_buf(length: number): ArrayBuffer; function jsi_randombytes_uniform(upper_bound: number): number; function jsi_crypto_secretbox_keygen(): ArrayBuffer; @@ -271,6 +276,21 @@ export function to_hex(input: string | Uint8Array): string { return global.jsi_to_hex(inputParam); } +export function memzero(bytes: Uint8Array): void { + if (!(bytes instanceof Uint8Array)) { + throw new TypeError('Only Uint8Array instances can be wiped'); + } + // the view's underlying buffer is passed to sodium_memzero so the bytes + // are wiped in place instead of on a copy + const buffer = bytes.buffer; + if (!(buffer instanceof ArrayBuffer)) { + throw new TypeError( + 'Only ArrayBuffer backed Uint8Array instances can be wiped' + ); + } + global.jsi_memzero(buffer, bytes.byteOffset, bytes.byteLength); +} + export function randombytes_buf( length: number, outputFormat?: Uint8ArrayOutputFormat | null @@ -990,6 +1010,7 @@ export default { crypto_sign_keypair, crypto_sign_verify_detached, from_base64, + memzero, randombytes_buf, randombytes_uniform, ready, From 3072c5a0fde6557ee0e9da59f478ed83408da641 Mon Sep 17 00:00:00 2001 From: slowsats <116310285+slowsats@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:49:20 -0400 Subject: [PATCH 3/4] feat: accept Uint8Array and null additional_data in crypto_aead_xchacha20poly1305_ietf functions Co-Authored-By: Claude Fable 5 --- cpp/react-native-libsodium.cpp | 57 +++++++++--- ...ead_xchacha20poly1305_ietf_decrypt_test.ts | 92 +++++++++++++++++++ ...ead_xchacha20poly1305_ietf_encrypt_test.ts | 52 +++++++++++ src/lib.native.ts | 32 +++---- 4 files changed, 201 insertions(+), 32 deletions(-) diff --git a/cpp/react-native-libsodium.cpp b/cpp/react-native-libsodium.cpp index 4850c9a..c3fcce3 100644 --- a/cpp/react-native-libsodium.cpp +++ b/cpp/react-native-libsodium.cpp @@ -1,5 +1,6 @@ #include "react-native-libsodium.h" #include // libsodium +#include #include #include #include @@ -1335,7 +1336,7 @@ namespace ReactNativeLibsodium std::string additionalDataArgumentName = "additionalData"; unsigned int additionalDataArgumentPosition = 1; - validateIsString(functionName, runtime, arguments[additionalDataArgumentPosition], additionalDataArgumentName, true); + JsiArgType additionalDataArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[additionalDataArgumentPosition], additionalDataArgumentName, false); std::string publicNonceArgumentName = "public_nonce"; unsigned int publicNonceArgumentPosition = 2; @@ -1345,7 +1346,23 @@ namespace ReactNativeLibsodium unsigned int keyArgumentPosition = 3; validateIsArrayBuffer(functionName, runtime, arguments[keyArgumentPosition], keyArgumentName, true); - std::string additionalData = arguments[additionalDataArgumentPosition].asString(runtime).utf8(runtime); + std::string additionalDataString; + std::optional additionalDataArrayBuffer; + const unsigned char *additionalDataData = NULL; + unsigned long long additionalDataLength = 0; + if (additionalDataArgType == JsiArgType::string) + { + additionalDataString = arguments[additionalDataArgumentPosition].asString(runtime).utf8(runtime); + additionalDataData = reinterpret_cast(additionalDataString.data()); + additionalDataLength = additionalDataString.length(); + } + else if (additionalDataArgType == JsiArgType::arrayBuffer) + { + additionalDataArrayBuffer.emplace(arguments[additionalDataArgumentPosition].asObject(runtime).getArrayBuffer(runtime)); + additionalDataData = additionalDataArrayBuffer->data(runtime); + additionalDataLength = additionalDataArrayBuffer->length(runtime); + } + auto publicNonceArrayBuffer = arguments[publicNonceArgumentPosition].asObject(runtime).getArrayBuffer(runtime); auto keyArrayBuffer = @@ -1373,8 +1390,8 @@ namespace ReactNativeLibsodium &ciphertextLength, reinterpret_cast(messageString.data()), messageString.length(), - reinterpret_cast(additionalData.data()), - additionalData.length(), + additionalDataData, + additionalDataLength, NULL, publicNonceArrayBuffer.data(runtime), keyArrayBuffer.data(runtime)); @@ -1390,8 +1407,8 @@ namespace ReactNativeLibsodium &ciphertextLength, messageArrayBuffer.data(runtime), messageArrayBuffer.length(runtime), - reinterpret_cast(additionalData.data()), - additionalData.length(), + additionalDataData, + additionalDataLength, NULL, publicNonceArrayBuffer.data(runtime), keyArrayBuffer.data(runtime)); @@ -1417,7 +1434,7 @@ namespace ReactNativeLibsodium std::string additionalDataArgumentName = "additionalData"; unsigned int additionalDataArgumentPosition = 1; - validateIsString(functionName, runtime, arguments[additionalDataArgumentPosition], additionalDataArgumentName, true); + JsiArgType additionalDataArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[additionalDataArgumentPosition], additionalDataArgumentName, false); std::string publicNonceArgumentName = "public_nonce"; unsigned int publicNonceArgumentPosition = 2; @@ -1427,7 +1444,23 @@ namespace ReactNativeLibsodium unsigned int keyArgumentPosition = 3; validateIsArrayBuffer(functionName, runtime, arguments[keyArgumentPosition], keyArgumentName, true); - std::string additionalData = arguments[additionalDataArgumentPosition].asString(runtime).utf8(runtime); + std::string additionalDataString; + std::optional additionalDataArrayBuffer; + const unsigned char *additionalDataData = NULL; + unsigned long long additionalDataLength = 0; + if (additionalDataArgType == JsiArgType::string) + { + additionalDataString = arguments[additionalDataArgumentPosition].asString(runtime).utf8(runtime); + additionalDataData = reinterpret_cast(additionalDataString.data()); + additionalDataLength = additionalDataString.length(); + } + else if (additionalDataArgType == JsiArgType::arrayBuffer) + { + additionalDataArrayBuffer.emplace(arguments[additionalDataArgumentPosition].asObject(runtime).getArrayBuffer(runtime)); + additionalDataData = additionalDataArrayBuffer->data(runtime); + additionalDataLength = additionalDataArrayBuffer->length(runtime); + } + auto publicNonceArrayBuffer = arguments[publicNonceArgumentPosition].asObject(runtime).getArrayBuffer(runtime); auto keyArrayBuffer = @@ -1456,8 +1489,8 @@ namespace ReactNativeLibsodium NULL, reinterpret_cast(ciphertextString.data()), ciphertextString.length(), - reinterpret_cast(additionalData.data()), - additionalData.length(), + additionalDataData, + additionalDataLength, publicNonceArrayBuffer.data(runtime), keyArrayBuffer.data(runtime)); } @@ -1473,8 +1506,8 @@ namespace ReactNativeLibsodium NULL, ciphertextArrayBuffer.data(runtime), ciphertextArrayBuffer.length(runtime), - reinterpret_cast(additionalData.data()), - additionalData.length(), + additionalDataData, + additionalDataLength, publicNonceArrayBuffer.data(runtime), keyArrayBuffer.data(runtime)); } diff --git a/example/src/tests/crypto_aead_xchacha20poly1305_ietf_decrypt_test.ts b/example/src/tests/crypto_aead_xchacha20poly1305_ietf_decrypt_test.ts index 5485cc9..ddab641 100644 --- a/example/src/tests/crypto_aead_xchacha20poly1305_ietf_decrypt_test.ts +++ b/example/src/tests/crypto_aead_xchacha20poly1305_ietf_decrypt_test.ts @@ -2,6 +2,7 @@ import { crypto_aead_xchacha20poly1305_ietf_KEYBYTES, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, crypto_aead_xchacha20poly1305_ietf_decrypt, + crypto_aead_xchacha20poly1305_ietf_encrypt, crypto_aead_xchacha20poly1305_ietf_keygen, from_string, randombytes_buf, @@ -120,3 +121,94 @@ test('crypto_aead_xchacha20poly1305_ietf_decrypt', () => { ); }).toThrow(); }); + +test('crypto_aead_xchacha20poly1305_ietf_decrypt with Uint8Array and null additional_data', () => { + const message = 'Hello, world!'; + const key = new Uint8Array([ + 108, 17, 177, 237, 16, 132, 96, 213, 10, 50, 109, 157, 209, 207, 131, 239, + 199, 127, 249, 166, 146, 48, 155, 115, 190, 244, 210, 252, 219, 38, 200, + 159, + ]); + const publicNonce = new Uint8Array([ + 137, 27, 59, 167, 152, 253, 53, 78, 125, 80, 246, 158, 107, 239, 217, 210, + 3, 212, 219, 223, 63, 14, 97, 107, + ]); + // the ciphertext of 'Hello, world!' created with the string additional_data + // 'additional data' + const ciphertext = new Uint8Array([ + 249, 165, 41, 20, 8, 68, 254, 59, 157, 166, 196, 51, 98, 212, 168, 126, 136, + 102, 109, 38, 148, 139, 198, 4, 142, 86, 112, 89, 239, + ]); + // the UTF-8 bytes of the string 'additional data' + const additionalDataBytes = new Uint8Array([ + 97, 100, 100, 105, 116, 105, 111, 110, 97, 108, 32, 100, 97, 116, 97, + ]); + + // a Uint8Array additional_data must authenticate the exact same bytes as + // its UTF-8 string equivalent + expect( + to_string( + crypto_aead_xchacha20poly1305_ietf_decrypt( + null, + ciphertext, + additionalDataBytes, + publicNonce, + key + ) + ) + ).toEqual(message); + + // round-trip with a Uint8Array additional_data that is no valid UTF-8 + const roundTripKey = crypto_aead_xchacha20poly1305_ietf_keygen(); + const roundTripNonce = randombytes_buf( + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + ); + const roundTripAdditionalData = new Uint8Array([0, 255, 254, 1, 128, 7]); + const roundTripCiphertext = crypto_aead_xchacha20poly1305_ietf_encrypt( + message, + roundTripAdditionalData, + null, + roundTripNonce, + roundTripKey + ); + expect( + to_string( + crypto_aead_xchacha20poly1305_ietf_decrypt( + null, + roundTripCiphertext, + roundTripAdditionalData, + roundTripNonce, + roundTripKey + ) + ) + ).toEqual(message); + + // a single tampered additional_data byte (first byte flipped from 0 to 1) + // must fail to decrypt + const tamperedAdditionalData = new Uint8Array([1, 255, 254, 1, 128, 7]); + expect(() => { + crypto_aead_xchacha20poly1305_ietf_decrypt( + null, + roundTripCiphertext, + tamperedAdditionalData, + roundTripNonce, + roundTripKey + ); + }).toThrow(); + + // null additional_data + expect( + to_string( + crypto_aead_xchacha20poly1305_ietf_decrypt( + null, + new Uint8Array([ + 249, 165, 41, 20, 8, 68, 254, 59, 157, 166, 196, 51, 98, 245, 181, + 152, 162, 160, 8, 101, 170, 191, 221, 127, 9, 8, 14, 197, 128, + ]), + null, + publicNonce, + key + ) + ) + ).toEqual(message); +}); diff --git a/example/src/tests/crypto_aead_xchacha20poly1305_ietf_encrypt_test.ts b/example/src/tests/crypto_aead_xchacha20poly1305_ietf_encrypt_test.ts index 9c663be..789475a 100644 --- a/example/src/tests/crypto_aead_xchacha20poly1305_ietf_encrypt_test.ts +++ b/example/src/tests/crypto_aead_xchacha20poly1305_ietf_encrypt_test.ts @@ -104,3 +104,55 @@ test('crypto_aead_xchacha20poly1305_ietf_encrypt', () => { ); }).toThrow(); }); + +test('crypto_aead_xchacha20poly1305_ietf_encrypt with Uint8Array and null additional_data', () => { + const message = 'Hello, world!'; + const key = new Uint8Array([ + 108, 17, 177, 237, 16, 132, 96, 213, 10, 50, 109, 157, 209, 207, 131, 239, + 199, 127, 249, 166, 146, 48, 155, 115, 190, 244, 210, 252, 219, 38, 200, + 159, + ]); + const publicNonce = new Uint8Array([ + 137, 27, 59, 167, 152, 253, 53, 78, 125, 80, 246, 158, 107, 239, 217, 210, + 3, 212, 219, 223, 63, 14, 97, 107, + ]); + // the UTF-8 bytes of the string 'additional data' + const additionalDataBytes = new Uint8Array([ + 97, 100, 100, 105, 116, 105, 111, 110, 97, 108, 32, 100, 97, 116, 97, + ]); + + // a Uint8Array additional_data must authenticate the exact same bytes as + // its UTF-8 string equivalent and therefore produce the same ciphertext + expect( + crypto_aead_xchacha20poly1305_ietf_encrypt( + message, + additionalDataBytes, + null, + publicNonce, + key + ) + ).toEqual( + crypto_aead_xchacha20poly1305_ietf_encrypt( + message, + 'additional data', + null, + publicNonce, + key + ) + ); + + expect( + crypto_aead_xchacha20poly1305_ietf_encrypt( + message, + null, + null, + publicNonce, + key + ) + ).toEqual( + new Uint8Array([ + 249, 165, 41, 20, 8, 68, 254, 59, 157, 166, 196, 51, 98, 245, 181, 152, + 162, 160, 8, 101, 170, 191, 221, 127, 9, 8, 14, 197, 128, + ]) + ); +}); diff --git a/src/lib.native.ts b/src/lib.native.ts index da99063..a39abd6 100644 --- a/src/lib.native.ts +++ b/src/lib.native.ts @@ -179,19 +179,13 @@ declare global { ): ArrayBuffer; function jsi_crypto_aead_xchacha20poly1305_ietf_encrypt( message: string | ArrayBuffer, - additionalData: string, + additionalData: string | ArrayBuffer | null, public_nonce: ArrayBuffer, key: ArrayBuffer ): ArrayBuffer; function jsi_crypto_aead_xchacha20poly1305_ietf_decrypt( ciphertext: string | ArrayBuffer, - additionalData: string, - public_nonce: ArrayBuffer, - key: ArrayBuffer - ): ArrayBuffer; - function jsi_crypto_aead_xchacha20poly1305_ietf_decrypt( - ciphertext: string | ArrayBuffer, - additionalData: string, + additionalData: string | ArrayBuffer | null, public_nonce: ArrayBuffer, key: ArrayBuffer ): ArrayBuffer; @@ -869,14 +863,13 @@ export function crypto_aead_xchacha20poly1305_ietf_encrypt( let result: ArrayBuffer; const messageParam = typeof message === 'string' ? message : toArrayBuffer(message); - if (typeof additional_data !== 'string') { - throw new Error( - 'crypto_aead_xchacha20poly1305_ietf_encrypt: input type not yet implemented' - ); - } + const additionalDataParam = + additional_data == null || typeof additional_data === 'string' + ? additional_data + : toArrayBuffer(additional_data); result = global.jsi_crypto_aead_xchacha20poly1305_ietf_encrypt( messageParam, - additional_data, + additionalDataParam, toArrayBuffer(public_nonce), toArrayBuffer(key) ); @@ -913,16 +906,15 @@ export function crypto_aead_xchacha20poly1305_ietf_decrypt( 'crypto_aead_xchacha20poly1305_ietf_decrypt: input type not yet implemented' ); } - if (typeof additional_data !== 'string') { - throw new Error( - 'crypto_aead_xchacha20poly1305_ietf_decrypt: input type not yet implemented' - ); - } const ciphertextParam = typeof ciphertext === 'string' ? ciphertext : toArrayBuffer(ciphertext); + const additionalDataParam = + additional_data == null || typeof additional_data === 'string' + ? additional_data + : toArrayBuffer(additional_data); result = global.jsi_crypto_aead_xchacha20poly1305_ietf_decrypt( ciphertextParam, - additional_data, + additionalDataParam, toArrayBuffer(public_nonce), toArrayBuffer(key) ); From 9e9422c5b316706f8e686156511bf01962f3e630 Mon Sep 17 00:00:00 2001 From: slowsats <116310285+slowsats@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:01:24 -0400 Subject: [PATCH 4/4] feat: add crypto_secretstream_xchacha20poly1305 functions and constants Co-Authored-By: Claude Fable 5 --- README.md | 12 + cpp/react-native-libsodium.cpp | 311 ++++++++++++++++++ example/src/components/TestResults.tsx | 1 + ...pto_secretstream_xchacha20poly1305_test.ts | 208 ++++++++++++ src/lib.native.ts | 199 ++++++++++- 5 files changed, 730 insertions(+), 1 deletion(-) create mode 100644 example/src/tests/crypto_secretstream_xchacha20poly1305_test.ts diff --git a/README.md b/README.md index 3dc05de..5cacc5e 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,18 @@ import { crypto_secretbox_keygen, crypto_secretbox_NONCEBYTES, crypto_secretbox_open_easy, + crypto_secretstream_xchacha20poly1305_ABYTES, + crypto_secretstream_xchacha20poly1305_HEADERBYTES, + crypto_secretstream_xchacha20poly1305_init_pull, + crypto_secretstream_xchacha20poly1305_init_push, + crypto_secretstream_xchacha20poly1305_KEYBYTES, + crypto_secretstream_xchacha20poly1305_keygen, + crypto_secretstream_xchacha20poly1305_pull, + crypto_secretstream_xchacha20poly1305_push, + crypto_secretstream_xchacha20poly1305_TAG_FINAL, + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE, + crypto_secretstream_xchacha20poly1305_TAG_PUSH, + crypto_secretstream_xchacha20poly1305_TAG_REKEY, crypto_sign_detached, crypto_sign_keypair, crypto_sign_SEEDBYTES, diff --git a/cpp/react-native-libsodium.cpp b/cpp/react-native-libsodium.cpp index c3fcce3..2575635 100644 --- a/cpp/react-native-libsodium.cpp +++ b/cpp/react-native-libsodium.cpp @@ -184,6 +184,34 @@ namespace ReactNativeLibsodium } } + // The secretstream state is passed between JavaScript and C++ as an opaque + // ArrayBuffer holding the raw crypto_secretstream_xchacha20poly1305_state + // bytes. push and pull copy the state out of the ArrayBuffer, run the + // libsodium function and copy the updated state back into the same + // ArrayBuffer so the state advances in place across calls. + crypto_secretstream_xchacha20poly1305_state readSecretstreamState(jsi::Runtime &runtime, const jsi::ArrayBuffer &stateArrayBuffer) + { + if (stateArrayBuffer.length(runtime) != crypto_secretstream_xchacha20poly1305_statebytes()) + { + throw jsi::JSError(runtime, "invalid state length"); + } + crypto_secretstream_xchacha20poly1305_state state; + memcpy(&state, stateArrayBuffer.data(runtime), sizeof state); + return state; + } + + void writeSecretstreamState(jsi::Runtime &runtime, const jsi::ArrayBuffer &stateArrayBuffer, const crypto_secretstream_xchacha20poly1305_state &state) + { + memcpy(stateArrayBuffer.data(runtime), &state, sizeof state); + } + + jsi::Object secretstreamStateAsObject(jsi::Runtime &runtime, const crypto_secretstream_xchacha20poly1305_state &state) + { + std::vector stateBytes(sizeof state); + memcpy(stateBytes.data(), &state, sizeof state); + return arrayBufferAsObject(runtime, stateBytes); + } + void installLibsodium(jsi::Runtime &jsiRuntime) { jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretbox_KEYBYTES", static_cast(crypto_secretbox_KEYBYTES)); @@ -211,6 +239,13 @@ namespace ReactNativeLibsodium jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_BYTES", static_cast(crypto_hash_BYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha256_BYTES", static_cast(crypto_hash_sha256_BYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha512_BYTES", static_cast(crypto_hash_sha512_BYTES)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_ABYTES", static_cast(crypto_secretstream_xchacha20poly1305_ABYTES)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_HEADERBYTES", static_cast(crypto_secretstream_xchacha20poly1305_HEADERBYTES)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_KEYBYTES", static_cast(crypto_secretstream_xchacha20poly1305_KEYBYTES)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_TAG_MESSAGE", static_cast(crypto_secretstream_xchacha20poly1305_TAG_MESSAGE)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_TAG_PUSH", static_cast(crypto_secretstream_xchacha20poly1305_TAG_PUSH)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_TAG_REKEY", static_cast(crypto_secretstream_xchacha20poly1305_TAG_REKEY)); + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_TAG_FINAL", static_cast(crypto_secretstream_xchacha20poly1305_TAG_FINAL)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_sign_SEEDBYTES", static_cast(crypto_sign_SEEDBYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_auth_BYTES", static_cast(crypto_auth_BYTES)); jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_auth_KEYBYTES", static_cast(crypto_auth_KEYBYTES)); @@ -1695,6 +1730,282 @@ namespace ReactNativeLibsodium jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_hash_sha512", std::move(jsi_crypto_hash_sha512)); + auto jsi_crypto_secretstream_xchacha20poly1305_keygen = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_keygen"), + 0, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + std::vector key(crypto_secretstream_xchacha20poly1305_KEYBYTES); + crypto_secretstream_xchacha20poly1305_keygen(key.data()); + return arrayBufferAsObject(runtime, key); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_keygen", std::move(jsi_crypto_secretstream_xchacha20poly1305_keygen)); + + auto jsi_crypto_secretstream_xchacha20poly1305_init_push = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_init_push"), + 1, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_secretstream_xchacha20poly1305_init_push"; + + std::string keyArgumentName = "key"; + unsigned int keyArgumentPosition = 0; + validateIsArrayBuffer(functionName, runtime, arguments[keyArgumentPosition], keyArgumentName, true); + + auto keyArrayBuffer = + arguments[keyArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + + if (keyArrayBuffer.length(runtime) != crypto_secretstream_xchacha20poly1305_KEYBYTES) + { + throw jsi::JSError(runtime, "invalid key length"); + } + + crypto_secretstream_xchacha20poly1305_state state; + std::vector header(crypto_secretstream_xchacha20poly1305_HEADERBYTES); + int result = crypto_secretstream_xchacha20poly1305_init_push( + &state, + header.data(), + keyArrayBuffer.data(runtime)); + + throwOnBadResult(functionName, runtime, result); + + jsi::Object stateBufferAsObject = secretstreamStateAsObject(runtime, state); + jsi::Object headerBufferAsObject = arrayBufferAsObject(runtime, header); + + auto object = jsi::Object(runtime); + object.setProperty(runtime, "state", stateBufferAsObject); + object.setProperty(runtime, "header", headerBufferAsObject); + return object; + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_init_push", std::move(jsi_crypto_secretstream_xchacha20poly1305_init_push)); + + auto jsi_crypto_secretstream_xchacha20poly1305_push = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_push"), + 4, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_secretstream_xchacha20poly1305_push"; + + std::string stateArgumentName = "state"; + unsigned int stateArgumentPosition = 0; + validateIsArrayBuffer(functionName, runtime, arguments[stateArgumentPosition], stateArgumentName, true); + + std::string messageArgumentName = "message"; + unsigned int messageArgumentPosition = 1; + JsiArgType messageArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[messageArgumentPosition], messageArgumentName, true); + + std::string additionalDataArgumentName = "additionalData"; + unsigned int additionalDataArgumentPosition = 2; + JsiArgType additionalDataArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[additionalDataArgumentPosition], additionalDataArgumentName, false); + + std::string tagArgumentName = "tag"; + unsigned int tagArgumentPosition = 3; + validateIsNumber(functionName, runtime, arguments[tagArgumentPosition], tagArgumentName, true); + + auto stateArrayBuffer = + arguments[stateArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + crypto_secretstream_xchacha20poly1305_state state = readSecretstreamState(runtime, stateArrayBuffer); + + std::string additionalDataString; + std::optional additionalDataArrayBuffer; + const unsigned char *additionalDataData = NULL; + unsigned long long additionalDataLength = 0; + if (additionalDataArgType == JsiArgType::string) + { + additionalDataString = arguments[additionalDataArgumentPosition].asString(runtime).utf8(runtime); + additionalDataData = reinterpret_cast(additionalDataString.data()); + additionalDataLength = additionalDataString.length(); + } + else if (additionalDataArgType == JsiArgType::arrayBuffer) + { + additionalDataArrayBuffer.emplace(arguments[additionalDataArgumentPosition].asObject(runtime).getArrayBuffer(runtime)); + additionalDataData = additionalDataArrayBuffer->data(runtime); + additionalDataLength = additionalDataArrayBuffer->length(runtime); + } + + unsigned char tag = static_cast(arguments[tagArgumentPosition].asNumber()); + + std::vector ciphertext; + int result = -1; + + if (messageArgType == JsiArgType::string) + { + std::string messageString = arguments[messageArgumentPosition].asString(runtime).utf8(runtime); + ciphertext.resize(messageString.length() + crypto_secretstream_xchacha20poly1305_ABYTES); + result = crypto_secretstream_xchacha20poly1305_push( + &state, + ciphertext.data(), + NULL, + reinterpret_cast(messageString.data()), + messageString.length(), + additionalDataData, + additionalDataLength, + tag); + } + else + { + auto messageArrayBuffer = + arguments[messageArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + ciphertext.resize(messageArrayBuffer.length(runtime) + crypto_secretstream_xchacha20poly1305_ABYTES); + result = crypto_secretstream_xchacha20poly1305_push( + &state, + ciphertext.data(), + NULL, + messageArrayBuffer.data(runtime), + messageArrayBuffer.length(runtime), + additionalDataData, + additionalDataLength, + tag); + } + + writeSecretstreamState(runtime, stateArrayBuffer, state); + throwOnBadResult(functionName, runtime, result); + return arrayBufferAsObject(runtime, ciphertext); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_push", std::move(jsi_crypto_secretstream_xchacha20poly1305_push)); + + auto jsi_crypto_secretstream_xchacha20poly1305_init_pull = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_init_pull"), + 2, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_secretstream_xchacha20poly1305_init_pull"; + + std::string headerArgumentName = "header"; + unsigned int headerArgumentPosition = 0; + validateIsArrayBuffer(functionName, runtime, arguments[headerArgumentPosition], headerArgumentName, true); + + std::string keyArgumentName = "key"; + unsigned int keyArgumentPosition = 1; + validateIsArrayBuffer(functionName, runtime, arguments[keyArgumentPosition], keyArgumentName, true); + + auto headerArrayBuffer = + arguments[headerArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + auto keyArrayBuffer = + arguments[keyArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + + if (headerArrayBuffer.length(runtime) != crypto_secretstream_xchacha20poly1305_HEADERBYTES) + { + throw jsi::JSError(runtime, "invalid header length"); + } + if (keyArrayBuffer.length(runtime) != crypto_secretstream_xchacha20poly1305_KEYBYTES) + { + throw jsi::JSError(runtime, "invalid key length"); + } + + crypto_secretstream_xchacha20poly1305_state state; + int result = crypto_secretstream_xchacha20poly1305_init_pull( + &state, + headerArrayBuffer.data(runtime), + keyArrayBuffer.data(runtime)); + + throwOnBadResult(functionName, runtime, result); + return secretstreamStateAsObject(runtime, state); + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_init_pull", std::move(jsi_crypto_secretstream_xchacha20poly1305_init_pull)); + + auto jsi_crypto_secretstream_xchacha20poly1305_pull = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_pull"), + 3, + [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count) -> jsi::Value + { + const std::string functionName = "crypto_secretstream_xchacha20poly1305_pull"; + + std::string stateArgumentName = "state"; + unsigned int stateArgumentPosition = 0; + validateIsArrayBuffer(functionName, runtime, arguments[stateArgumentPosition], stateArgumentName, true); + + std::string ciphertextArgumentName = "cipher"; + unsigned int ciphertextArgumentPosition = 1; + JsiArgType ciphertextArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[ciphertextArgumentPosition], ciphertextArgumentName, true); + + std::string additionalDataArgumentName = "additionalData"; + unsigned int additionalDataArgumentPosition = 2; + JsiArgType additionalDataArgType = validateIsStringOrArrayBuffer(functionName, runtime, arguments[additionalDataArgumentPosition], additionalDataArgumentName, false); + + auto stateArrayBuffer = + arguments[stateArgumentPosition].asObject(runtime).getArrayBuffer(runtime); + crypto_secretstream_xchacha20poly1305_state state = readSecretstreamState(runtime, stateArrayBuffer); + + std::string additionalDataString; + std::optional additionalDataArrayBuffer; + const unsigned char *additionalDataData = NULL; + unsigned long long additionalDataLength = 0; + if (additionalDataArgType == JsiArgType::string) + { + additionalDataString = arguments[additionalDataArgumentPosition].asString(runtime).utf8(runtime); + additionalDataData = reinterpret_cast(additionalDataString.data()); + additionalDataLength = additionalDataString.length(); + } + else if (additionalDataArgType == JsiArgType::arrayBuffer) + { + additionalDataArrayBuffer.emplace(arguments[additionalDataArgumentPosition].asObject(runtime).getArrayBuffer(runtime)); + additionalDataData = additionalDataArrayBuffer->data(runtime); + additionalDataLength = additionalDataArrayBuffer->length(runtime); + } + + std::string ciphertextString; + std::optional ciphertextArrayBuffer; + const unsigned char *ciphertextData = NULL; + unsigned long long ciphertextLength = 0; + if (ciphertextArgType == JsiArgType::string) + { + ciphertextString = arguments[ciphertextArgumentPosition].asString(runtime).utf8(runtime); + ciphertextData = reinterpret_cast(ciphertextString.data()); + ciphertextLength = ciphertextString.length(); + } + else + { + ciphertextArrayBuffer.emplace(arguments[ciphertextArgumentPosition].asObject(runtime).getArrayBuffer(runtime)); + ciphertextData = ciphertextArrayBuffer->data(runtime); + ciphertextLength = ciphertextArrayBuffer->length(runtime); + } + + if (ciphertextLength < crypto_secretstream_xchacha20poly1305_ABYTES) + { + throw jsi::JSError(runtime, "cipher is too short"); + } + + std::vector message(ciphertextLength - crypto_secretstream_xchacha20poly1305_ABYTES); + unsigned char tag = 0; + int result = crypto_secretstream_xchacha20poly1305_pull( + &state, + message.data(), + NULL, + &tag, + ciphertextData, + ciphertextLength, + additionalDataData, + additionalDataLength); + + writeSecretstreamState(runtime, stateArrayBuffer, state); + + // matching the libsodium-wrappers behavior of returning false + // instead of throwing when the ciphertext is invalid + if (result != 0) + { + return jsi::Value(false); + } + + jsi::Object messageBufferAsObject = arrayBufferAsObject(runtime, message); + + auto object = jsi::Object(runtime); + object.setProperty(runtime, "message", messageBufferAsObject); + object.setProperty(runtime, "tag", static_cast(tag)); + return object; + }); + + jsiRuntime.global().setProperty(jsiRuntime, "jsi_crypto_secretstream_xchacha20poly1305_pull", std::move(jsi_crypto_secretstream_xchacha20poly1305_pull)); + auto jsi_crypto_kdf_hkdf_sha256_extract = jsi::Function::createFromHostFunction( jsiRuntime, jsi::PropNameID::forUtf8(jsiRuntime, "jsi_crypto_kdf_hkdf_sha256_extract"), diff --git a/example/src/components/TestResults.tsx b/example/src/components/TestResults.tsx index c72a263..5f0498a 100644 --- a/example/src/components/TestResults.tsx +++ b/example/src/components/TestResults.tsx @@ -25,6 +25,7 @@ import '../tests/crypto_sign_ed25519_pk_to_curve25519_test'; import '../tests/crypto_secretbox_easy_test'; import '../tests/crypto_secretbox_keygen_test'; import '../tests/crypto_secretbox_open_easy_test'; +import '../tests/crypto_secretstream_xchacha20poly1305_test'; import '../tests/crypto_sign_detached_test'; import '../tests/crypto_sign_keypair_test'; import '../tests/crypto_sign_seed_keypair_test'; diff --git a/example/src/tests/crypto_secretstream_xchacha20poly1305_test.ts b/example/src/tests/crypto_secretstream_xchacha20poly1305_test.ts new file mode 100644 index 0000000..6ce0f6c --- /dev/null +++ b/example/src/tests/crypto_secretstream_xchacha20poly1305_test.ts @@ -0,0 +1,208 @@ +import { + crypto_secretstream_xchacha20poly1305_ABYTES, + crypto_secretstream_xchacha20poly1305_HEADERBYTES, + crypto_secretstream_xchacha20poly1305_KEYBYTES, + crypto_secretstream_xchacha20poly1305_TAG_FINAL, + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE, + crypto_secretstream_xchacha20poly1305_TAG_PUSH, + crypto_secretstream_xchacha20poly1305_TAG_REKEY, + crypto_secretstream_xchacha20poly1305_init_pull, + crypto_secretstream_xchacha20poly1305_init_push, + crypto_secretstream_xchacha20poly1305_keygen, + crypto_secretstream_xchacha20poly1305_pull, + crypto_secretstream_xchacha20poly1305_push, + to_string, +} from 'react-native-libsodium'; +import { expect, test } from '../utils/testRunner'; + +test('crypto_secretstream_xchacha20poly1305 constants', () => { + expect(crypto_secretstream_xchacha20poly1305_ABYTES).toEqual(17); + expect(crypto_secretstream_xchacha20poly1305_HEADERBYTES).toEqual(24); + expect(crypto_secretstream_xchacha20poly1305_KEYBYTES).toEqual(32); + expect(crypto_secretstream_xchacha20poly1305_TAG_MESSAGE).toEqual(0); + expect(crypto_secretstream_xchacha20poly1305_TAG_PUSH).toEqual(1); + expect(crypto_secretstream_xchacha20poly1305_TAG_REKEY).toEqual(2); + expect(crypto_secretstream_xchacha20poly1305_TAG_FINAL).toEqual(3); +}); + +test('crypto_secretstream_xchacha20poly1305 push and pull round-trip', () => { + const key = crypto_secretstream_xchacha20poly1305_keygen(); + expect(key.length).toEqual(crypto_secretstream_xchacha20poly1305_KEYBYTES); + + const { state, header } = + crypto_secretstream_xchacha20poly1305_init_push(key); + expect(header.length).toEqual( + crypto_secretstream_xchacha20poly1305_HEADERBYTES + ); + + const additionalDataBytes = new Uint8Array([7, 0, 255, 42]); + const chunk1 = crypto_secretstream_xchacha20poly1305_push( + state, + 'chunk one', + null, + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE + ); + expect(chunk1.length).toEqual( + 'chunk one'.length + crypto_secretstream_xchacha20poly1305_ABYTES + ); + const chunk2 = crypto_secretstream_xchacha20poly1305_push( + state, + new Uint8Array([1, 2, 3]), + additionalDataBytes, + crypto_secretstream_xchacha20poly1305_TAG_PUSH + ); + const chunk3 = crypto_secretstream_xchacha20poly1305_push( + state, + 'chunk three', + 'the additional data', + crypto_secretstream_xchacha20poly1305_TAG_REKEY + ); + const chunk4 = crypto_secretstream_xchacha20poly1305_push( + state, + 'the last chunk', + null, + crypto_secretstream_xchacha20poly1305_TAG_FINAL + ); + + const pullState = crypto_secretstream_xchacha20poly1305_init_pull( + header, + key + ); + const result1 = crypto_secretstream_xchacha20poly1305_pull( + pullState, + chunk1, + null + ); + if (result1 === false) { + throw new Error('pull of chunk1 failed'); + } + expect(to_string(result1.message)).toEqual('chunk one'); + expect(result1.tag).toEqual( + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE + ); + + // pulling with a missing or wrong additional_data must fail and must not + // advance the state + expect( + crypto_secretstream_xchacha20poly1305_pull(pullState, chunk2, null) + ).toEqual(false); + expect( + crypto_secretstream_xchacha20poly1305_pull( + pullState, + chunk2, + new Uint8Array([7, 0, 255, 43]) + ) + ).toEqual(false); + const result2 = crypto_secretstream_xchacha20poly1305_pull( + pullState, + chunk2, + additionalDataBytes + ); + if (result2 === false) { + throw new Error('pull of chunk2 failed'); + } + expect(result2.message).toEqual(new Uint8Array([1, 2, 3])); + expect(result2.tag).toEqual(crypto_secretstream_xchacha20poly1305_TAG_PUSH); + + const result3 = crypto_secretstream_xchacha20poly1305_pull( + pullState, + chunk3, + 'the additional data' + ); + if (result3 === false) { + throw new Error('pull of chunk3 failed'); + } + expect(to_string(result3.message)).toEqual('chunk three'); + expect(result3.tag).toEqual(crypto_secretstream_xchacha20poly1305_TAG_REKEY); + + const result4 = crypto_secretstream_xchacha20poly1305_pull( + pullState, + chunk4, + null + ); + if (result4 === false) { + throw new Error('pull of chunk4 failed'); + } + expect(to_string(result4.message)).toEqual('the last chunk'); + expect(result4.tag).toEqual(crypto_secretstream_xchacha20poly1305_TAG_FINAL); +}); + +test('crypto_secretstream_xchacha20poly1305_pull failures', () => { + const key = crypto_secretstream_xchacha20poly1305_keygen(); + const { state, header } = + crypto_secretstream_xchacha20poly1305_init_push(key); + const chunk1 = crypto_secretstream_xchacha20poly1305_push( + state, + 'chunk one', + null, + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE + ); + const chunk2 = crypto_secretstream_xchacha20poly1305_push( + state, + 'chunk two', + null, + crypto_secretstream_xchacha20poly1305_TAG_FINAL + ); + + // truncated ciphertext must fail + const truncationState = crypto_secretstream_xchacha20poly1305_init_pull( + header, + key + ); + expect( + crypto_secretstream_xchacha20poly1305_pull( + truncationState, + chunk1.slice(0, chunk1.length - 1), + null + ) + ).toEqual(false); + + // pulling the chunks out of order must fail + const reorderState = crypto_secretstream_xchacha20poly1305_init_pull( + header, + key + ); + expect( + crypto_secretstream_xchacha20poly1305_pull(reorderState, chunk2, null) + ).toEqual(false); + + // a ciphertext shorter than ABYTES can't contain a message at all + const tooShortState = crypto_secretstream_xchacha20poly1305_init_pull( + header, + key + ); + expect(() => { + crypto_secretstream_xchacha20poly1305_pull( + tooShortState, + chunk1.slice(0, crypto_secretstream_xchacha20poly1305_ABYTES - 1), + null + ); + }).toThrow('cipher is too short'); +}); + +test('crypto_secretstream_xchacha20poly1305 invalid input', () => { + const key = crypto_secretstream_xchacha20poly1305_keygen(); + const { state, header } = + crypto_secretstream_xchacha20poly1305_init_push(key); + + expect(() => { + crypto_secretstream_xchacha20poly1305_init_push(key.slice(0, 5)); + }).toThrow('invalid key length'); + expect(() => { + crypto_secretstream_xchacha20poly1305_init_pull(header.slice(0, 5), key); + }).toThrow('invalid header length'); + expect(() => { + crypto_secretstream_xchacha20poly1305_init_pull(header, key.slice(0, 5)); + }).toThrow('invalid key length'); + expect(() => { + crypto_secretstream_xchacha20poly1305_push( + state, + 'message', + null, + null as unknown as number + ); + }).toThrow('tag cannot be null or undefined'); + expect(() => { + crypto_secretstream_xchacha20poly1305_push(state, 'message', null, -1); + }).toThrow('tag must be an unsigned integer'); +}); diff --git a/src/lib.native.ts b/src/lib.native.ts index a39abd6..a68a4f4 100644 --- a/src/lib.native.ts +++ b/src/lib.native.ts @@ -1,6 +1,12 @@ export { base64_variants, to_string } from './libsodium-js-utils'; -import type { KeyPair, StringKeyPair } from './libsodium-types'; import type { + KeyPair, + MessageTag, + StringKeyPair, + StringMessageTag, +} from './libsodium-types'; +import type { + StateAddress, StringOutputFormat, Uint8ArrayOutputFormat, } from 'libsodium-wrappers'; @@ -66,6 +72,13 @@ declare global { var jsi_crypto_kdf_hkdf_sha256_BYTES_MIN: number; var jsi_crypto_kdf_hkdf_sha256_KEYBYTES: number; var jsi_crypto_pwhash_ALG_ARGON2ID13: number; + var jsi_crypto_secretstream_xchacha20poly1305_ABYTES: number; + var jsi_crypto_secretstream_xchacha20poly1305_HEADERBYTES: number; + var jsi_crypto_secretstream_xchacha20poly1305_KEYBYTES: number; + var jsi_crypto_secretstream_xchacha20poly1305_TAG_MESSAGE: number; + var jsi_crypto_secretstream_xchacha20poly1305_TAG_PUSH: number; + var jsi_crypto_secretstream_xchacha20poly1305_TAG_REKEY: number; + var jsi_crypto_secretstream_xchacha20poly1305_TAG_FINAL: number; function jsi_crypto_auth( message: string | ArrayBuffer, @@ -189,6 +202,28 @@ declare global { public_nonce: ArrayBuffer, key: ArrayBuffer ): ArrayBuffer; + function jsi_crypto_secretstream_xchacha20poly1305_keygen(): ArrayBuffer; + function jsi_crypto_secretstream_xchacha20poly1305_init_push( + key: ArrayBuffer + ): { + state: ArrayBuffer; + header: ArrayBuffer; + }; + function jsi_crypto_secretstream_xchacha20poly1305_push( + state: ArrayBuffer, + message: string | ArrayBuffer, + additionalData: string | ArrayBuffer | null, + tag: number + ): ArrayBuffer; + function jsi_crypto_secretstream_xchacha20poly1305_init_pull( + header: ArrayBuffer, + key: ArrayBuffer + ): ArrayBuffer; + function jsi_crypto_secretstream_xchacha20poly1305_pull( + state: ArrayBuffer, + cipher: string | ArrayBuffer, + additionalData: string | ArrayBuffer | null + ): { message: ArrayBuffer; tag: number } | false; function jsi_crypto_kdf_hkdf_sha256_extract( key: ArrayBuffer, salt: ArrayBuffer @@ -246,6 +281,20 @@ export const _unstable_crypto_kdf_hkdf_sha256_KEYBYTES = global.jsi_crypto_kdf_hkdf_sha256_KEYBYTES; export const crypto_pwhash_ALG_ARGON2ID13 = global.jsi_crypto_pwhash_ALG_ARGON2ID13; +export const crypto_secretstream_xchacha20poly1305_ABYTES = + global.jsi_crypto_secretstream_xchacha20poly1305_ABYTES; +export const crypto_secretstream_xchacha20poly1305_HEADERBYTES = + global.jsi_crypto_secretstream_xchacha20poly1305_HEADERBYTES; +export const crypto_secretstream_xchacha20poly1305_KEYBYTES = + global.jsi_crypto_secretstream_xchacha20poly1305_KEYBYTES; +export const crypto_secretstream_xchacha20poly1305_TAG_MESSAGE = + global.jsi_crypto_secretstream_xchacha20poly1305_TAG_MESSAGE; +export const crypto_secretstream_xchacha20poly1305_TAG_PUSH = + global.jsi_crypto_secretstream_xchacha20poly1305_TAG_PUSH; +export const crypto_secretstream_xchacha20poly1305_TAG_REKEY = + global.jsi_crypto_secretstream_xchacha20poly1305_TAG_REKEY; +export const crypto_secretstream_xchacha20poly1305_TAG_FINAL = + global.jsi_crypto_secretstream_xchacha20poly1305_TAG_FINAL; export const from_base64 = ( input: string, @@ -921,6 +970,142 @@ export function crypto_aead_xchacha20poly1305_ietf_decrypt( return convertToOutputFormat(result, outputFormat); } +// The secretstream state is an opaque handle. On native it is the raw +// crypto_secretstream_xchacha20poly1305_state bytes in an ArrayBuffer which +// push and pull advance in place. It is typed as StateAddress to match the +// libsodium-wrappers API and must not be inspected or modified by callers. +export function crypto_secretstream_xchacha20poly1305_keygen( + outputFormat?: Uint8ArrayOutputFormat | null +): Uint8Array; +export function crypto_secretstream_xchacha20poly1305_keygen( + outputFormat: StringOutputFormat +): string; +export function crypto_secretstream_xchacha20poly1305_keygen( + outputFormat: OutputFormat +): unknown { + const result = global.jsi_crypto_secretstream_xchacha20poly1305_keygen(); + return convertToOutputFormat(result, outputFormat); +} + +export function crypto_secretstream_xchacha20poly1305_init_push( + key: Uint8Array, + outputFormat?: Uint8ArrayOutputFormat | null +): { state: StateAddress; header: Uint8Array }; +export function crypto_secretstream_xchacha20poly1305_init_push( + key: Uint8Array, + outputFormat: StringOutputFormat +): { state: StateAddress; header: string }; +export function crypto_secretstream_xchacha20poly1305_init_push( + key: Uint8Array, + outputFormat: OutputFormat +): unknown { + const result = global.jsi_crypto_secretstream_xchacha20poly1305_init_push( + toArrayBuffer(key) + ); + return { + state: result.state as unknown as StateAddress, + header: convertToOutputFormat(result.header, outputFormat), + }; +} + +export function crypto_secretstream_xchacha20poly1305_push( + state_address: StateAddress, + message_chunk: string | Uint8Array, + ad: string | Uint8Array | null, + tag: number, + outputFormat?: Uint8ArrayOutputFormat | null +): Uint8Array; +export function crypto_secretstream_xchacha20poly1305_push( + state_address: StateAddress, + message_chunk: string | Uint8Array, + ad: string | Uint8Array | null, + tag: number, + outputFormat: StringOutputFormat +): string; +export function crypto_secretstream_xchacha20poly1305_push( + state_address: StateAddress, + message_chunk: string | Uint8Array, + ad: string | Uint8Array | null, + tag: number, + outputFormat: OutputFormat +): unknown { + if (tag == null) { + throw new TypeError('tag cannot be null or undefined'); + } + // eslint-disable-next-line no-bitwise + if (typeof tag !== 'number' || (tag | 0) !== tag || tag < 0) { + throw new TypeError('tag must be an unsigned integer'); + } + const messageParam = + typeof message_chunk === 'string' + ? message_chunk + : toArrayBuffer(message_chunk); + const adParam = + ad == null ? null : typeof ad === 'string' ? ad : toArrayBuffer(ad); + const result = global.jsi_crypto_secretstream_xchacha20poly1305_push( + state_address as unknown as ArrayBuffer, + messageParam, + adParam, + tag + ); + return convertToOutputFormat(result, outputFormat); +} + +export function crypto_secretstream_xchacha20poly1305_init_pull( + header: Uint8Array, + key: Uint8Array +): StateAddress { + const result = global.jsi_crypto_secretstream_xchacha20poly1305_init_pull( + toArrayBuffer(header), + toArrayBuffer(key) + ); + return result as unknown as StateAddress; +} + +export function crypto_secretstream_xchacha20poly1305_pull( + state_address: StateAddress, + cipher: string | Uint8Array, + ad?: string | Uint8Array | null, + outputFormat?: Uint8ArrayOutputFormat | null +): MessageTag | false; +export function crypto_secretstream_xchacha20poly1305_pull( + state_address: StateAddress, + cipher: string | Uint8Array, + ad: string | Uint8Array | null, + outputFormat: StringOutputFormat +): StringMessageTag | false; +export function crypto_secretstream_xchacha20poly1305_pull( + state_address: StateAddress, + cipher: string | Uint8Array, + ad?: string | Uint8Array | null, + outputFormat?: OutputFormat +): unknown { + if ( + cipher instanceof Uint8Array && + cipher.length < crypto_secretstream_xchacha20poly1305_ABYTES + ) { + throw new TypeError('cipher is too short'); + } + const cipherParam = + typeof cipher === 'string' ? cipher : toArrayBuffer(cipher); + const adParam = + ad == null ? null : typeof ad === 'string' ? ad : toArrayBuffer(ad); + const result = global.jsi_crypto_secretstream_xchacha20poly1305_pull( + state_address as unknown as ArrayBuffer, + cipherParam, + adParam + ); + // matching the libsodium-wrappers behavior of returning false instead of + // throwing when the ciphertext is invalid + if (result === false) { + return false; + } + return { + message: convertToOutputFormat(result.message, outputFormat), + tag: result.tag, + }; +} + export function _unstable_crypto_kdf_hkdf_sha256_extract( key: Uint8Array, salt: Uint8Array @@ -998,6 +1183,18 @@ export default { crypto_secretbox_keygen, crypto_secretbox_NONCEBYTES, crypto_secretbox_open_easy, + crypto_secretstream_xchacha20poly1305_ABYTES, + crypto_secretstream_xchacha20poly1305_HEADERBYTES, + crypto_secretstream_xchacha20poly1305_init_pull, + crypto_secretstream_xchacha20poly1305_init_push, + crypto_secretstream_xchacha20poly1305_KEYBYTES, + crypto_secretstream_xchacha20poly1305_keygen, + crypto_secretstream_xchacha20poly1305_pull, + crypto_secretstream_xchacha20poly1305_push, + crypto_secretstream_xchacha20poly1305_TAG_FINAL, + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE, + crypto_secretstream_xchacha20poly1305_TAG_PUSH, + crypto_secretstream_xchacha20poly1305_TAG_REKEY, crypto_sign_detached, crypto_sign_keypair, crypto_sign_verify_detached,