diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md
index 65048c2c..71c837f3 100644
--- a/.docs/implementation-coverage.md
+++ b/.docs/implementation-coverage.md
@@ -227,6 +227,18 @@ These algorithms provide quantum-resistant cryptography.
| `Ed448` | ✅ |
| `HMAC` | ✅ |
+## Extended Ciphers (Beyond Node.js API)
+
+These ciphers are **not available in Node.js** but are provided by RNQC via libsodium for mobile use cases requiring extended nonces.
+
+| Cipher | Key | Nonce | Tag | AAD | Notes |
+| ------ | :-: | :---: | :-: | :-: | ----- |
+| `xchacha20-poly1305` | 32B | 24B | 16B | ✅ | AEAD with extended nonce |
+| `xsalsa20-poly1305` | 32B | 24B | 16B | ❌ | Authenticated encryption (secretbox) |
+| `xsalsa20` | 32B | 24B | - | - | Stream cipher (no authentication) |
+
+> **Note:** These ciphers require `SODIUM_ENABLED=1` on both iOS and Android.
+
# `WebCrypto`
* ❌ Class: `Crypto`
diff --git a/docs/content/docs/api/cipher.mdx b/docs/content/docs/api/cipher.mdx
index 57af0237..abe07ee0 100644
--- a/docs/content/docs/api/cipher.mdx
+++ b/docs/content/docs/api/cipher.mdx
@@ -7,11 +7,12 @@ import { Callout } from 'fumadocs-ui/components/callout';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
-The `Cipher` module provides implementations of symmetric cipher algorithms. It supports standard Block Ciphers (AES) and Stream Ciphers (ChaCha20).
+The `Cipher` module provides implementations of symmetric cipher algorithms. It supports standard Block Ciphers (AES), Stream Ciphers (ChaCha20), and extended ciphers via libsodium (XChaCha20, XSalsa20).
## Table of Contents
- [Theory](#theory)
+- [Supported Algorithms](#supported-algorithms)
- [Class: Cipher](#class-cipher)
- [Class: Decipher](#class-decipher)
- [Module Methods](#module-methods)
@@ -33,6 +34,51 @@ Symmetric ciphers use the same key for encryption and decryption.
---
+## Supported Algorithms
+
+### Block Ciphers (AES)
+
+| Algorithm | Key Size | IV Size | Mode | AEAD |
+| --------- | -------- | ------- | ---- | :--: |
+| `aes-128-cbc` | 16 bytes | 16 bytes | CBC | No |
+| `aes-192-cbc` | 24 bytes | 16 bytes | CBC | No |
+| `aes-256-cbc` | 32 bytes | 16 bytes | CBC | No |
+| `aes-128-ctr` | 16 bytes | 16 bytes | CTR | No |
+| `aes-192-ctr` | 24 bytes | 16 bytes | CTR | No |
+| `aes-256-ctr` | 32 bytes | 16 bytes | CTR | No |
+| `aes-128-gcm` | 16 bytes | 12 bytes | GCM | Yes |
+| `aes-192-gcm` | 24 bytes | 12 bytes | GCM | Yes |
+| `aes-256-gcm` | 32 bytes | 12 bytes | GCM | Yes |
+| `aes-128-ccm` | 16 bytes | 7-13 bytes | CCM | Yes |
+| `aes-192-ccm` | 24 bytes | 7-13 bytes | CCM | Yes |
+| `aes-256-ccm` | 32 bytes | 7-13 bytes | CCM | Yes |
+| `aes-128-ocb` | 16 bytes | 12 bytes | OCB | Yes |
+| `aes-192-ocb` | 24 bytes | 12 bytes | OCB | Yes |
+| `aes-256-ocb` | 32 bytes | 12 bytes | OCB | Yes |
+
+### Stream Ciphers (ChaCha20)
+
+| Algorithm | Key Size | Nonce Size | Tag Size | AEAD | AAD |
+| --------- | -------- | ---------- | -------- | :--: | :-: |
+| `chacha20` | 32 bytes | 16 bytes | - | No | No |
+| `chacha20-poly1305` | 32 bytes | 12 bytes | 16 bytes | Yes | Yes |
+
+### Extended Ciphers (libsodium)
+
+
+ These ciphers require `SODIUM_ENABLED=1` on both iOS and Android. They are **not available in Node.js** and are provided as extensions for mobile use cases.
+
+
+| Algorithm | Key Size | Nonce Size | Tag Size | AEAD | AAD | Notes |
+| --------- | -------- | ---------- | -------- | :--: | :-: | ----- |
+| `xchacha20-poly1305` | 32 bytes | 24 bytes | 16 bytes | Yes | Yes | Extended nonce variant |
+| `xsalsa20-poly1305` | 32 bytes | 24 bytes | 16 bytes | Yes | No | NaCl secretbox |
+| `xsalsa20` | 32 bytes | 24 bytes | - | No | No | Stream cipher only |
+
+The extended nonce (24 bytes vs 12 bytes) in XChaCha20 and XSalsa20 variants allows safe random nonce generation without risk of collision, making them ideal for high-volume encryption scenarios.
+
+---
+
## Class: Cipher
Instances of the `Cipher` class are used to encrypt data.
@@ -141,7 +187,7 @@ function encrypt(text: string) {
}
```
-### File Encryption (Scanning)
+### File Encryption (Streaming)
Encrypting a file using streams with AES-CTR (counter mode).
@@ -158,3 +204,66 @@ const output = fs.createWriteStream('output.enc');
input.pipe(cipher).pipe(output);
```
+
+### XChaCha20-Poly1305 (Extended Nonce)
+
+XChaCha20-Poly1305 uses a 24-byte nonce, making random nonce generation safe for high-volume encryption.
+
+
+ Set `SODIUM_ENABLED=1` environment variable before building.
+
+
+```ts
+import { createCipheriv, createDecipheriv, randomBytes } from 'react-native-quick-crypto';
+
+const key = randomBytes(32);
+
+function encrypt(plaintext: Buffer, aad?: Buffer) {
+ // 24-byte nonce - safe to generate randomly
+ const nonce = randomBytes(24);
+
+ const cipher = createCipheriv('xchacha20-poly1305', key, nonce);
+ if (aad) cipher.setAAD(aad);
+
+ const ciphertext = Buffer.concat([
+ cipher.update(plaintext),
+ cipher.final()
+ ]);
+ const tag = cipher.getAuthTag();
+
+ return { ciphertext, nonce, tag };
+}
+
+function decrypt(ciphertext: Buffer, nonce: Buffer, tag: Buffer, aad?: Buffer) {
+ const decipher = createDecipheriv('xchacha20-poly1305', key, nonce);
+ if (aad) decipher.setAAD(aad);
+ decipher.setAuthTag(tag);
+
+ return Buffer.concat([
+ decipher.update(ciphertext),
+ decipher.final()
+ ]);
+}
+```
+
+### XSalsa20-Poly1305 (NaCl Secretbox)
+
+XSalsa20-Poly1305 provides authenticated encryption without AAD support (similar to NaCl's secretbox).
+
+```ts
+import { createCipheriv, createDecipheriv, randomBytes } from 'react-native-quick-crypto';
+
+const key = randomBytes(32);
+const nonce = randomBytes(24);
+const message = Buffer.from('Secret message');
+
+// Encrypt
+const cipher = createCipheriv('xsalsa20-poly1305', key, nonce);
+const ciphertext = Buffer.concat([cipher.update(message), cipher.final()]);
+const tag = cipher.getAuthTag();
+
+// Decrypt
+const decipher = createDecipheriv('xsalsa20-poly1305', key, nonce);
+decipher.setAuthTag(tag);
+const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
+```
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index cd4e217b..80835bbb 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -2811,7 +2811,7 @@ SPEC CHECKSUMS:
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
- QuickCrypto: 7f2ca14820e56e0785fad4e1b5527f2cc7b962d3
+ QuickCrypto: 9e46baaa4fea5a22fdf23c3aae184b983c948b23
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
diff --git a/example/package.json b/example/package.json
index 67da3236..cb2d1e71 100644
--- a/example/package.json
+++ b/example/package.json
@@ -40,7 +40,7 @@
"react-native-mmkv": "4.0.1",
"react-native-nitro-modules": "0.33.2",
"react-native-quick-base64": "2.2.2",
- "react-native-quick-crypto": "1.0.9",
+ "react-native-quick-crypto": "workspace:*",
"react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.18.0",
"react-native-vector-icons": "10.3.0",
diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts
index 763063b8..66b7c84f 100644
--- a/example/src/hooks/useTestsList.ts
+++ b/example/src/hooks/useTestsList.ts
@@ -6,6 +6,8 @@ import '../tests/blake3/blake3_tests';
import '../tests/cipher/cipher_tests';
import '../tests/cipher/chacha_tests';
import '../tests/cipher/xsalsa20_tests';
+import '../tests/cipher/xsalsa20_poly1305_tests';
+import '../tests/cipher/xchacha20_poly1305_tests';
import '../tests/dh/dh_tests';
import '../tests/ecdh/ecdh_tests';
import '../tests/hash/hash_tests';
diff --git a/example/src/tests/cipher/xchacha20_poly1305_tests.ts b/example/src/tests/cipher/xchacha20_poly1305_tests.ts
new file mode 100644
index 00000000..e8b6d6d3
--- /dev/null
+++ b/example/src/tests/cipher/xchacha20_poly1305_tests.ts
@@ -0,0 +1,162 @@
+/**
+ * XChaCha20-Poly1305 tests
+ *
+ * Test vectors from IETF draft-irtf-cfrg-xchacha and libsodium test suite.
+ * XChaCha20-Poly1305 is an AEAD cipher with:
+ * - 32-byte key
+ * - 24-byte nonce (extended nonce)
+ * - 16-byte authentication tag
+ * - AAD (Additional Authenticated Data) support
+ */
+
+import {
+ Buffer,
+ createCipheriv,
+ createDecipheriv,
+} from 'react-native-quick-crypto';
+import { expect } from 'chai';
+import { test } from '../util';
+import { roundTripAuth } from './roundTrip';
+
+const SUITE = 'cipher';
+
+function fromHex(h: string | Buffer): Buffer {
+ if (typeof h === 'string') {
+ h = h.replace(/([^0-9a-f])/gi, '');
+ return Buffer.from(h, 'hex');
+ }
+ return h;
+}
+
+interface XChaCha20Poly1305TestVector {
+ key: string;
+ nonce: string;
+ plaintext: string;
+ aad: string | Buffer;
+ ciphertext: string;
+ tag: string;
+}
+
+// Test vector from IETF draft-irtf-cfrg-xchacha (Appendix A.3.1)
+const ietfA31Vector: XChaCha20Poly1305TestVector = {
+ key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f',
+ nonce: '404142434445464748494a4b4c4d4e4f5051525354555657',
+ plaintext:
+ '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' +
+ '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' +
+ '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' +
+ '637265656e20776f756c642062652069742e',
+ aad: '50515253c0c1c2c3c4c5c6c7',
+ ciphertext:
+ 'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb' +
+ '731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452' +
+ '2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9' +
+ '21f9664c97637da9768812f615c68b13b52e',
+ tag: 'c0875924c1c7987947deafd8780acf49',
+};
+
+function testXChaCha20Poly1305Vector(
+ vector: XChaCha20Poly1305TestVector,
+ description: string,
+) {
+ test(SUITE, `xchacha20-poly1305 ${description}`, () => {
+ const key = fromHex(vector.key);
+ const nonce = fromHex(vector.nonce);
+ const plaintext = fromHex(vector.plaintext);
+ const aad = fromHex(vector.aad);
+ const expectedCiphertext = fromHex(vector.ciphertext);
+ const expectedTag = fromHex(vector.tag);
+
+ // First test round trip
+ roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad);
+
+ // Then test against expected values
+ const cipher = createCipheriv('xchacha20-poly1305', key, nonce);
+ cipher.setAAD(aad);
+ const actualCiphertext = Buffer.concat([
+ cipher.update(plaintext),
+ cipher.final(),
+ ]);
+ const actualTag = cipher.getAuthTag();
+
+ expect(actualCiphertext).to.deep.equal(expectedCiphertext);
+ expect(actualTag).to.deep.equal(expectedTag);
+ });
+}
+
+testXChaCha20Poly1305Vector(ietfA31Vector, 'IETF draft A.3.1 vector');
+
+// Basic round-trip tests
+test(SUITE, 'xchacha20-poly1305 basic round trip', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.from('Hello, XChaCha20-Poly1305!', 'utf8');
+ const aad = Buffer.from('additional data', 'utf8');
+
+ roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad);
+});
+
+test(SUITE, 'xchacha20-poly1305 without AAD', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.from('Hello, XChaCha20-Poly1305!', 'utf8');
+
+ roundTripAuth('xchacha20-poly1305', key, nonce, plaintext);
+});
+
+test(SUITE, 'xchacha20-poly1305 empty plaintext', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.alloc(0);
+ const aad = Buffer.from('aad only', 'utf8');
+
+ roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad);
+});
+
+test(SUITE, 'xchacha20-poly1305 large plaintext', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.alloc(4096, 0x55);
+ const aad = Buffer.from('large data test', 'utf8');
+
+ roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad);
+});
+
+// Error case tests
+test(SUITE, 'xchacha20-poly1305 wrong key size throws', () => {
+ const key = Buffer.alloc(16, 0x42); // Wrong size: should be 32
+ const nonce = Buffer.alloc(24, 0x24);
+
+ expect(() => {
+ createCipheriv('xchacha20-poly1305', key, nonce);
+ }).to.throw(/key must be 32 bytes/i);
+});
+
+test(SUITE, 'xchacha20-poly1305 wrong nonce size throws', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(12, 0x24); // Wrong size: should be 24
+
+ expect(() => {
+ createCipheriv('xchacha20-poly1305', key, nonce);
+ }).to.throw(/nonce must be 24 bytes/i);
+});
+
+test(SUITE, 'xchacha20-poly1305 tag mismatch throws', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.from('test message', 'utf8');
+
+ // Encrypt
+ const cipher = createCipheriv('xchacha20-poly1305', key, nonce);
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
+
+ // Try to decrypt with wrong tag
+ const decipher = createDecipheriv('xchacha20-poly1305', key, nonce);
+ const wrongTag = Buffer.alloc(16, 0xff); // Wrong tag
+ decipher.setAuthTag(wrongTag);
+ decipher.update(ciphertext);
+
+ expect(() => {
+ decipher.final();
+ }).to.throw(/authentication tag mismatch/i);
+});
diff --git a/example/src/tests/cipher/xsalsa20_poly1305_tests.ts b/example/src/tests/cipher/xsalsa20_poly1305_tests.ts
new file mode 100644
index 00000000..2c9b43f7
--- /dev/null
+++ b/example/src/tests/cipher/xsalsa20_poly1305_tests.ts
@@ -0,0 +1,150 @@
+/**
+ * XSalsa20-Poly1305 tests
+ *
+ * XSalsa20-Poly1305 is an authenticated cipher (secretbox) with:
+ * - 32-byte key
+ * - 24-byte nonce (extended nonce)
+ * - 16-byte authentication tag
+ * - NO AAD support (unlike XChaCha20-Poly1305)
+ *
+ * This is the authenticated version of the existing XSalsa20 stream cipher.
+ */
+
+import {
+ Buffer,
+ createCipheriv,
+ createDecipheriv,
+} from 'react-native-quick-crypto';
+import { expect } from 'chai';
+import { test } from '../util';
+
+const SUITE = 'cipher';
+
+// Helper for XSalsa20-Poly1305 round trip (no AAD support)
+function roundTripXSalsa20Poly1305(
+ key: Buffer,
+ nonce: Buffer,
+ plaintext: Buffer,
+) {
+ // Encrypt
+ const cipher = createCipheriv('xsalsa20-poly1305', key, nonce);
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
+ const tag = cipher.getAuthTag();
+
+ // Decrypt
+ const decipher = createDecipheriv('xsalsa20-poly1305', key, nonce);
+ decipher.setAuthTag(tag);
+ const decrypted = Buffer.concat([
+ decipher.update(ciphertext),
+ decipher.final(),
+ ]);
+
+ expect(decrypted).to.deep.equal(plaintext);
+}
+
+// Basic round-trip tests
+test(SUITE, 'xsalsa20-poly1305 basic round trip', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.from('Hello, XSalsa20-Poly1305!', 'utf8');
+
+ roundTripXSalsa20Poly1305(key, nonce, plaintext);
+});
+
+test(SUITE, 'xsalsa20-poly1305 empty plaintext', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.alloc(0);
+
+ roundTripXSalsa20Poly1305(key, nonce, plaintext);
+});
+
+test(SUITE, 'xsalsa20-poly1305 large plaintext', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.alloc(4096, 0x55);
+
+ roundTripXSalsa20Poly1305(key, nonce, plaintext);
+});
+
+test(SUITE, 'xsalsa20-poly1305 single byte', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.from([0x42]);
+
+ roundTripXSalsa20Poly1305(key, nonce, plaintext);
+});
+
+// Test with known test vector from libsodium
+test(SUITE, 'xsalsa20-poly1305 test vector', () => {
+ // Test vector derived from libsodium secretbox tests
+ const key = Buffer.from(
+ '1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389',
+ 'hex',
+ );
+ const nonce = Buffer.from(
+ '69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37',
+ 'hex',
+ );
+ const plaintext = Buffer.from(
+ 'be075fc53c81f2d5cf141316ebeb0c7b5228c52a4c62cbd44b66849b64244ffce5e' +
+ 'cbaaf33bd751a1ac728d45e6c61296cdc3c01233561f41db66cce314adb310e3be8' +
+ '250c46f06dceea3a7fa1348057e2f6556ad6b1318a024a838f21af1fde048977eb4' +
+ '8f59ffd4924ca1c60902e52f0a089bc76897040e082f937763848645e0705',
+ 'hex',
+ );
+
+ // Round trip test
+ roundTripXSalsa20Poly1305(key, nonce, plaintext);
+});
+
+// Error case tests
+test(SUITE, 'xsalsa20-poly1305 wrong key size throws', () => {
+ const key = Buffer.alloc(16, 0x42); // Wrong size: should be 32
+ const nonce = Buffer.alloc(24, 0x24);
+
+ expect(() => {
+ createCipheriv('xsalsa20-poly1305', key, nonce);
+ }).to.throw(/key must be 32 bytes/i);
+});
+
+test(SUITE, 'xsalsa20-poly1305 wrong nonce size throws', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(12, 0x24); // Wrong size: should be 24
+
+ expect(() => {
+ createCipheriv('xsalsa20-poly1305', key, nonce);
+ }).to.throw(/nonce must be 24 bytes/i);
+});
+
+test(SUITE, 'xsalsa20-poly1305 tag mismatch throws', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const plaintext = Buffer.from('test message', 'utf8');
+
+ // Encrypt
+ const cipher = createCipheriv('xsalsa20-poly1305', key, nonce);
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
+
+ // Try to decrypt with wrong tag
+ const decipher = createDecipheriv('xsalsa20-poly1305', key, nonce);
+ const wrongTag = Buffer.alloc(16, 0xff); // Wrong tag
+ decipher.setAuthTag(wrongTag);
+ decipher.update(ciphertext);
+
+ expect(() => {
+ decipher.final();
+ }).to.throw(/authentication tag mismatch/i);
+});
+
+test(SUITE, 'xsalsa20-poly1305 setAAD throws (not supported)', () => {
+ const key = Buffer.alloc(32, 0x42);
+ const nonce = Buffer.alloc(24, 0x24);
+ const aad = Buffer.from('additional data', 'utf8');
+
+ const cipher = createCipheriv('xsalsa20-poly1305', key, nonce);
+
+ expect(() => {
+ cipher.setAAD(aad);
+ }).to.throw(/AAD.*not supported/i);
+});
diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt
index 68fe558e..35724ac1 100644
--- a/packages/react-native-quick-crypto/android/CMakeLists.txt
+++ b/packages/react-native-quick-crypto/android/CMakeLists.txt
@@ -32,6 +32,8 @@ add_library(
../cpp/cipher/HybridRsaCipher.cpp
../cpp/cipher/OCBCipher.cpp
../cpp/cipher/XSalsa20Cipher.cpp
+ ../cpp/cipher/XSalsa20Poly1305Cipher.cpp
+ ../cpp/cipher/XChaCha20Poly1305Cipher.cpp
../cpp/cipher/ChaCha20Cipher.cpp
../cpp/cipher/ChaCha20Poly1305Cipher.cpp
../cpp/dh/HybridDiffieHellman.cpp
diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp
index 3f9a53e3..e7309ce1 100644
--- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp
+++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp
@@ -11,7 +11,9 @@
#include "HybridCipherFactorySpec.hpp"
#include "OCBCipher.hpp"
#include "QuickCryptoUtils.hpp"
+#include "XChaCha20Poly1305Cipher.hpp"
#include "XSalsa20Cipher.hpp"
+#include "XSalsa20Poly1305Cipher.hpp"
namespace margelo::nitro::crypto {
@@ -88,7 +90,7 @@ class HybridCipherFactory : public HybridCipherFactorySpec {
}
EVP_CIPHER_free(cipher);
- // libsodium
+ // libsodium ciphers
std::string cipherName = toLower(args.cipherType);
if (cipherName == "xsalsa20") {
cipherInstance = std::make_shared();
@@ -96,6 +98,18 @@ class HybridCipherFactory : public HybridCipherFactorySpec {
cipherInstance->init(args.cipherKey, args.iv);
return cipherInstance;
}
+ if (cipherName == "xsalsa20-poly1305") {
+ cipherInstance = std::make_shared();
+ cipherInstance->setArgs(args);
+ cipherInstance->init(args.cipherKey, args.iv);
+ return cipherInstance;
+ }
+ if (cipherName == "xchacha20-poly1305") {
+ cipherInstance = std::make_shared();
+ cipherInstance->setArgs(args);
+ cipherInstance->init(args.cipherKey, args.iv);
+ return cipherInstance;
+ }
// Unsupported cipher type
throw std::runtime_error("Unsupported or unknown cipher type: " + args.cipherType);
diff --git a/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp
new file mode 100644
index 00000000..431e2d1c
--- /dev/null
+++ b/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp
@@ -0,0 +1,161 @@
+#include "XChaCha20Poly1305Cipher.hpp"
+
+#include
+#include
+
+#include "NitroModules/ArrayBuffer.hpp"
+#include "QuickCryptoUtils.hpp"
+
+namespace margelo::nitro::crypto {
+
+XChaCha20Poly1305Cipher::~XChaCha20Poly1305Cipher() {
+#ifdef BLSALLOC_SODIUM
+ sodium_memzero(key_, kKeySize);
+ sodium_memzero(nonce_, kNonceSize);
+ sodium_memzero(auth_tag_, kTagSize);
+ if (!data_buffer_.empty()) {
+ sodium_memzero(data_buffer_.data(), data_buffer_.size());
+ }
+ if (!aad_.empty()) {
+ sodium_memzero(aad_.data(), aad_.size());
+ }
+#else
+ std::memset(key_, 0, kKeySize);
+ std::memset(nonce_, 0, kNonceSize);
+ std::memset(auth_tag_, 0, kTagSize);
+#endif
+ data_buffer_.clear();
+ aad_.clear();
+}
+
+void XChaCha20Poly1305Cipher::init(const std::shared_ptr cipher_key, const std::shared_ptr iv) {
+ auto native_key = ToNativeArrayBuffer(cipher_key);
+ auto native_iv = ToNativeArrayBuffer(iv);
+
+ if (native_key->size() != kKeySize) {
+ throw std::runtime_error("XChaCha20-Poly1305 key must be 32 bytes, got " + std::to_string(native_key->size()) + " bytes");
+ }
+
+ if (native_iv->size() != kNonceSize) {
+ throw std::runtime_error("XChaCha20-Poly1305 nonce must be 24 bytes, got " + std::to_string(native_iv->size()) + " bytes");
+ }
+
+ std::memcpy(key_, native_key->data(), kKeySize);
+ std::memcpy(nonce_, native_iv->data(), kNonceSize);
+
+ data_buffer_.clear();
+ aad_.clear();
+ final_called_ = false;
+}
+
+std::shared_ptr XChaCha20Poly1305Cipher::update(const std::shared_ptr& data) {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XChaCha20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ auto native_data = ToNativeArrayBuffer(data);
+ size_t data_len = native_data->size();
+
+ size_t old_size = data_buffer_.size();
+ data_buffer_.resize(old_size + data_len);
+ std::memcpy(data_buffer_.data() + old_size, native_data->data(), data_len);
+
+ return std::make_shared(nullptr, 0, nullptr);
+#endif
+}
+
+std::shared_ptr XChaCha20Poly1305Cipher::final() {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XChaCha20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ if (is_cipher) {
+ uint8_t* ciphertext = new uint8_t[data_buffer_.size()];
+
+ int result =
+ crypto_aead_xchacha20poly1305_ietf_encrypt_detached(ciphertext, auth_tag_, nullptr, data_buffer_.data(), data_buffer_.size(),
+ aad_.empty() ? nullptr : aad_.data(), aad_.size(), nullptr, nonce_, key_);
+
+ if (result != 0) {
+ sodium_memzero(ciphertext, data_buffer_.size());
+ delete[] ciphertext;
+ throw std::runtime_error("XChaCha20Poly1305Cipher: encryption failed");
+ }
+
+ final_called_ = true;
+ size_t ct_len = data_buffer_.size();
+ return std::make_shared(ciphertext, ct_len, [=]() { delete[] ciphertext; });
+ } else {
+ if (data_buffer_.empty()) {
+ final_called_ = true;
+ return std::make_shared(nullptr, 0, nullptr);
+ }
+
+ uint8_t* plaintext = new uint8_t[data_buffer_.size()];
+
+ int result =
+ crypto_aead_xchacha20poly1305_ietf_decrypt_detached(plaintext, nullptr, data_buffer_.data(), data_buffer_.size(), auth_tag_,
+ aad_.empty() ? nullptr : aad_.data(), aad_.size(), nonce_, key_);
+
+ if (result != 0) {
+ sodium_memzero(plaintext, data_buffer_.size());
+ delete[] plaintext;
+ throw std::runtime_error("XChaCha20Poly1305Cipher: decryption failed - authentication tag mismatch");
+ }
+
+ final_called_ = true;
+ size_t pt_len = data_buffer_.size();
+ return std::make_shared(plaintext, pt_len, [=]() { delete[] plaintext; });
+ }
+#endif
+}
+
+bool XChaCha20Poly1305Cipher::setAAD(const std::shared_ptr& data, std::optional plaintextLength) {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XChaCha20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ auto native_aad = ToNativeArrayBuffer(data);
+ aad_.resize(native_aad->size());
+ std::memcpy(aad_.data(), native_aad->data(), native_aad->size());
+ return true;
+#endif
+}
+
+std::shared_ptr XChaCha20Poly1305Cipher::getAuthTag() {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XChaCha20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ if (!is_cipher) {
+ throw std::runtime_error("getAuthTag can only be called during encryption");
+ }
+ if (!final_called_) {
+ throw std::runtime_error("getAuthTag must be called after final()");
+ }
+
+ uint8_t* tag_copy = new uint8_t[kTagSize];
+ std::memcpy(tag_copy, auth_tag_, kTagSize);
+ return std::make_shared(tag_copy, kTagSize, [=]() { delete[] tag_copy; });
+#endif
+}
+
+bool XChaCha20Poly1305Cipher::setAuthTag(const std::shared_ptr& tag) {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XChaCha20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ if (is_cipher) {
+ throw std::runtime_error("setAuthTag can only be called during decryption");
+ }
+
+ auto native_tag = ToNativeArrayBuffer(tag);
+ if (native_tag->size() != kTagSize) {
+ throw std::runtime_error("XChaCha20-Poly1305 tag must be 16 bytes, got " + std::to_string(native_tag->size()) + " bytes");
+ }
+
+ std::memcpy(auth_tag_, native_tag->data(), kTagSize);
+ return true;
+#endif
+}
+
+bool XChaCha20Poly1305Cipher::setAutoPadding(bool autoPad) {
+ throw std::runtime_error("setAutoPadding is not supported for xchacha20-poly1305");
+}
+
+} // namespace margelo::nitro::crypto
diff --git a/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.hpp
new file mode 100644
index 00000000..52320c99
--- /dev/null
+++ b/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#ifdef BLSALLOC_SODIUM
+#include "sodium.h"
+#else
+#define crypto_aead_xchacha20poly1305_ietf_KEYBYTES 32U
+#define crypto_aead_xchacha20poly1305_ietf_NPUBBYTES 24U
+#define crypto_aead_xchacha20poly1305_ietf_ABYTES 16U
+#endif
+
+#include
+
+#include "HybridCipher.hpp"
+
+namespace margelo::nitro::crypto {
+
+class XChaCha20Poly1305Cipher : public HybridCipher {
+ public:
+ XChaCha20Poly1305Cipher() : HybridObject(TAG), final_called_(false) {}
+ ~XChaCha20Poly1305Cipher();
+
+ void init(const std::shared_ptr cipher_key, const std::shared_ptr iv) override;
+ std::shared_ptr update(const std::shared_ptr& data) override;
+ std::shared_ptr final() override;
+ bool setAAD(const std::shared_ptr& data, std::optional plaintextLength) override;
+ std::shared_ptr getAuthTag() override;
+ bool setAuthTag(const std::shared_ptr& tag) override;
+ bool setAutoPadding(bool autoPad) override;
+
+ private:
+ static constexpr size_t kKeySize = crypto_aead_xchacha20poly1305_ietf_KEYBYTES;
+ static constexpr size_t kNonceSize = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
+ static constexpr size_t kTagSize = crypto_aead_xchacha20poly1305_ietf_ABYTES;
+
+ uint8_t key_[kKeySize];
+ uint8_t nonce_[kNonceSize];
+ std::vector aad_;
+ std::vector data_buffer_;
+ uint8_t auth_tag_[kTagSize];
+ bool final_called_;
+};
+
+} // namespace margelo::nitro::crypto
diff --git a/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp
new file mode 100644
index 00000000..7b09ff09
--- /dev/null
+++ b/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp
@@ -0,0 +1,145 @@
+#include "XSalsa20Poly1305Cipher.hpp"
+
+#include
+#include
+
+#include "NitroModules/ArrayBuffer.hpp"
+#include "QuickCryptoUtils.hpp"
+
+namespace margelo::nitro::crypto {
+
+XSalsa20Poly1305Cipher::~XSalsa20Poly1305Cipher() {
+#ifdef BLSALLOC_SODIUM
+ sodium_memzero(key_, kKeySize);
+ sodium_memzero(nonce_, kNonceSize);
+ sodium_memzero(auth_tag_, kTagSize);
+ if (!data_buffer_.empty()) {
+ sodium_memzero(data_buffer_.data(), data_buffer_.size());
+ }
+#else
+ std::memset(key_, 0, kKeySize);
+ std::memset(nonce_, 0, kNonceSize);
+ std::memset(auth_tag_, 0, kTagSize);
+#endif
+ data_buffer_.clear();
+}
+
+void XSalsa20Poly1305Cipher::init(const std::shared_ptr cipher_key, const std::shared_ptr iv) {
+ auto native_key = ToNativeArrayBuffer(cipher_key);
+ auto native_iv = ToNativeArrayBuffer(iv);
+
+ if (native_key->size() != kKeySize) {
+ throw std::runtime_error("XSalsa20-Poly1305 key must be 32 bytes, got " + std::to_string(native_key->size()) + " bytes");
+ }
+
+ if (native_iv->size() != kNonceSize) {
+ throw std::runtime_error("XSalsa20-Poly1305 nonce must be 24 bytes, got " + std::to_string(native_iv->size()) + " bytes");
+ }
+
+ std::memcpy(key_, native_key->data(), kKeySize);
+ std::memcpy(nonce_, native_iv->data(), kNonceSize);
+
+ data_buffer_.clear();
+ final_called_ = false;
+}
+
+std::shared_ptr XSalsa20Poly1305Cipher::update(const std::shared_ptr& data) {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XSalsa20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ auto native_data = ToNativeArrayBuffer(data);
+ size_t data_len = native_data->size();
+
+ size_t old_size = data_buffer_.size();
+ data_buffer_.resize(old_size + data_len);
+ std::memcpy(data_buffer_.data() + old_size, native_data->data(), data_len);
+
+ return std::make_shared(nullptr, 0, nullptr);
+#endif
+}
+
+std::shared_ptr XSalsa20Poly1305Cipher::final() {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XSalsa20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ if (is_cipher) {
+ uint8_t* ciphertext = new uint8_t[data_buffer_.size()];
+
+ int result = crypto_secretbox_detached(ciphertext, auth_tag_, data_buffer_.data(), data_buffer_.size(), nonce_, key_);
+
+ if (result != 0) {
+ sodium_memzero(ciphertext, data_buffer_.size());
+ delete[] ciphertext;
+ throw std::runtime_error("XSalsa20Poly1305Cipher: encryption failed");
+ }
+
+ final_called_ = true;
+ size_t ct_len = data_buffer_.size();
+ return std::make_shared(ciphertext, ct_len, [=]() { delete[] ciphertext; });
+ } else {
+ if (data_buffer_.empty()) {
+ final_called_ = true;
+ return std::make_shared(nullptr, 0, nullptr);
+ }
+
+ uint8_t* plaintext = new uint8_t[data_buffer_.size()];
+
+ int result = crypto_secretbox_open_detached(plaintext, data_buffer_.data(), auth_tag_, data_buffer_.size(), nonce_, key_);
+
+ if (result != 0) {
+ sodium_memzero(plaintext, data_buffer_.size());
+ delete[] plaintext;
+ throw std::runtime_error("XSalsa20Poly1305Cipher: decryption failed - authentication tag mismatch");
+ }
+
+ final_called_ = true;
+ size_t pt_len = data_buffer_.size();
+ return std::make_shared(plaintext, pt_len, [=]() { delete[] plaintext; });
+ }
+#endif
+}
+
+bool XSalsa20Poly1305Cipher::setAAD(const std::shared_ptr& data, std::optional plaintextLength) {
+ throw std::runtime_error("AAD is not supported for xsalsa20-poly1305 (use xchacha20-poly1305 instead)");
+}
+
+std::shared_ptr XSalsa20Poly1305Cipher::getAuthTag() {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XSalsa20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ if (!is_cipher) {
+ throw std::runtime_error("getAuthTag can only be called during encryption");
+ }
+ if (!final_called_) {
+ throw std::runtime_error("getAuthTag must be called after final()");
+ }
+
+ uint8_t* tag_copy = new uint8_t[kTagSize];
+ std::memcpy(tag_copy, auth_tag_, kTagSize);
+ return std::make_shared(tag_copy, kTagSize, [=]() { delete[] tag_copy; });
+#endif
+}
+
+bool XSalsa20Poly1305Cipher::setAuthTag(const std::shared_ptr& tag) {
+#ifndef BLSALLOC_SODIUM
+ throw std::runtime_error("XSalsa20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)");
+#else
+ if (is_cipher) {
+ throw std::runtime_error("setAuthTag can only be called during decryption");
+ }
+
+ auto native_tag = ToNativeArrayBuffer(tag);
+ if (native_tag->size() != kTagSize) {
+ throw std::runtime_error("XSalsa20-Poly1305 tag must be 16 bytes, got " + std::to_string(native_tag->size()) + " bytes");
+ }
+
+ std::memcpy(auth_tag_, native_tag->data(), kTagSize);
+ return true;
+#endif
+}
+
+bool XSalsa20Poly1305Cipher::setAutoPadding(bool autoPad) {
+ throw std::runtime_error("setAutoPadding is not supported for xsalsa20-poly1305");
+}
+
+} // namespace margelo::nitro::crypto
diff --git a/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.hpp
new file mode 100644
index 00000000..60900f4c
--- /dev/null
+++ b/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#ifdef BLSALLOC_SODIUM
+#include "sodium.h"
+#else
+#define crypto_secretbox_xsalsa20poly1305_KEYBYTES 32U
+#define crypto_secretbox_xsalsa20poly1305_NONCEBYTES 24U
+#define crypto_secretbox_xsalsa20poly1305_MACBYTES 16U
+#endif
+
+#include
+
+#include "HybridCipher.hpp"
+
+namespace margelo::nitro::crypto {
+
+class XSalsa20Poly1305Cipher : public HybridCipher {
+ public:
+ XSalsa20Poly1305Cipher() : HybridObject(TAG), final_called_(false) {}
+ ~XSalsa20Poly1305Cipher();
+
+ void init(const std::shared_ptr cipher_key, const std::shared_ptr iv) override;
+ std::shared_ptr update(const std::shared_ptr& data) override;
+ std::shared_ptr final() override;
+ bool setAAD(const std::shared_ptr& data, std::optional plaintextLength) override;
+ std::shared_ptr getAuthTag() override;
+ bool setAuthTag(const std::shared_ptr& tag) override;
+ bool setAutoPadding(bool autoPad) override;
+
+ private:
+ static constexpr size_t kKeySize = crypto_secretbox_xsalsa20poly1305_KEYBYTES;
+ static constexpr size_t kNonceSize = crypto_secretbox_xsalsa20poly1305_NONCEBYTES;
+ static constexpr size_t kTagSize = crypto_secretbox_xsalsa20poly1305_MACBYTES;
+
+ uint8_t key_[kKeySize];
+ uint8_t nonce_[kNonceSize];
+ std::vector data_buffer_;
+ uint8_t auth_tag_[kTagSize];
+ bool final_called_;
+};
+
+} // namespace margelo::nitro::crypto