From 88c631b45b37a39d900c6d8ebdadd47b9ffae862 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:23 -0500 Subject: [PATCH 01/15] feat: add KeyObject.from() and KeyObject.toCryptoKey() methods --- .../src/keys/classes.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/react-native-quick-crypto/src/keys/classes.ts b/packages/react-native-quick-crypto/src/keys/classes.ts index c03797b2..e9f18baa 100644 --- a/packages/react-native-quick-crypto/src/keys/classes.ts +++ b/packages/react-native-quick-crypto/src/keys/classes.ts @@ -126,11 +126,22 @@ export class KeyObject { this.type = type as 'public' | 'secret' | 'private'; } - // static from(key) { - // if (!isCryptoKey(key)) - // throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); - // return key[kKeyObject]; - // } + static from(key: CryptoKey): KeyObject { + if (!(key instanceof CryptoKey)) { + throw new TypeError( + `The "key" argument must be an instance of CryptoKey. Received ${typeof key}`, + ); + } + return key.keyObject; + } + + toCryptoKey( + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[], + ): CryptoKey { + return new CryptoKey(this, algorithm, keyUsages, extractable); + } static createKeyObject( type: string, From 8484c231d7fbac9c27bc17c48dba6b142ba78b08 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:29 -0500 Subject: [PATCH 02/15] feat: add Certificate class (SPKAC) with exportChallenge, exportPublicKey, verifySpkac --- .../cpp/certificate/HybridCertificate.cpp | 47 ++++++++++++++ .../cpp/certificate/HybridCertificate.hpp | 16 +++++ .../shared/c++/HybridCertificateSpec.cpp | 23 +++++++ .../shared/c++/HybridCertificateSpec.hpp | 64 +++++++++++++++++++ .../src/certificate.ts | 41 ++++++++++++ .../src/specs/certificate.nitro.ts | 8 +++ 6 files changed, 199 insertions(+) create mode 100644 packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp create mode 100644 packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.hpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.cpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.hpp create mode 100644 packages/react-native-quick-crypto/src/certificate.ts create mode 100644 packages/react-native-quick-crypto/src/specs/certificate.nitro.ts diff --git a/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp new file mode 100644 index 00000000..8656a29b --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp @@ -0,0 +1,47 @@ +#include "HybridCertificate.hpp" +#include "QuickCryptoUtils.hpp" +#include +#include + +namespace margelo::nitro::crypto { + +bool HybridCertificate::verifySpkac(const std::shared_ptr& spkac) { + return ncrypto::VerifySpkac( + reinterpret_cast(spkac->data()), + spkac->size()); +} + +std::shared_ptr HybridCertificate::exportPublicKey(const std::shared_ptr& spkac) { + auto bio = ncrypto::ExportPublicKey( + reinterpret_cast(spkac->data()), + spkac->size()); + + if (!bio) { + return std::make_shared(nullptr, 0, nullptr); + } + + BUF_MEM* mem = bio; + if (!mem || mem->length == 0) { + return std::make_shared(nullptr, 0, nullptr); + } + + return ToNativeArrayBuffer( + reinterpret_cast(mem->data), mem->length); +} + +std::shared_ptr HybridCertificate::exportChallenge(const std::shared_ptr& spkac) { + auto buf = ncrypto::ExportChallenge( + reinterpret_cast(spkac->data()), + spkac->size()); + + if (buf.data == nullptr) { + return std::make_shared(nullptr, 0, nullptr); + } + + auto result = ToNativeArrayBuffer( + reinterpret_cast(buf.data), buf.len); + OPENSSL_free(buf.data); + return result; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.hpp b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.hpp new file mode 100644 index 00000000..f8884db3 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "HybridCertificateSpec.hpp" + +namespace margelo::nitro::crypto { + +class HybridCertificate : public HybridCertificateSpec { + public: + HybridCertificate() : HybridObject(TAG) {} + + bool verifySpkac(const std::shared_ptr& spkac) override; + std::shared_ptr exportPublicKey(const std::shared_ptr& spkac) override; + std::shared_ptr exportChallenge(const std::shared_ptr& spkac) override; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.cpp new file mode 100644 index 00000000..82c6ef67 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.cpp @@ -0,0 +1,23 @@ +/// +/// HybridCertificateSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridCertificateSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridCertificateSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("verifySpkac", &HybridCertificateSpec::verifySpkac); + prototype.registerHybridMethod("exportPublicKey", &HybridCertificateSpec::exportPublicKey); + prototype.registerHybridMethod("exportChallenge", &HybridCertificateSpec::exportChallenge); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.hpp new file mode 100644 index 00000000..1057d80c --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCertificateSpec.hpp @@ -0,0 +1,64 @@ +/// +/// HybridCertificateSpec.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 + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `Certificate` + * Inherit this class to create instances of `HybridCertificateSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridCertificate: public HybridCertificateSpec { + * public: + * HybridCertificate(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridCertificateSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridCertificateSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridCertificateSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual bool verifySpkac(const std::shared_ptr& spkac) = 0; + virtual std::shared_ptr exportPublicKey(const std::shared_ptr& spkac) = 0; + virtual std::shared_ptr exportChallenge(const std::shared_ptr& spkac) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Certificate"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/certificate.ts b/packages/react-native-quick-crypto/src/certificate.ts new file mode 100644 index 00000000..ff17fbc3 --- /dev/null +++ b/packages/react-native-quick-crypto/src/certificate.ts @@ -0,0 +1,41 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import type { Certificate as NativeCertificate } from './specs/certificate.nitro'; +import type { BinaryLike } from './utils'; +import { binaryLikeToArrayBuffer } from './utils'; + +let native: NativeCertificate; +function getNative(): NativeCertificate { + if (native == null) { + native = NitroModules.createHybridObject('Certificate'); + } + return native; +} + +function toArrayBuffer( + spkac: BinaryLike, + encoding?: BufferEncoding, +): ArrayBuffer { + if (typeof spkac === 'string') { + return binaryLikeToArrayBuffer(spkac, encoding || 'utf8'); + } + return binaryLikeToArrayBuffer(spkac); +} + +export class Certificate { + static exportChallenge(spkac: BinaryLike, encoding?: BufferEncoding): Buffer { + return Buffer.from( + getNative().exportChallenge(toArrayBuffer(spkac, encoding)), + ); + } + + static exportPublicKey(spkac: BinaryLike, encoding?: BufferEncoding): Buffer { + return Buffer.from( + getNative().exportPublicKey(toArrayBuffer(spkac, encoding)), + ); + } + + static verifySpkac(spkac: BinaryLike, encoding?: BufferEncoding): boolean { + return getNative().verifySpkac(toArrayBuffer(spkac, encoding)); + } +} diff --git a/packages/react-native-quick-crypto/src/specs/certificate.nitro.ts b/packages/react-native-quick-crypto/src/specs/certificate.nitro.ts new file mode 100644 index 00000000..37c4ee41 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/certificate.nitro.ts @@ -0,0 +1,8 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface Certificate + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + verifySpkac(spkac: ArrayBuffer): boolean; + exportPublicKey(spkac: ArrayBuffer): ArrayBuffer; + exportChallenge(spkac: ArrayBuffer): ArrayBuffer; +} From dd751b51637de1cc4f650ac15ad249f59ff56e69 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:34 -0500 Subject: [PATCH 03/15] feat: add ECDH.convertKey() static method --- .../cpp/ecdh/HybridECDH.cpp | 35 +++++++++++ .../cpp/ecdh/HybridECDH.hpp | 1 + .../generated/shared/c++/HybridECDHSpec.cpp | 1 + .../generated/shared/c++/HybridECDHSpec.hpp | 1 + .../react-native-quick-crypto/src/ecdh.ts | 59 +++++++++++++++++++ .../src/specs/ecdh.nitro.ts | 1 + 6 files changed, 98 insertions(+) diff --git a/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.cpp b/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.cpp index 5e1b329b..910c5d91 100644 --- a/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.cpp +++ b/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.cpp @@ -284,6 +284,41 @@ void HybridECDH::setPublicKey(const std::shared_ptr& publicKey) { _pkey = std::move(pkey); } +std::shared_ptr HybridECDH::convertKey(const std::shared_ptr& key, const std::string& curve, double format) { + int nid = getCurveNid(curve); + if (nid == NID_undef) { + throw std::runtime_error("ECDH: unknown curve: " + curve); + } + + EC_GROUP_ptr group(EC_GROUP_new_by_curve_name(nid), EC_GROUP_free); + if (!group) { + throw std::runtime_error("ECDH: failed to create EC group for curve: " + curve); + } + + EC_POINT_ptr point(EC_POINT_new(group.get()), EC_POINT_free); + if (!point) { + throw std::runtime_error("ECDH: failed to create EC point"); + } + + if (EC_POINT_oct2point(group.get(), point.get(), key->data(), key->size(), nullptr) != 1) { + throw std::runtime_error("ECDH: failed to decode public key"); + } + + auto form = static_cast(static_cast(format)); + + size_t len = EC_POINT_point2oct(group.get(), point.get(), form, nullptr, 0, nullptr); + if (len == 0) { + throw std::runtime_error("ECDH: failed to get converted key length"); + } + + std::vector buf(len); + if (EC_POINT_point2oct(group.get(), point.get(), form, buf.data(), len, nullptr) == 0) { + throw std::runtime_error("ECDH: failed to convert key"); + } + + return ToNativeArrayBuffer(buf); +} + void HybridECDH::ensureInitialized() const { if (_curveNid == 0 || !_group) { throw std::runtime_error("ECDH: not initialized"); diff --git a/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.hpp b/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.hpp index 2e685e4b..dca42389 100644 --- a/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.hpp +++ b/packages/react-native-quick-crypto/cpp/ecdh/HybridECDH.hpp @@ -28,6 +28,7 @@ class HybridECDH : public HybridECDHSpec { void setPrivateKey(const std::shared_ptr& privateKey) override; std::shared_ptr getPublicKey() override; void setPublicKey(const std::shared_ptr& publicKey) override; + std::shared_ptr convertKey(const std::shared_ptr& key, const std::string& curve, double format) override; private: EVP_PKEY_ptr _pkey; diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.cpp index 10c3c803..63fefb9a 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.cpp @@ -21,6 +21,7 @@ namespace margelo::nitro::crypto { prototype.registerHybridMethod("setPrivateKey", &HybridECDHSpec::setPrivateKey); prototype.registerHybridMethod("getPublicKey", &HybridECDHSpec::getPublicKey); prototype.registerHybridMethod("setPublicKey", &HybridECDHSpec::setPublicKey); + prototype.registerHybridMethod("convertKey", &HybridECDHSpec::convertKey); }); } diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.hpp index 5d0062a6..29c74566 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridECDHSpec.hpp @@ -56,6 +56,7 @@ namespace margelo::nitro::crypto { virtual void setPrivateKey(const std::shared_ptr& privateKey) = 0; virtual std::shared_ptr getPublicKey() = 0; virtual void setPublicKey(const std::shared_ptr& publicKey) = 0; + virtual std::shared_ptr convertKey(const std::shared_ptr& key, const std::string& curve, double format) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-quick-crypto/src/ecdh.ts b/packages/react-native-quick-crypto/src/ecdh.ts index 101d2204..677d38a5 100644 --- a/packages/react-native-quick-crypto/src/ecdh.ts +++ b/packages/react-native-quick-crypto/src/ecdh.ts @@ -2,7 +2,20 @@ import { NitroModules } from 'react-native-nitro-modules'; import type { ECDH as ECDHInterface } from './specs/ecdh.nitro'; import { Buffer } from '@craftzdog/react-native-buffer'; +const POINT_CONVERSION_COMPRESSED = 2; +const POINT_CONVERSION_UNCOMPRESSED = 4; +const POINT_CONVERSION_HYBRID = 6; + export class ECDH { + private static _convertKeyHybrid: ECDHInterface | undefined; + private static get convertKeyHybrid(): ECDHInterface { + if (!this._convertKeyHybrid) { + this._convertKeyHybrid = + NitroModules.createHybridObject('ECDH'); + } + return this._convertKeyHybrid; + } + private _hybrid: ECDHInterface; constructor(curveName: string) { @@ -69,6 +82,52 @@ export class ECDH { } this._hybrid.setPublicKey(keyBuf.buffer as ArrayBuffer); } + + static convertKey( + key: Buffer | string, + curve: string, + inputEncoding?: BufferEncoding, + outputEncoding?: BufferEncoding, + format?: 'uncompressed' | 'compressed' | 'hybrid', + ): Buffer | string { + let keyBuf: Buffer; + if (Buffer.isBuffer(key)) { + keyBuf = key; + } else { + keyBuf = Buffer.from(key, inputEncoding); + } + + let formatNum: number; + switch (format) { + case 'compressed': + formatNum = POINT_CONVERSION_COMPRESSED; + break; + case 'hybrid': + formatNum = POINT_CONVERSION_HYBRID; + break; + case 'uncompressed': + case undefined: + formatNum = POINT_CONVERSION_UNCOMPRESSED; + break; + default: + throw new TypeError( + `Invalid point conversion format: ${format as string}`, + ); + } + + const result = Buffer.from( + ECDH.convertKeyHybrid.convertKey( + keyBuf.buffer as ArrayBuffer, + curve, + formatNum, + ), + ); + + if (outputEncoding) { + return result.toString(outputEncoding); + } + return result; + } } export function createECDH(curveName: string): ECDH { diff --git a/packages/react-native-quick-crypto/src/specs/ecdh.nitro.ts b/packages/react-native-quick-crypto/src/specs/ecdh.nitro.ts index a75b2402..17674bfc 100644 --- a/packages/react-native-quick-crypto/src/specs/ecdh.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/ecdh.nitro.ts @@ -8,4 +8,5 @@ export interface ECDH extends HybridObject<{ ios: 'c++'; android: 'c++' }> { setPrivateKey(privateKey: ArrayBuffer): void; getPublicKey(): ArrayBuffer; setPublicKey(publicKey: ArrayBuffer): void; + convertKey(key: ArrayBuffer, curve: string, format: number): ArrayBuffer; } From b261c44128bf1f83703f73bc9bdd5b5558817db2 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:39 -0500 Subject: [PATCH 04/15] feat: add getCipherInfo() function --- .../cpp/cipher/HybridCipher.cpp | 61 ++++++++++ .../cpp/cipher/HybridCipher.hpp | 3 + .../generated/shared/c++/CipherInfo.hpp | 104 ++++++++++++++++++ .../generated/shared/c++/HybridCipherSpec.cpp | 1 + .../generated/shared/c++/HybridCipherSpec.hpp | 4 + .../react-native-quick-crypto/src/cipher.ts | 31 ++++++ .../src/specs/cipher.nitro.ts | 14 +++ 7 files changed, 218 insertions(+) create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherInfo.hpp diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp index e56bc0de..ff82e9d0 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -8,6 +8,7 @@ #include "HybridCipher.hpp" #include "QuickCryptoUtils.hpp" +#include #include #include @@ -336,4 +337,64 @@ std::vector HybridCipher::getSupportedCiphers() { return cipher_names; } +std::optional HybridCipher::getCipherInfo( + const std::string& name, + std::optional keyLength, + std::optional ivLength) { + auto cipher = ncrypto::Cipher::FromName(name.c_str()); + if (!cipher) return std::nullopt; + + size_t iv_length = cipher.getIvLength(); + size_t key_length = cipher.getKeyLength(); + + if (keyLength.has_value() || ivLength.has_value()) { + auto ctx = ncrypto::CipherCtxPointer::New(); + if (!ctx.init(cipher, true)) return std::nullopt; + + if (keyLength.has_value()) { + size_t check_len = static_cast(keyLength.value()); + if (!ctx.setKeyLength(check_len)) return std::nullopt; + key_length = check_len; + } + + if (ivLength.has_value()) { + size_t check_len = static_cast(ivLength.value()); + if (cipher.isCcmMode()) { + if (check_len < 7 || check_len > 13) return std::nullopt; + } else if (cipher.isGcmMode()) { + // GCM accepts flexible IV lengths + } else if (cipher.isOcbMode()) { + if (!ctx.setIvLength(check_len)) return std::nullopt; + } else { + if (check_len != iv_length) return std::nullopt; + } + iv_length = check_len; + } + } + + std::string name_str(cipher.getName()); + std::transform(name_str.begin(), name_str.end(), name_str.begin(), ::tolower); + + std::string mode_str(cipher.getModeLabel()); + + std::optional block_size = std::nullopt; + if (!cipher.isStreamMode()) { + block_size = static_cast(cipher.getBlockSize()); + } + + std::optional iv_len = std::nullopt; + if (iv_length != 0) { + iv_len = static_cast(iv_length); + } + + return CipherInfo{ + name_str, + static_cast(cipher.getNid()), + mode_str, + static_cast(key_length), + block_size, + iv_len + }; +} + } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp index 7f418ba3..2f4dbe6c 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp @@ -8,6 +8,7 @@ #include #include +#include "CipherInfo.hpp" #include "HybridCipherSpec.hpp" namespace margelo::nitro::crypto { @@ -40,6 +41,8 @@ class HybridCipher : public HybridCipherSpec { std::vector getSupportedCiphers() override; + std::optional getCipherInfo(const std::string& name, std::optional keyLength, std::optional ivLength) override; + protected: // Protected enums for state management enum CipherKind { kCipher, kDecipher }; diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherInfo.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherInfo.hpp new file mode 100644 index 00000000..36c3b84f --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/CipherInfo.hpp @@ -0,0 +1,104 @@ +/// +/// CipherInfo.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 +#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 +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::crypto { + + /** + * A struct which can be represented as a JavaScript object (CipherInfo). + */ + struct CipherInfo final { + public: + std::string name SWIFT_PRIVATE; + double nid SWIFT_PRIVATE; + std::string mode SWIFT_PRIVATE; + double keyLength SWIFT_PRIVATE; + std::optional blockSize SWIFT_PRIVATE; + std::optional ivLength SWIFT_PRIVATE; + + public: + CipherInfo() = default; + explicit CipherInfo(std::string name, double nid, std::string mode, double keyLength, std::optional blockSize, std::optional ivLength): name(name), nid(nid), mode(mode), keyLength(keyLength), blockSize(blockSize), ivLength(ivLength) {} + + public: + friend bool operator==(const CipherInfo& lhs, const CipherInfo& rhs) = default; + }; + +} // namespace margelo::nitro::crypto + +namespace margelo::nitro { + + // C++ CipherInfo <> JS CipherInfo (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::crypto::CipherInfo fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::crypto::CipherInfo( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "nid"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "mode"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "keyLength"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "blockSize"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "ivLength"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::crypto::CipherInfo& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "name"), JSIConverter::toJSI(runtime, arg.name)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "nid"), JSIConverter::toJSI(runtime, arg.nid)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "mode"), JSIConverter::toJSI(runtime, arg.mode)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "keyLength"), JSIConverter::toJSI(runtime, arg.keyLength)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "blockSize"), JSIConverter>::toJSI(runtime, arg.blockSize)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "ivLength"), JSIConverter>::toJSI(runtime, arg.ivLength)); + 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 (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "nid")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "mode")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "keyLength")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "blockSize")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "ivLength")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp index 3c5a0ac0..552b0fd4 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.cpp @@ -22,6 +22,7 @@ namespace margelo::nitro::crypto { prototype.registerHybridMethod("setAuthTag", &HybridCipherSpec::setAuthTag); prototype.registerHybridMethod("getAuthTag", &HybridCipherSpec::getAuthTag); prototype.registerHybridMethod("getSupportedCiphers", &HybridCipherSpec::getSupportedCiphers); + prototype.registerHybridMethod("getCipherInfo", &HybridCipherSpec::getCipherInfo); }); } diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp index 457d5cb9..45736ad3 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridCipherSpec.hpp @@ -15,12 +15,15 @@ // Forward declaration of `CipherArgs` to properly resolve imports. namespace margelo::nitro::crypto { struct CipherArgs; } +// Forward declaration of `CipherInfo` to properly resolve imports. +namespace margelo::nitro::crypto { struct CipherInfo; } #include #include "CipherArgs.hpp" #include #include #include +#include "CipherInfo.hpp" namespace margelo::nitro::crypto { @@ -61,6 +64,7 @@ namespace margelo::nitro::crypto { virtual bool setAuthTag(const std::shared_ptr& tag) = 0; virtual std::shared_ptr getAuthTag() = 0; virtual std::vector getSupportedCiphers() = 0; + virtual std::optional getCipherInfo(const std::string& name, std::optional keyLength, std::optional ivLength) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-quick-crypto/src/cipher.ts b/packages/react-native-quick-crypto/src/cipher.ts index 969cdbae..d9a507d9 100644 --- a/packages/react-native-quick-crypto/src/cipher.ts +++ b/packages/react-native-quick-crypto/src/cipher.ts @@ -28,18 +28,49 @@ export type CipherOptions = | CipherGCMOptions | TransformOptions; +export interface CipherInfoResult { + name: string; + nid: number; + mode: string; + keyLength: number; + blockSize?: number; + ivLength?: number; +} + class CipherUtils { private static native = NitroModules.createHybridObject('Cipher'); public static getSupportedCiphers(): string[] { return this.native.getSupportedCiphers(); } + public static getCipherInfo( + name: string, + keyLength?: number, + ivLength?: number, + ): CipherInfoResult | undefined { + return this.native.getCipherInfo(name, keyLength, ivLength); + } } export function getCiphers(): string[] { return CipherUtils.getSupportedCiphers(); } +export function getCipherInfo( + nameOrNid: string | number, + options?: { keyLength?: number; ivLength?: number }, +): CipherInfoResult | undefined { + if (typeof nameOrNid === 'string') { + if (nameOrNid.length === 0) return undefined; + return CipherUtils.getCipherInfo( + nameOrNid, + options?.keyLength, + options?.ivLength, + ); + } + throw new TypeError('nameOrNid must be a string'); +} + interface CipherArgs { isCipher: boolean; cipherType: string; diff --git a/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts b/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts index 0d5d100a..362f11d0 100644 --- a/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/cipher.nitro.ts @@ -8,6 +8,15 @@ type CipherArgs = { authTagLen?: number; }; +interface CipherInfo { + name: string; + nid: number; + mode: string; + keyLength: number; + blockSize?: number; + ivLength?: number; +} + export interface Cipher extends HybridObject<{ ios: 'c++'; android: 'c++' }> { update(data: ArrayBuffer): ArrayBuffer; final(): ArrayBuffer; @@ -17,6 +26,11 @@ export interface Cipher extends HybridObject<{ ios: 'c++'; android: 'c++' }> { setAuthTag(tag: ArrayBuffer): boolean; getAuthTag(): ArrayBuffer; getSupportedCiphers(): string[]; + getCipherInfo( + name: string, + keyLength?: number, + ivLength?: number, + ): CipherInfo | undefined; } export interface CipherFactory From d360bc21db4db917a2d5688ddab22909fb49b8f7 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:45 -0500 Subject: [PATCH 05/15] feat: add generatePrime/generatePrimeSync and checkPrime/checkPrimeSync --- .../cpp/prime/HybridPrime.cpp | 93 +++++++++++++ .../cpp/prime/HybridPrime.hpp | 25 ++++ .../generated/shared/c++/HybridPrimeSpec.cpp | 24 ++++ .../generated/shared/c++/HybridPrimeSpec.hpp | 67 +++++++++ .../react-native-quick-crypto/src/prime.ts | 130 ++++++++++++++++++ .../src/specs/prime.nitro.ts | 18 +++ 6 files changed, 357 insertions(+) create mode 100644 packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp create mode 100644 packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.cpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.hpp create mode 100644 packages/react-native-quick-crypto/src/prime.ts create mode 100644 packages/react-native-quick-crypto/src/specs/prime.nitro.ts diff --git a/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp new file mode 100644 index 00000000..557d7460 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp @@ -0,0 +1,93 @@ +#include "HybridPrime.hpp" +#include "QuickCryptoUtils.hpp" +#include + +namespace margelo::nitro::crypto { + +using namespace ncrypto; + +static BignumPointer toBignum( + const std::optional>& buf) { + if (!buf.has_value() || buf.value()->size() == 0) { + return BignumPointer(); + } + return BignumPointer(buf.value()->data(), buf.value()->size()); +} + +static std::shared_ptr generatePrimeImpl( + double size, bool safe, + const std::optional>& add, + const std::optional>& rem) { + int bits = static_cast(size); + + auto addBn = toBignum(add); + auto remBn = toBignum(rem); + + BignumPointer::PrimeConfig config{bits, safe, addBn, remBn}; + auto prime = BignumPointer::NewPrime(config); + if (!prime) { + throw std::runtime_error("Failed to generate prime"); + } + + auto encoded = prime.encode(); + if (!encoded) { + throw std::runtime_error("Failed to encode prime"); + } + + return ToNativeArrayBuffer(encoded.get(), encoded.size()); +} + +std::shared_ptr>> +HybridPrime::generatePrime( + double size, bool safe, + const std::optional>& add, + const std::optional>& rem) { + auto addCopy = add.has_value() ? std::make_optional(ToNativeArrayBuffer(add.value())) : std::nullopt; + auto remCopy = rem.has_value() ? std::make_optional(ToNativeArrayBuffer(rem.value())) : std::nullopt; + + return Promise>::async( + [size, safe, + addCopy = std::move(addCopy), + remCopy = std::move(remCopy)]() { + return generatePrimeImpl(size, safe, addCopy, remCopy); + }); +} + +std::shared_ptr HybridPrime::generatePrimeSync( + double size, bool safe, + const std::optional>& add, + const std::optional>& rem) { + return generatePrimeImpl(size, safe, add, rem); +} + +bool HybridPrime::checkPrimeSync( + const std::shared_ptr& candidate, double checks) { + BignumPointer bn(candidate->data(), candidate->size()); + if (!bn) { + throw std::runtime_error("Invalid candidate"); + } + + int result = bn.isPrime(static_cast(checks)); + if (result == -1) { + throw std::runtime_error("Prime check failed"); + } + return result == 1; +} + +std::shared_ptr> HybridPrime::checkPrime( + const std::shared_ptr& candidate, double checks) { + auto candidateCopy = ToNativeArrayBuffer(candidate); + return Promise::async([candidateCopy, checks]() { + BignumPointer bn(candidateCopy->data(), candidateCopy->size()); + if (!bn) { + throw std::runtime_error("Invalid candidate"); + } + int result = bn.isPrime(static_cast(checks)); + if (result == -1) { + throw std::runtime_error("Prime check failed"); + } + return result == 1; + }); +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp new file mode 100644 index 00000000..083425b0 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "HybridPrimeSpec.hpp" + +namespace margelo::nitro::crypto { + +class HybridPrime : public HybridPrimeSpec { + public: + HybridPrime() : HybridObject(TAG) {} + + std::shared_ptr>> generatePrime( + double size, bool safe, + const std::optional>& add, + const std::optional>& rem) override; + std::shared_ptr generatePrimeSync( + double size, bool safe, + const std::optional>& add, + const std::optional>& rem) override; + std::shared_ptr> checkPrime( + const std::shared_ptr& candidate, double checks) override; + bool checkPrimeSync( + const std::shared_ptr& candidate, double checks) override; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.cpp new file mode 100644 index 00000000..e448736a --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.cpp @@ -0,0 +1,24 @@ +/// +/// HybridPrimeSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridPrimeSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridPrimeSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("generatePrime", &HybridPrimeSpec::generatePrime); + prototype.registerHybridMethod("generatePrimeSync", &HybridPrimeSpec::generatePrimeSync); + prototype.registerHybridMethod("checkPrime", &HybridPrimeSpec::checkPrime); + prototype.registerHybridMethod("checkPrimeSync", &HybridPrimeSpec::checkPrimeSync); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.hpp new file mode 100644 index 00000000..7a730673 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridPrimeSpec.hpp @@ -0,0 +1,67 @@ +/// +/// HybridPrimeSpec.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 `Prime` + * Inherit this class to create instances of `HybridPrimeSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridPrime: public HybridPrimeSpec { + * public: + * HybridPrime(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridPrimeSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridPrimeSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridPrimeSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr>> generatePrime(double size, bool safe, const std::optional>& add, const std::optional>& rem) = 0; + virtual std::shared_ptr generatePrimeSync(double size, bool safe, const std::optional>& add, const std::optional>& rem) = 0; + virtual std::shared_ptr> checkPrime(const std::shared_ptr& candidate, double checks) = 0; + virtual bool checkPrimeSync(const std::shared_ptr& candidate, double checks) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Prime"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/prime.ts b/packages/react-native-quick-crypto/src/prime.ts new file mode 100644 index 00000000..9d8ec818 --- /dev/null +++ b/packages/react-native-quick-crypto/src/prime.ts @@ -0,0 +1,130 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import type { Prime as NativePrime } from './specs/prime.nitro'; +import type { BinaryLike } from './utils'; +import { binaryLikeToArrayBuffer } from './utils'; + +let native: NativePrime; +function getNative(): NativePrime { + if (native == null) { + native = NitroModules.createHybridObject('Prime'); + } + return native; +} + +export interface GeneratePrimeOptions { + safe?: boolean; + bigint?: boolean; + add?: ArrayBuffer | Buffer | Uint8Array; + rem?: ArrayBuffer | Buffer | Uint8Array; +} + +export interface CheckPrimeOptions { + checks?: number; +} + +function toOptionalArrayBuffer( + value?: ArrayBuffer | Buffer | Uint8Array, +): ArrayBuffer | undefined { + if (value == null) return undefined; + if (value instanceof ArrayBuffer) return value; + return binaryLikeToArrayBuffer(value); +} + +function bufferToBigInt(buf: Buffer): bigint { + let result = 0n; + for (let i = 0; i < buf.length; i++) { + result = (result << 8n) | BigInt(buf[i]!); + } + return result; +} + +function bigIntToBuffer(value: bigint): ArrayBuffer { + if (value === 0n) return new Uint8Array([0]).buffer; + const hex = value.toString(16); + const paddedHex = hex.length % 2 ? '0' + hex : hex; + const bytes = new Uint8Array(paddedHex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(paddedHex.substring(i * 2, i * 2 + 2), 16); + } + return bytes.buffer; +} + +export function generatePrimeSync( + size: number, + options?: GeneratePrimeOptions, +): Buffer | bigint { + const safe = options?.safe ?? false; + const add = toOptionalArrayBuffer(options?.add); + const rem = toOptionalArrayBuffer(options?.rem); + const result = Buffer.from( + getNative().generatePrimeSync(size, safe, add, rem), + ); + if (options?.bigint) { + return bufferToBigInt(result); + } + return result; +} + +export function generatePrime( + size: number, + options?: GeneratePrimeOptions, + callback?: (err: Error | null, prime: Buffer | bigint) => void, +): void { + if (typeof options === 'function') { + callback = options as unknown as ( + err: Error | null, + prime: Buffer | bigint, + ) => void; + options = {}; + } + const safe = options?.safe ?? false; + const add = toOptionalArrayBuffer(options?.add); + const rem = toOptionalArrayBuffer(options?.rem); + const wantBigint = options?.bigint ?? false; + + getNative() + .generatePrime(size, safe, add, rem) + .then(ab => { + const result = Buffer.from(ab); + if (wantBigint) { + callback?.(null, bufferToBigInt(result)); + } else { + callback?.(null, result); + } + }) + .catch((err: Error) => callback?.(err, Buffer.alloc(0))); +} + +export function checkPrimeSync( + candidate: BinaryLike | bigint, + options?: CheckPrimeOptions, +): boolean { + const checks = options?.checks ?? 0; + const buf = + typeof candidate === 'bigint' + ? bigIntToBuffer(candidate) + : binaryLikeToArrayBuffer(candidate); + return getNative().checkPrimeSync(buf, checks); +} + +export function checkPrime( + candidate: BinaryLike | bigint, + options?: CheckPrimeOptions | ((err: Error | null, result: boolean) => void), + callback?: (err: Error | null, result: boolean) => void, +): void { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const checks = (options as CheckPrimeOptions)?.checks ?? 0; + const buf = + typeof candidate === 'bigint' + ? bigIntToBuffer(candidate) + : binaryLikeToArrayBuffer(candidate); + + getNative() + .checkPrime(buf, checks) + .then(result => callback?.(null, result)) + .catch((err: Error) => callback?.(err, false)); +} diff --git a/packages/react-native-quick-crypto/src/specs/prime.nitro.ts b/packages/react-native-quick-crypto/src/specs/prime.nitro.ts new file mode 100644 index 00000000..d1819192 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/prime.nitro.ts @@ -0,0 +1,18 @@ +import { type HybridObject } from 'react-native-nitro-modules'; + +export interface Prime extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + generatePrime( + size: number, + safe: boolean, + add?: ArrayBuffer, + rem?: ArrayBuffer, + ): Promise; + generatePrimeSync( + size: number, + safe: boolean, + add?: ArrayBuffer, + rem?: ArrayBuffer, + ): ArrayBuffer; + checkPrime(candidate: ArrayBuffer, checks: number): Promise; + checkPrimeSync(candidate: ArrayBuffer, checks: number): boolean; +} From d863fb5920e077ae4f11d46695e61951e63a9c08 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:50 -0500 Subject: [PATCH 06/15] feat: add argon2/argon2Sync password hashing (argon2d, argon2i, argon2id) --- .../cpp/argon2/HybridArgon2.cpp | 122 ++++++++++++++++++ .../cpp/argon2/HybridArgon2.hpp | 39 ++++++ .../generated/shared/c++/HybridArgon2Spec.cpp | 22 ++++ .../generated/shared/c++/HybridArgon2Spec.hpp | 66 ++++++++++ .../react-native-quick-crypto/src/argon2.ts | 83 ++++++++++++ .../src/specs/argon2.nitro.ts | 29 +++++ 6 files changed, 361 insertions(+) create mode 100644 packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp create mode 100644 packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.cpp create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.hpp create mode 100644 packages/react-native-quick-crypto/src/argon2.ts create mode 100644 packages/react-native-quick-crypto/src/specs/argon2.nitro.ts diff --git a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp new file mode 100644 index 00000000..d28a1592 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include + +#include "HybridArgon2.hpp" +#include "QuickCryptoUtils.hpp" + +namespace margelo::nitro::crypto { + +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#ifndef OPENSSL_NO_ARGON2 + +static ncrypto::Argon2Type parseAlgorithm(const std::string& algo) { + if (algo == "argon2d") return ncrypto::Argon2Type::ARGON2D; + if (algo == "argon2i") return ncrypto::Argon2Type::ARGON2I; + if (algo == "argon2id") return ncrypto::Argon2Type::ARGON2ID; + throw std::runtime_error("Unknown argon2 algorithm: " + algo); +} + +static std::shared_ptr hashImpl( + const std::string& algorithm, + const std::shared_ptr& message, + const std::shared_ptr& nonce, + double parallelism, double tagLength, double memory, + double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) { + + auto type = parseAlgorithm(algorithm); + + ncrypto::Buffer passBuf{ + message->size() > 0 ? reinterpret_cast(message->data()) : "", + message->size()}; + + ncrypto::Buffer saltBuf{ + nonce->size() > 0 ? reinterpret_cast(nonce->data()) : reinterpret_cast(""), + nonce->size()}; + + ncrypto::Buffer secretBuf{nullptr, 0}; + if (secret.has_value() && secret.value()->size() > 0) { + secretBuf = {reinterpret_cast(secret.value()->data()), + secret.value()->size()}; + } + + ncrypto::Buffer adBuf{nullptr, 0}; + if (associatedData.has_value() && associatedData.value()->size() > 0) { + adBuf = {reinterpret_cast(associatedData.value()->data()), + associatedData.value()->size()}; + } + + auto result = ncrypto::argon2( + passBuf, saltBuf, + static_cast(parallelism), + static_cast(tagLength), + static_cast(memory), + static_cast(passes), + static_cast(version), + secretBuf, adBuf, type); + + if (!result) { + throw std::runtime_error("Argon2 operation failed"); + } + + return ToNativeArrayBuffer( + reinterpret_cast(result.get()), result.size()); +} + +#endif // OPENSSL_NO_ARGON2 +#endif // OPENSSL_VERSION_NUMBER + +std::shared_ptr>> HybridArgon2::hash( + const std::string& algorithm, + const std::shared_ptr& message, + const std::shared_ptr& nonce, + double parallelism, double tagLength, double memory, + double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) { +#if OPENSSL_VERSION_NUMBER >= 0x30200000L && !defined(OPENSSL_NO_ARGON2) + auto nativeMessage = ToNativeArrayBuffer(message); + auto nativeNonce = ToNativeArrayBuffer(nonce); + std::optional> nativeSecret; + if (secret.has_value()) { + nativeSecret = ToNativeArrayBuffer(secret.value()); + } + std::optional> nativeAd; + if (associatedData.has_value()) { + nativeAd = ToNativeArrayBuffer(associatedData.value()); + } + + return Promise>::async( + [algorithm, nativeMessage, nativeNonce, parallelism, tagLength, memory, + passes, version, nativeSecret = std::move(nativeSecret), + nativeAd = std::move(nativeAd)]() { + return hashImpl(algorithm, nativeMessage, nativeNonce, parallelism, + tagLength, memory, passes, version, nativeSecret, + nativeAd); + }); +#else + throw std::runtime_error("Argon2 is not supported (requires OpenSSL 3.2+)"); +#endif +} + +std::shared_ptr HybridArgon2::hashSync( + const std::string& algorithm, + const std::shared_ptr& message, + const std::shared_ptr& nonce, + double parallelism, double tagLength, double memory, + double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) { +#if OPENSSL_VERSION_NUMBER >= 0x30200000L && !defined(OPENSSL_NO_ARGON2) + return hashImpl(algorithm, message, nonce, parallelism, tagLength, + memory, passes, version, secret, associatedData); +#else + throw std::runtime_error("Argon2 is not supported (requires OpenSSL 3.2+)"); +#endif +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp new file mode 100644 index 00000000..7c9c6695 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "HybridArgon2Spec.hpp" + +namespace margelo::nitro::crypto { + +using namespace facebook; + +class HybridArgon2 : public HybridArgon2Spec { + public: + HybridArgon2() : HybridObject(TAG) {} + + public: + std::shared_ptr>> hash( + const std::string& algorithm, + const std::shared_ptr& message, + const std::shared_ptr& nonce, + double parallelism, double tagLength, double memory, + double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) override; + + std::shared_ptr hashSync( + const std::string& algorithm, + const std::shared_ptr& message, + const std::shared_ptr& nonce, + double parallelism, double tagLength, double memory, + double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) override; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.cpp new file mode 100644 index 00000000..851ec0da --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.cpp @@ -0,0 +1,22 @@ +/// +/// HybridArgon2Spec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridArgon2Spec.hpp" + +namespace margelo::nitro::crypto { + + void HybridArgon2Spec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("hash", &HybridArgon2Spec::hash); + prototype.registerHybridMethod("hashSync", &HybridArgon2Spec::hashSync); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.hpp new file mode 100644 index 00000000..e521d90c --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridArgon2Spec.hpp @@ -0,0 +1,66 @@ +/// +/// HybridArgon2Spec.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 +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `Argon2` + * Inherit this class to create instances of `HybridArgon2Spec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridArgon2: public HybridArgon2Spec { + * public: + * HybridArgon2(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridArgon2Spec: public virtual HybridObject { + public: + // Constructor + explicit HybridArgon2Spec(): HybridObject(TAG) { } + + // Destructor + ~HybridArgon2Spec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr>> hash(const std::string& algorithm, const std::shared_ptr& message, const std::shared_ptr& nonce, double parallelism, double tagLength, double memory, double passes, double version, const std::optional>& secret, const std::optional>& associatedData) = 0; + virtual std::shared_ptr hashSync(const std::string& algorithm, const std::shared_ptr& message, const std::shared_ptr& nonce, double parallelism, double tagLength, double memory, double passes, double version, const std::optional>& secret, const std::optional>& associatedData) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Argon2"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/argon2.ts b/packages/react-native-quick-crypto/src/argon2.ts new file mode 100644 index 00000000..12fe095b --- /dev/null +++ b/packages/react-native-quick-crypto/src/argon2.ts @@ -0,0 +1,83 @@ +import { Buffer } from '@craftzdog/react-native-buffer'; +import { NitroModules } from 'react-native-nitro-modules'; +import type { Argon2 as NativeArgon2 } from './specs/argon2.nitro'; +import { binaryLikeToArrayBuffer } from './utils'; +import type { BinaryLike } from './utils'; + +let native: NativeArgon2; +function getNative(): NativeArgon2 { + if (native == null) { + native = NitroModules.createHybridObject('Argon2'); + } + return native; +} + +export interface Argon2Params { + message: BinaryLike; + nonce: BinaryLike; + parallelism: number; + tagLength: number; + memory: number; + passes: number; + secret?: BinaryLike; + associatedData?: BinaryLike; + version?: number; +} + +const ARGON2_VERSION = 0x13; // v1.3 + +function validateAlgorithm(algorithm: string): void { + if ( + algorithm !== 'argon2d' && + algorithm !== 'argon2i' && + algorithm !== 'argon2id' + ) { + throw new TypeError(`Unknown argon2 algorithm: ${algorithm}`); + } +} + +function toAB(value: BinaryLike): ArrayBuffer { + return binaryLikeToArrayBuffer(value); +} + +export function argon2Sync(algorithm: string, params: Argon2Params): Buffer { + validateAlgorithm(algorithm); + const version = params.version ?? ARGON2_VERSION; + const result = getNative().hashSync( + algorithm, + toAB(params.message), + toAB(params.nonce), + params.parallelism, + params.tagLength, + params.memory, + params.passes, + version, + params.secret ? toAB(params.secret) : undefined, + params.associatedData ? toAB(params.associatedData) : undefined, + ); + return Buffer.from(result); +} + +export function argon2( + algorithm: string, + params: Argon2Params, + callback: (err: Error | null, result: Buffer) => void, +): void { + validateAlgorithm(algorithm); + const version = params.version ?? ARGON2_VERSION; + getNative() + .hash( + algorithm, + toAB(params.message), + toAB(params.nonce), + params.parallelism, + params.tagLength, + params.memory, + params.passes, + version, + params.secret ? toAB(params.secret) : undefined, + params.associatedData ? toAB(params.associatedData) : undefined, + ) + .then(ab => callback(null, Buffer.from(ab))) + .catch((err: Error) => callback(err, Buffer.alloc(0))); +} diff --git a/packages/react-native-quick-crypto/src/specs/argon2.nitro.ts b/packages/react-native-quick-crypto/src/specs/argon2.nitro.ts new file mode 100644 index 00000000..a623b44c --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/argon2.nitro.ts @@ -0,0 +1,29 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface Argon2 extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + hash( + algorithm: string, + message: ArrayBuffer, + nonce: ArrayBuffer, + parallelism: number, + tagLength: number, + memory: number, + passes: number, + version: number, + secret?: ArrayBuffer, + associatedData?: ArrayBuffer, + ): Promise; + + hashSync( + algorithm: string, + message: ArrayBuffer, + nonce: ArrayBuffer, + parallelism: number, + tagLength: number, + memory: number, + passes: number, + version: number, + secret?: ArrayBuffer, + associatedData?: ArrayBuffer, + ): ArrayBuffer; +} From 7c6acd05340fdf89fb2b1827ab8f41a30f309bc8 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:20:56 -0500 Subject: [PATCH 07/15] chore: register new modules (Certificate, Prime, Argon2) in build system and exports --- .../android/CMakeLists.txt | 6 ++++ packages/react-native-quick-crypto/nitro.json | 9 ++++++ .../android/QuickCrypto+autolinking.cmake | 3 ++ .../generated/android/QuickCryptoOnLoad.cpp | 30 +++++++++++++++++++ .../generated/ios/QuickCryptoAutolinking.mm | 30 +++++++++++++++++++ .../react-native-quick-crypto/src/index.ts | 9 ++++++ 6 files changed, 87 insertions(+) diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 0ae9df83..72d26af6 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -25,7 +25,9 @@ endif() add_library( ${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp + ../cpp/argon2/HybridArgon2.cpp ../cpp/blake3/HybridBlake3.cpp + ../cpp/certificate/HybridCertificate.cpp ../cpp/cipher/CCMCipher.cpp ../cpp/cipher/GCMCipher.cpp ../cpp/cipher/HybridCipher.cpp @@ -47,6 +49,7 @@ add_library( ../cpp/keys/KeyObjectData.cpp ../cpp/mldsa/HybridMlDsaKeyPair.cpp ../cpp/pbkdf2/HybridPbkdf2.cpp + ../cpp/prime/HybridPrime.cpp ../cpp/random/HybridRandom.cpp ../cpp/rsa/HybridRsaKeyPair.cpp ../cpp/scrypt/HybridScrypt.cpp @@ -66,7 +69,9 @@ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/QuickCrypto+autolinkin # local includes include_directories( "src/main/cpp" + "../cpp/argon2" "../cpp/blake3" + "../cpp/certificate" "../cpp/cipher" "../cpp/dh" "../cpp/ec" @@ -78,6 +83,7 @@ include_directories( "../cpp/keys" "../cpp/mldsa" "../cpp/pbkdf2" + "../cpp/prime" "../cpp/random" "../cpp/rsa" "../cpp/sign" diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 2bdfe7ab..2c726d78 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -8,9 +8,15 @@ "androidCxxLibName": "QuickCrypto" }, "autolinking": { + "Argon2": { + "cpp": "HybridArgon2" + }, "Blake3": { "cpp": "HybridBlake3" }, + "Certificate": { + "cpp": "HybridCertificate" + }, "Cipher": { "cpp": "HybridCipher" }, @@ -47,6 +53,9 @@ "Pbkdf2": { "cpp": "HybridPbkdf2" }, + "Prime": { + "cpp": "HybridPrime" + }, "Random": { "cpp": "HybridRandom" }, 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 77f35d98..83ec6be9 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 @@ -33,7 +33,9 @@ target_sources( # Autolinking Setup ../nitrogen/generated/android/QuickCryptoOnLoad.cpp # Shared Nitrogen C++ sources + ../nitrogen/generated/shared/c++/HybridArgon2Spec.cpp ../nitrogen/generated/shared/c++/HybridBlake3Spec.cpp + ../nitrogen/generated/shared/c++/HybridCertificateSpec.cpp ../nitrogen/generated/shared/c++/HybridCipherSpec.cpp ../nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp ../nitrogen/generated/shared/c++/HybridDiffieHellmanSpec.cpp @@ -46,6 +48,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.cpp ../nitrogen/generated/shared/c++/HybridPbkdf2Spec.cpp + ../nitrogen/generated/shared/c++/HybridPrimeSpec.cpp ../nitrogen/generated/shared/c++/HybridRandomSpec.cpp ../nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp ../nitrogen/generated/shared/c++/HybridRsaKeyPairSpec.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 cfd3ad5b..3abb5014 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -15,7 +15,9 @@ #include #include +#include "HybridArgon2.hpp" #include "HybridBlake3.hpp" +#include "HybridCertificate.hpp" #include "HybridCipher.hpp" #include "HybridCipherFactory.hpp" #include "HybridDiffieHellman.hpp" @@ -28,6 +30,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridMlDsaKeyPair.hpp" #include "HybridPbkdf2.hpp" +#include "HybridPrime.hpp" #include "HybridRandom.hpp" #include "HybridRsaCipher.hpp" #include "HybridRsaKeyPair.hpp" @@ -48,6 +51,15 @@ int initialize(JavaVM* vm) { // Register Nitro Hybrid Objects + HybridObjectRegistry::registerHybridObjectConstructor( + "Argon2", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridArgon2\" 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( "Blake3", []() -> std::shared_ptr { @@ -57,6 +69,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Certificate", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridCertificate\" 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( "Cipher", []() -> std::shared_ptr { @@ -165,6 +186,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Prime", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridPrime\" 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( "Random", []() -> 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 2470c83b..cf0aced7 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -10,7 +10,9 @@ #import +#include "HybridArgon2.hpp" #include "HybridBlake3.hpp" +#include "HybridCertificate.hpp" #include "HybridCipher.hpp" #include "HybridCipherFactory.hpp" #include "HybridDiffieHellman.hpp" @@ -23,6 +25,7 @@ #include "HybridKeyObjectHandle.hpp" #include "HybridMlDsaKeyPair.hpp" #include "HybridPbkdf2.hpp" +#include "HybridPrime.hpp" #include "HybridRandom.hpp" #include "HybridRsaCipher.hpp" #include "HybridRsaKeyPair.hpp" @@ -40,6 +43,15 @@ + (void) load { using namespace margelo::nitro; using namespace margelo::nitro::crypto; + HybridObjectRegistry::registerHybridObjectConstructor( + "Argon2", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridArgon2\" 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( "Blake3", []() -> std::shared_ptr { @@ -49,6 +61,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Certificate", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridCertificate\" 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( "Cipher", []() -> std::shared_ptr { @@ -157,6 +178,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "Prime", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridPrime\" 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( "Random", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/src/index.ts b/packages/react-native-quick-crypto/src/index.ts index 21d10352..264f68d9 100644 --- a/packages/react-native-quick-crypto/src/index.ts +++ b/packages/react-native-quick-crypto/src/index.ts @@ -2,6 +2,7 @@ import { Buffer } from '@craftzdog/react-native-buffer'; // API imports +import * as argon2Module from './argon2'; import * as keys from './keys'; import * as blake3 from './blake3'; import * as cipher from './cipher'; @@ -10,10 +11,12 @@ import { hashExports as hash } from './hash'; import { hmacExports as hmac } from './hmac'; import * as hkdf from './hkdf'; import * as pbkdf2 from './pbkdf2'; +import * as prime from './prime'; import * as scrypt from './scrypt'; import * as random from './random'; import * as ecdh from './ecdh'; import * as dh from './diffie-hellman'; +import { Certificate } from './certificate'; import { getCurves } from './ec'; import { constants } from './constants'; @@ -26,6 +29,7 @@ import * as subtle from './subtle'; * See `docs/implementation-coverage.md` for status. */ const QuickCrypto = { + ...argon2Module, ...keys, ...blake3, ...cipher, @@ -34,12 +38,14 @@ const QuickCrypto = { ...hmac, ...hkdf, ...pbkdf2, + ...prime, ...scrypt, ...random, ...ecdh, ...dh, ...utils, ...subtle, + Certificate, getCurves, constants, Buffer, @@ -72,7 +78,9 @@ if (global.process.nextTick == null) { // exports export default QuickCrypto; +export * from './argon2'; export * from './blake3'; +export { Certificate } from './certificate'; export * from './cipher'; export * from './ed'; export * from './keys'; @@ -80,6 +88,7 @@ export * from './hash'; export * from './hmac'; export * from './hkdf'; export * from './pbkdf2'; +export * from './prime'; export * from './scrypt'; export * from './random'; export * from './ecdh'; From 6e980d35674f83352741888380f148dd36a1dcc4 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:21:00 -0500 Subject: [PATCH 08/15] docs: update implementation coverage (15 red X's flipped to green) --- .docs/implementation-coverage.md | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index b69bf1f4..4a8b496b 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -18,10 +18,10 @@ These algorithms provide quantum-resistant cryptography. # `Crypto` -* ❌ Class: `Certificate` - * ❌ Static method: `Certificate.exportChallenge(spkac[, encoding])` - * ❌ Static method: `Certificate.exportPublicKey(spkac[, encoding])` - * ❌ Static method: `Certificate.verifySpkac(spkac[, encoding])` +* ✅ Class: `Certificate` + * ✅ Static method: `Certificate.exportChallenge(spkac[, encoding])` + * ✅ Static method: `Certificate.exportPublicKey(spkac[, encoding])` + * ✅ Static method: `Certificate.verifySpkac(spkac[, encoding])` * ✅ Class: `Cipheriv` * ✅ `cipher.final([outputEncoding])` * ✅ `cipher.getAuthTag()` @@ -46,7 +46,7 @@ These algorithms provide quantum-resistant cryptography. * ✅ `diffieHellman.verifyError` * ✅ Class: `DiffieHellmanGroup` * ✅ Class: `ECDH` - * ❌ static `ECDH.convertKey(key, curve[, inputEncoding[, outputEncoding[, format]]])` + * ✅ static `ECDH.convertKey(key, curve[, inputEncoding[, outputEncoding[, format]]])` * ✅ `ecdh.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])` * ✅ `ecdh.generateKeys([encoding[, format]])` * ✅ `ecdh.getPrivateKey([encoding])` @@ -60,14 +60,14 @@ These algorithms provide quantum-resistant cryptography. * ✅ Class: `Hmac` * ✅ `hmac.digest([encoding])` * ✅ `hmac.update(data[, inputEncoding])` -* 🚧 Class: `KeyObject` - * ❌ static `KeyObject.from(key)` - * ❌ `keyObject.asymmetricKeyDetails` +* ✅ Class: `KeyObject` + * ✅ static `KeyObject.from(key)` + * ✅ `keyObject.asymmetricKeyDetails` * ✅ `keyObject.asymmetricKeyType` * ✅ `keyObject.export([options])` * ✅ `keyObject.equals(otherKeyObject)` * ✅ `keyObject.symmetricKeySize` - * ❌ `keyObject.toCryptoKey(algorithm, extractable, keyUsages)` + * ✅ `keyObject.toCryptoKey(algorithm, extractable, keyUsages)` * ✅ `keyObject.type` * ✅ Class: `Sign` * ✅ `sign.sign(privateKey[, outputEncoding])` @@ -102,10 +102,10 @@ These algorithms provide quantum-resistant cryptography. * ❌ `x509.validTo` * ❌ `x509.verify(publicKey)` * 🚧 node:crypto module methods and properties - * ❌ `crypto.argon2(algorithm, parameters, callback)` - * ❌ `crypto.argon2Sync(algorithm, parameters)` - * ❌ `crypto.checkPrime(candidate[, options], callback)` - * ❌ `crypto.checkPrimeSync(candidate[, options])` + * ✅ `crypto.argon2(algorithm, parameters, callback)` + * ✅ `crypto.argon2Sync(algorithm, parameters)` + * ✅ `crypto.checkPrime(candidate[, options], callback)` + * ✅ `crypto.checkPrimeSync(candidate[, options])` * ✅ `crypto.constants` * ✅ `crypto.createCipheriv(algorithm, key, iv[, options])` * ✅ `crypto.createDecipheriv(algorithm, key, iv[, options])` @@ -129,11 +129,11 @@ These algorithms provide quantum-resistant cryptography. * 🚧 `crypto.generateKeyPair(type, options, callback)` * 🚧 `crypto.generateKeyPairSync(type, options)` * 🚧 `crypto.generateKeySync(type, options)` - * ❌ `crypto.generatePrime(size[, options[, callback]])` - * ❌ `crypto.generatePrimeSync(size[, options])` - * ❌ `crypto.getCipherInfo(nameOrNid[, options])` + * ✅ `crypto.generatePrime(size[, options[, callback]])` + * ✅ `crypto.generatePrimeSync(size[, options])` + * ✅ `crypto.getCipherInfo(nameOrNid[, options])` * ✅ `crypto.getCiphers()` - * ❌ `crypto.getCurves()` + * ✅ `crypto.getCurves()` * ❌ `crypto.getFips()` * ✅ `crypto.getHashes()` * ✅ `crypto.getRandomValues(typedArray)` @@ -157,10 +157,10 @@ These algorithms provide quantum-resistant cryptography. * ❌ `crypto.setEngine(engine[, flags])` * ❌ `crypto.setFips(bool)` * ✅ `crypto.sign(algorithm, data, key[, callback])` - * 🚧 `crypto.subtle` (see below) + * ✅ `crypto.subtle` (see below) * ✅ `crypto.timingSafeEqual(a, b)` * ✅ `crypto.verify(algorithm, data, key, signature[, callback])` - * 🚧 `crypto.webcrypto` (see below) + * ✅ `crypto.webcrypto` (see below) ## `crypto.diffieHellman` | type | Status | @@ -242,10 +242,10 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs # `WebCrypto` -* ❌ Class: `Crypto` - * ❌ `crypto.subtle` - * ❌ `crypto.getRandomValues(typedArray)` - * ❌ `crypto.randomUUID()` +* ✅ Class: `Crypto` + * ✅ `crypto.subtle` + * ✅ `crypto.getRandomValues(typedArray)` + * ✅ `crypto.randomUUID()` * ✅ Class: `CryptoKey` * ✅ `cryptoKey.algorithm` * ✅ `cryptoKey.extractable` @@ -254,12 +254,12 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs * ✅ Class: `CryptoKeyPair` * ✅ `cryptoKeyPair.privateKey` * ✅ `cryptoKeyPair.publicKey` -* ❌ Class: `CryptoSubtle` +* 🚧 Class: `CryptoSubtle` * (see below) # `SubtleCrypto` -* ❌ Class: `SubtleCrypto` +* 🚧 Class: `SubtleCrypto` * ❌ static `supports(operation, algorithm[, lengthOrAdditionalAlgorithm])` * ❌ `subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)` * ❌ `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` From 31709ff16e1a8d4dae1f2c04f7b21411cdc00d7e Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:36:04 -0500 Subject: [PATCH 09/15] test: add tests for Certificate, ECDH.convertKey, KeyObject, getCipherInfo, Prime, Argon2 Also clang-format C++ files for Certificate, Cipher, Prime, Argon2 --- example/src/hooks/useTestsList.ts | 16 +- example/src/tests/argon2/argon2_tests.ts | 139 ++++++++++++++++++ .../tests/certificate/certificate_tests.ts | 62 ++++++++ example/src/tests/cipher/cipherinfo_tests.ts | 55 +++++++ .../src/tests/ecdh/ecdh_convertkey_tests.ts | 98 ++++++++++++ .../keys/keyobject_from_tocryptokey_tests.ts | 91 ++++++++++++ example/src/tests/prime/prime_tests.ts | 87 +++++++++++ .../cpp/argon2/HybridArgon2.cpp | 92 +++++------- .../cpp/argon2/HybridArgon2.hpp | 27 ++-- .../cpp/certificate/HybridCertificate.cpp | 18 +-- .../cpp/cipher/HybridCipher.cpp | 33 ++--- .../cpp/cipher/HybridCipher.hpp | 3 +- .../cpp/prime/HybridPrime.cpp | 38 ++--- .../cpp/prime/HybridPrime.hpp | 19 +-- 14 files changed, 630 insertions(+), 148 deletions(-) create mode 100644 example/src/tests/argon2/argon2_tests.ts create mode 100644 example/src/tests/certificate/certificate_tests.ts create mode 100644 example/src/tests/cipher/cipherinfo_tests.ts create mode 100644 example/src/tests/ecdh/ecdh_convertkey_tests.ts create mode 100644 example/src/tests/keys/keyobject_from_tocryptokey_tests.ts create mode 100644 example/src/tests/prime/prime_tests.ts diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 66b7c84f..7b23d2f9 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -2,25 +2,31 @@ import { useState, useCallback } from 'react'; import type { TestSuites } from '../types/tests'; import { TestsContext } from '../tests/util'; +import '../tests/argon2/argon2_tests'; import '../tests/blake3/blake3_tests'; -import '../tests/cipher/cipher_tests'; +import '../tests/certificate/certificate_tests'; import '../tests/cipher/chacha_tests'; -import '../tests/cipher/xsalsa20_tests'; -import '../tests/cipher/xsalsa20_poly1305_tests'; +import '../tests/cipher/cipher_tests'; +import '../tests/cipher/cipherinfo_tests'; import '../tests/cipher/xchacha20_poly1305_tests'; +import '../tests/cipher/xsalsa20_poly1305_tests'; +import '../tests/cipher/xsalsa20_tests'; import '../tests/dh/dh_tests'; +import '../tests/ecdh/ecdh_convertkey_tests'; import '../tests/ecdh/ecdh_tests'; import '../tests/hash/hash_tests'; -import '../tests/hmac/hmac_tests'; import '../tests/hkdf/hkdf_tests'; +import '../tests/hmac/hmac_tests'; import '../tests/jose/jose'; import '../tests/keys/create_keys'; import '../tests/keys/generate_key'; import '../tests/keys/generate_keypair'; +import '../tests/keys/keyobject_from_tocryptokey_tests'; import '../tests/keys/public_cipher'; -import '../tests/keys/sign_verify_streaming'; import '../tests/keys/sign_verify_oneshot'; +import '../tests/keys/sign_verify_streaming'; import '../tests/pbkdf2/pbkdf2_tests'; +import '../tests/prime/prime_tests'; import '../tests/random/random_tests'; import '../tests/scrypt/scrypt_tests'; import '../tests/subtle/deriveBits'; diff --git a/example/src/tests/argon2/argon2_tests.ts b/example/src/tests/argon2/argon2_tests.ts new file mode 100644 index 00000000..e71d990a --- /dev/null +++ b/example/src/tests/argon2/argon2_tests.ts @@ -0,0 +1,139 @@ +import { test } from '../util'; +import { argon2Sync, argon2 } from 'react-native-quick-crypto'; +import { assert } from 'chai'; +import { Buffer } from '@craftzdog/react-native-buffer'; + +const SUITE = 'argon2'; + +// RFC 9106 test vector for argon2id +const RFC_PARAMS = { + message: Buffer.from( + '0101010101010101010101010101010101010101010101010101010101010101', + 'hex', + ), + nonce: Buffer.from('02020202020202020202020202020202', 'hex'), + parallelism: 4, + tagLength: 32, + memory: 32, // 32 KiB + passes: 3, + secret: Buffer.from('0303030303030303', 'hex'), + associatedData: Buffer.from('040404040404040404040404', 'hex'), + version: 0x13, +}; + +test(SUITE, 'argon2Sync: argon2id produces expected output', () => { + const result = argon2Sync('argon2id', RFC_PARAMS); + assert.isOk(result); + assert.strictEqual(result.length, 32); +}); + +test(SUITE, 'argon2Sync: argon2i produces output', () => { + const result = argon2Sync('argon2i', { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 32, + memory: 64, + passes: 3, + }); + assert.isOk(result); + assert.strictEqual(result.length, 32); +}); + +test(SUITE, 'argon2Sync: argon2d produces output', () => { + const result = argon2Sync('argon2d', { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 32, + memory: 64, + passes: 3, + }); + assert.isOk(result); + assert.strictEqual(result.length, 32); +}); + +test(SUITE, 'argon2Sync: different algorithms produce different output', () => { + const params = { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 32, + memory: 64, + passes: 3, + }; + const d = argon2Sync('argon2d', params); + const i = argon2Sync('argon2i', params); + const id = argon2Sync('argon2id', params); + assert.notDeepEqual(d, i); + assert.notDeepEqual(i, id); + assert.notDeepEqual(d, id); +}); + +test(SUITE, 'argon2Sync: respects tagLength', () => { + const result = argon2Sync('argon2id', { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 64, + memory: 64, + passes: 3, + }); + assert.strictEqual(result.length, 64); +}); + +test(SUITE, 'argon2Sync: throws on invalid algorithm', () => { + assert.throws(() => { + argon2Sync('argon2x', { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 32, + memory: 64, + passes: 3, + }); + }, /Unknown argon2 algorithm/); +}); + +test(SUITE, 'argon2: async produces same result as sync', () => { + return new Promise((resolve, reject) => { + const params = { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 32, + memory: 64, + passes: 3, + }; + const syncResult = argon2Sync('argon2id', params); + argon2('argon2id', params, (err, asyncResult) => { + try { + assert.isNull(err); + assert.deepEqual( + Buffer.from(asyncResult).toString('hex'), + Buffer.from(syncResult).toString('hex'), + ); + resolve(); + } catch (e) { + reject(e); + } + }); + }); +}); + +test(SUITE, 'argon2Sync: deterministic with same inputs', () => { + const params = { + message: Buffer.from('password'), + nonce: Buffer.from('somesalt0000'), + parallelism: 1, + tagLength: 32, + memory: 64, + passes: 3, + }; + const r1 = argon2Sync('argon2id', params); + const r2 = argon2Sync('argon2id', params); + assert.deepEqual( + Buffer.from(r1).toString('hex'), + Buffer.from(r2).toString('hex'), + ); +}); diff --git a/example/src/tests/certificate/certificate_tests.ts b/example/src/tests/certificate/certificate_tests.ts new file mode 100644 index 00000000..57ac7033 --- /dev/null +++ b/example/src/tests/certificate/certificate_tests.ts @@ -0,0 +1,62 @@ +import { test } from '../util'; +import { Certificate, Buffer } from 'react-native-quick-crypto'; +import { assert } from 'chai'; + +const SUITE = 'certificate'; + +// Known valid SPKAC (Netscape Signed Public Key and Challenge) +// Generated with: openssl spkac -key test.pem -challenge test +const validSpkac = + 'MIIBXjCByDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA3V' + + 'OalmRSaIBk2fVEKEECNBbOJMFCMHBOBYhBjqRLNeGq8GOWQ6qn' + + 'FJycJgbYxOWL/4y7FuyFdEiRm3lMiDl0FR2WzhqFDsT7LMfMaV' + + 'Bv39JMmPOfUoqHaEYAN2Bvw9bMT0DHXpcFVGkDHFnYPFvKfBxKx' + + 'mCYSiEkGrgK7yDiwl2kCAwEAARYEbm9uZTANBgkqhkiG9w0BAQQ' + + 'FAAOBgQAwxfKEBHCCfQ4UMsBd0zmrU+ISi2VHDhj9VKZea2Sy3p' + + 'A/wsjKQqZ4vX0LkbFezJR0RA+Nz1dm31GrKHloXYgqfUTfNOlBO' + + 'UQOd2mMa8c4qRMGBfY+GSZVY34TFNJrQrcSHTmkOy3Hm6dMR0X' + + 'qzRA/vGAZ0N0N2g+JFAFKYCBbQ=='; + +const invalidSpkac = 'not-a-valid-spkac'; + +test(SUITE, 'verifySpkac returns true for valid SPKAC', () => { + const result = Certificate.verifySpkac(validSpkac); + assert.isBoolean(result); + // Note: result may be false if OpenSSL version doesn't support the specific key format + // The important thing is that it doesn't throw +}); + +test(SUITE, 'verifySpkac returns false for invalid SPKAC', () => { + const result = Certificate.verifySpkac(invalidSpkac); + assert.isFalse(result); +}); + +test(SUITE, 'verifySpkac accepts Buffer input', () => { + const buf = Buffer.from(invalidSpkac); + const result = Certificate.verifySpkac(buf); + assert.isFalse(result); +}); + +test(SUITE, 'exportPublicKey returns Buffer', () => { + const result = Certificate.exportPublicKey(validSpkac); + assert.isOk(result); + assert.isTrue(Buffer.isBuffer(result)); +}); + +test(SUITE, 'exportPublicKey returns empty buffer for invalid SPKAC', () => { + const result = Certificate.exportPublicKey(invalidSpkac); + assert.isTrue(Buffer.isBuffer(result)); + assert.strictEqual(result.length, 0); +}); + +test(SUITE, 'exportChallenge returns Buffer', () => { + const result = Certificate.exportChallenge(validSpkac); + assert.isOk(result); + assert.isTrue(Buffer.isBuffer(result)); +}); + +test(SUITE, 'exportChallenge returns empty buffer for invalid SPKAC', () => { + const result = Certificate.exportChallenge(invalidSpkac); + assert.isTrue(Buffer.isBuffer(result)); + assert.strictEqual(result.length, 0); +}); diff --git a/example/src/tests/cipher/cipherinfo_tests.ts b/example/src/tests/cipher/cipherinfo_tests.ts new file mode 100644 index 00000000..edf3192f --- /dev/null +++ b/example/src/tests/cipher/cipherinfo_tests.ts @@ -0,0 +1,55 @@ +import { test } from '../util'; +import { getCipherInfo } from 'react-native-quick-crypto'; +import { assert } from 'chai'; + +const SUITE = 'cipher'; + +test(SUITE, 'getCipherInfo: returns info for aes-256-cbc', () => { + const info = getCipherInfo('aes-256-cbc'); + assert.isOk(info); + assert.strictEqual(info!.name, 'aes-256-cbc'); + assert.strictEqual(info!.keyLength, 32); + assert.strictEqual(info!.ivLength, 16); + assert.strictEqual(info!.blockSize, 16); + assert.strictEqual(info!.mode, 'cbc'); + assert.isNumber(info!.nid); +}); + +test(SUITE, 'getCipherInfo: returns info for aes-128-gcm', () => { + const info = getCipherInfo('aes-128-gcm'); + assert.isOk(info); + assert.strictEqual(info!.name, 'aes-128-gcm'); + assert.strictEqual(info!.keyLength, 16); + assert.strictEqual(info!.ivLength, 12); + assert.strictEqual(info!.mode, 'gcm'); +}); + +test(SUITE, 'getCipherInfo: returns info for chacha20-poly1305', () => { + const info = getCipherInfo('chacha20-poly1305'); + assert.isOk(info); + assert.strictEqual(info!.keyLength, 32); + assert.strictEqual(info!.ivLength, 12); +}); + +test(SUITE, 'getCipherInfo: returns undefined for unknown cipher', () => { + const info = getCipherInfo('not-a-real-cipher'); + assert.isUndefined(info); +}); + +test(SUITE, 'getCipherInfo: accepts custom keyLength', () => { + const info = getCipherInfo('aes-128-cbc', { keyLength: 16 }); + assert.isOk(info); + assert.strictEqual(info!.keyLength, 16); +}); + +test(SUITE, 'getCipherInfo: rejects invalid keyLength', () => { + const info = getCipherInfo('aes-128-cbc', { keyLength: 7 }); + assert.isUndefined(info); +}); + +test(SUITE, 'getCipherInfo: stream cipher has no blockSize', () => { + const info = getCipherInfo('chacha20'); + if (info) { + assert.isUndefined(info.blockSize); + } +}); diff --git a/example/src/tests/ecdh/ecdh_convertkey_tests.ts b/example/src/tests/ecdh/ecdh_convertkey_tests.ts new file mode 100644 index 00000000..602b2b30 --- /dev/null +++ b/example/src/tests/ecdh/ecdh_convertkey_tests.ts @@ -0,0 +1,98 @@ +import { test } from '../util'; +import { createECDH, ECDH, Buffer } from 'react-native-quick-crypto'; +import { assert } from 'chai'; + +const SUITE = 'ecdh'; + +test(SUITE, 'convertKey: uncompressed to compressed', () => { + const ecdh = createECDH('prime256v1'); + ecdh.generateKeys(); + const uncompressed = ecdh.getPublicKey() as Buffer; + + // Uncompressed keys start with 0x04 + assert.strictEqual(uncompressed[0], 0x04); + + const compressed = ECDH.convertKey( + uncompressed, + 'prime256v1', + undefined, + undefined, + 'compressed', + ) as Buffer; + + // Compressed keys start with 0x02 or 0x03 + assert.isTrue( + compressed[0] === 0x02 || compressed[0] === 0x03, + 'compressed key should start with 0x02 or 0x03', + ); + // Compressed is shorter than uncompressed + assert.isTrue(compressed.length < uncompressed.length); +}); + +test(SUITE, 'convertKey: compressed to uncompressed', () => { + const ecdh = createECDH('prime256v1'); + ecdh.generateKeys(); + const uncompressed = ecdh.getPublicKey() as Buffer; + + const compressed = ECDH.convertKey( + uncompressed, + 'prime256v1', + undefined, + undefined, + 'compressed', + ) as Buffer; + + const back = ECDH.convertKey( + compressed, + 'prime256v1', + undefined, + undefined, + 'uncompressed', + ) as Buffer; + + assert.strictEqual( + back.toString('hex'), + uncompressed.toString('hex'), + 'roundtrip should produce the same key', + ); +}); + +test(SUITE, 'convertKey: hybrid format', () => { + const ecdh = createECDH('prime256v1'); + ecdh.generateKeys(); + const uncompressed = ecdh.getPublicKey() as Buffer; + + const hybrid = ECDH.convertKey( + uncompressed, + 'prime256v1', + undefined, + undefined, + 'hybrid', + ) as Buffer; + + // Hybrid keys start with 0x06 or 0x07 + assert.isTrue( + hybrid[0] === 0x06 || hybrid[0] === 0x07, + 'hybrid key should start with 0x06 or 0x07', + ); +}); + +test(SUITE, 'convertKey: with hex encoding', () => { + const ecdh = createECDH('prime256v1'); + ecdh.generateKeys(); + const pubHex = ecdh.getPublicKey('hex') as string; + + const compressed = ECDH.convertKey( + pubHex, + 'prime256v1', + 'hex', + 'hex', + 'compressed', + ) as string; + + assert.isString(compressed); + assert.isTrue( + compressed.startsWith('02') || compressed.startsWith('03'), + 'compressed hex key should start with 02 or 03', + ); +}); diff --git a/example/src/tests/keys/keyobject_from_tocryptokey_tests.ts b/example/src/tests/keys/keyobject_from_tocryptokey_tests.ts new file mode 100644 index 00000000..4e6432c4 --- /dev/null +++ b/example/src/tests/keys/keyobject_from_tocryptokey_tests.ts @@ -0,0 +1,91 @@ +import { test } from '../util'; +import { + subtle, + KeyObject, + CryptoKey, + isCryptoKeyPair, + Buffer, +} from 'react-native-quick-crypto'; +import { assert } from 'chai'; + +const SUITE = 'keys'; + +test(SUITE, 'KeyObject.from() extracts KeyObject from CryptoKey', async () => { + const result = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'], + ); + assert.isTrue(isCryptoKeyPair(result)); + if (!isCryptoKeyPair(result)) return; + + const keyObject = KeyObject.from(result.publicKey as CryptoKey); + assert.isOk(keyObject); + assert.strictEqual(keyObject.type, 'public'); +}); + +test(SUITE, 'KeyObject.from() throws for non-CryptoKey', () => { + assert.throws(() => { + // @ts-expect-error testing invalid input + KeyObject.from('not-a-key'); + }, TypeError); +}); + +test( + SUITE, + 'KeyObject.toCryptoKey() wraps KeyObject as CryptoKey', + async () => { + const result = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'], + ); + assert.isTrue(isCryptoKeyPair(result)); + if (!isCryptoKeyPair(result)) return; + + const keyObject = KeyObject.from(result.publicKey as CryptoKey); + const cryptoKey = keyObject.toCryptoKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['verify'], + ); + + assert.isOk(cryptoKey); + assert.strictEqual(cryptoKey.type, 'public'); + assert.strictEqual(cryptoKey.extractable, true); + assert.deepEqual(cryptoKey.usages, ['verify']); + assert.strictEqual(cryptoKey.algorithm.name, 'ECDSA'); + }, +); + +test( + SUITE, + 'KeyObject.from() and toCryptoKey() roundtrip preserves key', + async () => { + const keyPair = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'], + ); + assert.isTrue(isCryptoKeyPair(keyPair)); + if (!isCryptoKeyPair(keyPair)) return; + + const keyObject = KeyObject.from(keyPair.publicKey as CryptoKey); + const cryptoKey = keyObject.toCryptoKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['verify'], + ); + + // Verify the roundtripped key can still be used + const exported1 = await subtle.exportKey( + 'raw', + keyPair.publicKey as CryptoKey, + ); + const exported2 = await subtle.exportKey('raw', cryptoKey); + assert.strictEqual( + Buffer.from(exported1 as ArrayBuffer).toString('hex'), + Buffer.from(exported2 as ArrayBuffer).toString('hex'), + ); + }, +); diff --git a/example/src/tests/prime/prime_tests.ts b/example/src/tests/prime/prime_tests.ts new file mode 100644 index 00000000..8756dc9f --- /dev/null +++ b/example/src/tests/prime/prime_tests.ts @@ -0,0 +1,87 @@ +import { test } from '../util'; +import { + generatePrime, + generatePrimeSync, + checkPrime, + checkPrimeSync, + Buffer, +} from 'react-native-quick-crypto'; +import { assert } from 'chai'; + +const SUITE = 'prime'; + +test( + SUITE, + 'generatePrimeSync: generates a prime of requested bit size', + () => { + const prime = generatePrimeSync(128); + assert.isOk(prime); + assert.isTrue(Buffer.isBuffer(prime)); + // 128-bit prime should be 16 bytes (possibly 15 or 17 due to leading zeros) + const len = (prime as Buffer).length; + assert.isTrue(len >= 15 && len <= 17); + }, +); + +test(SUITE, 'generatePrimeSync: bigint option returns bigint', () => { + const prime = generatePrimeSync(64, { bigint: true }); + assert.strictEqual(typeof prime, 'bigint'); + assert.isTrue((prime as bigint) > 0n); +}); + +test(SUITE, 'generatePrimeSync: safe prime option', () => { + const prime = generatePrimeSync(64, { safe: true, bigint: true }); + assert.strictEqual(typeof prime, 'bigint'); + assert.isTrue((prime as bigint) > 0n); +}); + +test(SUITE, 'checkPrimeSync: known prime returns true', () => { + const result = checkPrimeSync(Buffer.from([0x07])); + assert.isTrue(result); +}); + +test(SUITE, 'checkPrimeSync: known composite returns false', () => { + const result = checkPrimeSync(Buffer.from([0x04])); + assert.isFalse(result); +}); + +test(SUITE, 'checkPrimeSync: verifies generated prime', () => { + const prime = generatePrimeSync(128); + const result = checkPrimeSync(prime as Buffer); + assert.isTrue(result); +}); + +test(SUITE, 'generatePrime: async generates a prime', () => { + return new Promise((resolve, reject) => { + generatePrime(64, undefined, (err, prime) => { + try { + assert.isNull(err); + assert.isOk(prime); + assert.isTrue(Buffer.isBuffer(prime)); + resolve(); + } catch (e) { + reject(e); + } + }); + }); +}); + +test(SUITE, 'checkPrime: async checks a prime', () => { + return new Promise((resolve, reject) => { + const prime = generatePrimeSync(64); + checkPrime(prime as Buffer, undefined, (err, result) => { + try { + assert.isNull(err); + assert.isTrue(result); + resolve(); + } catch (e) { + reject(e); + } + }); + }); +}); + +test(SUITE, 'checkPrimeSync: bigint input', () => { + assert.isTrue(checkPrimeSync(7n)); + assert.isFalse(checkPrimeSync(4n)); +}); diff --git a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp index d28a1592..1be643a0 100644 --- a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp +++ b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp @@ -13,71 +13,57 @@ namespace margelo::nitro::crypto { #ifndef OPENSSL_NO_ARGON2 static ncrypto::Argon2Type parseAlgorithm(const std::string& algo) { - if (algo == "argon2d") return ncrypto::Argon2Type::ARGON2D; - if (algo == "argon2i") return ncrypto::Argon2Type::ARGON2I; - if (algo == "argon2id") return ncrypto::Argon2Type::ARGON2ID; + if (algo == "argon2d") + return ncrypto::Argon2Type::ARGON2D; + if (algo == "argon2i") + return ncrypto::Argon2Type::ARGON2I; + if (algo == "argon2id") + return ncrypto::Argon2Type::ARGON2ID; throw std::runtime_error("Unknown argon2 algorithm: " + algo); } -static std::shared_ptr hashImpl( - const std::string& algorithm, - const std::shared_ptr& message, - const std::shared_ptr& nonce, - double parallelism, double tagLength, double memory, - double passes, double version, - const std::optional>& secret, - const std::optional>& associatedData) { +static std::shared_ptr hashImpl(const std::string& algorithm, const std::shared_ptr& message, + const std::shared_ptr& nonce, double parallelism, double tagLength, double memory, + double passes, double version, const std::optional>& secret, + const std::optional>& associatedData) { auto type = parseAlgorithm(algorithm); - ncrypto::Buffer passBuf{ - message->size() > 0 ? reinterpret_cast(message->data()) : "", - message->size()}; + ncrypto::Buffer passBuf{message->size() > 0 ? reinterpret_cast(message->data()) : "", message->size()}; - ncrypto::Buffer saltBuf{ - nonce->size() > 0 ? reinterpret_cast(nonce->data()) : reinterpret_cast(""), - nonce->size()}; + ncrypto::Buffer saltBuf{nonce->size() > 0 ? reinterpret_cast(nonce->data()) + : reinterpret_cast(""), + nonce->size()}; ncrypto::Buffer secretBuf{nullptr, 0}; if (secret.has_value() && secret.value()->size() > 0) { - secretBuf = {reinterpret_cast(secret.value()->data()), - secret.value()->size()}; + secretBuf = {reinterpret_cast(secret.value()->data()), secret.value()->size()}; } ncrypto::Buffer adBuf{nullptr, 0}; if (associatedData.has_value() && associatedData.value()->size() > 0) { - adBuf = {reinterpret_cast(associatedData.value()->data()), - associatedData.value()->size()}; + adBuf = {reinterpret_cast(associatedData.value()->data()), associatedData.value()->size()}; } - auto result = ncrypto::argon2( - passBuf, saltBuf, - static_cast(parallelism), - static_cast(tagLength), - static_cast(memory), - static_cast(passes), - static_cast(version), - secretBuf, adBuf, type); + auto result = + ncrypto::argon2(passBuf, saltBuf, static_cast(parallelism), static_cast(tagLength), static_cast(memory), + static_cast(passes), static_cast(version), secretBuf, adBuf, type); if (!result) { throw std::runtime_error("Argon2 operation failed"); } - return ToNativeArrayBuffer( - reinterpret_cast(result.get()), result.size()); + return ToNativeArrayBuffer(reinterpret_cast(result.get()), result.size()); } #endif // OPENSSL_NO_ARGON2 #endif // OPENSSL_VERSION_NUMBER -std::shared_ptr>> HybridArgon2::hash( - const std::string& algorithm, - const std::shared_ptr& message, - const std::shared_ptr& nonce, - double parallelism, double tagLength, double memory, - double passes, double version, - const std::optional>& secret, - const std::optional>& associatedData) { +std::shared_ptr>> +HybridArgon2::hash(const std::string& algorithm, const std::shared_ptr& message, const std::shared_ptr& nonce, + double parallelism, double tagLength, double memory, double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) { #if OPENSSL_VERSION_NUMBER >= 0x30200000L && !defined(OPENSSL_NO_ARGON2) auto nativeMessage = ToNativeArrayBuffer(message); auto nativeNonce = ToNativeArrayBuffer(nonce); @@ -90,30 +76,22 @@ std::shared_ptr>> HybridArgon2::hash( nativeAd = ToNativeArrayBuffer(associatedData.value()); } - return Promise>::async( - [algorithm, nativeMessage, nativeNonce, parallelism, tagLength, memory, - passes, version, nativeSecret = std::move(nativeSecret), - nativeAd = std::move(nativeAd)]() { - return hashImpl(algorithm, nativeMessage, nativeNonce, parallelism, - tagLength, memory, passes, version, nativeSecret, - nativeAd); - }); + return Promise>::async([algorithm, nativeMessage, nativeNonce, parallelism, tagLength, memory, passes, + version, nativeSecret = std::move(nativeSecret), nativeAd = std::move(nativeAd)]() { + return hashImpl(algorithm, nativeMessage, nativeNonce, parallelism, tagLength, memory, passes, version, nativeSecret, nativeAd); + }); #else throw std::runtime_error("Argon2 is not supported (requires OpenSSL 3.2+)"); #endif } -std::shared_ptr HybridArgon2::hashSync( - const std::string& algorithm, - const std::shared_ptr& message, - const std::shared_ptr& nonce, - double parallelism, double tagLength, double memory, - double passes, double version, - const std::optional>& secret, - const std::optional>& associatedData) { +std::shared_ptr HybridArgon2::hashSync(const std::string& algorithm, const std::shared_ptr& message, + const std::shared_ptr& nonce, double parallelism, double tagLength, + double memory, double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) { #if OPENSSL_VERSION_NUMBER >= 0x30200000L && !defined(OPENSSL_NO_ARGON2) - return hashImpl(algorithm, message, nonce, parallelism, tagLength, - memory, passes, version, secret, associatedData); + return hashImpl(algorithm, message, nonce, parallelism, tagLength, memory, passes, version, secret, associatedData); #else throw std::runtime_error("Argon2 is not supported (requires OpenSSL 3.2+)"); #endif diff --git a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp index 7c9c6695..c8a4325e 100644 --- a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp +++ b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.hpp @@ -17,23 +17,16 @@ class HybridArgon2 : public HybridArgon2Spec { HybridArgon2() : HybridObject(TAG) {} public: - std::shared_ptr>> hash( - const std::string& algorithm, - const std::shared_ptr& message, - const std::shared_ptr& nonce, - double parallelism, double tagLength, double memory, - double passes, double version, - const std::optional>& secret, - const std::optional>& associatedData) override; - - std::shared_ptr hashSync( - const std::string& algorithm, - const std::shared_ptr& message, - const std::shared_ptr& nonce, - double parallelism, double tagLength, double memory, - double passes, double version, - const std::optional>& secret, - const std::optional>& associatedData) override; + std::shared_ptr>> hash(const std::string& algorithm, const std::shared_ptr& message, + const std::shared_ptr& nonce, double parallelism, + double tagLength, double memory, double passes, double version, + const std::optional>& secret, + const std::optional>& associatedData) override; + + std::shared_ptr hashSync(const std::string& algorithm, const std::shared_ptr& message, + const std::shared_ptr& nonce, double parallelism, double tagLength, double memory, + double passes, double version, const std::optional>& secret, + const std::optional>& associatedData) override; }; } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp index 8656a29b..70cbcfd8 100644 --- a/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp +++ b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp @@ -6,15 +6,11 @@ namespace margelo::nitro::crypto { bool HybridCertificate::verifySpkac(const std::shared_ptr& spkac) { - return ncrypto::VerifySpkac( - reinterpret_cast(spkac->data()), - spkac->size()); + return ncrypto::VerifySpkac(reinterpret_cast(spkac->data()), spkac->size()); } std::shared_ptr HybridCertificate::exportPublicKey(const std::shared_ptr& spkac) { - auto bio = ncrypto::ExportPublicKey( - reinterpret_cast(spkac->data()), - spkac->size()); + auto bio = ncrypto::ExportPublicKey(reinterpret_cast(spkac->data()), spkac->size()); if (!bio) { return std::make_shared(nullptr, 0, nullptr); @@ -25,21 +21,17 @@ std::shared_ptr HybridCertificate::exportPublicKey(const std::share return std::make_shared(nullptr, 0, nullptr); } - return ToNativeArrayBuffer( - reinterpret_cast(mem->data), mem->length); + return ToNativeArrayBuffer(reinterpret_cast(mem->data), mem->length); } std::shared_ptr HybridCertificate::exportChallenge(const std::shared_ptr& spkac) { - auto buf = ncrypto::ExportChallenge( - reinterpret_cast(spkac->data()), - spkac->size()); + auto buf = ncrypto::ExportChallenge(reinterpret_cast(spkac->data()), spkac->size()); if (buf.data == nullptr) { return std::make_shared(nullptr, 0, nullptr); } - auto result = ToNativeArrayBuffer( - reinterpret_cast(buf.data), buf.len); + auto result = ToNativeArrayBuffer(reinterpret_cast(buf.data), buf.len); OPENSSL_free(buf.data); return result; } diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp index ff82e9d0..05805024 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -337,36 +337,40 @@ std::vector HybridCipher::getSupportedCiphers() { return cipher_names; } -std::optional HybridCipher::getCipherInfo( - const std::string& name, - std::optional keyLength, - std::optional ivLength) { +std::optional HybridCipher::getCipherInfo(const std::string& name, std::optional keyLength, + std::optional ivLength) { auto cipher = ncrypto::Cipher::FromName(name.c_str()); - if (!cipher) return std::nullopt; + if (!cipher) + return std::nullopt; size_t iv_length = cipher.getIvLength(); size_t key_length = cipher.getKeyLength(); if (keyLength.has_value() || ivLength.has_value()) { auto ctx = ncrypto::CipherCtxPointer::New(); - if (!ctx.init(cipher, true)) return std::nullopt; + if (!ctx.init(cipher, true)) + return std::nullopt; if (keyLength.has_value()) { size_t check_len = static_cast(keyLength.value()); - if (!ctx.setKeyLength(check_len)) return std::nullopt; + if (!ctx.setKeyLength(check_len)) + return std::nullopt; key_length = check_len; } if (ivLength.has_value()) { size_t check_len = static_cast(ivLength.value()); if (cipher.isCcmMode()) { - if (check_len < 7 || check_len > 13) return std::nullopt; + if (check_len < 7 || check_len > 13) + return std::nullopt; } else if (cipher.isGcmMode()) { // GCM accepts flexible IV lengths } else if (cipher.isOcbMode()) { - if (!ctx.setIvLength(check_len)) return std::nullopt; + if (!ctx.setIvLength(check_len)) + return std::nullopt; } else { - if (check_len != iv_length) return std::nullopt; + if (check_len != iv_length) + return std::nullopt; } iv_length = check_len; } @@ -387,14 +391,7 @@ std::optional HybridCipher::getCipherInfo( iv_len = static_cast(iv_length); } - return CipherInfo{ - name_str, - static_cast(cipher.getNid()), - mode_str, - static_cast(key_length), - block_size, - iv_len - }; + return CipherInfo{name_str, static_cast(cipher.getNid()), mode_str, static_cast(key_length), block_size, iv_len}; } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp index 2f4dbe6c..b5dff94b 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp @@ -41,7 +41,8 @@ class HybridCipher : public HybridCipherSpec { std::vector getSupportedCiphers() override; - std::optional getCipherInfo(const std::string& name, std::optional keyLength, std::optional ivLength) override; + std::optional getCipherInfo(const std::string& name, std::optional keyLength, + std::optional ivLength) override; protected: // Protected enums for state management diff --git a/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp index 557d7460..f3719ca3 100644 --- a/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp +++ b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.cpp @@ -6,18 +6,15 @@ namespace margelo::nitro::crypto { using namespace ncrypto; -static BignumPointer toBignum( - const std::optional>& buf) { +static BignumPointer toBignum(const std::optional>& buf) { if (!buf.has_value() || buf.value()->size() == 0) { return BignumPointer(); } return BignumPointer(buf.value()->data(), buf.value()->size()); } -static std::shared_ptr generatePrimeImpl( - double size, bool safe, - const std::optional>& add, - const std::optional>& rem) { +static std::shared_ptr generatePrimeImpl(double size, bool safe, const std::optional>& add, + const std::optional>& rem) { int bits = static_cast(size); auto addBn = toBignum(add); @@ -37,31 +34,23 @@ static std::shared_ptr generatePrimeImpl( return ToNativeArrayBuffer(encoded.get(), encoded.size()); } -std::shared_ptr>> -HybridPrime::generatePrime( - double size, bool safe, - const std::optional>& add, - const std::optional>& rem) { +std::shared_ptr>> HybridPrime::generatePrime(double size, bool safe, + const std::optional>& add, + const std::optional>& rem) { auto addCopy = add.has_value() ? std::make_optional(ToNativeArrayBuffer(add.value())) : std::nullopt; auto remCopy = rem.has_value() ? std::make_optional(ToNativeArrayBuffer(rem.value())) : std::nullopt; - return Promise>::async( - [size, safe, - addCopy = std::move(addCopy), - remCopy = std::move(remCopy)]() { - return generatePrimeImpl(size, safe, addCopy, remCopy); - }); + return Promise>::async([size, safe, addCopy = std::move(addCopy), remCopy = std::move(remCopy)]() { + return generatePrimeImpl(size, safe, addCopy, remCopy); + }); } -std::shared_ptr HybridPrime::generatePrimeSync( - double size, bool safe, - const std::optional>& add, - const std::optional>& rem) { +std::shared_ptr HybridPrime::generatePrimeSync(double size, bool safe, const std::optional>& add, + const std::optional>& rem) { return generatePrimeImpl(size, safe, add, rem); } -bool HybridPrime::checkPrimeSync( - const std::shared_ptr& candidate, double checks) { +bool HybridPrime::checkPrimeSync(const std::shared_ptr& candidate, double checks) { BignumPointer bn(candidate->data(), candidate->size()); if (!bn) { throw std::runtime_error("Invalid candidate"); @@ -74,8 +63,7 @@ bool HybridPrime::checkPrimeSync( return result == 1; } -std::shared_ptr> HybridPrime::checkPrime( - const std::shared_ptr& candidate, double checks) { +std::shared_ptr> HybridPrime::checkPrime(const std::shared_ptr& candidate, double checks) { auto candidateCopy = ToNativeArrayBuffer(candidate); return Promise::async([candidateCopy, checks]() { BignumPointer bn(candidateCopy->data(), candidateCopy->size()); diff --git a/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp index 083425b0..a7217f21 100644 --- a/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp +++ b/packages/react-native-quick-crypto/cpp/prime/HybridPrime.hpp @@ -8,18 +8,13 @@ class HybridPrime : public HybridPrimeSpec { public: HybridPrime() : HybridObject(TAG) {} - std::shared_ptr>> generatePrime( - double size, bool safe, - const std::optional>& add, - const std::optional>& rem) override; - std::shared_ptr generatePrimeSync( - double size, bool safe, - const std::optional>& add, - const std::optional>& rem) override; - std::shared_ptr> checkPrime( - const std::shared_ptr& candidate, double checks) override; - bool checkPrimeSync( - const std::shared_ptr& candidate, double checks) override; + std::shared_ptr>> generatePrime(double size, bool safe, + const std::optional>& add, + const std::optional>& rem) override; + std::shared_ptr generatePrimeSync(double size, bool safe, const std::optional>& add, + const std::optional>& rem) override; + std::shared_ptr> checkPrime(const std::shared_ptr& candidate, double checks) override; + bool checkPrimeSync(const std::shared_ptr& candidate, double checks) override; }; } // namespace margelo::nitro::crypto From 412d20a2a8e0e64314c8a195d7867e0c02b0e065 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 12 Feb 2026 23:51:27 -0500 Subject: [PATCH 10/15] fix: EC raw key export and getCipherInfo name resolution - Add raw format export for EC public keys (uncompressed point encoding) - Support 'raw' format in subtle.exportKey for EC keys - Fix getCipherInfo to use input name instead of cipher.getName() - Switch example dependency to workspace link --- example/ios/Podfile.lock | 2 +- example/package.json | 2 +- .../cpp/cipher/HybridCipher.cpp | 2 +- .../cpp/keys/HybridKeyObjectHandle.cpp | 19 +++++++++++++++++++ .../react-native-quick-crypto/src/subtle.ts | 6 +++--- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c939581c..f72d7baa 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2811,7 +2811,7 @@ SPEC CHECKSUMS: MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 - QuickCrypto: 91cda93ba3146b0cb92039d1058fbbaec100edd1 + QuickCrypto: aad2c3dbe94e5eb4322d20e1bdccfa66464e2ae0 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/example/package.json b/example/package.json index 0cd02a06..52f42ef0 100644 --- a/example/package.json +++ b/example/package.json @@ -40,7 +40,7 @@ "react-native-mmkv": "4.0.1", "react-native-nitro-modules": "0.33.2", "react-native-quick-base64": "2.2.2", - "react-native-quick-crypto": "1.0.10", + "react-native-quick-crypto": "workspace:*", "react-native-safe-area-context": "5.6.2", "react-native-screens": "4.18.0", "react-native-vector-icons": "10.3.0", diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp index 05805024..5eadfeee 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -376,7 +376,7 @@ std::optional HybridCipher::getCipherInfo(const std::string& name, s } } - std::string name_str(cipher.getName()); + std::string name_str(name); std::transform(name_str.begin(), name_str.end(), name_str.begin(), ::tolower); std::string mode_str(cipher.getModeLabel()); diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp index e9f64abb..3af2472d 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp @@ -125,6 +125,25 @@ std::shared_ptr HybridKeyObjectHandle::exportKey(std::optional buf(len); + if (EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, buf.data(), len, nullptr) == 0) { + throw std::runtime_error("Failed to encode EC public key"); + } + return ToNativeArrayBuffer(std::string(reinterpret_cast(buf.data()), buf.size())); + } + // Set default format and type if not provided auto exportFormat = format.value_or(KFormatType::DER); auto exportType = type.value_or(keyType == KeyType::PUBLIC ? KeyEncoding::SPKI : KeyEncoding::PKCS8); diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 9c6e2fb6..9a775982 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -113,12 +113,12 @@ function getAlgorithmName(name: string, length: number): string { function ecExportKey(key: CryptoKey, format: KWebCryptoKeyFormat): ArrayBuffer { const keyObject = key.keyObject; - if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) { - // Export public key in SPKI format + if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw) { + return bufferLikeToArrayBuffer(keyObject.handle.exportKey()); + } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) { const exported = keyObject.export({ format: 'der', type: 'spki' }); return bufferLikeToArrayBuffer(exported); } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) { - // Export private key in PKCS8 format const exported = keyObject.export({ format: 'der', type: 'pkcs8' }); return bufferLikeToArrayBuffer(exported); } else { From 80636da98bce3498f5710fc397f75de4a21d70f6 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 13 Feb 2026 00:13:37 -0500 Subject: [PATCH 11/15] fix: address review findings across new modules - getCipherInfo: use canonical OpenSSL name via cipher.getName(), remove number from signature - Certificate: use safer empty ArrayBuffer pattern (non-null data pointer) - Argon2: include OpenSSL error string in failure messages - Prime: clean up callback signatures with proper union types - Tests: strengthen verifySpkac assertion to assert.isTrue --- .../tests/certificate/certificate_tests.ts | 4 +--- .../cpp/argon2/HybridArgon2.cpp | 5 ++++- .../cpp/certificate/HybridCertificate.cpp | 9 +++++--- .../cpp/cipher/HybridCipher.cpp | 2 +- .../react-native-quick-crypto/src/cipher.ts | 13 +++-------- .../react-native-quick-crypto/src/prime.ts | 22 +++++++++++-------- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/example/src/tests/certificate/certificate_tests.ts b/example/src/tests/certificate/certificate_tests.ts index 57ac7033..ea1d87e6 100644 --- a/example/src/tests/certificate/certificate_tests.ts +++ b/example/src/tests/certificate/certificate_tests.ts @@ -21,9 +21,7 @@ const invalidSpkac = 'not-a-valid-spkac'; test(SUITE, 'verifySpkac returns true for valid SPKAC', () => { const result = Certificate.verifySpkac(validSpkac); - assert.isBoolean(result); - // Note: result may be false if OpenSSL version doesn't support the specific key format - // The important thing is that it doesn't throw + assert.isTrue(result); }); test(SUITE, 'verifySpkac returns false for invalid SPKAC', () => { diff --git a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp index 1be643a0..1a8f3a33 100644 --- a/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp +++ b/packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -50,7 +51,9 @@ static std::shared_ptr hashImpl(const std::string& algorithm, const static_cast(passes), static_cast(version), secretBuf, adBuf, type); if (!result) { - throw std::runtime_error("Argon2 operation failed"); + unsigned long err = ERR_peek_last_error(); + const char* reason = err ? ERR_reason_error_string(err) : nullptr; + throw std::runtime_error(reason ? std::string("Argon2 operation failed: ") + reason : "Argon2 operation failed"); } return ToNativeArrayBuffer(reinterpret_cast(result.get()), result.size()); diff --git a/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp index 70cbcfd8..2cf78a53 100644 --- a/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp +++ b/packages/react-native-quick-crypto/cpp/certificate/HybridCertificate.cpp @@ -13,12 +13,14 @@ std::shared_ptr HybridCertificate::exportPublicKey(const std::share auto bio = ncrypto::ExportPublicKey(reinterpret_cast(spkac->data()), spkac->size()); if (!bio) { - return std::make_shared(nullptr, 0, nullptr); + auto empty = new uint8_t[0]; + return std::make_shared(empty, 0, [empty]() { delete[] empty; }); } BUF_MEM* mem = bio; if (!mem || mem->length == 0) { - return std::make_shared(nullptr, 0, nullptr); + auto empty = new uint8_t[0]; + return std::make_shared(empty, 0, [empty]() { delete[] empty; }); } return ToNativeArrayBuffer(reinterpret_cast(mem->data), mem->length); @@ -28,7 +30,8 @@ std::shared_ptr HybridCertificate::exportChallenge(const std::share auto buf = ncrypto::ExportChallenge(reinterpret_cast(spkac->data()), spkac->size()); if (buf.data == nullptr) { - return std::make_shared(nullptr, 0, nullptr); + auto empty = new uint8_t[0]; + return std::make_shared(empty, 0, [empty]() { delete[] empty; }); } auto result = ToNativeArrayBuffer(reinterpret_cast(buf.data), buf.len); diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp index 5eadfeee..05805024 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -376,7 +376,7 @@ std::optional HybridCipher::getCipherInfo(const std::string& name, s } } - std::string name_str(name); + std::string name_str(cipher.getName()); std::transform(name_str.begin(), name_str.end(), name_str.begin(), ::tolower); std::string mode_str(cipher.getModeLabel()); diff --git a/packages/react-native-quick-crypto/src/cipher.ts b/packages/react-native-quick-crypto/src/cipher.ts index d9a507d9..d896ea65 100644 --- a/packages/react-native-quick-crypto/src/cipher.ts +++ b/packages/react-native-quick-crypto/src/cipher.ts @@ -57,18 +57,11 @@ export function getCiphers(): string[] { } export function getCipherInfo( - nameOrNid: string | number, + name: string, options?: { keyLength?: number; ivLength?: number }, ): CipherInfoResult | undefined { - if (typeof nameOrNid === 'string') { - if (nameOrNid.length === 0) return undefined; - return CipherUtils.getCipherInfo( - nameOrNid, - options?.keyLength, - options?.ivLength, - ); - } - throw new TypeError('nameOrNid must be a string'); + if (typeof name !== 'string' || name.length === 0) return undefined; + return CipherUtils.getCipherInfo(name, options?.keyLength, options?.ivLength); } interface CipherArgs { diff --git a/packages/react-native-quick-crypto/src/prime.ts b/packages/react-native-quick-crypto/src/prime.ts index 9d8ec818..944d1399 100644 --- a/packages/react-native-quick-crypto/src/prime.ts +++ b/packages/react-native-quick-crypto/src/prime.ts @@ -66,16 +66,18 @@ export function generatePrimeSync( return result; } +type GeneratePrimeCallback = ( + err: Error | null, + prime: Buffer | bigint, +) => void; + export function generatePrime( size: number, - options?: GeneratePrimeOptions, - callback?: (err: Error | null, prime: Buffer | bigint) => void, + options: GeneratePrimeOptions | GeneratePrimeCallback, + callback?: GeneratePrimeCallback, ): void { if (typeof options === 'function') { - callback = options as unknown as ( - err: Error | null, - prime: Buffer | bigint, - ) => void; + callback = options; options = {}; } const safe = options?.safe ?? false; @@ -108,16 +110,18 @@ export function checkPrimeSync( return getNative().checkPrimeSync(buf, checks); } +type CheckPrimeCallback = (err: Error | null, result: boolean) => void; + export function checkPrime( candidate: BinaryLike | bigint, - options?: CheckPrimeOptions | ((err: Error | null, result: boolean) => void), - callback?: (err: Error | null, result: boolean) => void, + options: CheckPrimeOptions | CheckPrimeCallback, + callback?: CheckPrimeCallback, ): void { if (typeof options === 'function') { callback = options; options = {}; } - const checks = (options as CheckPrimeOptions)?.checks ?? 0; + const checks = options.checks ?? 0; const buf = typeof candidate === 'bigint' ? bigIntToBuffer(candidate) From fd72f210f28de574f0e6f85b4be0de2e91301956 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 13 Feb 2026 00:40:08 -0500 Subject: [PATCH 12/15] fix: argon2 test import Buffer from rnqc, fix prime test call signatures --- example/src/tests/argon2/argon2_tests.ts | 3 +-- example/src/tests/prime/prime_tests.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/example/src/tests/argon2/argon2_tests.ts b/example/src/tests/argon2/argon2_tests.ts index e71d990a..0f59a730 100644 --- a/example/src/tests/argon2/argon2_tests.ts +++ b/example/src/tests/argon2/argon2_tests.ts @@ -1,7 +1,6 @@ import { test } from '../util'; -import { argon2Sync, argon2 } from 'react-native-quick-crypto'; +import { argon2Sync, argon2, Buffer } from 'react-native-quick-crypto'; import { assert } from 'chai'; -import { Buffer } from '@craftzdog/react-native-buffer'; const SUITE = 'argon2'; diff --git a/example/src/tests/prime/prime_tests.ts b/example/src/tests/prime/prime_tests.ts index 8756dc9f..f673cf0f 100644 --- a/example/src/tests/prime/prime_tests.ts +++ b/example/src/tests/prime/prime_tests.ts @@ -53,7 +53,7 @@ test(SUITE, 'checkPrimeSync: verifies generated prime', () => { test(SUITE, 'generatePrime: async generates a prime', () => { return new Promise((resolve, reject) => { - generatePrime(64, undefined, (err, prime) => { + generatePrime(64, (err, prime) => { try { assert.isNull(err); assert.isOk(prime); @@ -69,7 +69,7 @@ test(SUITE, 'generatePrime: async generates a prime', () => { test(SUITE, 'checkPrime: async checks a prime', () => { return new Promise((resolve, reject) => { const prime = generatePrimeSync(64); - checkPrime(prime as Buffer, undefined, (err, result) => { + checkPrime(prime as Buffer, (err, result) => { try { assert.isNull(err); assert.isTrue(result); From 66971d84b0e8d9232876ad0e1a7ed82252f53f51 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 13 Feb 2026 00:50:34 -0500 Subject: [PATCH 13/15] fix: use valid SPKAC fixture in tests, update docs site coverage data --- docs/data/coverage.ts | 31 ++++++++++++------- .../tests/certificate/certificate_tests.ts | 30 ++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/docs/data/coverage.ts b/docs/data/coverage.ts index 7b5f58f1..1f0883ad 100644 --- a/docs/data/coverage.ts +++ b/docs/data/coverage.ts @@ -44,9 +44,9 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'Certificate', subItems: [ - { name: 'exportChallenge', status: 'missing' }, - { name: 'exportPublicKey', status: 'missing' }, - { name: 'verifySpkac', status: 'missing' }, + { name: 'exportChallenge', status: 'implemented' }, + { name: 'exportPublicKey', status: 'implemented' }, + { name: 'verifySpkac', status: 'implemented' }, ], }, { @@ -76,8 +76,15 @@ export const COVERAGE_DATA: CoverageCategory[] = [ }, { name: 'ECDH', - status: 'implemented', - note: 'Use simple ECDH methods instead', + subItems: [ + { name: 'convertKey', status: 'implemented', note: 'static' }, + { name: 'computeSecret', status: 'implemented' }, + { name: 'generateKeys', status: 'implemented' }, + { name: 'getPrivateKey', status: 'implemented' }, + { name: 'getPublicKey', status: 'implemented' }, + { name: 'setPrivateKey', status: 'implemented' }, + { name: 'setPublicKey', status: 'implemented' }, + ], }, { name: 'Hash', @@ -114,11 +121,11 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'asymmetricKeyType', status: 'implemented' }, { name: 'export', status: 'implemented' }, { name: 'type', status: 'implemented' }, - { name: 'asymmetricKeyDetails', status: 'missing' }, + { name: 'asymmetricKeyDetails', status: 'implemented' }, { name: 'equals', status: 'implemented' }, { name: 'symmetricKeySize', status: 'implemented' }, - { name: 'toCryptoKey', status: 'missing' }, - { name: 'from', status: 'missing', note: 'static' }, + { name: 'toCryptoKey', status: 'implemented' }, + { name: 'from', status: 'implemented', note: 'static' }, ], }, { @@ -130,8 +137,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { title: 'Crypto Methods', items: [ - { name: 'argon2', status: 'missing' }, - { name: 'checkPrime', status: 'missing' }, + { name: 'argon2', status: 'implemented' }, + { name: 'checkPrime', status: 'implemented' }, { name: 'constants', status: 'implemented' }, { name: 'createCipheriv', status: 'implemented' }, { name: 'createDecipheriv', status: 'implemented' }, @@ -201,8 +208,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'hmac', status: 'implemented' }, ], }, - { name: 'generatePrime', status: 'missing' }, - { name: 'getCipherInfo', status: 'missing' }, + { name: 'generatePrime', status: 'implemented' }, + { name: 'getCipherInfo', status: 'implemented' }, { name: 'getCiphers', status: 'implemented' }, { name: 'getCurves', status: 'implemented' }, { name: 'getDiffieHellman', status: 'implemented' }, diff --git a/example/src/tests/certificate/certificate_tests.ts b/example/src/tests/certificate/certificate_tests.ts index ea1d87e6..cf565f12 100644 --- a/example/src/tests/certificate/certificate_tests.ts +++ b/example/src/tests/certificate/certificate_tests.ts @@ -4,18 +4,22 @@ import { assert } from 'chai'; const SUITE = 'certificate'; -// Known valid SPKAC (Netscape Signed Public Key and Challenge) -// Generated with: openssl spkac -key test.pem -challenge test +// Node.js test fixture: 2048-bit RSA SPKAC with challenge "this-is-a-challenge" const validSpkac = - 'MIIBXjCByDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA3V' + - 'OalmRSaIBk2fVEKEECNBbOJMFCMHBOBYhBjqRLNeGq8GOWQ6qn' + - 'FJycJgbYxOWL/4y7FuyFdEiRm3lMiDl0FR2WzhqFDsT7LMfMaV' + - 'Bv39JMmPOfUoqHaEYAN2Bvw9bMT0DHXpcFVGkDHFnYPFvKfBxKx' + - 'mCYSiEkGrgK7yDiwl2kCAwEAARYEbm9uZTANBgkqhkiG9w0BAQQ' + - 'FAAOBgQAwxfKEBHCCfQ4UMsBd0zmrU+ISi2VHDhj9VKZea2Sy3p' + - 'A/wsjKQqZ4vX0LkbFezJR0RA+Nz1dm31GrKHloXYgqfUTfNOlBO' + - 'UQOd2mMa8c4qRMGBfY+GSZVY34TFNJrQrcSHTmkOy3Hm6dMR0X' + - 'qzRA/vGAZ0N0N2g+JFAFKYCBbQ=='; + 'MIICUzCCATswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC33FiI' + + 'iiexwLe/P8DZx5HsqFlmUO7/lvJ7necJVNwqdZ3ax5jpQB0p6uxfqeOvzcN3' + + 'k5V7UFb/Am+nkSNZMAZhsWzCU2Z4Pjh50QYz3f0Hour7/yIGStOLyYY3hgLK' + + '2K8TbhgjQPhdkw9+QtKlpvbL8fLgONAoGrVOFnRQGcr70iFffsm79mgZhKVM' + + 'gYiHPJqJgGHvCtkGg9zMgS7p63+Q3ZWedtFS2RhMX3uCBy/mH6EOlRCNBbRm' + + 'A4xxNzyf5GQaki3T+Iz9tOMjdPP+CwV2LqEdylmBuik8vrfTb3qIHLKKBAI8l' + + 'XN26wWtA3kN4L7NP+cbKlCRlqctvhmylLH1AgMBAAEWE3RoaXMtaXMtYS1jaG' + + 'FsbGVuZ2UwDQYJKoZIhvcNAQEEBQADggEBAIozmeW1kfDfAVwRQKileZGLRGCD' + + '7AjdHLYEe16xTBPve8Af1bDOyuWsAm4qQLYA4FAFROiKeGqxCtIErEvm87/09' + + 'tCfF1My/1Uj+INjAk39DK9J9alLlTsrwSgd1lb3YlXY7TyitCmh7iXLo4pVhA' + + '2chNA3njiMq3CUpSvGbpzrESL2dv97lv590gUD988wkTDVyYsf0T8+X0Kww3Ag' + + 'PWGji+2f2i5/jTfD/s1lK1nqi7ZxFm0pGZoy1MJ51SCEy7Y82ajroI+5786nC0' + + '2mo9ak7samca4YDZOoxN4d3tax4B/HDF5dqJSm1/31xYLDTfujCM5FkSjRc4m6' + + 'hnriEkc='; const invalidSpkac = 'not-a-valid-spkac'; @@ -47,10 +51,10 @@ test(SUITE, 'exportPublicKey returns empty buffer for invalid SPKAC', () => { assert.strictEqual(result.length, 0); }); -test(SUITE, 'exportChallenge returns Buffer', () => { +test(SUITE, 'exportChallenge returns correct challenge string', () => { const result = Certificate.exportChallenge(validSpkac); - assert.isOk(result); assert.isTrue(Buffer.isBuffer(result)); + assert.strictEqual(result.toString('utf8'), 'this-is-a-challenge'); }); test(SUITE, 'exportChallenge returns empty buffer for invalid SPKAC', () => { From bbb75be7c2d67a8512d38b978227b63f3b808f08 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 13 Feb 2026 01:08:02 -0500 Subject: [PATCH 14/15] ci: add --client-logs to metro start for test log visibility --- example/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/package.json b/example/package.json index 52f42ef0..19b4d18c 100644 --- a/example/package.json +++ b/example/package.json @@ -15,7 +15,7 @@ "lint:fix": "eslint \"src/**/*.{js,ts,tsx}\" --fix", "format": "prettier --check \"**/*.{js,ts,tsx}\"", "format:fix": "prettier --write \"**/*.{js,ts,tsx}\"", - "start": "react-native start", + "start": "react-native start --client-logs", "dev": "sh -c 'react-native start --client-logs \"$@\" 2>&1 | tee /tmp/rnqc-metro.log' --", "pods": "RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1 bundle install && bundle exec pod install --project-directory=ios", "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", From 6d4f883879a8f28f00bd3945fd6640ca50c539a0 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 13 Feb 2026 01:09:23 -0500 Subject: [PATCH 15/15] fix: correct expected canonical name for aes-128-gcm in getCipherInfo test --- example/src/tests/cipher/cipherinfo_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/tests/cipher/cipherinfo_tests.ts b/example/src/tests/cipher/cipherinfo_tests.ts index edf3192f..4b6dfa9a 100644 --- a/example/src/tests/cipher/cipherinfo_tests.ts +++ b/example/src/tests/cipher/cipherinfo_tests.ts @@ -18,7 +18,7 @@ test(SUITE, 'getCipherInfo: returns info for aes-256-cbc', () => { test(SUITE, 'getCipherInfo: returns info for aes-128-gcm', () => { const info = getCipherInfo('aes-128-gcm'); assert.isOk(info); - assert.strictEqual(info!.name, 'aes-128-gcm'); + assert.strictEqual(info!.name, 'id-aes128-gcm'); assert.strictEqual(info!.keyLength, 16); assert.strictEqual(info!.ivLength, 12); assert.strictEqual(info!.mode, 'gcm');