diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index e3ee74d5..99a05ae4 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -25,6 +25,7 @@ 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/raw_key_formats'; import '../tests/keys/sign_verify_error_queue'; import '../tests/keys/sign_verify_oneshot'; import '../tests/keys/sign_verify_streaming'; diff --git a/example/src/tests/keys/raw_key_formats.ts b/example/src/tests/keys/raw_key_formats.ts new file mode 100644 index 00000000..2461943c --- /dev/null +++ b/example/src/tests/keys/raw_key_formats.ts @@ -0,0 +1,186 @@ +import { + Buffer, + createPrivateKey, + createPublicKey, + generateKeyPair, + generateKeyPairSync, + PrivateKeyObject, +} from 'react-native-quick-crypto'; +import { expect } from 'chai'; +import { test } from '../util'; + +const SUITE = 'keys.rawFormats'; + +type GenerateRawResult = { + publicKey: ArrayBuffer | Buffer; + privateKey: ArrayBuffer | Buffer; +}; + +function generateKeyPairAsync( + type: 'ec' | 'ed25519' | 'ed448' | 'x25519' | 'x448', + options: object, +): Promise { + return new Promise((resolve, reject) => { + generateKeyPair(type, options, (err, publicKey, privateKey) => { + if (err) reject(err); + else + resolve({ + publicKey: publicKey as ArrayBuffer | Buffer, + privateKey: privateKey as ArrayBuffer | Buffer, + }); + }); + }); +} + +// --- KeyObject.export with raw formats --- + +test(SUITE, 'PublicKeyObject.export raw-public for X25519', async () => { + const { publicKey, privateKey } = generateKeyPairSync('x25519'); + const rawPub = publicKey.export({ format: 'raw-public' }); + const rawPriv = privateKey.export({ format: 'raw-private' }); + expect(rawPub).to.be.instanceOf(Buffer); + expect(rawPriv).to.be.instanceOf(Buffer); + expect(rawPub.length).to.equal(32); + expect(rawPriv.length).to.equal(32); +}); + +test(SUITE, 'PublicKeyObject.export raw-public for Ed25519', async () => { + const { publicKey, privateKey } = generateKeyPairSync('ed25519'); + const rawPub = publicKey.export({ format: 'raw-public' }); + const rawPriv = privateKey.export({ format: 'raw-private' }); + expect(rawPub.length).to.equal(32); + expect(rawPriv.length).to.equal(32); +}); + +test(SUITE, 'PublicKeyObject.export raw-public for EC P-256', async () => { + const { publicKey, privateKey } = generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); + const uncompressed = publicKey.export({ format: 'raw-public' }); + expect(uncompressed.length).to.equal(65); + expect(uncompressed[0]).to.equal(0x04); + + const compressed = publicKey.export({ + format: 'raw-public', + type: 'compressed', + }); + expect(compressed.length).to.equal(33); + expect(compressed[0] === 0x02 || compressed[0] === 0x03).to.equal(true); + + const rawPriv = privateKey.export({ format: 'raw-private' }); + expect(rawPriv.length).to.equal(32); +}); + +test(SUITE, 'PrivateKeyObject.export raw-seed for ML-DSA-44', async () => { + // ML-DSA may not be supported on all OpenSSL builds — guard. + let privateKey: PrivateKeyObject; + try { + ({ privateKey } = generateKeyPairSync('ml-dsa-44')); + } catch { + return; // skip if not supported + } + if (!(privateKey instanceof PrivateKeyObject)) return; + const seed = privateKey.export({ format: 'raw-seed' }); + expect(seed).to.be.instanceOf(Buffer); + expect(seed.length).to.be.greaterThan(0); +}); + +// --- createPublicKey / createPrivateKey with raw formats --- + +test(SUITE, 'createPublicKey raw-public for X25519', async () => { + const { publicKey } = await generateKeyPairAsync('x25519', { + publicKeyEncoding: { format: 'raw-public' }, + privateKeyEncoding: { format: 'raw-private' }, + }); + + const pub = createPublicKey({ + key: publicKey as Buffer, + format: 'raw-public', + asymmetricKeyType: 'x25519', + }); + expect(pub.type).to.equal('public'); + expect(pub.asymmetricKeyType).to.equal('x25519'); + + const reExported = pub.export({ format: 'raw-public' }); + expect(Buffer.compare(reExported, publicKey as Buffer)).to.equal(0); +}); + +test(SUITE, 'createPrivateKey raw-private for Ed25519', async () => { + const { privateKey } = await generateKeyPairAsync('ed25519', { + publicKeyEncoding: { format: 'raw-public' }, + privateKeyEncoding: { format: 'raw-private' }, + }); + + const priv = createPrivateKey({ + key: privateKey as Buffer, + format: 'raw-private', + asymmetricKeyType: 'ed25519', + }); + expect(priv.type).to.equal('private'); + expect(priv.asymmetricKeyType).to.equal('ed25519'); + + const reExported = priv.export({ format: 'raw-private' }); + expect(Buffer.compare(reExported, privateKey as Buffer)).to.equal(0); +}); + +test(SUITE, 'createPublicKey raw-public EC P-256 round-trip', async () => { + const { publicKey } = await generateKeyPairAsync('ec', { + namedCurve: 'P-256', + publicKeyEncoding: { format: 'raw-public' }, + privateKeyEncoding: { format: 'raw-private' }, + }); + + const pub = createPublicKey({ + key: publicKey as Buffer, + format: 'raw-public', + asymmetricKeyType: 'ec', + namedCurve: 'P-256', + }); + expect(pub.type).to.equal('public'); + expect(pub.asymmetricKeyType).to.equal('ec'); + + const reExported = pub.export({ format: 'raw-public' }); + expect(Buffer.compare(reExported, publicKey as Buffer)).to.equal(0); +}); + +test(SUITE, 'createPrivateKey raw-private EC P-256 round-trip', async () => { + const { privateKey } = await generateKeyPairAsync('ec', { + namedCurve: 'P-256', + publicKeyEncoding: { format: 'raw-public' }, + privateKeyEncoding: { format: 'raw-private' }, + }); + + const priv = createPrivateKey({ + key: privateKey as Buffer, + format: 'raw-private', + asymmetricKeyType: 'ec', + namedCurve: 'P-256', + }); + expect(priv.type).to.equal('private'); + expect(priv.asymmetricKeyType).to.equal('ec'); + + const reExported = priv.export({ format: 'raw-private' }); + expect(Buffer.compare(reExported, privateKey as Buffer)).to.equal(0); +}); + +// --- generateKeyPair raw output --- + +test(SUITE, 'generateKeyPairSync x25519 with raw-public output', () => { + const { publicKey, privateKey } = generateKeyPairSync('x25519', { + publicKeyEncoding: { format: 'raw-public' }, + privateKeyEncoding: { format: 'raw-private' }, + }); + expect(Buffer.from(publicKey as ArrayBuffer).length).to.equal(32); + expect(Buffer.from(privateKey as ArrayBuffer).length).to.equal(32); +}); + +test(SUITE, 'generateKeyPair ec compressed raw-public', async () => { + const { publicKey } = await generateKeyPairAsync('ec', { + namedCurve: 'P-256', + publicKeyEncoding: { format: 'raw-public', type: 'compressed' }, + privateKeyEncoding: { format: 'raw-private' }, + }); + const buf = Buffer.from(publicKey as ArrayBuffer); + expect(buf.length).to.equal(33); + expect(buf[0] === 0x02 || buf[0] === 0x03).to.equal(true); +}); diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp index f38b3739..6f4f8f75 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp @@ -431,6 +431,233 @@ JWK HybridKeyObjectHandle::exportJwk(const JWK& key, bool handleRsaPss) { throw std::runtime_error("Unsupported key type for JWK export"); } +// Returns true if the EVP_PKEY type supports raw public key export +// (CFRG keys: Ed25519, Ed448, X25519, X448; PQC keys: ML-DSA, ML-KEM, SLH-DSA). +static bool supportsRawPublic(int keyId, const char* typeName) { + if (keyId == EVP_PKEY_ED25519 || keyId == EVP_PKEY_ED448 || keyId == EVP_PKEY_X25519 || keyId == EVP_PKEY_X448) { + return true; + } +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + if (keyId == EVP_PKEY_ML_DSA_44 || keyId == EVP_PKEY_ML_DSA_65 || keyId == EVP_PKEY_ML_DSA_87) { + return true; + } + if (typeName != nullptr) { + std::string name(typeName); + if (name.starts_with("ML-KEM-") || name.starts_with("ML-DSA-") || name.starts_with("SLH-DSA-")) { + return true; + } + } +#else + (void)typeName; +#endif + return false; +} + +// Returns true if the EVP_PKEY type supports raw private key export +// (CFRG keys: Ed25519, Ed448, X25519, X448; SLH-DSA private keys). +static bool supportsRawPrivate(int keyId, const char* typeName) { + if (keyId == EVP_PKEY_ED25519 || keyId == EVP_PKEY_ED448 || keyId == EVP_PKEY_X25519 || keyId == EVP_PKEY_X448) { + return true; + } +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + if (typeName != nullptr) { + std::string name(typeName); + if (name.starts_with("SLH-DSA-")) { + return true; + } + } +#else + (void)typeName; +#endif + return false; +} + +// Returns true if the EVP_PKEY type supports raw seed export +// (PQC keys: ML-DSA, ML-KEM, SLH-DSA). +static bool supportsRawSeed(int keyId, const char* typeName) { +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + if (keyId == EVP_PKEY_ML_DSA_44 || keyId == EVP_PKEY_ML_DSA_65 || keyId == EVP_PKEY_ML_DSA_87) { + return true; + } + if (typeName != nullptr) { + std::string name(typeName); + if (name.starts_with("ML-KEM-") || name.starts_with("ML-DSA-") || name.starts_with("SLH-DSA-")) { + return true; + } + } +#else + (void)keyId; + (void)typeName; +#endif + return false; +} + +std::shared_ptr HybridKeyObjectHandle::exportRawPublic() { + auto keyType = data_.GetKeyType(); + if (keyType == KeyType::SECRET) { + throw std::runtime_error("Raw public key export is not supported for secret keys"); + } + + const auto& pkey = data_.GetAsymmetricKey(); + if (!pkey) { + throw std::runtime_error("Invalid asymmetric key"); + } + + int keyId = EVP_PKEY_id(pkey.get()); + const char* typeName = EVP_PKEY_get0_type_name(pkey.get()); + + if (!supportsRawPublic(keyId, typeName)) { + throw std::runtime_error("The key type does not support raw public key export"); + } + + auto rawData = pkey.rawPublicKey(); + if (!rawData) { + throw std::runtime_error("Failed to get raw public key"); + } + return ToNativeArrayBuffer(reinterpret_cast(rawData.get()), rawData.size()); +} + +std::shared_ptr HybridKeyObjectHandle::exportRawPrivate() { + auto keyType = data_.GetKeyType(); + if (keyType != KeyType::PRIVATE) { + throw std::runtime_error("Raw private key export requires a private key"); + } + + const auto& pkey = data_.GetAsymmetricKey(); + if (!pkey) { + throw std::runtime_error("Invalid asymmetric key"); + } + + int keyId = EVP_PKEY_id(pkey.get()); + const char* typeName = EVP_PKEY_get0_type_name(pkey.get()); + + if (!supportsRawPrivate(keyId, typeName)) { + throw std::runtime_error("The key type does not support raw private key export"); + } + + auto rawData = pkey.rawPrivateKey(); + if (!rawData) { + throw std::runtime_error("Failed to get raw private key"); + } + return ToNativeArrayBuffer(reinterpret_cast(rawData.get()), rawData.size()); +} + +std::shared_ptr HybridKeyObjectHandle::exportRawSeed() { +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + auto keyType = data_.GetKeyType(); + if (keyType != KeyType::PRIVATE) { + throw std::runtime_error("Raw seed export requires a private key"); + } + + const auto& pkey = data_.GetAsymmetricKey(); + if (!pkey) { + throw std::runtime_error("Invalid asymmetric key"); + } + + int keyId = EVP_PKEY_id(pkey.get()); + const char* typeName = EVP_PKEY_get0_type_name(pkey.get()); + + if (!supportsRawSeed(keyId, typeName)) { + throw std::runtime_error("The key type does not support raw seed export"); + } + + auto rawData = pkey.rawSeed(); + if (!rawData) { + throw std::runtime_error("Key does not have an available seed"); + } + return ToNativeArrayBuffer(reinterpret_cast(rawData.get()), rawData.size()); +#else + throw std::runtime_error("Raw seed export requires OpenSSL 3.5+"); +#endif +} + +std::shared_ptr HybridKeyObjectHandle::exportECPublicRaw(bool compressed) { + auto keyType = data_.GetKeyType(); + if (keyType == KeyType::SECRET) { + throw std::runtime_error("EC raw public key export is not supported for secret keys"); + } + + const auto& pkey = data_.GetAsymmetricKey(); + if (!pkey) { + throw std::runtime_error("Invalid asymmetric key"); + } + + if (EVP_PKEY_id(pkey.get()) != EVP_PKEY_EC) { + throw std::runtime_error("Key is not an EC key"); + } + + const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey.get()); + if (!ec_key) { + throw std::runtime_error("Failed to get EC key"); + } + + const EC_GROUP* group = EC_KEY_get0_group(ec_key); + const EC_POINT* point = EC_KEY_get0_public_key(ec_key); + if (!group || !point) { + throw std::runtime_error("Failed to get EC public key point"); + } + + point_conversion_form_t form = compressed ? POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED; + + size_t len = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (len == 0) { + throw std::runtime_error("Failed to compute EC point size"); + } + std::vector buf(len); + if (EC_POINT_point2oct(group, point, form, buf.data(), buf.size(), nullptr) != len) { + throw std::runtime_error("Failed to encode EC public key point"); + } + return ToNativeArrayBuffer(buf.data(), buf.size()); +} + +std::shared_ptr HybridKeyObjectHandle::exportECPrivateRaw() { + auto keyType = data_.GetKeyType(); + if (keyType != KeyType::PRIVATE) { + throw std::runtime_error("EC raw private key export requires a private key"); + } + + const auto& pkey = data_.GetAsymmetricKey(); + if (!pkey) { + throw std::runtime_error("Invalid asymmetric key"); + } + + if (EVP_PKEY_id(pkey.get()) != EVP_PKEY_EC) { + throw std::runtime_error("Key is not an EC key"); + } + + const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey.get()); + if (!ec_key) { + throw std::runtime_error("Failed to get EC key"); + } + + const BIGNUM* priv_bn = EC_KEY_get0_private_key(ec_key); + if (!priv_bn) { + throw std::runtime_error("EC key has no private component"); + } + + const EC_GROUP* group = EC_KEY_get0_group(ec_key); + if (!group) { + throw std::runtime_error("Failed to get EC group"); + } + + BIGNUM* order = BN_new(); + if (!order) { + throw std::runtime_error("Failed to allocate BIGNUM"); + } + if (EC_GROUP_get_order(group, order, nullptr) != 1) { + BN_free(order); + throw std::runtime_error("Failed to get EC group order"); + } + size_t order_size = (BN_num_bits(order) + 7) / 8; + BN_free(order); + + std::vector buf(order_size, 0); + if (BN_bn2binpad(priv_bn, buf.data(), static_cast(order_size)) < 0) { + throw std::runtime_error("Failed to encode EC private key"); + } + return ToNativeArrayBuffer(buf.data(), buf.size()); +} + AsymmetricKeyType HybridKeyObjectHandle::getAsymmetricKeyType() { const auto& pkey = data_.GetAsymmetricKey(); if (!pkey) { @@ -1061,6 +1288,193 @@ bool HybridKeyObjectHandle::initPqcRaw(const std::string& algorithmName, const s #endif } +// Map a string asymmetricKeyType to an EVP_PKEY NID for OKP/PQC keys. +// Returns 0 if the type is not a known OKP or PQC type. +static int evpNidForAsymmetricKeyType(const std::string& asymmetricKeyType) { + if (asymmetricKeyType == "ed25519") + return EVP_PKEY_ED25519; + if (asymmetricKeyType == "ed448") + return EVP_PKEY_ED448; + if (asymmetricKeyType == "x25519") + return EVP_PKEY_X25519; + if (asymmetricKeyType == "x448") + return EVP_PKEY_X448; +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + if (asymmetricKeyType == "ml-dsa-44") + return EVP_PKEY_ML_DSA_44; + if (asymmetricKeyType == "ml-dsa-65") + return EVP_PKEY_ML_DSA_65; + if (asymmetricKeyType == "ml-dsa-87") + return EVP_PKEY_ML_DSA_87; + if (asymmetricKeyType == "ml-kem-512") + return EVP_PKEY_ML_KEM_512; + if (asymmetricKeyType == "ml-kem-768") + return EVP_PKEY_ML_KEM_768; + if (asymmetricKeyType == "ml-kem-1024") + return EVP_PKEY_ML_KEM_1024; + if (asymmetricKeyType == "slh-dsa-sha2-128s") + return EVP_PKEY_SLH_DSA_SHA2_128S; + if (asymmetricKeyType == "slh-dsa-sha2-128f") + return EVP_PKEY_SLH_DSA_SHA2_128F; + if (asymmetricKeyType == "slh-dsa-sha2-192s") + return EVP_PKEY_SLH_DSA_SHA2_192S; + if (asymmetricKeyType == "slh-dsa-sha2-192f") + return EVP_PKEY_SLH_DSA_SHA2_192F; + if (asymmetricKeyType == "slh-dsa-sha2-256s") + return EVP_PKEY_SLH_DSA_SHA2_256S; + if (asymmetricKeyType == "slh-dsa-sha2-256f") + return EVP_PKEY_SLH_DSA_SHA2_256F; + if (asymmetricKeyType == "slh-dsa-shake-128s") + return EVP_PKEY_SLH_DSA_SHAKE_128S; + if (asymmetricKeyType == "slh-dsa-shake-128f") + return EVP_PKEY_SLH_DSA_SHAKE_128F; + if (asymmetricKeyType == "slh-dsa-shake-192s") + return EVP_PKEY_SLH_DSA_SHAKE_192S; + if (asymmetricKeyType == "slh-dsa-shake-192f") + return EVP_PKEY_SLH_DSA_SHAKE_192F; + if (asymmetricKeyType == "slh-dsa-shake-256s") + return EVP_PKEY_SLH_DSA_SHAKE_256S; + if (asymmetricKeyType == "slh-dsa-shake-256f") + return EVP_PKEY_SLH_DSA_SHAKE_256F; +#endif + return 0; +} + +bool HybridKeyObjectHandle::initRawPublic(const std::string& asymmetricKeyType, const std::shared_ptr& keyData, + const std::optional& namedCurve) { + data_ = KeyObjectData(); + + if (asymmetricKeyType == "ec") { + if (!namedCurve.has_value()) { + throw std::runtime_error("namedCurve is required for EC raw public key import"); + } + return initECRaw(namedCurve.value(), keyData); + } + + int nid = evpNidForAsymmetricKeyType(asymmetricKeyType); + if (nid == 0) { + throw std::runtime_error("Invalid asymmetricKeyType for raw public key import: " + asymmetricKeyType); + } + + ncrypto::Buffer buffer{.data = reinterpret_cast(keyData->data()), .len = keyData->size()}; + auto pkey = ncrypto::EVPKeyPointer::NewRawPublic(nid, buffer); + if (!pkey) { + throw std::runtime_error("Failed to create raw public key"); + } + this->data_ = KeyObjectData::CreateAsymmetric(KeyType::PUBLIC, std::move(pkey)); + return true; +} + +bool HybridKeyObjectHandle::initRawPrivate(const std::string& asymmetricKeyType, const std::shared_ptr& keyData, + const std::optional& namedCurve) { + data_ = KeyObjectData(); + + if (asymmetricKeyType == "ec") { + if (!namedCurve.has_value()) { + throw std::runtime_error("namedCurve is required for EC raw private key import"); + } + + int nid = 0; + const std::string& curve = namedCurve.value(); + if (curve == "prime256v1" || curve == "P-256") + nid = NID_X9_62_prime256v1; + else if (curve == "secp384r1" || curve == "P-384") + nid = NID_secp384r1; + else if (curve == "secp521r1" || curve == "P-521") + nid = NID_secp521r1; + else if (curve == "secp256k1") + nid = NID_secp256k1; + else + nid = OBJ_txt2nid(curve.c_str()); + + if (nid == 0) { + throw std::runtime_error("Unknown curve: " + curve); + } + + auto ec_key = std::unique_ptr(EC_KEY_new_by_curve_name(nid), EC_KEY_free); + if (!ec_key) { + throw std::runtime_error("Failed to create EC_KEY"); + } + const EC_GROUP* group = EC_KEY_get0_group(ec_key.get()); + + BIGNUM* order = BN_new(); + if (!order || EC_GROUP_get_order(group, order, nullptr) != 1) { + if (order) + BN_free(order); + throw std::runtime_error("Failed to get EC group order"); + } + size_t order_size = (BN_num_bits(order) + 7) / 8; + BN_free(order); + + if (keyData->size() != order_size) { + throw std::runtime_error("Invalid EC private key length"); + } + + BIGNUM* priv_bn = BN_bin2bn(reinterpret_cast(keyData->data()), static_cast(keyData->size()), nullptr); + if (!priv_bn) { + throw std::runtime_error("Failed to decode EC private key"); + } + + if (EC_KEY_set_private_key(ec_key.get(), priv_bn) != 1) { + BN_free(priv_bn); + throw std::runtime_error("Failed to set EC private key"); + } + + auto pub_point = std::unique_ptr(EC_POINT_new(group), EC_POINT_free); + if (!pub_point || EC_POINT_mul(group, pub_point.get(), priv_bn, nullptr, nullptr, nullptr) != 1 || + EC_KEY_set_public_key(ec_key.get(), pub_point.get()) != 1) { + BN_free(priv_bn); + throw std::runtime_error("Failed to derive EC public key"); + } + BN_free(priv_bn); + + EVP_PKEY* pkey = EVP_PKEY_new(); + if (!pkey || EVP_PKEY_assign_EC_KEY(pkey, ec_key.get()) != 1) { + if (pkey) + EVP_PKEY_free(pkey); + throw std::runtime_error("Failed to create EVP_PKEY from EC_KEY"); + } + ec_key.release(); + + this->data_ = KeyObjectData::CreateAsymmetric(KeyType::PRIVATE, ncrypto::EVPKeyPointer(pkey)); + return true; + } + + int nid = evpNidForAsymmetricKeyType(asymmetricKeyType); + if (nid == 0) { + throw std::runtime_error("Invalid asymmetricKeyType for raw private key import: " + asymmetricKeyType); + } + + ncrypto::Buffer buffer{.data = reinterpret_cast(keyData->data()), .len = keyData->size()}; + auto pkey = ncrypto::EVPKeyPointer::NewRawPrivate(nid, buffer); + if (!pkey) { + throw std::runtime_error("Failed to create raw private key"); + } + this->data_ = KeyObjectData::CreateAsymmetric(KeyType::PRIVATE, std::move(pkey)); + return true; +} + +bool HybridKeyObjectHandle::initRawSeed(const std::string& asymmetricKeyType, const std::shared_ptr& keyData) { +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + data_ = KeyObjectData(); + + int nid = evpNidForAsymmetricKeyType(asymmetricKeyType); + if (nid == 0) { + throw std::runtime_error("Invalid asymmetricKeyType for raw seed import: " + asymmetricKeyType); + } + + ncrypto::Buffer buffer{.data = reinterpret_cast(keyData->data()), .len = keyData->size()}; + auto pkey = ncrypto::EVPKeyPointer::NewRawSeed(nid, buffer); + if (!pkey) { + throw std::runtime_error("Failed to create key from raw seed"); + } + this->data_ = KeyObjectData::CreateAsymmetric(KeyType::PRIVATE, std::move(pkey)); + return true; +#else + throw std::runtime_error("Raw seed import requires OpenSSL 3.5+"); +#endif +} + bool HybridKeyObjectHandle::keyEquals(const std::shared_ptr& other) { auto otherHandle = std::dynamic_pointer_cast(other); if (!otherHandle) diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp index 03c08e80..47a4a02c 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp @@ -24,6 +24,16 @@ class HybridKeyObjectHandle : public HybridKeyObjectHandleSpec { JWK exportJwk(const JWK& key, bool handleRsaPss) override; + std::shared_ptr exportRawPublic() override; + + std::shared_ptr exportRawPrivate() override; + + std::shared_ptr exportRawSeed() override; + + std::shared_ptr exportECPublicRaw(bool compressed) override; + + std::shared_ptr exportECPrivateRaw() override; + AsymmetricKeyType getAsymmetricKeyType() override; bool init(KeyType keyType, const std::variant, std::string>& key, std::optional format, @@ -33,6 +43,14 @@ class HybridKeyObjectHandle : public HybridKeyObjectHandleSpec { bool initPqcRaw(const std::string& algorithmName, const std::shared_ptr& keyData, bool isPublic) override; + bool initRawPublic(const std::string& asymmetricKeyType, const std::shared_ptr& keyData, + const std::optional& namedCurve) override; + + bool initRawPrivate(const std::string& asymmetricKeyType, const std::shared_ptr& keyData, + const std::optional& namedCurve) override; + + bool initRawSeed(const std::string& asymmetricKeyType, const std::shared_ptr& keyData) override; + std::optional initJwk(const JWK& keyData, std::optional namedCurve) override; KeyDetail keyDetail() override; diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp index e64c2e6f..e9ac57f1 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp @@ -16,10 +16,18 @@ namespace margelo::nitro::crypto { registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridMethod("exportKey", &HybridKeyObjectHandleSpec::exportKey); prototype.registerHybridMethod("exportJwk", &HybridKeyObjectHandleSpec::exportJwk); + prototype.registerHybridMethod("exportRawPublic", &HybridKeyObjectHandleSpec::exportRawPublic); + prototype.registerHybridMethod("exportRawPrivate", &HybridKeyObjectHandleSpec::exportRawPrivate); + prototype.registerHybridMethod("exportRawSeed", &HybridKeyObjectHandleSpec::exportRawSeed); + prototype.registerHybridMethod("exportECPublicRaw", &HybridKeyObjectHandleSpec::exportECPublicRaw); + prototype.registerHybridMethod("exportECPrivateRaw", &HybridKeyObjectHandleSpec::exportECPrivateRaw); prototype.registerHybridMethod("getAsymmetricKeyType", &HybridKeyObjectHandleSpec::getAsymmetricKeyType); prototype.registerHybridMethod("init", &HybridKeyObjectHandleSpec::init); prototype.registerHybridMethod("initECRaw", &HybridKeyObjectHandleSpec::initECRaw); prototype.registerHybridMethod("initPqcRaw", &HybridKeyObjectHandleSpec::initPqcRaw); + prototype.registerHybridMethod("initRawPublic", &HybridKeyObjectHandleSpec::initRawPublic); + prototype.registerHybridMethod("initRawPrivate", &HybridKeyObjectHandleSpec::initRawPrivate); + prototype.registerHybridMethod("initRawSeed", &HybridKeyObjectHandleSpec::initRawSeed); prototype.registerHybridMethod("initJwk", &HybridKeyObjectHandleSpec::initJwk); prototype.registerHybridMethod("keyDetail", &HybridKeyObjectHandleSpec::keyDetail); prototype.registerHybridMethod("keyEquals", &HybridKeyObjectHandleSpec::keyEquals); diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp index 5bcb74c9..c22cfde2 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp @@ -77,10 +77,18 @@ namespace margelo::nitro::crypto { // Methods virtual std::shared_ptr exportKey(std::optional format, std::optional type, const std::optional& cipher, const std::optional>& passphrase) = 0; virtual JWK exportJwk(const JWK& key, bool handleRsaPss) = 0; + virtual std::shared_ptr exportRawPublic() = 0; + virtual std::shared_ptr exportRawPrivate() = 0; + virtual std::shared_ptr exportRawSeed() = 0; + virtual std::shared_ptr exportECPublicRaw(bool compressed) = 0; + virtual std::shared_ptr exportECPrivateRaw() = 0; virtual AsymmetricKeyType getAsymmetricKeyType() = 0; virtual bool init(KeyType keyType, const std::variant, std::string>& key, std::optional format, std::optional type, const std::optional>& passphrase) = 0; virtual bool initECRaw(const std::string& namedCurve, const std::shared_ptr& keyData) = 0; virtual bool initPqcRaw(const std::string& algorithmName, const std::shared_ptr& keyData, bool isPublic) = 0; + virtual bool initRawPublic(const std::string& asymmetricKeyType, const std::shared_ptr& keyData, const std::optional& namedCurve) = 0; + virtual bool initRawPrivate(const std::string& asymmetricKeyType, const std::shared_ptr& keyData, const std::optional& namedCurve) = 0; + virtual bool initRawSeed(const std::string& asymmetricKeyType, const std::shared_ptr& keyData) = 0; virtual std::optional initJwk(const JWK& keyData, std::optional namedCurve) = 0; virtual KeyDetail keyDetail() = 0; virtual bool keyEquals(const std::shared_ptr& other) = 0; diff --git a/packages/react-native-quick-crypto/src/dhKeyPair.ts b/packages/react-native-quick-crypto/src/dhKeyPair.ts index ff9f8f07..402b98a3 100644 --- a/packages/react-native-quick-crypto/src/dhKeyPair.ts +++ b/packages/react-native-quick-crypto/src/dhKeyPair.ts @@ -97,6 +97,14 @@ function dh_formatKeyPairOutput( let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + if ( + publicFormat === 'raw-public' || + privateFormat === 'raw-private' || + privateFormat === 'raw-seed' + ) { + throw new Error('Raw key formats are not supported for DH keys'); + } + if (publicFormat === -1) { publicKey = pub; } else { diff --git a/packages/react-native-quick-crypto/src/dsa.ts b/packages/react-native-quick-crypto/src/dsa.ts index e1b2f55d..2cf2d215 100644 --- a/packages/react-native-quick-crypto/src/dsa.ts +++ b/packages/react-native-quick-crypto/src/dsa.ts @@ -79,6 +79,14 @@ function dsa_formatKeyPairOutput( let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + if ( + publicFormat === 'raw-public' || + privateFormat === 'raw-private' || + privateFormat === 'raw-seed' + ) { + throw new Error('Raw key formats are not supported for DSA keys'); + } + if (publicFormat === -1) { publicKey = pub; } else { diff --git a/packages/react-native-quick-crypto/src/ec.ts b/packages/react-native-quick-crypto/src/ec.ts index 9bd7cf51..1d0dd5e1 100644 --- a/packages/react-native-quick-crypto/src/ec.ts +++ b/packages/react-native-quick-crypto/src/ec.ts @@ -450,6 +450,10 @@ function ec_formatKeyPairOutput( if (publicFormat === -1) { publicKey = pub; + } else if (publicFormat === 'raw-public') { + publicKey = Buffer.from( + pub.handle.exportECPublicRaw(publicType === 'compressed'), + ); } else { const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; @@ -465,6 +469,8 @@ function ec_formatKeyPairOutput( if (privateFormat === -1) { privateKey = priv; + } else if (privateFormat === 'raw-private') { + privateKey = Buffer.from(priv.handle.exportECPrivateRaw()); } else { const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; diff --git a/packages/react-native-quick-crypto/src/ed.ts b/packages/react-native-quick-crypto/src/ed.ts index 791e17dc..926db72f 100644 --- a/packages/react-native-quick-crypto/src/ed.ts +++ b/packages/react-native-quick-crypto/src/ed.ts @@ -8,6 +8,7 @@ import { PrivateKeyObject as PrivateKeyObjectClass, } from './keys/classes'; import type { EdKeyPair } from './specs/edKeyPair.nitro'; +import type { KeyObjectHandle as KeyObjectHandleSpec } from './specs/keyObjectHandle.nitro'; import type { BinaryLike, CFRGKeyPairType, @@ -31,6 +32,10 @@ import { } from './utils'; import { ECDH } from './ecdh'; +function coerceToNumeric(value: unknown): number { + return typeof value === 'number' ? value : -1; +} + export class Ed { type: CFRGKeyPairType; config: KeyPairGenConfig; @@ -86,10 +91,10 @@ export class Ed { async generateKeyPair(): Promise { await this.native.generateKeyPair( - this.config.publicFormat ?? -1, - this.config.publicType ?? -1, - this.config.privateFormat ?? -1, - this.config.privateType ?? -1, + coerceToNumeric(this.config.publicFormat), + coerceToNumeric(this.config.publicType), + coerceToNumeric(this.config.privateFormat), + coerceToNumeric(this.config.privateType), this.config.cipher, this.config.passphrase as ArrayBuffer, ); @@ -97,10 +102,10 @@ export class Ed { generateKeyPairSync(): void { this.native.generateKeyPairSync( - this.config.publicFormat ?? -1, - this.config.publicType ?? -1, - this.config.privateFormat ?? -1, - this.config.privateType ?? -1, + coerceToNumeric(this.config.publicFormat), + coerceToNumeric(this.config.publicType), + coerceToNumeric(this.config.privateFormat), + coerceToNumeric(this.config.privateType), this.config.cipher, this.config.passphrase as ArrayBuffer, ); @@ -166,18 +171,86 @@ export function diffieHellman( options: DiffieHellmanOptions, callback?: DiffieHellmanCallback, ): Buffer | void { - checkDiffieHellmanOptions(options); + if (!options || typeof options !== 'object') { + throw new TypeError('options must be an object'); + } + if (callback !== undefined && typeof callback !== 'function') { + throw new TypeError('callback must be a function'); + } - const privateKey = options.privateKey as PrivateKeyObject; + const resolvedOptions: DiffieHellmanOptions = { + publicKey: resolveDhKeyInput(options.publicKey, 'publicKey'), + privateKey: resolveDhKeyInput(options.privateKey, 'privateKey'), + }; + checkDiffieHellmanOptions(resolvedOptions); + + const privateKey = resolvedOptions.privateKey as PrivateKeyObject; const keyType = privateKey.asymmetricKeyType; if (keyType === 'ec') { - return ecDiffieHellman(options, callback); + return ecDiffieHellman(resolvedOptions, callback); + } + + if (keyType === 'dh') { + throw new Error( + 'crypto.diffieHellman with DH KeyObjects is not supported yet — use the DiffieHellman class for now', + ); } const type = keyType as CFRGKeyPairType; const ed = new Ed(type, {}); - return ed.diffieHellman(options, callback); + return ed.diffieHellman(resolvedOptions, callback); +} + +function isRawKeyInput(value: unknown): value is { + key: ArrayBuffer | ArrayBufferView | string; + format: 'raw-public' | 'raw-private' | 'raw-seed'; + asymmetricKeyType: string; + namedCurve?: string; +} { + if (!value || typeof value !== 'object') return false; + const obj = value as { format?: string }; + return ( + obj.format === 'raw-public' || + obj.format === 'raw-private' || + obj.format === 'raw-seed' + ); +} + +function resolveDhKeyInput( + input: DiffieHellmanOptions['publicKey'], + name: 'publicKey' | 'privateKey', +): AsymmetricKeyObject { + if (isRawKeyInput(input)) { + const expectedKeyType = name === 'publicKey' ? 'public' : 'private'; + if (input.format === 'raw-public' && expectedKeyType !== 'public') { + throw new Error(`Invalid format 'raw-public' for ${name}`); + } + if ( + (input.format === 'raw-private' || input.format === 'raw-seed') && + expectedKeyType !== 'private' + ) { + throw new Error(`Invalid format '${input.format}' for ${name}`); + } + if (input.asymmetricKeyType === 'ec' && !input.namedCurve) { + throw new Error(`namedCurve is required for EC raw key in ${name}`); + } + + const handle = + NitroModules.createHybridObject('KeyObjectHandle'); + const keyData = toAB(input.key as BinaryLike); + if (input.format === 'raw-public') { + handle.initRawPublic(input.asymmetricKeyType, keyData, input.namedCurve); + return new PublicKeyObject(handle); + } + if (input.format === 'raw-seed') { + handle.initRawSeed(input.asymmetricKeyType, keyData); + return new PrivateKeyObjectClass(handle); + } + handle.initRawPrivate(input.asymmetricKeyType, keyData, input.namedCurve); + return new PrivateKeyObjectClass(handle); + } + return input as AsymmetricKeyObject; } function ed_createKeyObjects(ed: Ed): { @@ -206,17 +279,19 @@ function ed_formatKeyPairOutput( ed: Ed, encoding: KeyPairGenConfig, ): { - publicKey: PublicKeyObject | string | ArrayBuffer; - privateKey: PrivateKeyObjectClass | string | ArrayBuffer; + publicKey: PublicKeyObject | string | ArrayBuffer | Buffer; + privateKey: PrivateKeyObjectClass | string | ArrayBuffer | Buffer; } { const { publicFormat, privateFormat, cipher, passphrase } = encoding; const { pub, priv } = ed_createKeyObjects(ed); - let publicKey: PublicKeyObject | string | ArrayBuffer; - let privateKey: PrivateKeyObjectClass | string | ArrayBuffer; + let publicKey: PublicKeyObject | string | ArrayBuffer | Buffer; + let privateKey: PrivateKeyObjectClass | string | ArrayBuffer | Buffer; if (publicFormat == null || publicFormat === -1) { publicKey = pub; + } else if (publicFormat === 'raw-public') { + publicKey = Buffer.from(pub.handle.exportRawPublic()); } else { const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; @@ -230,6 +305,10 @@ function ed_formatKeyPairOutput( if (privateFormat == null || privateFormat === -1) { privateKey = priv; + } else if (privateFormat === 'raw-private') { + privateKey = Buffer.from(priv.handle.exportRawPrivate()); + } else if (privateFormat === 'raw-seed') { + privateKey = Buffer.from(priv.handle.exportRawSeed()); } else { const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; diff --git a/packages/react-native-quick-crypto/src/keys/classes.ts b/packages/react-native-quick-crypto/src/keys/classes.ts index d7bc8dda..bbb1f953 100644 --- a/packages/react-native-quick-crypto/src/keys/classes.ts +++ b/packages/react-native-quick-crypto/src/keys/classes.ts @@ -1,4 +1,4 @@ -import { Buffer } from 'buffer'; +import { Buffer } from '@craftzdog/react-native-buffer'; import { NitroModules } from 'react-native-nitro-modules'; import type { AsymmetricKeyType, @@ -287,15 +287,36 @@ export class PublicKeyObject extends AsymmetricKeyObject { export(options: { format: 'pem' } & EncodingOptions): string; export(options: { format: 'der' } & EncodingOptions): Buffer; export(options: { format: 'jwk' } & EncodingOptions): JWK; + export(options: { format: 'raw-public' } & EncodingOptions): Buffer; export(options: EncodingOptions): string | Buffer | JWK { if (options?.format === 'jwk') { return this.handle.exportJwk({}, false); } + if (options?.format === 'raw-public') { + if (this.asymmetricKeyType === 'ec') { + const pointType = options.type ?? 'uncompressed'; + if (pointType !== 'compressed' && pointType !== 'uncompressed') { + throw new Error( + `Invalid options.type for raw-public EC export: ${pointType}`, + ); + } + return Buffer.from( + this.handle.exportECPublicRaw(pointType === 'compressed'), + ); + } + return Buffer.from(this.handle.exportRawPublic()); + } const { format, type } = parsePublicKeyEncoding( options, this.asymmetricKeyType, ); - const key = this.handle.exportKey(format, type); + if (typeof format !== 'number') { + throw new Error(`Unexpected format ${format} after raw-format dispatch`); + } + const key = this.handle.exportKey( + format, + typeof type === 'string' ? undefined : type, + ); const buffer = Buffer.from(key); if (options?.format === 'pem') { return buffer.toString('utf-8'); @@ -312,6 +333,8 @@ export class PrivateKeyObject extends AsymmetricKeyObject { export(options: { format: 'pem' } & EncodingOptions): string; export(options: { format: 'der' } & EncodingOptions): Buffer; export(options: { format: 'jwk' } & EncodingOptions): JWK; + export(options: { format: 'raw-private' } & EncodingOptions): Buffer; + export(options: { format: 'raw-seed' } & EncodingOptions): Buffer; export(options: EncodingOptions): string | Buffer | JWK { if (options?.format === 'jwk') { if (options.passphrase !== undefined) { @@ -319,11 +342,28 @@ export class PrivateKeyObject extends AsymmetricKeyObject { } return this.handle.exportJwk({}, false); } + if (options?.format === 'raw-private') { + if (this.asymmetricKeyType === 'ec') { + return Buffer.from(this.handle.exportECPrivateRaw()); + } + return Buffer.from(this.handle.exportRawPrivate()); + } + if (options?.format === 'raw-seed') { + return Buffer.from(this.handle.exportRawSeed()); + } const { format, type, cipher, passphrase } = parsePrivateKeyEncoding( options, this.asymmetricKeyType, ); - const key = this.handle.exportKey(format, type, cipher, passphrase); + if (typeof format !== 'number') { + throw new Error(`Unexpected format ${format} after raw-format dispatch`); + } + const key = this.handle.exportKey( + format, + typeof type === 'string' ? undefined : type, + cipher, + passphrase, + ); const buffer = Buffer.from(key); if (options?.format === 'pem') { return buffer.toString('utf-8'); diff --git a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts index 3c0320bb..41c31c01 100644 --- a/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts +++ b/packages/react-native-quick-crypto/src/keys/generateKeyPair.ts @@ -55,8 +55,8 @@ function slhDsaFormatKeyPairOutput( slhdsa: SlhDsa, encoding: KeyPairGenConfig, ): { - publicKey: PublicKeyObject | string | ArrayBuffer; - privateKey: PrivateKeyObject | string | ArrayBuffer; + publicKey: PublicKeyObject | string | ArrayBuffer | Buffer; + privateKey: PrivateKeyObject | string | ArrayBuffer | Buffer; } { const { publicFormat, privateFormat, cipher, passphrase } = encoding; @@ -73,11 +73,13 @@ function slhDsaFormatKeyPairOutput( KeyEncoding.PKCS8, ) as PrivateKeyObject; - let publicKeyOutput: PublicKeyObject | string | ArrayBuffer; - let privateKeyOutput: PrivateKeyObject | string | ArrayBuffer; + let publicKeyOutput: PublicKeyObject | string | ArrayBuffer | Buffer; + let privateKeyOutput: PrivateKeyObject | string | ArrayBuffer | Buffer; if (publicFormat === -1) { publicKeyOutput = publicKey; + } else if (publicFormat === 'raw-public') { + publicKeyOutput = Buffer.from(publicKey.handle.exportRawPublic()); } else { const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; @@ -91,6 +93,10 @@ function slhDsaFormatKeyPairOutput( if (privateFormat === -1) { privateKeyOutput = privateKey; + } else if (privateFormat === 'raw-private') { + privateKeyOutput = Buffer.from(privateKey.handle.exportRawPrivate()); + } else if (privateFormat === 'raw-seed') { + privateKeyOutput = Buffer.from(privateKey.handle.exportRawSeed()); } else { const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; @@ -116,8 +122,8 @@ function slhDsaGenerateKeyPairNodeSync( type: SlhDsaKeyPairType, encoding: KeyPairGenConfig, ): { - publicKey: PublicKeyObject | string | ArrayBuffer; - privateKey: PrivateKeyObject | string | ArrayBuffer; + publicKey: PublicKeyObject | string | ArrayBuffer | Buffer; + privateKey: PrivateKeyObject | string | ArrayBuffer | Buffer; } { const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); slhdsa.generateKeyPairSync(); @@ -128,8 +134,8 @@ async function slhDsaGenerateKeyPairNode( type: SlhDsaKeyPairType, encoding: KeyPairGenConfig, ): Promise<{ - publicKey: PublicKeyObject | string | ArrayBuffer; - privateKey: PrivateKeyObject | string | ArrayBuffer; + publicKey: PublicKeyObject | string | ArrayBuffer | Buffer; + privateKey: PrivateKeyObject | string | ArrayBuffer | Buffer; }> { const slhdsa = new SlhDsa(SLH_DSA_TYPE_TO_VARIANT[type]); await slhdsa.generateKeyPair(); @@ -163,7 +169,24 @@ export const generateKeyPairPromise = ( }; // generateKeyPairSync -export function generateKeyPairSync(type: KeyPairType): CryptoKeyPair; +export type KeyObjectKeyPair = { + publicKey: PublicKeyObject; + privateKey: PrivateKeyObject; +}; + +type KeyObjectGenerateKeyPairOptions = Omit< + GenerateKeyPairOptions, + 'publicKeyEncoding' | 'privateKeyEncoding' +> & { + publicKeyEncoding?: undefined; + privateKeyEncoding?: undefined; +}; + +export function generateKeyPairSync(type: KeyPairType): KeyObjectKeyPair; +export function generateKeyPairSync( + type: KeyPairType, + options: KeyObjectGenerateKeyPairOptions, +): KeyObjectKeyPair; export function generateKeyPairSync( type: KeyPairType, options: GenerateKeyPairOptions, @@ -233,10 +256,10 @@ function parseKeyPairEncoding( } return { - publicFormat, - publicType, - privateFormat, - privateType, + publicFormat: publicFormat as KeyPairGenConfig['publicFormat'], + publicType: publicType as KeyPairGenConfig['publicType'], + privateFormat: privateFormat as KeyPairGenConfig['privateFormat'], + privateType: privateType as KeyPairGenConfig['privateType'], cipher, passphrase, }; diff --git a/packages/react-native-quick-crypto/src/keys/index.ts b/packages/react-native-quick-crypto/src/keys/index.ts index 5e75c184..0d973ed6 100644 --- a/packages/react-native-quick-crypto/src/keys/index.ts +++ b/packages/react-native-quick-crypto/src/keys/index.ts @@ -40,14 +40,68 @@ import { randomBytes } from '../random'; interface KeyInputObject { key: BinaryLike | KeyObject | CryptoKey | JWK; - format?: 'pem' | 'der' | 'jwk'; + format?: 'pem' | 'der' | 'jwk' | 'raw-public' | 'raw-private' | 'raw-seed'; type?: 'pkcs1' | 'pkcs8' | 'spki' | 'sec1'; passphrase?: BinaryLike; encoding?: BufferEncoding; + asymmetricKeyType?: string; + namedCurve?: string; } type KeyInput = BinaryLike | KeyInputObject | KeyObject | CryptoKey; +function isRawFormat( + format: string | undefined, +): format is 'raw-public' | 'raw-private' | 'raw-seed' { + return ( + format === 'raw-public' || format === 'raw-private' || format === 'raw-seed' + ); +} + +function createPublicKeyFromRaw(input: KeyInputObject): PublicKeyObject { + if (input.format !== 'raw-public') { + throw new Error('Invalid format for createPublicKey raw import'); + } + if (typeof input.asymmetricKeyType !== 'string') { + throw new Error('options.asymmetricKeyType is required for raw key import'); + } + if (input.asymmetricKeyType === 'ec' && !input.namedCurve) { + throw new Error('options.namedCurve is required for EC raw key import'); + } + const handle = + NitroModules.createHybridObject('KeyObjectHandle'); + handle.initRawPublic( + input.asymmetricKeyType, + toAB(input.key as BinaryLike), + input.namedCurve, + ); + return new PublicKeyObject(handle); +} + +function createPrivateKeyFromRaw(input: KeyInputObject): PrivateKeyObject { + if (input.format !== 'raw-private' && input.format !== 'raw-seed') { + throw new Error('Invalid format for createPrivateKey raw import'); + } + if (typeof input.asymmetricKeyType !== 'string') { + throw new Error('options.asymmetricKeyType is required for raw key import'); + } + if (input.asymmetricKeyType === 'ec' && !input.namedCurve) { + throw new Error('options.namedCurve is required for EC raw key import'); + } + const handle = + NitroModules.createHybridObject('KeyObjectHandle'); + if (input.format === 'raw-seed') { + handle.initRawSeed(input.asymmetricKeyType, toAB(input.key as BinaryLike)); + } else { + handle.initRawPrivate( + input.asymmetricKeyType, + toAB(input.key as BinaryLike), + input.namedCurve, + ); + } + return new PrivateKeyObject(handle); +} + function createSecretKey(key: BinaryLike): SecretKeyObject { const keyBuffer = toAB(key); return KeyObject.createKeyObject('secret', keyBuffer) as SecretKeyObject; @@ -116,8 +170,10 @@ function prepareAsymmetricKey( return { data: toAB(data), format: 'pem', type }; } - // Filter out 'jwk' format - only 'pem' and 'der' are supported here - const filteredFormat = format === 'jwk' ? undefined : format; + // Filter to only 'pem' or 'der' — JWK and raw formats are handled + // separately via dedicated paths. + const filteredFormat: 'pem' | 'der' | undefined = + format === 'pem' || format === 'der' ? format : undefined; return { data: toAB(data), format: filteredFormat, type }; } @@ -125,6 +181,14 @@ function prepareAsymmetricKey( } function createPublicKey(key: KeyInput): PublicKeyObject { + if (typeof key === 'object' && 'key' in key && isRawFormat(key.format)) { + if (key.format !== 'raw-public') { + throw new Error( + `Invalid format ${key.format} for createPublicKey — only 'raw-public' is allowed`, + ); + } + return createPublicKeyFromRaw(key as KeyInputObject); + } if (typeof key === 'object' && 'key' in key && key.format === 'jwk') { const handle = NitroModules.createHybridObject('KeyObjectHandle'); @@ -169,6 +233,12 @@ function createPublicKey(key: KeyInput): PublicKeyObject { } function createPrivateKey(key: KeyInput): PrivateKeyObject { + if (typeof key === 'object' && 'key' in key && isRawFormat(key.format)) { + if (key.format === 'raw-public') { + throw new Error("Invalid format 'raw-public' for createPrivateKey"); + } + return createPrivateKeyFromRaw(key as KeyInputObject); + } if (typeof key === 'object' && 'key' in key && key.format === 'jwk') { const handle = NitroModules.createHybridObject('KeyObjectHandle'); diff --git a/packages/react-native-quick-crypto/src/keys/utils.ts b/packages/react-native-quick-crypto/src/keys/utils.ts index 0a3558ce..f32e4977 100644 --- a/packages/react-native-quick-crypto/src/keys/utils.ts +++ b/packages/react-native-quick-crypto/src/keys/utils.ts @@ -5,13 +5,36 @@ import { KFormatType, } from '../utils'; import type { CryptoKeyPair, EncodingOptions } from '../utils'; -import type { CryptoKey } from './classes'; +import type { CryptoKey, PublicKeyObject, PrivateKeyObject } from './classes'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isCryptoKey = (obj: any): boolean => { return obj !== null && obj?.keyObject !== undefined; }; +export function exportPublicKeyRaw( + pub: PublicKeyObject, + pointType: 'compressed' | 'uncompressed' | undefined, +): ArrayBuffer { + if (pub.asymmetricKeyType === 'ec') { + return pub.handle.exportECPublicRaw(pointType === 'compressed'); + } + return pub.handle.exportRawPublic(); +} + +export function exportPrivateKeyRaw( + priv: PrivateKeyObject, + format: 'raw-private' | 'raw-seed', +): ArrayBuffer { + if (format === 'raw-seed') { + return priv.handle.exportRawSeed(); + } + if (priv.asymmetricKeyType === 'ec') { + return priv.handle.exportECPrivateRaw(); + } + return priv.handle.exportRawPrivate(); +} + export function getCryptoKeyPair( key: CryptoKey | CryptoKeyPair, ): CryptoKeyPair { @@ -62,6 +85,19 @@ export function parseKeyEncoding( objName, ); + if ( + format === 'raw-public' || + format === 'raw-private' || + format === 'raw-seed' + ) { + if (enc.cipher != null || enc.passphrase !== undefined) { + throw new Error( + `Incompatible key options: ${format} does not support encryption`, + ); + } + return { format, type, cipher: undefined, passphrase: undefined }; + } + let cipher, passphrase, encoding; if (isPublic !== true) { ({ cipher, passphrase, encoding } = enc); @@ -77,7 +113,7 @@ export function parseKeyEncoding( (type === KeyEncoding.PKCS1 || type === KeyEncoding.SEC1) ) { throw new Error( - `Incompatible key options ${encodingNames[type]} does not support encryption`, + `Incompatible key options ${encodingNames[type as KeyEncoding]} does not support encryption`, ); } } else if (passphrase !== undefined) { @@ -120,12 +156,15 @@ function parseKeyFormat( formatStr?: string, defaultFormat?: KFormatType, optionName?: string, -) { +): KFormatType | 'raw-public' | 'raw-private' | 'raw-seed' { if (formatStr === undefined && defaultFormat !== undefined) return defaultFormat; else if (formatStr === 'pem') return KFormatType.PEM; else if (formatStr === 'der') return KFormatType.DER; else if (formatStr === 'jwk') return KFormatType.JWK; + else if (formatStr === 'raw-public') return 'raw-public'; + else if (formatStr === 'raw-private') return 'raw-private'; + else if (formatStr === 'raw-seed') return 'raw-seed'; throw new Error(`Invalid key format str: ${optionName}`); } @@ -166,7 +205,10 @@ function parseKeyFormatAndType( keyType?: string, isPublic?: boolean, objName?: string, -) { +): { + format: KFormatType | 'raw-public' | 'raw-private' | 'raw-seed'; + type: KeyEncoding | 'compressed' | 'uncompressed' | undefined; +} { const { format: formatStr, type: typeStr } = enc; const isInput = keyType === undefined; @@ -176,11 +218,42 @@ function parseKeyFormatAndType( option('format', objName), ); + if (format === 'raw-public') { + if (isPublic === false) { + throw new Error( + `Invalid format 'raw-public' for ${option('format', objName)}`, + ); + } + if (typeStr === undefined || typeStr === 'uncompressed') { + return { format, type: 'uncompressed' }; + } + if (typeStr === 'compressed') { + return { format, type: 'compressed' }; + } + throw new Error( + `Invalid ${option('type', objName)} for raw-public: ${typeStr}`, + ); + } + + if (format === 'raw-private' || format === 'raw-seed') { + if (isPublic === true) { + throw new Error( + `Invalid format '${format}' for ${option('format', objName)}`, + ); + } + if (typeStr !== undefined) { + throw new Error( + `Invalid ${option('type', objName)} for ${format}: ${typeStr}`, + ); + } + return { format, type: undefined }; + } + const isRequired = (!isInput || format === KFormatType.DER) && format !== KFormatType.JWK; const type = parseKeyType( - typeStr, + typeStr as string | undefined, isRequired, keyType, isPublic, diff --git a/packages/react-native-quick-crypto/src/rsa.ts b/packages/react-native-quick-crypto/src/rsa.ts index db79bce7..f5852bfb 100644 --- a/packages/react-native-quick-crypto/src/rsa.ts +++ b/packages/react-native-quick-crypto/src/rsa.ts @@ -260,6 +260,14 @@ function rsa_formatKeyPairOutput( let publicKey: PublicKeyObject | Buffer | string | ArrayBuffer; let privateKey: PrivateKeyObject | Buffer | string | ArrayBuffer; + if ( + publicFormat === 'raw-public' || + privateFormat === 'raw-private' || + privateFormat === 'raw-seed' + ) { + throw new Error('Raw key formats are not supported for RSA keys'); + } + if (publicFormat === -1) { publicKey = pub; } else { diff --git a/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts b/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts index 8bf018a0..b964716c 100644 --- a/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/keyObjectHandle.nitro.ts @@ -18,6 +18,11 @@ export interface KeyObjectHandle passphrase?: ArrayBuffer, ): ArrayBuffer; exportJwk(key: JWK, handleRsaPss: boolean): JWK; + exportRawPublic(): ArrayBuffer; + exportRawPrivate(): ArrayBuffer; + exportRawSeed(): ArrayBuffer; + exportECPublicRaw(compressed: boolean): ArrayBuffer; + exportECPrivateRaw(): ArrayBuffer; getAsymmetricKeyType(): AsymmetricKeyType; init( keyType: KeyType, @@ -32,6 +37,17 @@ export interface KeyObjectHandle keyData: ArrayBuffer, isPublic: boolean, ): boolean; + initRawPublic( + asymmetricKeyType: string, + keyData: ArrayBuffer, + namedCurve?: string, + ): boolean; + initRawPrivate( + asymmetricKeyType: string, + keyData: ArrayBuffer, + namedCurve?: string, + ): boolean; + initRawSeed(asymmetricKeyType: string, keyData: ArrayBuffer): boolean; initJwk(keyData: JWK, namedCurve?: NamedCurve): KeyType | undefined; keyDetail(): KeyDetail; keyEquals(other: KeyObjectHandle): boolean; diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index 8ef6bfa8..1317ed1b 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -295,7 +295,7 @@ export type KeyPairType = | ECKeyPairType | DSAKeyPairType | DHKeyPairType - | SlhDsaKeyPairType; + | PQCKeyPairType; export type KeyUsage = | 'encrypt' @@ -347,10 +347,13 @@ export const kNamedCurveAliases = { } as const; // end TODO +export type RawPublicFormat = 'raw-public'; +export type RawPrivateFormat = 'raw-private' | 'raw-seed'; + export type KeyPairGenConfig = { - publicFormat?: KFormatType | -1; - publicType?: KeyEncoding; - privateFormat?: KFormatType | -1; + publicFormat?: KFormatType | RawPublicFormat | -1; + publicType?: KeyEncoding | RawECPointType; + privateFormat?: KFormatType | RawPrivateFormat | -1; privateType?: KeyEncoding; cipher?: string; passphrase?: ArrayBuffer; @@ -399,14 +402,22 @@ export type KTypePrivate = 'pkcs1' | 'pkcs8' | 'sec1'; export type KTypePublic = 'pkcs1' | 'spki'; export type KType = KTypePrivate | KTypePublic; -export type KFormat = 'der' | 'pem' | 'jwk'; +export type KFormat = + | 'der' + | 'pem' + | 'jwk' + | 'raw-public' + | 'raw-private' + | 'raw-seed'; export type DSAEncoding = 'der' | 'ieee-p1363'; +export type RawECPointType = 'compressed' | 'uncompressed'; + export type EncodingOptions = { // eslint-disable-next-line @typescript-eslint/no-explicit-any key?: any; - type?: KType; + type?: KType | RawECPointType; encoding?: string; dsaEncoding?: DSAEncoding; format?: KFormat; @@ -416,6 +427,8 @@ export type EncodingOptions = { saltLength?: number; oaepHash?: string; oaepLabel?: BinaryLike; + asymmetricKeyType?: string; + namedCurve?: string; }; export interface KeyDetail { @@ -450,6 +463,7 @@ export type GenerateKeyPairOptions = { export type KeyPairKey = | ArrayBuffer | Buffer + | CraftzdogBuffer | string | KeyObject | KeyObjectHandle @@ -537,9 +551,16 @@ export type CipherOFBType = 'aes-128-ofb' | 'aes-192-ofb' | 'aes-256-ofb'; export type KeyObjectHandle = KeyObjectHandleType; +export type RawDiffieHellmanKeyInput = { + key: ArrayBuffer | ArrayBufferView | string; + format: 'raw-public' | 'raw-private' | 'raw-seed'; + asymmetricKeyType: string; + namedCurve?: string; +}; + export type DiffieHellmanOptions = { - privateKey: KeyObject; - publicKey: KeyObject; + privateKey: KeyObject | RawDiffieHellmanKeyInput; + publicKey: KeyObject | RawDiffieHellmanKeyInput; }; export type DiffieHellmanCallback = (