From 47b1d48a3cfdee8d321afe434de493628cfe049d Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sun, 26 Apr 2026 23:17:42 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(security):=20Phase=202=20audit=20memor?= =?UTF-8?q?y-safety=20sweep=20=E2=80=94=20RAII=20for=20buffers,=20EVP=5FPK?= =?UTF-8?q?EY*,=20EVP=5FPKEY=5FCTX,=20EVP=5FMD=5FCTX;=20shared=5Ffrom=5Fth?= =?UTF-8?q?is=20in=20PQ=20async?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes Phase 2 of plans/todo/security-audit.md across 24 C++ files (+327/-480 net). All five Phase 2 items shipped together: 2.1 Raw `new uint8_t[]` → `std::unique_ptr` + `release()` into `NativeArrayBuffer` in Hash, HMAC, KMAC, BLAKE3, PBKDF2, Scrypt, HKDF, the cipher base `update()`, ChaCha20/ChaCha20-Poly1305/XChaCha20-Poly1305/ XSalsa20-Poly1305, CCM `final()`, RSA-cipher decrypt sentinels, Ed25519 (6 sites), ML-DSA (3 sites), and ML-KEM (4 sites). 2.2 Raw `EVP_PKEY*` → `std::unique_ptr` in RSA, EC, and Ed25519 keypairs (DSA pattern as template). `Ed25519::importPublicKey`/`importPrivateKey` now return owning `EVP_PKEY_ptr` and use `EVP_PKEY_up_ref` for the borrow-the-instance-key path, closing the audit-flagged leak at HybridEdKeyPair.cpp:155,221. 2.3 `Promise<…>::async([this, …])` → `auto self = this->shared_cast<…>(); [self, …]` in ML-DSA (3 sites) and ML-KEM (3 sites). Closes the use-after-free risk if the JS object is GC'd while async is in flight. 2.4 Eliminate the unnecessary `EVP_PKEY_CTX_new_from_name` pre-creation in Sign/Verify handles, ML-DSA, and Ed25519 — pass `nullptr` for the `EVP_PKEY_CTX**` arg and let `EVP_DigestSignInit`/`EVP_DigestVerifyInit` allocate from the key's keymgmt (matches `ncrypto::EVPMDCtxPointer:: signInit`). Crypto-specialist review confirmed the old code was actually *leaking* the pre-allocated PKEY_CTX (OpenSSL silently overwrote the pointer on success), so this fix closes both the audited double-free *and* an unreported leak. Wraps EVP_MD_CTX/EVP_PKEY_CTX in local `unique_ptr` aliases so all manual error-path frees collapse. 2.5 Replace Ed25519's two `ERR_error_string(ERR_get_error(), NULL)` calls (thread-unsafe static buffer) with the shared `getOpenSSLError()` helper. Defense-in-depth: `secureZero` added on Scrypt/HKDF error paths and on Ed25519/ML-DSA/ML-KEM `getPrivateKey` BIO buffers. Crypto-specialist approved all four substantive concerns: algorithm selection unchanged (verified against OpenSSL 3.x m_sigver.c::do_sigver_init), refcount semantics correct, BIO secure-zero is safe redundancy with `BUF_MEM_free`'s `OPENSSL_clear_free`, `release()` + `make_shared` window matches Nitro's own `ArrayBuffer::wrap`. --- .../cpp/blake3/HybridBlake3.cpp | 8 +- .../cpp/cipher/CCMCipher.cpp | 5 +- .../cpp/cipher/ChaCha20Cipher.cpp | 13 +- .../cpp/cipher/ChaCha20Poly1305Cipher.cpp | 16 +- .../cpp/cipher/HybridCipher.cpp | 8 +- .../cpp/cipher/HybridRsaCipher.cpp | 10 +- .../cpp/cipher/XChaCha20Poly1305Cipher.cpp | 27 +-- .../cpp/cipher/XSalsa20Poly1305Cipher.cpp | 27 +-- .../cpp/ec/HybridEcKeyPair.cpp | 46 ++--- .../cpp/ec/HybridEcKeyPair.hpp | 10 +- .../cpp/ed25519/HybridEdKeyPair.cpp | 190 +++++++----------- .../cpp/ed25519/HybridEdKeyPair.hpp | 14 +- .../cpp/hash/HybridHash.cpp | 12 +- .../cpp/hkdf/HybridHkdf.cpp | 10 +- .../cpp/hmac/HybridHmac.cpp | 10 +- .../cpp/kmac/HybridKmac.cpp | 8 +- .../cpp/mldsa/HybridMlDsaKeyPair.cpp | 86 ++++---- .../cpp/mlkem/HybridMlKemKeyPair.cpp | 82 ++++---- .../cpp/pbkdf2/HybridPbkdf2.cpp | 15 +- .../cpp/rsa/HybridRsaKeyPair.cpp | 13 +- .../cpp/rsa/HybridRsaKeyPair.hpp | 11 +- .../cpp/scrypt/HybridScrypt.cpp | 10 +- .../cpp/sign/HybridSignHandle.cpp | 93 +++------ .../cpp/sign/HybridVerifyHandle.cpp | 83 +++----- plans/todo/security-audit.md | 20 +- 25 files changed, 338 insertions(+), 489 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/blake3/HybridBlake3.cpp b/packages/react-native-quick-crypto/cpp/blake3/HybridBlake3.cpp index 1379ebe0..176a533f 100644 --- a/packages/react-native-quick-crypto/cpp/blake3/HybridBlake3.cpp +++ b/packages/react-native-quick-crypto/cpp/blake3/HybridBlake3.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "QuickCryptoUtils.hpp" @@ -67,10 +68,11 @@ std::shared_ptr HybridBlake3::digest(std::optional length) outLen = static_cast(len); } - auto output = new uint8_t[outLen]; - blake3_hasher_finalize(&hasher, output, outLen); + auto output = std::make_unique(outLen); + blake3_hasher_finalize(&hasher, output.get(), outLen); - return std::make_shared(output, outLen, [=]() { delete[] output; }); + uint8_t* raw_ptr = output.get(); + return std::make_shared(output.release(), outLen, [raw_ptr]() { delete[] raw_ptr; }); } void HybridBlake3::reset() { diff --git a/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp index ec54cbbf..7adde891 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp @@ -110,8 +110,9 @@ std::shared_ptr CCMCipher::final() { // CCM decryption does not use final. Verification happens in the last update call. if (!is_cipher) { is_finalized = true; - unsigned char* empty_output = new unsigned char[0]; - return std::make_shared(empty_output, 0, [=]() { delete[] empty_output; }); + auto empty_output = std::make_unique(0); + unsigned char* raw_ptr = empty_output.get(); + return std::make_shared(empty_output.release(), 0, [raw_ptr]() { delete[] raw_ptr; }); } // Proceed only for encryption diff --git a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp index fc3b04ac..e7a4b24a 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp @@ -68,11 +68,10 @@ std::shared_ptr ChaCha20Cipher::update(const std::shared_ptr(out_len); // Perform the cipher update operation - if (EVP_CipherUpdate(ctx.get(), out, &out_len, native_data->data(), in_len) != 1) { - delete[] out; + if (EVP_CipherUpdate(ctx.get(), out_buf.get(), &out_len, native_data->data(), in_len) != 1) { unsigned long err = ERR_get_error(); char err_buf[256]; ERR_error_string_n(err, err_buf, sizeof(err_buf)); @@ -80,15 +79,17 @@ std::shared_ptr ChaCha20Cipher::update(const std::shared_ptr(out, out_len, [=]() { delete[] out; }); + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), out_len, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr ChaCha20Cipher::final() { checkCtx(); checkNotFinalized(); is_finalized = true; - unsigned char* empty_output = new unsigned char[0]; - return std::make_shared(empty_output, 0, [=]() { delete[] empty_output; }); + auto empty_buf = std::make_unique(0); + unsigned char* raw_ptr = empty_buf.get(); + return std::make_shared(empty_buf.release(), 0, [raw_ptr]() { delete[] raw_ptr; }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp index ea24ba4e..c23615de 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp @@ -69,11 +69,10 @@ std::shared_ptr ChaCha20Poly1305Cipher::update(const std::shared_pt // For ChaCha20-Poly1305, output size equals input size since it's a stream cipher int out_len = in_len; - uint8_t* out = new uint8_t[out_len]; + auto out_buf = std::make_unique(out_len); // Perform the cipher update operation - if (EVP_CipherUpdate(ctx.get(), out, &out_len, native_data->data(), in_len) != 1) { - delete[] out; + if (EVP_CipherUpdate(ctx.get(), out_buf.get(), &out_len, native_data->data(), in_len) != 1) { unsigned long err = ERR_get_error(); char err_buf[256]; ERR_error_string_n(err, err_buf, sizeof(err_buf)); @@ -81,7 +80,8 @@ std::shared_ptr ChaCha20Poly1305Cipher::update(const std::shared_pt } // Create and return a new buffer of exact size needed - return std::make_shared(out, out_len, [=]() { delete[] out; }); + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), out_len, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr ChaCha20Poly1305Cipher::final() { @@ -90,10 +90,9 @@ std::shared_ptr ChaCha20Poly1305Cipher::final() { // For ChaCha20-Poly1305, we need to call final to generate the tag int out_len = 0; - unsigned char* out = new unsigned char[0]; + auto out_buf = std::make_unique(0); - if (EVP_CipherFinal_ex(ctx.get(), out, &out_len) != 1) { - delete[] out; + if (EVP_CipherFinal_ex(ctx.get(), out_buf.get(), &out_len) != 1) { unsigned long err = ERR_get_error(); char err_buf[256]; ERR_error_string_n(err, err_buf, sizeof(err_buf)); @@ -101,7 +100,8 @@ std::shared_ptr ChaCha20Poly1305Cipher::final() { } is_finalized = true; - return std::make_shared(out, out_len, [=]() { delete[] out; }); + unsigned char* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), out_len, [raw_ptr]() { delete[] raw_ptr; }); } bool ChaCha20Poly1305Cipher::setAAD(const std::shared_ptr& data, std::optional plaintextLength) { diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp index 4d966c3e..fabda609 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridCipher.cpp @@ -106,21 +106,21 @@ std::shared_ptr HybridCipher::update(const std::shared_ptr(out_len); // Perform the cipher update operation. The real size of the output is // returned in out_len - int ret = EVP_CipherUpdate(ctx.get(), out, &out_len, native_data->data(), in_len); + int ret = EVP_CipherUpdate(ctx.get(), out_buf.get(), &out_len, native_data->data(), in_len); if (!ret) { unsigned long err = ERR_get_error(); char err_buf[256]; ERR_error_string_n(err, err_buf, sizeof(err_buf)); - delete[] out; throw std::runtime_error("Cipher update failed: " + std::string(err_buf)); } // Create and return a new buffer of exact size needed - return std::make_shared(out, out_len, [=]() { delete[] out; }); + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), out_len, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr HybridCipher::final() { diff --git a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp index 5801c50f..935f4ba3 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/HybridRsaCipher.cpp @@ -283,8 +283,9 @@ std::shared_ptr HybridRsaCipher::publicDecrypt(const std::shared_pt if (outlen == 0) { EVP_PKEY_CTX_free(ctx); - uint8_t* empty_buf = new uint8_t[1]; - return std::make_shared(empty_buf, 0, [empty_buf]() { delete[] empty_buf; }); + auto empty_buf = std::make_unique(1); + uint8_t* raw_ptr = empty_buf.get(); + return std::make_shared(empty_buf.release(), 0, [raw_ptr]() { delete[] raw_ptr; }); } auto out_buf = std::make_unique(outlen); @@ -306,8 +307,9 @@ std::shared_ptr HybridRsaCipher::publicDecrypt(const std::shared_pt if ((err & 0xFFFFFFF) == 0x1C880004 || (err & 0xFF) == 0x04) { ERR_clear_error(); EVP_PKEY_CTX_free(ctx); - uint8_t* empty_buf = new uint8_t[1]; - return std::make_shared(empty_buf, 0, [empty_buf]() { delete[] empty_buf; }); + auto empty_buf = std::make_unique(1); + uint8_t* raw_ptr = empty_buf.get(); + return std::make_shared(empty_buf.release(), 0, [raw_ptr]() { delete[] raw_ptr; }); } EVP_PKEY_CTX_free(ctx); throwOpaqueDecryptFailure(); diff --git a/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp index bf1cd9a3..a2556255 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/XChaCha20Poly1305Cipher.cpp @@ -70,42 +70,42 @@ std::shared_ptr XChaCha20Poly1305Cipher::final() { throw std::runtime_error("XChaCha20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)"); #else if (is_cipher) { - uint8_t* ciphertext = new uint8_t[data_buffer_.size()]; + auto ciphertext = std::make_unique(data_buffer_.size()); int result = - crypto_aead_xchacha20poly1305_ietf_encrypt_detached(ciphertext, auth_tag_, nullptr, data_buffer_.data(), data_buffer_.size(), + crypto_aead_xchacha20poly1305_ietf_encrypt_detached(ciphertext.get(), auth_tag_, nullptr, data_buffer_.data(), data_buffer_.size(), aad_.empty() ? nullptr : aad_.data(), aad_.size(), nullptr, nonce_, key_); if (result != 0) { - sodium_memzero(ciphertext, data_buffer_.size()); - delete[] ciphertext; + sodium_memzero(ciphertext.get(), data_buffer_.size()); throw std::runtime_error("XChaCha20Poly1305Cipher: encryption failed"); } is_finalized = true; size_t ct_len = data_buffer_.size(); - return std::make_shared(ciphertext, ct_len, [=]() { delete[] ciphertext; }); + uint8_t* raw_ptr = ciphertext.get(); + return std::make_shared(ciphertext.release(), ct_len, [raw_ptr]() { delete[] raw_ptr; }); } else { if (data_buffer_.empty()) { is_finalized = true; return std::make_shared(nullptr, 0, nullptr); } - uint8_t* plaintext = new uint8_t[data_buffer_.size()]; + auto plaintext = std::make_unique(data_buffer_.size()); int result = - crypto_aead_xchacha20poly1305_ietf_decrypt_detached(plaintext, nullptr, data_buffer_.data(), data_buffer_.size(), auth_tag_, + crypto_aead_xchacha20poly1305_ietf_decrypt_detached(plaintext.get(), nullptr, data_buffer_.data(), data_buffer_.size(), auth_tag_, aad_.empty() ? nullptr : aad_.data(), aad_.size(), nonce_, key_); if (result != 0) { - sodium_memzero(plaintext, data_buffer_.size()); - delete[] plaintext; + sodium_memzero(plaintext.get(), data_buffer_.size()); throw std::runtime_error("XChaCha20Poly1305Cipher: decryption failed - authentication tag mismatch"); } is_finalized = true; size_t pt_len = data_buffer_.size(); - return std::make_shared(plaintext, pt_len, [=]() { delete[] plaintext; }); + uint8_t* raw_ptr = plaintext.get(); + return std::make_shared(plaintext.release(), pt_len, [raw_ptr]() { delete[] raw_ptr; }); } #endif } @@ -132,9 +132,10 @@ std::shared_ptr XChaCha20Poly1305Cipher::getAuthTag() { throw std::runtime_error("getAuthTag must be called after final()"); } - uint8_t* tag_copy = new uint8_t[kTagSize]; - std::memcpy(tag_copy, auth_tag_, kTagSize); - return std::make_shared(tag_copy, kTagSize, [=]() { delete[] tag_copy; }); + auto tag_copy = std::make_unique(kTagSize); + std::memcpy(tag_copy.get(), auth_tag_, kTagSize); + uint8_t* raw_ptr = tag_copy.get(); + return std::make_shared(tag_copy.release(), kTagSize, [raw_ptr]() { delete[] raw_ptr; }); #endif } diff --git a/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp b/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp index da6b5f69..163528ab 100644 --- a/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp +++ b/packages/react-native-quick-crypto/cpp/cipher/XSalsa20Poly1305Cipher.cpp @@ -60,38 +60,38 @@ std::shared_ptr XSalsa20Poly1305Cipher::final() { throw std::runtime_error("XSalsa20Poly1305Cipher: libsodium must be enabled (BLSALLOC_SODIUM)"); #else if (is_cipher) { - uint8_t* ciphertext = new uint8_t[data_buffer_.size()]; + auto ciphertext = std::make_unique(data_buffer_.size()); - int result = crypto_secretbox_detached(ciphertext, auth_tag_, data_buffer_.data(), data_buffer_.size(), nonce_, key_); + int result = crypto_secretbox_detached(ciphertext.get(), auth_tag_, data_buffer_.data(), data_buffer_.size(), nonce_, key_); if (result != 0) { - sodium_memzero(ciphertext, data_buffer_.size()); - delete[] ciphertext; + sodium_memzero(ciphertext.get(), data_buffer_.size()); throw std::runtime_error("XSalsa20Poly1305Cipher: encryption failed"); } is_finalized = true; size_t ct_len = data_buffer_.size(); - return std::make_shared(ciphertext, ct_len, [=]() { delete[] ciphertext; }); + uint8_t* raw_ptr = ciphertext.get(); + return std::make_shared(ciphertext.release(), ct_len, [raw_ptr]() { delete[] raw_ptr; }); } else { if (data_buffer_.empty()) { is_finalized = true; return std::make_shared(nullptr, 0, nullptr); } - uint8_t* plaintext = new uint8_t[data_buffer_.size()]; + auto plaintext = std::make_unique(data_buffer_.size()); - int result = crypto_secretbox_open_detached(plaintext, data_buffer_.data(), auth_tag_, data_buffer_.size(), nonce_, key_); + int result = crypto_secretbox_open_detached(plaintext.get(), data_buffer_.data(), auth_tag_, data_buffer_.size(), nonce_, key_); if (result != 0) { - sodium_memzero(plaintext, data_buffer_.size()); - delete[] plaintext; + sodium_memzero(plaintext.get(), data_buffer_.size()); throw std::runtime_error("XSalsa20Poly1305Cipher: decryption failed - authentication tag mismatch"); } is_finalized = true; size_t pt_len = data_buffer_.size(); - return std::make_shared(plaintext, pt_len, [=]() { delete[] plaintext; }); + uint8_t* raw_ptr = plaintext.get(); + return std::make_shared(plaintext.release(), pt_len, [raw_ptr]() { delete[] raw_ptr; }); } #endif } @@ -111,9 +111,10 @@ std::shared_ptr XSalsa20Poly1305Cipher::getAuthTag() { throw std::runtime_error("getAuthTag must be called after final()"); } - uint8_t* tag_copy = new uint8_t[kTagSize]; - std::memcpy(tag_copy, auth_tag_, kTagSize); - return std::make_shared(tag_copy, kTagSize, [=]() { delete[] tag_copy; }); + auto tag_copy = std::make_unique(kTagSize); + std::memcpy(tag_copy.get(), auth_tag_, kTagSize); + uint8_t* raw_ptr = tag_copy.get(); + return std::make_shared(tag_copy.release(), kTagSize, [raw_ptr]() { delete[] raw_ptr; }); #endif } diff --git a/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp index 6b08f12e..dd4534fd 100644 --- a/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.cpp @@ -40,10 +40,7 @@ void HybridEcKeyPair::generateKeyPairSync() { } // Clean up existing key if any - if (this->pkey != nullptr) { - EVP_PKEY_free(this->pkey); - this->pkey = nullptr; - } + this->pkey_.reset(); // Get curve NID from curve name int curve_nid = GetCurveFromName(this->curve.c_str()); @@ -105,17 +102,14 @@ void HybridEcKeyPair::generateKeyPairSync() { throw std::runtime_error("Failed to generate EC key pair"); } - this->pkey = raw_pkey; + this->pkey_.reset(raw_pkey); } KeyObject HybridEcKeyPair::importKey(const std::string& format, const std::shared_ptr& keyData, const std::string& /* algorithm */, bool /* extractable */, const std::vector& /* keyUsages */) { // Clean up any existing key - if (this->pkey != nullptr) { - EVP_PKEY_free(this->pkey); - this->pkey = nullptr; - } + this->pkey_.reset(); // Reset curve state to avoid interference between different uses this->curve.clear(); @@ -143,7 +137,7 @@ KeyObject HybridEcKeyPair::importKey(const std::string& format, const std::share PKCS8_PRIV_KEY_INFO_free(p8inf); BIO_free(pkcs8_bio); if (pkcs8_pkey != nullptr) { - this->pkey = pkcs8_pkey; + this->pkey_.reset(pkcs8_pkey); KeyObject keyObj; return keyObj; } @@ -157,7 +151,7 @@ KeyObject HybridEcKeyPair::importKey(const std::string& format, const std::share EVP_PKEY* spki_pkey = d2i_PUBKEY_bio(spki_bio, nullptr); BIO_free(spki_bio); if (spki_pkey != nullptr) { - this->pkey = spki_pkey; + this->pkey_.reset(spki_pkey); KeyObject keyObj; return keyObj; } @@ -166,7 +160,7 @@ KeyObject HybridEcKeyPair::importKey(const std::string& format, const std::share throw std::runtime_error("Failed to import EC key from DER data"); } - this->pkey = pkey; + this->pkey_.reset(pkey); // Return a placeholder KeyObject - this would need proper implementation // For now, we just need the key imported into this->pkey for sign/verify @@ -178,20 +172,20 @@ std::shared_ptr HybridEcKeyPair::exportKey(const KeyObject& key, co // Suppress unused parameter warning (void)key; - if (!this->pkey) { + if (!this->pkey_) { throw std::runtime_error("No key pair generated"); } if (format == "der-spki") { // Export public key in DER SPKI format - int len = i2d_PUBKEY(this->pkey, nullptr); + int len = i2d_PUBKEY(this->pkey_.get(), nullptr); if (len <= 0) { throw std::runtime_error("Failed to get public key DER length"); } std::vector derData(len); unsigned char* ptr = derData.data(); - i2d_PUBKEY(this->pkey, &ptr); + i2d_PUBKEY(this->pkey_.get(), &ptr); return ToNativeArrayBuffer(std::string(derData.begin(), derData.end())); } else if (format == "der-pkcs8") { // Export private key in DER PKCS8 format @@ -200,7 +194,7 @@ std::shared_ptr HybridEcKeyPair::exportKey(const KeyObject& key, co throw std::runtime_error("Failed to create BIO for private key export"); } - if (i2d_PKCS8PrivateKey_bio(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + if (i2d_PKCS8PrivateKey_bio(bio, this->pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export private key to DER PKCS8 format"); } @@ -218,7 +212,7 @@ std::shared_ptr HybridEcKeyPair::exportKey(const KeyObject& key, co throw std::runtime_error("Failed to create BIO for public key export"); } - if (PEM_write_bio_PUBKEY(bio, this->pkey) != 1) { + if (PEM_write_bio_PUBKEY(bio, this->pkey_.get()) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export public key to PEM SPKI format"); } @@ -236,7 +230,7 @@ std::shared_ptr HybridEcKeyPair::exportKey(const KeyObject& key, co throw std::runtime_error("Failed to create BIO for private key export"); } - if (PEM_write_bio_PKCS8PrivateKey(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + if (PEM_write_bio_PKCS8PrivateKey(bio, this->pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export private key to PEM PKCS8 format"); } @@ -261,7 +255,7 @@ std::shared_ptr HybridEcKeyPair::getPublicKey() { throw std::runtime_error("Failed to create BIO for public key export"); } - if (i2d_PUBKEY_bio(bio, this->pkey) != 1) { + if (i2d_PUBKEY_bio(bio, this->pkey_.get()) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export public key to DER format"); } @@ -277,13 +271,13 @@ std::shared_ptr HybridEcKeyPair::getPublicKey() { } std::shared_ptr HybridEcKeyPair::getPrivateKey() { - if (this->pkey == nullptr) { + if (!this->pkey_) { throw std::runtime_error("No private key available"); } // Export private key in PKCS8 DER format BIO* bio = BIO_new(BIO_s_mem()); - if (i2d_PKCS8PrivateKey_bio(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + if (i2d_PKCS8PrivateKey_bio(bio, this->pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export private key"); } @@ -350,7 +344,7 @@ std::shared_ptr HybridEcKeyPair::sign(const std::shared_ptrpkey) <= 0) { + if (EVP_DigestSignInit(md_ctx.get(), nullptr, md, nullptr, this->pkey_.get()) <= 0) { throw std::runtime_error("Failed to initialize ECDSA signing"); } @@ -377,7 +371,7 @@ std::shared_ptr HybridEcKeyPair::sign(const std::shared_ptrpkey); + unsigned int n = getBytesOfRS(this->pkey_.get()); if (n == 0) { throw std::runtime_error("Failed to determine EC key order size for P1363 conversion"); } @@ -415,7 +409,7 @@ bool HybridEcKeyPair::verify(const std::shared_ptr& data, const std } // Initialize verification - if (EVP_DigestVerifyInit(md_ctx.get(), nullptr, md, nullptr, this->pkey) <= 0) { + if (EVP_DigestVerifyInit(md_ctx.get(), nullptr, md, nullptr, this->pkey_.get()) <= 0) { throw std::runtime_error("Failed to initialize ECDSA verification"); } @@ -429,7 +423,7 @@ bool HybridEcKeyPair::verify(const std::shared_ptr& data, const std size_t sig_len = signature->size(); std::unique_ptr der_sig_buf; - unsigned int n = getBytesOfRS(this->pkey); + unsigned int n = getBytesOfRS(this->pkey_.get()); if (n == 0) { throw std::runtime_error("Failed to determine EC key order size for DER conversion"); } @@ -451,7 +445,7 @@ bool HybridEcKeyPair::verify(const std::shared_ptr& data, const std } void HybridEcKeyPair::checkKeyPair() { - if (this->pkey == nullptr) { + if (!this->pkey_) { throw std::runtime_error("EC KeyPair not initialized"); } } diff --git a/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp index 0504a841..8548405b 100644 --- a/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/ec/HybridEcKeyPair.hpp @@ -13,12 +13,7 @@ namespace margelo::nitro::crypto { class HybridEcKeyPair : public HybridEcKeyPairSpec { public: HybridEcKeyPair() : HybridObject(TAG) {} - ~HybridEcKeyPair() { - if (pkey != nullptr) { - EVP_PKEY_free(pkey); - pkey = nullptr; - } - } + ~HybridEcKeyPair() override = default; public: // Methods @@ -41,7 +36,8 @@ class HybridEcKeyPair : public HybridEcKeyPairSpec { private: std::string curve; - EVP_PKEY* pkey = nullptr; + using EVP_PKEY_ptr = std::unique_ptr; + EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; static int GetCurveFromName(const char* name); }; diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp index 97cfa186..00df22c8 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.cpp @@ -11,7 +11,6 @@ namespace margelo::nitro::crypto { std::shared_ptr HybridEdKeyPair::diffieHellman(const std::shared_ptr& privateKey, const std::shared_ptr& publicKey) { - using EVP_PKEY_ptr = std::unique_ptr; using EVP_PKEY_CTX_ptr = std::unique_ptr; // Determine key type from curve name @@ -55,16 +54,16 @@ std::shared_ptr HybridEdKeyPair::diffieHellman(const std::shared_pt } // 7. Allocate memory for the shared secret - auto shared_secret = new uint8_t[shared_secret_len]; + auto shared_secret = std::make_unique(shared_secret_len); // 8. Derive the shared secret - if (EVP_PKEY_derive(ctx.get(), shared_secret, &shared_secret_len) <= 0) { - delete[] shared_secret; + if (EVP_PKEY_derive(ctx.get(), shared_secret.get(), &shared_secret_len) <= 0) { throw std::runtime_error("Failed to derive shared secret: " + getOpenSSLError()); } // 9. Return a newly-created ArrayBuffer from the raw buffer w/ cleanup - return std::make_shared(shared_secret, shared_secret_len, [=]() { delete[] shared_secret; }); + uint8_t* raw_ptr = shared_secret.get(); + return std::make_shared(shared_secret.release(), shared_secret_len, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr> HybridEdKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat, @@ -98,34 +97,26 @@ void HybridEdKeyPair::generateKeyPairSync(double publicFormat, double publicType this->privateType_ = static_cast(privateType); // Clean up existing key if any - if (this->pkey != nullptr) { - EVP_PKEY_free(this->pkey); - this->pkey = nullptr; - } - - EVP_PKEY_CTX* pctx; + this->pkey_.reset(); - // key context - pctx = EVP_PKEY_CTX_new_from_name(nullptr, this->curve.c_str(), nullptr); - if (pctx == nullptr) { + std::unique_ptr pctx(EVP_PKEY_CTX_new_from_name(nullptr, this->curve.c_str(), nullptr), + EVP_PKEY_CTX_free); + if (!pctx) { throw std::runtime_error("Invalid curve name: " + this->curve); } // keygen init - if (EVP_PKEY_keygen_init(pctx) <= 0) { - EVP_PKEY_CTX_free(pctx); + if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { throw std::runtime_error("Failed to initialize keygen"); } // generate key - EVP_PKEY_keygen(pctx, &this->pkey); - if (this->pkey == nullptr) { - EVP_PKEY_CTX_free(pctx); + EVP_PKEY* raw_pkey = nullptr; + EVP_PKEY_keygen(pctx.get(), &raw_pkey); + if (raw_pkey == nullptr) { throw std::runtime_error("Failed to generate key"); } - - // cleanup - EVP_PKEY_CTX_free(pctx); + this->pkey_.reset(raw_pkey); } std::shared_ptr>> HybridEdKeyPair::sign(const std::shared_ptr& message, @@ -146,55 +137,34 @@ std::shared_ptr HybridEdKeyPair::signSync(const std::shared_ptrimportPrivateKey(key); + EVP_PKEY_ptr pkey = this->importPrivateKey(key); // key context - md_ctx = EVP_MD_CTX_new(); - if (md_ctx == nullptr) { + std::unique_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!md_ctx) { throw std::runtime_error("Error creating signing context"); } - pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, this->curve.c_str(), nullptr); - if (pkey_ctx == nullptr) { - EVP_MD_CTX_free(md_ctx); - throw std::runtime_error("Error creating signing context: " + this->curve); - } - - if (EVP_DigestSignInit(md_ctx, &pkey_ctx, NULL, NULL, pkey) <= 0) { - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_CTX_free(pkey_ctx); - char* err = ERR_error_string(ERR_get_error(), NULL); - throw std::runtime_error("Failed to initialize signing: " + std::string(err)); + if (EVP_DigestSignInit(md_ctx.get(), nullptr, NULL, NULL, pkey.get()) <= 0) { + throw std::runtime_error("Failed to initialize signing: " + getOpenSSLError()); } // Calculate the required size for the signature by passing a NULL buffer. - if (EVP_DigestSign(md_ctx, NULL, &sig_len, message.get()->data(), message.get()->size()) <= 0) { - EVP_MD_CTX_free(md_ctx); + size_t sig_len = 0; + if (EVP_DigestSign(md_ctx.get(), NULL, &sig_len, message.get()->data(), message.get()->size()) <= 0) { throw std::runtime_error("Failed to calculate signature size"); } - sig = new uint8_t[sig_len]; + auto sig = std::make_unique(sig_len); // Actually calculate the signature - if (EVP_DigestSign(md_ctx, sig, &sig_len, message.get()->data(), message.get()->size()) <= 0) { - EVP_MD_CTX_free(md_ctx); - delete[] sig; + if (EVP_DigestSign(md_ctx.get(), sig.get(), &sig_len, message.get()->data(), message.get()->size()) <= 0) { throw std::runtime_error("Failed to calculate signature"); } // return value for JS - std::shared_ptr signature = std::make_shared(sig, sig_len, [=]() { delete[] sig; }); - - // Clean up - EVP_MD_CTX_free(md_ctx); - // Note: pkey_ctx is freed automatically by EVP_MD_CTX_free when using EVP_DigestSignInit - - return signature; + uint8_t* raw_ptr = sig.get(); + return std::make_shared(sig.release(), sig_len, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr> HybridEdKeyPair::verify(const std::shared_ptr& signature, @@ -218,36 +188,23 @@ bool HybridEdKeyPair::verifySync(const std::shared_ptr& signature, clearOpenSSLErrors(); // get key to use for verifying - EVP_PKEY* pkey = this->importPublicKey(key); - - EVP_MD_CTX* md_ctx = nullptr; - EVP_PKEY_CTX* pkey_ctx = nullptr; + EVP_PKEY_ptr pkey = this->importPublicKey(key); // key context - md_ctx = EVP_MD_CTX_new(); - if (md_ctx == nullptr) { + std::unique_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!md_ctx) { throw std::runtime_error("Error creating verify context"); } - pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, this->curve.c_str(), nullptr); - if (pkey_ctx == nullptr) { - EVP_MD_CTX_free(md_ctx); - throw std::runtime_error("Error creating verify context: " + this->curve); - } - - if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, NULL, NULL, pkey) <= 0) { - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_CTX_free(pkey_ctx); - char* err = ERR_error_string(ERR_get_error(), NULL); - throw std::runtime_error("Failed to initialize verify: " + std::string(err)); + if (EVP_DigestVerifyInit(md_ctx.get(), nullptr, NULL, NULL, pkey.get()) <= 0) { + throw std::runtime_error("Failed to initialize verify: " + getOpenSSLError()); } // verify - auto res = EVP_DigestVerify(md_ctx, signature.get()->data(), signature.get()->size(), message.get()->data(), message.get()->size()); + auto res = EVP_DigestVerify(md_ctx.get(), signature.get()->data(), signature.get()->size(), message.get()->data(), message.get()->size()); // return value for JS if (res < 0) { - EVP_MD_CTX_free(md_ctx); throw std::runtime_error("Failed to verify"); } return res == 1; // true if 1, false if 0 @@ -258,7 +215,7 @@ std::shared_ptr HybridEdKeyPair::getPublicKey() { // If format is DER (0) or PEM (1), export in SPKI format if (publicFormat_ == 0 || publicFormat_ == 1) { - BIO* bio = BIO_new(BIO_s_mem()); + std::unique_ptr bio(BIO_new(BIO_s_mem()), BIO_free); if (!bio) { throw std::runtime_error("Failed to create BIO for public key export"); } @@ -266,36 +223,35 @@ std::shared_ptr HybridEdKeyPair::getPublicKey() { int result; if (publicFormat_ == 1) { // PEM format - result = PEM_write_bio_PUBKEY(bio, this->pkey); + result = PEM_write_bio_PUBKEY(bio.get(), this->pkey_.get()); } else { // DER format - result = i2d_PUBKEY_bio(bio, this->pkey); + result = i2d_PUBKEY_bio(bio.get(), this->pkey_.get()); } if (result != 1) { - BIO_free(bio); throw std::runtime_error("Failed to export public key"); } BUF_MEM* bptr; - BIO_get_mem_ptr(bio, &bptr); + BIO_get_mem_ptr(bio.get(), &bptr); - uint8_t* data = new uint8_t[bptr->length]; - memcpy(data, bptr->data, bptr->length); + auto data = std::make_unique(bptr->length); + memcpy(data.get(), bptr->data, bptr->length); size_t len = bptr->length; - BIO_free(bio); - - return std::make_shared(data, len, [=]() { delete[] data; }); + uint8_t* raw_ptr = data.get(); + return std::make_shared(data.release(), len, [raw_ptr]() { delete[] raw_ptr; }); } // Default: raw format size_t len = 0; - EVP_PKEY_get_raw_public_key(this->pkey, nullptr, &len); - uint8_t* publ = new uint8_t[len]; - EVP_PKEY_get_raw_public_key(this->pkey, publ, &len); + EVP_PKEY_get_raw_public_key(this->pkey_.get(), nullptr, &len); + auto publ = std::make_unique(len); + EVP_PKEY_get_raw_public_key(this->pkey_.get(), publ.get(), &len); - return std::make_shared(publ, len, [=]() { delete[] publ; }); + uint8_t* raw_ptr = publ.get(); + return std::make_shared(publ.release(), len, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr HybridEdKeyPair::getPrivateKey() { @@ -303,7 +259,7 @@ std::shared_ptr HybridEdKeyPair::getPrivateKey() { // If format is DER (0) or PEM (1), export in PKCS8 format if (privateFormat_ == 0 || privateFormat_ == 1) { - BIO* bio = BIO_new(BIO_s_mem()); + std::unique_ptr bio(BIO_new(BIO_s_mem()), BIO_free); if (!bio) { throw std::runtime_error("Failed to create BIO for private key export"); } @@ -311,40 +267,42 @@ std::shared_ptr HybridEdKeyPair::getPrivateKey() { int result; if (privateFormat_ == 1) { // PEM format (PKCS8) - result = PEM_write_bio_PrivateKey(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr); + result = PEM_write_bio_PrivateKey(bio.get(), this->pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr); } else { // DER format (PKCS8) - result = i2d_PrivateKey_bio(bio, this->pkey); + result = i2d_PrivateKey_bio(bio.get(), this->pkey_.get()); } if (result != 1) { - BIO_free(bio); throw std::runtime_error("Failed to export private key"); } BUF_MEM* bptr; - BIO_get_mem_ptr(bio, &bptr); + BIO_get_mem_ptr(bio.get(), &bptr); - uint8_t* data = new uint8_t[bptr->length]; - memcpy(data, bptr->data, bptr->length); + auto data = std::make_unique(bptr->length); + memcpy(data.get(), bptr->data, bptr->length); size_t len = bptr->length; - BIO_free(bio); + // Zero the BIO's internal buffer — it held private key bytes (PEM/DER PKCS8) + secureZero(bptr->data, bptr->length); - return std::make_shared(data, len, [=]() { delete[] data; }); + uint8_t* raw_ptr = data.get(); + return std::make_shared(data.release(), len, [raw_ptr]() { delete[] raw_ptr; }); } // Default: raw format size_t len = 0; - EVP_PKEY_get_raw_private_key(this->pkey, nullptr, &len); - uint8_t* priv = new uint8_t[len]; - EVP_PKEY_get_raw_private_key(this->pkey, priv, &len); + EVP_PKEY_get_raw_private_key(this->pkey_.get(), nullptr, &len); + auto priv = std::make_unique(len); + EVP_PKEY_get_raw_private_key(this->pkey_.get(), priv.get(), &len); - return std::make_shared(priv, len, [=]() { delete[] priv; }); + uint8_t* raw_ptr = priv.get(); + return std::make_shared(priv.release(), len, [raw_ptr]() { delete[] raw_ptr; }); } void HybridEdKeyPair::checkKeyPair() { - if (this->pkey == nullptr) { + if (!this->pkey_) { throw std::runtime_error("Keypair not initialized"); } } @@ -353,8 +311,7 @@ void HybridEdKeyPair::setCurve(const std::string& curve) { this->curve = curve; } -EVP_PKEY* HybridEdKeyPair::importPublicKey(const std::optional>& key) { - EVP_PKEY* pkey = nullptr; +auto HybridEdKeyPair::importPublicKey(const std::optional>& key) -> EVP_PKEY_ptr { if (key.has_value()) { // Determine key type from curve name int keyType = EVP_PKEY_ED25519; @@ -366,19 +323,18 @@ EVP_PKEY* HybridEdKeyPair::importPublicKey(const std::optionaldata(), key.value()->size()); - if (pkey == nullptr) { + EVP_PKEY_ptr pkey(EVP_PKEY_new_raw_public_key(keyType, NULL, key.value()->data(), key.value()->size()), EVP_PKEY_free); + if (!pkey) { throw std::runtime_error("Failed to read public key"); } - } else { - this->checkKeyPair(); - pkey = this->pkey; + return pkey; } - return pkey; + this->checkKeyPair(); + EVP_PKEY_up_ref(this->pkey_.get()); + return EVP_PKEY_ptr(this->pkey_.get(), EVP_PKEY_free); } -EVP_PKEY* HybridEdKeyPair::importPrivateKey(const std::optional>& key) { - EVP_PKEY* pkey = nullptr; +auto HybridEdKeyPair::importPrivateKey(const std::optional>& key) -> EVP_PKEY_ptr { if (key.has_value()) { // Determine key type from curve name int keyType = EVP_PKEY_ED25519; @@ -390,15 +346,15 @@ EVP_PKEY* HybridEdKeyPair::importPrivateKey(const std::optionaldata(), key.value()->size()); - if (pkey == nullptr) { + EVP_PKEY_ptr pkey(EVP_PKEY_new_raw_private_key(keyType, NULL, key.value()->data(), key.value()->size()), EVP_PKEY_free); + if (!pkey) { throw std::runtime_error("Failed to read private key"); } - } else { - this->checkKeyPair(); - pkey = this->pkey; + return pkey; } - return pkey; + this->checkKeyPair(); + EVP_PKEY_up_ref(this->pkey_.get()); + return EVP_PKEY_ptr(this->pkey_.get(), EVP_PKEY_free); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp index 5fa4897d..9f9fa0b2 100644 --- a/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/ed25519/HybridEdKeyPair.hpp @@ -11,12 +11,7 @@ namespace margelo::nitro::crypto { class HybridEdKeyPair : public HybridEdKeyPairSpec { public: HybridEdKeyPair() : HybridObject(TAG) {} - ~HybridEdKeyPair() { - if (pkey != nullptr) { - EVP_PKEY_free(pkey); - pkey = nullptr; - } - } + ~HybridEdKeyPair() override = default; public: // Methods @@ -54,7 +49,8 @@ class HybridEdKeyPair : public HybridEdKeyPairSpec { private: std::string curve; - EVP_PKEY* pkey = nullptr; + using EVP_PKEY_ptr = std::unique_ptr; + EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; // Encoding configuration for key export // Format: -1 = default (raw), 0 = DER, 1 = PEM @@ -64,8 +60,8 @@ class HybridEdKeyPair : public HybridEdKeyPairSpec { int privateFormat_ = -1; int privateType_ = -1; - EVP_PKEY* importPublicKey(const std::optional>& key); - EVP_PKEY* importPrivateKey(const std::optional>& key); + EVP_PKEY_ptr importPublicKey(const std::optional>& key); + EVP_PKEY_ptr importPrivateKey(const std::optional>& key); }; } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp b/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp index c3bdf797..7df66ec7 100644 --- a/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp +++ b/packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp @@ -101,24 +101,22 @@ std::shared_ptr HybridHash::digest(const std::optional throw std::runtime_error("Invalid digest size: " + std::to_string(digestSize)); } - // Create a buffer for the hash output - uint8_t* hashBuffer = new uint8_t[digestSize]; + auto hashBuffer = std::make_unique(digestSize); size_t hashLength = digestSize; - // Finalize the digest int ret; if (digestSize == defaultLen) { - ret = EVP_DigestFinal_ex(ctx, hashBuffer, reinterpret_cast(&hashLength)); + ret = EVP_DigestFinal_ex(ctx, hashBuffer.get(), reinterpret_cast(&hashLength)); } else { - ret = EVP_DigestFinalXOF(ctx, hashBuffer, hashLength); + ret = EVP_DigestFinalXOF(ctx, hashBuffer.get(), hashLength); } if (ret != 1) { - delete[] hashBuffer; throw std::runtime_error("Failed to finalize hash digest: " + std::to_string(ERR_get_error())); } - return std::make_shared(hashBuffer, hashLength, [=]() { delete[] hashBuffer; }); + uint8_t* raw_ptr = hashBuffer.get(); + return std::make_shared(hashBuffer.release(), hashLength, [raw_ptr]() { delete[] raw_ptr; }); } std::shared_ptr HybridHash::copy(const std::optional outputLengthArg) { diff --git a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp index b26dfcbd..3b1b32ce 100644 --- a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp +++ b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp @@ -80,17 +80,19 @@ std::shared_ptr HybridHkdf::deriveKeySync(const std::string& algori throw std::runtime_error("HKDF length cannot be zero"); } - uint8_t* outBuf = new uint8_t[outLen]; + auto out_buf = std::make_unique(outLen); - if (EVP_KDF_derive(ctx, outBuf, outLen, params) <= 0) { + if (EVP_KDF_derive(ctx, out_buf.get(), outLen, params) <= 0) { EVP_KDF_CTX_free(ctx); - delete[] outBuf; + // Zero any partially-derived secret bits before unique_ptr frees the buffer. + secureZero(out_buf.get(), outLen); throw std::runtime_error("HKDF derivation failed: " + std::to_string(ERR_get_error())); } EVP_KDF_CTX_free(ctx); - return std::make_shared(outBuf, outLen, [=]() { delete[] outBuf; }); + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), outLen, [raw_ptr]() { delete[] raw_ptr; }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/hmac/HybridHmac.cpp b/packages/react-native-quick-crypto/cpp/hmac/HybridHmac.cpp index 378e0dad..d6865422 100644 --- a/packages/react-native-quick-crypto/cpp/hmac/HybridHmac.cpp +++ b/packages/react-native-quick-crypto/cpp/hmac/HybridHmac.cpp @@ -89,16 +89,14 @@ std::shared_ptr HybridHmac::digest() { const EVP_MD* md = EVP_get_digestbyname(algorithm.c_str()); const size_t hmacLength = EVP_MD_get_size(md); - // Allocate buffer with the exact required size - uint8_t* hmacBuffer = new uint8_t[hmacLength]; + auto hmacBuffer = std::make_unique(hmacLength); - // Finalize the HMAC computation directly into the final buffer - if (EVP_MAC_final(ctx, hmacBuffer, nullptr, hmacLength) != 1) { - delete[] hmacBuffer; + if (EVP_MAC_final(ctx, hmacBuffer.get(), nullptr, hmacLength) != 1) { throw std::runtime_error("Failed to finalize HMAC digest: " + std::to_string(ERR_get_error())); } - return std::make_shared(hmacBuffer, hmacLength, [=]() { delete[] hmacBuffer; }); + uint8_t* raw_ptr = hmacBuffer.get(); + return std::make_shared(hmacBuffer.release(), hmacLength, [raw_ptr]() { delete[] raw_ptr; }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/kmac/HybridKmac.cpp b/packages/react-native-quick-crypto/cpp/kmac/HybridKmac.cpp index d7788121..7da17ae7 100644 --- a/packages/react-native-quick-crypto/cpp/kmac/HybridKmac.cpp +++ b/packages/react-native-quick-crypto/cpp/kmac/HybridKmac.cpp @@ -68,16 +68,16 @@ std::shared_ptr HybridKmac::digest() { throw std::runtime_error("KMAC context not initialized"); } - uint8_t* buffer = new uint8_t[outputLen]; + auto buffer = std::make_unique(outputLen); - if (EVP_MAC_final(ctx.get(), buffer, nullptr, outputLen) != 1) { - delete[] buffer; + if (EVP_MAC_final(ctx.get(), buffer.get(), nullptr, outputLen) != 1) { throw std::runtime_error("Failed to finalize KMAC digest: " + std::to_string(ERR_get_error())); } ctx.reset(); - return std::make_shared(buffer, outputLen, [=]() { delete[] buffer; }); + uint8_t* raw_ptr = buffer.get(); + return std::make_shared(buffer.release(), outputLen, [raw_ptr]() { delete[] raw_ptr; }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/mldsa/HybridMlDsaKeyPair.cpp b/packages/react-native-quick-crypto/cpp/mldsa/HybridMlDsaKeyPair.cpp index 495b744b..a9e130df 100644 --- a/packages/react-native-quick-crypto/cpp/mldsa/HybridMlDsaKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/mldsa/HybridMlDsaKeyPair.cpp @@ -15,6 +15,9 @@ namespace margelo::nitro::crypto { +using EVP_MD_CTX_ptr = std::unique_ptr; +using EVP_PKEY_CTX_ptr = std::unique_ptr; + int HybridMlDsaKeyPair::getEvpPkeyType() const { #if RNQC_HAS_ML_DSA if (variant_ == "ML-DSA-44") @@ -39,8 +42,9 @@ void HybridMlDsaKeyPair::setVariant(const std::string& variant) { std::shared_ptr> HybridMlDsaKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat, double privateType) { - return Promise::async([this, publicFormat, publicType, privateFormat, privateType]() { - this->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType); + auto self = this->shared_cast(); + return Promise::async([self, publicFormat, publicType, privateFormat, privateType]() { + self->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType); }); } @@ -61,24 +65,20 @@ void HybridMlDsaKeyPair::generateKeyPairSync(double publicFormat, double publicT pkey_.reset(); - EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr); + EVP_PKEY_CTX_ptr pctx(EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr), EVP_PKEY_CTX_free); if (pctx == nullptr) { throw std::runtime_error("Failed to create key context for " + variant_ + ": " + getOpenSSLError()); } - if (EVP_PKEY_keygen_init(pctx) <= 0) { - EVP_PKEY_CTX_free(pctx); + if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { throw std::runtime_error("Failed to initialize keygen: " + getOpenSSLError()); } EVP_PKEY* raw = nullptr; - if (EVP_PKEY_keygen(pctx, &raw) <= 0) { - EVP_PKEY_CTX_free(pctx); + if (EVP_PKEY_keygen(pctx.get(), &raw) <= 0) { throw std::runtime_error("Failed to generate ML-DSA key pair: " + getOpenSSLError()); } pkey_.reset(raw); - - EVP_PKEY_CTX_free(pctx); #endif } @@ -108,13 +108,14 @@ std::shared_ptr HybridMlDsaKeyPair::getPublicKey() { BUF_MEM* bptr; BIO_get_mem_ptr(bio, &bptr); - uint8_t* data = new uint8_t[bptr->length]; - memcpy(data, bptr->data, bptr->length); size_t len = bptr->length; + auto buf = std::make_unique(len); + memcpy(buf.get(), bptr->data, len); BIO_free(bio); - return std::make_shared(data, len, [=]() { delete[] data; }); + uint8_t* raw_ptr = buf.get(); + return std::make_shared(buf.release(), len, [raw_ptr]() { delete[] raw_ptr; }); #endif } @@ -144,19 +145,23 @@ std::shared_ptr HybridMlDsaKeyPair::getPrivateKey() { BUF_MEM* bptr; BIO_get_mem_ptr(bio, &bptr); - uint8_t* data = new uint8_t[bptr->length]; - memcpy(data, bptr->data, bptr->length); size_t len = bptr->length; + auto buf = std::make_unique(len); + memcpy(buf.get(), bptr->data, len); + // Wipe the private key bytes from the BIO before freeing. + secureZero(bptr->data, bptr->length); BIO_free(bio); - return std::make_shared(data, len, [=]() { delete[] data; }); + uint8_t* raw_ptr = buf.get(); + return std::make_shared(buf.release(), len, [raw_ptr]() { delete[] raw_ptr; }); #endif } std::shared_ptr>> HybridMlDsaKeyPair::sign(const std::shared_ptr& message) { auto nativeMessage = ToNativeArrayBuffer(message); - return Promise>::async([this, nativeMessage]() { return this->signSync(nativeMessage); }); + auto self = this->shared_cast(); + return Promise>::async([self, nativeMessage]() { return self->signSync(nativeMessage); }); } std::shared_ptr HybridMlDsaKeyPair::signSync(const std::shared_ptr& message) { @@ -166,40 +171,30 @@ std::shared_ptr HybridMlDsaKeyPair::signSync(const std::shared_ptr< clearOpenSSLErrors(); checkKeyPair(); - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + EVP_MD_CTX_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); if (md_ctx == nullptr) { throw std::runtime_error("Failed to create signing context"); } - EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr); - if (pkey_ctx == nullptr) { - EVP_MD_CTX_free(md_ctx); - throw std::runtime_error("Failed to create signing context for " + variant_); - } - - if (EVP_DigestSignInit(md_ctx, &pkey_ctx, nullptr, nullptr, pkey_.get()) <= 0) { - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_CTX_free(pkey_ctx); + // Pass nullptr — EVP_DigestSignInit allocates the matching PKEY_CTX from + // pkey_ and the EVP_MD_CTX takes ownership of it. + if (EVP_DigestSignInit(md_ctx.get(), nullptr, nullptr, nullptr, pkey_.get()) <= 0) { throw std::runtime_error("Failed to initialize signing: " + getOpenSSLError()); } size_t sig_len = 0; - if (EVP_DigestSign(md_ctx, nullptr, &sig_len, message->data(), message->size()) <= 0) { - EVP_MD_CTX_free(md_ctx); + if (EVP_DigestSign(md_ctx.get(), nullptr, &sig_len, message->data(), message->size()) <= 0) { throw std::runtime_error("Failed to calculate signature size: " + getOpenSSLError()); } - uint8_t* sig = new uint8_t[sig_len]; + auto sig = std::make_unique(sig_len); - if (EVP_DigestSign(md_ctx, sig, &sig_len, message->data(), message->size()) <= 0) { - EVP_MD_CTX_free(md_ctx); - delete[] sig; + if (EVP_DigestSign(md_ctx.get(), sig.get(), &sig_len, message->data(), message->size()) <= 0) { throw std::runtime_error("Failed to sign message: " + getOpenSSLError()); } - EVP_MD_CTX_free(md_ctx); - - return std::make_shared(sig, sig_len, [=]() { delete[] sig; }); + uint8_t* raw_ptr = sig.get(); + return std::make_shared(sig.release(), sig_len, [raw_ptr]() { delete[] raw_ptr; }); #endif } @@ -207,7 +202,8 @@ std::shared_ptr> HybridMlDsaKeyPair::verify(const std::shared_ptr< const std::shared_ptr& message) { auto nativeSignature = ToNativeArrayBuffer(signature); auto nativeMessage = ToNativeArrayBuffer(message); - return Promise::async([this, nativeSignature, nativeMessage]() { return this->verifySync(nativeSignature, nativeMessage); }); + auto self = this->shared_cast(); + return Promise::async([self, nativeSignature, nativeMessage]() { return self->verifySync(nativeSignature, nativeMessage); }); } bool HybridMlDsaKeyPair::verifySync(const std::shared_ptr& signature, const std::shared_ptr& message) { @@ -217,26 +213,18 @@ bool HybridMlDsaKeyPair::verifySync(const std::shared_ptr& signatur clearOpenSSLErrors(); checkKeyPair(); - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + EVP_MD_CTX_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); if (md_ctx == nullptr) { throw std::runtime_error("Failed to create verify context"); } - EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr); - if (pkey_ctx == nullptr) { - EVP_MD_CTX_free(md_ctx); - throw std::runtime_error("Failed to create verify context for " + variant_); - } - - if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, nullptr, nullptr, pkey_.get()) <= 0) { - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_CTX_free(pkey_ctx); + // Pass nullptr — EVP_DigestVerifyInit allocates the matching PKEY_CTX from + // pkey_ and the EVP_MD_CTX takes ownership of it. + if (EVP_DigestVerifyInit(md_ctx.get(), nullptr, nullptr, nullptr, pkey_.get()) <= 0) { throw std::runtime_error("Failed to initialize verification: " + getOpenSSLError()); } - int result = EVP_DigestVerify(md_ctx, signature->data(), signature->size(), message->data(), message->size()); - - EVP_MD_CTX_free(md_ctx); + int result = EVP_DigestVerify(md_ctx.get(), signature->data(), signature->size(), message->data(), message->size()); if (result < 0) { throw std::runtime_error("Verification error: " + getOpenSSLError()); diff --git a/packages/react-native-quick-crypto/cpp/mlkem/HybridMlKemKeyPair.cpp b/packages/react-native-quick-crypto/cpp/mlkem/HybridMlKemKeyPair.cpp index deee182a..97d4cbb9 100644 --- a/packages/react-native-quick-crypto/cpp/mlkem/HybridMlKemKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/mlkem/HybridMlKemKeyPair.cpp @@ -15,6 +15,8 @@ namespace margelo::nitro::crypto { +using EVP_PKEY_CTX_ptr = std::unique_ptr; + void HybridMlKemKeyPair::setVariant(const std::string& variant) { #if !RNQC_HAS_ML_KEM throw std::runtime_error("ML-KEM requires OpenSSL 3.5+"); @@ -27,8 +29,9 @@ void HybridMlKemKeyPair::setVariant(const std::string& variant) { std::shared_ptr> HybridMlKemKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat, double privateType) { - return Promise::async([this, publicFormat, publicType, privateFormat, privateType]() { - this->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType); + auto self = this->shared_cast(); + return Promise::async([self, publicFormat, publicType, privateFormat, privateType]() { + self->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType); }); } @@ -49,24 +52,21 @@ void HybridMlKemKeyPair::generateKeyPairSync(double publicFormat, double publicT pkey_.reset(); - EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr); + EVP_PKEY_CTX_ptr pctx(EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr), EVP_PKEY_CTX_free); if (pctx == nullptr) { throw std::runtime_error("Failed to create key context for " + variant_ + ": " + getOpenSSLError()); } - if (EVP_PKEY_keygen_init(pctx) <= 0) { - EVP_PKEY_CTX_free(pctx); + if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { throw std::runtime_error("Failed to initialize keygen: " + getOpenSSLError()); } EVP_PKEY* raw = nullptr; - if (EVP_PKEY_keygen(pctx, &raw) <= 0) { - EVP_PKEY_CTX_free(pctx); + if (EVP_PKEY_keygen(pctx.get(), &raw) <= 0) { throw std::runtime_error("Failed to generate ML-KEM key pair: " + getOpenSSLError()); } pkey_.reset(raw); - EVP_PKEY_CTX_free(pctx); #endif } @@ -96,13 +96,14 @@ std::shared_ptr HybridMlKemKeyPair::getPublicKey() { BUF_MEM* bptr; BIO_get_mem_ptr(bio, &bptr); - uint8_t* data = new uint8_t[bptr->length]; - memcpy(data, bptr->data, bptr->length); size_t len = bptr->length; + auto buf = std::make_unique(len); + memcpy(buf.get(), bptr->data, len); BIO_free(bio); - return std::make_shared(data, len, [=]() { delete[] data; }); + uint8_t* raw_ptr = buf.get(); + return std::make_shared(buf.release(), len, [raw_ptr]() { delete[] raw_ptr; }); #endif } @@ -132,13 +133,16 @@ std::shared_ptr HybridMlKemKeyPair::getPrivateKey() { BUF_MEM* bptr; BIO_get_mem_ptr(bio, &bptr); - uint8_t* data = new uint8_t[bptr->length]; - memcpy(data, bptr->data, bptr->length); size_t len = bptr->length; + auto buf = std::make_unique(len); + memcpy(buf.get(), bptr->data, len); + // Wipe the private key bytes from the BIO before freeing. + secureZero(bptr->data, bptr->length); BIO_free(bio); - return std::make_shared(data, len, [=]() { delete[] data; }); + uint8_t* raw_ptr = buf.get(); + return std::make_shared(buf.release(), len, [raw_ptr]() { delete[] raw_ptr; }); #endif } @@ -213,7 +217,8 @@ void HybridMlKemKeyPair::setPrivateKey(const std::shared_ptr& keyDa } std::shared_ptr>> HybridMlKemKeyPair::encapsulate() { - return Promise>::async([this]() { return this->encapsulateSync(); }); + auto self = this->shared_cast(); + return Promise>::async([self]() { return self->encapsulateSync(); }); } std::shared_ptr HybridMlKemKeyPair::encapsulateSync() { @@ -223,51 +228,47 @@ std::shared_ptr HybridMlKemKeyPair::encapsulateSync() { clearOpenSSLErrors(); checkKeyPair(); - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey_.get(), nullptr); + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new(pkey_.get(), nullptr), EVP_PKEY_CTX_free); if (ctx == nullptr) { throw std::runtime_error("Failed to create encapsulation context: " + getOpenSSLError()); } - if (EVP_PKEY_encapsulate_init(ctx, nullptr) <= 0) { - EVP_PKEY_CTX_free(ctx); + if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) { throw std::runtime_error("Failed to initialize encapsulation: " + getOpenSSLError()); } size_t ct_len = 0; size_t sk_len = 0; - if (EVP_PKEY_encapsulate(ctx, nullptr, &ct_len, nullptr, &sk_len) <= 0) { - EVP_PKEY_CTX_free(ctx); + if (EVP_PKEY_encapsulate(ctx.get(), nullptr, &ct_len, nullptr, &sk_len) <= 0) { throw std::runtime_error("Failed to determine encapsulation output sizes: " + getOpenSSLError()); } // Pack result as: [uint32 ct_len][uint32 sk_len][ciphertext][shared_key] size_t header_size = sizeof(uint32_t) * 2; size_t total_size = header_size + ct_len + sk_len; - uint8_t* out = new uint8_t[total_size]; + auto out = std::make_unique(total_size); uint32_t ct_len_u32 = static_cast(ct_len); uint32_t sk_len_u32 = static_cast(sk_len); - memcpy(out, &ct_len_u32, sizeof(uint32_t)); - memcpy(out + sizeof(uint32_t), &sk_len_u32, sizeof(uint32_t)); + memcpy(out.get(), &ct_len_u32, sizeof(uint32_t)); + memcpy(out.get() + sizeof(uint32_t), &sk_len_u32, sizeof(uint32_t)); - uint8_t* ct_data = out + header_size; + uint8_t* ct_data = out.get() + header_size; uint8_t* sk_data = ct_data + ct_len; - if (EVP_PKEY_encapsulate(ctx, ct_data, &ct_len, sk_data, &sk_len) <= 0) { - EVP_PKEY_CTX_free(ctx); - delete[] out; + if (EVP_PKEY_encapsulate(ctx.get(), ct_data, &ct_len, sk_data, &sk_len) <= 0) { throw std::runtime_error("Failed to encapsulate: " + getOpenSSLError()); } - EVP_PKEY_CTX_free(ctx); - - return std::make_shared(out, total_size, [=]() { delete[] out; }); + uint8_t* raw_ptr = out.get(); + return std::make_shared(out.release(), total_size, [raw_ptr]() { delete[] raw_ptr; }); #endif } std::shared_ptr>> HybridMlKemKeyPair::decapsulate(const std::shared_ptr& ciphertext) { auto nativeCiphertext = ToNativeArrayBuffer(ciphertext); - return Promise>::async([this, nativeCiphertext]() { return this->decapsulateSync(nativeCiphertext); }); + auto self = this->shared_cast(); + return Promise>::async([self, nativeCiphertext]() { return self->decapsulateSync(nativeCiphertext); }); } std::shared_ptr HybridMlKemKeyPair::decapsulateSync(const std::shared_ptr& ciphertext) { @@ -277,13 +278,12 @@ std::shared_ptr HybridMlKemKeyPair::decapsulateSync(const std::shar clearOpenSSLErrors(); checkKeyPair(); - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey_.get(), nullptr); + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new(pkey_.get(), nullptr), EVP_PKEY_CTX_free); if (ctx == nullptr) { throw std::runtime_error("Failed to create decapsulation context: " + getOpenSSLError()); } - if (EVP_PKEY_decapsulate_init(ctx, nullptr) <= 0) { - EVP_PKEY_CTX_free(ctx); + if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) { throw std::runtime_error("Failed to initialize decapsulation: " + getOpenSSLError()); } @@ -291,22 +291,18 @@ std::shared_ptr HybridMlKemKeyPair::decapsulateSync(const std::shar size_t ct_size = ciphertext->size(); size_t sk_len = 0; - if (EVP_PKEY_decapsulate(ctx, nullptr, &sk_len, ct_data, ct_size) <= 0) { - EVP_PKEY_CTX_free(ctx); + if (EVP_PKEY_decapsulate(ctx.get(), nullptr, &sk_len, ct_data, ct_size) <= 0) { throw std::runtime_error("Failed to determine shared key size: " + getOpenSSLError()); } - uint8_t* sk_data = new uint8_t[sk_len]; + auto sk_buf = std::make_unique(sk_len); - if (EVP_PKEY_decapsulate(ctx, sk_data, &sk_len, ct_data, ct_size) <= 0) { - EVP_PKEY_CTX_free(ctx); - delete[] sk_data; + if (EVP_PKEY_decapsulate(ctx.get(), sk_buf.get(), &sk_len, ct_data, ct_size) <= 0) { throw std::runtime_error("Failed to decapsulate: " + getOpenSSLError()); } - EVP_PKEY_CTX_free(ctx); - - return std::make_shared(sk_data, sk_len, [=]() { delete[] sk_data; }); + uint8_t* raw_ptr = sk_buf.get(); + return std::make_shared(sk_buf.release(), sk_len, [raw_ptr]() { delete[] raw_ptr; }); #endif } diff --git a/packages/react-native-quick-crypto/cpp/pbkdf2/HybridPbkdf2.cpp b/packages/react-native-quick-crypto/cpp/pbkdf2/HybridPbkdf2.cpp index 78661718..ff51539b 100644 --- a/packages/react-native-quick-crypto/cpp/pbkdf2/HybridPbkdf2.cpp +++ b/packages/react-native-quick-crypto/cpp/pbkdf2/HybridPbkdf2.cpp @@ -19,19 +19,18 @@ std::shared_ptr HybridPbkdf2::pbkdf2Sync(const std::shared_ptr& salt, double iterations, double keylen, const std::string& digest) { size_t bufferSize = static_cast(keylen); - uint8_t* data = new uint8_t[bufferSize]; - auto result = std::make_shared(data, bufferSize, [=]() { delete[] data; }); + auto out_buf = std::make_unique(bufferSize); // use fastpbkdf2 when possible if (digest == "sha1") { fastpbkdf2_hmac_sha1(password.get()->data(), password.get()->size(), salt.get()->data(), salt.get()->size(), - static_cast(iterations), result.get()->data(), result.get()->size()); + static_cast(iterations), out_buf.get(), bufferSize); } else if (digest == "sha256") { fastpbkdf2_hmac_sha256(password.get()->data(), password.get()->size(), salt.get()->data(), salt.get()->size(), - static_cast(iterations), result.get()->data(), result.get()->size()); + static_cast(iterations), out_buf.get(), bufferSize); } else if (digest == "sha512") { fastpbkdf2_hmac_sha512(password.get()->data(), password.get()->size(), salt.get()->data(), salt.get()->size(), - static_cast(iterations), result.get()->data(), result.get()->size()); + static_cast(iterations), out_buf.get(), bufferSize); } else { // fallback to OpenSSL auto* digestByName = EVP_get_digestbyname(digest.c_str()); @@ -40,12 +39,12 @@ std::shared_ptr HybridPbkdf2::pbkdf2Sync(const std::shared_ptr(password.get()->data()); const unsigned char* saltAsCharA = reinterpret_cast(salt.get()->data()); - unsigned char* resultAsCharA = reinterpret_cast(result.get()->data()); PKCS5_PBKDF2_HMAC(passAsCharA, password.get()->size(), saltAsCharA, salt.get()->size(), static_cast(iterations), digestByName, - result.get()->size(), resultAsCharA); + bufferSize, out_buf.get()); } - return result; + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), bufferSize, [raw_ptr]() { delete[] raw_ptr; }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp index 959cb54a..3c11f7e9 100644 --- a/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp +++ b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.cpp @@ -21,10 +21,7 @@ std::shared_ptr> HybridRsaKeyPair::generateKeyPair() { void HybridRsaKeyPair::generateKeyPairSync() { // Clean up existing key if any - if (this->pkey != nullptr) { - EVP_PKEY_free(this->pkey); - this->pkey = nullptr; - } + this->pkey_.reset(); // Create key generation context std::unique_ptr ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr), EVP_PKEY_CTX_free); @@ -69,7 +66,7 @@ void HybridRsaKeyPair::generateKeyPairSync() { throw std::runtime_error("Failed to generate RSA key pair"); } - this->pkey = raw_pkey; + this->pkey_.reset(raw_pkey); } void HybridRsaKeyPair::setModulusLength(double modulusLength) { @@ -96,7 +93,7 @@ std::shared_ptr HybridRsaKeyPair::getPublicKey() { throw std::runtime_error("Failed to create BIO for public key export"); } - if (i2d_PUBKEY_bio(bio, this->pkey) != 1) { + if (i2d_PUBKEY_bio(bio, this->pkey_.get()) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export public key to DER format"); } @@ -120,7 +117,7 @@ std::shared_ptr HybridRsaKeyPair::getPrivateKey() { throw std::runtime_error("Failed to create BIO for private key export"); } - if (i2d_PKCS8PrivateKey_bio(bio, this->pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + if (i2d_PKCS8PrivateKey_bio(bio, this->pkey_.get(), nullptr, nullptr, 0, nullptr, nullptr) != 1) { BIO_free(bio); throw std::runtime_error("Failed to export private key to DER PKCS8 format"); } @@ -146,7 +143,7 @@ std::shared_ptr HybridRsaKeyPair::exportKey(const KeyObject& /* key } void HybridRsaKeyPair::checkKeyPair() { - if (this->pkey == nullptr) { + if (!this->pkey_) { throw std::runtime_error("RSA KeyPair not initialized"); } } diff --git a/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp index f56f4e69..2e96f697 100644 --- a/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp +++ b/packages/react-native-quick-crypto/cpp/rsa/HybridRsaKeyPair.hpp @@ -13,12 +13,8 @@ namespace margelo::nitro::crypto { class HybridRsaKeyPair : public HybridRsaKeyPairSpec { public: - HybridRsaKeyPair() : HybridObject(TAG), pkey(nullptr), modulusLength(2048), hashAlgorithm("SHA-256") {} - ~HybridRsaKeyPair() { - if (pkey) { - EVP_PKEY_free(pkey); - } - } + HybridRsaKeyPair() : HybridObject(TAG), modulusLength(2048), hashAlgorithm("SHA-256") {} + ~HybridRsaKeyPair() override = default; std::shared_ptr> generateKeyPair() override; void generateKeyPairSync() override; @@ -32,7 +28,8 @@ class HybridRsaKeyPair : public HybridRsaKeyPairSpec { std::shared_ptr exportKey(const KeyObject& key, const std::string& format) override; private: - EVP_PKEY* pkey; + using EVP_PKEY_ptr = std::unique_ptr; + EVP_PKEY_ptr pkey_{nullptr, EVP_PKEY_free}; int modulusLength; std::vector publicExponent; std::string hashAlgorithm; diff --git a/packages/react-native-quick-crypto/cpp/scrypt/HybridScrypt.cpp b/packages/react-native-quick-crypto/cpp/scrypt/HybridScrypt.cpp index a9c853e0..ea9eff11 100644 --- a/packages/react-native-quick-crypto/cpp/scrypt/HybridScrypt.cpp +++ b/packages/react-native-quick-crypto/cpp/scrypt/HybridScrypt.cpp @@ -46,17 +46,19 @@ std::shared_ptr HybridScrypt::deriveKeySync(const std::shared_ptrsize() : 0; // Allocate output buffer - uint8_t* outBuf = new uint8_t[outLen]; + auto out_buf = std::make_unique(outLen); // Use EVP_PBE_scrypt - the same API Node.js uses - int result = EVP_PBE_scrypt(pass_data, pass_len, salt_data, salt_len, n_val, r_val, p_val, maxmem_val, outBuf, outLen); + int result = EVP_PBE_scrypt(pass_data, pass_len, salt_data, salt_len, n_val, r_val, p_val, maxmem_val, out_buf.get(), outLen); if (result != 1) { - delete[] outBuf; + // Zero any partially-derived secret bits before unique_ptr frees the buffer. + secureZero(out_buf.get(), outLen); throw std::runtime_error("SCRYPT derivation failed: " + getOpenSSLError()); } - return std::make_shared(outBuf, outLen, [=]() { delete[] outBuf; }); + uint8_t* raw_ptr = out_buf.get(); + return std::make_shared(out_buf.release(), outLen, [raw_ptr]() { delete[] raw_ptr; }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp index e48f159d..ff8199db 100644 --- a/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/sign/HybridSignHandle.cpp @@ -86,35 +86,13 @@ static bool isOneShotVariant(EVP_PKEY* pkey) { #endif } -// Get the algorithm name for creating PKEY_CTX (for ML-DSA variants) -static const char* getAlgorithmName(EVP_PKEY* pkey) { - int type = EVP_PKEY_id(pkey); -#if RNQC_HAS_ML_DSA - switch (type) { - case EVP_PKEY_ML_DSA_44: - return "ML-DSA-44"; - case EVP_PKEY_ML_DSA_65: - return "ML-DSA-65"; - case EVP_PKEY_ML_DSA_87: - return "ML-DSA-87"; - case EVP_PKEY_ED25519: - return "ED25519"; - case EVP_PKEY_ED448: - return "ED448"; - default: - return nullptr; - } -#else - switch (type) { - case EVP_PKEY_ED25519: - return "ED25519"; - case EVP_PKEY_ED448: - return "ED448"; - default: - return nullptr; - } -#endif -} +// RAII owners for short-lived OpenSSL handles used in this method. EVP_MD_CTX +// transitively owns its EVP_PKEY_CTX after a successful EVP_DigestSignInit, so +// we deliberately rely on EVP_MD_CTX_free to clean both up; the standalone +// EvpPkeyCtxPtr alias is kept only for the RSA/ECDSA branch, where we +// allocate the PKEY_CTX directly via EVP_PKEY_CTX_new. +using EvpMdCtxPtr = std::unique_ptr; +using EvpPkeyCtxPtr = std::unique_ptr; std::shared_ptr HybridSignHandle::sign(const std::shared_ptr& keyHandle, std::optional padding, std::optional saltLength, @@ -139,28 +117,19 @@ std::shared_ptr HybridSignHandle::sign(const std::shared_ptr HybridSignHandle::sign(const std::shared_ptr(sig_len); - if (EVP_DigestSign(sign_ctx, sig_buf.get(), &sig_len, data_buffer.data(), data_buffer.size()) <= 0) { - EVP_MD_CTX_free(sign_ctx); + if (EVP_DigestSign(sign_ctx.get(), sig_buf.get(), &sig_len, data_buffer.data(), data_buffer.size()) <= 0) { unsigned long err = ERR_get_error(); char err_buf[256]; ERR_error_string_n(err, err_buf, sizeof(err_buf)); throw std::runtime_error("Failed to sign with Ed key: " + std::string(err_buf)); } - - EVP_MD_CTX_free(sign_ctx); } else { // Standard signing flow for RSA/ECDSA unsigned char digest[EVP_MAX_MD_SIZE]; @@ -195,13 +160,12 @@ std::shared_ptr HybridSignHandle::sign(const std::shared_ptr HybridSignHandle::sign(const std::shared_ptr(padding.value()); - if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, pad) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx.get(), pad) <= 0) { throw std::runtime_error("Failed to set RSA padding"); } } if (saltLength.has_value() && padding.has_value() && static_cast(padding.value()) == RSA_PKCS1_PSS_PADDING) { int salt_len = static_cast(saltLength.value()); - if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_len) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx.get(), salt_len) <= 0) { throw std::runtime_error("Failed to set PSS salt length"); } } - if (EVP_PKEY_CTX_set_signature_md(pkey_ctx, md) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_CTX_set_signature_md(pkey_ctx.get(), md) <= 0) { throw std::runtime_error("Failed to set signature digest"); } - if (EVP_PKEY_sign(pkey_ctx, nullptr, &sig_len, digest, digest_len) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_sign(pkey_ctx.get(), nullptr, &sig_len, digest, digest_len) <= 0) { throw std::runtime_error("Failed to determine signature length"); } sig_buf = std::make_unique(sig_len); - if (EVP_PKEY_sign(pkey_ctx, sig_buf.get(), &sig_len, digest, digest_len) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_sign(pkey_ctx.get(), sig_buf.get(), &sig_len, digest, digest_len) <= 0) { unsigned long err = ERR_get_error(); char err_buf[256]; ERR_error_string_n(err, err_buf, sizeof(err_buf)); throw std::runtime_error("Failed to sign: " + std::string(err_buf)); } - - EVP_PKEY_CTX_free(pkey_ctx); } int dsa_enc = dsaEncoding.has_value() ? static_cast(dsaEncoding.value()) : kSigEncDER; diff --git a/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp index 3052ac5b..75e18e67 100644 --- a/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp +++ b/packages/react-native-quick-crypto/cpp/sign/HybridVerifyHandle.cpp @@ -86,35 +86,13 @@ static bool isOneShotVariant(EVP_PKEY* pkey) { #endif } -// Get the algorithm name for creating PKEY_CTX (for ML-DSA variants) -static const char* getAlgorithmName(EVP_PKEY* pkey) { - int type = EVP_PKEY_id(pkey); -#if RNQC_HAS_ML_DSA - switch (type) { - case EVP_PKEY_ML_DSA_44: - return "ML-DSA-44"; - case EVP_PKEY_ML_DSA_65: - return "ML-DSA-65"; - case EVP_PKEY_ML_DSA_87: - return "ML-DSA-87"; - case EVP_PKEY_ED25519: - return "ED25519"; - case EVP_PKEY_ED448: - return "ED448"; - default: - return nullptr; - } -#else - switch (type) { - case EVP_PKEY_ED25519: - return "ED25519"; - case EVP_PKEY_ED448: - return "ED448"; - default: - return nullptr; - } -#endif -} +// RAII owners for short-lived OpenSSL handles used in this method. EVP_MD_CTX +// transitively owns its EVP_PKEY_CTX after a successful EVP_DigestVerifyInit, +// so we deliberately rely on EVP_MD_CTX_free to clean both up; the standalone +// EvpPkeyCtxPtr alias is kept only for the RSA/ECDSA branch, where we +// allocate the PKEY_CTX directly via EVP_PKEY_CTX_new. +using EvpMdCtxPtr = std::unique_ptr; +using EvpPkeyCtxPtr = std::unique_ptr; bool HybridVerifyHandle::verify(const std::shared_ptr& keyHandle, const std::shared_ptr& signature, std::optional padding, std::optional saltLength, std::optional dsaEncoding) { @@ -136,32 +114,23 @@ bool HybridVerifyHandle::verify(const std::shared_ptr // Ed25519/Ed448/ML-DSA require one-shot verification with EVP_DigestVerify // Also use one-shot path if no digest was specified (md == nullptr) if (isOneShotVariant(pkey) || md == nullptr) { - EVP_MD_CTX* verify_ctx = EVP_MD_CTX_new(); + EvpMdCtxPtr verify_ctx{EVP_MD_CTX_new(), EVP_MD_CTX_free}; if (!verify_ctx) { throw std::runtime_error("Failed to create verification context"); } - // Get algorithm name and create PKEY_CTX for ML-DSA - const char* alg_name = getAlgorithmName(pkey); - EVP_PKEY_CTX* pkey_ctx = nullptr; - if (alg_name != nullptr) { - pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, alg_name, nullptr); - if (!pkey_ctx) { - EVP_MD_CTX_free(verify_ctx); - throw std::runtime_error(std::string("Failed to create verification context for ") + alg_name); - } - } - - // Initialize for one-shot verification (pass nullptr for md - these algorithms have built-in hash) - if (EVP_DigestVerifyInit(verify_ctx, pkey_ctx ? &pkey_ctx : nullptr, nullptr, nullptr, pkey) <= 0) { - EVP_MD_CTX_free(verify_ctx); - if (pkey_ctx) - EVP_PKEY_CTX_free(pkey_ctx); + // Let OpenSSL allocate the PKEY_CTX from the key's keymgmt. On success the + // EVP_MD_CTX assumes ownership and EVP_MD_CTX_free will dispose it; on + // failure pkey_ctx_raw stays nullptr, so there is nothing to leak. This + // mirrors ncrypto's EVPMDCtxPointer::verifyInit (Node.js deps/ncrypto/ncrypto.cc + // and ~/dev/ncrypto/src/ncrypto.cpp), which works for RSA, ECDSA, Ed25519, + // Ed448 and ML-DSA without any algorithm-name pre-creation. + EVP_PKEY_CTX* pkey_ctx_raw = nullptr; + if (EVP_DigestVerifyInit(verify_ctx.get(), &pkey_ctx_raw, nullptr, nullptr, pkey) <= 0) { throw std::runtime_error("Failed to initialize one-shot verification"); } - int result = EVP_DigestVerify(verify_ctx, sig_data, sig_len, data_buffer.data(), data_buffer.size()); - EVP_MD_CTX_free(verify_ctx); + int result = EVP_DigestVerify(verify_ctx.get(), sig_data, sig_len, data_buffer.data(), data_buffer.size()); return result == 1; } @@ -187,40 +156,34 @@ bool HybridVerifyHandle::verify(const std::shared_ptr } } - EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new(pkey, nullptr); + EvpPkeyCtxPtr pkey_ctx{EVP_PKEY_CTX_new(pkey, nullptr), EVP_PKEY_CTX_free}; if (!pkey_ctx) { throw std::runtime_error("Failed to create verification context"); } - if (EVP_PKEY_verify_init(pkey_ctx) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_verify_init(pkey_ctx.get()) <= 0) { throw std::runtime_error("Failed to initialize verification"); } if (padding.has_value()) { int pad = static_cast(padding.value()); - if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, pad) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx.get(), pad) <= 0) { throw std::runtime_error("Failed to set RSA padding"); } } if (saltLength.has_value() && padding.has_value() && static_cast(padding.value()) == RSA_PKCS1_PSS_PADDING) { int salt_len = static_cast(saltLength.value()); - if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_len) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx.get(), salt_len) <= 0) { throw std::runtime_error("Failed to set PSS salt length"); } } - if (EVP_PKEY_CTX_set_signature_md(pkey_ctx, md) <= 0) { - EVP_PKEY_CTX_free(pkey_ctx); + if (EVP_PKEY_CTX_set_signature_md(pkey_ctx.get(), md) <= 0) { throw std::runtime_error("Failed to set signature digest"); } - int result = EVP_PKEY_verify(pkey_ctx, sig_data, sig_len, digest, digest_len); - EVP_PKEY_CTX_free(pkey_ctx); - + int result = EVP_PKEY_verify(pkey_ctx.get(), sig_data, sig_len, digest, digest_len); return result == 1; } diff --git a/plans/todo/security-audit.md b/plans/todo/security-audit.md index f442ff4c..d2d9ac44 100644 --- a/plans/todo/security-audit.md +++ b/plans/todo/security-audit.md @@ -1203,20 +1203,20 @@ Once these helpers exist the bulk Phase 2/3 sweep just consumes them. | # | Status | Helper | Closes | | --- | ------ | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1.1 | [ ] | `validateDouble()` — reject NaN/Inf/negative/non-integer at JS↔C++ boundary | ~20 findings (Hash, HMAC, KMAC, BLAKE3, all KDFs, RSA, ML-DSA, AES-CCM) | -| 1.2 | [ ] | `secureZero()` — `OPENSSL_cleanse` / `sodium_memzero` wrapper | XSalsa20, XChaCha20-Poly1305, all KDFs, DH/ECDH, RSA/EC/Ed/DSA DER strings | -| 1.3 | [ ] | `EVP_CIPHER_CTX` `unique_ptr` in `HybridCipher` base | CCMCipher, ChaCha20, ChaCha20-Poly1305 destructor leaks | -| 1.4 | [ ] | Replace `Record` `getUIntOption` with typed helper | Cross-cutting cipher options | +| 1.1 | [x] | `validateUInt()` — reject NaN/Inf/negative/non-integer at JS↔C++ boundary | ~20 findings (Hash, HMAC, KMAC, BLAKE3, all KDFs, RSA, ML-DSA, AES-CCM) | +| 1.2 | [x] | `secureZero()` — `OPENSSL_cleanse` / `sodium_memzero` wrapper | XSalsa20, XChaCha20-Poly1305, all KDFs, DH/ECDH, RSA/EC/Ed/DSA DER strings | +| 1.3 | [x] | `EVP_CIPHER_CTX` `unique_ptr` in `HybridCipher` base | CCMCipher, ChaCha20, ChaCha20-Poly1305 destructor leaks | +| 1.4 | [x] | Replace `Record` `getUIntOption` with typed helper | Cross-cutting cipher options | ### Phase 2 — Memory Safety Sweep Depends on Phase 1. -- [ ] Raw `new uint8_t[]` → `std::unique_ptr` (Hash, HMAC, KMAC, BLAKE3, all KDFs, all ciphers' `update()`) -- [ ] Raw `EVP_PKEY*` → smart pointers (RSA, EC, Ed, Sign/Verify, ML-DSA, ML-KEM — DSA pattern as template) -- [ ] `Promise::async` raw `this` capture → `shared_from_this` (ML-DSA, ML-KEM, DH) -- [ ] `EVP_PKEY_CTX` double-free in `EVP_DigestSignInit` paths (Sign/Verify, Ed25519, ML-DSA) -- [ ] Ed25519 thread-unsafe `ERR_error_string(.., NULL)` → `ERR_error_string_n` +- [x] Raw `new uint8_t[]` → `std::unique_ptr` (Hash, HMAC, KMAC, BLAKE3, all KDFs, all ciphers' `update()`) +- [x] Raw `EVP_PKEY*` → smart pointers (RSA, EC, Ed, Sign/Verify, ML-DSA, ML-KEM — DSA pattern as template) +- [x] `Promise::async` raw `this` capture → `shared_from_this` (ML-DSA, ML-KEM; DH had no async sites) +- [x] `EVP_PKEY_CTX` double-free in `EVP_DigestSignInit` paths (Sign/Verify, Ed25519, ML-DSA) +- [x] Ed25519 thread-unsafe `ERR_error_string(.., NULL)` → `ERR_error_string_n` ### Phase 3 — TypeScript Boundary Validation @@ -1255,3 +1255,5 @@ _Append entries as PRs land. Format: `YYYY-MM-DD — [phase.task] description (P - 2026-04-26 — [0.2] Fix XSalsa20 keystream restart on every `update()`. Replace `crypto_stream_xor` with `crypto_stream_xsalsa20_xor_ic` plus per-instance `block_counter` + 64-byte `leftover_keystream` so the keystream advances correctly across chunked update() calls. Output now uses `unique_ptr` for exception safety on the failure path. Adds 6 streaming regression tests covering block-aligned splits, mid-block splits, many-small-chunk splits, drain-to-boundary continuation, the catastrophic two-time-pad regression (identical plaintext in two updates → distinct ciphertexts), and a streaming round-trip across encrypt + decrypt instances. Independent crypto-specialist review approved correctness, exception safety, and re-init isolation. (branch: `feat/security-audit`, PR: TBD) - 2026-04-26 — [0.3] Add explicit peer-public-key validation in `DiffieHellman::computeSecret` and `ECDH::computeSecret`. DH path calls `DH_check_pub_key` (matching ncrypto's `DHPointer::checkPublicKey`) and distinguishes TOO_SMALL / TOO_LARGE / INVALID error codes, closing the small-subgroup attack on peer pubkeys 0, 1, p-1, and p. ECDH path calls `EC_POINT_oct2point` → `EC_POINT_is_at_infinity` → `EC_POINT_is_on_curve` against the configured group, closing the invalid-curve attack (peer point on a related weaker curve). Adds 4 DH and 5 ECDH regression tests covering each rejection path plus a cross-curve attack (P-384 pubkey sent to a P-256 instance) and a bit-flipped-coordinate test. Crypto-specialist review approved both fixes; flagged that the q-less subgroup gap for caller-supplied DH primes matches Node.js behavior and is not a regression. (branch: `feat/security-audit`, PR: TBD) - 2026-04-26 — [0.4] Close the RSA PKCS#1 v1.5 Bleichenbacher oracle. `HybridRsaCipher` now (1) enables OpenSSL 3.2+ implicit rejection (`EVP_PKEY_CTX_ctrl_str(ctx, "rsa_pkcs1_implicit_rejection", "1")`) for every PKCS#1 v1.5 decryption — corrupted ciphertexts deterministically decrypt to random-looking bytes instead of throwing — and (2) routes every decrypt-failure path in `decrypt`, `privateDecrypt`, and `publicDecrypt` (verify-recover) through a single `throwOpaqueDecryptFailure()` helper that emits the same `"RSA decryption failed"` message and clears the OpenSSL error stack so the underlying reason never reaches the caller. The TS wrapper drops the `: ${error.message}` interpolation in `privateDecrypt`/`publicDecrypt`. If the OpenSSL build does not support the implicit-rejection knob (BoringSSL or pre-3.2) we hard-fail PKCS#1 v1.5 decryption with a build-config error rather than silently leaving the timing-side oracle open — matches Node.js's `crypto_cipher.cc` policy. Adds 5 regression tests: corrupted PKCS#1 v1.5 doesn't throw, the implicit-rejection output is deterministic per (key, ciphertext) and distinct across different ciphertexts, OAEP/wrong-label errors are opaque (no "openssl/padding/oaep/label" terms in the message), OAEP and PKCS#1 wrong-padding errors are equivalent, and `publicDecrypt` errors are opaque. Crypto-specialist review confirmed the fix is closer to Node-compat than the previous behavior and approved the hard-fail fallback. (branch: `feat/security-audit`, PR: TBD) +- 2026-04-26 — [1.1–1.4] Phase 1 shared foundation: add `validateUInt()`, `secureZero()` overloads, `EVP_CIPHER_CTX` RAII in the cipher base, and a typed `getUIntOption` helper to `cpp/utils/QuickCryptoUtils.hpp` and `src/utils/cipher.ts`. Sweeps the cipher base + GCM/CCM/ChaCha20/ChaCha20-Poly1305/OCB/XSalsa20-Poly1305 to consume the new RAII context. Adds Argon2/cipher boundary tests. (PR #983) +- 2026-04-26 — [2.1–2.5] Phase 2 memory safety sweep across 24 C++ files (+327/−480 lines net). Item 2.1: convert raw `new uint8_t[]` to `std::unique_ptr` + `release()` into `NativeArrayBuffer` in Hash, HMAC, KMAC, BLAKE3, PBKDF2, Scrypt, HKDF, the cipher base `update()`, ChaCha20/ChaCha20-Poly1305/XChaCha20-Poly1305/XSalsa20-Poly1305, CCM `final()`, RSA-cipher decrypt sentinels, Ed25519 (6 sites), ML-DSA (3 sites), and ML-KEM (4 sites). Item 2.2: replace raw `EVP_PKEY*` ownership with `std::unique_ptr` in RSA, EC, and Ed25519 keypair classes (DSA pattern as template); `Ed25519::importPublicKey`/`importPrivateKey` now return owning `EVP_PKEY_ptr` and use `EVP_PKEY_up_ref` for the borrow-the-instance-key path, closing the audit-flagged leak. Item 2.3: replace `Promise<…>::async([this, …])` with `auto self = this->shared_cast<…>(); [self, …]` in ML-DSA (3 sites) and ML-KEM (3 sites); DH had no async sites despite the audit listing. Item 2.4: eliminate the unnecessary `EVP_PKEY_CTX_new_from_name` pre-creation in Sign/Verify handles, ML-DSA, and Ed25519 — pass `nullptr` for the `EVP_PKEY_CTX**` arg and let `EVP_DigestSignInit` allocate from the key's keymgmt (matches `ncrypto::EVPMDCtxPointer::signInit`). Crypto-specialist review confirmed the old code was actually *leaking* the pre-allocated PKEY_CTX (OpenSSL silently overwrote the pointer on success), so this fix closes both the audited double-free *and* an unreported leak. Wraps EVP_MD_CTX/EVP_PKEY_CTX in local `unique_ptr` aliases so all manual error-path frees collapse. Item 2.5: replace Ed25519's two `ERR_error_string(ERR_get_error(), NULL)` calls with the shared `getOpenSSLError()` helper. Defense-in-depth: `secureZero` added on Scrypt/HKDF error paths and on Ed25519/ML-DSA/ML-KEM `getPrivateKey` BIO buffers. Crypto-specialist approved all four substantive concerns (algorithm selection unchanged, refcount semantics correct, BIO secure-zero is safe redundancy with `BUF_MEM_free`'s `OPENSSL_clear_free`, `release()` + `make_shared` window matches Nitro's own `ArrayBuffer::wrap`). (branch: `feat/security-audit-phase-2`, PR: TBD) From 98f1d48fbce87ec0af5f2adc51a828c1ac217dad Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sun, 26 Apr 2026 23:25:44 -0400 Subject: [PATCH 2/3] fix(ci): correct workspace paths in E2E workflow filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The e2e-android-test.yml and e2e-ios-test.yml workflows referenced 'cpp/**', 'nitrogen/**', and 'src/**' at repo root — directories that no longer exist after the workspace migration to 'packages/react-native-quick-crypto/'. Result: every C++-only PR silently skipped both E2E suites (PR #982 Phase 0, PR #983 Phase 1, and PR #984 Phase 2 all hit this). Updates both pull_request and push path filters to point at the workspace locations. Each workflow file is itself in its own paths filter, so this commit triggers the workflows on PR #984 to run for the first time on this branch's C++ changes. --- .github/workflows/e2e-android-test.yml | 12 ++++++------ .github/workflows/e2e-ios-test.yml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-android-test.yml b/.github/workflows/e2e-android-test.yml index 41d648e5..b3ea5345 100644 --- a/.github/workflows/e2e-android-test.yml +++ b/.github/workflows/e2e-android-test.yml @@ -10,17 +10,17 @@ on: paths: - '.github/workflows/e2e-android-test.yml' - 'example/**' - - 'cpp/**' - - 'nitrogen/**' - - 'src/**' + - 'packages/react-native-quick-crypto/cpp/**' + - 'packages/react-native-quick-crypto/nitrogen/**' + - 'packages/react-native-quick-crypto/src/**' - 'packages/react-native-quick-crypto/android/**' push: branches: [main] paths: - 'example/**' - - 'cpp/**' - - 'nitrogen/**' - - 'src/**' + - 'packages/react-native-quick-crypto/cpp/**' + - 'packages/react-native-quick-crypto/nitrogen/**' + - 'packages/react-native-quick-crypto/src/**' - 'packages/react-native-quick-crypto/android/**' env: diff --git a/.github/workflows/e2e-ios-test.yml b/.github/workflows/e2e-ios-test.yml index 3c794141..631f3aa9 100644 --- a/.github/workflows/e2e-ios-test.yml +++ b/.github/workflows/e2e-ios-test.yml @@ -10,17 +10,17 @@ on: paths: - '.github/workflows/e2e-ios-test.yml' - 'example/**' - - 'cpp/**' - - 'nitrogen/**' - - 'src/**' + - 'packages/react-native-quick-crypto/cpp/**' + - 'packages/react-native-quick-crypto/nitrogen/**' + - 'packages/react-native-quick-crypto/src/**' - 'packages/react-native-quick-crypto/ios/**' push: branches: [main] paths: - 'example/**' - - 'cpp/**' - - 'nitrogen/**' - - 'src/**' + - 'packages/react-native-quick-crypto/cpp/**' + - 'packages/react-native-quick-crypto/nitrogen/**' + - 'packages/react-native-quick-crypto/src/**' - 'packages/react-native-quick-crypto/ios/**' jobs: From e19daa1ef4e0b6345944916a536e3772286aff5c Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Sun, 26 Apr 2026 23:26:39 -0400 Subject: [PATCH 3/3] =?UTF-8?q?docs(security-audit):=20add=20phase=20execu?= =?UTF-8?q?tion=20rules=20=E2=80=94=20commit=20per=20sub-section,=20gate?= =?UTF-8?q?=20push=20on=20local=20test=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three Phase Execution Rules to the Implementation Plan: 1. Commit after each sub-section (single numbered task or wave), not at the end of the phase. Preserves bisectability and review-ability. 2. Do not push or open a PR until the user has run the example app's tests locally and reported pass/fail. Pre-commit hooks cover lint / format / tsc / bob build only — they cannot exercise the native bridge. Tests live in example/src/tests/ and require the RN example app environment. 3. Fix path-filtered CI workflows on the same branch when they fail to trigger. Workflow files are in their own paths filters, so the fix re-triggers the run. --- plans/todo/security-audit.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plans/todo/security-audit.md b/plans/todo/security-audit.md index d2d9ac44..c6dbc572 100644 --- a/plans/todo/security-audit.md +++ b/plans/todo/security-audit.md @@ -1188,6 +1188,14 @@ The scan surfaces ~120 HIGH / ~180 MEDIUM / ~50 LOW findings across 32 modules, Status key: `[ ]` not started · `[~]` in progress · `[x]` complete +### Phase Execution Rules + +These rules apply to every phase below. Follow them on every multi-task phase, not just the first. + +1. **Commit after each sub-section, not at the end of the phase.** A "sub-section" is a single numbered task (e.g. 0.1, 1.2, 2.3) or a single logical wave inside a sweep (e.g. Wave 1A: digests). Each commit should be self-contained, reviewable, and revertable on its own. Do **not** batch a whole phase into one commit — that loses bisectability and makes review harder. +2. **Do NOT push or open a PR until the user has run the example app's tests locally and reported pass/fail.** Tests live in `example/src/tests/` and run inside the React Native example app, not under any Node.js test runner. Pre-commit hooks only cover lint, format, tsc, and bob build — they cannot exercise the native bridge. Wait for explicit user confirmation ("tests pass") before `git push -u` or `gh pr create`. If tests fail, fix in place on the local branch and re-request a test run. +3. **CI gate**: PRs must run the full CI matrix — `Validate C++`, `Validate JS`, `End-to-End Tests for Android`, `End-to-End Tests for iOS`. If a path-filtered workflow doesn't trigger on a C++-only or TS-only PR, fix the workflow's `paths:` filter and push that fix on the same branch (workflow files are in their own filters, so the change re-triggers the run). + ### Phase 0 — Stop the Bleeding (actively exploitable) | # | Status | Issue | Files | Closes |