From 8c2e828919e055c11fc7a12b78513f355c1c7033 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sat, 14 Feb 2026 22:48:36 -0500 Subject: [PATCH 1/3] feat: implement generateKeyPair DSA and DH key types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add DSA and DH support to crypto.generateKeyPair() and crypto.generateKeyPairSync(), completing all 9 key types. DSA: EVP paramgen with modulusLength + optional divisorLength, then keygen. Keys exported as SPKI/PKCS8 DER. DH: Three modes โ€” custom prime (binary ArrayBuffer), random prime (primeLength + generator), or named group (resolved in TS from dh-groups.ts). Uses deprecated DH_* APIs matching Node.js ncrypto. New Nitro specs, C++ implementations, TS wrappers, dispatcher wiring, 10 tests, and coverage doc updated (generateKeyPair ๐Ÿšงโ†’โœ…). --- .docs/implementation-coverage.md | 12 +- example/src/tests/keys/generate_keypair.ts | 199 ++++++++++++++++++ .../android/CMakeLists.txt | 3 + .../cpp/dh/HybridDhKeyPair.cpp | 183 ++++++++++++++++ .../cpp/dh/HybridDhKeyPair.hpp | 39 ++++ .../cpp/dsa/HybridDsaKeyPair.cpp | 131 ++++++++++++ .../cpp/dsa/HybridDsaKeyPair.hpp | 35 +++ packages/react-native-quick-crypto/nitro.json | 6 + .../android/QuickCrypto+autolinking.cmake | 2 + .../generated/android/QuickCryptoOnLoad.cpp | 20 ++ .../generated/ios/QuickCryptoAutolinking.mm | 20 ++ .../shared/c++/HybridDhKeyPairSpec.cpp | 28 +++ .../shared/c++/HybridDhKeyPairSpec.hpp | 71 +++++++ .../shared/c++/HybridDsaKeyPairSpec.cpp | 26 +++ .../shared/c++/HybridDsaKeyPairSpec.hpp | 68 ++++++ .../src/dhKeyPair.ts | 167 +++++++++++++++ packages/react-native-quick-crypto/src/dsa.ts | 140 ++++++++++++ .../src/keys/generateKeyPair.ts | 14 ++ .../src/specs/dhKeyPair.nitro.ts | 15 ++ .../src/specs/dsaKeyPair.nitro.ts | 13 ++ 20 files changed, 1186 insertions(+), 6 deletions(-) create mode 100644 packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp create mode 100644 packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp create mode 100644 packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp create mode 100644 packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.cpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.hpp create mode 100644 packages/react-native-quick-crypto/src/dhKeyPair.ts create mode 100644 packages/react-native-quick-crypto/src/dsa.ts create mode 100644 packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts create mode 100644 packages/react-native-quick-crypto/src/specs/dsaKeyPair.nitro.ts diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index b078b0e9..cce9b23e 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -127,8 +127,8 @@ These algorithms provide quantum-resistant cryptography. * โŒ `crypto.encapsulate(key[, callback])` * `-` `crypto.fips` deprecated, not applicable to RN * โœ… `crypto.generateKey(type, options, callback)` - * ๐Ÿšง `crypto.generateKeyPair(type, options, callback)` - * ๐Ÿšง `crypto.generateKeyPairSync(type, options)` + * โœ… `crypto.generateKeyPair(type, options, callback)` + * โœ… `crypto.generateKeyPairSync(type, options)` * ๐Ÿšง `crypto.generateKeySync(type, options)` * โœ… `crypto.generatePrime(size[, options[, callback]])` * โœ… `crypto.generatePrimeSync(size[, options])` @@ -182,26 +182,26 @@ These algorithms provide quantum-resistant cryptography. | --------- | :----: | | `rsa` | โœ… | | `rsa-pss` | โœ… | -| `dsa` | โŒ | +| `dsa` | โœ… | | `ec` | โœ… | | `ed25519` | โœ… | | `ed448` | โœ… | | `x25519` | โœ… | | `x448` | โœ… | -| `dh` | โŒ | +| `dh` | โœ… | ## `crypto.generateKeyPairSync` | type | Status | | --------- | :----: | | `rsa` | โœ… | | `rsa-pss` | โœ… | -| `dsa` | โŒ | +| `dsa` | โœ… | | `ec` | โœ… | | `ed25519` | โœ… | | `ed448` | โœ… | | `x25519` | โœ… | | `x448` | โœ… | -| `dh` | โŒ | +| `dh` | โœ… | ## `crypto.generateKeySync` | type | Status | diff --git a/example/src/tests/keys/generate_keypair.ts b/example/src/tests/keys/generate_keypair.ts index 60f2dc77..eb7af471 100644 --- a/example/src/tests/keys/generate_keypair.ts +++ b/example/src/tests/keys/generate_keypair.ts @@ -636,3 +636,202 @@ test(SUITE, 'generateKeyPairSync EC with DER encoding', () => { expect((privateKey as ArrayBuffer).byteLength).to.be.greaterThan(0); expect((publicKey as ArrayBuffer).byteLength).to.be.greaterThan(0); }); + +// --- DSA Key Generation Tests --- + +test(SUITE, 'generateKeyPair DSA 2048-bit with PEM encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'dsa', + { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as string, + publicKey: pubKey as string, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair DSA with custom divisorLength', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'dsa', + { + modulusLength: 2048, + divisorLength: 256, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as string, + publicKey: pubKey as string, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair DSA with DER encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: ArrayBuffer; + publicKey: ArrayBuffer; + }>((resolve, reject) => { + generateKeyPair( + 'dsa', + { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'der' }, + privateKeyEncoding: { type: 'pkcs8', format: 'der' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as ArrayBuffer, + publicKey: pubKey as ArrayBuffer, + }); + }, + ); + }); + + expect(privateKey instanceof ArrayBuffer).to.equal(true); + expect(publicKey instanceof ArrayBuffer).to.equal(true); + expect(privateKey.byteLength).to.be.greaterThan(0); + expect(publicKey.byteLength).to.be.greaterThan(0); +}); + +test(SUITE, 'generateKeyPairSync DSA 2048-bit', () => { + const { privateKey, publicKey } = generateKeyPairSync('dsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + expect(typeof privateKey).to.equal('string'); + expect(typeof publicKey).to.equal('string'); + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPairSync DSA keys work for signing', () => { + const { privateKey, publicKey } = generateKeyPairSync('dsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + const testData = 'Test data for DSA signing'; + const signature = createSign('SHA256') + .update(testData) + .sign(privateKey as string); + const isValid = createVerify('SHA256') + .update(testData) + .verify(publicKey as string, signature); + + expect(isValid).to.equal(true); +}); + +// --- DH Key Generation Tests --- + +test(SUITE, 'generateKeyPair DH with named group modp14', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'dh', + { + groupName: 'modp14', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as string, + publicKey: pubKey as string, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPair DH with PEM encoding', async () => { + const { privateKey, publicKey } = await new Promise<{ + privateKey: string; + publicKey: string; + }>((resolve, reject) => { + generateKeyPair( + 'dh', + { + groupName: 'modp14', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }, + (err, pubKey, privKey) => { + if (err) reject(err); + else + resolve({ + privateKey: privKey as string, + publicKey: pubKey as string, + }); + }, + ); + }); + + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPairSync DH with named group', () => { + const { privateKey, publicKey } = generateKeyPairSync('dh', { + groupName: 'modp14', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + expect(typeof privateKey).to.equal('string'); + expect(typeof publicKey).to.equal('string'); + expect(privateKey).to.match(/^-----BEGIN PRIVATE KEY-----/); + expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); +}); + +test(SUITE, 'generateKeyPairSync DH with DER encoding', () => { + const { privateKey, publicKey } = generateKeyPairSync('dh', { + groupName: 'modp14', + publicKeyEncoding: { type: 'spki', format: 'der' }, + privateKeyEncoding: { type: 'pkcs8', format: 'der' }, + }); + + expect(privateKey instanceof ArrayBuffer).to.equal(true); + expect(publicKey instanceof ArrayBuffer).to.equal(true); + expect((privateKey as ArrayBuffer).byteLength).to.be.greaterThan(0); + expect((publicKey as ArrayBuffer).byteLength).to.be.greaterThan(0); +}); diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 72d26af6..9ccb313b 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -39,6 +39,8 @@ add_library( ../cpp/cipher/ChaCha20Cipher.cpp ../cpp/cipher/ChaCha20Poly1305Cipher.cpp ../cpp/dh/HybridDiffieHellman.cpp + ../cpp/dh/HybridDhKeyPair.cpp + ../cpp/dsa/HybridDsaKeyPair.cpp ../cpp/ec/HybridEcKeyPair.cpp ../cpp/ecdh/HybridECDH.cpp ../cpp/ed25519/HybridEdKeyPair.cpp @@ -74,6 +76,7 @@ include_directories( "../cpp/certificate" "../cpp/cipher" "../cpp/dh" + "../cpp/dsa" "../cpp/ec" "../cpp/ecdh" "../cpp/ed25519" diff --git a/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp new file mode 100644 index 00000000..0907fe59 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp @@ -0,0 +1,183 @@ +#include "HybridDhKeyPair.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Suppress deprecation warnings for DH_* functions +// Node.js ncrypto uses the same pattern โ€” these APIs work but are deprecated in OpenSSL 3.x +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +namespace margelo::nitro::crypto { + +using BN_ptr = std::unique_ptr; +using DH_ptr = std::unique_ptr; +using EVP_PKEY_CTX_ptr = std::unique_ptr; + +void HybridDhKeyPair::setPrimeLength(double primeLength) { + primeLength_ = static_cast(primeLength); +} + +void HybridDhKeyPair::setPrime(const std::shared_ptr& prime) { + prime_.assign(prime->data(), prime->data() + prime->size()); +} + +void HybridDhKeyPair::setGenerator(double generator) { + generator_ = static_cast(generator); +} + +void HybridDhKeyPair::setGroupName(const std::string& groupName) { + groupName_ = groupName; +} + +std::shared_ptr> HybridDhKeyPair::generateKeyPair() { + return Promise::async([this]() { this->generateKeyPairSync(); }); +} + +void HybridDhKeyPair::generateKeyPairSync() { + pkey_.reset(); + + EVP_PKEY* params = nullptr; + + if (!prime_.empty()) { + // Mode B: Custom prime provided as binary + DH_ptr dh(DH_new(), DH_free); + if (!dh) { + throw std::runtime_error("DH: failed to create DH structure"); + } + + BIGNUM* p = BN_bin2bn(prime_.data(), static_cast(prime_.size()), nullptr); + BIGNUM* g = BN_new(); + if (!p || !g) { + if (p) + BN_free(p); + if (g) + BN_free(g); + throw std::runtime_error("DH: failed to create BIGNUM parameters"); + } + BN_set_word(g, static_cast(generator_)); + + if (DH_set0_pqg(dh.get(), p, nullptr, g) != 1) { + BN_free(p); + BN_free(g); + throw std::runtime_error("DH: failed to set DH parameters"); + } + + EVP_PKEY* pkey_params = EVP_PKEY_new(); + if (!pkey_params) { + throw std::runtime_error("DH: failed to create EVP_PKEY for parameters"); + } + + if (EVP_PKEY_assign_DH(pkey_params, dh.get()) != 1) { + EVP_PKEY_free(pkey_params); + throw std::runtime_error("DH: failed to assign DH to EVP_PKEY"); + } + dh.release(); // EVP_PKEY now owns it + + params = pkey_params; + + } else if (primeLength_ > 0) { + // Mode C: Generate random prime of given size + EVP_PKEY_CTX_ptr pctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DH, nullptr), EVP_PKEY_CTX_free); + if (!pctx) { + throw std::runtime_error("DH: failed to create parameter context"); + } + + if (EVP_PKEY_paramgen_init(pctx.get()) <= 0) { + throw std::runtime_error("DH: failed to initialize parameter generation"); + } + + if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(pctx.get(), primeLength_) <= 0) { + throw std::runtime_error("DH: failed to set prime length"); + } + + if (EVP_PKEY_CTX_set_dh_paramgen_generator(pctx.get(), generator_) <= 0) { + throw std::runtime_error("DH: failed to set generator"); + } + + if (EVP_PKEY_paramgen(pctx.get(), ¶ms) <= 0) { + throw std::runtime_error("DH: failed to generate parameters"); + } + } else { + throw std::runtime_error("DH: either prime, primeLength, or groupName must be set"); + } + + std::unique_ptr params_guard(params, EVP_PKEY_free); + + // Generate key pair from parameters + EVP_PKEY_CTX_ptr kctx(EVP_PKEY_CTX_new(params, nullptr), EVP_PKEY_CTX_free); + if (!kctx) { + throw std::runtime_error("DH: failed to create keygen context"); + } + + if (EVP_PKEY_keygen_init(kctx.get()) <= 0) { + throw std::runtime_error("DH: failed to initialize key generation"); + } + + EVP_PKEY* raw_pkey = nullptr; + if (EVP_PKEY_keygen(kctx.get(), &raw_pkey) <= 0) { + throw std::runtime_error("DH: failed to generate key pair"); + } + + pkey_.reset(raw_pkey); +} + +std::shared_ptr HybridDhKeyPair::getPublicKey() { + if (!pkey_) { + throw std::runtime_error("DH: no key pair generated"); + } + + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("DH: failed to create BIO for public key export"); + } + + if (i2d_PUBKEY_bio(bio, pkey_.get()) != 1) { + BIO_free(bio); + throw std::runtime_error("DH: failed to export public key"); + } + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + std::string derData(mem->data, mem->length); + BIO_free(bio); + + return ToNativeArrayBuffer(derData); +} + +std::shared_ptr HybridDhKeyPair::getPrivateKey() { + if (!pkey_) { + throw std::runtime_error("DH: no key pair generated"); + } + + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("DH: failed to create BIO for private key export"); + } + + if (i2d_PKCS8PrivateKey_bio(bio, pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr) != 1) { + BIO_free(bio); + throw std::runtime_error("DH: failed to export private key"); + } + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + std::string derData(mem->data, mem->length); + BIO_free(bio); + + return ToNativeArrayBuffer(derData); +} + +#pragma clang diagnostic pop + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp new file mode 100644 index 00000000..48828ef4 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "HybridDhKeyPairSpec.hpp" +#include "QuickCryptoUtils.hpp" + +namespace margelo::nitro::crypto { + +class HybridDhKeyPair : public HybridDhKeyPairSpec { + public: + HybridDhKeyPair() : HybridObject(TAG) {} + ~HybridDhKeyPair() override = default; + + public: + std::shared_ptr> generateKeyPair() override; + void generateKeyPairSync() override; + void setPrimeLength(double primeLength) override; + void setPrime(const std::shared_ptr& prime) override; + void setGenerator(double generator) override; + void setGroupName(const std::string& groupName) override; + std::shared_ptr getPublicKey() override; + std::shared_ptr getPrivateKey() override; + + private: + int primeLength_ = 0; + std::vector prime_; + int generator_ = 2; + std::string groupName_; + + using EVP_PKEY_ptr = std::unique_ptr; + EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp new file mode 100644 index 00000000..3b63fd1e --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp @@ -0,0 +1,131 @@ +#include "HybridDsaKeyPair.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace margelo::nitro::crypto { + +void HybridDsaKeyPair::setModulusLength(double modulusLength) { + modulusLength_ = static_cast(modulusLength); +} + +void HybridDsaKeyPair::setDivisorLength(double divisorLength) { + divisorLength_ = static_cast(divisorLength); +} + +std::shared_ptr> HybridDsaKeyPair::generateKeyPair() { + return Promise::async([this]() { this->generateKeyPairSync(); }); +} + +void HybridDsaKeyPair::generateKeyPairSync() { + if (modulusLength_ <= 0) { + throw std::runtime_error("DSA modulusLength must be set before generating key pair"); + } + + if (pkey != nullptr) { + EVP_PKEY_free(pkey); + pkey = nullptr; + } + + // Step 1: Generate DSA parameters + std::unique_ptr param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DSA, nullptr), EVP_PKEY_CTX_free); + + if (!param_ctx) { + throw std::runtime_error("DSA: failed to create parameter context"); + } + + if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) { + throw std::runtime_error("DSA: failed to initialize parameter generation"); + } + + if (EVP_PKEY_CTX_set_dsa_paramgen_bits(param_ctx.get(), modulusLength_) <= 0) { + throw std::runtime_error("DSA: failed to set modulus length"); + } + + if (divisorLength_ >= 0) { + if (EVP_PKEY_CTX_set_dsa_paramgen_q_bits(param_ctx.get(), divisorLength_) <= 0) { + throw std::runtime_error("DSA: failed to set divisor length"); + } + } + + EVP_PKEY* raw_params = nullptr; + if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) { + throw std::runtime_error("DSA: failed to generate parameters"); + } + + std::unique_ptr params(raw_params, EVP_PKEY_free); + + // Step 2: Generate key pair from parameters + std::unique_ptr key_ctx(EVP_PKEY_CTX_new(params.get(), nullptr), EVP_PKEY_CTX_free); + + if (!key_ctx) { + throw std::runtime_error("DSA: failed to create key generation context"); + } + + if (EVP_PKEY_keygen_init(key_ctx.get()) <= 0) { + throw std::runtime_error("DSA: failed to initialize key generation"); + } + + EVP_PKEY* raw_pkey = nullptr; + if (EVP_PKEY_keygen(key_ctx.get(), &raw_pkey) <= 0) { + throw std::runtime_error("DSA: failed to generate key pair"); + } + + pkey = raw_pkey; +} + +std::shared_ptr HybridDsaKeyPair::getPublicKey() { + if (pkey == nullptr) { + throw std::runtime_error("DSA: no key pair generated"); + } + + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("DSA: failed to create BIO for public key export"); + } + + if (i2d_PUBKEY_bio(bio, pkey) != 1) { + BIO_free(bio); + throw std::runtime_error("DSA: failed to export public key"); + } + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + std::string derData(mem->data, mem->length); + BIO_free(bio); + + return ToNativeArrayBuffer(derData); +} + +std::shared_ptr HybridDsaKeyPair::getPrivateKey() { + if (pkey == nullptr) { + throw std::runtime_error("DSA: no key pair generated"); + } + + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("DSA: failed to create BIO for private key export"); + } + + if (i2d_PKCS8PrivateKey_bio(bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + BIO_free(bio); + throw std::runtime_error("DSA: failed to export private key"); + } + + BUF_MEM* mem; + BIO_get_mem_ptr(bio, &mem); + std::string derData(mem->data, mem->length); + BIO_free(bio); + + return ToNativeArrayBuffer(derData); +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp new file mode 100644 index 00000000..e0c5c230 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "HybridDsaKeyPairSpec.hpp" +#include "QuickCryptoUtils.hpp" + +namespace margelo::nitro::crypto { + +class HybridDsaKeyPair : public HybridDsaKeyPairSpec { + public: + HybridDsaKeyPair() : HybridObject(TAG) {} + ~HybridDsaKeyPair() override { + if (pkey != nullptr) { + EVP_PKEY_free(pkey); + pkey = nullptr; + } + } + + public: + std::shared_ptr> generateKeyPair() override; + void generateKeyPairSync() override; + void setModulusLength(double modulusLength) override; + void setDivisorLength(double divisorLength) override; + std::shared_ptr getPublicKey() override; + std::shared_ptr getPrivateKey() override; + + private: + int modulusLength_ = 0; + int divisorLength_ = -1; + EVP_PKEY* pkey = nullptr; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 2c726d78..76e26fb8 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -23,9 +23,15 @@ "CipherFactory": { "cpp": "HybridCipherFactory" }, + "DhKeyPair": { + "cpp": "HybridDhKeyPair" + }, "DiffieHellman": { "cpp": "HybridDiffieHellman" }, + "DsaKeyPair": { + "cpp": "HybridDsaKeyPair" + }, "ECDH": { "cpp": "HybridECDH" }, 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 83ec6be9..d39000d6 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 @@ -38,7 +38,9 @@ target_sources( ../nitrogen/generated/shared/c++/HybridCertificateSpec.cpp ../nitrogen/generated/shared/c++/HybridCipherSpec.cpp ../nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp + ../nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridDiffieHellmanSpec.cpp + ../nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridECDHSpec.cpp ../nitrogen/generated/shared/c++/HybridEcKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridEdKeyPairSpec.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 3abb5014..89711208 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -20,7 +20,9 @@ #include "HybridCertificate.hpp" #include "HybridCipher.hpp" #include "HybridCipherFactory.hpp" +#include "HybridDhKeyPair.hpp" #include "HybridDiffieHellman.hpp" +#include "HybridDsaKeyPair.hpp" #include "HybridECDH.hpp" #include "HybridEcKeyPair.hpp" #include "HybridEdKeyPair.hpp" @@ -96,6 +98,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "DhKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridDhKeyPair\" 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( "DiffieHellman", []() -> std::shared_ptr { @@ -105,6 +116,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "DsaKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridDsaKeyPair\" 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( "ECDH", []() -> 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 cf0aced7..d942efe4 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -15,7 +15,9 @@ #include "HybridCertificate.hpp" #include "HybridCipher.hpp" #include "HybridCipherFactory.hpp" +#include "HybridDhKeyPair.hpp" #include "HybridDiffieHellman.hpp" +#include "HybridDsaKeyPair.hpp" #include "HybridECDH.hpp" #include "HybridEcKeyPair.hpp" #include "HybridEdKeyPair.hpp" @@ -88,6 +90,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "DhKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridDhKeyPair\" 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( "DiffieHellman", []() -> std::shared_ptr { @@ -97,6 +108,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "DsaKeyPair", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridDsaKeyPair\" 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( "ECDH", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp new file mode 100644 index 00000000..402e6759 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp @@ -0,0 +1,28 @@ +/// +/// HybridDhKeyPairSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ Marc Rousavy @ Margelo +/// + +#include "HybridDhKeyPairSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridDhKeyPairSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("generateKeyPair", &HybridDhKeyPairSpec::generateKeyPair); + prototype.registerHybridMethod("generateKeyPairSync", &HybridDhKeyPairSpec::generateKeyPairSync); + prototype.registerHybridMethod("setPrimeLength", &HybridDhKeyPairSpec::setPrimeLength); + prototype.registerHybridMethod("setPrime", &HybridDhKeyPairSpec::setPrime); + prototype.registerHybridMethod("setGenerator", &HybridDhKeyPairSpec::setGenerator); + prototype.registerHybridMethod("setGroupName", &HybridDhKeyPairSpec::setGroupName); + prototype.registerHybridMethod("getPublicKey", &HybridDhKeyPairSpec::getPublicKey); + prototype.registerHybridMethod("getPrivateKey", &HybridDhKeyPairSpec::getPrivateKey); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp new file mode 100644 index 00000000..10172d08 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp @@ -0,0 +1,71 @@ +/// +/// HybridDhKeyPairSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `DhKeyPair` + * Inherit this class to create instances of `HybridDhKeyPairSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridDhKeyPair: public HybridDhKeyPairSpec { + * public: + * HybridDhKeyPair(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridDhKeyPairSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridDhKeyPairSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridDhKeyPairSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> generateKeyPair() = 0; + virtual void generateKeyPairSync() = 0; + virtual void setPrimeLength(double primeLength) = 0; + virtual void setPrime(const std::shared_ptr& prime) = 0; + virtual void setGenerator(double generator) = 0; + virtual void setGroupName(const std::string& groupName) = 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 = "DhKeyPair"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.cpp new file mode 100644 index 00000000..49425134 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.cpp @@ -0,0 +1,26 @@ +/// +/// HybridDsaKeyPairSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ Marc Rousavy @ Margelo +/// + +#include "HybridDsaKeyPairSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridDsaKeyPairSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("generateKeyPair", &HybridDsaKeyPairSpec::generateKeyPair); + prototype.registerHybridMethod("generateKeyPairSync", &HybridDsaKeyPairSpec::generateKeyPairSync); + prototype.registerHybridMethod("setModulusLength", &HybridDsaKeyPairSpec::setModulusLength); + prototype.registerHybridMethod("setDivisorLength", &HybridDsaKeyPairSpec::setDivisorLength); + prototype.registerHybridMethod("getPublicKey", &HybridDsaKeyPairSpec::getPublicKey); + prototype.registerHybridMethod("getPrivateKey", &HybridDsaKeyPairSpec::getPrivateKey); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.hpp new file mode 100644 index 00000000..285a91d5 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDsaKeyPairSpec.hpp @@ -0,0 +1,68 @@ +/// +/// HybridDsaKeyPairSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `DsaKeyPair` + * Inherit this class to create instances of `HybridDsaKeyPairSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridDsaKeyPair: public HybridDsaKeyPairSpec { + * public: + * HybridDsaKeyPair(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridDsaKeyPairSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridDsaKeyPairSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridDsaKeyPairSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> generateKeyPair() = 0; + virtual void generateKeyPairSync() = 0; + virtual void setModulusLength(double modulusLength) = 0; + virtual void setDivisorLength(double divisorLength) = 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 = "DsaKeyPair"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/dhKeyPair.ts b/packages/react-native-quick-crypto/src/dhKeyPair.ts new file mode 100644 index 00000000..bc772dea --- /dev/null +++ b/packages/react-native-quick-crypto/src/dhKeyPair.ts @@ -0,0 +1,167 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import { KeyObject, PublicKeyObject, PrivateKeyObject } from './keys'; +import type { DhKeyPair } from './specs/dhKeyPair.nitro'; +import type { GenerateKeyPairOptions, KeyPairGenConfig } from './utils/types'; +import { KFormatType, KeyEncoding } from './utils'; +import { DH_GROUPS } from './dh-groups'; + +export class DhKeyPairGen { + native: DhKeyPair; + + constructor(options: GenerateKeyPairOptions) { + this.native = NitroModules.createHybridObject('DhKeyPair'); + + const { groupName, prime, primeLength, generator } = options; + + if (groupName) { + // Resolve named group to prime + generator + const group = DH_GROUPS[groupName]; + if (!group) { + throw new Error(`Unknown DH group: ${groupName}`); + } + const primeBuf = Buffer.from(group.prime, 'hex'); + this.native.setPrime( + primeBuf.buffer.slice( + primeBuf.byteOffset, + primeBuf.byteOffset + primeBuf.byteLength, + ) as ArrayBuffer, + ); + const gen = parseInt(group.generator, 16); + this.native.setGenerator(gen); + } else if (prime) { + // Custom prime as Buffer + const primeBuf = Buffer.from(prime); + this.native.setPrime( + primeBuf.buffer.slice( + primeBuf.byteOffset, + primeBuf.byteOffset + primeBuf.byteLength, + ) as ArrayBuffer, + ); + this.native.setGenerator(generator ?? 2); + } else if (primeLength) { + this.native.setPrimeLength(primeLength); + this.native.setGenerator(generator ?? 2); + } else { + throw new Error( + 'DH key generation requires one of: groupName, prime, or primeLength', + ); + } + } + + async generateKeyPair(): Promise { + await this.native.generateKeyPair(); + } + + generateKeyPairSync(): void { + this.native.generateKeyPairSync(); + } +} + +function dh_prepareKeyGenParams( + options: GenerateKeyPairOptions | undefined, +): DhKeyPairGen { + if (!options) { + throw new Error('Options are required for DH key generation'); + } + + return new DhKeyPairGen(options); +} + +function dh_formatKeyPairOutput( + dh: DhKeyPairGen, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +} { + const { + publicFormat, + publicType, + privateFormat, + privateType, + cipher, + passphrase, + } = encoding; + + const publicKeyData = dh.native.getPublicKey(); + const privateKeyData = dh.native.getPrivateKey(); + + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + + const priv = KeyObject.createKeyObject( + 'private', + privateKeyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObject; + + let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + + if (publicFormat === -1) { + publicKey = pub; + } else { + const format = + publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const keyEncoding = + publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.SPKI; + const exported = pub.handle.exportKey(format, keyEncoding); + if (format === KFormatType.PEM) { + publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + publicKey = exported; + } + } + + if (privateFormat === -1) { + privateKey = priv; + } else { + const format = + privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const keyEncoding = + privateType === KeyEncoding.PKCS8 ? KeyEncoding.PKCS8 : KeyEncoding.PKCS8; + const exported = priv.handle.exportKey( + format, + keyEncoding, + cipher, + passphrase, + ); + if (format === KFormatType.PEM) { + privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + privateKey = exported; + } + } + + return { publicKey, privateKey }; +} + +export async function dh_generateKeyPairNode( + options: GenerateKeyPairOptions | undefined, + encoding: KeyPairGenConfig, +): Promise<{ + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +}> { + const dh = dh_prepareKeyGenParams(options); + await dh.generateKeyPair(); + return dh_formatKeyPairOutput(dh, encoding); +} + +export function dh_generateKeyPairNodeSync( + options: GenerateKeyPairOptions | undefined, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +} { + const dh = dh_prepareKeyGenParams(options); + dh.generateKeyPairSync(); + return dh_formatKeyPairOutput(dh, encoding); +} diff --git a/packages/react-native-quick-crypto/src/dsa.ts b/packages/react-native-quick-crypto/src/dsa.ts new file mode 100644 index 00000000..2409e0c8 --- /dev/null +++ b/packages/react-native-quick-crypto/src/dsa.ts @@ -0,0 +1,140 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import { KeyObject, PublicKeyObject, PrivateKeyObject } from './keys'; +import type { DsaKeyPair } from './specs/dsaKeyPair.nitro'; +import type { GenerateKeyPairOptions, KeyPairGenConfig } from './utils/types'; +import { KFormatType, KeyEncoding } from './utils'; + +export class Dsa { + native: DsaKeyPair; + + constructor(modulusLength: number, divisorLength?: number) { + this.native = NitroModules.createHybridObject('DsaKeyPair'); + this.native.setModulusLength(modulusLength); + if (divisorLength !== undefined && divisorLength >= 0) { + this.native.setDivisorLength(divisorLength); + } + } + + async generateKeyPair(): Promise { + await this.native.generateKeyPair(); + } + + generateKeyPairSync(): void { + this.native.generateKeyPairSync(); + } +} + +function dsa_prepareKeyGenParams( + options: GenerateKeyPairOptions | undefined, +): Dsa { + if (!options) { + throw new Error('Options are required for DSA key generation'); + } + + const { modulusLength, divisorLength } = options; + + if (!modulusLength || modulusLength < 1024) { + throw new Error('Invalid or missing modulusLength for DSA key generation'); + } + + return new Dsa(modulusLength, divisorLength); +} + +function dsa_formatKeyPairOutput( + dsa: Dsa, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +} { + const { + publicFormat, + publicType, + privateFormat, + privateType, + cipher, + passphrase, + } = encoding; + + const publicKeyData = dsa.native.getPublicKey(); + const privateKeyData = dsa.native.getPrivateKey(); + + const pub = KeyObject.createKeyObject( + 'public', + publicKeyData, + KFormatType.DER, + KeyEncoding.SPKI, + ) as PublicKeyObject; + + const priv = KeyObject.createKeyObject( + 'private', + privateKeyData, + KFormatType.DER, + KeyEncoding.PKCS8, + ) as PrivateKeyObject; + + let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + + if (publicFormat === -1) { + publicKey = pub; + } else { + const format = + publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const keyEncoding = + publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.SPKI; + const exported = pub.handle.exportKey(format, keyEncoding); + if (format === KFormatType.PEM) { + publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + publicKey = exported; + } + } + + if (privateFormat === -1) { + privateKey = priv; + } else { + const format = + privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; + const keyEncoding = + privateType === KeyEncoding.PKCS8 ? KeyEncoding.PKCS8 : KeyEncoding.PKCS8; + const exported = priv.handle.exportKey( + format, + keyEncoding, + cipher, + passphrase, + ); + if (format === KFormatType.PEM) { + privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); + } else { + privateKey = exported; + } + } + + return { publicKey, privateKey }; +} + +export async function dsa_generateKeyPairNode( + options: GenerateKeyPairOptions | undefined, + encoding: KeyPairGenConfig, +): Promise<{ + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +}> { + const dsa = dsa_prepareKeyGenParams(options); + await dsa.generateKeyPair(); + return dsa_formatKeyPairOutput(dsa, encoding); +} + +export function dsa_generateKeyPairNodeSync( + options: GenerateKeyPairOptions | undefined, + encoding: KeyPairGenConfig, +): { + publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; + privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; +} { + const dsa = dsa_prepareKeyGenParams(options); + dsa.generateKeyPairSync(); + return dsa_formatKeyPairOutput(dsa, encoding); +} diff --git a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts index 904c8a12..0d2fefb7 100644 --- a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts +++ b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts @@ -1,6 +1,11 @@ import { ed_generateKeyPair } from '../ed'; import { rsa_generateKeyPairNode, rsa_generateKeyPairNodeSync } from '../rsa'; import { ec_generateKeyPairNode, ec_generateKeyPairNodeSync } from '../ec'; +import { dsa_generateKeyPairNode, dsa_generateKeyPairNodeSync } from '../dsa'; +import { + dh_generateKeyPairNode, + dh_generateKeyPairNodeSync, +} from '../dhKeyPair'; import { kEmptyObject, validateFunction, @@ -139,6 +144,7 @@ function internalGenerateKeyPair( case 'rsa-pss': case 'dsa': case 'ec': + case 'dh': break; default: { const err = new Error(` @@ -158,6 +164,10 @@ function internalGenerateKeyPair( result = await rsa_generateKeyPairNode(type, options, encoding); } else if (type === 'ec') { result = await ec_generateKeyPairNode(options, encoding); + } else if (type === 'dsa') { + result = await dsa_generateKeyPairNode(options, encoding); + } else if (type === 'dh') { + result = await dh_generateKeyPairNode(options, encoding); } else { throw new Error(`Unsupported key type: ${type}`); } @@ -184,6 +194,10 @@ function internalGenerateKeyPair( result = rsa_generateKeyPairNodeSync(type, options, encoding); } else if (type === 'ec') { result = ec_generateKeyPairNodeSync(options, encoding); + } else if (type === 'dsa') { + result = dsa_generateKeyPairNodeSync(options, encoding); + } else if (type === 'dh') { + result = dh_generateKeyPairNodeSync(options, encoding); } else { throw new Error(`Unsupported key type: ${type}`); } diff --git a/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts b/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts new file mode 100644 index 00000000..176f26fc --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts @@ -0,0 +1,15 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface DhKeyPair + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + generateKeyPair(): Promise; + generateKeyPairSync(): void; + + setPrimeLength(primeLength: number): void; + setPrime(prime: ArrayBuffer): void; + setGenerator(generator: number): void; + setGroupName(groupName: string): void; + + getPublicKey(): ArrayBuffer; + getPrivateKey(): ArrayBuffer; +} diff --git a/packages/react-native-quick-crypto/src/specs/dsaKeyPair.nitro.ts b/packages/react-native-quick-crypto/src/specs/dsaKeyPair.nitro.ts new file mode 100644 index 00000000..82eb0cc7 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/dsaKeyPair.nitro.ts @@ -0,0 +1,13 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface DsaKeyPair + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + generateKeyPair(): Promise; + generateKeyPairSync(): void; + + setModulusLength(modulusLength: number): void; + setDivisorLength(divisorLength: number): void; + + getPublicKey(): ArrayBuffer; + getPrivateKey(): ArrayBuffer; +} From a0cc8cc3cde66a86aec3ab0505bb654c31a415ec Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sat, 14 Feb 2026 22:56:02 -0500 Subject: [PATCH 2/3] refactor: address code review issues for DSA/DH key generation - Convert HybridDsaKeyPair from raw EVP_PKEY* to RAII unique_ptr - Remove dead setGroupName/groupName_ from DH Nitro spec and C++ - Replace duplicate DH test with primeLength coverage - Simplify always-true ternaries in DSA/DH format helpers - Delegate DSA modulusLength validation to OpenSSL --- example/src/tests/keys/generate_keypair.ts | 4 ++-- .../cpp/dh/HybridDhKeyPair.cpp | 6 +----- .../cpp/dh/HybridDhKeyPair.hpp | 2 -- .../cpp/dsa/HybridDsaKeyPair.cpp | 15 ++++++--------- .../cpp/dsa/HybridDsaKeyPair.hpp | 11 ++++------- .../shared/c++/HybridDhKeyPairSpec.cpp | 1 - .../shared/c++/HybridDhKeyPairSpec.hpp | 2 -- .../src/dhKeyPair.ts | 17 +++-------------- packages/react-native-quick-crypto/src/dsa.ts | 19 ++++--------------- .../src/specs/dhKeyPair.nitro.ts | 1 - 10 files changed, 20 insertions(+), 58 deletions(-) diff --git a/example/src/tests/keys/generate_keypair.ts b/example/src/tests/keys/generate_keypair.ts index eb7af471..b6a7ade2 100644 --- a/example/src/tests/keys/generate_keypair.ts +++ b/example/src/tests/keys/generate_keypair.ts @@ -783,7 +783,7 @@ test(SUITE, 'generateKeyPair DH with named group modp14', async () => { expect(publicKey).to.match(/^-----BEGIN PUBLIC KEY-----/); }); -test(SUITE, 'generateKeyPair DH with PEM encoding', async () => { +test(SUITE, 'generateKeyPair DH with primeLength', async () => { const { privateKey, publicKey } = await new Promise<{ privateKey: string; publicKey: string; @@ -791,7 +791,7 @@ test(SUITE, 'generateKeyPair DH with PEM encoding', async () => { generateKeyPair( 'dh', { - groupName: 'modp14', + primeLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }, diff --git a/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp index 0907fe59..774d5e7e 100644 --- a/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.cpp @@ -36,10 +36,6 @@ void HybridDhKeyPair::setGenerator(double generator) { generator_ = static_cast(generator); } -void HybridDhKeyPair::setGroupName(const std::string& groupName) { - groupName_ = groupName; -} - std::shared_ptr> HybridDhKeyPair::generateKeyPair() { return Promise::async([this]() { this->generateKeyPairSync(); }); } @@ -109,7 +105,7 @@ void HybridDhKeyPair::generateKeyPairSync() { throw std::runtime_error("DH: failed to generate parameters"); } } else { - throw std::runtime_error("DH: either prime, primeLength, or groupName must be set"); + throw std::runtime_error("DH: either prime or primeLength must be set"); } std::unique_ptr params_guard(params, EVP_PKEY_free); diff --git a/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp index 48828ef4..671384c8 100644 --- a/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/dh/HybridDhKeyPair.hpp @@ -22,7 +22,6 @@ class HybridDhKeyPair : public HybridDhKeyPairSpec { void setPrimeLength(double primeLength) override; void setPrime(const std::shared_ptr& prime) override; void setGenerator(double generator) override; - void setGroupName(const std::string& groupName) override; std::shared_ptr getPublicKey() override; std::shared_ptr getPrivateKey() override; @@ -30,7 +29,6 @@ class HybridDhKeyPair : public HybridDhKeyPairSpec { int primeLength_ = 0; std::vector prime_; int generator_ = 2; - std::string groupName_; using EVP_PKEY_ptr = std::unique_ptr; EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; diff --git a/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp index 3b63fd1e..1a1d1e55 100644 --- a/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.cpp @@ -30,10 +30,7 @@ void HybridDsaKeyPair::generateKeyPairSync() { throw std::runtime_error("DSA modulusLength must be set before generating key pair"); } - if (pkey != nullptr) { - EVP_PKEY_free(pkey); - pkey = nullptr; - } + pkey_.reset(); // Step 1: Generate DSA parameters std::unique_ptr param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DSA, nullptr), EVP_PKEY_CTX_free); @@ -79,11 +76,11 @@ void HybridDsaKeyPair::generateKeyPairSync() { throw std::runtime_error("DSA: failed to generate key pair"); } - pkey = raw_pkey; + pkey_.reset(raw_pkey); } std::shared_ptr HybridDsaKeyPair::getPublicKey() { - if (pkey == nullptr) { + if (!pkey_) { throw std::runtime_error("DSA: no key pair generated"); } @@ -92,7 +89,7 @@ std::shared_ptr HybridDsaKeyPair::getPublicKey() { throw std::runtime_error("DSA: failed to create BIO for public key export"); } - if (i2d_PUBKEY_bio(bio, pkey) != 1) { + if (i2d_PUBKEY_bio(bio, pkey_.get()) != 1) { BIO_free(bio); throw std::runtime_error("DSA: failed to export public key"); } @@ -106,7 +103,7 @@ std::shared_ptr HybridDsaKeyPair::getPublicKey() { } std::shared_ptr HybridDsaKeyPair::getPrivateKey() { - if (pkey == nullptr) { + if (!pkey_) { throw std::runtime_error("DSA: no key pair generated"); } @@ -115,7 +112,7 @@ std::shared_ptr HybridDsaKeyPair::getPrivateKey() { throw std::runtime_error("DSA: failed to create BIO for private key export"); } - if (i2d_PKCS8PrivateKey_bio(bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + if (i2d_PKCS8PrivateKey_bio(bio, pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr) != 1) { BIO_free(bio); throw std::runtime_error("DSA: failed to export private key"); } diff --git a/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp index e0c5c230..646291b8 100644 --- a/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/dsa/HybridDsaKeyPair.hpp @@ -11,12 +11,7 @@ namespace margelo::nitro::crypto { class HybridDsaKeyPair : public HybridDsaKeyPairSpec { public: HybridDsaKeyPair() : HybridObject(TAG) {} - ~HybridDsaKeyPair() override { - if (pkey != nullptr) { - EVP_PKEY_free(pkey); - pkey = nullptr; - } - } + ~HybridDsaKeyPair() override = default; public: std::shared_ptr> generateKeyPair() override; @@ -29,7 +24,9 @@ class HybridDsaKeyPair : public HybridDsaKeyPairSpec { private: int modulusLength_ = 0; int divisorLength_ = -1; - EVP_PKEY* pkey = nullptr; + + using EVP_PKEY_ptr = std::unique_ptr; + EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; }; } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp index 402e6759..196fcb2d 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.cpp @@ -19,7 +19,6 @@ namespace margelo::nitro::crypto { prototype.registerHybridMethod("setPrimeLength", &HybridDhKeyPairSpec::setPrimeLength); prototype.registerHybridMethod("setPrime", &HybridDhKeyPairSpec::setPrime); prototype.registerHybridMethod("setGenerator", &HybridDhKeyPairSpec::setGenerator); - prototype.registerHybridMethod("setGroupName", &HybridDhKeyPairSpec::setGroupName); prototype.registerHybridMethod("getPublicKey", &HybridDhKeyPairSpec::getPublicKey); prototype.registerHybridMethod("getPrivateKey", &HybridDhKeyPairSpec::getPrivateKey); }); diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp index 10172d08..85421c04 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridDhKeyPairSpec.hpp @@ -17,7 +17,6 @@ #include #include -#include namespace margelo::nitro::crypto { @@ -55,7 +54,6 @@ namespace margelo::nitro::crypto { virtual void setPrimeLength(double primeLength) = 0; virtual void setPrime(const std::shared_ptr& prime) = 0; virtual void setGenerator(double generator) = 0; - virtual void setGroupName(const std::string& groupName) = 0; virtual std::shared_ptr getPublicKey() = 0; virtual std::shared_ptr getPrivateKey() = 0; diff --git a/packages/react-native-quick-crypto/src/dhKeyPair.ts b/packages/react-native-quick-crypto/src/dhKeyPair.ts index bc772dea..2399450d 100644 --- a/packages/react-native-quick-crypto/src/dhKeyPair.ts +++ b/packages/react-native-quick-crypto/src/dhKeyPair.ts @@ -75,14 +75,7 @@ function dh_formatKeyPairOutput( publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; } { - const { - publicFormat, - publicType, - privateFormat, - privateType, - cipher, - passphrase, - } = encoding; + const { publicFormat, privateFormat, cipher, passphrase } = encoding; const publicKeyData = dh.native.getPublicKey(); const privateKeyData = dh.native.getPrivateKey(); @@ -109,9 +102,7 @@ function dh_formatKeyPairOutput( } else { const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; - const keyEncoding = - publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.SPKI; - const exported = pub.handle.exportKey(format, keyEncoding); + const exported = pub.handle.exportKey(format, KeyEncoding.SPKI); if (format === KFormatType.PEM) { publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); } else { @@ -124,11 +115,9 @@ function dh_formatKeyPairOutput( } else { const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; - const keyEncoding = - privateType === KeyEncoding.PKCS8 ? KeyEncoding.PKCS8 : KeyEncoding.PKCS8; const exported = priv.handle.exportKey( format, - keyEncoding, + KeyEncoding.PKCS8, cipher, passphrase, ); diff --git a/packages/react-native-quick-crypto/src/dsa.ts b/packages/react-native-quick-crypto/src/dsa.ts index 2409e0c8..5c4074da 100644 --- a/packages/react-native-quick-crypto/src/dsa.ts +++ b/packages/react-native-quick-crypto/src/dsa.ts @@ -34,7 +34,7 @@ function dsa_prepareKeyGenParams( const { modulusLength, divisorLength } = options; - if (!modulusLength || modulusLength < 1024) { + if (!modulusLength || modulusLength <= 0) { throw new Error('Invalid or missing modulusLength for DSA key generation'); } @@ -48,14 +48,7 @@ function dsa_formatKeyPairOutput( publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; } { - const { - publicFormat, - publicType, - privateFormat, - privateType, - cipher, - passphrase, - } = encoding; + const { publicFormat, privateFormat, cipher, passphrase } = encoding; const publicKeyData = dsa.native.getPublicKey(); const privateKeyData = dsa.native.getPrivateKey(); @@ -82,9 +75,7 @@ function dsa_formatKeyPairOutput( } else { const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; - const keyEncoding = - publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.SPKI; - const exported = pub.handle.exportKey(format, keyEncoding); + const exported = pub.handle.exportKey(format, KeyEncoding.SPKI); if (format === KFormatType.PEM) { publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); } else { @@ -97,11 +88,9 @@ function dsa_formatKeyPairOutput( } else { const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; - const keyEncoding = - privateType === KeyEncoding.PKCS8 ? KeyEncoding.PKCS8 : KeyEncoding.PKCS8; const exported = priv.handle.exportKey( format, - keyEncoding, + KeyEncoding.PKCS8, cipher, passphrase, ); diff --git a/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts b/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts index 176f26fc..0f943e9a 100644 --- a/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/dhKeyPair.nitro.ts @@ -8,7 +8,6 @@ export interface DhKeyPair setPrimeLength(primeLength: number): void; setPrime(prime: ArrayBuffer): void; setGenerator(generator: number): void; - setGroupName(groupName: string): void; getPublicKey(): ArrayBuffer; getPrivateKey(): ArrayBuffer; From e194e41cb9f0d8f3cd763dcc5349b71904056770 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sat, 14 Feb 2026 22:57:57 -0500 Subject: [PATCH 3/3] docs: mark generateKeySync as fully implemented --- .docs/implementation-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index cce9b23e..8ec4adfc 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -129,7 +129,7 @@ These algorithms provide quantum-resistant cryptography. * โœ… `crypto.generateKey(type, options, callback)` * โœ… `crypto.generateKeyPair(type, options, callback)` * โœ… `crypto.generateKeyPairSync(type, options)` - * ๐Ÿšง `crypto.generateKeySync(type, options)` + * โœ… `crypto.generateKeySync(type, options)` * โœ… `crypto.generatePrime(size[, options[, callback]])` * โœ… `crypto.generatePrimeSync(size[, options])` * โœ… `crypto.getCipherInfo(nameOrNid[, options])`