Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .docs/implementation-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
113 changes: 111 additions & 2 deletions docs/content/docs/api/cipher.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

<Callout type="warn" title="Requires SODIUM_ENABLED">
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.
</Callout>

| 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.
Expand Down Expand Up @@ -141,7 +187,7 @@ function encrypt(text: string) {
}
```

### File Encryption (Scanning)
### File Encryption (Streaming)

Encrypting a file using streams with AES-CTR (counter mode).

Expand All @@ -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.

<Callout type="info" title="Requires libsodium">
Set `SODIUM_ENABLED=1` environment variable before building.
</Callout>

```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()]);
```
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2811,7 +2811,7 @@ SPEC CHECKSUMS:
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
QuickCrypto: 7f2ca14820e56e0785fad4e1b5527f2cc7b962d3
QuickCrypto: 9e46baaa4fea5a22fdf23c3aae184b983c948b23
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions example/src/hooks/useTestsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
162 changes: 162 additions & 0 deletions example/src/tests/cipher/xchacha20_poly1305_tests.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading
Loading