diff --git a/.rules b/.rules index 53d5ecfd..58960cc8 100644 --- a/.rules +++ b/.rules @@ -8,6 +8,7 @@ Every time you choose to apply a rule(s), explicitly state the rule(s) in the ou - It uses Nitro Modules to bridge JS & C++. - Use the documentation of Nitro Modules if you have access locally to its `llms.txt` file. - Part of the API strives to be a polyfill of the Node.js `{crypto}` module. +- When in doubt, favor in order: WebCrypto API, NodeJS implementation, 0.x implementation - The goal is to migrate 0.x of this library that uses OpenSSL 1.1.1 to now use OpenSSL 3.3 and modern C++ with Nitro Modules. ## Tech Stack diff --git a/docs/implementation-coverage.md b/docs/implementation-coverage.md index ab9ea571..ce7dac38 100644 --- a/docs/implementation-coverage.md +++ b/docs/implementation-coverage.md @@ -352,9 +352,9 @@ This document attempts to describe the implementation status of Crypto APIs/Inte | `ML-KEM-512` | ❌ | | `ML-KEM-768` | ❌ | | `ML-KEM-1024` | ❌ | -| `RSA-OAEP` | ❌ | -| `RSA-PSS` | ❌ | -| `RSASSA-PKCS1-v1_5` | ❌ | +| `RSA-OAEP` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | | `X25519` | ❌ | | `X448` | ❌ | diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 2914c319..d5989e2a 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -5,8 +5,8 @@ import { TestsContext } from '../tests/util'; import '../tests/cipher/cipher_tests'; import '../tests/cipher/chacha_tests'; import '../tests/cipher/xsalsa20_tests'; -import '../tests/ed25519/ed25519_tests'; -import '../tests/ed25519/x25519_tests'; +import '../tests/cfrg/ed25519_tests'; +import '../tests/cfrg/x25519_tests'; import '../tests/hash/hash_tests'; import '../tests/hmac/hmac_tests'; import '../tests/pbkdf2/pbkdf2_tests'; diff --git a/example/src/tests/ed25519/ed25519_tests.ts b/example/src/tests/cfrg/ed25519_tests.ts similarity index 100% rename from example/src/tests/ed25519/ed25519_tests.ts rename to example/src/tests/cfrg/ed25519_tests.ts diff --git a/example/src/tests/ed25519/x25519_tests.ts b/example/src/tests/cfrg/x25519_tests.ts similarity index 100% rename from example/src/tests/ed25519/x25519_tests.ts rename to example/src/tests/cfrg/x25519_tests.ts diff --git a/example/src/tests/subtle/generateKey.ts b/example/src/tests/subtle/generateKey.ts index dc23acf1..f8e396d9 100644 --- a/example/src/tests/subtle/generateKey.ts +++ b/example/src/tests/subtle/generateKey.ts @@ -1,8 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { expect } from 'chai'; import { subtle, - type AESAlgorithm, - type AESLength, + // type AESAlgorithm, + // type AESLength, type AnyAlgorithm, type NamedCurve, } from 'react-native-quick-crypto'; @@ -64,33 +65,33 @@ const vectors: Vectors = { // result: 'CryptoKey', // usages: ['sign', 'verify'], // }, - // 'RSASSA-PKCS1-v1_5': { - // algorithm: { - // modulusLength: 1024, - // publicExponent: new Uint8Array([1, 0, 1]), - // hash: 'SHA-256', - // }, - // result: 'CryptoKeyPair', - // usages: ['sign', 'verify'], - // }, - // 'RSA-PSS': { - // algorithm: { - // modulusLength: 1024, - // publicExponent: new Uint8Array([1, 0, 1]), - // hash: 'SHA-256', - // }, - // result: 'CryptoKeyPair', - // usages: ['sign', 'verify'], - // }, - // 'RSA-OAEP': { - // algorithm: { - // modulusLength: 1024, - // publicExponent: new Uint8Array([1, 0, 1]), - // hash: 'SHA-256', - // }, - // result: 'CryptoKeyPair', - // usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], - // }, + 'RSASSA-PKCS1-v1_5': { + algorithm: { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + result: 'CryptoKeyPair', + usages: ['sign', 'verify'], + }, + 'RSA-PSS': { + algorithm: { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + result: 'CryptoKeyPair', + usages: ['sign', 'verify'], + }, + 'RSA-OAEP': { + algorithm: { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + result: 'CryptoKeyPair', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + }, ECDSA: { algorithm: { namedCurve: 'P-521' }, result: 'CryptoKeyPair', @@ -120,7 +121,6 @@ const vectors: Vectors = { }; // Test invalid algorithms -// eslint-disable-next-line @typescript-eslint/no-explicit-any async function testInvalidAlgorithm(algorithm: any) { // one test is slightly different than the others const errorText = @@ -231,206 +231,6 @@ async function testBadUsage(name: string) { const badUsageTests = Object.keys(vectors); badUsageTests.map(testBadUsage); -/* - // Test RSA key generation - { - async function test( - name, - modulusLength, - publicExponent, - hash, - privateUsages, - publicUsages = privateUsages - ) { - let usages = privateUsages; - if (publicUsages !== privateUsages) usages = usages.concat(publicUsages); - const { publicKey, privateKey } = await subtle.generateKey( - { - name, - modulusLength, - publicExponent, - hash, - }, - true, - usages - ); - - assert(publicKey); - assert(privateKey); - assert(isCryptoKey(publicKey)); - assert(isCryptoKey(privateKey)); - - assert(publicKey instanceof CryptoKey); - assert(privateKey instanceof CryptoKey); - - assert.strictEqual(publicKey.type, 'public'); - assert.strictEqual(privateKey.type, 'private'); - assert.strictEqual(publicKey.toString(), '[object CryptoKey]'); - assert.strictEqual(privateKey.toString(), '[object CryptoKey]'); - assert.strictEqual(publicKey.extractable, true); - assert.strictEqual(privateKey.extractable, true); - assert.deepStrictEqual(publicKey.usages, publicUsages); - assert.deepStrictEqual(privateKey.usages, privateUsages); - assert.strictEqual(publicKey.algorithm.name, name); - assert.strictEqual(publicKey.algorithm.modulusLength, modulusLength); - assert.deepStrictEqual(publicKey.algorithm.publicExponent, publicExponent); - assert.strictEqual( - KeyObject.from(publicKey).asymmetricKeyDetails.publicExponent, - bigIntArrayToUnsignedBigInt(publicExponent) - ); - assert.strictEqual(publicKey.algorithm.hash.name, hash); - assert.strictEqual(privateKey.algorithm.name, name); - assert.strictEqual(privateKey.algorithm.modulusLength, modulusLength); - assert.deepStrictEqual(privateKey.algorithm.publicExponent, publicExponent); - assert.strictEqual( - KeyObject.from(privateKey).asymmetricKeyDetails.publicExponent, - bigIntArrayToUnsignedBigInt(publicExponent) - ); - assert.strictEqual(privateKey.algorithm.hash.name, hash); - - // Missing parameters - await assert.rejects( - subtle.generateKey({ name, publicExponent, hash }, true, usages), - { - code: 'ERR_MISSING_OPTION', - } - ); - - await assert.rejects( - subtle.generateKey({ name, modulusLength, hash }, true, usages), - { - code: 'ERR_MISSING_OPTION', - } - ); - - await assert.rejects( - subtle.generateKey({ name, modulusLength }, true, usages), - { - code: 'ERR_MISSING_OPTION', - } - ); - - await Promise.all( - [{}].map((modulusLength) => { - return assert.rejects( - subtle.generateKey( - { - name, - modulusLength, - publicExponent, - hash, - }, - true, - usages - ), - { - code: 'ERR_INVALID_ARG_TYPE', - } - ); - }) - ); - - await Promise.all( - ['', true, {}, 1, [], new Uint32Array(2)].map((publicExponent) => { - return assert.rejects( - subtle.generateKey( - { name, modulusLength, publicExponent, hash }, - true, - usages - ), - { code: 'ERR_INVALID_ARG_TYPE' } - ); - }) - ); - - await Promise.all( - [true, 1].map((hash) => { - return assert.rejects( - subtle.generateKey( - { - name, - modulusLength, - publicExponent, - hash, - }, - true, - usages - ), - { - message: /Unrecognized algorithm name/, - name: 'NotSupportedError', - } - ); - }) - ); - - await Promise.all( - ['', {}, 1, false].map((usages) => { - return assert.rejects( - subtle.generateKey( - { - name, - modulusLength, - publicExponent, - hash, - }, - true, - usages - ), - { - code: 'ERR_INVALID_ARG_TYPE', - } - ); - }) - ); - - await Promise.all( - [[1], [1, 0, 0]].map((publicExponent) => { - return assert.rejects( - subtle.generateKey( - { - name, - modulusLength, - publicExponent: new Uint8Array(publicExponent), - hash, - }, - true, - usages - ), - { - name: 'OperationError', - } - ); - }) - ); - } - - const kTests = [ - [ - 'RSASSA-PKCS1-v1_5', - 1024, - Buffer.from([1, 0, 1]), - 'SHA-256', - ['sign'], - ['verify'], - ], - ['RSA-PSS', 2048, Buffer.from([1, 0, 1]), 'SHA-512', ['sign'], ['verify']], - [ - 'RSA-OAEP', - 1024, - Buffer.from([3]), - 'SHA-384', - ['decrypt', 'unwrapKey'], - ['encrypt', 'wrapKey'], - ], - ]; - - const tests = kTests.map((args) => test(...args)); - - Promise.all(tests).then(common.mustCall()); - } - */ - // Test EC Key Generation async function testECKeyGen( name: AnyAlgorithm, @@ -505,11 +305,120 @@ testECKeyGen('ECDSA', 'P-521', ['sign'], ['verify']); testECKeyGen('ECDH', 'P-384', ['deriveKey', 'deriveBits'], []); testECKeyGen('ECDH', 'P-521', ['deriveKey', 'deriveBits'], []); +// Test RSA Key Generation +async function testRSAKeyGen( + name: 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' | 'RSA-OAEP', + modulusLength: number, + publicExponent: Uint8Array, + hash: string, + privateUsages: KeyUsage[], + publicUsages: KeyUsage[] = privateUsages, +) { + test( + SUITE, + `RSA keygen: ${name} ${modulusLength} ${hash} ${privateUsages} ${publicUsages}`, + async () => { + let usages = privateUsages; + if (publicUsages !== privateUsages) { + usages = usages.concat(publicUsages); + } + + const keyPair = await subtle.generateKey( + { + name, + modulusLength, + publicExponent, + hash, + } as any, + true, + usages, + ); + + const { publicKey, privateKey } = keyPair as TestCryptoKeyPair; + expect(publicKey !== undefined); + expect(privateKey !== undefined); + expect(publicKey instanceof Object); + expect(privateKey instanceof Object); + + expect(publicKey.type).to.equal('public'); + expect(privateKey.type).to.equal('private'); + expect(publicKey.extractable).to.equal(true); + expect(privateKey.extractable).to.equal(true); + expect(publicKey.usages).to.deep.equal(publicUsages); + expect(privateKey.usages).to.deep.equal(privateUsages); + expect(publicKey.algorithm.name).to.equal(name); + expect(privateKey.algorithm.name).to.equal(name); + expect((publicKey.algorithm as any).modulusLength).to.equal( + modulusLength, + ); + expect((privateKey.algorithm as any).modulusLength).to.equal( + modulusLength, + ); + expect((publicKey.algorithm as any).publicExponent).to.deep.equal( + publicExponent, + ); + expect((privateKey.algorithm as any).publicExponent).to.deep.equal( + publicExponent, + ); + expect((publicKey.algorithm as any).hash.name).to.equal(hash); + expect((privateKey.algorithm as any).hash.name).to.equal(hash); + + // Test invalid usage + await assertThrowsAsync( + async () => + subtle.generateKey( + { name, modulusLength, publicExponent, hash } as any, + true, + name === 'RSA-OAEP' ? ['sign'] : ['encrypt'], + ), + `Unsupported key usage for a ${name} key`, + ); + + // Test invalid modulus length + await assertThrowsAsync( + async () => + subtle.generateKey( + { name, modulusLength: 0, publicExponent, hash } as any, + true, + usages, + ), + 'Invalid key length', + ); + }, + ); +} + +testRSAKeyGen( + 'RSASSA-PKCS1-v1_5', + 1024, + new Uint8Array([1, 0, 1]), + 'SHA-256', + ['sign'], + ['verify'], +); +testRSAKeyGen( + 'RSA-PSS', + 2048, + new Uint8Array([1, 0, 1]), + 'SHA-512', + ['sign'], + ['verify'], +); +testRSAKeyGen( + 'RSA-OAEP', + 1024, + new Uint8Array([3]), + 'SHA-384', + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey'], +); + +/* // Test AES Key Generation type AESArgs = [AESAlgorithm, AESLength, KeyUsage[]]; async function testAesKeyGen(args: AESArgs) { const [name, length, usages] = args; - it(`AES keygen: ${name} ${length} ${usages}`, async () => { + test(SUITE, `AES keygen: ${name} ${length} ${usages}`, async () => { const key = await subtle.generateKey({ name, length }, true, usages); const k = key as CryptoKey; expect(k !== undefined); @@ -549,6 +458,7 @@ const aesTests: AESArgs[] = [ ]; aesTests.map(args => testAesKeyGen(args)); +*/ /* // Test HMAC Key Generation diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 9dfeb1e7..f8f0464a 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -23,6 +23,7 @@ add_library( ../cpp/keys/KeyObjectData.cpp ../cpp/pbkdf2/HybridPbkdf2.cpp ../cpp/random/HybridRandom.cpp + ../cpp/rsa/HybridRsaKeyPair.cpp ../deps/fastpbkdf2/fastpbkdf2.c ../deps/ncrypto/src/ncrypto.cpp ) @@ -41,6 +42,7 @@ include_directories( "../cpp/keys" "../cpp/pbkdf2" "../cpp/random" + "../cpp/rsa" "../cpp/utils" "../deps/fastpbkdf2" "../deps/ncrypto/include" diff --git a/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp new file mode 100644 index 00000000..df7b75a0 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "HybridRsaKeyPair.hpp" +#include "Utils.hpp" + +namespace margelo::nitro::crypto { + +std::shared_ptr> HybridRsaKeyPair::generateKeyPair() { + return Promise::async([this]() { this->generateKeyPairSync(); }); +} + +void HybridRsaKeyPair::generateKeyPairSync() { + // Clean up existing key if any + if (this->pkey != nullptr) { + EVP_PKEY_free(this->pkey); + this->pkey = nullptr; + } + + // Create key generation context + std::unique_ptr ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr), EVP_PKEY_CTX_free); + + if (!ctx) { + throw std::runtime_error("Failed to create RSA key generation context"); + } + + if (EVP_PKEY_keygen_init(ctx.get()) <= 0) { + throw std::runtime_error("Failed to initialize RSA key generation"); + } + + // Set modulus length + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), this->modulusLength) <= 0) { + throw std::runtime_error("Failed to set RSA modulus length"); + } + + // Set public exponent + std::unique_ptr exponent(BN_new(), BN_free); + if (!exponent) { + throw std::runtime_error("Failed to create BIGNUM for public exponent"); + } + + // Default to 65537 (0x10001) if no public exponent is set + if (this->publicExponent.empty()) { + if (BN_set_word(exponent.get(), RSA_F4) != 1) { + throw std::runtime_error("Failed to set default public exponent"); + } + } else { + if (BN_bin2bn(this->publicExponent.data(), this->publicExponent.size(), exponent.get()) == nullptr) { + throw std::runtime_error("Failed to convert public exponent to BIGNUM"); + } + } + + if (EVP_PKEY_CTX_set1_rsa_keygen_pubexp(ctx.get(), exponent.get()) <= 0) { + throw std::runtime_error("Failed to set RSA public exponent"); + } + + // Generate the key pair + EVP_PKEY* raw_pkey = nullptr; + if (EVP_PKEY_keygen(ctx.get(), &raw_pkey) <= 0) { + throw std::runtime_error("Failed to generate RSA key pair"); + } + + this->pkey = raw_pkey; +} + +void HybridRsaKeyPair::setModulusLength(double modulusLength) { + this->modulusLength = static_cast(modulusLength); +} + +void HybridRsaKeyPair::setPublicExponent(const std::shared_ptr& publicExponent) { + if (publicExponent && publicExponent->size() > 0) { + const uint8_t* data = publicExponent->data(); + this->publicExponent.assign(data, data + publicExponent->size()); + } +} + +void HybridRsaKeyPair::setHashAlgorithm(const std::string& hashAlgorithm) { + this->hashAlgorithm = hashAlgorithm; +} + +std::shared_ptr HybridRsaKeyPair::getPublicKey() { + this->checkKeyPair(); + + // Export as DER format using direct OpenSSL calls + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO for public key export"); + } + + if (i2d_PUBKEY_bio(bio, this->pkey) != 1) { + BIO_free(bio); + throw std::runtime_error("Failed to export public key to DER format"); + } + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + + // Create a string from the DER data and use ToNativeArrayBuffer utility + std::string derData(mem->data, mem->length); + BIO_free(bio); + + return ToNativeArrayBuffer(derData); +} + +std::shared_ptr HybridRsaKeyPair::getPrivateKey() { + this->checkKeyPair(); + + // Export as DER format in PKCS8 format using direct OpenSSL calls + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO for private key export"); + } + + if (i2d_PKCS8PrivateKey_bio(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + BIO_free(bio); + throw std::runtime_error("Failed to export private key to DER PKCS8 format"); + } + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + + // Create a string from the DER data and use ToNativeArrayBuffer utility + std::string derData(mem->data, mem->length); + BIO_free(bio); + + return ToNativeArrayBuffer(derData); +} + +KeyObject HybridRsaKeyPair::importKey(const std::string& /* format */, const std::shared_ptr& /* keyData */, + const std::string& /* algorithm */, bool /* extractable */, + const std::vector& /* keyUsages */) { + throw std::runtime_error("HybridRsaKeyPair::importKey() is not yet implemented"); +} + +std::shared_ptr HybridRsaKeyPair::exportKey(const KeyObject& /* key */, const std::string& /* format */) { + throw std::runtime_error("HybridRsaKeyPair::exportKey() is not yet implemented"); +} + +void HybridRsaKeyPair::checkKeyPair() { + if (this->pkey == nullptr) { + throw std::runtime_error("RSA KeyPair not initialized"); + } +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp new file mode 100644 index 00000000..f56f4e69 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "HybridRsaKeyPairSpec.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace margelo::nitro::crypto { + +class HybridRsaKeyPair : public HybridRsaKeyPairSpec { + public: + HybridRsaKeyPair() : HybridObject(TAG), pkey(nullptr), modulusLength(2048), hashAlgorithm("SHA-256") {} + ~HybridRsaKeyPair() { + if (pkey) { + EVP_PKEY_free(pkey); + } + } + + std::shared_ptr> generateKeyPair() override; + void generateKeyPairSync() override; + void setModulusLength(double modulusLength) override; + void setPublicExponent(const std::shared_ptr& publicExponent) override; + void setHashAlgorithm(const std::string& hashAlgorithm) override; + std::shared_ptr getPublicKey() override; + std::shared_ptr getPrivateKey() override; + KeyObject importKey(const std::string& format, const std::shared_ptr& keyData, const std::string& algorithm, + bool extractable, const std::vector& keyUsages) override; + std::shared_ptr exportKey(const KeyObject& key, const std::string& format) override; + + private: + EVP_PKEY* pkey; + int modulusLength; + std::vector publicExponent; + std::string hashAlgorithm; + + void checkKeyPair(); +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index ffb401d4..ce7d5749 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -16,7 +16,8 @@ "Hmac": { "cpp": "HybridHmac" }, "KeyObjectHandle": { "cpp": "HybridKeyObjectHandle" }, "Pbkdf2": { "cpp": "HybridPbkdf2" }, - "Random": { "cpp": "HybridRandom" } + "Random": { "cpp": "HybridRandom" }, + "RsaKeyPair": { "cpp": "HybridRsaKeyPair" } }, "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 93a77f7d..f671a66a 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 @@ -36,6 +36,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridPbkdf2Spec.cpp ../nitrogen/generated/shared/c++/HybridRandomSpec.cpp + ../nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.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 6e1d5dc6..3334bbea 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -24,6 +24,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridPbkdf2.hpp" #include "HybridRandom.hpp" +#include "HybridRsaKeyPair.hpp" namespace margelo::nitro::crypto { @@ -118,6 +119,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RsaKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRsaKeyPair\" 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 797937b4..721aa588 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -19,6 +19,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridPbkdf2.hpp" #include "HybridRandom.hpp" +#include "HybridRsaKeyPair.hpp" @interface QuickCryptoAutolinking : NSObject @end @@ -110,6 +111,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RsaKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRsaKeyPair\" 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++/HybridRsaKeyPairSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.cpp new file mode 100644 index 00000000..f2885fc5 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.cpp @@ -0,0 +1,29 @@ +/// +/// HybridRsaKeyPairSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridRsaKeyPairSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridRsaKeyPairSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("generateKeyPair", &HybridRsaKeyPairSpec::generateKeyPair); + prototype.registerHybridMethod("generateKeyPairSync", &HybridRsaKeyPairSpec::generateKeyPairSync); + prototype.registerHybridMethod("setModulusLength", &HybridRsaKeyPairSpec::setModulusLength); + prototype.registerHybridMethod("setPublicExponent", &HybridRsaKeyPairSpec::setPublicExponent); + prototype.registerHybridMethod("setHashAlgorithm", &HybridRsaKeyPairSpec::setHashAlgorithm); + prototype.registerHybridMethod("importKey", &HybridRsaKeyPairSpec::importKey); + prototype.registerHybridMethod("exportKey", &HybridRsaKeyPairSpec::exportKey); + prototype.registerHybridMethod("getPublicKey", &HybridRsaKeyPairSpec::getPublicKey); + prototype.registerHybridMethod("getPrivateKey", &HybridRsaKeyPairSpec::getPrivateKey); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.hpp new file mode 100644 index 00000000..3eedf5d7 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.hpp @@ -0,0 +1,77 @@ +/// +/// HybridRsaKeyPairSpec.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 `KeyObject` to properly resolve imports. +namespace margelo::nitro::crypto { struct KeyObject; } + +#include +#include +#include +#include "KeyObject.hpp" +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `RsaKeyPair` + * Inherit this class to create instances of `HybridRsaKeyPairSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridRsaKeyPair: public HybridRsaKeyPairSpec { + * public: + * HybridRsaKeyPair(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridRsaKeyPairSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridRsaKeyPairSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridRsaKeyPairSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> generateKeyPair() = 0; + virtual void generateKeyPairSync() = 0; + virtual void setModulusLength(double modulusLength) = 0; + virtual void setPublicExponent(const std::shared_ptr& publicExponent) = 0; + virtual void setHashAlgorithm(const std::string& hashAlgorithm) = 0; + virtual KeyObject importKey(const std::string& format, const std::shared_ptr& keyData, const std::string& algorithm, bool extractable, const std::vector& keyUsages) = 0; + virtual std::shared_ptr exportKey(const KeyObject& key, const std::string& format) = 0; + virtual std::shared_ptr getPublicKey() = 0; + virtual std::shared_ptr getPrivateKey() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "RsaKeyPair"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/rsa.ts b/packages/react-native-quick-crypto/src/rsa.ts new file mode 100644 index 00000000..ed75e317 --- /dev/null +++ b/packages/react-native-quick-crypto/src/rsa.ts @@ -0,0 +1,176 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { + CryptoKey, + KeyObject, + PrivateKeyObject, + PublicKeyObject, +} from './keys'; +import { + getUsagesUnion, + hasAnyNotIn, + lazyDOMException, + normalizeHashName, +} from './utils'; +import type { + CryptoKeyPair, + KeyUsage, + RsaHashedKeyGenParams, + SubtleAlgorithm, +} from './utils'; +import type { RsaKeyPair } from './specs/rsaKeyPair.nitro'; + +export class Rsa { + native: RsaKeyPair; + + constructor( + modulusLength: number, + publicExponent: Uint8Array, + hashAlgorithm: string, + ) { + this.native = NitroModules.createHybridObject('RsaKeyPair'); + this.native.setModulusLength(modulusLength); + this.native.setPublicExponent( + publicExponent.buffer.slice( + publicExponent.byteOffset, + publicExponent.byteOffset + publicExponent.byteLength, + ) as ArrayBuffer, + ); + this.native.setHashAlgorithm(hashAlgorithm); + } + + async generateKeyPair(): Promise { + await this.native.generateKeyPair(); + return { + publicKey: this.native.getPublicKey(), + privateKey: this.native.getPrivateKey(), + }; + } + + generateKeyPairSync(): CryptoKeyPair { + this.native.generateKeyPairSync(); + return { + publicKey: this.native.getPublicKey(), + privateKey: this.native.getPrivateKey(), + }; + } +} + +// Node API +export async function rsa_generateKeyPair( + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], +): Promise { + const { name, modulusLength, publicExponent, hash } = + algorithm as RsaHashedKeyGenParams; + + // Validate parameters first + if (!modulusLength || modulusLength < 256) { + throw lazyDOMException('Invalid key length', 'OperationError'); + } + + if (!publicExponent || publicExponent.length === 0) { + throw lazyDOMException('Invalid public exponent', 'OperationError'); + } + + // Validate hash algorithm using existing validation function + let hashName: string; + try { + const normalizedHash = normalizeHashName(hash); + hashName = typeof hash === 'string' ? hash : hash?.name || normalizedHash; + } catch { + throw lazyDOMException('Invalid Hash Algorithm', 'NotSupportedError'); + } + + // Validate usages are not empty + if (keyUsages.length === 0) { + throw lazyDOMException('Usages cannot be empty', 'SyntaxError'); + } + + // Usage validation based on algorithm type + switch (name) { + case 'RSASSA-PKCS1-v1_5': + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException( + `Unsupported key usage for a ${name} key`, + 'SyntaxError', + ); + } + break; + case 'RSA-PSS': + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException( + `Unsupported key usage for a ${name} key`, + 'SyntaxError', + ); + } + break; + case 'RSA-OAEP': + if ( + hasAnyNotIn(keyUsages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']) + ) { + throw lazyDOMException( + `Unsupported key usage for a ${name} key`, + 'SyntaxError', + ); + } + break; + default: + throw lazyDOMException( + 'The algorithm is not supported', + 'NotSupportedError', + ); + } + + // Split usages between public and private keys + let publicUsages: KeyUsage[] = []; + let privateUsages: KeyUsage[] = []; + switch (name) { + case 'RSASSA-PKCS1-v1_5': + case 'RSA-PSS': + publicUsages = getUsagesUnion(keyUsages, 'verify'); + privateUsages = getUsagesUnion(keyUsages, 'sign'); + break; + case 'RSA-OAEP': + publicUsages = getUsagesUnion(keyUsages, 'encrypt', 'wrapKey'); + privateUsages = getUsagesUnion(keyUsages, 'decrypt', 'unwrapKey'); + break; + } + + // Validate that private key has usages for CryptoKeyPair + if (privateUsages.length === 0) { + throw lazyDOMException('Usages cannot be empty', 'SyntaxError'); + } + + const rsa = new Rsa(modulusLength, publicExponent, hashName); + await rsa.generateKeyPair(); + + const keyAlgorithm = { + name, + modulusLength, + publicExponent, + hash: { name: hashName }, + }; + + // Create KeyObject instances using the standard createKeyObject method + const publicKeyData = rsa.native.getPublicKey(); + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + ) as PublicKeyObject; + const publicKey = new CryptoKey(pub, keyAlgorithm, publicUsages, true); + + const privateKeyData = rsa.native.getPrivateKey(); + const priv = KeyObject.createKeyObject( + 'private', + privateKeyData, + ) as PrivateKeyObject; + const privateKey = new CryptoKey( + priv, + keyAlgorithm, + privateUsages, + extractable, + ); + + return { publicKey, privateKey }; +} diff --git a/packages/react-native-quick-crypto/src/specs/rsaKeyPair.nitro.ts b/packages/react-native-quick-crypto/src/specs/rsaKeyPair.nitro.ts new file mode 100644 index 00000000..b607b90c --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/rsaKeyPair.nitro.ts @@ -0,0 +1,33 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +// Nitro-compatible interfaces defined locally +interface KeyObject { + extractable: boolean; +} + +export interface RsaKeyPair + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + // generateKeyPair functions + generateKeyPair(): Promise; + generateKeyPairSync(): void; + + // RSA-specific setters + setModulusLength(modulusLength: number): void; + setPublicExponent(publicExponent: ArrayBuffer): void; + setHashAlgorithm(hashAlgorithm: string): void; + + // importKey + importKey( + format: string, + keyData: ArrayBuffer, + algorithm: string, + extractable: boolean, + keyUsages: string[], + ): KeyObject; + + // exportKey + exportKey(key: KeyObject, format: string): ArrayBuffer; + + getPublicKey(): ArrayBuffer; + getPrivateKey(): ArrayBuffer; +} diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 92bf5b3c..fe49a2e7 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -22,6 +22,7 @@ import { asyncDigest } from './hash'; import { createSecretKey } from './keys'; import { pbkdf2DeriveBits } from './pbkdf2'; import { ec_generateKeyPair } from './ec'; +import { rsa_generateKeyPair } from './rsa'; // Placeholder imports - these modules need to be implemented or adapted // import { ecImportKey, ecExportKey, ecGenerateKey, ecdsaSignVerify } from './ec'; @@ -105,14 +106,6 @@ function aesCipher( throw new Error('aesCipher not implemented'); } -async function rsaKeyGenerate( - _algorithm: SubtleAlgorithm, - _extractable: boolean, - _keyUsages: KeyUsage[], -): Promise { - throw new Error('rsaKeyGenerate not implemented'); -} - async function aesGenerateKey( _algorithm: AesKeyGenParams, _extractable: boolean, @@ -501,7 +494,7 @@ export class Subtle { case 'RSA-PSS': // Fall through case 'RSA-OAEP': - result = await rsaKeyGenerate(algorithm, extractable, keyUsages); + result = await rsa_generateKeyPair(algorithm, extractable, keyUsages); break; case 'ECDSA': // Fall through diff --git a/packages/react-native-quick-crypto/src/utils/hashnames.ts b/packages/react-native-quick-crypto/src/utils/hashnames.ts index 57078f13..c794c5a6 100644 --- a/packages/react-native-quick-crypto/src/utils/hashnames.ts +++ b/packages/react-native-quick-crypto/src/utils/hashnames.ts @@ -79,11 +79,13 @@ const kHashNames: HashNames = { } export function normalizeHashName( - algo: string | HashAlgorithm | undefined, + algo: string | HashAlgorithm | { name: string } | undefined, context: HashContext = HashContext.Node, ): HashAlgorithm { if (typeof algo !== 'undefined') { - const normAlgo = algo.toString().toLowerCase(); + const hashName = + typeof algo === 'string' ? algo : algo.name || algo.toString(); + const normAlgo = hashName.toLowerCase(); try { const alias = kHashNames[normAlgo]![context] as HashAlgorithm; if (alias) return alias; diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index 040ac2cc..66832390 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -45,6 +45,20 @@ export type HashAlgorithm = DigestAlgorithm | 'SHA-224' | 'RIPEMD-160'; export type RSAKeyPairAlgorithm = 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' | 'RSA-OAEP'; +export interface RsaHashedKeyGenParams { + name: RSAKeyPairAlgorithm; + modulusLength: number; + publicExponent: Uint8Array; + hash: string | { name: string }; +} + +export interface RsaKeyAlgorithm { + name: RSAKeyPairAlgorithm; + modulusLength: number; + publicExponent: Uint8Array; + hash: { name: string }; +} + export type ECKeyPairAlgorithm = 'ECDSA' | 'ECDH'; export type CFRGKeyPairAlgorithm = 'Ed25519' | 'Ed448' | 'X25519' | 'X448'; @@ -142,7 +156,7 @@ export type SubtleAlgorithm = { name: AnyAlgorithm; salt?: string; iterations?: number; - hash?: HashAlgorithm; + hash?: HashAlgorithm | { name: string }; namedCurve?: NamedCurve; length?: number; modulusLength?: number;