diff --git a/.claude/rules/code-cpp.xml b/.claude/rules/code-cpp.xml
index ddcb9d38..914dbca8 100644
--- a/.claude/rules/code-cpp.xml
+++ b/.claude/rules/code-cpp.xml
@@ -185,4 +185,14 @@
Link against OpenSSL 3.3+
+
+
+ Code Formatting
+ Run clang-format on all C++ files before committing
+
+ Run clang-format -i on all modified .cpp/.hpp/.h files
+ Pre-commit hook enforces clang-format compliance
+
+ clang-format -i path/to/file.cpp
+
diff --git a/README.md b/README.md
index a1378b19..affbacb0 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
A fast implementation of Node's `crypto` module.
-> Note: This version `1.x` is undergoing a major refactor, porting to New Architecture, Bridgeless, and [`Nitro Modules`](https://github.com/mrousavy/react-native-nitro) and is incomplete compared to the `0.x` version. Status, as always, will be represented in [implementation-coverage.md](../main/docs/implementation-coverage.md).
+> Note: This version `1.x` completed a major refactor, porting to OpenSSL 3.3+, New Architecture, Bridgeless, and [`Nitro Modules`](https://github.com/mrousavy/react-native-nitro). It should be at or above feature-parity compared to the `0.x` version. Status, as always, will be represented in [implementation-coverage.md](../main/docs/implementation-coverage.md).
> Note: Minimum supported version of React Native is `0.75`. If you need to use earlier versions, please use `0.x` versions of this library.
@@ -33,28 +33,13 @@ QuickCrypto can be used as a drop-in replacement for your Web3/Crypto apps to sp
| `1.x` | new [->](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/enable-apps.md) | Nitro Modules [->](https://github.com/mrousavy/nitro) |
| `0.x` | old, new 🤞 | Bridge & JSI |
-## Benchmarks
-
-For example, creating a Wallet using ethers.js uses complex algorithms to generate a private-key/mnemonic-phrase pair:
-
-```ts
-const start = performance.now();
-const wallet = ethers.Wallet.createRandom();
-const end = performance.now();
-console.log(`Creating a Wallet took ${end - start} ms.`);
-```
-
-**Without** react-native-quick-crypto 🐢:
+## Migration
-```
-Creating a Wallet took 16862 ms
-```
+Our goal in refactoring to v1.0 was to maintain API compatibility. If you are upgrading to v1.0 from v0.x, and find any discrepancies, please open an issue in this repo.
-**With** react-native-quick-crypto ⚡️:
+## Benchmarks
-```
-Creating a Wallet took 289 ms
-```
+There is a benchmark suite in the Example app in this repo that has benchmarks of algorithms against their pure JS counterparts. This is not meant to disparage the other libraries. On the contrary, they perform amazingly well when used in a server-side Node environment. This library exists because React Native does not have that environment nor the Node Crypto API implementation at hand. So the benchmark suite is there to show you the speedup vs. the alternative of using a pure JS library on React Native.
---
@@ -154,8 +139,6 @@ const hashed = QuickCrypto.createHash('sha256')
## Limitations
-As the library uses JSI for synchronous native methods access, remote debugging (e.g. with Chrome) is no longer possible. Instead, you should use [Flipper](https://fbflipper.com).
-
Not all cryptographic algorithms are supported yet. See the [implementation coverage](./docs/implementation-coverage.md) document for more details. If you need a specific algorithm, please open a `feature request` issue and we'll see what we can do.
## Community Discord
@@ -164,7 +147,7 @@ Not all cryptographic algorithms are supported yet. See the [implementation cove
## Adopting at scale
-react-native-quick-crypto was built at Margelo, an elite app development agency. For enterprise support or other business inquiries, contact us at hello@margelo.io!
+`react-native-quick-crypto` was built at Margelo, an elite app development agency. For enterprise support or other business inquiries, contact us at hello@margelo.io!
## Contributing
diff --git a/docs/VERSION-1.0.0.md b/docs/VERSION-1.0.0.md
deleted file mode 100644
index ddee3165..00000000
--- a/docs/VERSION-1.0.0.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Version 1.0.0 - Post-Release Enhancements
-
-Items deferred from the 1.0.0 release for future consideration.
-
-## Known Limitations
-
-### 1. `generateKeyPairSync()` for RSA/EC
-
-**Status:** Throws descriptive error; async version works
-
-**Current Behavior:**
-```typescript
-generateKeyPairSync('rsa', options); // throws "Sync key generation for RSA/EC not yet implemented"
-generateKeyPair('rsa', options, callback); // works
-```
-
-**Implementation Notes:**
-- Async version uses WebCrypto-style async APIs internally
-- True sync requires adding synchronous C++ methods to `HybridRsaKeyPair` and `HybridEcKeyPair`
-- Pattern exists in Ed25519/X25519 which have working sync variants
-- Work: Add sync variants to RSA/EC Nitro specs, implement in C++, wire up TypeScript
-
-**Effort:** Medium-High (~2-3 hours)
-
----
-
-### 2. `publicEncrypt`/`publicDecrypt` - PKCS1 Padding
-
-**Status:** Only OAEP padding supported
-
-**Current Behavior:**
-```typescript
-publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer); // works
-publicEncrypt({ key, padding: constants.RSA_PKCS1_PADDING }, buffer); // not implemented
-```
-
-**Implementation Notes:**
-- `HybridRsaCipher` already exists with OAEP support
-- PKCS1 padding is simpler than OAEP (no hash algorithm, no label)
-- Add padding mode parameter and handle `RSA_PKCS1_PADDING` in C++
-- OpenSSL EVP APIs support it directly
-
-**Effort:** Low-Medium (~1-2 hours)
-
----
-
-### 3. `privateEncrypt`/`privateDecrypt`
-
-**Status:** Not implemented (0.x didn't have these either)
-
-**Description:**
-- Inverse operations of publicEncrypt/publicDecrypt
-- Sign with private key, verify/decrypt with public key
-- Used for raw RSA signatures
-
-**Implementation Notes:**
-- Similar to publicEncrypt/publicDecrypt but swap key roles
-- Can reuse much of `HybridRsaCipher` infrastructure
-- OpenSSL: `EVP_PKEY_sign`/`EVP_PKEY_verify` or raw RSA operations
-
-**Effort:** Low-Medium (~1-2 hours)
-
----
-
-## Summary
-
-| Feature | Effort | Priority |
-|---------|--------|----------|
-| `generateKeyPairSync` RSA/EC | Medium-High | Low (async works) |
-| PKCS1 padding | Low-Medium | Medium |
-| `privateEncrypt`/`privateDecrypt` | Low-Medium | Low (0.x parity) |
-
-**Total estimated effort:** 4-7 hours
-
-These can be addressed based on user demand post-1.0.0 release.
diff --git a/docs/implementation-coverage.md b/docs/implementation-coverage.md
index fa01f9df..14f68865 100644
--- a/docs/implementation-coverage.md
+++ b/docs/implementation-coverage.md
@@ -133,8 +133,8 @@ This document attempts to describe the implementation status of Crypto APIs/Inte
* ❌ `crypto.hkdfSync(digest, ikm, salt, info, keylen)`
* ✅ `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`
* ✅ `crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)`
- * ❌ `crypto.privateDecrypt(privateKey, buffer)`
- * ❌ `crypto.privateEncrypt(privateKey, buffer)`
+ * ✅ `crypto.privateDecrypt(privateKey, buffer)`
+ * ✅ `crypto.privateEncrypt(privateKey, buffer)`
* ✅ `crypto.publicDecrypt(key, buffer)`
* ✅ `crypto.publicEncrypt(key, buffer)`
* ✅ `crypto.randomBytes(size[, callback])`
@@ -185,10 +185,10 @@ This document attempts to describe the implementation status of Crypto APIs/Inte
## `crypto.generateKeyPairSync`
| type | Status |
| --------- | :----: |
-| `rsa` | ❌ |
-| `rsa-pss` | ❌ |
+| `rsa` | ✅ |
+| `rsa-pss` | ✅ |
| `dsa` | ❌ |
-| `ec` | ❌ |
+| `ec` | ✅ |
| `ed25519` | ✅ |
| `ed448` | ✅ |
| `x25519` | ✅ |
diff --git a/example/src/tests/keys/generate_keypair.ts b/example/src/tests/keys/generate_keypair.ts
index 3188cbe8..60f2dc77 100644
--- a/example/src/tests/keys/generate_keypair.ts
+++ b/example/src/tests/keys/generate_keypair.ts
@@ -502,16 +502,137 @@ test(
},
);
-test(
- SUITE,
- 'generateKeyPairSync RSA throws (not implemented sync for RSA)',
- async () => {
- await assertThrowsAsync(async () => {
- generateKeyPairSync('rsa', {
- modulusLength: 2048,
- publicKeyEncoding: { type: 'spki', format: 'pem' },
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
- });
- }, '');
- },
-);
+// --- generateKeyPairSync RSA Tests ---
+
+test(SUITE, 'generateKeyPairSync RSA 2048-bit with PEM encoding', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ expect(typeof privateKey).to.equal('string');
+ expect(typeof publicKey).to.equal('string');
+ expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/);
+ expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/);
+});
+
+test(SUITE, 'generateKeyPairSync RSA with DER encoding', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: { type: 'spki', format: 'der' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' },
+ });
+
+ expect(privateKey instanceof ArrayBuffer).to.equal(true);
+ expect(publicKey instanceof ArrayBuffer).to.equal(true);
+ expect((privateKey as ArrayBuffer).byteLength).to.be.greaterThan(0);
+ expect((publicKey as ArrayBuffer).byteLength).to.be.greaterThan(0);
+});
+
+test(SUITE, 'generateKeyPairSync RSA keys work for signing', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ const testData = 'Test data for sync RSA signing';
+ const signature = createSign('SHA256')
+ .update(testData)
+ .sign(privateKey as string);
+ const isValid = createVerify('SHA256')
+ .update(testData)
+ .verify(publicKey as string, signature);
+
+ expect(isValid).to.equal(true);
+});
+
+test(SUITE, 'generateKeyPairSync RSA-PSS', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('rsa-pss', {
+ modulusLength: 2048,
+ hashAlgorithm: 'SHA-256',
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ expect(typeof privateKey).to.equal('string');
+ expect(typeof publicKey).to.equal('string');
+ expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/);
+ expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/);
+});
+
+// --- generateKeyPairSync EC Tests ---
+
+test(SUITE, 'generateKeyPairSync EC P-256', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('ec', {
+ namedCurve: 'P-256',
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ expect(typeof privateKey).to.equal('string');
+ expect(typeof publicKey).to.equal('string');
+ expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/);
+ expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/);
+
+ const key = createPrivateKey(privateKey as string);
+ expect(key.asymmetricKeyType).to.equal('ec');
+});
+
+test(SUITE, 'generateKeyPairSync EC P-384', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('ec', {
+ namedCurve: 'P-384',
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ expect(typeof privateKey).to.equal('string');
+ expect(typeof publicKey).to.equal('string');
+ expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/);
+ expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/);
+});
+
+test(SUITE, 'generateKeyPairSync EC P-521', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('ec', {
+ namedCurve: 'P-521',
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ expect(typeof privateKey).to.equal('string');
+ expect(typeof publicKey).to.equal('string');
+ expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/);
+ expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/);
+});
+
+test(SUITE, 'generateKeyPairSync EC keys work for signing', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('ec', {
+ namedCurve: 'P-256',
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ });
+
+ const testData = 'Test data for sync ECDSA signing';
+ const signature = createSign('SHA256')
+ .update(testData)
+ .sign(privateKey as string);
+ const isValid = createVerify('SHA256')
+ .update(testData)
+ .verify(publicKey as string, signature);
+
+ expect(isValid).to.equal(true);
+});
+
+test(SUITE, 'generateKeyPairSync EC with DER encoding', () => {
+ const { privateKey, publicKey } = generateKeyPairSync('ec', {
+ namedCurve: 'P-256',
+ publicKeyEncoding: { type: 'spki', format: 'der' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' },
+ });
+
+ expect(privateKey instanceof ArrayBuffer).to.equal(true);
+ expect(publicKey instanceof ArrayBuffer).to.equal(true);
+ expect((privateKey as ArrayBuffer).byteLength).to.be.greaterThan(0);
+ expect((publicKey as ArrayBuffer).byteLength).to.be.greaterThan(0);
+});
diff --git a/example/src/tests/keys/public_cipher.ts b/example/src/tests/keys/public_cipher.ts
index d4d84134..9563af13 100644
--- a/example/src/tests/keys/public_cipher.ts
+++ b/example/src/tests/keys/public_cipher.ts
@@ -2,11 +2,14 @@ import { Buffer } from '@craftzdog/react-native-buffer';
import {
publicEncrypt,
publicDecrypt,
+ privateEncrypt,
+ privateDecrypt,
generateKeyPair,
createPrivateKey,
createPublicKey,
subtle,
isCryptoKeyPair,
+ constants,
} from 'react-native-quick-crypto';
import type { WebCryptoKeyPair } from 'react-native-quick-crypto';
import { expect } from 'chai';
@@ -551,3 +554,391 @@ test(SUITE, 'publicEncrypt fails with oversized plaintext', async () => {
publicEncrypt({ key: publicKey, oaepHash: 'SHA-256' }, oversizedPlaintext);
}, '');
});
+
+// --- PKCS1 v1.5 Padding Tests ---
+
+test(SUITE, 'publicEncrypt/publicDecrypt with PKCS1 padding', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ const encrypted = publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ shortPlaintext,
+ );
+ const decrypted = publicDecrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
+ encrypted,
+ );
+
+ expect(decrypted.toString()).to.equal(shortPlaintext.toString());
+});
+
+test(SUITE, 'PKCS1 padding round-trip with longer message', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ const encrypted = publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ testMessage,
+ );
+ const decrypted = publicDecrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
+ encrypted,
+ );
+
+ expect(Buffer.compare(decrypted, testMessage)).to.equal(0);
+});
+
+test(SUITE, 'PKCS1 padding max plaintext size', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // For RSA PKCS1 v1.5: max = keySize - 11 = 256 - 11 = 245 bytes
+ const maxPlaintext = Buffer.alloc(245, 'B');
+ const encrypted = publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ maxPlaintext,
+ );
+ const decrypted = publicDecrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
+ encrypted,
+ );
+
+ expect(Buffer.compare(decrypted, maxPlaintext)).to.equal(0);
+});
+
+test(SUITE, 'PKCS1 fails with oversized plaintext', async () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ // For RSA PKCS1 v1.5: max = 245 bytes, try 246
+ const oversizedPlaintext = Buffer.alloc(246, 'C');
+
+ await assertThrowsAsync(async () => {
+ publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ oversizedPlaintext,
+ );
+ }, '');
+});
+
+test(SUITE, 'OAEP and PKCS1 produce different ciphertexts', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // Encrypt with OAEP
+ const encryptedOaep = publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_OAEP_PADDING },
+ shortPlaintext,
+ );
+
+ // Encrypt with PKCS1
+ const encryptedPkcs1 = publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ shortPlaintext,
+ );
+
+ // Both should decrypt correctly with matching padding
+ const decryptedOaep = publicDecrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_OAEP_PADDING },
+ encryptedOaep,
+ );
+ const decryptedPkcs1 = publicDecrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
+ encryptedPkcs1,
+ );
+
+ expect(decryptedOaep.toString()).to.equal(shortPlaintext.toString());
+ expect(decryptedPkcs1.toString()).to.equal(shortPlaintext.toString());
+
+ // Ciphertexts should be different (different padding schemes)
+ expect(Buffer.compare(encryptedOaep, encryptedPkcs1)).to.not.equal(0);
+});
+
+// --- privateEncrypt / privateDecrypt Tests ---
+
+const PRIVATE_CIPHER_SUITE = 'keys.privateEncrypt/privateDecrypt';
+
+test(PRIVATE_CIPHER_SUITE, 'privateEncrypt/privateDecrypt round-trip', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // Encrypt with private key
+ const encrypted = privateEncrypt(privateKey, shortPlaintext);
+ // Decrypt with public key
+ const decrypted = privateDecrypt(publicKey, encrypted);
+
+ expect(decrypted.toString()).to.equal(shortPlaintext.toString());
+});
+
+test(
+ PRIVATE_CIPHER_SUITE,
+ 'privateEncrypt/privateDecrypt with explicit PKCS1 padding',
+ () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ const encrypted = privateEncrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
+ testMessage,
+ );
+ const decrypted = privateDecrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ encrypted,
+ );
+
+ expect(Buffer.compare(decrypted, testMessage)).to.equal(0);
+ },
+);
+
+test(PRIVATE_CIPHER_SUITE, 'privateEncrypt with PEM key', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // Export to PEM for test
+ const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });
+ const publicPem = publicKey.export({ type: 'spki', format: 'pem' });
+
+ const encrypted = privateEncrypt(privatePem as string, shortPlaintext);
+ const decrypted = privateDecrypt(publicPem as string, encrypted);
+
+ expect(decrypted.toString()).to.equal(shortPlaintext.toString());
+});
+
+test(
+ PRIVATE_CIPHER_SUITE,
+ 'privateEncrypt/privateDecrypt with generateKeyPair',
+ async () => {
+ const { privateKey, publicKey } = await new Promise<{
+ privateKey: string;
+ publicKey: string;
+ }>((resolve, reject) => {
+ generateKeyPair(
+ 'rsa',
+ {
+ modulusLength: 2048,
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+ },
+ (err, pubKey, privKey) => {
+ if (err) reject(err);
+ else
+ resolve({
+ privateKey: privKey as string,
+ publicKey: pubKey as string,
+ });
+ },
+ );
+ });
+
+ const encrypted = privateEncrypt(privateKey, testMessage);
+ const decrypted = privateDecrypt(publicKey, encrypted);
+
+ expect(Buffer.compare(decrypted, testMessage)).to.equal(0);
+ },
+);
+
+test(PRIVATE_CIPHER_SUITE, 'privateEncrypt max plaintext size', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // For RSA PKCS1 v1.5 signing: max = keySize - 11 = 256 - 11 = 245 bytes
+ const maxPlaintext = Buffer.alloc(245, 'D');
+ const encrypted = privateEncrypt(privateKey, maxPlaintext);
+ const decrypted = privateDecrypt(publicKey, encrypted);
+
+ expect(Buffer.compare(decrypted, maxPlaintext)).to.equal(0);
+});
+
+test(
+ PRIVATE_CIPHER_SUITE,
+ 'privateEncrypt fails with oversized data',
+ async () => {
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // For RSA PKCS1 v1.5: max = 245 bytes, try 246
+ const oversizedData = Buffer.alloc(246, 'E');
+
+ await assertThrowsAsync(async () => {
+ privateEncrypt(privateKey, oversizedData);
+ }, '');
+ },
+);
+
+test(PRIVATE_CIPHER_SUITE, 'privateEncrypt requires private key', async () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ await assertThrowsAsync(async () => {
+ privateEncrypt(publicKey, shortPlaintext);
+ }, 'privateEncrypt requires a private key');
+});
+
+test(PRIVATE_CIPHER_SUITE, 'privateDecrypt requires public key', async () => {
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ const encrypted = privateEncrypt(privateKey, shortPlaintext);
+
+ await assertThrowsAsync(async () => {
+ privateDecrypt(privateKey, encrypted);
+ }, 'privateDecrypt requires a public key');
+});
+
+test(PRIVATE_CIPHER_SUITE, 'privateEncrypt empty plaintext', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ const emptyBuffer = Buffer.from('');
+ const encrypted = privateEncrypt(privateKey, emptyBuffer);
+ const decrypted = privateDecrypt(publicKey, encrypted);
+
+ expect(decrypted.length).to.equal(0);
+});
+
+test(PRIVATE_CIPHER_SUITE, 'privateEncrypt single byte', () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ const singleByte = Buffer.from([0x42]);
+ const encrypted = privateEncrypt(privateKey, singleByte);
+ const decrypted = privateDecrypt(publicKey, encrypted);
+
+ expect(decrypted.length).to.equal(1);
+ expect(decrypted[0]).to.equal(0x42);
+});
+
+test(
+ PRIVATE_CIPHER_SUITE,
+ 'publicEncrypt/privateDecrypt are inverses (cross-compatibility)',
+ () => {
+ const publicKey = createPublicKey({
+ key: Buffer.from(spki),
+ format: 'der',
+ type: 'spki',
+ });
+
+ const privateKey = createPrivateKey({
+ key: Buffer.from(pkcs8),
+ format: 'der',
+ type: 'pkcs8',
+ });
+
+ // publicEncrypt with PKCS1 -> privateDecrypt with private key (standard decrypt)
+ const encrypted = publicEncrypt(
+ { key: publicKey, padding: constants.RSA_PKCS1_PADDING },
+ shortPlaintext,
+ );
+
+ // This should work with publicDecrypt using private key
+ const decrypted = publicDecrypt(
+ { key: privateKey, padding: constants.RSA_PKCS1_PADDING },
+ encrypted,
+ );
+
+ expect(decrypted.toString()).to.equal(shortPlaintext.toString());
+ },
+);
diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp
index 109e81a9..1d7c120d 100644
--- a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp
+++ b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp
@@ -11,7 +11,9 @@ namespace margelo::nitro::crypto {
using margelo::nitro::NativeArrayBuffer;
-// Helper to get OpenSSL digest from hash algorithm name
+constexpr int kRsaPkcs1Padding = 1;
+constexpr int kRsaOaepPadding = 4;
+
const EVP_MD* getDigestByName(const std::string& hashAlgorithm) {
if (hashAlgorithm == "SHA-1" || hashAlgorithm == "SHA1" || hashAlgorithm == "sha1" || hashAlgorithm == "sha-1") {
return EVP_sha1();
@@ -25,10 +27,21 @@ const EVP_MD* getDigestByName(const std::string& hashAlgorithm) {
throw std::runtime_error("Unsupported hash algorithm: " + hashAlgorithm);
}
+int toOpenSSLPadding(int padding) {
+ switch (padding) {
+ case kRsaPkcs1Padding:
+ return RSA_PKCS1_PADDING;
+ case kRsaOaepPadding:
+ return RSA_PKCS1_OAEP_PADDING;
+ default:
+ throw std::runtime_error("Unsupported padding mode: " + std::to_string(padding));
+ }
+}
+
std::shared_ptr HybridRsaCipher::encrypt(const std::shared_ptr& keyHandle,
- const std::shared_ptr& data, const std::string& hashAlgorithm,
+ const std::shared_ptr& data, double padding,
+ const std::string& hashAlgorithm,
const std::optional>& label) {
- // Get the EVP_PKEY from the key handle
auto keyHandleImpl = std::static_pointer_cast(keyHandle);
EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get();
@@ -36,13 +49,11 @@ std::shared_ptr HybridRsaCipher::encrypt(const std::shared_ptr HybridRsaCipher::encrypt(const std::shared_ptr(padding);
+ int opensslPadding = toOpenSSLPadding(paddingInt);
- // Set MGF1 hash (same as OAEP hash per WebCrypto spec)
- if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) {
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, opensslPadding) <= 0) {
EVP_PKEY_CTX_free(ctx);
- throw std::runtime_error("Failed to set MGF1 hash algorithm");
+ throw std::runtime_error("Failed to set RSA padding");
}
- // Set OAEP label if provided
- if (label.has_value() && label.value()->size() > 0) {
- auto native_label = ToNativeArrayBuffer(label.value());
- // OpenSSL takes ownership of the label, so we need to allocate a copy
- unsigned char* label_copy = (unsigned char*)OPENSSL_malloc(native_label->size());
- if (!label_copy) {
+ if (paddingInt == kRsaOaepPadding) {
+ const EVP_MD* md = getDigestByName(hashAlgorithm);
+ if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) {
EVP_PKEY_CTX_free(ctx);
- throw std::runtime_error("Failed to allocate memory for label");
+ throw std::runtime_error("Failed to set OAEP hash algorithm");
}
- std::memcpy(label_copy, native_label->data(), native_label->size());
- if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label_copy, native_label->size()) <= 0) {
- OPENSSL_free(label_copy);
+ if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) {
EVP_PKEY_CTX_free(ctx);
- throw std::runtime_error("Failed to set OAEP label");
+ throw std::runtime_error("Failed to set MGF1 hash algorithm");
+ }
+
+ if (label.has_value() && label.value()->size() > 0) {
+ auto native_label = ToNativeArrayBuffer(label.value());
+ unsigned char* label_copy = (unsigned char*)OPENSSL_malloc(native_label->size());
+ if (!label_copy) {
+ EVP_PKEY_CTX_free(ctx);
+ throw std::runtime_error("Failed to allocate memory for label");
+ }
+ std::memcpy(label_copy, native_label->data(), native_label->size());
+
+ if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label_copy, native_label->size()) <= 0) {
+ OPENSSL_free(label_copy);
+ EVP_PKEY_CTX_free(ctx);
+ throw std::runtime_error("Failed to set OAEP label");
+ }
}
}
- // Get input data
auto native_data = ToNativeArrayBuffer(data);
const unsigned char* in = native_data->data();
size_t inlen = native_data->size();
- // Determine output length
size_t outlen;
if (EVP_PKEY_encrypt(ctx, nullptr, &outlen, in, inlen) <= 0) {
EVP_PKEY_CTX_free(ctx);
@@ -103,10 +112,8 @@ std::shared_ptr HybridRsaCipher::encrypt(const std::shared_ptr(outlen);
- // Perform encryption
if (EVP_PKEY_encrypt(ctx, out_buf.get(), &outlen, in, inlen) <= 0) {
EVP_PKEY_CTX_free(ctx);
unsigned long err = ERR_get_error();
@@ -117,15 +124,14 @@ std::shared_ptr HybridRsaCipher::encrypt(const std::shared_ptr(out_buf.release(), outlen, [raw_ptr]() { delete[] raw_ptr; });
}
std::shared_ptr HybridRsaCipher::decrypt(const std::shared_ptr& keyHandle,
- const std::shared_ptr& data, const std::string& hashAlgorithm,
+ const std::shared_ptr& data, double padding,
+ const std::string& hashAlgorithm,
const std::optional>& label) {
- // Get the EVP_PKEY from the key handle
auto keyHandleImpl = std::static_pointer_cast(keyHandle);
EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get();
@@ -133,13 +139,11 @@ std::shared_ptr HybridRsaCipher::decrypt(const std::shared_ptr HybridRsaCipher::decrypt(const std::shared_ptr(padding);
+ int opensslPadding = toOpenSSLPadding(paddingInt);
- // Set MGF1 hash (same as OAEP hash per WebCrypto spec)
- if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) {
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, opensslPadding) <= 0) {
EVP_PKEY_CTX_free(ctx);
- throw std::runtime_error("Failed to set MGF1 hash algorithm");
+ throw std::runtime_error("Failed to set RSA padding");
}
- // Set OAEP label if provided
- if (label.has_value() && label.value()->size() > 0) {
- auto native_label = ToNativeArrayBuffer(label.value());
- // OpenSSL takes ownership of the label, so we need to allocate a copy
- unsigned char* label_copy = (unsigned char*)OPENSSL_malloc(native_label->size());
- if (!label_copy) {
+ if (paddingInt == kRsaOaepPadding) {
+ const EVP_MD* md = getDigestByName(hashAlgorithm);
+ if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) {
EVP_PKEY_CTX_free(ctx);
- throw std::runtime_error("Failed to allocate memory for label");
+ throw std::runtime_error("Failed to set OAEP hash algorithm");
}
- std::memcpy(label_copy, native_label->data(), native_label->size());
- if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label_copy, native_label->size()) <= 0) {
- OPENSSL_free(label_copy);
+ if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) {
EVP_PKEY_CTX_free(ctx);
- throw std::runtime_error("Failed to set OAEP label");
+ throw std::runtime_error("Failed to set MGF1 hash algorithm");
+ }
+
+ if (label.has_value() && label.value()->size() > 0) {
+ auto native_label = ToNativeArrayBuffer(label.value());
+ unsigned char* label_copy = (unsigned char*)OPENSSL_malloc(native_label->size());
+ if (!label_copy) {
+ EVP_PKEY_CTX_free(ctx);
+ throw std::runtime_error("Failed to allocate memory for label");
+ }
+ std::memcpy(label_copy, native_label->data(), native_label->size());
+
+ if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label_copy, native_label->size()) <= 0) {
+ OPENSSL_free(label_copy);
+ EVP_PKEY_CTX_free(ctx);
+ throw std::runtime_error("Failed to set OAEP label");
+ }
}
}
- // Get input data
auto native_data = ToNativeArrayBuffer(data);
const unsigned char* in = native_data->data();
size_t inlen = native_data->size();
- // Determine output length
size_t outlen;
if (EVP_PKEY_decrypt(ctx, nullptr, &outlen, in, inlen) <= 0) {
EVP_PKEY_CTX_free(ctx);
@@ -200,10 +202,8 @@ std::shared_ptr HybridRsaCipher::decrypt(const std::shared_ptr(outlen);
- // Perform decryption
if (EVP_PKEY_decrypt(ctx, out_buf.get(), &outlen, in, inlen) <= 0) {
EVP_PKEY_CTX_free(ctx);
unsigned long err = ERR_get_error();
@@ -214,7 +214,124 @@ std::shared_ptr HybridRsaCipher::decrypt(const std::shared_ptr(out_buf.release(), outlen, [raw_ptr]() { delete[] raw_ptr; });
+}
+
+std::shared_ptr HybridRsaCipher::privateEncrypt(const std::shared_ptr& keyHandle,
+ const std::shared_ptr& data, double padding) {
+ auto keyHandleImpl = std::static_pointer_cast(keyHandle);
+ EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get();
+
+ if (!pkey) {
+ throw std::runtime_error("Invalid key for RSA private encryption");
+ }
+
+ EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
+ if (!ctx) {
+ throw std::runtime_error("Failed to create EVP_PKEY_CTX");
+ }
+
+ if (EVP_PKEY_sign_init(ctx) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ unsigned long err = ERR_get_error();
+ char err_buf[256];
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
+ throw std::runtime_error("Failed to initialize signing: " + std::string(err_buf));
+ }
+
+ int paddingInt = static_cast(padding);
+ int opensslPadding = toOpenSSLPadding(paddingInt);
+
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, opensslPadding) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ throw std::runtime_error("Failed to set RSA padding");
+ }
+
+ auto native_data = ToNativeArrayBuffer(data);
+ const unsigned char* in = native_data->data();
+ size_t inlen = native_data->size();
+
+ size_t outlen;
+ if (EVP_PKEY_sign(ctx, nullptr, &outlen, in, inlen) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ unsigned long err = ERR_get_error();
+ char err_buf[256];
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
+ throw std::runtime_error("Failed to determine output length: " + std::string(err_buf));
+ }
+
+ auto out_buf = std::make_unique(outlen);
+
+ if (EVP_PKEY_sign(ctx, out_buf.get(), &outlen, in, inlen) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ unsigned long err = ERR_get_error();
+ char err_buf[256];
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
+ throw std::runtime_error("Private encryption failed: " + std::string(err_buf));
+ }
+
+ EVP_PKEY_CTX_free(ctx);
+
+ uint8_t* raw_ptr = out_buf.get();
+ return std::make_shared(out_buf.release(), outlen, [raw_ptr]() { delete[] raw_ptr; });
+}
+
+std::shared_ptr HybridRsaCipher::privateDecrypt(const std::shared_ptr& keyHandle,
+ const std::shared_ptr& data, double padding) {
+ auto keyHandleImpl = std::static_pointer_cast(keyHandle);
+ EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get();
+
+ if (!pkey) {
+ throw std::runtime_error("Invalid key for RSA private decryption");
+ }
+
+ EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
+ if (!ctx) {
+ throw std::runtime_error("Failed to create EVP_PKEY_CTX");
+ }
+
+ if (EVP_PKEY_verify_recover_init(ctx) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ unsigned long err = ERR_get_error();
+ char err_buf[256];
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
+ throw std::runtime_error("Failed to initialize verify recover: " + std::string(err_buf));
+ }
+
+ int paddingInt = static_cast(padding);
+ int opensslPadding = toOpenSSLPadding(paddingInt);
+
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, opensslPadding) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ throw std::runtime_error("Failed to set RSA padding");
+ }
+
+ auto native_data = ToNativeArrayBuffer(data);
+ const unsigned char* in = native_data->data();
+ size_t inlen = native_data->size();
+
+ size_t outlen;
+ if (EVP_PKEY_verify_recover(ctx, nullptr, &outlen, in, inlen) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ unsigned long err = ERR_get_error();
+ char err_buf[256];
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
+ throw std::runtime_error("Failed to determine output length: " + std::string(err_buf));
+ }
+
+ auto out_buf = std::make_unique(outlen);
+
+ if (EVP_PKEY_verify_recover(ctx, out_buf.get(), &outlen, in, inlen) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ unsigned long err = ERR_get_error();
+ char err_buf[256];
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
+ throw std::runtime_error("Private decryption failed: " + std::string(err_buf));
+ }
+
+ EVP_PKEY_CTX_free(ctx);
+
uint8_t* raw_ptr = out_buf.get();
return std::make_shared(out_buf.release(), outlen, [raw_ptr]() { delete[] raw_ptr; });
}
@@ -223,6 +340,8 @@ void HybridRsaCipher::loadHybridMethods() {
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("encrypt", &HybridRsaCipher::encrypt);
prototype.registerHybridMethod("decrypt", &HybridRsaCipher::decrypt);
+ prototype.registerHybridMethod("privateEncrypt", &HybridRsaCipher::privateEncrypt);
+ prototype.registerHybridMethod("privateDecrypt", &HybridRsaCipher::privateDecrypt);
});
}
diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp
index d04ae984..89795709 100644
--- a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp
+++ b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.hpp
@@ -10,13 +10,19 @@ class HybridRsaCipher : public HybridRsaCipherSpec {
HybridRsaCipher() : HybridObject(TAG) {}
std::shared_ptr encrypt(const std::shared_ptr& keyHandle,
- const std::shared_ptr& data, const std::string& hashAlgorithm,
+ const std::shared_ptr& data, double padding, const std::string& hashAlgorithm,
const std::optional>& label) override;
std::shared_ptr decrypt(const std::shared_ptr& keyHandle,
- const std::shared_ptr& data, const std::string& hashAlgorithm,
+ const std::shared_ptr& data, double padding, const std::string& hashAlgorithm,
const std::optional>& label) override;
+ std::shared_ptr privateEncrypt(const std::shared_ptr& keyHandle,
+ const std::shared_ptr& data, double padding) override;
+
+ std::shared_ptr privateDecrypt(const std::shared_ptr& keyHandle,
+ const std::shared_ptr& data, double padding) override;
+
void loadHybridMethods() override;
};
diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp
index 1ec4d242..0d002b8e 100644
--- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp
+++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp
@@ -16,6 +16,8 @@ namespace margelo::nitro::crypto {
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("encrypt", &HybridRsaCipherSpec::encrypt);
prototype.registerHybridMethod("decrypt", &HybridRsaCipherSpec::decrypt);
+ prototype.registerHybridMethod("privateEncrypt", &HybridRsaCipherSpec::privateEncrypt);
+ prototype.registerHybridMethod("privateDecrypt", &HybridRsaCipherSpec::privateDecrypt);
});
}
diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp
index 2212c668..0da1ec60 100644
--- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp
+++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp
@@ -55,8 +55,10 @@ namespace margelo::nitro::crypto {
public:
// Methods
- virtual std::shared_ptr encrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, const std::string& hashAlgorithm, const std::optional>& label) = 0;
- virtual std::shared_ptr decrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, const std::string& hashAlgorithm, const std::optional>& label) = 0;
+ virtual std::shared_ptr encrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, double padding, const std::string& hashAlgorithm, const std::optional>& label) = 0;
+ virtual std::shared_ptr decrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, double padding, const std::string& hashAlgorithm, const std::optional>& label) = 0;
+ virtual std::shared_ptr privateEncrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, double padding) = 0;
+ virtual std::shared_ptr privateDecrypt(const std::shared_ptr& keyHandle, const std::shared_ptr& data, double padding) = 0;
protected:
// Hybrid Setup
diff --git a/packages/react-native-quick-crypto/src/ec.ts b/packages/react-native-quick-crypto/src/ec.ts
index 0b3cf76c..6bf10cb0 100644
--- a/packages/react-native-quick-crypto/src/ec.ts
+++ b/packages/react-native-quick-crypto/src/ec.ts
@@ -535,13 +535,9 @@ export async function ec_generateKeyPair(
return { publicKey, privateKey };
}
-export async function ec_generateKeyPairNode(
+function ec_prepareKeyGenParams(
options: GenerateKeyPairOptions | undefined,
- encoding: KeyPairGenConfig,
-): Promise<{
- publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
- privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
-}> {
+): Ec {
if (!options) {
throw new Error('Options are required for EC key generation');
}
@@ -555,15 +551,16 @@ export async function ec_generateKeyPairNode(
throw new Error(`Invalid or unsupported named curve: ${namedCurve}`);
}
- const keyPair = await ec_generateKeyPair('ECDSA', namedCurve, true, [
- 'sign',
- 'verify',
- ]);
-
- // ec_generateKeyPair returns CryptoKey objects
- const pubCryptoKey = keyPair.publicKey as CryptoKey;
- const privCryptoKey = keyPair.privateKey as CryptoKey;
+ return new Ec(namedCurve);
+}
+function ec_formatKeyPairOutput(
+ ec: Ec,
+ encoding: KeyPairGenConfig,
+): {
+ publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
+ privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
+} {
const {
publicFormat,
publicType,
@@ -573,20 +570,34 @@ export async function ec_generateKeyPairNode(
passphrase,
} = encoding;
+ const publicKeyData = ec.native.getPublicKey();
+ const privateKeyData = ec.native.getPrivateKey();
+
+ const pub = KeyObject.createKeyObject(
+ 'public',
+ publicKeyData,
+ KFormatType.DER,
+ KeyEncoding.SPKI,
+ ) as PublicKeyObject;
+
+ const priv = KeyObject.createKeyObject(
+ 'private',
+ privateKeyData,
+ KFormatType.DER,
+ KeyEncoding.PKCS8,
+ ) as PrivateKeyObject;
+
let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
if (publicFormat === -1) {
- publicKey = pubCryptoKey.keyObject as PublicKeyObject;
+ publicKey = pub;
} else {
const format =
publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER;
const keyEncoding =
publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.SPKI;
- const exported = pubCryptoKey.keyObject.handle.exportKey(
- format,
- keyEncoding,
- );
+ const exported = pub.handle.exportKey(format, keyEncoding);
if (format === KFormatType.PEM) {
publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8');
} else {
@@ -595,7 +606,7 @@ export async function ec_generateKeyPairNode(
}
if (privateFormat === -1) {
- privateKey = privCryptoKey.keyObject as PrivateKeyObject;
+ privateKey = priv;
} else {
const format =
privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER;
@@ -605,7 +616,7 @@ export async function ec_generateKeyPairNode(
: privateType === KeyEncoding.SEC1
? KeyEncoding.SEC1
: KeyEncoding.PKCS8;
- const exported = privCryptoKey.keyObject.handle.exportKey(
+ const exported = priv.handle.exportKey(
format,
keyEncoding,
cipher,
@@ -620,3 +631,27 @@ export async function ec_generateKeyPairNode(
return { publicKey, privateKey };
}
+
+export async function ec_generateKeyPairNode(
+ options: GenerateKeyPairOptions | undefined,
+ encoding: KeyPairGenConfig,
+): Promise<{
+ publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
+ privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
+}> {
+ const ec = ec_prepareKeyGenParams(options);
+ await ec.generateKeyPair();
+ return ec_formatKeyPairOutput(ec, encoding);
+}
+
+export function ec_generateKeyPairNodeSync(
+ options: GenerateKeyPairOptions | undefined,
+ encoding: KeyPairGenConfig,
+): {
+ publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
+ privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
+} {
+ const ec = ec_prepareKeyGenParams(options);
+ ec.generateKeyPairSync();
+ return ec_formatKeyPairOutput(ec, encoding);
+}
diff --git a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts
index d886bca9..dd5c4d53 100644
--- a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts
+++ b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts
@@ -1,6 +1,6 @@
import { ed_generateKeyPair } from '../ed';
-import { rsa_generateKeyPairNode } from '../rsa';
-import { ec_generateKeyPairNode } from '../ec';
+import { rsa_generateKeyPairNode, rsa_generateKeyPairNodeSync } from '../rsa';
+import { ec_generateKeyPairNode, ec_generateKeyPairNodeSync } from '../ec';
import {
kEmptyObject,
validateFunction,
@@ -149,33 +149,41 @@ function internalGenerateKeyPair(
}
}
- const impl = async () => {
- try {
- let result;
- if (type === 'rsa' || type === 'rsa-pss') {
- result = await rsa_generateKeyPairNode(type, options, encoding);
- } else if (type === 'ec') {
- result = await ec_generateKeyPairNode(options, encoding);
- } else {
- throw new Error(`Unsupported key type: ${type}`);
+ if (isAsync) {
+ const impl = async (): Promise => {
+ try {
+ let result;
+ if (type === 'rsa' || type === 'rsa-pss') {
+ result = await rsa_generateKeyPairNode(type, options, encoding);
+ } else if (type === 'ec') {
+ result = await ec_generateKeyPairNode(options, encoding);
+ } else {
+ throw new Error(`Unsupported key type: ${type}`);
+ }
+ return [undefined, result.publicKey, result.privateKey];
+ } catch (error) {
+ return [error as Error, undefined, undefined];
}
- return [
- undefined,
- result.publicKey,
- result.privateKey,
- ] as GenerateKeyPairReturn;
- } catch (error) {
- return [error as Error, undefined, undefined] as GenerateKeyPairReturn;
- }
- };
+ };
- if (isAsync) {
impl().then(result => {
const [err, publicKey, privateKey] = result;
callback!(err, publicKey, privateKey);
});
return;
- } else {
- throw new Error('Sync key generation for RSA/EC not yet implemented');
+ }
+
+ try {
+ let result;
+ if (type === 'rsa' || type === 'rsa-pss') {
+ result = rsa_generateKeyPairNodeSync(type, options, encoding);
+ } else if (type === 'ec') {
+ result = ec_generateKeyPairNodeSync(options, encoding);
+ } else {
+ throw new Error(`Unsupported key type: ${type}`);
+ }
+ return [undefined, result.publicKey, result.privateKey];
+ } catch (error) {
+ return [error as Error, undefined, undefined];
}
}
diff --git a/packages/react-native-quick-crypto/src/keys/index.ts b/packages/react-native-quick-crypto/src/keys/index.ts
index 4d10bfe3..959620e1 100644
--- a/packages/react-native-quick-crypto/src/keys/index.ts
+++ b/packages/react-native-quick-crypto/src/keys/index.ts
@@ -8,7 +8,12 @@ import {
} from './classes';
import { generateKeyPair, generateKeyPairSync } from './generateKeyPair';
import { createSign, createVerify, Sign, Verify } from './signVerify';
-import { publicEncrypt, publicDecrypt } from './publicCipher';
+import {
+ publicEncrypt,
+ publicDecrypt,
+ privateEncrypt,
+ privateDecrypt,
+} from './publicCipher';
import {
isCryptoKey,
parseKeyEncoding,
@@ -235,6 +240,8 @@ export {
Verify,
publicEncrypt,
publicDecrypt,
+ privateEncrypt,
+ privateDecrypt,
// Node Internal API
parsePublicKeyEncoding,
diff --git a/packages/react-native-quick-crypto/src/keys/publicCipher.ts b/packages/react-native-quick-crypto/src/keys/publicCipher.ts
index 079a11a2..0caeb476 100644
--- a/packages/react-native-quick-crypto/src/keys/publicCipher.ts
+++ b/packages/react-native-quick-crypto/src/keys/publicCipher.ts
@@ -9,6 +9,7 @@ import {
} from '../utils';
import { isCryptoKey } from './utils';
import { KeyObject, CryptoKey } from './classes';
+import { constants } from '../constants';
interface PublicCipherOptions {
key: BinaryLike | KeyObject | CryptoKey;
@@ -23,6 +24,17 @@ type PublicCipherInput =
| CryptoKey
| PublicCipherOptions;
+interface PrivateCipherOptions {
+ key: BinaryLike | KeyObject | CryptoKey;
+ padding?: number;
+}
+
+type PrivateCipherInput =
+ | BinaryLike
+ | KeyObject
+ | CryptoKey
+ | PrivateCipherOptions;
+
function preparePublicCipherKey(
key: PublicCipherInput,
isEncrypt: boolean,
@@ -78,16 +90,21 @@ export function publicEncrypt(
key: PublicCipherInput,
buffer: BinaryLike,
): Buffer {
- const { keyHandle, oaepHash, oaepLabel } = preparePublicCipherKey(key, true);
+ const { keyHandle, padding, oaepHash, oaepLabel } = preparePublicCipherKey(
+ key,
+ true,
+ );
const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
const data = toAB(buffer);
+ const paddingMode = padding ?? constants.RSA_PKCS1_OAEP_PADDING;
const hashAlgorithm = oaepHash || 'SHA-256';
try {
const encrypted = rsaCipher.encrypt(
keyHandle.handle,
data,
+ paddingMode,
hashAlgorithm,
oaepLabel,
);
@@ -101,16 +118,21 @@ export function publicDecrypt(
key: PublicCipherInput,
buffer: BinaryLike,
): Buffer {
- const { keyHandle, oaepHash, oaepLabel } = preparePublicCipherKey(key, false);
+ const { keyHandle, padding, oaepHash, oaepLabel } = preparePublicCipherKey(
+ key,
+ false,
+ );
const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
const data = toAB(buffer);
+ const paddingMode = padding ?? constants.RSA_PKCS1_OAEP_PADDING;
const hashAlgorithm = oaepHash || 'SHA-256';
try {
const decrypted = rsaCipher.decrypt(
keyHandle.handle,
data,
+ paddingMode,
hashAlgorithm,
oaepLabel,
);
@@ -119,3 +141,89 @@ export function publicDecrypt(
throw new Error(`publicDecrypt failed: ${(error as Error).message}`);
}
}
+
+function preparePrivateCipherKey(
+ key: PrivateCipherInput,
+ isEncrypt: boolean,
+): {
+ keyHandle: KeyObject;
+ padding?: number;
+} {
+ let keyObj: KeyObject;
+ let padding: number | undefined;
+
+ if (key instanceof KeyObject) {
+ if (isEncrypt && key.type !== 'private') {
+ throw new Error('privateEncrypt requires a private key');
+ }
+ if (!isEncrypt && key.type !== 'public') {
+ throw new Error('privateDecrypt requires a public key');
+ }
+ keyObj = key;
+ } else if (isCryptoKey(key)) {
+ const cryptoKey = key as CryptoKey;
+ keyObj = cryptoKey.keyObject;
+ } else if (isStringOrBuffer(key)) {
+ const data = toAB(key);
+ const isPem = typeof key === 'string' && key.includes('-----BEGIN');
+ keyObj = KeyObject.createKeyObject(
+ isEncrypt ? 'private' : 'public',
+ data,
+ isPem ? KFormatType.PEM : KFormatType.DER,
+ isEncrypt ? KeyEncoding.PKCS8 : KeyEncoding.SPKI,
+ );
+ } else if (typeof key === 'object' && 'key' in key) {
+ const options = key as PrivateCipherOptions;
+ const result = preparePrivateCipherKey(options.key, isEncrypt);
+ keyObj = result.keyHandle;
+ padding = options.padding;
+ } else {
+ throw new Error('Invalid key input');
+ }
+
+ return { keyHandle: keyObj, padding };
+}
+
+export function privateEncrypt(
+ key: PrivateCipherInput,
+ buffer: BinaryLike,
+): Buffer {
+ const { keyHandle, padding } = preparePrivateCipherKey(key, true);
+
+ const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
+ const data = toAB(buffer);
+ const paddingMode = padding ?? constants.RSA_PKCS1_PADDING;
+
+ try {
+ const encrypted = rsaCipher.privateEncrypt(
+ keyHandle.handle,
+ data,
+ paddingMode,
+ );
+ return Buffer.from(encrypted);
+ } catch (error) {
+ throw new Error(`privateEncrypt failed: ${(error as Error).message}`);
+ }
+}
+
+export function privateDecrypt(
+ key: PrivateCipherInput,
+ buffer: BinaryLike,
+): Buffer {
+ const { keyHandle, padding } = preparePrivateCipherKey(key, false);
+
+ const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
+ const data = toAB(buffer);
+ const paddingMode = padding ?? constants.RSA_PKCS1_PADDING;
+
+ try {
+ const decrypted = rsaCipher.privateDecrypt(
+ keyHandle.handle,
+ data,
+ paddingMode,
+ );
+ return Buffer.from(decrypted);
+ } catch (error) {
+ throw new Error(`privateDecrypt failed: ${(error as Error).message}`);
+ }
+}
diff --git a/packages/react-native-quick-crypto/src/rsa.ts b/packages/react-native-quick-crypto/src/rsa.ts
index fbefe2ee..5d7352bd 100644
--- a/packages/react-native-quick-crypto/src/rsa.ts
+++ b/packages/react-native-quick-crypto/src/rsa.ts
@@ -179,14 +179,10 @@ export async function rsa_generateKeyPair(
return { publicKey, privateKey };
}
-export async function rsa_generateKeyPairNode(
- type: 'rsa' | 'rsa-pss',
+function rsa_prepareKeyGenParams(
+ _type: 'rsa' | 'rsa-pss',
options: GenerateKeyPairOptions | undefined,
- encoding: KeyPairGenConfig,
-): Promise<{
- publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
- privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
-}> {
+): Rsa {
if (!options) {
throw new Error('Options are required for RSA key generation');
}
@@ -212,25 +208,18 @@ export async function rsa_generateKeyPairNode(
pubExp & 0xff,
]);
- const algorithmName = type === 'rsa-pss' ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5';
+ const hashName = typeof hash === 'string' ? hash : hash;
- const algorithm: RsaHashedKeyGenParams = {
- name: algorithmName,
- modulusLength,
- publicExponent: pubExpBytes,
- hash: typeof hash === 'string' ? hash : hash,
- };
-
- const keyPair = await rsa_generateKeyPair(
- algorithm as SubtleAlgorithm,
- true,
- ['sign', 'verify'],
- );
-
- // rsa_generateKeyPair returns CryptoKey objects
- const pubCryptoKey = keyPair.publicKey as CryptoKey;
- const privCryptoKey = keyPair.privateKey as CryptoKey;
+ return new Rsa(modulusLength, pubExpBytes, hashName);
+}
+function rsa_formatKeyPairOutput(
+ rsa: Rsa,
+ encoding: KeyPairGenConfig,
+): {
+ publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
+ privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
+} {
const {
publicFormat,
publicType,
@@ -240,20 +229,30 @@ export async function rsa_generateKeyPairNode(
passphrase,
} = encoding;
+ const publicKeyData = rsa.native.getPublicKey();
+ const privateKeyData = rsa.native.getPrivateKey();
+
+ const pub = KeyObject.createKeyObject(
+ 'public',
+ publicKeyData,
+ ) as PublicKeyObject;
+
+ const priv = KeyObject.createKeyObject(
+ 'private',
+ privateKeyData,
+ ) as PrivateKeyObject;
+
let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
if (publicFormat === -1) {
- publicKey = pubCryptoKey.keyObject as PublicKeyObject;
+ publicKey = pub;
} else {
const format =
publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER;
const keyEncoding =
publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.PKCS1;
- const exported = pubCryptoKey.keyObject.handle.exportKey(
- format,
- keyEncoding,
- );
+ const exported = pub.handle.exportKey(format, keyEncoding);
if (format === KFormatType.PEM) {
publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8');
} else {
@@ -262,13 +261,13 @@ export async function rsa_generateKeyPairNode(
}
if (privateFormat === -1) {
- privateKey = privCryptoKey.keyObject as PrivateKeyObject;
+ privateKey = priv;
} else {
const format =
privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER;
const keyEncoding =
privateType === KeyEncoding.PKCS8 ? KeyEncoding.PKCS8 : KeyEncoding.PKCS1;
- const exported = privCryptoKey.keyObject.handle.exportKey(
+ const exported = priv.handle.exportKey(
format,
keyEncoding,
cipher,
@@ -283,3 +282,29 @@ export async function rsa_generateKeyPairNode(
return { publicKey, privateKey };
}
+
+export async function rsa_generateKeyPairNode(
+ type: 'rsa' | 'rsa-pss',
+ options: GenerateKeyPairOptions | undefined,
+ encoding: KeyPairGenConfig,
+): Promise<{
+ publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
+ privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
+}> {
+ const rsa = rsa_prepareKeyGenParams(type, options);
+ await rsa.generateKeyPair();
+ return rsa_formatKeyPairOutput(rsa, encoding);
+}
+
+export function rsa_generateKeyPairNodeSync(
+ type: 'rsa' | 'rsa-pss',
+ options: GenerateKeyPairOptions | undefined,
+ encoding: KeyPairGenConfig,
+): {
+ publicKey: PublicKeyObject | Buffer | string | ArrayBuffer;
+ privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer;
+} {
+ const rsa = rsa_prepareKeyGenParams(type, options);
+ rsa.generateKeyPairSync();
+ return rsa_formatKeyPairOutput(rsa, encoding);
+}
diff --git a/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts b/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts
index 39f0ccde..643df09a 100644
--- a/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts
+++ b/packages/react-native-quick-crypto/src/specs/rsaCipher.nitro.ts
@@ -4,32 +4,62 @@ import type { KeyObjectHandle } from './keyObjectHandle.nitro';
export interface RsaCipher
extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
/**
- * Encrypt data using RSA-OAEP
+ * Encrypt data using RSA with specified padding
* @param keyHandle The public key handle
* @param data The data to encrypt
- * @param hashAlgorithm The hash algorithm (e.g., 'SHA-256')
+ * @param padding RSA padding mode (1=PKCS1, 4=OAEP)
+ * @param hashAlgorithm The hash algorithm for OAEP (e.g., 'SHA-256')
* @param label Optional label for OAEP
* @returns Encrypted data
*/
encrypt(
keyHandle: KeyObjectHandle,
data: ArrayBuffer,
+ padding: number,
hashAlgorithm: string,
label?: ArrayBuffer,
): ArrayBuffer;
/**
- * Decrypt data using RSA-OAEP
+ * Decrypt data using RSA with specified padding
* @param keyHandle The private key handle
* @param data The data to decrypt
- * @param hashAlgorithm The hash algorithm (e.g., 'SHA-256')
+ * @param padding RSA padding mode (1=PKCS1, 4=OAEP)
+ * @param hashAlgorithm The hash algorithm for OAEP (e.g., 'SHA-256')
* @param label Optional label for OAEP
* @returns Decrypted data
*/
decrypt(
keyHandle: KeyObjectHandle,
data: ArrayBuffer,
+ padding: number,
hashAlgorithm: string,
label?: ArrayBuffer,
): ArrayBuffer;
+
+ /**
+ * Encrypt data using private key (for signatures)
+ * @param keyHandle The private key handle
+ * @param data The data to encrypt
+ * @param padding RSA padding mode (1=PKCS1)
+ * @returns Encrypted data
+ */
+ privateEncrypt(
+ keyHandle: KeyObjectHandle,
+ data: ArrayBuffer,
+ padding: number,
+ ): ArrayBuffer;
+
+ /**
+ * Decrypt data using public key (for signature verification)
+ * @param keyHandle The public key handle
+ * @param data The data to decrypt
+ * @param padding RSA padding mode (1=PKCS1)
+ * @returns Decrypted data
+ */
+ privateDecrypt(
+ keyHandle: KeyObjectHandle,
+ data: ArrayBuffer,
+ padding: number,
+ ): ArrayBuffer;
}
diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts
index a244f957..b8d576d1 100644
--- a/packages/react-native-quick-crypto/src/subtle.ts
+++ b/packages/react-native-quick-crypto/src/subtle.ts
@@ -146,11 +146,15 @@ async function rsaCipher(
const rsaCipherModule =
NitroModules.createHybridObject('RsaCipher');
+ // RSA-OAEP padding constant = 4
+ const RSA_PKCS1_OAEP_PADDING = 4;
+
if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
// Encrypt with public key
return rsaCipherModule.encrypt(
key.keyObject.handle,
data,
+ RSA_PKCS1_OAEP_PADDING,
hashAlgorithm,
label,
);
@@ -159,6 +163,7 @@ async function rsaCipher(
return rsaCipherModule.decrypt(
key.keyObject.handle,
data,
+ RSA_PKCS1_OAEP_PADDING,
hashAlgorithm,
label,
);
diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts
index 51385c06..c74cc1c8 100644
--- a/packages/react-native-quick-crypto/src/utils/types.ts
+++ b/packages/react-native-quick-crypto/src/utils/types.ts
@@ -327,6 +327,7 @@ export type GenerateKeyPairOptions = {
export type KeyPairKey =
| ArrayBuffer
+ | Buffer
| string
| KeyObject
| KeyObjectHandle