diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..cee114e1 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,8 @@ +{ + "agent": { + "default_model": { + "provider": "anthropic", + "model": "claude-opus-4-5-20251101" + } + } +} diff --git a/docs/VERSION-1.0.0.md b/docs/VERSION-1.0.0.md new file mode 100644 index 00000000..ddee3165 --- /dev/null +++ b/docs/VERSION-1.0.0.md @@ -0,0 +1,75 @@ +# 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/VERSION_1.0.0_TODO.md b/docs/VERSION_1.0.0_TODO.md deleted file mode 100644 index e24c9173..00000000 --- a/docs/VERSION_1.0.0_TODO.md +++ /dev/null @@ -1,337 +0,0 @@ -# Version 1.0.0 TODO - Parity with 0.x - -This document tracks what needs to be implemented in the `main` branch to achieve full parity with the `0.x` branch before releasing version 1.0.0. - -**Last Updated:** 2025-11-29 - -## Status Legend - -- ✅ **Implemented** - Feature is complete (both TS and C++/Nitro) -- 🚧 **Partial** - Feature exists but incomplete or commented out -- ❌ **Missing** - Feature needs implementation from scratch -- 📝 **Docs Only** - Feature exists but documentation needs updating - ---- - -## Critical Findings Summary - -### Documentation Issues (No Code Needed) - -These are already implemented but incorrectly marked in `docs/implementation-coverage.md`: - -1. 📝 **`keyObject.type`** - - Status: ✅ Implemented in `src/keys/classes.ts:111` - - Action: Update docs from ❌ to ✅ - -2. 📝 **`crypto.createSecretKey`** - - Status: ✅ Fully implemented (TS + C++/Nitro) - - TypeScript: `src/keys/index.ts:20-23` - - C++ Native: `cpp/keys/HybridKeyObjectHandle.cpp:352-354` - - Action: Update docs from ❌ to ✅ - ---- - -## Missing Features (Implementation Required) - -### 1. Sign/Verify Classes ❌ - -**Node.js Classic API** - -**What's Missing:** -- `crypto.createSign(algorithm[, options])` -- `crypto.createVerify(algorithm[, options])` -- `Sign` class with methods: - - `sign.update(data[, inputEncoding])` - - `sign.sign(privateKey[, outputEncoding])` -- `Verify` class with methods: - - `verify.update(data[, inputEncoding])` - - `verify.verify(object, signature[, signatureEncoding])` - -**Evidence:** -- Code commented out in `src/keys/index.ts:7` -- Classes not found in codebase - -**Reference:** -- 0.x had this: ✅ -- Node.js reference: `$REPOS/node/deps/ncrypto` - -**Implementation Notes:** -- WebCrypto equivalents (`subtle.sign`/`subtle.verify`) are partially implemented -- Classic Node API provides streaming interface vs WebCrypto's Promise-based -- Need C++ integration with OpenSSL EVP_DigestSign/EVP_DigestVerify - -**Priority:** HIGH - Common API in Node.js crypto - ---- - -### 2. createPrivateKey / createPublicKey ❌ - -**Node.js Classic API** - -**What's Missing:** -- `crypto.createPrivateKey(key)` -- `crypto.createPublicKey(key)` - -**Evidence:** -- Both commented out in `src/keys/index.ts:26-27` -- `createSecretKey` is implemented, so pattern exists - -**Reference:** -- 0.x had this: ✅ -- Node.js reference: `$REPOS/node/deps/ncrypto` - -**Implementation Notes:** -- C++ infrastructure exists in `KeyObjectHandle::init()` for public/private keys -- TypeScript wrapper needed similar to `createSecretKey` pattern -- Should support multiple input formats (PEM, DER, JWK, KeyObject) - -**Priority:** HIGH - Fundamental key management API - ---- - -### 3. crypto.constants ❌ - -**Node.js Classic API** - -**What's Missing:** -- `crypto.constants` object with OpenSSL constants - -**Evidence:** -- No `constants` export found in `src/` directory -- Grep for `export.*constants` returned no results - -**Reference:** -- 0.x had this: ✅ -- Node.js reference: `$REPOS/node/lib/crypto.js` - -**Implementation Notes:** -- Should include constants for: - - RSA padding modes (RSA_PKCS1_PADDING, RSA_PKCS1_OAEP_PADDING, etc.) - - Point conversion forms - - OpenSSL engine constants - - Default DH groups -- Most are mappings to OpenSSL constants from `openssl/rsa.h`, etc. - -**Priority:** MEDIUM - Required for advanced crypto operations - ---- - -### 4. publicEncrypt / publicDecrypt ❌ - -**Node.js Classic API** - -**What's Missing:** -- `crypto.publicEncrypt(key, buffer)` -- `crypto.publicDecrypt(key, buffer)` -- Note: `privateEncrypt`/`privateDecrypt` also missing (0.x didn't have these either) - -**Evidence:** -- Grep returned no matches in `src/` -- 0.x had `publicEncrypt`/`publicDecrypt`: ✅ - -**Reference:** -- 0.x implementation -- Node.js reference: `$REPOS/node/deps/ncrypto` - -**Implementation Notes:** -- C++ implementation would use `EVP_PKEY_encrypt`/`EVP_PKEY_decrypt` -- WebCrypto equivalent (`subtle.encrypt` with RSA-OAEP) is implemented -- Classic API provides more control over padding modes -- Should support various key formats and padding options - -**Priority:** MEDIUM - Less common than Sign/Verify but still used - ---- - -### 5. generateKeyPair for RSA/EC ❌ - -**Node.js Classic API** - -**What's Missing:** -- `crypto.generateKeyPair('rsa', options, callback)` -- `crypto.generateKeyPair('rsa-pss', options, callback)` -- `crypto.generateKeyPair('ec', options, callback)` -- `crypto.generateKeyPairSync('rsa', options)` -- `crypto.generateKeyPairSync('rsa-pss', options)` -- `crypto.generateKeyPairSync('ec', options)` - -**Evidence:** -- `src/keys/generateKeyPair.ts:123-138` only supports: `ed25519`, `ed448`, `x25519`, `x448` -- Switch statement falls through to error for all other types - -**Reference:** -- 0.x had RSA and EC: ✅ -- Node.js reference: `$REPOS/node/deps/ncrypto` - -**Implementation Notes:** -- WebCrypto `subtle.generateKey` for RSA/EC/ECDH is implemented -- C++ helpers exist: `rsa_generateKeyPair()` in `src/rsa.ts:59`, `ec_generateKeyPair()` in `src/ec.ts:446` -- Need to wire these into classic `generateKeyPair` API -- Key encoding/format conversion already exists in `parseKeyPairEncoding()` - -**Priority:** HIGH - Common key generation patterns - ---- - -### 6. generateKey / generateKeySync for AES ❌ - -**Node.js Classic API** - -**What's Missing:** -- `crypto.generateKey('aes', { length: 128|192|256 }, callback)` -- `crypto.generateKeySync('aes', { length: 128|192|256 })` -- `crypto.generateKey('hmac', options, callback)` (both branches missing) -- `crypto.generateKeySync('hmac', options)` (both branches missing) - -**Evidence:** -- No exports for `generateKey` or `generateKeySync` found -- Grepped for `export.*function generateKey[^P]` - no results - -**Reference:** -- 0.x had AES: ✅ (HMAC was missing in both) -- Node.js reference: `$REPOS/node/deps/ncrypto` - -**Implementation Notes:** -- WebCrypto `subtle.generateKey` for AES is implemented (`aesGenerateKey` exists) -- Need classic Node API wrapper that calls crypto.randomBytes -- Return should be `KeyObject` (SecretKeyObject) not `CryptoKey` -- Simpler than asymmetric key generation - -**Priority:** MEDIUM - AES key generation often done manually with randomBytes - ---- - -### 7. subtle.generateKey for Ed25519 ❌ - -**WebCrypto API** - -**What's Missing:** -- `subtle.generateKey({ name: 'Ed25519' }, extractable, keyUsages)` - -**Evidence:** -- `src/subtle.ts:967-1013` shows `generateKey()` implementation -- Switch statement handles: RSA variants, ECDSA, ECDH, AES variants -- Ed25519 not in the list (falls through to error) - -**Reference:** -- 0.x had this: ✅ -- WebCrypto spec: Ed25519 is standardized -- Node.js reference: `$REPOS/node/deps/ncrypto` - -**Implementation Notes:** -- Classic `generateKeyPair('ed25519')` IS implemented in `src/ed.ts:171` -- Need to expose this through WebCrypto `subtle.generateKey` interface -- Should return `CryptoKeyPair` with Ed25519 algorithm -- Ed448 also missing (and also supported in classic API) - -**Priority:** MEDIUM - Modern signing algorithm, WebCrypto standard - ---- - -## Implementation Priority Order - -### Phase 1: High Priority (Core APIs) -1. ✅ **createPrivateKey / createPublicKey** - Fundamental for key management -2. ✅ **Sign/Verify classes** - Common Node.js pattern -3. ✅ **generateKeyPair for RSA/EC** - Common key generation - -### Phase 2: Medium Priority (Completeness) -4. ✅ **publicEncrypt / publicDecrypt** - RSA encryption operations -5. ✅ **crypto.constants** - Required for advanced usage -6. ✅ **subtle.generateKey for Ed25519/Ed448** - Modern WebCrypto standard -7. ✅ **generateKey/generateKeySync for AES** - Symmetric key generation - -### Phase 3: Documentation -8. 📝 Update `docs/implementation-coverage.md` with correct status - ---- - -## Testing Checklist - -For each feature implemented, ensure: - -- [ ] TypeScript implementation with proper types -- [ ] C++/Nitro native implementation if needed -- [ ] Test vectors from NIST/RFC/Node.js -- [ ] WebCrypto compliance (for subtle.* APIs) -- [ ] Node.js API compatibility (for crypto.* APIs) -- [ ] Memory safety (RAII, smart pointers, no leaks) -- [ ] Security properties (constant-time where needed, secure RNG) -- [ ] Error handling (proper OpenSSL error propagation) -- [ ] Example app tests pass -- [ ] Documentation updated - ---- - -## Architecture Reference - -### API Priority Order (from CLAUDE.md) -When implementing, prefer in this order: -1. **WebCrypto API** - Modern standard, best for `subtle.*` methods -2. **Node.js Implementation** - Use `$REPOS/node/deps/ncrypto` as reference -3. **RNQC 0.x** - Legacy reference at `$REPOS/rnqc/0.x` (OpenSSL 1.1.1, deprecated patterns) - -### Tech Stack -- **TypeScript** (strict, no `any`) -- **C++20+** (smart pointers, RAII) -- **OpenSSL 3.3+** (EVP APIs only, no deprecated) -- **Nitro Modules** (native bridging) - -### Code Philosophy -- Minimize code rather than add more -- Prefer iteration and modularization over duplication -- No comments unless code is sufficiently complex -- Code should be self-documenting - ---- - -## How to Use This Document - -### For Implementation -1. Pick a feature from the priority order -2. Check the "What's Missing" and "Evidence" sections -3. Review "Reference" for where to find implementation details -4. Follow "Implementation Notes" for architecture guidance -5. Run through "Testing Checklist" before marking complete - -### For Progress Tracking -- Update status emoji when work begins (❌ → 🚧) -- Mark complete when tests pass (🚧 → ✅) -- Add notes/blockers in the feature section -- Update "Last Updated" date at top - -### For Release Planning -- Count remaining ❌ and 🚧 items -- Estimate based on similar completed features -- Block 1.0.0 release until all ❌ → ✅ - ---- - -## Notes - -### createSecretKey Discovery -The `createSecretKey` function is **fully implemented**: -- TypeScript wrapper: `src/keys/index.ts:20-23` -- C++ native support: `cpp/keys/HybridKeyObjectHandle.cpp:352-354` -- Uses `KeyObjectData::CreateSecret(ab)` which handles the ArrayBuffer -- This serves as the template for implementing `createPrivateKey`/`createPublicKey` - -### Sign/Verify Pattern -While Sign/Verify classes are missing, there are existing patterns: -- WebCrypto `subtle.sign`/`subtle.verify` are partially implemented -- C++ signing infrastructure exists via `ecdsaSignVerify` and similar -- Need to create streaming/updateable interface vs Promise-based - -### KeyObject Infrastructure -The C++ `KeyObjectHandle` class is robust and handles: -- Secret keys (symmetric) -- Public keys (asymmetric) -- Private keys (asymmetric) -- Multiple formats: raw, DER, PEM, JWK -- Special curves: X25519, X448, Ed25519, Ed448, EC - -This means most missing features are TypeScript wrappers around existing C++ functionality. - ---- - -**For questions or updates to this document, reference the conversation that generated it or update directly based on implementation progress.** diff --git a/docs/implementation-coverage.md b/docs/implementation-coverage.md index 4315367c..fa01f9df 100644 --- a/docs/implementation-coverage.md +++ b/docs/implementation-coverage.md @@ -60,12 +60,12 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ `keyObject.symmetricKeySize` * ❌ `keyObject.toCryptoKey(algorithm, extractable, keyUsages)` * ✅ `keyObject.type` -* ❌ Class: `Sign` - * ❌ `sign.sign(privateKey[, outputEncoding])` - * ❌ `sign.update(data[, inputEncoding])` -* ❌ Class: `Verify` - * ❌ `verify.update(data[, inputEncoding])` - * ❌ `verify.verify(object, signature[, signatureEncoding])` +* ✅ Class: `Sign` + * ✅ `sign.sign(privateKey[, outputEncoding])` + * ✅ `sign.update(data[, inputEncoding])` +* ✅ Class: `Verify` + * ✅ `verify.update(data[, inputEncoding])` + * ✅ `verify.verify(object, signature[, signatureEncoding])` * ❌ Class: `X509Certificate` * ❌ `new X509Certificate(buffer)` * ❌ `x509.ca` @@ -97,7 +97,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ `crypto.argon2Sync(algorithm, parameters)` * ❌ `crypto.checkPrime(candidate[, options], callback)` * ❌ `crypto.checkPrimeSync(candidate[, options])` - * ❌ `crypto.constants` + * ✅ `crypto.constants` * ✅ `crypto.createCipheriv(algorithm, key, iv[, options])` * ✅ `crypto.createDecipheriv(algorithm, key, iv[, options])` * ❌ `crypto.createDiffieHellman(prime[, primeEncoding][, generator][, generatorEncoding])` @@ -106,19 +106,19 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ❌ `crypto.createECDH(curveName)` * ✅ `crypto.createHash(algorithm[, options])` * ✅ `crypto.createHmac(algorithm, key[, options])` - * ❌ `crypto.createPrivateKey(key)` - * ❌ `crypto.createPublicKey(key)` - * ❌ `crypto.createSecretKey(key[, encoding])` - * ❌ `crypto.createSign(algorithm[, options])` - * ❌ `crypto.createVerify(algorithm[, options])` + * ✅ `crypto.createPrivateKey(key)` + * ✅ `crypto.createPublicKey(key)` + * ✅ `crypto.createSecretKey(key[, encoding])` + * ✅ `crypto.createSign(algorithm[, options])` + * ✅ `crypto.createVerify(algorithm[, options])` * ❌ `crypto.decapsulate(key, ciphertext[, callback])` * ❌ `crypto.diffieHellman(options[, callback])` * ❌ `crypto.encapsulate(key[, callback])` * ❌ `crypto.fips` deprecated - * ❌ `crypto.generateKey(type, options, callback)` + * ✅ `crypto.generateKey(type, options, callback)` * 🚧 `crypto.generateKeyPair(type, options, callback)` * 🚧 `crypto.generateKeyPairSync(type, options)` - * ❌ `crypto.generateKeySync(type, options)` + * 🚧 `crypto.generateKeySync(type, options)` * ❌ `crypto.generatePrime(size[, options[, callback]])` * ❌ `crypto.generatePrimeSync(size[, options])` * ❌ `crypto.getCipherInfo(nameOrNid[, options])` @@ -135,8 +135,8 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * ✅ `crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)` * ❌ `crypto.privateDecrypt(privateKey, buffer)` * ❌ `crypto.privateEncrypt(privateKey, buffer)` - * ❌ `crypto.publicDecrypt(key, buffer)` - * ❌ `crypto.publicEncrypt(key, buffer)` + * ✅ `crypto.publicDecrypt(key, buffer)` + * ✅ `crypto.publicEncrypt(key, buffer)` * ✅ `crypto.randomBytes(size[, callback])` * ✅ `crypto.randomFill(buffer[, offset][, size], callback)` * ✅ `crypto.randomFillSync(buffer[, offset][, size])` @@ -166,16 +166,16 @@ This document attempts to describe the implementation status of Crypto APIs/Inte ## `crypto.generateKey` | type | Status | | --------- | :----: | -| `aes` | ❌ | -| `hmac` | ❌ | +| `aes` | ✅ | +| `hmac` | ✅ | ## `crypto.generateKeyPair` | type | Status | | --------- | :----: | -| `rsa` | ❌ | -| `rsa-pss` | ❌ | +| `rsa` | ✅ | +| `rsa-pss` | ✅ | | `dsa` | ❌ | -| `ec` | ❌ | +| `ec` | ✅ | | `ed25519` | ✅ | | `ed448` | ✅ | | `x25519` | ✅ | @@ -198,8 +198,8 @@ This document attempts to describe the implementation status of Crypto APIs/Inte ## `crypto.generateKeySync` | type | Status | | --------- | :----: | -| `aes` | ❌ | -| `hmac` | ❌ | +| `aes` | ✅ | +| `hmac` | ✅ | ## `crypto.sign` | Algorithm | Status | @@ -344,8 +344,8 @@ This document attempts to describe the implementation status of Crypto APIs/Inte | --------- | :----: | | `ECDH` | ✅ | | `ECDSA` | ✅ | -| `Ed25519` | ❌ | -| `Ed448` | ❌ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | | `ML-DSA-44` | ❌ | | `ML-DSA-65` | ❌ | | `ML-DSA-87` | ❌ | diff --git a/example/package.json b/example/package.json index 619f51f0..04fe0342 100644 --- a/example/package.json +++ b/example/package.json @@ -38,7 +38,7 @@ "react-native-bouncy-checkbox": "4.1.2", "react-native-nitro-modules": "0.29.1", "react-native-quick-base64": "2.2.2", - "react-native-quick-crypto": "1.0.0-beta.23", + "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "^5.2.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 faf17dd8..56c3264b 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -8,8 +8,14 @@ import '../tests/cipher/chacha_tests'; import '../tests/cipher/xsalsa20_tests'; import '../tests/cfrg/ed25519_tests'; import '../tests/cfrg/x25519_tests'; +import '../tests/constants/constants_tests'; import '../tests/hash/hash_tests'; import '../tests/hmac/hmac_tests'; +import '../tests/keys/sign_verify_streaming'; +import '../tests/keys/public_cipher'; +import '../tests/keys/create_keys'; +import '../tests/keys/generate_key'; +import '../tests/keys/generate_keypair'; import '../tests/pbkdf2/pbkdf2_tests'; import '../tests/random/random_tests'; import '../tests/subtle/deriveBits'; diff --git a/example/src/tests/constants/constants_tests.ts b/example/src/tests/constants/constants_tests.ts new file mode 100644 index 00000000..20b73c8b --- /dev/null +++ b/example/src/tests/constants/constants_tests.ts @@ -0,0 +1,122 @@ +import { constants } from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test } from '../util'; + +const SUITE = 'constants'; + +// --- RSA Padding Constants --- + +test(SUITE, 'RSA_PKCS1_PADDING exists and is a number', () => { + expect(typeof constants.RSA_PKCS1_PADDING).to.equal('number'); + expect(constants.RSA_PKCS1_PADDING).to.equal(1); +}); + +test(SUITE, 'RSA_PKCS1_OAEP_PADDING exists and is a number', () => { + expect(typeof constants.RSA_PKCS1_OAEP_PADDING).to.equal('number'); + expect(constants.RSA_PKCS1_OAEP_PADDING).to.equal(4); +}); + +test(SUITE, 'RSA_NO_PADDING exists and is a number', () => { + expect(typeof constants.RSA_NO_PADDING).to.equal('number'); + expect(constants.RSA_NO_PADDING).to.equal(3); +}); + +test(SUITE, 'RSA_PKCS1_PSS_PADDING exists and is a number', () => { + expect(typeof constants.RSA_PKCS1_PSS_PADDING).to.equal('number'); + expect(constants.RSA_PKCS1_PSS_PADDING).to.equal(6); +}); + +// --- RSA PSS Salt Length Constants --- + +test(SUITE, 'RSA_PSS_SALTLEN_DIGEST exists and is a number', () => { + expect(typeof constants.RSA_PSS_SALTLEN_DIGEST).to.equal('number'); + expect(constants.RSA_PSS_SALTLEN_DIGEST).to.equal(-1); +}); + +test(SUITE, 'RSA_PSS_SALTLEN_MAX_SIGN exists and is a number', () => { + expect(typeof constants.RSA_PSS_SALTLEN_MAX_SIGN).to.equal('number'); + expect(constants.RSA_PSS_SALTLEN_MAX_SIGN).to.equal(-2); +}); + +test(SUITE, 'RSA_PSS_SALTLEN_AUTO exists and is a number', () => { + expect(typeof constants.RSA_PSS_SALTLEN_AUTO).to.equal('number'); + expect(constants.RSA_PSS_SALTLEN_AUTO).to.equal(-2); +}); + +// --- Point Conversion Form Constants --- + +test(SUITE, 'POINT_CONVERSION_COMPRESSED exists', () => { + expect(typeof constants.POINT_CONVERSION_COMPRESSED).to.equal('number'); + expect(constants.POINT_CONVERSION_COMPRESSED).to.equal(2); +}); + +test(SUITE, 'POINT_CONVERSION_UNCOMPRESSED exists', () => { + expect(typeof constants.POINT_CONVERSION_UNCOMPRESSED).to.equal('number'); + expect(constants.POINT_CONVERSION_UNCOMPRESSED).to.equal(4); +}); + +test(SUITE, 'POINT_CONVERSION_HYBRID exists', () => { + expect(typeof constants.POINT_CONVERSION_HYBRID).to.equal('number'); + expect(constants.POINT_CONVERSION_HYBRID).to.equal(6); +}); + +// --- DH Constants --- + +test(SUITE, 'DH_CHECK_P_NOT_PRIME exists', () => { + expect(typeof constants.DH_CHECK_P_NOT_PRIME).to.equal('number'); +}); + +test(SUITE, 'DH_CHECK_P_NOT_SAFE_PRIME exists', () => { + expect(typeof constants.DH_CHECK_P_NOT_SAFE_PRIME).to.equal('number'); +}); + +test(SUITE, 'DH_NOT_SUITABLE_GENERATOR exists', () => { + expect(typeof constants.DH_NOT_SUITABLE_GENERATOR).to.equal('number'); +}); + +test(SUITE, 'DH_UNABLE_TO_CHECK_GENERATOR exists', () => { + expect(typeof constants.DH_UNABLE_TO_CHECK_GENERATOR).to.equal('number'); +}); + +// --- Cipher Constants --- + +test(SUITE, 'OPENSSL_VERSION_NUMBER exists', () => { + expect(typeof constants.OPENSSL_VERSION_NUMBER).to.equal('number'); + expect(constants.OPENSSL_VERSION_NUMBER).to.be.greaterThan(0); +}); + +// --- All Constants Are Numbers --- + +test(SUITE, 'All exported constants are numbers', () => { + const allKeys = Object.keys(constants); + expect(allKeys.length).to.be.greaterThan(0); + + for (const key of allKeys) { + const value = constants[key as keyof typeof constants]; + expect(typeof value).to.equal('number'); + } +}); + +// --- Node.js Compatibility --- + +test(SUITE, 'RSA padding constants match Node.js values', () => { + // These values are defined by OpenSSL and should match Node.js + expect(constants.RSA_PKCS1_PADDING).to.equal(1); + expect(constants.RSA_NO_PADDING).to.equal(3); + expect(constants.RSA_PKCS1_OAEP_PADDING).to.equal(4); + expect(constants.RSA_PKCS1_PSS_PADDING).to.equal(6); +}); + +test(SUITE, 'RSA PSS salt length constants match Node.js values', () => { + // These values are defined by OpenSSL and should match Node.js + expect(constants.RSA_PSS_SALTLEN_DIGEST).to.equal(-1); + expect(constants.RSA_PSS_SALTLEN_MAX_SIGN).to.equal(-2); + expect(constants.RSA_PSS_SALTLEN_AUTO).to.equal(-2); +}); + +test(SUITE, 'Point conversion constants match Node.js values', () => { + // These values are defined by OpenSSL EC_POINT conversion forms + expect(constants.POINT_CONVERSION_COMPRESSED).to.equal(2); + expect(constants.POINT_CONVERSION_UNCOMPRESSED).to.equal(4); + expect(constants.POINT_CONVERSION_HYBRID).to.equal(6); +}); diff --git a/example/src/tests/keys/create_keys.ts b/example/src/tests/keys/create_keys.ts new file mode 100644 index 00000000..ac6a827c --- /dev/null +++ b/example/src/tests/keys/create_keys.ts @@ -0,0 +1,413 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { + createSecretKey, + createPrivateKey, + createPublicKey, + generateKeyPair, + randomBytes, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test, assertThrowsAsync, decodeHex } from '../util'; +import { rsaPrivateKeyPem, rsaPublicKeyPem } from './fixtures'; + +const SUITE = 'keys.createKey'; + +// RSA 2048-bit test keys in DER format +// Source: Node.js test fixtures / WebCrypto test vectors +const pkcs8Der = decodeHex( + '308204bf020100300d06092a864886f70d0101010500048204a930820' + + '4a50201000282010100d3576092e62957364544e7e4233b7bdb293db2' + + '085122c479328546f9f0f712f657c4b17868c930908cc594f7ed00c01' + + '442c1af04c2f678a48ba2c80fd1713e30b5ac50787ac3516589f17196' + + '7f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d716' + + '6c08ab6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7' + + 'e1032144ea949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f1346' + + '63d6f656f5ceabc725da8c02aabeaaa13ac36a75cc0bae135df3114b6' + + '6589c7ed3cb61559ae5a384f162bfa80dbe4617f86c3f1d010c94fe2c' + + '9bf019a6e63b3efc028d43cee611c85ec263c906c463772c6911b19ee' + + 'c096ca76ec5e31e1e3020301000102820101008b375ccb87c825c5ff3' + + 'd53d009916e9641057e18527227a07ab226be1088813a3b38bb7b48f3' + + '77055165fa2a9339d24dc667d5c5ba3427e6a481176eac15ffd490683' + + '11e1c283b9f3a8e0cb809b4630c50aa8f3e45a60b359e19bf8cbb5eca' + + 'd64e761f1095743ff36aaf5cf0ecb97fedaddda60b5bf35d811a75b82' + + '2230cfaa0192fad40547e275448aa3316bf8e2b4ce0854fc7708b537b' + + 'a22d13210b09aec37a2759efc082a1531b23a91730037dde4ef26b5f9' + + '6efdcc39fd34c345ad51cbbe44fe58b8a3b4ec997866c086dff1b8831' + + 'ef0a1fea263cf7dacd03c04cbcc2b279e57fa5b953996bfb1dd68817a' + + 'f7fb42cdef7a5294a57fac2b8ad739f1b029902818100fbf833c2c631' + + 'c970240c8e7485f06a3ea2a84822511a8627dd464ef8afaf7148d1a42' + + '5b6b8657ddd5246832b8e533020c5bbb568855a6aec3e4221d793f1dc' + + '5b2f2584e2415e48e9a2bd292b134031f99c8eb42fc0bcd0449bf22ce' + + '6dec97014efe5ac93ebe835877656252cbbb16c415b67b184d2284568' + + 'a277d59335585cfd02818100d6b8ce27c7295d5d16fc3570ed64c8da9' + + '303fad29488c1a65e9ad711f90370187dbbfd81316d69648bc88cc5c8' + + '3551afff45debacfb61105f709e4c30809b90031ebd686244496c6f69' + + 'e692ebdc814f64239f4ad15756ecb78c5a5b09931db183077c546a38c' + + '4c743889ad3d3ed079b5622ed0120fa0e1f93b593db7d852e05f02818' + + '038874b9d83f78178ce2d9efc175c83897fd67f306bbfa69f64ee3423' + + '68ced47c80c3f1ce177a758d64bafb0c9786a44285fa01cdec3507cde' + + 'e7dc9b7e2b21d3cbbcc100eee9967843b057329fdcca62998ed0f11b3' + + '8ce8b0abc7de39017c71cfd0ae57546c559144cdd0afd0645f7ea8ff0' + + '7b974d1ed44fd1f8e00f560bf6d45028181008529ef9073cf8f7b5ff9' + + 'e21abadf3a4173d3900670dfaf59426abcdf0493c13d2f1d1b46b824a' + + '6ac1894b3d925250c181e3472c16078056eb19a8d28f71f3080927534' + + '81d49444fdf78c9ea6c24407dc018e77d3afef385b2ff7439e9623794' + + '1332dd446cebeffdb4404fe4f71595161d016402c334d0f57c61abe4f' + + 'f9f4cbf90281810087d87708d46763e4ccbeb2d1e9712e5bf0216d70d' + + 'e9420a5b2069b7459b99f5d9f7f2fad7cd79aaee67a7f9a34437e3c79' + + 'a84af0cd8de9dff268eb0c4793f501f988d540f6d3475c2079b8227a2' + + '3d968dec4e3c66503187193459630472bfdb6ba1de786c797fa6f4ea6' + + '5a2a8419262f29678856cb73c9bd4bc89b5e041b2277', +); + +const spkiDer = decodeHex( + '30820122300d06092a864886f70d01010105000382010f003082010a0' + + '282010100d3576092e62957364544e7e4233b7bdb293db2085122c479' + + '328546f9f0f712f657c4b17868c930908cc594f7ed00c01442c1af04c' + + '2f678a48ba2c80fd1713e30b5ac50787ac3516589f171967f6386ada3' + + '4900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d7166c08ab6ce2' + + '04640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e1032144ea' + + '949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5' + + 'ceabc725da8c02aabeaaa13ac36a75cc0bae135df3114b66589c7ed3c' + + 'b61559ae5a384f162bfa80dbe4617f86c3f1d010c94fe2c9bf019a6e6' + + '3b3efc028d43cee611c85ec263c906c463772c6911b19eec096ca76ec' + + '5e31e1e30203010001', +); + +// --- createSecretKey Tests --- + +test(SUITE, 'createSecretKey from Buffer', () => { + const keyData = randomBytes(32); + const key = createSecretKey(keyData); + + expect(key.type).to.equal('secret'); +}); + +test(SUITE, 'createSecretKey from Uint8Array', () => { + const keyData = new Uint8Array(randomBytes(32)); + const key = createSecretKey(keyData); + + expect(key.type).to.equal('secret'); +}); + +test(SUITE, 'createSecretKey 128-bit key', () => { + const keyData = randomBytes(16); + const key = createSecretKey(keyData); + + expect(key.type).to.equal('secret'); +}); + +test(SUITE, 'createSecretKey 256-bit key', () => { + const keyData = randomBytes(32); + const key = createSecretKey(keyData); + + expect(key.type).to.equal('secret'); +}); + +test(SUITE, 'createSecretKey export and reimport', () => { + const keyData = randomBytes(32); + const key = createSecretKey(keyData); + + const exported = key.export(); + const reimported = createSecretKey(exported); + + expect(reimported.type).to.equal('secret'); + expect(Buffer.compare(reimported.export(), keyData)).to.equal(0); +}); + +// --- createPublicKey Tests --- + +test(SUITE, 'createPublicKey from PEM string', () => { + const key = createPublicKey(rsaPublicKeyPem); + + expect(key.type).to.equal('public'); + expect(key.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPublicKey from DER buffer (SPKI)', () => { + const key = createPublicKey({ + key: Buffer.from(spkiDer), + format: 'der', + type: 'spki', + }); + + expect(key.type).to.equal('public'); + expect(key.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPublicKey extracts public from private key', () => { + const privateKey = createPrivateKey(rsaPrivateKeyPem); + const publicKey = createPublicKey(privateKey); + + expect(publicKey.type).to.equal('public'); + expect(publicKey.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPublicKey from generateKeyPair', async () => { + const { 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 key = createPublicKey(publicKey); + + expect(key.type).to.equal('public'); + expect(key.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPublicKey EC P-256', async () => { + const { publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-256', + 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 key = createPublicKey(publicKey); + + expect(key.type).to.equal('public'); + expect(key.asymmetricKeyType).to.equal('ec'); +}); + +test(SUITE, 'createPublicKey Ed25519', async () => { + const { publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ed25519', + { + 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 key = createPublicKey(publicKey); + + expect(key.type).to.equal('public'); + expect(key.asymmetricKeyType).to.equal('ed25519'); +}); + +// --- createPrivateKey Tests --- + +test(SUITE, 'createPrivateKey from PEM string', () => { + const key = createPrivateKey(rsaPrivateKeyPem); + + expect(key.type).to.equal('private'); + expect(key.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPrivateKey from DER buffer (PKCS8)', () => { + const key = createPrivateKey({ + key: Buffer.from(pkcs8Der), + format: 'der', + type: 'pkcs8', + }); + + expect(key.type).to.equal('private'); + expect(key.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPrivateKey from generateKeyPair', async () => { + const { privateKey } = 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 key = createPrivateKey(privateKey); + + expect(key.type).to.equal('private'); + expect(key.asymmetricKeyType).to.equal('rsa'); +}); + +test(SUITE, 'createPrivateKey EC P-256', async () => { + const { privateKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-256', + 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 key = createPrivateKey(privateKey); + + expect(key.type).to.equal('private'); + expect(key.asymmetricKeyType).to.equal('ec'); +}); + +test(SUITE, 'createPrivateKey Ed25519', async () => { + const { privateKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ed25519', + { + 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 key = createPrivateKey(privateKey); + + expect(key.type).to.equal('private'); + expect(key.asymmetricKeyType).to.equal('ed25519'); +}); + +// --- Round-Trip Tests --- + +test(SUITE, 'RSA key round-trip: create -> export -> create', () => { + const originalPrivate = createPrivateKey(rsaPrivateKeyPem); + const exportedPrivate = originalPrivate.export({ + type: 'pkcs8', + format: 'pem', + }); + const reimportedPrivate = createPrivateKey(exportedPrivate as string); + + expect(reimportedPrivate.type).to.equal('private'); + expect(reimportedPrivate.asymmetricKeyType).to.equal('rsa'); +}); + +test( + SUITE, + 'EC key round-trip: generateKeyPair -> createPrivateKey -> export -> createPrivateKey', + async () => { + const { privateKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-384', + 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 key1 = createPrivateKey(privateKey); + const exported = key1.export({ type: 'pkcs8', format: 'pem' }); + const key2 = createPrivateKey(exported as string); + + expect(key2.type).to.equal('private'); + expect(key2.asymmetricKeyType).to.equal('ec'); + }, +); + +// --- Error Cases --- + +test(SUITE, 'createPublicKey throws with invalid PEM', async () => { + await assertThrowsAsync(async () => { + createPublicKey('not a valid PEM key'); + }, ''); +}); + +test(SUITE, 'createPrivateKey throws with invalid PEM', async () => { + await assertThrowsAsync(async () => { + createPrivateKey('not a valid PEM key'); + }, ''); +}); + +test( + SUITE, + 'createPublicKey throws with private key PEM (wrong type)', + async () => { + await assertThrowsAsync(async () => { + createPublicKey({ + key: rsaPrivateKeyPem, + format: 'pem', + type: 'spki', // Wrong type for private key + }); + }, ''); + }, +); diff --git a/example/src/tests/keys/fixtures.ts b/example/src/tests/keys/fixtures.ts new file mode 100644 index 00000000..03232fb8 --- /dev/null +++ b/example/src/tests/keys/fixtures.ts @@ -0,0 +1,39 @@ +// Shared RSA test keys (2048-bit) +export const rsaPrivateKeyPem = `-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHNjLDbik/Xy7Q +NA3a7TtjaPI9OEA1j+zle8rU9NKP8bYFXFNnphMsWX5X4hB4DPz5WPxSRx2LR8Dq +N0vGcotj/PRjr2d9hCW1Nsp8VwvQMxk0BZPjSpPup0uuzEgfcZrtjqsthakmcG8r +DY+69CXh4wEromwQQI+s43Ls2vg5fSevbJNOVE7CKy2riaqDIylrUoCiZ1LO+Vve +FLZ4jXHErY2Wb6h8jrW1c7fVhJdJ3RzLDW/zF3TAkslu1KDlfiNL23v2AtR5VKA5 +Cx6z+33i98XCfUzBipDbnbE1ww5qZXVDuozs5iomtQ7EnQ5RCeoJkoXk/GOlK46X +3CHNQ6GjAgMBAAECggEAXv4+OKjILHrj5M5dqP6k6iN6F61CGQh3i3p7Xw8bdR5q +kKXU88Ditaw7LgcTmVuAKhq/vzBAK1Fc8ZLKpGeshlJx6zMSI20nWgE1jxMnA/HJ +29+pBKJkZlIKKeEppyzSFuOIRt3MqhLFP/9ogVq40b0gqsD5zMoseOHAxKcp5Kct +JHCrx10XuLyKU3vFZWF5b4hKU4szAHHybtT39mSFIaCz5LSkAl5ihcrUFdCNmapV +PhfNBgRU/eURoTkDKfv3y/vwhgzedEoPc+U+7+NrnLufzQRMkir5ZOBvtB0oCI33 +1BgXtOZ1SKcw196XAjfXfUqnrwjXvZ9qCr7CR1mq8QKBgQDlz1dedrQWNRswd9VU +BR4TYM5M/KohWOxJvChBOaedVEUN0WDng7+6XcLuO6khwVCtY94EWrx4RF35stlT +Qv19ECW78NmXLczemxG4hI9VcJf42LHirj9HotSZzEPicLJwjcCdRX9Oq29EoPjJ +IOZGQKXfmV+AGy5+qBjJUtfqiQKBgQDd6ikDInEqt3HBx2aTHjoMFHJxdrCIUjAk +Ajn+ld2Reoa/Nh+yV2Ux0O8BN7cLHvUmoeMKIZ8XoXhSztNZVz50Jju/EeuspN+V +AVeryhJGLniRYO81GpBaqcq7tZMXxuTlDrW5pVYL4J3tu1lfy9+iZjzD1lockwO5 +kdzhCrGvywKBgQCVOp/UcqacqR2fyqEXrz8JfFpaudPMVc8SToGhYUwLqRYyU91m +WTJeVdZoFwvMJJk8Dtaz4yvxuQuBQvdGzwCGfr7SHSNevVoEz5OhS0s8QyIccLKK +rXXgEceWm4MVfvMQjawfNGrn7gESAqmrCZce1Yog+Zp/OKdnjcaSrR4SaQKBgQDZ +1Z3koc6Mq/5SxbX+/FDmwruEfYnUhzkSX80mB160C55x3GNI4VlIiVvTyik4FW94 +OLlxnIda3voJ71SwAmAgC9fiO2ko079VuTeiPn2pvrxDmO+3JRhGpx2HHToCwQ63 +erUQQygwCJF+Z8XXr30bIVjMtIFIQ1gItRIpJiI9+QKBgQCDJFzQ1Ol6LHOJvGol +iTTx3SvzcO1RwhJBxW6UtOpxZ3yWywR3rCYDqtmDF/rLGWF0b8cY2l3H0ZirmsN9 +46k7NcYf9KVb1D9I90pZNTBWuHusPHo7IclwgA6wW6AEwrQRJXo9k5Hq7gE6nK9k +T/XawRehDcCKlsreP/cMnWmkDw== +-----END PRIVATE KEY-----`; + +export const rsaPublicKeyPem = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYyw24pP18u0DQN2u07 +Y2jyPThANY/s5XvK1PTSj/G2BVxTZ6YTLFl+V+IQeAz8+Vj8Ukcdi0fA6jdLxnKL +Y/z0Y69nfYQltTbKfFcL0DMZNAWT40qT7qdLrsxIH3Ga7Y6rLYWpJnBvKw2PuvQl +4eMBK6JsEECPrONy7Nr4OX0nr2yTTlROwistq4mqgyMpa1KAomdSzvlb3hS2eI1x +xK2Nlm+ofI61tXO31YSXSd0cyw1v8xd0wJLJbtSg5X4jS9t79gLUeVSgOQses/t9 +4vfFwn1MwYqQ252xNcMOamV1Q7qM7OYqJrUOxJ0OUQnqCZKF5PxjpSuOl9whzUOh +owIDAQAB +-----END PUBLIC KEY-----`; diff --git a/example/src/tests/keys/generate_key.ts b/example/src/tests/keys/generate_key.ts new file mode 100644 index 00000000..403342c9 --- /dev/null +++ b/example/src/tests/keys/generate_key.ts @@ -0,0 +1,200 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { generateKey, generateKeySync } from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test, assertThrowsAsync } from '../util'; + +const SUITE = 'keys.generateKey'; + +// --- generateKeySync AES Tests --- + +test(SUITE, 'generateKeySync AES-128', () => { + const key = generateKeySync('aes', { length: 128 }); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(16); +}); + +test(SUITE, 'generateKeySync AES-192', () => { + const key = generateKeySync('aes', { length: 192 }); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(24); +}); + +test(SUITE, 'generateKeySync AES-256', () => { + const key = generateKeySync('aes', { length: 256 }); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(32); +}); + +test(SUITE, 'generateKeySync AES keys are unique', () => { + const key1 = generateKeySync('aes', { length: 256 }); + const key2 = generateKeySync('aes', { length: 256 }); + + const exported1 = key1.export(); + const exported2 = key2.export(); + + expect(Buffer.compare(exported1, exported2)).to.not.equal(0); +}); + +// --- generateKeySync HMAC Tests --- + +test(SUITE, 'generateKeySync HMAC 256-bit', () => { + const key = generateKeySync('hmac', { length: 256 }); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(32); +}); + +test(SUITE, 'generateKeySync HMAC 512-bit', () => { + const key = generateKeySync('hmac', { length: 512 }); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(64); +}); + +test(SUITE, 'generateKeySync HMAC minimum length (8 bits)', () => { + const key = generateKeySync('hmac', { length: 8 }); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(1); +}); + +test(SUITE, 'generateKeySync HMAC keys are unique', () => { + const key1 = generateKeySync('hmac', { length: 256 }); + const key2 = generateKeySync('hmac', { length: 256 }); + + const exported1 = key1.export(); + const exported2 = key2.export(); + + expect(Buffer.compare(exported1, exported2)).to.not.equal(0); +}); + +// --- generateKey async AES Tests --- + +test(SUITE, 'generateKey AES-128 async', async () => { + const key = await new Promise>( + (resolve, reject) => { + generateKey('aes', { length: 128 }, (err, k) => { + if (err) reject(err); + else resolve(k!); + }); + }, + ); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(16); +}); + +test(SUITE, 'generateKey AES-256 async', async () => { + const key = await new Promise>( + (resolve, reject) => { + generateKey('aes', { length: 256 }, (err, k) => { + if (err) reject(err); + else resolve(k!); + }); + }, + ); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(32); +}); + +// --- generateKey async HMAC Tests --- + +test(SUITE, 'generateKey HMAC 256-bit async', async () => { + const key = await new Promise>( + (resolve, reject) => { + generateKey('hmac', { length: 256 }, (err, k) => { + if (err) reject(err); + else resolve(k!); + }); + }, + ); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(32); +}); + +test(SUITE, 'generateKey HMAC 512-bit async', async () => { + const key = await new Promise>( + (resolve, reject) => { + generateKey('hmac', { length: 512 }, (err, k) => { + if (err) reject(err); + else resolve(k!); + }); + }, + ); + + expect(key.type).to.equal('secret'); + const exported = key.export(); + expect(exported.length).to.equal(64); +}); + +// --- Error Cases --- + +test(SUITE, 'generateKeySync throws for invalid AES length', async () => { + await assertThrowsAsync(async () => { + generateKeySync('aes', { length: 64 }); + }, 'must be 128, 192, or 256'); +}); + +test(SUITE, 'generateKeySync throws for AES length 512', async () => { + await assertThrowsAsync(async () => { + generateKeySync('aes', { length: 512 }); + }, 'must be 128, 192, or 256'); +}); + +test(SUITE, 'generateKeySync throws for HMAC length < 8', async () => { + await assertThrowsAsync(async () => { + generateKeySync('hmac', { length: 4 }); + }, 'must be >= 8'); +}); + +test(SUITE, 'generateKeySync throws for invalid type', async () => { + await assertThrowsAsync(async () => { + // @ts-expect-error Testing invalid type + generateKeySync('invalid', { length: 128 }); + }, "must be 'aes' or 'hmac'"); +}); + +test(SUITE, 'generateKeySync throws for missing options', async () => { + await assertThrowsAsync(async () => { + // @ts-expect-error Testing missing options + generateKeySync('aes'); + }, 'must be an object'); +}); + +test(SUITE, 'generateKeySync throws for non-integer length', async () => { + await assertThrowsAsync(async () => { + generateKeySync('aes', { length: 128.5 }); + }, 'must be an integer'); +}); + +test(SUITE, 'generateKey async passes error to callback', async () => { + const error = await new Promise(resolve => { + generateKey('aes', { length: 64 }, err => { + resolve(err!); + }); + }); + + expect(error).to.be.instanceOf(Error); + expect(error.message).to.include('must be 128, 192, or 256'); +}); + +test(SUITE, 'generateKey throws for non-function callback', async () => { + await assertThrowsAsync(async () => { + // @ts-expect-error Testing invalid callback + generateKey('aes', { length: 128 }, 'not a function'); + }, 'must be a function'); +}); diff --git a/example/src/tests/keys/generate_keypair.ts b/example/src/tests/keys/generate_keypair.ts new file mode 100644 index 00000000..3188cbe8 --- /dev/null +++ b/example/src/tests/keys/generate_keypair.ts @@ -0,0 +1,517 @@ +import { + generateKeyPair, + generateKeyPairSync, + createSign, + createVerify, + createPrivateKey, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test, assertThrowsAsync } from '../util'; + +const SUITE = 'keys.generateKeyPair'; + +// --- RSA Key Generation Tests --- + +test(SUITE, 'generateKeyPair RSA 2048-bit with PEM encoding', 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair RSA 4096-bit', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'rsa', + { + modulusLength: 4096, + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair RSA with DER encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: ArrayBuffer; + publicKey: ArrayBuffer; + }>((resolve, reject) => { + generateKeyPair( + 'rsa', + { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'der' }, + privateKeyEncoding: { type: 'pkcs8', format: 'der' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as ArrayBuffer, + publicKey: pubKey as ArrayBuffer, + }); + }, + ); + }); + + expect(privateKey instanceof ArrayBuffer).to.equal(true); + expect(publicKey instanceof ArrayBuffer).to.equal(true); + expect(privateKey.byteLength).to.be.greaterThan(0); + expect(publicKey.byteLength).to.be.greaterThan(0); +}); + +test(SUITE, 'generateKeyPair RSA keys work for signing', 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 testData = 'Test data for signing'; + const signature = createSign('SHA256').update(testData).sign(privateKey); + const isValid = createVerify('SHA256') + .update(testData) + .verify(publicKey, signature); + + expect(isValid).to.equal(true); +}); + +// --- RSA-PSS Key Generation Tests --- + +test(SUITE, 'generateKeyPair RSA-PSS', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'rsa-pss', + { + modulusLength: 2048, + hashAlgorithm: 'SHA-256', + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +// --- EC Key Generation Tests --- + +test(SUITE, 'generateKeyPair EC P-256', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-256', + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); + + const key = createPrivateKey(privateKey); + expect(key.asymmetricKeyType).to.equal('ec'); +}); + +test(SUITE, 'generateKeyPair EC P-384', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-384', + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair EC P-521', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-521', + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair EC keys work for signing', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-256', + 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 testData = 'Test data for ECDSA signing'; + const signature = createSign('SHA256').update(testData).sign(privateKey); + const isValid = createVerify('SHA256') + .update(testData) + .verify(publicKey, signature); + + expect(isValid).to.equal(true); +}); + +// --- Ed25519 Key Generation Tests --- + +test(SUITE, 'generateKeyPair Ed25519 with PEM encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ed25519', + { + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); + + const key = createPrivateKey(privateKey); + expect(key.asymmetricKeyType).to.equal('ed25519'); +}); + +test(SUITE, 'generateKeyPair Ed25519 with DER encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: ArrayBuffer; + publicKey: ArrayBuffer; + }>((resolve, reject) => { + generateKeyPair( + 'ed25519', + { + publicKeyEncoding: { type: 'spki', format: 'der' }, + privateKeyEncoding: { type: 'pkcs8', format: 'der' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as ArrayBuffer, + publicKey: pubKey as ArrayBuffer, + }); + }, + ); + }); + + expect(privateKey instanceof ArrayBuffer).to.equal(true); + expect(publicKey instanceof ArrayBuffer).to.equal(true); +}); + +// --- Ed448 Key Generation Tests --- + +test(SUITE, 'generateKeyPair Ed448 with PEM encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ed448', + { + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); + + const key = createPrivateKey(privateKey); + expect(key.asymmetricKeyType).to.equal('ed448'); +}); + +// --- X25519 Key Generation Tests --- + +test(SUITE, 'generateKeyPair X25519 with PEM encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'x25519', + { + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); + + const key = createPrivateKey(privateKey); + expect(key.asymmetricKeyType).to.equal('x25519'); +}); + +// --- X448 Key Generation Tests --- + +test(SUITE, 'generateKeyPair X448 with PEM encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'x448', + { + 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, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); + + const key = createPrivateKey(privateKey); + expect(key.asymmetricKeyType).to.equal('x448'); +}); + +// --- generateKeyPairSync Tests --- + +test(SUITE, 'generateKeyPairSync Ed25519', () => { + const { privateKey, publicKey } = generateKeyPairSync('ed25519', { + 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 X25519', () => { + const { privateKey, publicKey } = generateKeyPairSync('x25519', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + expect(typeof privateKey).to.equal('string'); + expect(typeof publicKey).to.equal('string'); +}); + +test(SUITE, 'generateKeyPairSync Ed448', () => { + const { privateKey, publicKey } = generateKeyPairSync('ed448', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + expect(typeof privateKey).to.equal('string'); + expect(typeof publicKey).to.equal('string'); +}); + +test(SUITE, 'generateKeyPairSync X448', () => { + const { privateKey, publicKey } = generateKeyPairSync('x448', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + expect(typeof privateKey).to.equal('string'); + expect(typeof publicKey).to.equal('string'); +}); + +// --- Error Cases --- + +test( + SUITE, + 'generateKeyPair with invalid type calls callback with error', + async () => { + await assertThrowsAsync(async () => { + await Promise.race([ + new Promise((resolve, reject) => { + generateKeyPair( + 'invalid-type' as 'rsa', + { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }, + err => { + if (err) reject(err); + else resolve(); + }, + ); + }), + new Promise((_, reject) => + setTimeout( + () => reject(new Error('Timeout: callback never called')), + 1000, + ), + ), + ]); + }, ''); + }, +); + +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' }, + }); + }, ''); + }, +); diff --git a/example/src/tests/keys/public_cipher.ts b/example/src/tests/keys/public_cipher.ts new file mode 100644 index 00000000..d4d84134 --- /dev/null +++ b/example/src/tests/keys/public_cipher.ts @@ -0,0 +1,553 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { + publicEncrypt, + publicDecrypt, + generateKeyPair, + createPrivateKey, + createPublicKey, + subtle, + isCryptoKeyPair, +} from 'react-native-quick-crypto'; +import type { WebCryptoKeyPair } from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test, assertThrowsAsync, decodeHex } from '../util'; + +const SUITE = 'keys.publicEncrypt/publicDecrypt'; + +// RSA 2048-bit test keys from fixtures +// Source: Node.js test fixtures / WebCrypto test vectors +const pkcs8 = decodeHex( + '308204bf020100300d06092a864886f70d0101010500048204a930820' + + '4a50201000282010100d3576092e62957364544e7e4233b7bdb293db2' + + '085122c479328546f9f0f712f657c4b17868c930908cc594f7ed00c01' + + '442c1af04c2f678a48ba2c80fd1713e30b5ac50787ac3516589f17196' + + '7f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d716' + + '6c08ab6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7' + + 'e1032144ea949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f1346' + + '63d6f656f5ceabc725da8c02aabeaaa13ac36a75cc0bae135df3114b6' + + '6589c7ed3cb61559ae5a384f162bfa80dbe4617f86c3f1d010c94fe2c' + + '9bf019a6e63b3efc028d43cee611c85ec263c906c463772c6911b19ee' + + 'c096ca76ec5e31e1e3020301000102820101008b375ccb87c825c5ff3' + + 'd53d009916e9641057e18527227a07ab226be1088813a3b38bb7b48f3' + + '77055165fa2a9339d24dc667d5c5ba3427e6a481176eac15ffd490683' + + '11e1c283b9f3a8e0cb809b4630c50aa8f3e45a60b359e19bf8cbb5eca' + + 'd64e761f1095743ff36aaf5cf0ecb97fedaddda60b5bf35d811a75b82' + + '2230cfaa0192fad40547e275448aa3316bf8e2b4ce0854fc7708b537b' + + 'a22d13210b09aec37a2759efc082a1531b23a91730037dde4ef26b5f9' + + '6efdcc39fd34c345ad51cbbe44fe58b8a3b4ec997866c086dff1b8831' + + 'ef0a1fea263cf7dacd03c04cbcc2b279e57fa5b953996bfb1dd68817a' + + 'f7fb42cdef7a5294a57fac2b8ad739f1b029902818100fbf833c2c631' + + 'c970240c8e7485f06a3ea2a84822511a8627dd464ef8afaf7148d1a42' + + '5b6b8657ddd5246832b8e533020c5bbb568855a6aec3e4221d793f1dc' + + '5b2f2584e2415e48e9a2bd292b134031f99c8eb42fc0bcd0449bf22ce' + + '6dec97014efe5ac93ebe835877656252cbbb16c415b67b184d2284568' + + 'a277d59335585cfd02818100d6b8ce27c7295d5d16fc3570ed64c8da9' + + '303fad29488c1a65e9ad711f90370187dbbfd81316d69648bc88cc5c8' + + '3551afff45debacfb61105f709e4c30809b90031ebd686244496c6f69' + + 'e692ebdc814f64239f4ad15756ecb78c5a5b09931db183077c546a38c' + + '4c743889ad3d3ed079b5622ed0120fa0e1f93b593db7d852e05f02818' + + '038874b9d83f78178ce2d9efc175c83897fd67f306bbfa69f64ee3423' + + '68ced47c80c3f1ce177a758d64bafb0c9786a44285fa01cdec3507cde' + + 'e7dc9b7e2b21d3cbbcc100eee9967843b057329fdcca62998ed0f11b3' + + '8ce8b0abc7de39017c71cfd0ae57546c559144cdd0afd0645f7ea8ff0' + + '7b974d1ed44fd1f8e00f560bf6d45028181008529ef9073cf8f7b5ff9' + + 'e21abadf3a4173d3900670dfaf59426abcdf0493c13d2f1d1b46b824a' + + '6ac1894b3d925250c181e3472c16078056eb19a8d28f71f3080927534' + + '81d49444fdf78c9ea6c24407dc018e77d3afef385b2ff7439e9623794' + + '1332dd446cebeffdb4404fe4f71595161d016402c334d0f57c61abe4f' + + 'f9f4cbf90281810087d87708d46763e4ccbeb2d1e9712e5bf0216d70d' + + 'e9420a5b2069b7459b99f5d9f7f2fad7cd79aaee67a7f9a34437e3c79' + + 'a84af0cd8de9dff268eb0c4793f501f988d540f6d3475c2079b8227a2' + + '3d968dec4e3c66503187193459630472bfdb6ba1de786c797fa6f4ea6' + + '5a2a8419262f29678856cb73c9bd4bc89b5e041b2277', +); + +const spki = decodeHex( + '30820122300d06092a864886f70d01010105000382010f003082010a0' + + '282010100d3576092e62957364544e7e4233b7bdb293db2085122c479' + + '328546f9f0f712f657c4b17868c930908cc594f7ed00c01442c1af04c' + + '2f678a48ba2c80fd1713e30b5ac50787ac3516589f171967f6386ada3' + + '4900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d7166c08ab6ce2' + + '04640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e1032144ea' + + '949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5' + + 'ceabc725da8c02aabeaaa13ac36a75cc0bae135df3114b66589c7ed3c' + + 'b61559ae5a384f162bfa80dbe4617f86c3f1d010c94fe2c9bf019a6e6' + + '3b3efc028d43cee611c85ec263c906c463772c6911b19eec096ca76ec' + + '5e31e1e30203010001', +); + +const label = decodeHex( + '5468657265206172652037206675727468657220656469746f7269616' + + 'c206e6f74657320696e2074686520646f63756d656e742e', +); + +// Test plaintext values +const shortPlaintext = Buffer.from('Hello, World!'); +const testMessage = Buffer.from('Test message for RSA encryption'); + +// --- Basic Encrypt/Decrypt Tests --- + +test(SUITE, 'publicEncrypt/publicDecrypt round-trip with DER keys', () => { + 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(publicKey, shortPlaintext); + const decrypted = publicDecrypt(privateKey, encrypted); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +test(SUITE, 'publicEncrypt/publicDecrypt with KeyObject', () => { + 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(publicKey, testMessage); + const decrypted = publicDecrypt(privateKey, encrypted); + + expect(Buffer.compare(decrypted, testMessage)).to.equal(0); +}); + +// --- OAEP Hash Algorithm Tests --- + +test(SUITE, 'publicEncrypt/publicDecrypt with SHA-1 hash', () => { + 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, oaepHash: 'SHA-1' }, + shortPlaintext, + ); + const decrypted = publicDecrypt( + { key: privateKey, oaepHash: 'SHA-1' }, + encrypted, + ); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +test(SUITE, 'publicEncrypt/publicDecrypt with SHA-256 hash', () => { + 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, oaepHash: 'SHA-256' }, + shortPlaintext, + ); + const decrypted = publicDecrypt( + { key: privateKey, oaepHash: 'SHA-256' }, + encrypted, + ); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +test(SUITE, 'publicEncrypt/publicDecrypt with SHA-384 hash', () => { + 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, oaepHash: 'SHA-384' }, + shortPlaintext, + ); + const decrypted = publicDecrypt( + { key: privateKey, oaepHash: 'SHA-384' }, + encrypted, + ); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +test(SUITE, 'publicEncrypt/publicDecrypt with SHA-512 hash', () => { + 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, oaepHash: 'SHA-512' }, + shortPlaintext, + ); + const decrypted = publicDecrypt( + { key: privateKey, oaepHash: 'SHA-512' }, + encrypted, + ); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +// --- OAEP Label Tests --- + +test(SUITE, 'publicEncrypt/publicDecrypt with OAEP label', () => { + 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, oaepHash: 'SHA-256', oaepLabel: label }, + shortPlaintext, + ); + const decrypted = publicDecrypt( + { key: privateKey, oaepHash: 'SHA-256', oaepLabel: label }, + encrypted, + ); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +test(SUITE, 'Decrypt fails with wrong OAEP label', async () => { + 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, oaepHash: 'SHA-256', oaepLabel: label }, + shortPlaintext, + ); + + const wrongLabel = Buffer.from('wrong label'); + + await assertThrowsAsync(async () => { + publicDecrypt( + { key: privateKey, oaepHash: 'SHA-256', oaepLabel: wrongLabel }, + encrypted, + ); + }, 'publicDecrypt failed'); +}); + +// --- generateKeyPair Integration Tests --- + +test( + SUITE, + 'publicEncrypt/publicDecrypt with generateKeyPair RSA', + 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 pubKeyObj = createPublicKey(publicKey); + const privKeyObj = createPrivateKey(privateKey); + + const encrypted = publicEncrypt(pubKeyObj, testMessage); + const decrypted = publicDecrypt(privKeyObj, encrypted); + + expect(Buffer.compare(decrypted, testMessage)).to.equal(0); + }, +); + +// --- WebCrypto Compatibility Tests --- + +test(SUITE, 'publicEncrypt compatible with subtle.decrypt', async () => { + // Import keys for WebCrypto + const cryptoKeyPair = await subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['encrypt', 'decrypt'], + ); + if (!isCryptoKeyPair(cryptoKeyPair)) throw new Error('Expected key pair'); + const keyPair = cryptoKeyPair as WebCryptoKeyPair; + + // Export public key for publicEncrypt + const publicKeySpki = await subtle.exportKey('spki', keyPair.publicKey); + const publicKeyObj = createPublicKey({ + key: Buffer.from(publicKeySpki as ArrayBuffer), + format: 'der', + type: 'spki', + }); + + // Encrypt with publicEncrypt + const encrypted = publicEncrypt( + { key: publicKeyObj, oaepHash: 'SHA-256' }, + shortPlaintext, + ); + + // Decrypt with subtle.decrypt + const decrypted = await subtle.decrypt( + { name: 'RSA-OAEP' }, + keyPair.privateKey, + encrypted, + ); + + expect(Buffer.from(decrypted).toString()).to.equal(shortPlaintext.toString()); +}); + +test(SUITE, 'subtle.encrypt compatible with publicDecrypt', async () => { + // Import keys for WebCrypto + const cryptoKeyPair = await subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['encrypt', 'decrypt'], + ); + if (!isCryptoKeyPair(cryptoKeyPair)) throw new Error('Expected key pair'); + const keyPair = cryptoKeyPair as WebCryptoKeyPair; + + // Encrypt with subtle.encrypt + const encrypted = await subtle.encrypt( + { name: 'RSA-OAEP' }, + keyPair.publicKey, + shortPlaintext, + ); + + // Export private key for publicDecrypt + const privateKeyPkcs8 = await subtle.exportKey('pkcs8', keyPair.privateKey); + const privateKeyObj = createPrivateKey({ + key: Buffer.from(privateKeyPkcs8 as ArrayBuffer), + format: 'der', + type: 'pkcs8', + }); + + // Decrypt with publicDecrypt + const decrypted = publicDecrypt( + { key: privateKeyObj, oaepHash: 'SHA-256' }, + Buffer.from(encrypted), + ); + + expect(decrypted.toString()).to.equal(shortPlaintext.toString()); +}); + +// --- Error Cases --- + +test(SUITE, 'Decrypt fails with wrong hash algorithm', async () => { + 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, oaepHash: 'SHA-256' }, + shortPlaintext, + ); + + await assertThrowsAsync(async () => { + publicDecrypt({ key: privateKey, oaepHash: 'SHA-1' }, encrypted); + }, 'publicDecrypt failed'); +}); + +test(SUITE, 'publicEncrypt throws with invalid key', async () => { + await assertThrowsAsync(async () => { + publicEncrypt('not a valid key', shortPlaintext); + }, ''); +}); + +// --- Different Key Sizes --- + +test(SUITE, 'publicEncrypt/publicDecrypt with 4096-bit RSA', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'rsa', + { + modulusLength: 4096, + 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 pubKeyObj = createPublicKey(publicKey); + const privKeyObj = createPrivateKey(privateKey); + + const encrypted = publicEncrypt(pubKeyObj, testMessage); + const decrypted = publicDecrypt(privKeyObj, encrypted); + + expect(Buffer.compare(decrypted, testMessage)).to.equal(0); +}); + +// --- Empty and Edge Case Plaintexts --- + +test(SUITE, 'publicEncrypt/publicDecrypt with 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 = publicEncrypt(publicKey, emptyBuffer); + const decrypted = publicDecrypt(privateKey, encrypted); + + expect(decrypted.length).to.equal(0); +}); + +test(SUITE, 'publicEncrypt/publicDecrypt with single byte plaintext', () => { + 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 = publicEncrypt(publicKey, singleByte); + const decrypted = publicDecrypt(privateKey, encrypted); + + expect(decrypted.length).to.equal(1); + expect(decrypted[0]).to.equal(0x42); +}); + +// --- Maximum Plaintext Size Tests --- + +test(SUITE, 'publicEncrypt with max size plaintext for SHA-256', () => { + const publicKey = createPublicKey({ + key: Buffer.from(spki), + format: 'der', + type: 'spki', + }); + + const privateKey = createPrivateKey({ + key: Buffer.from(pkcs8), + format: 'der', + type: 'pkcs8', + }); + + // For RSA-OAEP with SHA-256: max = keySize - 2*hashSize - 2 = 256 - 64 - 2 = 190 bytes + const maxPlaintext = Buffer.alloc(190, 'A'); + const encrypted = publicEncrypt( + { key: publicKey, oaepHash: 'SHA-256' }, + maxPlaintext, + ); + const decrypted = publicDecrypt( + { key: privateKey, oaepHash: 'SHA-256' }, + encrypted, + ); + + expect(Buffer.compare(decrypted, maxPlaintext)).to.equal(0); +}); + +test(SUITE, 'publicEncrypt fails with oversized plaintext', async () => { + const publicKey = createPublicKey({ + key: Buffer.from(spki), + format: 'der', + type: 'spki', + }); + + // For RSA-OAEP with SHA-256: max = 190 bytes, try 191 + const oversizedPlaintext = Buffer.alloc(191, 'A'); + + await assertThrowsAsync(async () => { + publicEncrypt({ key: publicKey, oaepHash: 'SHA-256' }, oversizedPlaintext); + }, ''); +}); diff --git a/example/src/tests/keys/sign_verify_streaming.ts b/example/src/tests/keys/sign_verify_streaming.ts new file mode 100644 index 00000000..4e740458 --- /dev/null +++ b/example/src/tests/keys/sign_verify_streaming.ts @@ -0,0 +1,457 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { + createSign, + createVerify, + generateKeyPair, + createPrivateKey, + createPublicKey, + constants, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test, assertThrowsAsync } from '../util'; +import { rsaPrivateKeyPem, rsaPublicKeyPem } from './fixtures'; + +const SUITE = 'keys.sign/verify'; + +// EC P-256 keys for ECDSA testing (TODO: add EC sign/verify tests) +const _ecPrivateKeyPem = `-----BEGIN EC PRIVATE KEY----- +MHQCAQEEICJxApEBg7MxZzh5JhtRSAj2rFnE0UYrj/swevFPCIGRoAcGBSuBBAAK +oUQDQgAEHtKhP2bJUHQoON4fB0ND/Z1ND6uQgfT7wBhMADWNxon36qP5Ypzb5z5x +nTHEi4WkLkxTqFsLYK5Gw/XPa+3hvw== +-----END EC PRIVATE KEY-----`; +void _ecPrivateKeyPem; + +const _ecPublicKeyPem = `-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEHtKhP2bJUHQoON4fB0ND/Z1ND6uQgfT7 +wBhMADWNxon36qP5Ypzb5z5xnTHEi4WkLkxTqFsLYK5Gw/XPa+3hvw== +-----END PUBLIC KEY-----`; +void _ecPublicKeyPem; + +// Test data +const testData = 'Test message for signing'; +const testDataBuffer = Buffer.from(testData); + +// --- Basic Sign/Verify Tests --- + +test(SUITE, 'createSign returns Sign instance', () => { + const sign = createSign('SHA256'); + expect(typeof sign.update).to.equal('function'); + expect(typeof sign.sign).to.equal('function'); +}); + +test(SUITE, 'createVerify returns Verify instance', () => { + const verify = createVerify('SHA256'); + expect(typeof verify.update).to.equal('function'); + expect(typeof verify.verify).to.equal('function'); +}); + +test(SUITE, 'RSA SHA256 sign and verify with PEM keys', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA SHA256 sign and verify with Buffer data', () => { + const sign = createSign('SHA256'); + sign.update(testDataBuffer); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA256'); + verify.update(testDataBuffer); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA SHA256 multiple update calls', () => { + const sign = createSign('SHA256'); + sign.update('Test '); + sign.update('message '); + sign.update('for signing'); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA256'); + verify.update('Test '); + verify.update('message '); + verify.update('for signing'); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA SHA256 chainable update calls', () => { + const signature = createSign('SHA256') + .update('Test ') + .update('message ') + .update('for signing') + .sign(rsaPrivateKeyPem); + + const isValid = createVerify('SHA256') + .update('Test ') + .update('message ') + .update('for signing') + .verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +// --- Output Encoding Tests --- + +test(SUITE, 'RSA sign with hex output encoding', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem, 'hex'); + + expect(typeof signature).to.equal('string'); + expect(signature).to.match(/^[0-9a-f]+$/i); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, signature, 'hex'); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA sign with base64 output encoding', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem, 'base64'); + + expect(typeof signature).to.equal('string'); + expect(signature).to.match(/^[A-Za-z0-9+/]+=*$/); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, signature, 'base64'); + + expect(isValid).to.equal(true); +}); + +// --- Different Hash Algorithms --- + +test(SUITE, 'RSA SHA1 sign and verify', () => { + const sign = createSign('SHA1'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA1'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA SHA384 sign and verify', () => { + const sign = createSign('SHA384'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA384'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA SHA512 sign and verify', () => { + const sign = createSign('SHA512'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA512'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(true); +}); + +// --- RSA-PSS Tests --- + +test(SUITE, 'RSA-PSS with SHA256 and salt length', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign({ + key: rsaPrivateKeyPem, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: 32, + }); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify( + { + key: rsaPublicKeyPem, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: 32, + }, + signature, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA-PSS with SHA256 and auto salt length', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign({ + key: rsaPrivateKeyPem, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_AUTO, + }); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify( + { + key: rsaPublicKeyPem, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_AUTO, + }, + signature, + ); + + expect(isValid).to.equal(true); +}); + +// --- KeyObject Tests --- + +test(SUITE, 'Sign/Verify with KeyObject', () => { + const privateKey = createPrivateKey(rsaPrivateKeyPem); + const publicKey = createPublicKey(rsaPublicKeyPem); + + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(privateKey); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify(publicKey, signature); + + expect(isValid).to.equal(true); +}); + +// --- Verification Failure Tests --- + +test(SUITE, 'Verify fails with wrong data', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA256'); + verify.update('Wrong data'); + const isValid = verify.verify(rsaPublicKeyPem, signature); + + expect(isValid).to.equal(false); +}); + +test(SUITE, 'Verify fails with tampered signature', () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + // Tamper with the signature + const tamperedSig = Buffer.from(signature); + tamperedSig[0] = (tamperedSig[0] ?? 0) ^ 0xff; + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify(rsaPublicKeyPem, tamperedSig); + + expect(isValid).to.equal(false); +}); + +// --- Ed25519 Tests --- + +test(SUITE, 'Ed25519 sign and verify', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ed25519', + { + 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 sign = createSign('SHA512'); + sign.update(testData); + const signature = sign.sign(privateKey); + + const verify = createVerify('SHA512'); + verify.update(testData); + const isValid = verify.verify(publicKey, signature); + + expect(isValid).to.equal(true); +}); + +// --- ECDSA Tests --- + +test(SUITE, 'ECDSA P-256 sign and verify with DER encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-256', + 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 sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign({ + key: privateKey, + dsaEncoding: 'der', + }); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify( + { + key: publicKey, + dsaEncoding: 'der', + }, + signature, + ); + + expect(isValid).to.equal(true); +}); + +test( + SUITE, + 'ECDSA P-256 sign and verify with IEEE-P1363 encoding', + async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'ec', + { + namedCurve: 'P-256', + 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 sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign({ + key: privateKey, + dsaEncoding: 'ieee-p1363', + }); + + // IEEE-P1363 signature for P-256 should be exactly 64 bytes (32 + 32) + expect(signature.length).to.equal(64); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify( + { + key: publicKey, + dsaEncoding: 'ieee-p1363', + }, + signature, + ); + + expect(isValid).to.equal(true); + }, +); + +// --- Error Cases --- + +test(SUITE, 'Sign throws with null private key', async () => { + const sign = createSign('SHA256'); + sign.update(testData); + + await assertThrowsAsync(async () => { + sign.sign(null as unknown as string); + }, 'Private key is required'); +}); + +test(SUITE, 'Verify throws with null public key', async () => { + const sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(rsaPrivateKeyPem); + + const verify = createVerify('SHA256'); + verify.update(testData); + + await assertThrowsAsync(async () => { + verify.verify(null as unknown as string, signature); + }, 'Public key is required'); +}); + +// --- generateKeyPair Integration Tests --- + +test(SUITE, 'Sign/Verify with generateKeyPair RSA', 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 sign = createSign('SHA256'); + sign.update(testData); + const signature = sign.sign(privateKey); + + const verify = createVerify('SHA256'); + verify.update(testData); + const isValid = verify.verify(publicKey, signature); + + expect(isValid).to.equal(true); +}); diff --git a/example/src/tests/subtle/generateKey.ts b/example/src/tests/subtle/generateKey.ts index f8e396d9..f705936a 100644 --- a/example/src/tests/subtle/generateKey.ts +++ b/example/src/tests/subtle/generateKey.ts @@ -122,9 +122,9 @@ const vectors: Vectors = { // Test invalid algorithms async function testInvalidAlgorithm(algorithm: any) { - // one test is slightly different than the others + // Tests with invalid hash algorithms get a different error message const errorText = - algorithm.hash === 'SHA' + algorithm.hash === 'SHA' || algorithm.hash === 'MD5' ? 'Invalid Hash Algorithm' : 'Unrecognized algorithm name'; const algo = JSON.stringify(algorithm); diff --git a/example/src/tests/subtle/sign_verify.ts b/example/src/tests/subtle/sign_verify.ts index 2c750dac..fc85733e 100644 --- a/example/src/tests/subtle/sign_verify.ts +++ b/example/src/tests/subtle/sign_verify.ts @@ -1,5 +1,13 @@ -import { normalizeHashName, subtle } from 'react-native-quick-crypto'; -import type { CryptoKey, CryptoKeyPair } from 'react-native-quick-crypto'; +import { + normalizeHashName, + subtle, + isCryptoKeyPair, + CryptoKey, +} from 'react-native-quick-crypto'; +import type { + CryptoKeyPair, + WebCryptoKeyPair, +} from 'react-native-quick-crypto'; import { test } from '../util'; import { expect } from 'chai'; @@ -7,53 +15,24 @@ const encoder = new TextEncoder(); const SUITE = 'subtle.sign/verify'; -// // Test Sign/Verify RSASSA-PKCS1-v1_5 -// { -// async function test(data) { -// const ec = new TextEncoder(); -// const { publicKey, privateKey } = await subtle.generateKey({ -// name: 'RSASSA-PKCS1-v1_5', -// modulusLength: 1024, -// publicExponent: new Uint8Array([1, 0, 1]), -// hash: 'SHA-256' -// }, true, ['sign', 'verify']); - -// const signature = await subtle.sign({ -// name: 'RSASSA-PKCS1-v1_5' -// }, privateKey, ec.encode(data)); - -// assert(await subtle.verify({ -// name: 'RSASSA-PKCS1-v1_5' -// }, publicKey, signature, ec.encode(data))); -// } - -// test('hello world').then(common.mustCall()); -// } - -// // Test Sign/Verify RSA-PSS -// { -// async function test(data) { -// const ec = new TextEncoder(); -// const { publicKey, privateKey } = await subtle.generateKey({ -// name: 'RSA-PSS', -// modulusLength: 4096, -// publicExponent: new Uint8Array([1, 0, 1]), -// hash: 'SHA-256' -// }, true, ['sign', 'verify']); - -// const signature = await subtle.sign({ -// name: 'RSA-PSS', -// saltLength: 256, -// }, privateKey, ec.encode(data)); - -// assert(await subtle.verify({ -// name: 'RSA-PSS', -// saltLength: 256, -// }, publicKey, signature, ec.encode(data))); -// } - -// test('hello world').then(common.mustCall()); -// } +const testData = encoder.encode('Test message for WebCrypto signing'); +const emptyData = new Uint8Array(0); + +async function generateKeyPairChecked( + ...args: Parameters +): Promise { + const result = await subtle.generateKey(...args); + if (!isCryptoKeyPair(result)) throw new Error('Expected key pair'); + return result as WebCryptoKeyPair; +} + +async function generateSymmetricKeyChecked( + ...args: Parameters +): Promise { + const result = await subtle.generateKey(...args); + if (isCryptoKeyPair(result)) throw new Error('Expected single key'); + return result; +} test(SUITE, 'ECDSA P-384', async () => { const pair = await subtle.generateKey( @@ -103,71 +82,651 @@ test(SUITE, 'ECDSA with HashAlgorithmIdentifier', async () => { ).to.equal(true); }); -// // Test Sign/Verify HMAC -// { -// async function test(data) { -// const ec = new TextEncoder(); - -// const key = await subtle.generateKey({ -// name: 'HMAC', -// length: 256, -// hash: 'SHA-256' -// }, true, ['sign', 'verify']); - -// const signature = await subtle.sign({ -// name: 'HMAC', -// }, key, ec.encode(data)); - -// assert(await subtle.verify({ -// name: 'HMAC', -// }, key, signature, ec.encode(data))); -// } - -// test('hello world').then(common.mustCall()); -// } - -// // Test Sign/Verify Ed25519 -// { -// async function test(data) { -// const ec = new TextEncoder(); -// const { publicKey, privateKey } = await subtle.generateKey({ -// name: 'Ed25519', -// }, true, ['sign', 'verify']); - -// const signature = await subtle.sign({ -// name: 'Ed25519', -// }, privateKey, ec.encode(data)); - -// assert(await subtle.verify({ -// name: 'Ed25519', -// }, publicKey, signature, ec.encode(data))); -// } - -// test('hello world').then(common.mustCall()); -// } - -// // Test Sign/Verify Ed448 -// { -// async function test(data) { -// const ec = new TextEncoder(); -// const { publicKey, privateKey } = await subtle.generateKey({ -// name: 'Ed448', -// }, true, ['sign', 'verify']); - -// const signature = await subtle.sign({ -// name: 'Ed448', -// }, privateKey, ec.encode(data)); - -// assert(await subtle.verify({ -// name: 'Ed448', -// }, publicKey, signature, ec.encode(data))); -// } - -// test('hello world').then(common.mustCall()); -// } - -// TODO: when other algorithms are implemented, add the tests in -// * test-webcrypto-sign-verify-ecdsa.js -// * test-webcrypto-sign-verify-eddsa.js -// * test-webcrypto-sign-verify-hmac.js -// * test-webcrypto-sign-verify-rsa.js +// --- RSASSA-PKCS1-v1_5 Tests --- + +test(SUITE, 'RSASSA-PKCS1-v1_5 with SHA-256 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSASSA-PKCS1-v1_5 with SHA-384 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-384', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSASSA-PKCS1-v1_5 with SHA-512 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-512', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test( + SUITE, + 'RSASSA-PKCS1-v1_5 verify fails with tampered signature', + async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.privateKey, + testData, + ); + + // Tamper with the signature + const tamperedSig = new Uint8Array(signature); + tamperedSig[0] = (tamperedSig[0] ?? 0) ^ 0xff; + + const isValid = await subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.publicKey, + tamperedSig, + testData, + ); + + expect(isValid).to.equal(false); + }, +); + +test(SUITE, 'RSASSA-PKCS1-v1_5 verify fails with wrong data', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.privateKey, + testData, + ); + + const wrongData = encoder.encode('Different message'); + const isValid = await subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + keyPair.publicKey, + signature, + wrongData, + ); + + expect(isValid).to.equal(false); +}); + +// --- RSA-PSS Tests --- + +test(SUITE, 'RSA-PSS with SHA-256 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSA-PSS', saltLength: 32 }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'RSA-PSS', saltLength: 32 }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'RSA-PSS with different salt lengths', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + + // Test with salt length 0 + const sig0 = await subtle.sign( + { name: 'RSA-PSS', saltLength: 0 }, + keyPair.privateKey, + testData, + ); + const valid0 = await subtle.verify( + { name: 'RSA-PSS', saltLength: 0 }, + keyPair.publicKey, + sig0, + testData, + ); + expect(valid0).to.equal(true); + + // Test with salt length 64 + const sig64 = await subtle.sign( + { name: 'RSA-PSS', saltLength: 64 }, + keyPair.privateKey, + testData, + ); + const valid64 = await subtle.verify( + { name: 'RSA-PSS', saltLength: 64 }, + keyPair.publicKey, + sig64, + testData, + ); + expect(valid64).to.equal(true); +}); + +test(SUITE, 'RSA-PSS with SHA-512 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-512', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'RSA-PSS', saltLength: 64 }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'RSA-PSS', saltLength: 64 }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +// --- ECDSA Tests --- + +test(SUITE, 'ECDSA P-256 with SHA-256 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'ECDSA', hash: 'SHA-256' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'ECDSA P-384 with SHA-384 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'ECDSA', + namedCurve: 'P-384', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-384' }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'ECDSA', hash: 'SHA-384' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'ECDSA P-521 with SHA-512 sign/verify', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'ECDSA', + namedCurve: 'P-521', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-512' }, + keyPair.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'ECDSA', hash: 'SHA-512' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'ECDSA verify fails with tampered signature', async () => { + const keyPair = await generateKeyPairChecked( + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, + keyPair.privateKey, + testData, + ); + + // Tamper with the signature + const tamperedSig = new Uint8Array(signature); + tamperedSig[10] = (tamperedSig[10] ?? 0) ^ 0xff; + + const isValid = await subtle.verify( + { name: 'ECDSA', hash: 'SHA-256' }, + keyPair.publicKey, + tamperedSig, + testData, + ); + + expect(isValid).to.equal(false); +}); + +// --- Ed25519 Tests --- + +test(SUITE, 'Ed25519 sign/verify', async () => { + const keyPair = await generateKeyPairChecked({ name: 'Ed25519' }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: 'Ed25519' }, + keyPair.privateKey, + testData, + ); + + // Ed25519 signature is always 64 bytes + expect(signature.byteLength).to.equal(64); + + const isValid = await subtle.verify( + { name: 'Ed25519' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'Ed25519 sign/verify with empty data', async () => { + const keyPair = await generateKeyPairChecked({ name: 'Ed25519' }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: 'Ed25519' }, + keyPair.privateKey, + emptyData, + ); + + const isValid = await subtle.verify( + { name: 'Ed25519' }, + keyPair.publicKey, + signature, + emptyData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'Ed25519 verify fails with tampered signature', async () => { + const keyPair = await generateKeyPairChecked({ name: 'Ed25519' }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: 'Ed25519' }, + keyPair.privateKey, + testData, + ); + + const tamperedSig = new Uint8Array(signature); + tamperedSig[0] = (tamperedSig[0] ?? 0) ^ 0xff; + + const isValid = await subtle.verify( + { name: 'Ed25519' }, + keyPair.publicKey, + tamperedSig, + testData, + ); + + expect(isValid).to.equal(false); +}); + +test(SUITE, 'Ed25519 verify fails with wrong public key', async () => { + const keyPair1 = await generateKeyPairChecked({ name: 'Ed25519' }, true, [ + 'sign', + 'verify', + ]); + + const keyPair2 = await generateKeyPairChecked({ name: 'Ed25519' }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: 'Ed25519' }, + keyPair1.privateKey, + testData, + ); + + const isValid = await subtle.verify( + { name: 'Ed25519' }, + keyPair2.publicKey, // Wrong public key + signature, + testData, + ); + + expect(isValid).to.equal(false); +}); + +// --- Ed448 Tests --- + +test(SUITE, 'Ed448 sign/verify', async () => { + const keyPair = await generateKeyPairChecked({ name: 'Ed448' }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: 'Ed448' }, + keyPair.privateKey, + testData, + ); + + // Ed448 signature is always 114 bytes + expect(signature.byteLength).to.equal(114); + + const isValid = await subtle.verify( + { name: 'Ed448' }, + keyPair.publicKey, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'Ed448 sign/verify with empty data', async () => { + const keyPair = await generateKeyPairChecked({ name: 'Ed448' }, true, [ + 'sign', + 'verify', + ]); + + const signature = await subtle.sign( + { name: 'Ed448' }, + keyPair.privateKey, + emptyData, + ); + + const isValid = await subtle.verify( + { name: 'Ed448' }, + keyPair.publicKey, + signature, + emptyData, + ); + + expect(isValid).to.equal(true); +}); + +// --- HMAC Tests --- + +test(SUITE, 'HMAC SHA-256 sign/verify', async () => { + const key = await generateSymmetricKeyChecked( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign({ name: 'HMAC' }, key, testData); + + // HMAC-SHA-256 produces 32 bytes + expect(signature.byteLength).to.equal(32); + + const isValid = await subtle.verify( + { name: 'HMAC' }, + key, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'HMAC SHA-512 sign/verify', async () => { + const key = await generateSymmetricKeyChecked( + { name: 'HMAC', hash: 'SHA-512' }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign({ name: 'HMAC' }, key, testData); + + // HMAC-SHA-512 produces 64 bytes + expect(signature.byteLength).to.equal(64); + + const isValid = await subtle.verify( + { name: 'HMAC' }, + key, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); + +test(SUITE, 'HMAC verify fails with different key', async () => { + const key1 = await generateSymmetricKeyChecked( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + + const key2 = await generateSymmetricKeyChecked( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign({ name: 'HMAC' }, key1, testData); + + const isValid = await subtle.verify( + { name: 'HMAC' }, + key2, // Different key + signature, + testData, + ); + + expect(isValid).to.equal(false); +}); + +test(SUITE, 'HMAC verify fails with tampered signature', async () => { + const key = await generateSymmetricKeyChecked( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + + const signature = await subtle.sign({ name: 'HMAC' }, key, testData); + + const tamperedSig = new Uint8Array(signature); + tamperedSig[0] = (tamperedSig[0] ?? 0) ^ 0xff; + + const isValid = await subtle.verify( + { name: 'HMAC' }, + key, + tamperedSig, + testData, + ); + + expect(isValid).to.equal(false); +}); + +// --- Key Import/Export and Sign/Verify --- + +test(SUITE, 'Sign with imported Ed25519 key', async () => { + const keyPair = await generateKeyPairChecked({ name: 'Ed25519' }, true, [ + 'sign', + 'verify', + ]); + + // Export and reimport private key + const pkcs8 = await subtle.exportKey('pkcs8', keyPair.privateKey); + const reimportedPrivate = await subtle.importKey( + 'pkcs8', + pkcs8, + { name: 'Ed25519' }, + true, + ['sign'], + ); + + // Export and reimport public key + const spki = await subtle.exportKey('spki', keyPair.publicKey); + const reimportedPublic = await subtle.importKey( + 'spki', + spki, + { name: 'Ed25519' }, + true, + ['verify'], + ); + + // Sign with reimported private key + const signature = await subtle.sign( + { name: 'Ed25519' }, + reimportedPrivate, + testData, + ); + + // Verify with reimported public key + const isValid = await subtle.verify( + { name: 'Ed25519' }, + reimportedPublic, + signature, + testData, + ); + + expect(isValid).to.equal(true); +}); diff --git a/example/tsconfig.json b/example/tsconfig.json index e0a12451..e1499f15 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,8 +1,5 @@ { - "extends": [ - "@react-native/typescript-config", - "../config/tsconfig.json" - ], + "extends": ["@react-native/typescript-config", "../config/tsconfig.json"], "include": [ "index.ts", "app.json", @@ -11,5 +8,5 @@ "./**/*.tsx", "../packages/react-native-quick-crypto/src/**/*.ts" ], - "exclude": ["**/node_modules", "**/Pods"], + "exclude": ["**/node_modules", "**/Pods"] } diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 38fd0ebe..5ec43330 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -43,6 +43,8 @@ add_library( ../cpp/pbkdf2/HybridPbkdf2.cpp ../cpp/random/HybridRandom.cpp ../cpp/rsa/HybridRsaKeyPair.cpp + ../cpp/sign/HybridSignHandle.cpp + ../cpp/sign/HybridVerifyHandle.cpp ${BLAKE3_SOURCES} ../deps/fastpbkdf2/fastpbkdf2.c ../deps/ncrypto/ncrypto.cc @@ -64,6 +66,7 @@ include_directories( "../cpp/pbkdf2" "../cpp/random" "../cpp/rsa" + "../cpp/sign" "../cpp/utils" "../deps/blake3/c" "../deps/fastpbkdf2" diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp index cb36d986..a38ea677 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include "HybridEdKeyPair.hpp" @@ -83,6 +85,12 @@ void HybridEdKeyPair::generateKeyPairSync(double publicFormat, double publicType throw std::runtime_error("EC curve not set. Call setCurve() first."); } + // Store encoding configuration for later use in getPublicKey/getPrivateKey + this->publicFormat_ = static_cast(publicFormat); + this->publicType_ = static_cast(publicType); + this->privateFormat_ = static_cast(privateFormat); + this->privateType_ = static_cast(privateType); + // Clean up existing key if any if (this->pkey != nullptr) { EVP_PKEY_free(this->pkey); @@ -241,7 +249,43 @@ bool HybridEdKeyPair::verifySync(const std::shared_ptr& signature, std::shared_ptr HybridEdKeyPair::getPublicKey() { this->checkKeyPair(); - size_t len = 32; + + // If format is DER (0) or PEM (1), export in SPKI format + if (publicFormat_ == 0 || publicFormat_ == 1) { + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO for public key export"); + } + + int result; + if (publicFormat_ == 1) { + // PEM format + result = PEM_write_bio_PUBKEY(bio, this->pkey); + } else { + // DER format + result = i2d_PUBKEY_bio(bio, this->pkey); + } + + if (result != 1) { + BIO_free(bio); + throw std::runtime_error("Failed to export public key"); + } + + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + + uint8_t* data = new uint8_t[bptr->length]; + memcpy(data, bptr->data, bptr->length); + size_t len = bptr->length; + + BIO_free(bio); + + return std::make_shared(data, len, [=]() { delete[] data; }); + } + + // Default: raw format + size_t len = 0; + EVP_PKEY_get_raw_public_key(this->pkey, nullptr, &len); uint8_t* publ = new uint8_t[len]; EVP_PKEY_get_raw_public_key(this->pkey, publ, &len); @@ -250,7 +294,43 @@ std::shared_ptr HybridEdKeyPair::getPublicKey() { std::shared_ptr HybridEdKeyPair::getPrivateKey() { this->checkKeyPair(); - size_t len = 32; + + // If format is DER (0) or PEM (1), export in PKCS8 format + if (privateFormat_ == 0 || privateFormat_ == 1) { + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO for private key export"); + } + + int result; + if (privateFormat_ == 1) { + // PEM format (PKCS8) + result = PEM_write_bio_PrivateKey(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr); + } else { + // DER format (PKCS8) + result = i2d_PrivateKey_bio(bio, this->pkey); + } + + if (result != 1) { + BIO_free(bio); + throw std::runtime_error("Failed to export private key"); + } + + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + + uint8_t* data = new uint8_t[bptr->length]; + memcpy(data, bptr->data, bptr->length); + size_t len = bptr->length; + + BIO_free(bio); + + return std::make_shared(data, len, [=]() { delete[] data; }); + } + + // Default: raw format + size_t len = 0; + EVP_PKEY_get_raw_private_key(this->pkey, nullptr, &len); uint8_t* priv = new uint8_t[len]; EVP_PKEY_get_raw_private_key(this->pkey, priv, &len); @@ -270,8 +350,17 @@ void HybridEdKeyPair::setCurve(const std::string& curve) { EVP_PKEY* HybridEdKeyPair::importPublicKey(const std::optional>& key) { EVP_PKEY* pkey = nullptr; if (key.has_value()) { - pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, // TODO: use this->curve somehow - NULL, key.value()->data(), 32); + // Determine key type from curve name + int keyType = EVP_PKEY_ED25519; + if (this->curve == "ed448" || this->curve == "Ed448") { + keyType = EVP_PKEY_ED448; + } else if (this->curve == "x25519" || this->curve == "X25519") { + keyType = EVP_PKEY_X25519; + } else if (this->curve == "x448" || this->curve == "X448") { + keyType = EVP_PKEY_X448; + } + + pkey = EVP_PKEY_new_raw_public_key(keyType, NULL, key.value()->data(), key.value()->size()); if (pkey == nullptr) { throw std::runtime_error("Failed to read public key"); } @@ -285,8 +374,17 @@ EVP_PKEY* HybridEdKeyPair::importPublicKey(const std::optional>& key) { EVP_PKEY* pkey = nullptr; if (key.has_value()) { - pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, // TODO: use this->curve somehow - NULL, key.value()->data(), 32); + // Determine key type from curve name + int keyType = EVP_PKEY_ED25519; + if (this->curve == "ed448" || this->curve == "Ed448") { + keyType = EVP_PKEY_ED448; + } else if (this->curve == "x25519" || this->curve == "X25519") { + keyType = EVP_PKEY_X25519; + } else if (this->curve == "x448" || this->curve == "X448") { + keyType = EVP_PKEY_X448; + } + + pkey = EVP_PKEY_new_raw_private_key(keyType, NULL, key.value()->data(), key.value()->size()); if (pkey == nullptr) { throw std::runtime_error("Failed to read private key"); } diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp index 7d608b31..4c32d3ff 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp @@ -56,6 +56,14 @@ class HybridEdKeyPair : public HybridEdKeyPairSpec { std::string curve; EVP_PKEY* pkey = nullptr; + // Encoding configuration for key export + // Format: -1 = default (raw), 0 = DER, 1 = PEM + // Type: 0 = PKCS1, 1 = PKCS8, 2 = SPKI, 3 = SEC1 + int publicFormat_ = -1; + int publicType_ = -1; + int privateFormat_ = -1; + int privateType_ = -1; + EVP_PKEY* importPublicKey(const std::optional>& key); EVP_PKEY* importPrivateKey(const std::optional>& key); }; diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp index 19fd953c..0820f79b 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp @@ -1,3 +1,4 @@ +#include #include #include "../utils/base64.h" @@ -127,8 +128,12 @@ std::shared_ptr HybridKeyObjectHandle::exportKey(std::optional(exportFormat), static_cast(exportType)); @@ -328,16 +333,18 @@ bool HybridKeyObjectHandle::init(KeyType keyType, const std::variant ab; if (std::holds_alternative(key)) { ab = ToNativeArrayBuffer(std::get(key)); } else { - ab = std::get>(key); + const auto& abPtr = std::get>(key); + ab = ToNativeArrayBuffer(abPtr); } // Handle raw asymmetric key material - only for special curves with known raw sizes - if (!format.has_value() && !type.has_value() && (keyType == KeyType::PUBLIC || keyType == KeyType::PRIVATE)) { + std::optional actualFormat = format; + if (!actualFormat.has_value() && !type.has_value() && (keyType == KeyType::PUBLIC || keyType == KeyType::PRIVATE)) { size_t keySize = ab->size(); // Only route to initRawKey for exact special curve sizes: // X25519/Ed25519: 32 bytes, X448: 56 bytes, Ed448: 57 bytes @@ -354,14 +361,14 @@ bool HybridKeyObjectHandle::init(KeyType keyType, const std::variantdata_ = data.addRefWithType(KeyType::PUBLIC); break; } case KeyType::PRIVATE: { - if (auto data = KeyObjectData::GetPrivateKey(ab, format, type, passphrase, false)) { + if (auto data = KeyObjectData::GetPrivateKey(ab, actualFormat, type, passphrase, false)) { this->data_ = std::move(data); } break; diff --git a/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp b/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp index 801e3a31..d746f663 100644 --- a/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp @@ -1,5 +1,6 @@ #include "KeyObjectData.hpp" #include "Utils.hpp" +#include #include namespace margelo::nitro::crypto { @@ -21,18 +22,34 @@ ncrypto::EVPKeyPointer::PublicKeyEncodingConfig GetPublicKeyEncodingConfig(KForm } KeyObjectData TryParsePrivateKey(std::shared_ptr key, std::optional format, std::optional type, - const std::optional>& /* passphrase */) { - auto config = GetPrivateKeyEncodingConfig(format.value(), type.value()); + const std::optional>& passphrase) { + // For PEM format, use PKCS8 as default encoding + KeyEncoding actualType = type.value_or(KeyEncoding::PKCS8); + auto config = GetPrivateKeyEncodingConfig(format.value(), actualType); + + if (passphrase.has_value()) { + auto& passphrase_ptr = passphrase.value(); + config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); + } + auto buffer = ncrypto::Buffer{key->data(), key->size()}; + + // Clear any existing OpenSSL errors before parsing + ERR_clear_error(); + auto res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, buffer); if (res) { return KeyObjectData::CreateAsymmetric(KeyType::PRIVATE, std::move(res.value)); } - if (res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + if (res.error.has_value() && res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { throw std::runtime_error("Passphrase required for encrypted key"); } else { - throw std::runtime_error("Failed to read private key"); + // Get OpenSSL error details + 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 read private key: " + std::string(err_buf)); } } @@ -93,40 +110,65 @@ size_t KeyObjectData::GetSymmetricKeySize() const { KeyObjectData KeyObjectData::GetPublicOrPrivateKey(std::shared_ptr key, std::optional format, std::optional type, const std::optional>& passphrase) { - // Check if key size fits in int32_t without using double conversion if (key->size() > static_cast(std::numeric_limits::max())) { - std::string error_msg = "key is too big (int32): size=" + std::to_string(key->size()) + - ", max_int32=" + std::to_string(std::numeric_limits::max()); - throw std::runtime_error(error_msg); + throw std::runtime_error("key is too big"); } - // If no format is specified, assume DER format for binary data - KFormatType actualFormat = format.has_value() ? format.value() : KFormatType::DER; + KFormatType actualFormat = format.value_or(KFormatType::DER); if (actualFormat == KFormatType::PEM || actualFormat == KFormatType::DER) { auto buffer = ncrypto::Buffer{key->data(), key->size()}; if (actualFormat == KFormatType::PEM) { - // For PEM, we can easily determine whether it is a public or private key - // by looking for the respective PEM tags. - auto res = ncrypto::EVPKeyPointer::TryParsePublicKeyPEM(buffer); - if (res) { - return CreateAsymmetric(KeyType::PUBLIC, std::move(res.value)); + if (type.has_value() && type.value() == KeyEncoding::SPKI) { + auto res = ncrypto::EVPKeyPointer::TryParsePublicKeyPEM(buffer); + if (res) { + return CreateAsymmetric(KeyType::PUBLIC, std::move(res.value)); + } + throw std::runtime_error("Failed to read PEM public key: key is not in SPKI format"); } - if (res.error.has_value() && res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NOT_RECOGNIZED) { + if (type.has_value() && + (type.value() == KeyEncoding::PKCS8 || type.value() == KeyEncoding::SEC1 || type.value() == KeyEncoding::PKCS1)) { auto config = GetPrivateKeyEncodingConfig(actualFormat, type.value()); if (passphrase.has_value()) { auto& passphrase_ptr = passphrase.value(); config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); } - + ERR_clear_error(); auto private_res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, buffer); if (private_res) { return CreateAsymmetric(KeyType::PRIVATE, std::move(private_res.value)); } + 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 read PEM private key: " + std::string(err_buf)); + } + + auto res = ncrypto::EVPKeyPointer::TryParsePublicKeyPEM(buffer); + if (res) { + return CreateAsymmetric(KeyType::PUBLIC, std::move(res.value)); + } + + KeyEncoding actualType = KeyEncoding::PKCS8; + auto config = GetPrivateKeyEncodingConfig(actualFormat, actualType); + if (passphrase.has_value()) { + auto& passphrase_ptr = passphrase.value(); + config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); + } + + ERR_clear_error(); + + auto private_res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, buffer); + if (private_res) { + return CreateAsymmetric(KeyType::PRIVATE, std::move(private_res.value)); } - throw std::runtime_error("Failed to read PEM asymmetric key"); + + 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 read PEM asymmetric key: " + std::string(err_buf)); } else if (actualFormat == KFormatType::DER) { // For DER, try parsing as public key first if (type.has_value() && type.value() == KeyEncoding::SPKI) { diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp new file mode 100644 index 00000000..3a7aec31 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp @@ -0,0 +1,191 @@ +#include "HybridSignHandle.hpp" + +#include "../keys/HybridKeyObjectHandle.hpp" +#include "SignUtils.hpp" +#include "Utils.hpp" + +#include +#include +#include +#include + +namespace margelo::nitro::crypto { + +using margelo::nitro::NativeArrayBuffer; + +HybridSignHandle::~HybridSignHandle() { + if (md_ctx) { + EVP_MD_CTX_free(md_ctx); + md_ctx = nullptr; + } +} + +void HybridSignHandle::init(const std::string& algorithm) { + algorithm_name = algorithm; + md = getDigestByName(algorithm); + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) { + throw std::runtime_error("Failed to create message digest context"); + } + + if (EVP_DigestInit_ex(md_ctx, md, nullptr) <= 0) { + EVP_MD_CTX_free(md_ctx); + md_ctx = nullptr; + throw std::runtime_error("Failed to initialize message digest"); + } +} + +void HybridSignHandle::update(const std::shared_ptr& data) { + if (!md_ctx) { + throw std::runtime_error("Sign not initialized"); + } + + auto native_data = ToNativeArrayBuffer(data); + + // Accumulate raw data for potential one-shot signing (Ed25519/Ed448) + const uint8_t* ptr = reinterpret_cast(native_data->data()); + data_buffer.insert(data_buffer.end(), ptr, ptr + native_data->size()); + + if (EVP_DigestUpdate(md_ctx, native_data->data(), native_data->size()) <= 0) { + 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 update digest: " + std::string(err_buf)); + } +} + +// Check if key type requires one-shot signing (Ed25519, Ed448) +static bool isOneShotVariant(EVP_PKEY* pkey) { + int type = EVP_PKEY_id(pkey); + return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; +} + +std::shared_ptr HybridSignHandle::sign(const std::shared_ptr& keyHandle, + std::optional padding, std::optional saltLength, + std::optional dsaEncoding) { + if (!md_ctx) { + throw std::runtime_error("Sign not initialized"); + } + + auto keyHandleImpl = std::static_pointer_cast(keyHandle); + EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get(); + + if (!pkey) { + throw std::runtime_error("Invalid private key for signing"); + } + + size_t sig_len = 0; + std::unique_ptr sig_buf; + + // Ed25519/Ed448 require one-shot signing with EVP_DigestSign + if (isOneShotVariant(pkey)) { + // Create a new context for one-shot signing + EVP_MD_CTX* sign_ctx = EVP_MD_CTX_new(); + if (!sign_ctx) { + throw std::runtime_error("Failed to create signing context"); + } + + // Initialize for one-shot signing (pass nullptr for md - Ed25519/Ed448 have built-in hash) + if (EVP_DigestSignInit(sign_ctx, nullptr, nullptr, nullptr, pkey) <= 0) { + EVP_MD_CTX_free(sign_ctx); + throw std::runtime_error("Failed to initialize Ed signing"); + } + + // Get the accumulated data from the digest context + // For Ed25519/Ed448, we need to pass the original data, not a digest + // Since we've been accumulating with DigestUpdate, we need to use the data buffer + // Unfortunately, EVP_MD_CTX doesn't expose the accumulated data directly + // We need to use EVP_DigestSign with the accumulated data + + // For one-shot variants, determine signature length first + if (EVP_DigestSign(sign_ctx, nullptr, &sig_len, data_buffer.data(), data_buffer.size()) <= 0) { + EVP_MD_CTX_free(sign_ctx); + throw std::runtime_error("Failed to determine Ed signature length"); + } + + sig_buf = std::make_unique(sig_len); + if (EVP_DigestSign(sign_ctx, sig_buf.get(), &sig_len, data_buffer.data(), data_buffer.size()) <= 0) { + EVP_MD_CTX_free(sign_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 sign with Ed key: " + std::string(err_buf)); + } + + EVP_MD_CTX_free(sign_ctx); + } else { + // Standard signing flow for RSA/ECDSA + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len = 0; + + if (EVP_DigestFinal_ex(md_ctx, digest, &digest_len) <= 0) { + throw std::runtime_error("Failed to finalize digest"); + } + + EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (!pkey_ctx) { + throw std::runtime_error("Failed to create signing context"); + } + + if (EVP_PKEY_sign_init(pkey_ctx) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to initialize signing"); + } + + if (padding.has_value()) { + int pad = static_cast(padding.value()); + if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, pad) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to set RSA padding"); + } + } + + if (saltLength.has_value() && padding.has_value() && static_cast(padding.value()) == RSA_PKCS1_PSS_PADDING) { + int salt_len = static_cast(saltLength.value()); + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_len) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to set PSS salt length"); + } + } + + if (EVP_PKEY_CTX_set_signature_md(pkey_ctx, md) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to set signature digest"); + } + + if (EVP_PKEY_sign(pkey_ctx, nullptr, &sig_len, digest, digest_len) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to determine signature length"); + } + + sig_buf = std::make_unique(sig_len); + if (EVP_PKEY_sign(pkey_ctx, sig_buf.get(), &sig_len, digest, digest_len) <= 0) { + EVP_PKEY_CTX_free(pkey_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 sign: " + std::string(err_buf)); + } + + EVP_PKEY_CTX_free(pkey_ctx); + } + + int dsa_enc = dsaEncoding.has_value() ? static_cast(dsaEncoding.value()) : kSigEncDER; + if (dsa_enc == kSigEncP1363) { + unsigned int n = getBytesOfRS(pkey); + if (n > 0) { + auto p1363_buf = std::make_unique(2 * n); + std::memset(p1363_buf.get(), 0, 2 * n); + if (convertSignatureToP1363(sig_buf.get(), sig_len, p1363_buf.get(), n)) { + uint8_t* raw_ptr = p1363_buf.get(); + return std::make_shared(p1363_buf.release(), 2 * n, [raw_ptr]() { delete[] raw_ptr; }); + } + } + } + + uint8_t* raw_ptr = sig_buf.get(); + return std::make_shared(sig_buf.release(), sig_len, [raw_ptr]() { delete[] raw_ptr; }); +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.hpp b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.hpp new file mode 100644 index 00000000..e049d130 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "HybridKeyObjectHandleSpec.hpp" +#include "HybridSignHandleSpec.hpp" + +namespace margelo::nitro::crypto { + +using namespace facebook; + +class HybridSignHandle : public HybridSignHandleSpec { + public: + HybridSignHandle() : HybridObject(TAG) {} + ~HybridSignHandle(); + + public: + void init(const std::string& algorithm) override; + void update(const std::shared_ptr& data) override; + std::shared_ptr sign(const std::shared_ptr& keyHandle, std::optional padding, + std::optional saltLength, std::optional dsaEncoding) override; + + private: + EVP_MD_CTX* md_ctx = nullptr; + const EVP_MD* md = nullptr; + std::string algorithm_name; + // Buffer for accumulating data for one-shot signing (Ed25519/Ed448) + std::vector data_buffer; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp new file mode 100644 index 00000000..bdd899d5 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp @@ -0,0 +1,158 @@ +#include "HybridVerifyHandle.hpp" + +#include "../keys/HybridKeyObjectHandle.hpp" +#include "SignUtils.hpp" +#include "Utils.hpp" + +#include +#include +#include +#include + +namespace margelo::nitro::crypto { + +using margelo::nitro::NativeArrayBuffer; + +HybridVerifyHandle::~HybridVerifyHandle() { + if (md_ctx) { + EVP_MD_CTX_free(md_ctx); + md_ctx = nullptr; + } +} + +void HybridVerifyHandle::init(const std::string& algorithm) { + algorithm_name = algorithm; + md = getDigestByName(algorithm); + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) { + throw std::runtime_error("Failed to create message digest context"); + } + + if (EVP_DigestInit_ex(md_ctx, md, nullptr) <= 0) { + EVP_MD_CTX_free(md_ctx); + md_ctx = nullptr; + throw std::runtime_error("Failed to initialize message digest"); + } +} + +void HybridVerifyHandle::update(const std::shared_ptr& data) { + if (!md_ctx) { + throw std::runtime_error("Verify not initialized"); + } + + auto native_data = ToNativeArrayBuffer(data); + + // Accumulate raw data for potential one-shot verification (Ed25519/Ed448) + const uint8_t* ptr = reinterpret_cast(native_data->data()); + data_buffer.insert(data_buffer.end(), ptr, ptr + native_data->size()); + + if (EVP_DigestUpdate(md_ctx, native_data->data(), native_data->size()) <= 0) { + 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 update digest: " + std::string(err_buf)); + } +} + +// Check if key type requires one-shot verification (Ed25519, Ed448) +static bool isOneShotVariant(EVP_PKEY* pkey) { + int type = EVP_PKEY_id(pkey); + return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; +} + +bool HybridVerifyHandle::verify(const std::shared_ptr& keyHandle, const std::shared_ptr& signature, + std::optional padding, std::optional saltLength, std::optional dsaEncoding) { + if (!md_ctx) { + throw std::runtime_error("Verify not initialized"); + } + + auto keyHandleImpl = std::static_pointer_cast(keyHandle); + EVP_PKEY* pkey = keyHandleImpl->getKeyObjectData().GetAsymmetricKey().get(); + + if (!pkey) { + throw std::runtime_error("Invalid public key for verification"); + } + + auto native_sig = ToNativeArrayBuffer(signature); + const unsigned char* sig_data = native_sig->data(); + size_t sig_len = native_sig->size(); + + // Ed25519/Ed448 require one-shot verification with EVP_DigestVerify + if (isOneShotVariant(pkey)) { + EVP_MD_CTX* verify_ctx = EVP_MD_CTX_new(); + if (!verify_ctx) { + throw std::runtime_error("Failed to create verification context"); + } + + // Initialize for one-shot verification (pass nullptr for md - Ed25519/Ed448 have built-in hash) + if (EVP_DigestVerifyInit(verify_ctx, nullptr, nullptr, nullptr, pkey) <= 0) { + EVP_MD_CTX_free(verify_ctx); + throw std::runtime_error("Failed to initialize Ed verification"); + } + + int result = EVP_DigestVerify(verify_ctx, sig_data, sig_len, data_buffer.data(), data_buffer.size()); + EVP_MD_CTX_free(verify_ctx); + return result == 1; + } + + // Standard verification flow for RSA/ECDSA + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len = 0; + + if (EVP_DigestFinal_ex(md_ctx, digest, &digest_len) <= 0) { + throw std::runtime_error("Failed to finalize digest"); + } + + std::unique_ptr der_sig_buf; + int dsa_enc = dsaEncoding.has_value() ? static_cast(dsaEncoding.value()) : kSigEncDER; + if (dsa_enc == kSigEncP1363) { + unsigned int n = getBytesOfRS(pkey); + if (n > 0) { + size_t der_len = 0; + der_sig_buf = convertSignatureToDER(sig_data, sig_len, n, &der_len); + if (der_sig_buf) { + sig_data = der_sig_buf.get(); + sig_len = der_len; + } + } + } + + EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (!pkey_ctx) { + throw std::runtime_error("Failed to create verification context"); + } + + if (EVP_PKEY_verify_init(pkey_ctx) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to initialize verification"); + } + + if (padding.has_value()) { + int pad = static_cast(padding.value()); + if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, pad) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to set RSA padding"); + } + } + + if (saltLength.has_value() && padding.has_value() && static_cast(padding.value()) == RSA_PKCS1_PSS_PADDING) { + int salt_len = static_cast(saltLength.value()); + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_len) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to set PSS salt length"); + } + } + + if (EVP_PKEY_CTX_set_signature_md(pkey_ctx, md) <= 0) { + EVP_PKEY_CTX_free(pkey_ctx); + throw std::runtime_error("Failed to set signature digest"); + } + + int result = EVP_PKEY_verify(pkey_ctx, sig_data, sig_len, digest, digest_len); + EVP_PKEY_CTX_free(pkey_ctx); + + return result == 1; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.hpp b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.hpp new file mode 100644 index 00000000..36878313 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "HybridKeyObjectHandleSpec.hpp" +#include "HybridVerifyHandleSpec.hpp" + +namespace margelo::nitro::crypto { + +using namespace facebook; + +class HybridVerifyHandle : public HybridVerifyHandleSpec { + public: + HybridVerifyHandle() : HybridObject(TAG) {} + ~HybridVerifyHandle(); + + public: + void init(const std::string& algorithm) override; + void update(const std::shared_ptr& data) override; + bool verify(const std::shared_ptr& keyHandle, const std::shared_ptr& signature, + std::optional padding, std::optional saltLength, std::optional dsaEncoding) override; + + private: + EVP_MD_CTX* md_ctx = nullptr; + const EVP_MD* md = nullptr; + std::string algorithm_name; + // Buffer for accumulating data for one-shot verification (Ed25519/Ed448) + std::vector data_buffer; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/sign/SignUtils.hpp b/packages/react-native-quick-crypto/cpp/sign/SignUtils.hpp new file mode 100644 index 00000000..cdebea94 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/sign/SignUtils.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace margelo::nitro::crypto { + +enum DSASigEnc { + kSigEncDER = 0, + kSigEncP1363 = 1, +}; + +inline const EVP_MD* getDigestByName(const std::string& algorithm) { + if (algorithm == "SHA1" || algorithm == "sha1" || algorithm == "SHA-1" || algorithm == "sha-1") { + return EVP_sha1(); + } else if (algorithm == "SHA224" || algorithm == "sha224" || algorithm == "SHA-224" || algorithm == "sha-224") { + return EVP_sha224(); + } else if (algorithm == "SHA256" || algorithm == "sha256" || algorithm == "SHA-256" || algorithm == "sha-256") { + return EVP_sha256(); + } else if (algorithm == "SHA384" || algorithm == "sha384" || algorithm == "SHA-384" || algorithm == "sha-384") { + return EVP_sha384(); + } else if (algorithm == "SHA512" || algorithm == "sha512" || algorithm == "SHA-512" || algorithm == "sha-512") { + return EVP_sha512(); + } else if (algorithm == "SHA3-224" || algorithm == "sha3-224") { + return EVP_sha3_224(); + } else if (algorithm == "SHA3-256" || algorithm == "sha3-256") { + return EVP_sha3_256(); + } else if (algorithm == "SHA3-384" || algorithm == "sha3-384") { + return EVP_sha3_384(); + } else if (algorithm == "SHA3-512" || algorithm == "sha3-512") { + return EVP_sha3_512(); + } + throw std::runtime_error("Unsupported hash algorithm: " + algorithm); +} + +inline unsigned int getBytesOfRS(EVP_PKEY* pkey) { + int bits; + int base_id = EVP_PKEY_base_id(pkey); + + if (base_id == EVP_PKEY_DSA) { + const DSA* dsa_key = EVP_PKEY_get0_DSA(pkey); + bits = BN_num_bits(DSA_get0_q(dsa_key)); + } else if (base_id == EVP_PKEY_EC) { + const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey); + const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key); + bits = EC_GROUP_order_bits(ec_group); + } else { + return 0; + } + + return (bits + 7) / 8; +} + +inline bool convertSignatureToP1363(const unsigned char* sig_data, size_t sig_len, unsigned char* out, size_t n) { + ECDSA_SIG* asn1_sig = d2i_ECDSA_SIG(nullptr, &sig_data, sig_len); + if (!asn1_sig) + return false; + + const BIGNUM* pr = ECDSA_SIG_get0_r(asn1_sig); + const BIGNUM* ps = ECDSA_SIG_get0_s(asn1_sig); + + bool success = BN_bn2binpad(pr, out, static_cast(n)) > 0 && BN_bn2binpad(ps, out + n, static_cast(n)) > 0; + ECDSA_SIG_free(asn1_sig); + return success; +} + +inline std::unique_ptr convertSignatureToDER(const unsigned char* sig_data, size_t sig_len, size_t n, size_t* out_len) { + if (sig_len != 2 * n) { + return nullptr; + } + + ECDSA_SIG* asn1_sig = ECDSA_SIG_new(); + if (!asn1_sig) + return nullptr; + + BIGNUM* r = BN_bin2bn(sig_data, static_cast(n), nullptr); + BIGNUM* s = BN_bin2bn(sig_data + n, static_cast(n), nullptr); + + if (!r || !s || !ECDSA_SIG_set0(asn1_sig, r, s)) { + if (r) + BN_free(r); + if (s) + BN_free(s); + ECDSA_SIG_free(asn1_sig); + return nullptr; + } + + int der_len = i2d_ECDSA_SIG(asn1_sig, nullptr); + if (der_len <= 0) { + ECDSA_SIG_free(asn1_sig); + return nullptr; + } + + auto der_buf = std::make_unique(der_len); + unsigned char* der_ptr = der_buf.get(); + i2d_ECDSA_SIG(asn1_sig, &der_ptr); + + ECDSA_SIG_free(asn1_sig); + *out_len = der_len; + return der_buf; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 9dbcfc97..4c0a0edd 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -19,7 +19,9 @@ "Pbkdf2": { "cpp": "HybridPbkdf2" }, "Random": { "cpp": "HybridRandom" }, "RsaCipher": { "cpp": "HybridRsaCipher" }, - "RsaKeyPair": { "cpp": "HybridRsaKeyPair" } + "RsaKeyPair": { "cpp": "HybridRsaKeyPair" }, + "SignHandle": { "cpp": "HybridSignHandle" }, + "VerifyHandle": { "cpp": "HybridVerifyHandle" } }, "ignorePaths": ["node_modules", "lib"] } diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake index ebf152eb..881e0b40 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake @@ -39,6 +39,8 @@ target_sources( ../nitrogen/generated/shared/c++/HybridRandomSpec.cpp ../nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp ../nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.cpp + ../nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp + ../nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp # Android-specific Nitrogen C++ sources ) diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp index 7c8c28e4..2e991d8a 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -27,6 +27,8 @@ #include "HybridRandom.hpp" #include "HybridRsaCipher.hpp" #include "HybridRsaKeyPair.hpp" +#include "HybridSignHandle.hpp" +#include "HybridVerifyHandle.hpp" namespace margelo::nitro::crypto { @@ -148,6 +150,24 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "SignHandle", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridSignHandle\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); + HybridObjectRegistry::registerHybridObjectConstructor( + "VerifyHandle", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridVerifyHandle\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); }); } diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm index 63dda86c..8086bccf 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -22,6 +22,8 @@ #include "HybridRandom.hpp" #include "HybridRsaCipher.hpp" #include "HybridRsaKeyPair.hpp" +#include "HybridSignHandle.hpp" +#include "HybridVerifyHandle.hpp" @interface QuickCryptoAutolinking : NSObject @end @@ -140,6 +142,24 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "SignHandle", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridSignHandle\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); + HybridObjectRegistry::registerHybridObjectConstructor( + "VerifyHandle", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridVerifyHandle\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); } @end diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp new file mode 100644 index 00000000..edeb9be9 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp @@ -0,0 +1,23 @@ +/// +/// HybridSignHandleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridSignHandleSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridSignHandleSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("init", &HybridSignHandleSpec::init); + prototype.registerHybridMethod("update", &HybridSignHandleSpec::update); + prototype.registerHybridMethod("sign", &HybridSignHandleSpec::sign); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSignHandleSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSignHandleSpec.hpp new file mode 100644 index 00000000..5878becc --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridSignHandleSpec.hpp @@ -0,0 +1,71 @@ +/// +/// HybridSignHandleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `HybridKeyObjectHandleSpec` to properly resolve imports. +namespace margelo::nitro::crypto { class HybridKeyObjectHandleSpec; } + +#include +#include +#include +#include "HybridKeyObjectHandleSpec.hpp" +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `SignHandle` + * Inherit this class to create instances of `HybridSignHandleSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridSignHandle: public HybridSignHandleSpec { + * public: + * HybridSignHandle(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridSignHandleSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridSignHandleSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridSignHandleSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual void init(const std::string& algorithm) = 0; + virtual void update(const std::shared_ptr& data) = 0; + virtual std::shared_ptr sign(const std::shared_ptr& keyHandle, std::optional padding, std::optional saltLength, std::optional dsaEncoding) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "SignHandle"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp new file mode 100644 index 00000000..cc2c6b84 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp @@ -0,0 +1,23 @@ +/// +/// HybridVerifyHandleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridVerifyHandleSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridVerifyHandleSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("init", &HybridVerifyHandleSpec::init); + prototype.registerHybridMethod("update", &HybridVerifyHandleSpec::update); + prototype.registerHybridMethod("verify", &HybridVerifyHandleSpec::verify); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridVerifyHandleSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridVerifyHandleSpec.hpp new file mode 100644 index 00000000..0ff331b6 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridVerifyHandleSpec.hpp @@ -0,0 +1,71 @@ +/// +/// HybridVerifyHandleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } +// Forward declaration of `HybridKeyObjectHandleSpec` to properly resolve imports. +namespace margelo::nitro::crypto { class HybridKeyObjectHandleSpec; } + +#include +#include +#include +#include "HybridKeyObjectHandleSpec.hpp" +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `VerifyHandle` + * Inherit this class to create instances of `HybridVerifyHandleSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridVerifyHandle: public HybridVerifyHandleSpec { + * public: + * HybridVerifyHandle(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridVerifyHandleSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridVerifyHandleSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridVerifyHandleSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual void init(const std::string& algorithm) = 0; + virtual void update(const std::shared_ptr& data) = 0; + virtual bool verify(const std::shared_ptr& keyHandle, const std::shared_ptr& signature, std::optional padding, std::optional saltLength, std::optional dsaEncoding) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "VerifyHandle"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/constants.ts b/packages/react-native-quick-crypto/src/constants.ts new file mode 100644 index 00000000..0c8c8fac --- /dev/null +++ b/packages/react-native-quick-crypto/src/constants.ts @@ -0,0 +1,32 @@ +export const constants = { + // RSA Padding + RSA_PKCS1_PADDING: 1, + RSA_NO_PADDING: 3, + RSA_PKCS1_OAEP_PADDING: 4, + RSA_X931_PADDING: 5, + RSA_PKCS1_PSS_PADDING: 6, + + // RSA PSS Salt Length + RSA_PSS_SALTLEN_DIGEST: -1, + RSA_PSS_SALTLEN_MAX_SIGN: -2, + RSA_PSS_SALTLEN_AUTO: -2, + + // Point Conversion + POINT_CONVERSION_COMPRESSED: 2, + POINT_CONVERSION_UNCOMPRESSED: 4, + POINT_CONVERSION_HYBRID: 6, + + // DH Check + DH_CHECK_P_NOT_PRIME: 1, + DH_CHECK_P_NOT_SAFE_PRIME: 2, + DH_UNABLE_TO_CHECK_GENERATOR: 4, + DH_NOT_SUITABLE_GENERATOR: 8, + + // OpenSSL Version (3.0.0 = 0x30000000) + OPENSSL_VERSION_NUMBER: 0x30000000, +} as const; + +export const defaultCoreCipherList = + 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256'; +export const defaultCipherList = + 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256'; diff --git a/packages/react-native-quick-crypto/src/ec.ts b/packages/react-native-quick-crypto/src/ec.ts index 287a7665..0b3cf76c 100644 --- a/packages/react-native-quick-crypto/src/ec.ts +++ b/packages/react-native-quick-crypto/src/ec.ts @@ -17,6 +17,8 @@ import type { JWK, ImportFormat, NamedCurve, + GenerateKeyPairOptions, + KeyPairGenConfig, } from './utils/types'; import { bufferLikeToArrayBuffer, @@ -29,6 +31,7 @@ import { KeyEncoding, KFormatType, } from './utils'; +import { Buffer } from 'buffer'; export class Ec { native: EcKeyPair; @@ -251,8 +254,8 @@ export function ecImportKey( keyObject = KeyObject.createKeyObject( expectedKeyType, keyBuffer, - 'der', - format as 'spki' | 'pkcs8', + KFormatType.DER, + format === 'spki' ? KeyEncoding.SPKI : KeyEncoding.PKCS8, ); } @@ -505,8 +508,8 @@ export async function ec_generateKeyPair( const pub = KeyObject.createKeyObject( 'public', publicKeyData, - 'der', - 'spki', + KFormatType.DER, + KeyEncoding.SPKI, ) as PublicKeyObject; const publicKey = new CryptoKey( pub, @@ -516,12 +519,11 @@ export async function ec_generateKeyPair( ); // All keys are now exported in PKCS8 format for consistency - const privateEncoding = 'pkcs8'; const priv = KeyObject.createKeyObject( 'private', privateKeyData, - 'der', - privateEncoding as 'pkcs8' | 'spki' | 'sec1', + KFormatType.DER, + KeyEncoding.PKCS8, ) as PrivateKeyObject; const privateKey = new CryptoKey( priv, @@ -532,3 +534,89 @@ export async function ec_generateKeyPair( return { publicKey, privateKey }; } + +export async function ec_generateKeyPairNode( + options: GenerateKeyPairOptions | undefined, + encoding: KeyPairGenConfig, +): Promise<{ + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +}> { + if (!options) { + throw new Error('Options are required for EC key generation'); + } + + const { namedCurve } = options as { namedCurve?: string }; + + if ( + !namedCurve || + !kNamedCurveAliases[namedCurve as keyof typeof kNamedCurveAliases] + ) { + 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; + + const { + publicFormat, + publicType, + privateFormat, + privateType, + cipher, + passphrase, + } = encoding; + + let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + + if (publicFormat === -1) { + publicKey = pubCryptoKey.keyObject as PublicKeyObject; + } 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, + ); + if (format === KFormatType.PEM) { + publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + publicKey = exported; + } + } + + if (privateFormat === -1) { + privateKey = privCryptoKey.keyObject as PrivateKeyObject; + } else { + const format = + privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const keyEncoding = + privateType === KeyEncoding.PKCS8 + ? KeyEncoding.PKCS8 + : privateType === KeyEncoding.SEC1 + ? KeyEncoding.SEC1 + : KeyEncoding.PKCS8; + const exported = privCryptoKey.keyObject.handle.exportKey( + format, + keyEncoding, + cipher, + passphrase, + ); + if (format === KFormatType.PEM) { + privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + privateKey = exported; + } + } + + return { publicKey, privateKey }; +} diff --git a/packages/react-native-quick-crypto/src/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index 00189a00..2ebd4ad0 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -1,19 +1,34 @@ import { NitroModules } from 'react-native-nitro-modules'; import { Buffer } from '@craftzdog/react-native-buffer'; import type { AsymmetricKeyObject, PrivateKeyObject } from './keys'; +import { + CryptoKey, + KeyObject, + PublicKeyObject, + PrivateKeyObject as PrivateKeyObjectClass, +} from './keys'; import type { EdKeyPair } from './specs/edKeyPair.nitro'; import type { BinaryLike, CFRGKeyPairType, + CryptoKeyPair, DiffieHellmanCallback, DiffieHellmanOptions, GenerateKeyPairCallback, GenerateKeyPairReturn, Hex, KeyPairGenConfig, - KeyPairType, + KeyUsage, + SubtleAlgorithm, +} from './utils'; +import { + binaryLikeToArrayBuffer as toAB, + hasAnyNotIn, + lazyDOMException, + getUsagesUnion, + KFormatType, + KeyEncoding, } from './utils'; -import { binaryLikeToArrayBuffer as toAB } from './utils'; export class Ed { type: CFRGKeyPairType; @@ -80,23 +95,23 @@ export class Ed { } async generateKeyPair(): Promise { - this.native.generateKeyPair( - this.config.publicFormat || (-1 as number), - this.config.publicType || (-1 as number), - this.config.privateFormat || (-1 as number), - this.config.privateType || (-1 as number), - this.config.cipher as string, + await this.native.generateKeyPair( + this.config.publicFormat ?? -1, + this.config.publicType ?? -1, + this.config.privateFormat ?? -1, + this.config.privateType ?? -1, + this.config.cipher, this.config.passphrase as ArrayBuffer, ); } generateKeyPairSync(): void { this.native.generateKeyPairSync( - this.config.publicFormat || (-1 as number), - this.config.publicType || (-1 as number), - this.config.privateFormat || (-1 as number), - this.config.privateType || (-1 as number), - this.config.cipher as string, + this.config.publicFormat ?? -1, + this.config.publicType ?? -1, + this.config.privateFormat ?? -1, + this.config.privateType ?? -1, + this.config.cipher, this.config.passphrase as ArrayBuffer, ); } @@ -170,12 +185,39 @@ export function diffieHellman( // Node API export function ed_generateKeyPair( isAsync: boolean, - type: KeyPairType, + type: CFRGKeyPairType, encoding: KeyPairGenConfig, callback: GenerateKeyPairCallback | undefined, ): GenerateKeyPairReturn | void { const ed = new Ed(type, encoding); + // Helper to convert keys to proper output format + const formatKeys = (): { + publicKey: string | ArrayBuffer; + privateKey: string | ArrayBuffer; + } => { + const publicKeyRaw = ed.getPublicKey(); + const privateKeyRaw = ed.getPrivateKey(); + + // Check if PEM format was requested (KFormatType.PEM = 1) + const isPemPublic = encoding.publicFormat === KFormatType.PEM; + const isPemPrivate = encoding.privateFormat === KFormatType.PEM; + + // Convert ArrayBuffer to string for PEM format + const arrayBufferToString = (ab: ArrayBuffer): string => { + return Buffer.from(new Uint8Array(ab)).toString('utf-8'); + }; + + const publicKey = isPemPublic + ? arrayBufferToString(publicKeyRaw) + : publicKeyRaw; + const privateKey = isPemPrivate + ? arrayBufferToString(privateKeyRaw) + : privateKeyRaw; + + return { publicKey, privateKey }; + }; + // Async path if (isAsync) { if (!callback) { @@ -184,7 +226,8 @@ export function ed_generateKeyPair( } ed.generateKeyPair() .then(() => { - callback(undefined, ed.getPublicKey(), ed.getPrivateKey()); + const { publicKey, privateKey } = formatKeys(); + callback(undefined, publicKey, privateKey); }) .catch(err => { callback(err, undefined, undefined); @@ -200,11 +243,15 @@ export function ed_generateKeyPair( err = e instanceof Error ? e : new Error(String(e)); } + const { publicKey, privateKey } = err + ? { publicKey: undefined, privateKey: undefined } + : formatKeys(); + if (callback) { - callback(err, ed.getPublicKey(), ed.getPrivateKey()); + callback(err, publicKey, privateKey); return; } - return [err, ed.getPublicKey(), ed.getPrivateKey()]; + return [err, publicKey, privateKey]; } function checkDiffieHellmanOptions(options: DiffieHellmanOptions): void { @@ -254,3 +301,63 @@ function checkDiffieHellmanOptions(options: DiffieHellmanOptions): void { ); } } + +export async function ed_generateKeyPairWebCrypto( + type: 'ed25519' | 'ed448', + extractable: boolean, + keyUsages: KeyUsage[], +): Promise { + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException(`Unsupported key usage for ${type}`, 'SyntaxError'); + } + + const publicUsages = getUsagesUnion(keyUsages, 'verify'); + const privateUsages = getUsagesUnion(keyUsages, 'sign'); + + if (privateUsages.length === 0) { + throw lazyDOMException('Usages cannot be empty', 'SyntaxError'); + } + + // Request DER-encoded SPKI for public key, PKCS8 for private key + const config = { + publicFormat: KFormatType.DER, + publicType: KeyEncoding.SPKI, + privateFormat: KFormatType.DER, + privateType: KeyEncoding.PKCS8, + }; + const ed = new Ed(type, config); + await ed.generateKeyPair(); + + const algorithmName = type === 'ed25519' ? 'Ed25519' : 'Ed448'; + + const publicKeyData = ed.getPublicKey(); + const privateKeyData = ed.getPrivateKey(); + + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + const publicKey = new CryptoKey( + pub, + { name: algorithmName } as SubtleAlgorithm, + publicUsages, + true, + ); + + const priv = KeyObject.createKeyObject( + 'private', + privateKeyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObjectClass; + const privateKey = new CryptoKey( + priv, + { name: algorithmName } as SubtleAlgorithm, + privateUsages, + extractable, + ); + + return { publicKey, privateKey }; +} diff --git a/packages/react-native-quick-crypto/src/index.ts b/packages/react-native-quick-crypto/src/index.ts index 97bb4dbf..0115af89 100644 --- a/packages/react-native-quick-crypto/src/index.ts +++ b/packages/react-native-quick-crypto/src/index.ts @@ -10,6 +10,7 @@ import { hashExports as hash } from './hash'; import { hmacExports as hmac } from './hmac'; import * as pbkdf2 from './pbkdf2'; import * as random from './random'; +import { constants } from './constants'; // utils import import * as utils from './utils'; @@ -30,6 +31,7 @@ const QuickCrypto = { ...random, ...utils, ...subtle, + constants, }; /** @@ -60,6 +62,7 @@ export * from './random'; export * from './utils'; export * from './subtle'; export { subtle, isCryptoKeyPair } from './subtle'; +export { constants } from './constants'; // Additional exports for CommonJS compatibility module.exports = QuickCrypto; diff --git a/packages/react-native-quick-crypto/src/keys/classes.ts b/packages/react-native-quick-crypto/src/keys/classes.ts index a3771820..551c5727 100644 --- a/packages/react-native-quick-crypto/src/keys/classes.ts +++ b/packages/react-native-quick-crypto/src/keys/classes.ts @@ -119,8 +119,8 @@ export class KeyObject { static createKeyObject( type: string, key: ArrayBuffer, - format?: 'der' | 'pem', - encoding?: 'pkcs8' | 'spki' | 'sec1', + format?: KFormatType, + encoding?: KeyEncoding, ): KeyObject { if (type !== 'secret' && type !== 'public' && type !== 'private') throw new Error(`invalid KeyObject type: ${type}`); @@ -143,22 +143,9 @@ export class KeyObject { throw new Error('invalid key type'); } - // If format and encoding are explicitly provided, use them - if ( - format && - encoding && - (keyType === KeyType.PUBLIC || keyType === KeyType.PRIVATE) - ) { - const kFormat = format === 'der' ? KFormatType.DER : KFormatType.PEM; - const kEncoding = - encoding === 'spki' - ? KeyEncoding.SPKI - : encoding === 'pkcs8' - ? KeyEncoding.PKCS8 - : encoding === 'sec1' - ? KeyEncoding.SEC1 - : KeyEncoding.SEC1; - handle.init(keyType, key, kFormat, kEncoding); + // If format is provided, use it (encoding is optional) + if (format !== undefined) { + handle.init(keyType, key, format, encoding); } else { handle.init(keyType, key); } diff --git a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts index 8078f69e..d886bca9 100644 --- a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts +++ b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts @@ -1,4 +1,6 @@ import { ed_generateKeyPair } from '../ed'; +import { rsa_generateKeyPairNode } from '../rsa'; +import { ec_generateKeyPairNode } from '../ec'; import { kEmptyObject, validateFunction, @@ -132,14 +134,48 @@ function internalGenerateKeyPair( case 'x25519': case 'x448': return ed_generateKeyPair(isAsync, type, encoding, callback); - default: - // fall through + case 'rsa': + case 'rsa-pss': + case 'dsa': + case 'ec': + break; + default: { + const err = new Error(` + Invalid Argument options: '${type}' scheme not supported for + generateKeyPair(). Currently not all encryption methods are supported in + this library. Check docs/implementation_coverage.md for status. + `); + return [err, undefined, undefined]; + } } - const err = new Error(` - Invalid Argument options: '${type}' scheme not supported for - generateKeyPair(). Currently not all encryption methods are supported in - this library. Check docs/implementation_coverage.md for status. - `); - return [err, undefined, undefined]; + 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}`); + } + 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'); + } } diff --git a/packages/react-native-quick-crypto/src/keys/index.ts b/packages/react-native-quick-crypto/src/keys/index.ts index 1cfc122b..4d10bfe3 100644 --- a/packages/react-native-quick-crypto/src/keys/index.ts +++ b/packages/react-native-quick-crypto/src/keys/index.ts @@ -7,7 +7,8 @@ import { PrivateKeyObject, } from './classes'; import { generateKeyPair, generateKeyPairSync } from './generateKeyPair'; -// import { sign, verify } from './signVerify'; +import { createSign, createVerify, Sign, Verify } from './signVerify'; +import { publicEncrypt, publicDecrypt } from './publicCipher'; import { isCryptoKey, parseKeyEncoding, @@ -15,38 +16,232 @@ import { parsePublicKeyEncoding, } from './utils'; import type { BinaryLike } from '../utils'; -import { binaryLikeToArrayBuffer as toAB } from '../utils'; +import { + binaryLikeToArrayBuffer as toAB, + isStringOrBuffer, + KFormatType, + KeyEncoding, +} from '../utils'; +import { randomBytes } from '../random'; + +interface KeyInputObject { + key: BinaryLike | KeyObject | CryptoKey; + format?: 'pem' | 'der' | 'jwk'; + type?: 'pkcs1' | 'pkcs8' | 'spki' | 'sec1'; + passphrase?: BinaryLike; + encoding?: BufferEncoding; +} + +type KeyInput = BinaryLike | KeyInputObject | KeyObject | CryptoKey; function createSecretKey(key: BinaryLike): SecretKeyObject { const keyBuffer = toAB(key); return KeyObject.createKeyObject('secret', keyBuffer) as SecretKeyObject; } +function prepareAsymmetricKey( + key: KeyInput, + isPublic: boolean, +): { + data: ArrayBuffer; + format?: 'pem' | 'der'; + type?: 'pkcs1' | 'pkcs8' | 'spki' | 'sec1'; +} { + if (key instanceof KeyObject) { + if (isPublic) { + // createPublicKey can accept either a public key or extract public from private + if (key.type === 'secret') { + throw new Error('Cannot create public key from secret key'); + } + // Export as SPKI (public key format) - works for both public and private keys + const exported = key.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI); + return { data: exported, format: 'der', type: 'spki' }; + } else { + // createPrivateKey requires a private key + if (key.type !== 'private') { + throw new Error('Key must be a private key'); + } + const exported = key.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8); + return { data: exported, format: 'der', type: 'pkcs8' }; + } + } + + if (isCryptoKey(key)) { + const cryptoKey = key as CryptoKey; + return prepareAsymmetricKey(cryptoKey.keyObject, isPublic); + } + + if (isStringOrBuffer(key)) { + // Detect PEM format from string content + const isPem = typeof key === 'string' && key.includes('-----BEGIN'); + return { data: toAB(key), format: isPem ? 'pem' : undefined }; + } + + if (typeof key === 'object' && 'key' in key) { + const keyObj = key as KeyInputObject; + const { key: data, format, type } = keyObj; + + if (data instanceof KeyObject) { + return prepareAsymmetricKey(data, isPublic); + } + + if (isCryptoKey(data)) { + return prepareAsymmetricKey((data as CryptoKey).keyObject, isPublic); + } + + if (!isStringOrBuffer(data)) { + throw new Error('Invalid key data type'); + } + + // For PEM format with string data, convert to ArrayBuffer + if ( + (format === 'pem' || + (typeof data === 'string' && data.includes('-----BEGIN'))) && + typeof data === 'string' + ) { + return { data: toAB(data), format: 'pem', type }; + } + + // Filter out 'jwk' format - only 'pem' and 'der' are supported here + const filteredFormat = format === 'jwk' ? undefined : format; + return { data: toAB(data), format: filteredFormat, type }; + } + + throw new Error('Invalid key input'); +} + +function createPublicKey(key: KeyInput): PublicKeyObject { + const { data, format, type } = prepareAsymmetricKey(key, true); + + // Map format string to KFormatType enum + let kFormat: KFormatType | undefined; + if (format === 'pem') kFormat = KFormatType.PEM; + else if (format === 'der') kFormat = KFormatType.DER; + + // Map type string to KeyEncoding enum + let kType: KeyEncoding | undefined; + if (type === 'spki') kType = KeyEncoding.SPKI; + else if (type === 'pkcs1') kType = KeyEncoding.PKCS1; + + return KeyObject.createKeyObject( + 'public', + data, + kFormat, + kType, + ) as PublicKeyObject; +} + +function createPrivateKey(key: KeyInput): PrivateKeyObject { + const { data, format, type } = prepareAsymmetricKey(key, false); + + // Map format string to KFormatType enum + let kFormat: KFormatType | undefined; + if (format === 'pem') kFormat = KFormatType.PEM; + else if (format === 'der') kFormat = KFormatType.DER; + + // Map type string to KeyEncoding enum + let kType: KeyEncoding | undefined; + if (type === 'pkcs8') kType = KeyEncoding.PKCS8; + else if (type === 'pkcs1') kType = KeyEncoding.PKCS1; + else if (type === 'sec1') kType = KeyEncoding.SEC1; + + return KeyObject.createKeyObject( + 'private', + data, + kFormat, + kType, + ) as PrivateKeyObject; +} + +export interface GenerateKeyOptions { + length: number; +} + +function generateKeySync( + type: 'aes' | 'hmac', + options: GenerateKeyOptions, +): SecretKeyObject { + if (typeof type !== 'string') { + throw new TypeError('The "type" argument must be a string'); + } + if (typeof options !== 'object' || options === null) { + throw new TypeError('The "options" argument must be an object'); + } + + const { length } = options; + + if (typeof length !== 'number' || !Number.isInteger(length)) { + throw new TypeError('The "options.length" property must be an integer'); + } + + switch (type) { + case 'hmac': + if (length < 8 || length > 2 ** 31 - 1) { + throw new RangeError( + 'The "options.length" property must be >= 8 and <= 2147483647', + ); + } + break; + case 'aes': + if (length !== 128 && length !== 192 && length !== 256) { + throw new RangeError( + 'The "options.length" property must be 128, 192, or 256', + ); + } + break; + default: + throw new TypeError( + `The "type" argument must be 'aes' or 'hmac'. Received '${type}'`, + ); + } + + const keyBytes = length / 8; + const keyMaterial = randomBytes(keyBytes); + return createSecretKey(keyMaterial); +} + +function generateKey( + type: 'aes' | 'hmac', + options: GenerateKeyOptions, + callback: (err: Error | null, key?: SecretKeyObject) => void, +): void { + if (typeof callback !== 'function') { + throw new TypeError('The "callback" argument must be a function'); + } + + try { + const key = generateKeySync(type, options); + process.nextTick(callback, null, key); + } catch (err) { + process.nextTick(callback, err as Error); + } +} + export { // Node Public API createSecretKey, - // createPublicKey, - // createPrivateKey, + createPublicKey, + createPrivateKey, CryptoKey, + generateKey, + generateKeySync, generateKeyPair, generateKeyPairSync, AsymmetricKeyObject, KeyObject, - // InternalCryptoKey, - // sign, - // verify, + createSign, + createVerify, + Sign, + Verify, + publicEncrypt, + publicDecrypt, // Node Internal API parsePublicKeyEncoding, parsePrivateKeyEncoding, parseKeyEncoding, - // preparePrivateKey, - // preparePublicOrPrivateKey, - // prepareSecretKey, SecretKeyObject, PublicKeyObject, PrivateKeyObject, - // isKeyObject, isCryptoKey, - // importGenericSecretKey, }; diff --git a/packages/react-native-quick-crypto/src/keys/publicCipher.ts b/packages/react-native-quick-crypto/src/keys/publicCipher.ts new file mode 100644 index 00000000..079a11a2 --- /dev/null +++ b/packages/react-native-quick-crypto/src/keys/publicCipher.ts @@ -0,0 +1,121 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { RsaCipher } from '../specs/rsaCipher.nitro'; +import type { BinaryLike } from '../utils'; +import { + binaryLikeToArrayBuffer as toAB, + isStringOrBuffer, + KFormatType, + KeyEncoding, +} from '../utils'; +import { isCryptoKey } from './utils'; +import { KeyObject, CryptoKey } from './classes'; + +interface PublicCipherOptions { + key: BinaryLike | KeyObject | CryptoKey; + padding?: number; + oaepHash?: string; + oaepLabel?: BinaryLike; +} + +type PublicCipherInput = + | BinaryLike + | KeyObject + | CryptoKey + | PublicCipherOptions; + +function preparePublicCipherKey( + key: PublicCipherInput, + isEncrypt: boolean, +): { + keyHandle: KeyObject; + padding?: number; + oaepHash?: string; + oaepLabel?: ArrayBuffer; +} { + let keyObj: KeyObject; + let padding: number | undefined; + let oaepHash: string | undefined; + let oaepLabel: ArrayBuffer | undefined; + + if (key instanceof KeyObject) { + if (isEncrypt && key.type !== 'public') { + throw new Error('publicEncrypt requires a public key'); + } + if (!isEncrypt && key.type !== 'private') { + throw new Error('publicDecrypt requires a private key'); + } + keyObj = key; + } else if (isCryptoKey(key)) { + const cryptoKey = key as CryptoKey; + keyObj = cryptoKey.keyObject; + } else if (isStringOrBuffer(key)) { + const data = toAB(key); + // Detect if it's PEM format (contains PEM headers) or DER binary + const isPem = typeof key === 'string' && key.includes('-----BEGIN'); + keyObj = KeyObject.createKeyObject( + isEncrypt ? 'public' : 'private', + data, + isPem ? KFormatType.PEM : KFormatType.DER, + isEncrypt ? KeyEncoding.SPKI : KeyEncoding.PKCS8, + ); + } else if (typeof key === 'object' && 'key' in key) { + const options = key as PublicCipherOptions; + const result = preparePublicCipherKey(options.key, isEncrypt); + keyObj = result.keyHandle; + padding = options.padding; + oaepHash = options.oaepHash; + if (options.oaepLabel) { + oaepLabel = toAB(options.oaepLabel); + } + } else { + throw new Error('Invalid key input'); + } + + return { keyHandle: keyObj, padding, oaepHash, oaepLabel }; +} + +export function publicEncrypt( + key: PublicCipherInput, + buffer: BinaryLike, +): Buffer { + const { keyHandle, oaepHash, oaepLabel } = preparePublicCipherKey(key, true); + + const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher'); + const data = toAB(buffer); + const hashAlgorithm = oaepHash || 'SHA-256'; + + try { + const encrypted = rsaCipher.encrypt( + keyHandle.handle, + data, + hashAlgorithm, + oaepLabel, + ); + return Buffer.from(encrypted); + } catch (error) { + throw new Error(`publicEncrypt failed: ${(error as Error).message}`); + } +} + +export function publicDecrypt( + key: PublicCipherInput, + buffer: BinaryLike, +): Buffer { + const { keyHandle, oaepHash, oaepLabel } = preparePublicCipherKey(key, false); + + const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher'); + const data = toAB(buffer); + const hashAlgorithm = oaepHash || 'SHA-256'; + + try { + const decrypted = rsaCipher.decrypt( + keyHandle.handle, + data, + hashAlgorithm, + oaepLabel, + ); + return Buffer.from(decrypted); + } catch (error) { + throw new Error(`publicDecrypt failed: ${(error as Error).message}`); + } +} diff --git a/packages/react-native-quick-crypto/src/keys/signVerify.ts b/packages/react-native-quick-crypto/src/keys/signVerify.ts index 28dd0914..3b17eb81 100644 --- a/packages/react-native-quick-crypto/src/keys/signVerify.ts +++ b/packages/react-native-quick-crypto/src/keys/signVerify.ts @@ -1,39 +1,239 @@ -// import { KeyObject, PublicKeyObject, PrivateKeyObject } from '.'; -// import { ed25519 } from '../ed25519'; -// import type { -// BinaryLike, -// BinaryLikeNode, -// SignCallback, -// VerifyCallback, -// } from '../utils'; - -// export function sign( -// algorithm: string | null | undefined, -// data: BinaryLike, -// key: BinaryLikeNode | KeyObject, -// callback: SignCallback, -// ): ArrayBuffer { -// console.log('sign ', algorithm, data, key, callback); -// return new ArrayBuffer(32); -// } - -// export function verify( -// algorithm: string | null | undefined, -// data: BinaryLike, -// key: BinaryLikeNode | KeyObject, -// signature: ArrayBuffer, -// callback: VerifyCallback, -// ): boolean { -// if (!algorithm) { -// if (key instanceof PublicKeyObject) { -// switch (key.asymmetricKeyType) { -// case 'ed25519': -// case 'ed448': -// case 'x25519': -// case 'x448': { -// return ed25519.verify(signature, data, key); -// } -// } -// } -// throw new Error('Verify not implemented', algorithm, data, key, signature, callback); -// } +import { Buffer } from '@craftzdog/react-native-buffer'; +import { NitroModules } from 'react-native-nitro-modules'; +import type { + SignHandle as SignHandleSpec, + VerifyHandle as VerifyHandleSpec, +} from '../specs/sign.nitro'; +import { KeyObject, CryptoKey } from './classes'; +import { isCryptoKey } from './utils'; +import type { BinaryLike } from '../utils'; +import { + binaryLikeToArrayBuffer as toAB, + isStringOrBuffer, + KFormatType, + KeyEncoding, +} from '../utils'; + +type KeyInput = BinaryLike | KeyObject | CryptoKey | KeyInputObject; + +interface KeyInputObject { + key: BinaryLike | KeyObject | CryptoKey; + format?: 'pem' | 'der'; + type?: 'pkcs1' | 'pkcs8' | 'spki' | 'sec1'; + passphrase?: BinaryLike; + padding?: number; + saltLength?: number; + dsaEncoding?: 'der' | 'ieee-p1363'; +} + +interface SignOptions { + padding?: number; + saltLength?: number; + dsaEncoding?: 'der' | 'ieee-p1363'; +} + +interface PreparedKey { + keyObject: KeyObject; + options?: SignOptions; +} + +function prepareKey(key: KeyInput, isPublic: boolean): PreparedKey { + // Already a KeyObject + if (key instanceof KeyObject) { + if (isPublic) { + if (key.type === 'secret') { + throw new Error('Cannot use secret key for signature verification'); + } + } else { + if (key.type !== 'private') { + throw new Error('Key must be a private key for signing'); + } + } + return { keyObject: key }; + } + + // CryptoKey - extract KeyObject + if (isCryptoKey(key)) { + const cryptoKey = key as CryptoKey; + return prepareKey(cryptoKey.keyObject, isPublic); + } + + // Raw string or buffer - create KeyObject + if (isStringOrBuffer(key)) { + const isPem = typeof key === 'string' && key.includes('-----BEGIN'); + const format = isPem ? KFormatType.PEM : undefined; + const type = isPublic ? 'public' : 'private'; + const keyData = toAB(key); + const keyObject = KeyObject.createKeyObject(type, keyData, format); + return { keyObject }; + } + + // KeyInputObject with options + if (typeof key === 'object' && 'key' in key) { + const keyObj = key as KeyInputObject; + const { + key: data, + format, + type, + padding, + saltLength, + dsaEncoding, + } = keyObj; + + // Nested KeyObject + if (data instanceof KeyObject) { + return { + keyObject: data, + options: { padding, saltLength, dsaEncoding }, + }; + } + + // Nested CryptoKey + if (isCryptoKey(data)) { + return { + keyObject: (data as CryptoKey).keyObject, + options: { padding, saltLength, dsaEncoding }, + }; + } + + if (!isStringOrBuffer(data)) { + throw new Error('Invalid key data type'); + } + + // Determine format + const isPem = + format === 'pem' || + (typeof data === 'string' && data.includes('-----BEGIN')); + const kFormat = isPem + ? KFormatType.PEM + : format === 'der' + ? KFormatType.DER + : undefined; + + // Determine encoding type + let kType: KeyEncoding | undefined; + if (type === 'pkcs8') kType = KeyEncoding.PKCS8; + else if (type === 'pkcs1') kType = KeyEncoding.PKCS1; + else if (type === 'sec1') kType = KeyEncoding.SEC1; + else if (type === 'spki') kType = KeyEncoding.SPKI; + + const keyType = isPublic ? 'public' : 'private'; + // Always convert to ArrayBuffer to avoid Nitro bridge string truncation bug + const originalLength = + typeof data === 'string' ? data.length : data.byteLength; + const keyData = toAB(data); + console.log( + `[prepareKey KeyInputObject] ${keyType} key, original length: ${originalLength}, ArrayBuffer size: ${keyData.byteLength}`, + ); + const keyObject = KeyObject.createKeyObject( + keyType, + keyData, + kFormat, + kType, + ); + + return { + keyObject, + options: { padding, saltLength, dsaEncoding }, + }; + } + + throw new Error('Invalid key input'); +} + +function dsaEncodingToNumber( + dsaEncoding?: 'der' | 'ieee-p1363', +): number | undefined { + if (dsaEncoding === 'der') return 0; + if (dsaEncoding === 'ieee-p1363') return 1; + return undefined; +} + +export class Sign { + private handle: SignHandleSpec; + + constructor(algorithm: string) { + this.handle = NitroModules.createHybridObject('SignHandle'); + this.handle.init(algorithm); + } + + update(data: BinaryLike): this { + const dataBuffer = toAB(data); + this.handle.update(dataBuffer); + return this; + } + + sign(privateKey: KeyInput, outputEncoding?: BufferEncoding): Buffer; + sign(privateKey: KeyInput, outputEncoding?: BufferEncoding): Buffer | string { + if (privateKey === null || privateKey === undefined) { + throw new Error('Private key is required'); + } + + const { keyObject, options } = prepareKey(privateKey, false); + + const signature = this.handle.sign( + keyObject.handle, + options?.padding, + options?.saltLength, + dsaEncodingToNumber(options?.dsaEncoding), + ); + + const buf = Buffer.from(signature); + if (outputEncoding) { + return buf.toString(outputEncoding); + } + return buf; + } +} + +export class Verify { + private handle: VerifyHandleSpec; + + constructor(algorithm: string) { + this.handle = + NitroModules.createHybridObject('VerifyHandle'); + this.handle.init(algorithm); + } + + update(data: BinaryLike): this { + const dataBuffer = toAB(data); + this.handle.update(dataBuffer); + return this; + } + + verify( + publicKey: KeyInput, + signature: BinaryLike, + signatureEncoding?: BufferEncoding, + ): boolean { + if (publicKey === null || publicKey === undefined) { + throw new Error('Public key is required'); + } + + const { keyObject, options } = prepareKey(publicKey, true); + + // Convert signature to ArrayBuffer + let sigBuffer: ArrayBuffer; + if (signatureEncoding && typeof signature === 'string') { + sigBuffer = toAB(Buffer.from(signature, signatureEncoding)); + } else { + sigBuffer = toAB(signature); + } + + return this.handle.verify( + keyObject.handle, + sigBuffer, + options?.padding, + options?.saltLength, + dsaEncodingToNumber(options?.dsaEncoding), + ); + } +} + +export function createSign(algorithm: string): Sign { + return new Sign(algorithm); +} + +export function createVerify(algorithm: string): Verify { + return new Verify(algorithm); +} diff --git a/packages/react-native-quick-crypto/src/rsa.ts b/packages/react-native-quick-crypto/src/rsa.ts index ed75e317..fbefe2ee 100644 --- a/packages/react-native-quick-crypto/src/rsa.ts +++ b/packages/react-native-quick-crypto/src/rsa.ts @@ -10,12 +10,16 @@ import { hasAnyNotIn, lazyDOMException, normalizeHashName, + KFormatType, + KeyEncoding, } from './utils'; import type { CryptoKeyPair, KeyUsage, RsaHashedKeyGenParams, SubtleAlgorithm, + GenerateKeyPairOptions, + KeyPairGenConfig, } from './utils'; import type { RsaKeyPair } from './specs/rsaKeyPair.nitro'; @@ -174,3 +178,108 @@ export async function rsa_generateKeyPair( 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; +}> { + if (!options) { + throw new Error('Options are required for RSA key generation'); + } + + const { + modulusLength, + publicExponent, + hash = 'sha256', + } = options as { + modulusLength?: number; + publicExponent?: number; + hash?: string; + }; + + if (!modulusLength || modulusLength < 256) { + throw new Error('Invalid modulus length'); + } + + const pubExp = publicExponent || 65537; + const pubExpBytes = new Uint8Array([ + (pubExp >> 16) & 0xff, + (pubExp >> 8) & 0xff, + pubExp & 0xff, + ]); + + const algorithmName = type === 'rsa-pss' ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5'; + + 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; + + const { + publicFormat, + publicType, + privateFormat, + privateType, + cipher, + passphrase, + } = encoding; + + let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + + if (publicFormat === -1) { + publicKey = pubCryptoKey.keyObject as PublicKeyObject; + } 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, + ); + if (format === KFormatType.PEM) { + publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + publicKey = exported; + } + } + + if (privateFormat === -1) { + privateKey = privCryptoKey.keyObject as PrivateKeyObject; + } 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( + format, + keyEncoding, + cipher, + passphrase, + ); + if (format === KFormatType.PEM) { + privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + privateKey = exported; + } + } + + return { publicKey, privateKey }; +} diff --git a/packages/react-native-quick-crypto/src/specs/sign.nitro.ts b/packages/react-native-quick-crypto/src/specs/sign.nitro.ts new file mode 100644 index 00000000..c1703f88 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/sign.nitro.ts @@ -0,0 +1,31 @@ +import type { HybridObject } from 'react-native-nitro-modules'; +import type { KeyObjectHandle } from './keyObjectHandle.nitro'; + +export interface SignHandle + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + init(algorithm: string): void; + + update(data: ArrayBuffer): void; + + sign( + keyHandle: KeyObjectHandle, + padding?: number, + saltLength?: number, + dsaEncoding?: number, + ): ArrayBuffer; +} + +export interface VerifyHandle + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + init(algorithm: string): void; + + update(data: ArrayBuffer): void; + + verify( + keyHandle: KeyObjectHandle, + signature: ArrayBuffer, + padding?: number, + saltLength?: number, + dsaEncoding?: number, + ): boolean; +} diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 4026875f..a244f957 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -16,6 +16,7 @@ import type { AesGcmParams, RsaOaepParams, } from './utils'; +import { KFormatType, KeyEncoding } from './utils'; import { CryptoKey, KeyObject, @@ -38,6 +39,9 @@ import { pbkdf2DeriveBits } from './pbkdf2'; import { ecImportKey, ecdsaSignVerify, ec_generateKeyPair } from './ec'; import { rsa_generateKeyPair } from './rsa'; import { getRandomValues } from './random'; +import { createHmac } from './hmac'; +import { createSign, createVerify } from './keys/signVerify'; +import { ed_generateKeyPairWebCrypto, Ed } from './ed'; // import { pbkdf2DeriveBits } from './pbkdf2'; // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes'; // import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa'; @@ -403,6 +407,73 @@ async function aesGenerateKey( return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } +async function hmacGenerateKey( + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], +): Promise { + // Validate usages + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException('Unsupported key usage for HMAC key', 'SyntaxError'); + } + + // Get hash algorithm + const hash = algorithm.hash; + if (!hash) { + throw lazyDOMException( + 'HMAC algorithm requires a hash parameter', + 'TypeError', + ); + } + + const hashName = normalizeHashName(hash); + + // Determine key length + let length = algorithm.length; + if (length === undefined) { + // Use hash output length as default key length + switch (hashName) { + case 'SHA-1': + length = 160; + break; + case 'SHA-256': + length = 256; + break; + case 'SHA-384': + length = 384; + break; + case 'SHA-512': + length = 512; + break; + default: + length = 256; // Default to 256 bits + } + } + + if (length === 0) { + throw lazyDOMException( + 'Zero-length key is not supported', + 'OperationError', + ); + } + + // Generate random key bytes + const keyBytes = new Uint8Array(Math.ceil(length / 8)); + getRandomValues(keyBytes); + + // Create secret key + const keyObject = createSecretKey(keyBytes); + + // Construct algorithm object + const keyAlgorithm: SubtleAlgorithm = { + name: 'HMAC', + hash: hashName, + length, + }; + + return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); +} + function rsaImportKey( format: ImportFormat, data: BufferLike | JWK, @@ -458,10 +529,20 @@ function rsaImportKey( } } else if (format === 'spki') { const keyData = bufferLikeToArrayBuffer(data as BufferLike); - keyObject = KeyObject.createKeyObject('public', keyData, 'der', 'spki'); + keyObject = KeyObject.createKeyObject( + 'public', + keyData, + KFormatType.DER, + KeyEncoding.SPKI, + ); } else if (format === 'pkcs8') { const keyData = bufferLikeToArrayBuffer(data as BufferLike); - keyObject = KeyObject.createKeyObject('private', keyData, 'der', 'pkcs8'); + keyObject = KeyObject.createKeyObject( + 'private', + keyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ); } else { throw new Error(`Unsupported format for RSA import: ${format}`); } @@ -631,6 +712,62 @@ async function aesImportKey( ); } +function edImportKey( + format: ImportFormat, + data: BufferLike, + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], +): CryptoKey { + const { name } = algorithm; + + // Validate usages + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException( + `Unsupported key usage for ${name} key`, + 'SyntaxError', + ); + } + + let keyObject: KeyObject; + + if (format === 'spki') { + // Import public key + const keyData = bufferLikeToArrayBuffer(data); + keyObject = KeyObject.createKeyObject( + 'public', + keyData, + KFormatType.DER, + KeyEncoding.SPKI, + ); + } else if (format === 'pkcs8') { + // Import private key + const keyData = bufferLikeToArrayBuffer(data); + keyObject = KeyObject.createKeyObject( + 'private', + keyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ); + } else if (format === 'raw') { + // Raw format - public key only for Ed keys + const keyData = bufferLikeToArrayBuffer(data); + const handle = + NitroModules.createHybridObject('KeyObjectHandle'); + // For raw Ed keys, we need to create them differently + // Raw public keys are just the key bytes + handle.init(1, keyData); // 1 = public key type + keyObject = new PublicKeyObject(handle); + } else { + throw lazyDOMException( + `Unsupported format for ${name} import: ${format}`, + 'NotSupportedError', + ); + } + + return new CryptoKey(keyObject, { name }, keyUsages, extractable); +} + const exportKeySpki = async ( key: CryptoKey, ): Promise => { @@ -651,6 +788,16 @@ const exportKeySpki = async ( return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); } break; + case 'Ed25519': + // Fall through + case 'Ed448': + if (key.type === 'public') { + // Export Ed key in SPKI DER format + return bufferLikeToArrayBuffer( + key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI), + ); + } + break; } throw new Error( @@ -678,6 +825,16 @@ const exportKeyPkcs8 = async ( return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8); } break; + case 'Ed25519': + // Fall through + case 'Ed448': + if (key.type === 'private') { + // Export Ed key in PKCS8 DER format + return bufferLikeToArrayBuffer( + key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8), + ); + } + break; } throw new Error( @@ -828,6 +985,128 @@ export function isCryptoKeyPair( return 'publicKey' in result && 'privateKey' in result; } +function hmacSignVerify( + key: CryptoKey, + data: BufferLike, + signature?: BufferLike, +): ArrayBuffer | boolean { + // Get hash algorithm from key + const hashName = normalizeHashName(key.algorithm.hash); + + // Export the secret key material + const keyData = key.keyObject.export(); + + // Create HMAC and compute digest + const hmac = createHmac(hashName, keyData); + hmac.update(bufferLikeToArrayBuffer(data)); + const computed = hmac.digest(); + + if (signature === undefined) { + // Sign operation - return the HMAC as ArrayBuffer + return computed.buffer.slice( + computed.byteOffset, + computed.byteOffset + computed.byteLength, + ); + } + + // Verify operation - compare computed HMAC with provided signature + const sigBytes = new Uint8Array(bufferLikeToArrayBuffer(signature)); + const computedBytes = new Uint8Array( + computed.buffer, + computed.byteOffset, + computed.byteLength, + ); + + if (computedBytes.length !== sigBytes.length) { + return false; + } + + // Constant-time comparison to prevent timing attacks + let result = 0; + for (let i = 0; i < computedBytes.length; i++) { + result |= computedBytes[i]! ^ sigBytes[i]!; + } + return result === 0; +} + +function rsaSignVerify( + key: CryptoKey, + data: BufferLike, + padding: 'pkcs1' | 'pss', + signature?: BufferLike, + saltLength?: number, +): ArrayBuffer | boolean { + // Get hash algorithm from key + const hashName = normalizeHashName(key.algorithm.hash); + + // Determine RSA padding constant + const RSA_PKCS1_PADDING = 1; + const RSA_PKCS1_PSS_PADDING = 6; + const paddingValue = + padding === 'pss' ? RSA_PKCS1_PSS_PADDING : RSA_PKCS1_PADDING; + + if (signature === undefined) { + // Sign operation + const signer = createSign(hashName); + signer.update(data); + const sig = signer.sign({ + key: key, + padding: paddingValue, + saltLength, + }); + return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength); + } + + // Verify operation + const verifier = createVerify(hashName); + verifier.update(data); + return verifier.verify( + { + key: key, + padding: paddingValue, + saltLength, + }, + signature, + ); +} + +function edSignVerify( + key: CryptoKey, + data: BufferLike, + signature?: BufferLike, +): ArrayBuffer | boolean { + const isSign = signature === undefined; + const expectedKeyType = isSign ? 'private' : 'public'; + + if (key.type !== expectedKeyType) { + throw lazyDOMException( + `Key must be a ${expectedKeyType} key`, + 'InvalidAccessError', + ); + } + + // Get curve type from algorithm name (Ed25519 or Ed448) + const algorithmName = key.algorithm.name; + const curveType = algorithmName.toLowerCase() as 'ed25519' | 'ed448'; + + // Create Ed instance with the curve + const ed = new Ed(curveType, {}); + + // Export raw key bytes (exportKey with no format returns raw for Ed keys) + const rawKey = key.keyObject.handle.exportKey(); + const dataBuffer = bufferLikeToArrayBuffer(data); + + if (isSign) { + // Sign operation - use raw private key + const sig = ed.signSync(dataBuffer, rawKey); + return sig; + } else { + // Verify operation - use raw public key + const signatureBuffer = bufferLikeToArrayBuffer(signature!); + return ed.verifySync(signatureBuffer, dataBuffer, rawKey); + } +} + const signVerify = ( algorithm: SubtleAlgorithm, key: CryptoKey, @@ -847,9 +1126,18 @@ const signVerify = ( switch (algorithm.name) { case 'ECDSA': return ecdsaSignVerify(key, data, algorithm, signature); + case 'HMAC': + return hmacSignVerify(key, data, signature); + case 'RSASSA-PKCS1-v1_5': + return rsaSignVerify(key, data, 'pkcs1', signature); + case 'RSA-PSS': + return rsaSignVerify(key, data, 'pss', signature, algorithm.saltLength); + case 'Ed25519': + case 'Ed448': + return edSignVerify(key, data, signature); } throw lazyDOMException( - `Unrecognized algorithm name '${algorithm}' for '${usage}'`, + `Unrecognized algorithm name '${algorithm.name}' for '${usage}'`, 'NotSupportedError', ); }; @@ -1003,6 +1291,19 @@ export class Subtle { keyUsages, ); break; + case 'HMAC': + result = await hmacGenerateKey(algorithm, extractable, keyUsages); + break; + case 'Ed25519': + // Fall through + case 'Ed448': + result = await ed_generateKeyPairWebCrypto( + algorithm.name.toLowerCase() as 'ed25519' | 'ed448', + extractable, + keyUsages, + ); + checkCryptoKeyPairUsages(result as CryptoKeyPair); + break; default: throw new Error( `'subtle.generateKey()' is not implemented for ${algorithm.name}. @@ -1080,6 +1381,17 @@ export class Subtle { keyUsages, ); break; + case 'Ed25519': + // Fall through + case 'Ed448': + result = edImportKey( + format, + data as BufferLike, + normalizedAlgorithm, + extractable, + keyUsages, + ); + break; default: throw new Error( `"subtle.importKey()" is not implemented for ${normalizedAlgorithm.name}`, diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index 77d06ef0..51385c06 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -32,6 +32,7 @@ export type BinaryLike = | Buffer | ArrayBuffer | ArrayBufferLike + | ArrayBufferView | CraftzdogBuffer | SafeBuffer | TypedArray @@ -64,6 +65,12 @@ export type ECKeyPairAlgorithm = 'ECDSA' | 'ECDH'; export type CFRGKeyPairAlgorithm = 'Ed25519' | 'Ed448' | 'X25519' | 'X448'; export type CFRGKeyPairType = 'ed25519' | 'ed448' | 'x25519' | 'x448'; +// Node.js style key pair types (lowercase) +export type RSAKeyPairType = 'rsa' | 'rsa-pss'; +export type ECKeyPairType = 'ec'; +export type DSAKeyPairType = 'dsa'; +export type DHKeyPairType = 'dh'; + export type KeyPairAlgorithm = | RSAKeyPairAlgorithm | ECKeyPairAlgorithm @@ -161,9 +168,15 @@ export type SubtleAlgorithm = { length?: number; modulusLength?: number; publicExponent?: number | Uint8Array; + saltLength?: number; }; -export type KeyPairType = CFRGKeyPairType; +export type KeyPairType = + | CFRGKeyPairType + | RSAKeyPairType + | ECKeyPairType + | DSAKeyPairType + | DHKeyPairType; export type KeyUsage = | 'encrypt' @@ -216,9 +229,9 @@ export const kNamedCurveAliases = { // end TODO export type KeyPairGenConfig = { - publicFormat?: KFormatType; + publicFormat?: KFormatType | -1; publicType?: KeyEncoding; - privateFormat?: KFormatType; + privateFormat?: KFormatType | -1; privateType?: KeyEncoding; cipher?: string; passphrase?: ArrayBuffer; @@ -314,6 +327,7 @@ export type GenerateKeyPairOptions = { export type KeyPairKey = | ArrayBuffer + | string | KeyObject | KeyObjectHandle | CryptoKey @@ -343,6 +357,11 @@ export type CryptoKeyPair = { privateKey: KeyPairKey; }; +export type WebCryptoKeyPair = { + publicKey: CryptoKey; + privateKey: CryptoKey; +}; + export enum KeyVariant { RSA_SSA_PKCS1_v1_5, RSA_PSS,