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