diff --git a/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml b/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml index 7ccbf12a..eff246e1 100644 --- a/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml +++ b/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml @@ -41,7 +41,6 @@ body: "react-native": "^0.74.3", "react-native-quick-crypto": "^0.7.1", "@craftzdog/react-native-buffer": "^6.0.5", - "react-native-fast-encoder": "^0.1.12", "react-native-quick-base64": "^2.1.2", ... }, diff --git a/bun.lock b/bun.lock index 014ae957..3b87056e 100644 --- a/bun.lock +++ b/bun.lock @@ -41,9 +41,8 @@ "react": "19.1.0", "react-native": "0.81.1", "react-native-bouncy-checkbox": "4.1.2", - "react-native-fast-encoder": "0.2.0", "react-native-nitro-modules": "0.29.1", - "react-native-quick-base64": "2.2.1", + "react-native-quick-base64": "2.2.2", "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "^5.2.2", "react-native-screens": "4.15.4", @@ -83,7 +82,7 @@ "@craftzdog/react-native-buffer": "6.1.0", "events": "3.3.0", "expo-build-properties": "0.14.6", - "react-native-quick-base64": "2.2.0", + "react-native-quick-base64": "2.2.2", "readable-stream": "4.5.2", "util": "0.12.5", }, @@ -1396,8 +1395,6 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatbuffers": ["flatbuffers@2.0.6", "", {}, "sha512-QTTZTXTbVfuOVQu2X6eLOw4vefUxnFJZxAKeN3rEPhjEzBtIbehimJLfVGHPM8iX0Na+9i76SBEg0skf0c0sCA=="], - "flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], @@ -2190,13 +2187,11 @@ "react-native-builder-bob": ["react-native-builder-bob@0.39.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-transform-strict-mode": "^7.24.7", "@babel/preset-env": "^7.25.2", "@babel/preset-flow": "^7.24.7", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", "babel-plugin-module-resolver": "^5.0.2", "browserslist": "^4.20.4", "cross-spawn": "^7.0.3", "dedent": "^0.7.0", "del": "^6.1.1", "escape-string-regexp": "^4.0.0", "fs-extra": "^10.1.0", "glob": "^8.0.3", "is-git-dirty": "^2.0.1", "json5": "^2.2.1", "kleur": "^4.1.4", "metro-config": "^0.80.9", "prompts": "^2.4.2", "which": "^2.0.2", "yargs": "^17.5.1" }, "bin": { "bob": "bin/bob" } }, "sha512-nEG9FB5a2Rxw0251dnlM9QtqvuM2os8avRhYDWDdvsZOnQJhQI4fGV5wF5FypAqHNWPQUNXmvhPUFrPSwiPnAQ=="], - "react-native-fast-encoder": ["react-native-fast-encoder@0.2.0", "", { "dependencies": { "big-integer": "^1.6.51", "flatbuffers": "2.0.6" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-E4mx81fRMVs0qq8is3cZTrbuEJdsDo8Nfe7qTxKZwsCianpYpA2QfyH6cEYumSOEht6l+KeRJ4RqcyfxMDyesg=="], - "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="], "react-native-nitro-modules": ["react-native-nitro-modules@0.29.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-91A/Lc4Zc1Bvzj1iMSnD6vA5Swqv8aVcwGcv8ddjoPd9mahNvVS2arFh3o7kAqRH4RIh3KcQ0NpYslu7AYn55Q=="], - "react-native-quick-base64": ["react-native-quick-base64@2.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r7/BRsRl8QKEhS0JsHW6QX9+8LrC6NNWlwNnBnZ69h2kbcfABYsUILT71obrs9fqElEIMzuYSI5aHID955akyQ=="], + "react-native-quick-base64": ["react-native-quick-base64@2.2.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-WLHSifHLoamr2kF00Gov0W9ud6CfPshe1rmqWTquVIi9c62qxOaJCFVDrXFZhEBU8B8PvGLVuOlVKH78yhY0Fg=="], "react-native-quick-crypto": ["react-native-quick-crypto@workspace:packages/react-native-quick-crypto"], @@ -2908,6 +2903,8 @@ "@conventional-changelog/git-client/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + "@craftzdog/react-native-buffer/react-native-quick-base64": ["react-native-quick-base64@2.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r7/BRsRl8QKEhS0JsHW6QX9+8LrC6NNWlwNnBnZ69h2kbcfABYsUILT71obrs9fqElEIMzuYSI5aHID955akyQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="], @@ -3544,8 +3541,6 @@ "react-native-quick-crypto-example/@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], - "react-native-quick-crypto-example/react-native-quick-base64": ["react-native-quick-base64@2.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rAECaDhq3v+P8IM10cLgUVvt3kPJq3v+Jznp7tQRLXk1LlV/VCepump3am0ObwHlE6EoXblm4cddPJoXAlO+CQ=="], - "read-package-up/type-fest": ["type-fest@4.25.0", "", {}, "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw=="], "read-pkg/parse-json": ["parse-json@8.1.0", "", { "dependencies": { "@babel/code-frame": "^7.22.13", "index-to-position": "^0.1.2", "type-fest": "^4.7.1" } }, "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA=="], diff --git a/docs/implementation-coverage.md b/docs/implementation-coverage.md index d8816750..ab9ea571 100644 --- a/docs/implementation-coverage.md +++ b/docs/implementation-coverage.md @@ -342,8 +342,8 @@ This document attempts to describe the implementation status of Crypto APIs/Inte ### `CryptoKeyPair` algorithms | Algorithm | Status | | --------- | :----: | -| `ECDH` | ❌ | -| `ECDSA` | ❌ | +| `ECDH` | ✅ | +| `ECDSA` | ✅ | | `Ed25519` | ❌ | | `Ed448` | ❌ | | `ML-DSA-44` | ❌ | diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 79b23c90..62288686 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1331,29 +1331,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - ReactNativeDependencies - - react-native-fast-encoder (0.2.0): - - hermes-engine - - RCTRequired - - RCTTypeSafety - - React-Core - - React-Core-prebuilt - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-jsi - - React-NativeModulesApple - - React-RCTFabric - - React-renderercss - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - ReactNativeDependencies - - Yoga - - react-native-quick-base64 (2.2.1): + - react-native-quick-base64 (2.2.2): - React-Core - react-native-safe-area-context (5.6.1): - hermes-engine @@ -1884,8 +1862,7 @@ DEPENDENCIES: - React-logger (from `../../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - - react-native-fast-encoder (from `../../node_modules/react-native-fast-encoder`) - - react-native-quick-base64 (from `../node_modules/react-native-quick-base64`) + - react-native-quick-base64 (from `../../node_modules/react-native-quick-base64`) - react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`) - React-NativeModulesApple (from `../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-oscompat (from `../../node_modules/react-native/ReactCommon/oscompat`) @@ -2002,10 +1979,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" - react-native-fast-encoder: - :path: "../../node_modules/react-native-fast-encoder" react-native-quick-base64: - :path: "../node_modules/react-native-quick-base64" + :path: "../../node_modules/react-native-quick-base64" react-native-safe-area-context: :path: "../../node_modules/react-native-safe-area-context" React-NativeModulesApple: @@ -2116,8 +2091,7 @@ SPEC CHECKSUMS: React-logger: a1c9c8dfc56711d06a21109b55908e177113e554 React-Mapbuffer: bc36232966c55d5b1cbef3b84cb97c4317fa4403 React-microtasksnativemodule: cbabfedcf6c2984e59d07f9ab553f3d277599b1b - react-native-fast-encoder: f65266038936a5ade3a54d34755fce3c7bd92483 - react-native-quick-base64: 7b7f689c9b7b4d40916bf5d27e211db1040abac5 + react-native-quick-base64: 6568199bb2ac8e72ecdfdc73a230fbc5c1d3aac4 react-native-safe-area-context: 42a1b4f8774b577d03b53de7326e3d5757fe9513 React-NativeModulesApple: 055e2d1417c663e7a26fc0847609f503e626e9e1 React-oscompat: 8f2893713639e12c7558750a9f7de3f08ac255b0 diff --git a/example/package.json b/example/package.json index ac01190b..4b2f6408 100644 --- a/example/package.json +++ b/example/package.json @@ -36,9 +36,8 @@ "react": "19.1.0", "react-native": "0.81.1", "react-native-bouncy-checkbox": "4.1.2", - "react-native-fast-encoder": "0.2.0", "react-native-nitro-modules": "0.29.1", - "react-native-quick-base64": "2.2.1", + "react-native-quick-base64": "2.2.2", "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "^5.2.2", "react-native-screens": "4.15.4", diff --git a/example/src/tests/subtle/encrypt_decrypt.ts b/example/src/tests/subtle/encrypt_decrypt.ts index c649d720..850f4e75 100644 --- a/example/src/tests/subtle/encrypt_decrypt.ts +++ b/example/src/tests/subtle/encrypt_decrypt.ts @@ -30,10 +30,6 @@ import aes_gcm_fixtures from '../../fixtures/aes_gcm'; import { assertThrowsAsync } from '../util'; import { ab2str } from 'react-native-quick-crypto'; -import RNFE from 'react-native-fast-encoder'; -// @ts-expect-error polyfill types are wonky -globalThis.TextEncoder = () => RNFE; - export type RsaEncryptDecryptTestVector = { name: string; publicKey: Buffer | null; diff --git a/example/src/tests/subtle/sign_verify.ts b/example/src/tests/subtle/sign_verify.ts index 0c1ec953..2c750dac 100644 --- a/example/src/tests/subtle/sign_verify.ts +++ b/example/src/tests/subtle/sign_verify.ts @@ -5,6 +5,8 @@ import { expect } from 'chai'; const encoder = new TextEncoder(); +const SUITE = 'subtle.sign/verify'; + // // Test Sign/Verify RSASSA-PKCS1-v1_5 // { // async function test(data) { @@ -53,7 +55,7 @@ const encoder = new TextEncoder(); // test('hello world').then(common.mustCall()); // } -test('subtle.sign_verify', 'ECDSA P-384', async () => { +test(SUITE, 'ECDSA P-384', async () => { const pair = await subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-384' }, true, @@ -78,7 +80,7 @@ test('subtle.sign_verify', 'ECDSA P-384', async () => { ).to.equal(true); }); -test('subtle.sign_verify', 'ECDSA with HashAlgorithmIdentifier', async () => { +test(SUITE, 'ECDSA with HashAlgorithmIdentifier', async () => { const pair = await subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 9c4be833..9dfeb1e7 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -15,6 +15,7 @@ add_library( ../cpp/cipher/XSalsa20Cipher.cpp ../cpp/cipher/ChaCha20Cipher.cpp ../cpp/cipher/ChaCha20Poly1305Cipher.cpp + ../cpp/ec/HybridEcKeyPair.cpp ../cpp/ed25519/HybridEdKeyPair.cpp ../cpp/hash/HybridHash.cpp ../cpp/hmac/HybridHmac.cpp @@ -33,6 +34,7 @@ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/QuickCrypto+autolinkin include_directories( "src/main/cpp" "../cpp/cipher" + "../cpp/ec" "../cpp/ed25519" "../cpp/hash" "../cpp/hmac" @@ -42,8 +44,6 @@ include_directories( "../cpp/utils" "../deps/fastpbkdf2" "../deps/ncrypto/include" - "../build/includes" # flattened Nitro Modules headers - "../../../node_modules/react-native/ReactCommon/jsi" ) # Third party libraries (Prefabs) diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp index 30ed4412..ae842bbd 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp @@ -40,12 +40,14 @@ class HybridCipherFactory : public HybridCipherFactorySpec { // Pass tag length (default 16 if not present) size_t tag_len = args.authTagLen.has_value() ? static_cast(args.authTagLen.value()) : 16; std::static_pointer_cast(cipherInstance)->init(args.cipherKey, args.iv, tag_len); + EVP_CIPHER_free(cipher); return cipherInstance; } case EVP_CIPH_CCM_MODE: { cipherInstance = std::make_shared(); cipherInstance->setArgs(args); cipherInstance->init(args.cipherKey, args.iv); + EVP_CIPHER_free(cipher); return cipherInstance; } case EVP_CIPH_STREAM_CIPHER: { @@ -55,12 +57,14 @@ class HybridCipherFactory : public HybridCipherFactorySpec { cipherInstance = std::make_shared(); cipherInstance->setArgs(args); cipherInstance->init(args.cipherKey, args.iv); + EVP_CIPHER_free(cipher); return cipherInstance; } if (cipherName == "chacha20-poly1305") { cipherInstance = std::make_shared(); cipherInstance->setArgs(args); cipherInstance->init(args.cipherKey, args.iv); + EVP_CIPHER_free(cipher); return cipherInstance; } } @@ -69,6 +73,7 @@ class HybridCipherFactory : public HybridCipherFactorySpec { cipherInstance = std::make_shared(); cipherInstance->setArgs(args); cipherInstance->init(args.cipherKey, args.iv); + EVP_CIPHER_free(cipher); return cipherInstance; } } diff --git a/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp new file mode 100644 index 00000000..80b37fb5 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp @@ -0,0 +1,198 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// OpenSSL EC parameter encoding constants +#ifndef OPENSSL_EC_EXPLICIT_CURVE +#define OPENSSL_EC_EXPLICIT_CURVE 0x000 +#endif +#ifndef OPENSSL_EC_NAMED_CURVE +#define OPENSSL_EC_NAMED_CURVE 0x001 +#endif + +#include "HybridEcKeyPair.hpp" +#include "Utils.hpp" + +namespace margelo::nitro::crypto { + +std::shared_ptr> HybridEcKeyPair::generateKeyPair() { + return Promise::async([this]() { this->generateKeyPairSync(); }); +} + +void HybridEcKeyPair::generateKeyPairSync() { + if (this->curve.empty()) { + throw std::runtime_error("EC curve not set. Call setCurve() first."); + } + + // Clean up existing key if any + if (this->pkey != nullptr) { + EVP_PKEY_free(this->pkey); + this->pkey = nullptr; + } + + // Get curve NID from curve name + int curve_nid = GetCurveFromName(this->curve.c_str()); + if (curve_nid == NID_undef) { + throw std::runtime_error("Invalid or unsupported curve: " + this->curve); + } + + std::unique_ptr key_ctx(nullptr, EVP_PKEY_CTX_free); + + // Handle special curves (Ed25519, X25519, etc.) + switch (curve_nid) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + key_ctx.reset(EVP_PKEY_CTX_new_id(curve_nid, nullptr)); + break; + default: { + // Standard EC curves + std::unique_ptr param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), EVP_PKEY_CTX_free); + + if (!param_ctx) { + throw std::runtime_error("Failed to create parameter context"); + } + + if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) { + throw std::runtime_error("Failed to initialize parameter generation"); + } + + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(param_ctx.get(), curve_nid) <= 0) { + throw std::runtime_error("Failed to set curve NID"); + } + + if (EVP_PKEY_CTX_set_ec_param_enc(param_ctx.get(), OPENSSL_EC_NAMED_CURVE) <= 0) { + throw std::runtime_error("Failed to set parameter encoding"); + } + + EVP_PKEY* raw_params = nullptr; + if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) { + throw std::runtime_error("Failed to generate parameters"); + } + + std::unique_ptr key_params(raw_params, EVP_PKEY_free); + key_ctx.reset(EVP_PKEY_CTX_new(key_params.get(), nullptr)); + break; + } + } + + if (!key_ctx) { + throw std::runtime_error("Failed to create key generation context"); + } + + if (EVP_PKEY_keygen_init(key_ctx.get()) <= 0) { + throw std::runtime_error("Failed to initialize key generation"); + } + + EVP_PKEY* raw_pkey = nullptr; + if (EVP_PKEY_keygen(key_ctx.get(), &raw_pkey) <= 0) { + throw std::runtime_error("Failed to generate EC key pair"); + } + + this->pkey = raw_pkey; +} + +KeyObject HybridEcKeyPair::importKey(const std::string& format, const std::shared_ptr& keyData, const std::string& algorithm, + bool extractable, const std::vector& keyUsages) { + throw std::runtime_error("HybridEcKeyPair::importKey() is not yet implemented"); +} + +std::shared_ptr HybridEcKeyPair::exportKey(const KeyObject& key, const std::string& format) { + throw std::runtime_error("HybridEcKeyPair::exportKey() is not yet implemented"); +} + +std::shared_ptr HybridEcKeyPair::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 HybridEcKeyPair::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); +} + +void HybridEcKeyPair::setCurve(const std::string& curve) { + this->curve = curve; +} + +int HybridEcKeyPair::GetCurveFromName(const char* name) { + // Handle NIST curve name mappings first + std::string curve_name(name); + if (curve_name == "P-256") { + return NID_X9_62_prime256v1; + } else if (curve_name == "P-384") { + return NID_secp384r1; + } else if (curve_name == "P-521") { + return NID_secp521r1; + } else if (curve_name == "secp256k1") { + return NID_secp256k1; + } + + // Try standard OpenSSL name resolution + int nid = OBJ_txt2nid(name); + if (nid == NID_undef) { + // Try short names + nid = OBJ_sn2nid(name); + } + if (nid == NID_undef) { + // Try long names + nid = OBJ_ln2nid(name); + } + return nid; +} + +void HybridEcKeyPair::checkKeyPair() { + if (this->pkey == nullptr) { + throw std::runtime_error("EC KeyPair not initialized"); + } +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp new file mode 100644 index 00000000..3bdeda4f --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include +#include + +#include "HybridEcKeyPairSpec.hpp" +#include "Utils.hpp" + +namespace margelo::nitro::crypto { + +class HybridEcKeyPair : public HybridEcKeyPairSpec { + public: + HybridEcKeyPair() : HybridObject(TAG) {} + ~HybridEcKeyPair() { + if (pkey != nullptr) { + EVP_PKEY_free(pkey); + pkey = nullptr; + } + } + + public: + // Methods + std::shared_ptr> generateKeyPair() override; + void generateKeyPairSync() 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; + std::shared_ptr getPublicKey() override; + std::shared_ptr getPrivateKey() override; + void setCurve(const std::string& curve) override; + + protected: + void checkKeyPair(); + + private: + std::string curve; + EVP_PKEY* pkey = nullptr; + + static int GetCurveFromName(const char* name); +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp index 1d5b3971..cb36d986 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp @@ -76,6 +76,19 @@ std::shared_ptr> HybridEdKeyPair::generateKeyPair(double publicFor void HybridEdKeyPair::generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType, const std::optional& cipher, const std::optional>& passphrase) { + // Clear any previous OpenSSL errors to prevent pollution + clearOpenSSLErrors(); + + if (this->curve.empty()) { + throw std::runtime_error("EC curve not set. Call setCurve() first."); + } + + // Clean up existing key if any + if (this->pkey != nullptr) { + EVP_PKEY_free(this->pkey); + this->pkey = nullptr; + } + EVP_PKEY_CTX* pctx; // key context @@ -116,6 +129,8 @@ std::shared_ptr>> HybridEdKeyPair::sign(con std::shared_ptr HybridEdKeyPair::signSync(const std::shared_ptr& message, const std::optional>& key) { + // Clear any previous OpenSSL errors to prevent pollution + clearOpenSSLErrors(); size_t sig_len = 0; uint8_t* sig = NULL; @@ -128,18 +143,18 @@ std::shared_ptr HybridEdKeyPair::signSync(const std::shared_ptrcurve.c_str(), nullptr); if (pkey_ctx == nullptr) { - EVP_PKEY_CTX_free(pkey_ctx); + EVP_MD_CTX_free(md_ctx); throw std::runtime_error("Error creating signing context: " + this->curve); } if (EVP_DigestSignInit(md_ctx, &pkey_ctx, NULL, NULL, pkey) <= 0) { EVP_MD_CTX_free(md_ctx); + EVP_PKEY_CTX_free(pkey_ctx); char* err = ERR_error_string(ERR_get_error(), NULL); throw std::runtime_error("Failed to initialize signing: " + std::string(err)); } @@ -154,6 +169,7 @@ std::shared_ptr HybridEdKeyPair::signSync(const std::shared_ptrdata(), message.get()->size()) <= 0) { EVP_MD_CTX_free(md_ctx); + delete[] sig; throw std::runtime_error("Failed to calculate signature"); } @@ -162,6 +178,7 @@ std::shared_ptr HybridEdKeyPair::signSync(const std::shared_ptr> HybridEdKeyPair::verify(const std::shared_ptr& signature, const std::shared_ptr& message, const std::optional>& key) { + // Clear any previous OpenSSL errors to prevent pollution + clearOpenSSLErrors(); + // get key to use for verifying EVP_PKEY* pkey = this->importPublicKey(key); @@ -192,18 +212,18 @@ bool HybridEdKeyPair::verifySync(const std::shared_ptr& signature, // key context md_ctx = EVP_MD_CTX_new(); if (md_ctx == nullptr) { - EVP_MD_CTX_free(md_ctx); throw std::runtime_error("Error creating verify context"); } pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, this->curve.c_str(), nullptr); if (pkey_ctx == nullptr) { - EVP_PKEY_CTX_free(pkey_ctx); + EVP_MD_CTX_free(md_ctx); throw std::runtime_error("Error creating verify context: " + this->curve); } if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, NULL, NULL, pkey) <= 0) { EVP_MD_CTX_free(md_ctx); + EVP_PKEY_CTX_free(pkey_ctx); char* err = ERR_error_string(ERR_get_error(), NULL); throw std::runtime_error("Failed to initialize verify: " + std::string(err)); } diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp index 7f9b4b37..7d608b31 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp @@ -11,6 +11,12 @@ namespace margelo::nitro::crypto { class HybridEdKeyPair : public HybridEdKeyPairSpec { public: HybridEdKeyPair() : HybridObject(TAG) {} + ~HybridEdKeyPair() { + if (pkey != nullptr) { + EVP_PKEY_free(pkey); + pkey = nullptr; + } + } public: // Methods diff --git a/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp b/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp index a8fcfdc5..307c2374 100644 --- a/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp +++ b/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp @@ -2,12 +2,12 @@ #include #include #include -#include #include #include #include #include "HybridHash.hpp" +#include "Utils.hpp" namespace margelo::nitro::crypto { @@ -23,6 +23,20 @@ HybridHash::~HybridHash() { } void HybridHash::createHash(const std::string& hashAlgorithmArg, const std::optional outputLengthArg) { + // Clear any previous OpenSSL errors to prevent pollution + clearOpenSSLErrors(); + + // Clean up existing resources before creating new ones + if (ctx) { + EVP_MD_CTX_free(ctx); + ctx = nullptr; + } + if (md && md_fetched) { + EVP_MD_free(md); + md = nullptr; + md_fetched = false; + } + algorithm = hashAlgorithmArg; outputLength = outputLengthArg; diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp index 9bf81c82..4f6eb4b7 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp @@ -3,7 +3,9 @@ #include "CFRGKeyPairType.hpp" #include "HybridKeyObjectHandle.hpp" #include "Utils.hpp" +#include #include +#include namespace margelo::nitro::crypto { @@ -132,9 +134,16 @@ bool HybridKeyObjectHandle::init(KeyType keyType, const std::variant>(key); } - // Handle raw asymmetric key material (curves only) + // 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)) { - return initRawKey(keyType, ab); + size_t keySize = ab->size(); + // Only route to initRawKey for exact special curve sizes: + // X25519/Ed25519: 32 bytes, X448: 56 bytes, Ed448: 57 bytes + // DER-encoded keys will be much larger and should use standard parsing + if ((keySize == 32) || (keySize == 56) || (keySize == 57)) { + return initRawKey(keyType, ab); + } + // For larger sizes (DER-encoded keys), fall through to standard parsing } switch (keyType) { @@ -159,16 +168,37 @@ bool HybridKeyObjectHandle::init(KeyType keyType, const std::variant& keyData) { - throw std::runtime_error("Not yet implemented"); -} - std::optional HybridKeyObjectHandle::initJwk(const JWK& keyData, std::optional namedCurve) { throw std::runtime_error("Not yet implemented"); } KeyDetail HybridKeyObjectHandle::keyDetail() { - throw std::runtime_error("Not yet implemented"); + const auto& pkey_ptr = data_.GetAsymmetricKey(); + if (!pkey_ptr) { + return KeyDetail{}; + } + + EVP_PKEY* pkey = pkey_ptr.get(); + + if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC) { + // Extract EC curve name + EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(pkey); + if (ec_key) { + const EC_GROUP* group = EC_KEY_get0_group(ec_key); + if (group) { + int nid = EC_GROUP_get_curve_name(group); + const char* curve_name = OBJ_nid2sn(nid); + if (curve_name) { + std::string namedCurve(curve_name); + EC_KEY_free(ec_key); + return KeyDetail(std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, namedCurve); + } + } + EC_KEY_free(ec_key); + } + } + + return KeyDetail{}; } bool HybridKeyObjectHandle::initRawKey(KeyType keyType, std::shared_ptr keyData) { diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp index 2b755549..5e9b38d4 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp @@ -29,8 +29,6 @@ class HybridKeyObjectHandle : public HybridKeyObjectHandleSpec { bool init(KeyType keyType, const std::variant>& key, std::optional format, std::optional type, const std::optional>& passphrase) override; - bool initECRaw(const std::string& curveName, const std::shared_ptr& keyData) override; - std::optional initJwk(const JWK& keyData, std::optional namedCurve) override; KeyDetail keyDetail() override; diff --git a/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp b/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp index c11c35bb..2eadb487 100644 --- a/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.cpp @@ -12,6 +12,14 @@ ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig GetPrivateKeyEncodingConfig(KFo return config; } +ncrypto::EVPKeyPointer::PublicKeyEncodingConfig GetPublicKeyEncodingConfig(KFormatType format, KeyEncoding type) { + auto pk_format = static_cast(format); + auto pk_type = static_cast(type); + + auto config = ncrypto::EVPKeyPointer::PublicKeyEncodingConfig(false, pk_format, pk_type); + return config; +} + KeyObjectData TryParsePrivateKey(std::shared_ptr key, std::optional format, std::optional type, const std::optional>& passphrase) { auto config = GetPrivateKeyEncodingConfig(format.value(), type.value()); @@ -85,44 +93,96 @@ size_t KeyObjectData::GetSymmetricKeySize() const { KeyObjectData KeyObjectData::GetPublicOrPrivateKey(std::shared_ptr key, std::optional format, std::optional type, const std::optional>& passphrase) { - if (!CheckIsInt32(key->size())) { - throw std::runtime_error("key is too big (int32)"); + // 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); } - if (format.has_value() && format.value() == KFormatType::PEM) { - // For PEM, we can easily determine whether it is a public or private key - // by looking for the respective PEM tags. - auto config = GetPrivateKeyEncodingConfig(format.value(), type.value()); + if (format.has_value() && (format.value() == KFormatType::PEM || format.value() == KFormatType::DER)) { auto buffer = ncrypto::Buffer{key->data(), key->size()}; - auto res = ncrypto::EVPKeyPointer::TryParsePublicKeyPEM(buffer); - if (res) { - return CreateAsymmetric(KeyType::PUBLIC, std::move(res.value)); - } - if (res.error.has_value() && res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NOT_RECOGNIZED) { - if (passphrase.has_value()) { - auto& passphrase_ptr = passphrase.value(); - config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); + if (format.value() == 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)); } - auto private_res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, buffer); - if (private_res) { - return CreateAsymmetric(KeyType::PRIVATE, std::move(private_res.value)); + if (res.error.has_value() && res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NOT_RECOGNIZED) { + auto config = GetPrivateKeyEncodingConfig(format.value(), type.value()); + if (passphrase.has_value()) { + auto& passphrase_ptr = passphrase.value(); + config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); + } + + 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"); + } else if (format.value() == KFormatType::DER) { + // For DER, try parsing as public key first + if (type.has_value() && type.value() == KeyEncoding::SPKI) { + auto public_config = GetPublicKeyEncodingConfig(format.value(), type.value()); + auto res = ncrypto::EVPKeyPointer::TryParsePublicKey(public_config, buffer); + if (res) { + return CreateAsymmetric(KeyType::PUBLIC, std::move(res.value)); + } + } else if (type.has_value() && type.value() == KeyEncoding::PKCS8) { + auto private_config = GetPrivateKeyEncodingConfig(format.value(), type.value()); + if (passphrase.has_value()) { + auto& passphrase_ptr = passphrase.value(); + private_config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); + } + auto res = ncrypto::EVPKeyPointer::TryParsePrivateKey(private_config, buffer); + if (res) { + return CreateAsymmetric(KeyType::PRIVATE, std::move(res.value)); + } } - // TODO: Handle private key parsing errors + throw std::runtime_error("Failed to read DER asymmetric key"); } - throw std::runtime_error("Failed to read asymmetric key"); } - throw std::runtime_error("Unsupported key format for GetPublicOrPrivateKey. Only PEM is supported."); + throw std::runtime_error("Unsupported key format for GetPublicOrPrivateKey. Only PEM and DER are supported."); } KeyObjectData KeyObjectData::GetPrivateKey(std::shared_ptr key, std::optional format, std::optional type, const std::optional>& passphrase, bool isPublic) { - // TODO: Node's KeyObjectData::GetPrivateKeyFromJs checks for key "IsString" or "IsAnyBufferSource" - // We have converted key to an ArrayBuffer - not sure if that's correct - return TryParsePrivateKey(key, format, type, 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); + } + + if (format.has_value() && (format.value() == KFormatType::PEM || format.value() == KFormatType::DER)) { + auto buffer = ncrypto::Buffer{key->data(), key->size()}; + + if (format.value() == KFormatType::PEM) { + return TryParsePrivateKey(key, format, type, passphrase); + } else if (format.value() == KFormatType::DER) { + // For DER private keys, use PKCS8 encoding + if (type.has_value() && type.value() == KeyEncoding::PKCS8) { + auto private_config = GetPrivateKeyEncodingConfig(format.value(), type.value()); + if (passphrase.has_value()) { + auto& passphrase_ptr = passphrase.value(); + private_config.passphrase = std::make_optional(ncrypto::DataPointer(passphrase_ptr->data(), passphrase_ptr->size())); + } + auto res = ncrypto::EVPKeyPointer::TryParsePrivateKey(private_config, buffer); + if (res) { + return CreateAsymmetric(KeyType::PRIVATE, std::move(res.value)); + } + } + throw std::runtime_error("Failed to read DER private key"); + } + } + + throw std::runtime_error("Unsupported key format for GetPrivateKey. Only PEM and DER are supported."); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/utils/Utils.hpp b/packages/react-native-quick-crypto/cpp/utils/Utils.hpp index 2307d7b2..d058a871 100644 --- a/packages/react-native-quick-crypto/cpp/utils/Utils.hpp +++ b/packages/react-native-quick-crypto/cpp/utils/Utils.hpp @@ -11,7 +11,7 @@ namespace margelo::nitro::crypto { -// Function to get the last OpenSSL error message +// Function to get the last OpenSSL error message and clear the error stack inline std::string getOpenSSLError() { unsigned long errCode = ERR_get_error(); if (errCode == 0) { @@ -19,9 +19,16 @@ inline std::string getOpenSSLError() { } char errStr[256]; ERR_error_string_n(errCode, errStr, sizeof(errStr)); + // Clear any remaining errors from the error stack to prevent pollution + ERR_clear_error(); return std::string(errStr); } +// Function to clear OpenSSL error stack without getting error message +inline void clearOpenSSLErrors() { + ERR_clear_error(); +} + // copy a JSArrayBuffer that we do not own into a NativeArrayBuffer that we do own inline std::shared_ptr ToNativeArrayBuffer(const std::shared_ptr& buffer) { size_t bufferSize = buffer.get()->size(); diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 51b5c03e..ffb401d4 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -10,6 +10,7 @@ "autolinking": { "Cipher": { "cpp": "HybridCipher" }, "CipherFactory": { "cpp": "HybridCipherFactory" }, + "EcKeyPair": { "cpp": "HybridEcKeyPair" }, "EdKeyPair": { "cpp": "HybridEdKeyPair" }, "Hash": { "cpp": "HybridHash" }, "Hmac": { "cpp": "HybridHmac" }, 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 da3e7bf3..93a77f7d 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 @@ -29,6 +29,7 @@ target_sources( # Shared Nitrogen C++ sources ../nitrogen/generated/shared/c++/HybridCipherSpec.cpp ../nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp + ../nitrogen/generated/shared/c++/HybridEcKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridEdKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridHashSpec.cpp ../nitrogen/generated/shared/c++/HybridHmacSpec.cpp 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 c909cbb4..6e1d5dc6 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -17,6 +17,7 @@ #include "HybridCipher.hpp" #include "HybridCipherFactory.hpp" +#include "HybridEcKeyPair.hpp" #include "HybridEdKeyPair.hpp" #include "HybridHash.hpp" #include "HybridHmac.hpp" @@ -54,6 +55,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "EcKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridEcKeyPair\" 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( "EdKeyPair", []() -> std::shared_ptr { 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 d928c816..797937b4 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -12,6 +12,7 @@ #include "HybridCipher.hpp" #include "HybridCipherFactory.hpp" +#include "HybridEcKeyPair.hpp" #include "HybridEdKeyPair.hpp" #include "HybridHash.hpp" #include "HybridHmac.hpp" @@ -46,6 +47,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "EcKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridEcKeyPair\" 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( "EdKeyPair", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEcKeyPairSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEcKeyPairSpec.cpp new file mode 100644 index 00000000..2f97819c --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEcKeyPairSpec.cpp @@ -0,0 +1,27 @@ +/// +/// HybridEcKeyPairSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridEcKeyPairSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridEcKeyPairSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("generateKeyPair", &HybridEcKeyPairSpec::generateKeyPair); + prototype.registerHybridMethod("generateKeyPairSync", &HybridEcKeyPairSpec::generateKeyPairSync); + prototype.registerHybridMethod("importKey", &HybridEcKeyPairSpec::importKey); + prototype.registerHybridMethod("exportKey", &HybridEcKeyPairSpec::exportKey); + prototype.registerHybridMethod("getPublicKey", &HybridEcKeyPairSpec::getPublicKey); + prototype.registerHybridMethod("getPrivateKey", &HybridEcKeyPairSpec::getPrivateKey); + prototype.registerHybridMethod("setCurve", &HybridEcKeyPairSpec::setCurve); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEcKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEcKeyPairSpec.hpp new file mode 100644 index 00000000..0b3fddce --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridEcKeyPairSpec.hpp @@ -0,0 +1,75 @@ +/// +/// HybridEcKeyPairSpec.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 `KeyObject` to properly resolve imports. +namespace margelo::nitro::crypto { struct KeyObject; } +// Forward declaration of `ArrayBuffer` to properly resolve imports. +namespace NitroModules { class ArrayBuffer; } + +#include +#include "KeyObject.hpp" +#include +#include +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `EcKeyPair` + * Inherit this class to create instances of `HybridEcKeyPairSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridEcKeyPair: public HybridEcKeyPairSpec { + * public: + * HybridEcKeyPair(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridEcKeyPairSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridEcKeyPairSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridEcKeyPairSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> generateKeyPair() = 0; + virtual void generateKeyPairSync() = 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; + virtual void setCurve(const std::string& curve) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "EcKeyPair"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp index 3094479c..5c0bdce7 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp @@ -18,7 +18,6 @@ namespace margelo::nitro::crypto { prototype.registerHybridMethod("exportJwk", &HybridKeyObjectHandleSpec::exportJwk); prototype.registerHybridMethod("getAsymmetricKeyType", &HybridKeyObjectHandleSpec::getAsymmetricKeyType); prototype.registerHybridMethod("init", &HybridKeyObjectHandleSpec::init); - prototype.registerHybridMethod("initECRaw", &HybridKeyObjectHandleSpec::initECRaw); prototype.registerHybridMethod("initJwk", &HybridKeyObjectHandleSpec::initJwk); prototype.registerHybridMethod("keyDetail", &HybridKeyObjectHandleSpec::keyDetail); }); diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp index 53b6006a..cbbebb73 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp @@ -77,7 +77,6 @@ namespace margelo::nitro::crypto { virtual JWK exportJwk(const JWK& key, bool handleRsaPss) = 0; virtual CFRGKeyPairType getAsymmetricKeyType() = 0; virtual bool init(KeyType keyType, const std::variant>& key, std::optional format, std::optional type, const std::optional>& passphrase) = 0; - virtual bool initECRaw(const std::string& curveName, const std::shared_ptr& keyData) = 0; virtual std::optional initJwk(const JWK& keyData, std::optional namedCurve) = 0; virtual KeyDetail keyDetail() = 0; diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyObject.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyObject.hpp new file mode 100644 index 00000000..e27d4ff9 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyObject.hpp @@ -0,0 +1,67 @@ +/// +/// KeyObject.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 +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + + + +namespace margelo::nitro::crypto { + + /** + * A struct which can be represented as a JavaScript object (KeyObject). + */ + struct KeyObject { + public: + bool extractable SWIFT_PRIVATE; + + public: + KeyObject() = default; + explicit KeyObject(bool extractable): extractable(extractable) {} + }; + +} // namespace margelo::nitro::crypto + +namespace margelo::nitro { + + // C++ KeyObject <> JS KeyObject (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::crypto::KeyObject fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::crypto::KeyObject( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "extractable")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::crypto::KeyObject& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "extractable", JSIConverter::toJSI(runtime, arg.extractable)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "extractable"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp index a9b38f87..0c7dfc6f 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KeyUsage.hpp @@ -35,8 +35,12 @@ namespace margelo::nitro::crypto { VERIFY SWIFT_NAME(verify) = 3, DERIVEKEY SWIFT_NAME(derivekey) = 4, DERIVEBITS SWIFT_NAME(derivebits) = 5, - WRAPKEY SWIFT_NAME(wrapkey) = 6, - UNWRAPKEY SWIFT_NAME(unwrapkey) = 7, + ENCAPSULATEBITS SWIFT_NAME(encapsulatebits) = 6, + DECAPSULATEBITS SWIFT_NAME(decapsulatebits) = 7, + ENCAPSULATEKEY SWIFT_NAME(encapsulatekey) = 8, + DECAPSULATEKEY SWIFT_NAME(decapsulatekey) = 9, + WRAPKEY SWIFT_NAME(wrapkey) = 10, + UNWRAPKEY SWIFT_NAME(unwrapkey) = 11, } CLOSED_ENUM; } // namespace margelo::nitro::crypto @@ -55,6 +59,10 @@ namespace margelo::nitro { case hashString("verify"): return margelo::nitro::crypto::KeyUsage::VERIFY; case hashString("deriveKey"): return margelo::nitro::crypto::KeyUsage::DERIVEKEY; case hashString("deriveBits"): return margelo::nitro::crypto::KeyUsage::DERIVEBITS; + case hashString("encapsulateBits"): return margelo::nitro::crypto::KeyUsage::ENCAPSULATEBITS; + case hashString("decapsulateBits"): return margelo::nitro::crypto::KeyUsage::DECAPSULATEBITS; + case hashString("encapsulateKey"): return margelo::nitro::crypto::KeyUsage::ENCAPSULATEKEY; + case hashString("decapsulateKey"): return margelo::nitro::crypto::KeyUsage::DECAPSULATEKEY; case hashString("wrapKey"): return margelo::nitro::crypto::KeyUsage::WRAPKEY; case hashString("unwrapKey"): return margelo::nitro::crypto::KeyUsage::UNWRAPKEY; default: [[unlikely]] @@ -69,6 +77,10 @@ namespace margelo::nitro { case margelo::nitro::crypto::KeyUsage::VERIFY: return JSIConverter::toJSI(runtime, "verify"); case margelo::nitro::crypto::KeyUsage::DERIVEKEY: return JSIConverter::toJSI(runtime, "deriveKey"); case margelo::nitro::crypto::KeyUsage::DERIVEBITS: return JSIConverter::toJSI(runtime, "deriveBits"); + case margelo::nitro::crypto::KeyUsage::ENCAPSULATEBITS: return JSIConverter::toJSI(runtime, "encapsulateBits"); + case margelo::nitro::crypto::KeyUsage::DECAPSULATEBITS: return JSIConverter::toJSI(runtime, "decapsulateBits"); + case margelo::nitro::crypto::KeyUsage::ENCAPSULATEKEY: return JSIConverter::toJSI(runtime, "encapsulateKey"); + case margelo::nitro::crypto::KeyUsage::DECAPSULATEKEY: return JSIConverter::toJSI(runtime, "decapsulateKey"); case margelo::nitro::crypto::KeyUsage::WRAPKEY: return JSIConverter::toJSI(runtime, "wrapKey"); case margelo::nitro::crypto::KeyUsage::UNWRAPKEY: return JSIConverter::toJSI(runtime, "unwrapKey"); default: [[unlikely]] @@ -88,6 +100,10 @@ namespace margelo::nitro { case hashString("verify"): case hashString("deriveKey"): case hashString("deriveBits"): + case hashString("encapsulateBits"): + case hashString("decapsulateBits"): + case hashString("encapsulateKey"): + case hashString("decapsulateKey"): case hashString("wrapKey"): case hashString("unwrapKey"): return true; diff --git a/packages/react-native-quick-crypto/package.json b/packages/react-native-quick-crypto/package.json index ef129955..b371ed8b 100644 --- a/packages/react-native-quick-crypto/package.json +++ b/packages/react-native-quick-crypto/package.json @@ -75,7 +75,7 @@ "@craftzdog/react-native-buffer": "6.1.0", "events": "3.3.0", "expo-build-properties": "0.14.6", - "react-native-quick-base64": "2.2.0", + "react-native-quick-base64": "2.2.2", "readable-stream": "4.5.2", "util": "0.12.5" }, diff --git a/packages/react-native-quick-crypto/src/ec.ts b/packages/react-native-quick-crypto/src/ec.ts new file mode 100644 index 00000000..44212a8c --- /dev/null +++ b/packages/react-native-quick-crypto/src/ec.ts @@ -0,0 +1,362 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { + PublicKeyObject, + PrivateKeyObject, + CryptoKey, + KeyObject, +} from './keys/classes'; +import type { EcKeyPair } from './specs/ecKeyPair.nitro'; +import { + // KeyType, + // KeyFormat, + // ab2str, + // bufferLikeToArrayBuffer, + // binaryLikeToArrayBuffer, + getUsagesUnion, + hasAnyNotIn, + kNamedCurveAliases, + lazyDOMException, + // normalizeHashName, + // validateKeyOps, +} from './utils'; +import type { + // AnyAlgorithm, + // BufferLike, + // BinaryLike, + CryptoKeyPair, + // ImportFormat, + KeyUsage, + // NamedCurve, + // JWK, + SubtleAlgorithm, + // AsymmetricKeyType, + // KeyObjectHandle, +} from './utils'; + +export class Ec { + native: EcKeyPair; + + constructor(curve: string) { + this.native = NitroModules.createHybridObject('EcKeyPair'); + this.native.setCurve(curve); + } + + 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(), + }; + } +} + +// function verifyAcceptableEcKeyUse( +// name: AnyAlgorithm, +// isPublic: boolean, +// usages: KeyUsage[], +// ): void { +// let checkSet; +// switch (name) { +// case 'ECDH': +// checkSet = isPublic ? [] : ['deriveKey', 'deriveBits']; +// break; +// case 'ECDSA': +// checkSet = isPublic ? ['verify'] : ['sign']; +// break; +// default: +// throw lazyDOMException( +// 'The algorithm is not supported', +// 'NotSupportedError', +// ); +// } +// if (hasAnyNotIn(usages, checkSet)) { +// throw lazyDOMException( +// `Unsupported key usage for a ${name} key`, +// 'SyntaxError', +// ); +// } +// } + +// function createECPublicKeyRaw( +// namedCurve: NamedCurve | undefined, +// keyData: ArrayBuffer, +// ): PublicKeyObject { +// if (!namedCurve) { +// throw new Error('Invalid namedCurve'); +// } +// const handle = NitroModules.createHybridObject( +// 'KeyObjectHandle', +// ) as KeyObjectHandle; + +// if (!handle.initECRaw(kNamedCurveAliases[namedCurve], keyData)) { +// console.log('keyData', ab2str(keyData)); +// throw new Error('Invalid keyData 1'); +// } + +// return new PublicKeyObject(handle); +// } + +// // Node API +// export function ec_exportKey(key: CryptoKey, format: KeyFormat): ArrayBuffer { +// return ec.native.exportKey(format, key.keyObject.handle); +// } + +// // Node API +// export function ecImportKey( +// format: ImportFormat, +// keyData: BufferLike | BinaryLike | JWK, +// algorithm: SubtleAlgorithm, +// extractable: boolean, +// keyUsages: KeyUsage[], +// ): CryptoKey { +// const { name, namedCurve } = algorithm; + +// // if (!ArrayPrototypeIncludes(ObjectKeys(kNamedCurveAliases), namedCurve)) { +// // throw lazyDOMException('Unrecognized namedCurve', 'NotSupportedError'); +// // } + +// let keyObject; +// // const usagesSet = new SafeSet(keyUsages); +// switch (format) { +// // case 'spki': { +// // // verifyAcceptableEcKeyUse(name, true, usagesSet); +// // try { +// // keyObject = createPublicKey({ +// // key: keyData, +// // format: 'der', +// // type: 'spki', +// // }); +// // } catch (err) { +// // throw new Error(`Invalid keyData 2: ${err}`); +// // } +// // break; +// // } +// // case 'pkcs8': { +// // // verifyAcceptableEcKeyUse(name, false, usagesSet); +// // try { +// // keyObject = createPrivateKey({ +// // key: keyData, +// // format: 'der', +// // type: 'pkcs8', +// // }); +// // } catch (err) { +// // throw new Error(`Invalid keyData 3 ${err}`); +// // } +// // break; +// // } +// case 'jwk': { +// const data = keyData as JWK; + +// if (!data.kty) throw lazyDOMException('Invalid keyData 4', 'DataError'); +// if (data.kty !== 'EC') +// throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); +// if (data.crv !== namedCurve) +// throw lazyDOMException( +// 'JWK "crv" does not match the requested algorithm', +// 'DataError', +// ); + +// verifyAcceptableEcKeyUse(name, data.d === undefined, keyUsages); + +// if (keyUsages.length > 0 && data.use !== undefined) { +// const checkUse = name === 'ECDH' ? 'enc' : 'sig'; +// if (data.use !== checkUse) +// throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); +// } + +// validateKeyOps(data.key_ops, keyUsages); + +// if ( +// data.ext !== undefined && +// data.ext === false && +// extractable === true +// ) { +// throw lazyDOMException( +// 'JWK "ext" Parameter and extractable mismatch', +// 'DataError', +// ); +// } + +// if (algorithm.name === 'ECDSA' && data.alg !== undefined) { +// let algNamedCurve; +// switch (data.alg) { +// case 'ES256': +// algNamedCurve = 'P-256'; +// break; +// case 'ES384': +// algNamedCurve = 'P-384'; +// break; +// case 'ES512': +// algNamedCurve = 'P-521'; +// break; +// } +// if (algNamedCurve !== namedCurve) +// throw lazyDOMException( +// 'JWK "alg" does not match the requested algorithm', +// 'DataError', +// ); +// } + +// const handle = NativeQuickCrypto.webcrypto.createKeyObjectHandle(); +// const type = handle.initJwk(data, namedCurve); +// if (type === undefined) +// throw lazyDOMException('Invalid JWK', 'DataError'); +// keyObject = +// type === KeyType.PRIVATE +// ? new PrivateKeyObject(handle) +// : new PublicKeyObject(handle); +// break; +// } +// case 'raw': { +// const data = keyData as BufferLike | BinaryLike; +// verifyAcceptableEcKeyUse(name, true, keyUsages); +// const buffer = +// typeof data === 'string' +// ? binaryLikeToArrayBuffer(data) +// : bufferLikeToArrayBuffer(data); +// keyObject = createECPublicKeyRaw(namedCurve, buffer); +// break; +// } +// default: { +// throw new Error(`Unknown EC import format: ${format}`); +// } +// } + +// switch (algorithm.name) { +// case 'ECDSA': +// // Fall through +// case 'ECDH': +// if (keyObject.asymmetricKeyType !== ('ec' as AsymmetricKeyType)) +// throw new Error('Invalid key type'); +// break; +// } + +// // if (!keyObject[kHandle].checkEcKeyData()) { +// // throw new Error('Invalid keyData 5'); +// // } + +// // const { namedCurve: checkNamedCurve } = keyObject[kHandle].keyDetail({}); +// // if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) +// // throw new Error('Named curve mismatch'); + +// return new CryptoKey(keyObject, { name, namedCurve }, keyUsages, extractable); +// } + +// // Node API +// export const ecdsaSignVerify = ( +// key: CryptoKey, +// data: BufferLike, +// { hash }: SubtleAlgorithm, +// signature?: BufferLike, +// ) => { +// const mode: SignMode = +// signature === undefined +// ? SignMode.kSignJobModeSign +// : SignMode.kSignJobModeVerify; +// const type = mode === SignMode.kSignJobModeSign ? 'private' : 'public'; + +// if (key.type !== type) +// throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + +// const hashname = normalizeHashName(hash); + +// return NativeQuickCrypto.webcrypto.signVerify( +// mode, +// key.keyObject.handle, +// // three undefined args because C++ uses `GetPublicOrPrivateKeyFromJs` & friends +// undefined, +// undefined, +// undefined, +// bufferLikeToArrayBuffer(data), +// hashname, +// undefined, // salt length, not used with ECDSA +// undefined, // pss padding, not used with ECDSA +// DSASigEnc.kSigEncP1363, +// bufferLikeToArrayBuffer(signature || new ArrayBuffer(0)), +// ); +// }; + +// Node API +export const ec_generateKeyPair = async ( + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], +): Promise => { + const { name, namedCurve } = algorithm; + + // validation checks + if (!Object.keys(kNamedCurveAliases).includes(namedCurve || '')) { + throw lazyDOMException( + `Unrecognized namedCurve '${namedCurve}'`, + 'NotSupportedError', + ); + } + + // const usageSet = new SafeSet(keyUsages); + switch (name) { + case 'ECDSA': + if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { + throw lazyDOMException( + 'Unsupported key usage for an ECDSA key', + 'SyntaxError', + ); + } + break; + case 'ECDH': + if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { + throw lazyDOMException( + 'Unsupported key usage for an ECDH key', + 'SyntaxError', + ); + } + // Fall through + } + + const ec = new Ec(namedCurve!); + await ec.generateKeyPair(); + + let publicUsages: KeyUsage[] = []; + let privateUsages: KeyUsage[] = []; + switch (name) { + case 'ECDSA': + publicUsages = getUsagesUnion(keyUsages, 'verify'); + privateUsages = getUsagesUnion(keyUsages, 'sign'); + break; + case 'ECDH': + publicUsages = []; + privateUsages = getUsagesUnion(keyUsages, 'deriveKey', 'deriveBits'); + break; + } + + const keyAlgorithm = { name, namedCurve: namedCurve! }; + + // Create KeyObject instances using the standard createKeyObject method + const publicKeyData = ec.native.getPublicKey(); + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + ) as PublicKeyObject; + const publicKey = new CryptoKey(pub, keyAlgorithm, publicUsages, true); + + const privateKeyData = ec.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/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index b66530cc..00189a00 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -1,6 +1,6 @@ import { NitroModules } from 'react-native-nitro-modules'; import { Buffer } from '@craftzdog/react-native-buffer'; -import { AsymmetricKeyObject, PrivateKeyObject } from './keys'; +import type { AsymmetricKeyObject, PrivateKeyObject } from './keys'; import type { EdKeyPair } from './specs/edKeyPair.nitro'; import type { BinaryLike, diff --git a/packages/react-native-quick-crypto/src/keys/classes.ts b/packages/react-native-quick-crypto/src/keys/classes.ts index b20b5eb9..33025209 100644 --- a/packages/react-native-quick-crypto/src/keys/classes.ts +++ b/packages/react-native-quick-crypto/src/keys/classes.ts @@ -3,11 +3,12 @@ import { NitroModules } from 'react-native-nitro-modules'; import type { AsymmetricKeyType, EncodingOptions, + KeyDetail, KeyObjectHandle, KeyUsage, SubtleAlgorithm, } from '../utils'; -import { KeyType } from '../utils'; +import { KeyType, KFormatType, KeyEncoding } from '../utils'; import { parsePrivateKeyEncoding, parsePublicKeyEncoding } from './utils'; export class CryptoKey { @@ -136,7 +137,20 @@ export class KeyObject { default: throw new Error('invalid key type'); } - handle.init(keyType, key); + + // Detect DER format by checking if the key starts with DER ASN.1 structure + const keyData = new Uint8Array(key); + const isDER = keyData.length > 10 && keyData[0] === 0x30; // ASN.1 SEQUENCE tag + + if (isDER && (keyType === KeyType.PUBLIC || keyType === KeyType.PRIVATE)) { + // For DER-encoded keys, specify format and type + const format = KFormatType.DER; + const encoding = + keyType === KeyType.PUBLIC ? KeyEncoding.SPKI : KeyEncoding.PKCS8; + handle.init(keyType, key, format, encoding); + } else { + handle.init(keyType, key); + } // For asymmetric keys, return the appropriate subclass if (type === 'public' || type === 'private') { @@ -224,22 +238,18 @@ export class AsymmetricKeyObject extends KeyObject { return this._asymmetricKeyType; } - // get asymmetricKeyDetails() { - // switch (this._asymmetricKeyType) { - // case 'rsa': - // case 'rsa-pss': - // case 'dsa': - // case 'ec': - // return ( - // this[kAsymmetricKeyDetails] || - // (this[kAsymmetricKeyDetails] = normalizeKeyDetails( - // this[kHandle].keyDetail({}) - // )) - // ); - // default: - // return {}; - // } - // } + private _asymmetricKeyDetails?: KeyDetail; + + get asymmetricKeyDetails() { + if (!this._asymmetricKeyDetails) { + this._asymmetricKeyDetails = this.handle.keyDetail(); + } + return this._asymmetricKeyDetails; + } + + get namedCurve(): string | undefined { + return this.asymmetricKeyDetails?.namedCurve; + } } export class PublicKeyObject extends AsymmetricKeyObject { diff --git a/packages/react-native-quick-crypto/src/specs/ecKeyPair.nitro.ts b/packages/react-native-quick-crypto/src/specs/ecKeyPair.nitro.ts new file mode 100644 index 00000000..44bd0532 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/ecKeyPair.nitro.ts @@ -0,0 +1,30 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +// Nitro-compatible interfaces defined locally +interface KeyObject { + extractable: boolean; +} + +export interface EcKeyPair + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + // generateKeyPair functions + generateKeyPair(): Promise; + generateKeyPairSync(): 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; + + setCurve(curve: string): void; +} diff --git a/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts b/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts index e38b59ac..d787e422 100644 --- a/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts @@ -26,7 +26,6 @@ export interface KeyObjectHandle type?: KeyEncoding, passphrase?: ArrayBuffer, ): boolean; - initECRaw(curveName: string, keyData: ArrayBuffer): boolean; initJwk(keyData: JWK, namedCurve?: NamedCurve): KeyType | undefined; keyDetail(): KeyDetail; } diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index c787cd05..92bf5b3c 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -12,7 +12,7 @@ import type { EncryptDecryptParams, Operation, } from './utils'; -import { CryptoKey, KeyObject } from './keys'; +import { CryptoKey } from './keys'; import type { CryptoKeyPair } from './utils/types'; import { bufferLikeToArrayBuffer } from './utils/conversion'; import { lazyDOMException } from './utils/errors'; @@ -21,6 +21,7 @@ import { validateMaxBufferLength } from './utils/validation'; import { asyncDigest } from './hash'; import { createSecretKey } from './keys'; import { pbkdf2DeriveBits } from './pbkdf2'; +import { ec_generateKeyPair } from './ec'; // Placeholder imports - these modules need to be implemented or adapted // import { ecImportKey, ecExportKey, ecGenerateKey, ecdsaSignVerify } from './ec'; @@ -112,32 +113,6 @@ async function rsaKeyGenerate( throw new Error('rsaKeyGenerate not implemented'); } -async function ecGenerateKey( - algorithm: SubtleAlgorithm, - extractable: boolean, - keyUsages: KeyUsage[], -): Promise { - // Temporary implementation - create mock CryptoKey objects - const mockKeyObject = {} as KeyObject; - const publicKey = new CryptoKey( - mockKeyObject, - algorithm, - keyUsages, - extractable, - ); - const privateKey = new CryptoKey( - mockKeyObject, - algorithm, - keyUsages, - extractable, - ); - - return { - publicKey, - privateKey, - }; -} - async function aesGenerateKey( _algorithm: AesKeyGenParams, _extractable: boolean, @@ -531,8 +506,8 @@ export class Subtle { case 'ECDSA': // Fall through case 'ECDH': - result = await ecGenerateKey(algorithm, extractable, keyUsages); - checkCryptoKeyPairUsages(result); + result = await ec_generateKeyPair(algorithm, extractable, keyUsages); + checkCryptoKeyPairUsages(result as CryptoKeyPair); break; case 'AES-CTR': // Fall through diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index 08d7a4a4..040ac2cc 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -158,9 +158,14 @@ export type KeyUsage = | 'verify' | 'deriveKey' | 'deriveBits' + | 'encapsulateBits' + | 'decapsulateBits' + | 'encapsulateKey' + | 'decapsulateKey' | 'wrapKey' | 'unwrapKey'; +// TODO: These enums need to be defined on the native side export enum KFormatType { DER, PEM, @@ -180,6 +185,22 @@ export enum KeyEncoding { SEC1, } +export enum KeyFormat { + RAW, + PKCS8, + SPKI, + JWK, +} + +export type KeyData = BufferLike | BinaryLike | JWK; + +export const kNamedCurveAliases = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1', +} as const; +// end TODO + export type KeyPairGenConfig = { publicFormat?: KFormatType; publicType?: KeyEncoding; diff --git a/packages/react-native-quick-crypto/src/utils/validation.ts b/packages/react-native-quick-crypto/src/utils/validation.ts index ca8cafce..ba8dd158 100644 --- a/packages/react-native-quick-crypto/src/utils/validation.ts +++ b/packages/react-native-quick-crypto/src/utils/validation.ts @@ -1,5 +1,5 @@ import { Buffer as SBuffer } from 'safe-buffer'; -import type { BinaryLike, BufferLike } from './types'; +import type { BinaryLike, BufferLike, KeyUsage } from './types'; import { lazyDOMException } from './errors'; // The maximum buffer size that we'll support in the WebCrypto impl @@ -56,3 +56,75 @@ export const validateMaxBufferLength = ( ); } }; + +export const getUsagesUnion = (usageSet: KeyUsage[], ...usages: KeyUsage[]) => { + const newset: KeyUsage[] = []; + for (let n = 0; n < usages.length; n++) { + if (!usages[n] || usages[n] === undefined) continue; + if (usageSet.includes(usages[n] as KeyUsage)) + newset.push(usages[n] as KeyUsage); + } + return newset; +}; + +const kKeyOps: { + [key in KeyUsage]: number; +} = { + sign: 1, + verify: 2, + encrypt: 3, + decrypt: 4, + wrapKey: 5, + unwrapKey: 6, + deriveKey: 7, + deriveBits: 8, + encapsulateBits: 9, + decapsulateBits: 10, + encapsulateKey: 11, + decapsulateKey: 12, +}; + +export const validateKeyOps = ( + keyOps: KeyUsage[] | undefined, + usagesSet: KeyUsage[], +) => { + if (keyOps === undefined) return; + if (!Array.isArray(keyOps)) { + throw lazyDOMException('keyData.key_ops', 'InvalidArgument'); + } + let flags = 0; + for (let n = 0; n < keyOps.length; n++) { + const op: KeyUsage = keyOps[n] as KeyUsage; + const op_flag = kKeyOps[op]; + // Skipping unknown key ops + if (op_flag === undefined) continue; + // Have we seen it already? if so, error + if (flags & (1 << op_flag)) + throw lazyDOMException('Duplicate key operation', 'DataError'); + flags |= 1 << op_flag; + + // TODO(@jasnell): RFC7517 section 4.3 strong recommends validating + // key usage combinations. Specifically, it says that unrelated key + // ops SHOULD NOT be used together. We're not yet validating that here. + } + + if (usagesSet !== undefined) { + for (const use of usagesSet) { + if (!keyOps.includes(use)) { + throw lazyDOMException( + 'Key operations and usage mismatch', + 'DataError', + ); + } + } + } +}; + +export function hasAnyNotIn(set: string[], checks: string[]) { + for (const s of set) { + if (!checks.includes(s)) { + return true; + } + } + return false; +}