From e7a69c499242d270df50a29d2bef5642aff03c13 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 23 Aug 2026 14:07:56 +0200 Subject: [PATCH] crypto: discover ciphers from OpenSSL providers Enumerate usable ciphers and aliases from activated OpenSSL 3 providers instead of maintaining lists of provider-only algorithms. Skip numeric OID aliases and filter NULL, TLS composite, multiblock, and encrypt-then-MAC implementations that the Cipher APIs cannot use. Preserve the OpenSSL 1.1.1 and BoringSSL paths. Expose CBC-CTS, SM4-GCM, SM4-CCM, SM4-XTS, and additional AES key wrap implementations. Add `ctsMode` (CS1/CS2/CS3) and `xtsStandard` (GB/IEEE) options for selecting provider CTS and SM4-XTS variants. Keep ordinary cipher construction on the original binding and legacy lookup paths. Lazily cache successful provider fetches per Environment for string initialization and `getCipherInfo()`. Index entries by case-insensitive query, canonical, and alias names. Deduplicate owners by provider and canonical identity. Return borrowed pointers on warm hits. Use the shared process-wide FIPS-state generation to invalidate per-Environment cipher caches and refresh `getCiphers()` snapshots in the main thread and workers. Existing cipher contexts retain their implementation and can finish across a transition. Release provider owners before unloading worker addon DSOs. Enforce one-shot updates for CBC-CTS, AES key wrap, SIV/GCM-SIV, and CCM decryption. Reject finalization without required input or CCM tags, and defer authentication failures to `final()`. Document streaming and XTS data-unit constraints. Add known-answer vectors, option validation, provider round trips, cache, worker, snapshot, FIPS transition, and construction benchmark coverage. Fixes: https://github.com/nodejs/node/issues/43040 Fixes: https://github.com/nodejs/node/issues/64866 Refs: https://github.com/nodejs/node/issues/62982 Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65484 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli --- benchmark/crypto/create-cipheriv.js | 62 +++ deps/ncrypto/ncrypto.cc | 388 ++++++++++++++---- deps/ncrypto/ncrypto.h | 81 ++-- doc/api/crypto.md | 267 +++++++++--- lib/internal/crypto/cipher.js | 32 +- lib/internal/crypto/util.js | 3 +- src/crypto/crypto_aes.h | 30 +- src/crypto/crypto_chacha20_poly1305.cc | 2 +- src/crypto/crypto_cipher.cc | 96 +++-- src/crypto/crypto_cipher.h | 10 +- src/crypto/crypto_context.cc | 11 +- src/env.cc | 2 + src/env.h | 2 + test/addons/openssl-providers/providers.cjs | 80 +++- test/fixtures/aead-vectors.js | 32 ++ .../snapshot/crypto-provider-cipher-cache.js | 55 +++ test/parallel/test-crypto-aes-wrap.js | 143 +++++++ test/parallel/test-crypto-authenticated.js | 44 +- ...est-crypto-cipherbase-options-fast-path.js | 130 ++++++ test/parallel/test-crypto-cipheriv-cbc-cts.js | 123 ++++++ .../test-crypto-cipheriv-decipheriv.js | 79 ++++ test/parallel/test-crypto-cipheriv-xts.js | 71 ++++ test/parallel/test-crypto-getcipherinfo.js | 69 +++- ...t-crypto-provider-cipher-cache-snapshot.js | 28 ++ .../test-crypto-provider-cipher-cache.js | 183 +++++++++ typings/internalBinding/crypto.d.ts | 2 + 26 files changed, 1801 insertions(+), 224 deletions(-) create mode 100644 benchmark/crypto/create-cipheriv.js create mode 100644 test/fixtures/snapshot/crypto-provider-cipher-cache.js create mode 100644 test/parallel/test-crypto-cipherbase-options-fast-path.js create mode 100644 test/parallel/test-crypto-cipheriv-cbc-cts.js create mode 100644 test/parallel/test-crypto-cipheriv-xts.js create mode 100644 test/parallel/test-crypto-provider-cipher-cache-snapshot.js create mode 100644 test/parallel/test-crypto-provider-cipher-cache.js diff --git a/benchmark/crypto/create-cipheriv.js b/benchmark/crypto/create-cipheriv.js new file mode 100644 index 00000000000..7774e393b40 --- /dev/null +++ b/benchmark/crypto/create-cipheriv.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('node:assert'); +const { + createCipheriv, + createDecipheriv, + getCiphers, +} = require('node:crypto'); + +const configurations = { + 'aes-128-cbc': { keyLength: 16, ivLength: 16 }, + 'aes-128-gcm': { keyLength: 16, ivLength: 12 }, + 'aes-128-cbc-cts': { keyLength: 16, ivLength: 16 }, + 'aes-128-wrap-inv': { keyLength: 16, ivLength: 8 }, + 'aes128-wrap-inv': { + keyLength: 16, + ivLength: 8, + warmupCipher: 'aes-128-wrap-inv', + }, +}; + +const ciphers = ['aes-128-cbc', 'aes-128-gcm']; +const availableCiphers = new Set(getCiphers()); +for (const cipher of [ + 'aes-128-cbc-cts', + 'aes-128-wrap-inv', + 'aes128-wrap-inv', +]) { + if (availableCiphers.has(cipher)) { + ciphers.push(cipher); + } +} + +const bench = common.createBenchmark(main, { + n: [1e5], + cipher: ciphers, + operation: ['encrypt', 'decrypt'], +}); + +function main({ n, cipher, operation }) { + const { + keyLength, + ivLength, + warmupCipher = cipher, + } = configurations[cipher]; + const key = Buffer.alloc(keyLength); + const iv = Buffer.alloc(ivLength); + const results = new Array(n); + const method = operation === 'encrypt' ? createCipheriv : createDecipheriv; + + const warmup = method(warmupCipher, key, iv); + assert.strictEqual(typeof warmup, 'object'); + + bench.start(); + for (let i = 0; i < n; ++i) { + results[i] = method(cipher, key, iv); + } + bench.end(n); + + assert.strictEqual(typeof results[n - 1], 'object'); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 18dac63be72..d334d17c300 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -537,7 +537,7 @@ bool setFipsEnabled(bool enable, CryptoErrorList* errors) { #else const bool success = FIPS_mode_set(enable ? 1 : 0) == 1; #endif - if (isFipsEnabledRaw() != was_enabled) { + if (success && isFipsEnabledRaw() != was_enabled) { fips_state_generation.fetch_add(1, std::memory_order_release); } return success; @@ -4433,6 +4433,40 @@ constexpr char AsciiToLower(char c) { } #if NCRYPTO_USE_OPENSSL3_PROVIDER +constexpr auto kUnsupportedCipherFlags = + EVP_CIPH_FLAG_CIPHER_WITH_MAC | EVP_CIPH_FLAG_TLS1_1_MULTIBLOCK; + +bool HasUnsupportedCipherFlags(const EVP_CIPHER* cipher) { + return (EVP_CIPHER_get_flags(cipher) & kUnsupportedCipherFlags) != 0; +} + +bool IsSupportedLegacyCipher(const EVP_CIPHER* cipher) { + return cipher != nullptr && cipher != EVP_enc_null() && + !HasUnsupportedCipherFlags(cipher); +} + +bool IsSupportedFetchedCipher(const EVP_CIPHER* cipher) { + if (cipher == nullptr || EVP_CIPHER_is_a(cipher, "NULL") || + HasUnsupportedCipherFlags(cipher)) { + return false; + } + +#ifdef OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC + int encrypt_then_mac = 0; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC, + &encrypt_then_mac), + OSSL_PARAM_construct_end(), + }; + if (EVP_CIPHER_get_params(const_cast(cipher), params) == 1 && + encrypt_then_mac != 0) { + return false; + } +#endif + + return true; +} + void PushAlgorithmAlias(const char* name, void* arg) { if (name == nullptr) return; static_cast*>(arg)->emplace_back(name); @@ -4440,7 +4474,7 @@ void PushAlgorithmAlias(const char* name, void* arg) { #endif } // namespace -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER Cipher::Cipher(DeleteFnPtr cipher) : cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {} #endif @@ -4541,8 +4575,63 @@ const DigestCache::AliasMap& DigestCache::aliases() const { #endif } +const EVP_CIPHER* CipherCache::lookup(const char* name, uint64_t generation) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation) { + aliases_.clear(); + ciphers_.clear(); + generation_ = generation; + } + + const auto it = aliases_.find(name); + if (it == aliases_.end()) return nullptr; + if (it->second >= ciphers_.size()) return nullptr; + return ciphers_[it->second].get(); +#else + static_cast(name); + static_cast(generation); + return nullptr; +#endif +} + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +const EVP_CIPHER* CipherCache::insert( + const char* name, + DeleteFnPtr&& cipher, + uint64_t generation) { + if (generation_ != generation || cipher == nullptr) return nullptr; + + const char* canonical_name = EVP_CIPHER_get0_name(cipher.get()); + const OSSL_PROVIDER* provider = EVP_CIPHER_get0_provider(cipher.get()); + if (canonical_name != nullptr && provider != nullptr) { + for (size_t id = 0; id < ciphers_.size(); id++) { + const EVP_CIPHER* cached = ciphers_[id].get(); + const char* cached_name = EVP_CIPHER_get0_name(cached); + if (EVP_CIPHER_get0_provider(cached) == provider && + cached_name != nullptr && + CaseInsensitiveNameEqual()(cached_name, canonical_name)) { + aliases_.insert_or_assign(name, id); + return cached; + } + } + } + + ciphers_.emplace_back(std::move(cipher)); + const size_t id = ciphers_.size() - 1; + + std::vector aliases; + EVP_CIPHER_names_do_all(ciphers_[id].get(), PushAlgorithmAlias, &aliases); + for (const std::string& alias : aliases) { + aliases_.emplace(alias, id); + } + aliases_.insert_or_assign(name, id); + + return ciphers_[id].get(); +} +#endif + Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) { -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER if (other.fetched_cipher_ != nullptr) { if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) { fetched_cipher_.reset(other.fetched_cipher_.get()); @@ -4555,7 +4644,7 @@ Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) { Cipher& Cipher::operator=(const Cipher& other) { if (this == &other) return *this; -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER if (other.fetched_cipher_ != nullptr) { if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) { fetched_cipher_.reset(other.fetched_cipher_.get()); @@ -4572,40 +4661,59 @@ Cipher& Cipher::operator=(const Cipher& other) { return *this; } -const Cipher Cipher::FromName(const char* name) { +const Cipher Cipher::FromName(const char* name, CipherCache* cache) { const EVP_CIPHER* cipher = EVP_get_cipherbyname(name); - if (cipher != nullptr) return Cipher(cipher); + if (cipher != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!IsSupportedLegacyCipher(cipher)) return Cipher(); +#endif + return Cipher(cipher); + } + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // A resolution that overlaps a FIPS transition may use either property + // state. The cache retains the generation observed here, so the first + // resolution begun after the transition clears any stale entries. + const uint64_t generation = getFipsStateGeneration(); + if (cache != nullptr) { + if (const EVP_CIPHER* cached = cache->lookup(name, generation)) { + return Cipher(cached); + } + } -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV MarkPopErrorOnReturn mark_pop_error_on_return; DeleteFnPtr fetched( EVP_CIPHER_fetch(nullptr, name, nullptr)); - if (fetched == nullptr) return Cipher(); + if (!IsSupportedFetchedCipher(fetched.get())) return Cipher(); - const int mode = EVP_CIPHER_mode(fetched.get()); - const bool is_siv_mode = -#if OPENSSL_WITH_AES_SIV - mode == EVP_CIPH_SIV_MODE || -#endif -#if OPENSSL_WITH_AES_GCM_SIV - mode == EVP_CIPH_GCM_SIV_MODE || -#endif - false; - if (is_siv_mode) return Cipher(std::move(fetched)); + if (cache != nullptr && generation == getFipsStateGeneration()) { + if (const EVP_CIPHER* cached = + cache->insert(name, std::move(fetched), generation)) { + return Cipher(cached); + } + } - return Cipher(); + return Cipher(std::move(fetched)); #else + static_cast(cache); return Cipher(); #endif } -const Cipher Cipher::FromNid(int nid) { +const Cipher Cipher::FromNid(int nid, CipherCache* cache) { const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid); - if (cipher != nullptr) return Cipher(cipher); + if (cipher != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!IsSupportedLegacyCipher(cipher)) return Cipher(); +#endif + return Cipher(cipher); + } -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER const char* name = OBJ_nid2sn(nid); - if (name != nullptr) return FromName(name); + if (name != nullptr) return FromName(name, cache); +#else + static_cast(cache); #endif return Cipher(); @@ -4615,27 +4723,79 @@ const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) { return Cipher(GetCipherCtxCipher(ctx.get())); } -const Cipher Cipher::EMPTY = Cipher(); -const Cipher Cipher::AES_128_CBC = Cipher::FromNid(NID_aes_128_cbc); -const Cipher Cipher::AES_192_CBC = Cipher::FromNid(NID_aes_192_cbc); -const Cipher Cipher::AES_256_CBC = Cipher::FromNid(NID_aes_256_cbc); -const Cipher Cipher::AES_128_CTR = Cipher::FromNid(NID_aes_128_ctr); -const Cipher Cipher::AES_192_CTR = Cipher::FromNid(NID_aes_192_ctr); -const Cipher Cipher::AES_256_CTR = Cipher::FromNid(NID_aes_256_ctr); -const Cipher Cipher::AES_128_GCM = Cipher::FromNid(NID_aes_128_gcm); -const Cipher Cipher::AES_192_GCM = Cipher::FromNid(NID_aes_192_gcm); -const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm); -const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap); -const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap); -const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap); +namespace { +template +const Cipher& GetPredefinedCipher() { + static const Cipher cipher = Cipher::FromNid(nid); + return cipher; +} +} // namespace + +const Cipher& Cipher::AES_128_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_KW() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_KW() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_KW() { + return GetPredefinedCipher(); +} #ifndef OPENSSL_IS_BORINGSSL -const Cipher Cipher::AES_128_OCB = Cipher::FromNid(NID_aes_128_ocb); -const Cipher Cipher::AES_192_OCB = Cipher::FromNid(NID_aes_192_ocb); -const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb); +const Cipher& Cipher::AES_128_OCB() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_OCB() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_OCB() { + return GetPredefinedCipher(); +} #endif -const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); +const Cipher& Cipher::CHACHA20_POLY1305() { + return GetPredefinedCipher(); +} bool Cipher::isGcmMode() const { if (!cipher_) return false; @@ -4657,6 +4817,15 @@ bool Cipher::isCcmMode() const { return getMode() == EVP_CIPH_CCM_MODE; } +bool Cipher::isCtsMode() const { + if (!cipher_) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return (EVP_CIPHER_get_flags(cipher_) & EVP_CIPH_FLAG_CTS) != 0; +#else + return false; +#endif +} + bool Cipher::isOcbMode() const { if (!cipher_) return false; return getMode() == EVP_CIPH_OCB_MODE; @@ -4761,7 +4930,7 @@ const char* Cipher::getName() const { const char* name = OBJ_nid2sn(nid); if (name != nullptr) return name; } -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER return EVP_CIPHER_get0_name(cipher_); #else return {}; @@ -4858,11 +5027,57 @@ bool CipherCtxPointer::setAeadTagLength(size_t length) { ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +// OSSL_CIPHER_PARAM_XTS_STANDARD is not defined by OpenSSL 3.0. Use its +// parameter name directly so custom 3.0 providers can advertise it too. +constexpr char kCipherParamXtsStandard[] = "xts_standard"; + +bool SetCipherCtxStringParam(EVP_CIPHER_CTX* ctx, + const char* key, + const char* value) { + if (ctx == nullptr || value == nullptr) return false; + + const OSSL_PARAM* settable = EVP_CIPHER_CTX_settable_params(ctx); + const OSSL_PARAM* descriptor = + settable == nullptr ? nullptr : OSSL_PARAM_locate_const(settable, key); + if (descriptor == nullptr || + descriptor->data_type != OSSL_PARAM_UTF8_STRING) { + return false; + } + + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string(key, const_cast(value), 0), + OSSL_PARAM_END, + }; + return EVP_CIPHER_CTX_set_params(ctx, params) == 1; +} +} // namespace +#endif + +bool CipherCtxPointer::setCtsMode(const char* mode) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return SetCipherCtxStringParam(ctx_.get(), OSSL_CIPHER_PARAM_CTS_MODE, mode); +#else + static_cast(mode); + return false; +#endif +} + bool CipherCtxPointer::setPadding(bool padding) { if (!ctx_) return false; return EVP_CIPHER_CTX_set_padding(ctx_.get(), padding); } +bool CipherCtxPointer::setXtsStandard(const char* standard) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return SetCipherCtxStringParam(ctx_.get(), kCipherParamXtsStandard, standard); +#else + static_cast(standard); + return false; +#endif +} + int CipherCtxPointer::getBlockSize() const { if (!ctx_) return 0; return EVP_CIPHER_CTX_block_size(ctx_.get()); @@ -4888,6 +5103,16 @@ bool CipherCtxPointer::isCcmMode() const { return getMode() == EVP_CIPH_CCM_MODE; } +bool CipherCtxPointer::isCtsMode() const { + if (!ctx_) return false; + return Cipher::FromCtx(*this).isCtsMode(); +} + +bool CipherCtxPointer::isXtsMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_XTS_MODE; +} + bool CipherCtxPointer::isWrapMode() const { if (!ctx_) return false; return getMode() == EVP_CIPH_WRAP_MODE; @@ -6359,23 +6584,7 @@ struct CipherCallbackContext { void operator()(const char* name) { cb(name); } }; -#if OPENSSL_WITH_AES_SIV -constexpr const char* kProviderOnlyAesSivCiphers[] = { - "aes-128-siv", - "aes-192-siv", - "aes-256-siv", -}; -#endif - -#if OPENSSL_WITH_AES_GCM_SIV -constexpr const char* kProviderOnlyAesGcmSivCiphers[] = { - "aes-128-gcm-siv", - "aes-192-gcm-siv", - "aes-256-gcm-siv", -}; -#endif - -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER template fetched( + fetch_type(nullptr, real_name, nullptr)); + if (!IsSupportedFetchedCipher(fetched.get())) return; - free_type(fetched); auto& cb = *(static_cast(arg)); cb(from); } + +void array_push_back_provider_name(const char* name, void* arg) { + if (name == nullptr) return; + + const std::string_view name_view(name); + const bool is_dotted_decimal = + name_view.find('.') != std::string_view::npos && + std::all_of(name_view.begin(), name_view.end(), [](unsigned char c) { + return (c >= '0' && c <= '9') || c == '.'; + }); + if (is_dotted_decimal) return; + + std::string normalized_name(name_view); + std::transform(normalized_name.begin(), + normalized_name.end(), + normalized_name.begin(), + [](unsigned char c) { + if (c >= 'A' && c <= 'Z') { + return static_cast(c + ('a' - 'A')); + } + return static_cast(c); + }); + auto& cb = *(static_cast(arg)); + cb(normalized_name.c_str()); +} + +void array_push_back_provider(EVP_CIPHER* cipher, void* arg) { + const char* name = EVP_CIPHER_get0_name(cipher); + if (name == nullptr) return; + + DeleteFnPtr fetched( + EVP_CIPHER_fetch(nullptr, name, nullptr)); + if (!IsSupportedFetchedCipher(fetched.get())) return; + + EVP_CIPHER_names_do_all(fetched.get(), array_push_back_provider_name, arg); +} #else template void array_push_back(const TypeName* evp_ref, @@ -6431,7 +6676,7 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { } #else EVP_CIPHER_do_all_sorted( -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER array_push_back, #endif &context); -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV - auto maybe_push_provider_only_cipher = [&](const char* name) { - EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, name, nullptr); - if (cipher == nullptr) return; - EVP_CIPHER_free(cipher); - context.cb(name); - }; -#endif -#if OPENSSL_WITH_AES_SIV - for (const char* name : kProviderOnlyAesSivCiphers) { - maybe_push_provider_only_cipher(name); - } -#endif -#if OPENSSL_WITH_AES_GCM_SIV - for (const char* name : kProviderOnlyAesGcmSivCiphers) { - maybe_push_provider_only_cipher(name); - } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_CIPHER_do_all_provided(nullptr, array_push_back_provider, &context); #endif #endif } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 8d4091c75a9..8c09ac5f165 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -20,6 +20,8 @@ #include #include #include +#include +#include #if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \ !defined(OPENSSL_NO_ENGINE) #include @@ -498,6 +500,32 @@ DataPointer xofHashDigest(const Buffer& data, const EVP_MD* md, size_t length); +class CipherCache final { + public: + CipherCache() = default; + NCRYPTO_DISALLOW_COPY_AND_MOVE(CipherCache) + + const EVP_CIPHER* lookup(const char* name, uint64_t generation); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const EVP_CIPHER* insert(const char* name, + DeleteFnPtr&& cipher, + uint64_t generation); +#endif + + private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + using EVPCipherPointer = DeleteFnPtr; + + uint64_t generation_ = 0; + std::vector ciphers_; + std::unordered_map + aliases_; +#endif +}; + class Cipher final { public: static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH; @@ -519,7 +547,7 @@ class Cipher final { Cipher(const Cipher& other); Cipher& operator=(const Cipher& other); inline Cipher& operator=(const EVP_CIPHER* cipher) { -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER fetched_cipher_.reset(); #endif cipher_ = cipher; @@ -543,6 +571,7 @@ class Cipher final { bool isWrapMode() const; bool isCtrMode() const; bool isCcmMode() const; + bool isCtsMode() const; bool isOcbMode() const; bool isSivMode() const; bool isGcmSivMode() const; @@ -556,8 +585,8 @@ class Cipher final { unsigned char* key, unsigned char* iv) const; - static const Cipher FromName(const char* name); - static const Cipher FromNid(int nid); + static const Cipher FromName(const char* name, CipherCache* cache = nullptr); + static const Cipher FromNid(int nid, CipherCache* cache = nullptr); static const Cipher FromCtx(const CipherCtxPointer& ctx); using CipherNameCallback = std::function; @@ -566,28 +595,24 @@ class Cipher final { // is able to do so. static void ForEach(CipherNameCallback callback); - // Utilities to get various ciphers by type. If the underlying - // implementation does not support the requested cipher, then - // the result will be an empty Cipher object whose bool operator - // will return false. - - static const Cipher EMPTY; - static const Cipher AES_128_CBC; - static const Cipher AES_192_CBC; - static const Cipher AES_256_CBC; - static const Cipher AES_128_CTR; - static const Cipher AES_192_CTR; - static const Cipher AES_256_CTR; - static const Cipher AES_128_GCM; - static const Cipher AES_192_GCM; - static const Cipher AES_256_GCM; - static const Cipher AES_128_KW; - static const Cipher AES_192_KW; - static const Cipher AES_256_KW; - static const Cipher AES_128_OCB; - static const Cipher AES_192_OCB; - static const Cipher AES_256_OCB; - static const Cipher CHACHA20_POLY1305; + // Lazily resolves common ciphers. If the underlying implementation does not + // support the requested cipher, the returned Cipher will be empty. + static const Cipher& AES_128_CBC(); + static const Cipher& AES_192_CBC(); + static const Cipher& AES_256_CBC(); + static const Cipher& AES_128_CTR(); + static const Cipher& AES_192_CTR(); + static const Cipher& AES_256_CTR(); + static const Cipher& AES_128_GCM(); + static const Cipher& AES_192_GCM(); + static const Cipher& AES_256_GCM(); + static const Cipher& AES_128_KW(); + static const Cipher& AES_192_KW(); + static const Cipher& AES_256_KW(); + static const Cipher& AES_128_OCB(); + static const Cipher& AES_192_OCB(); + static const Cipher& AES_256_OCB(); + static const Cipher& CHACHA20_POLY1305(); struct CipherParams { int padding; @@ -617,7 +642,7 @@ class Cipher final { private: const EVP_CIPHER* cipher_ = nullptr; -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER explicit Cipher(DeleteFnPtr cipher); DeleteFnPtr fetched_cipher_; #endif @@ -1007,7 +1032,9 @@ class CipherCtxPointer final { bool setIvLength(size_t length); bool setAeadTag(const Buffer& tag); bool setAeadTagLength(size_t length); + bool setCtsMode(const char* mode); bool setPadding(bool padding); + bool setXtsStandard(const char* standard); bool init(const Cipher& cipher, bool encrypt, const unsigned char* key = nullptr, @@ -1020,6 +1047,8 @@ class CipherCtxPointer final { bool isGcmMode() const; bool isOcbMode() const; bool isCcmMode() const; + bool isCtsMode() const; + bool isXtsMode() const; bool isWrapMode() const; bool isSivMode() const; bool isGcmSivMode() const; diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 78add10d64f..3555414444b 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -633,6 +633,10 @@ The [`crypto.createCipheriv()`][] method is used to create `Cipheriv` instances. `Cipheriv` objects are not to be created directly using the `new` keyword. +The selected algorithm may impose additional restrictions on streaming and +calls to [`cipher.update()`][]. See [CCM mode][], [CBC-CTS mode][], [XTS mode][], +[AES key wrap modes][], and [SIV and GCM-SIV modes][]. + Example: Using `Cipheriv` objects as streams: ```mjs @@ -905,14 +909,15 @@ added: v0.7.1 * `autoPadding` {boolean} **Default:** `true` * Returns: {Cipheriv} The same `Cipheriv` instance for method chaining. -When using block encryption algorithms, the `Cipheriv` class will automatically -add padding to the input data to the appropriate block size. To disable the -default padding call `cipher.setAutoPadding(false)`. +When using block ciphers that use standard block padding, the `Cipheriv` class +will automatically add padding to the input data to the appropriate block size. +To disable the default padding call `cipher.setAutoPadding(false)`. -When `autoPadding` is `false`, the length of the entire input data must be a -multiple of the cipher's block size or [`cipher.final()`][] will throw an error. -Disabling automatic padding is useful for non-standard padding, for instance -using `0x0` instead of PKCS padding. +For block ciphers that use standard block padding, when `autoPadding` is +`false`, the length of the entire input data must be a multiple of the cipher's +block size or [`cipher.final()`][] will throw an error. Disabling automatic +padding is useful for non-standard padding, for instance using `0x0` instead of +PKCS padding. The `cipher.setAutoPadding()` method must be called before [`cipher.final()`][]. @@ -946,9 +951,12 @@ is specified, a string using the specified encoding is returned. If no When `outputEncoding` is specified, it must use the same encoding as previous calls to `cipher.update()`. -The `cipher.update()` method can be called multiple times with new data until -[`cipher.final()`][] is called. Calling `cipher.update()` after -[`cipher.final()`][] will result in an error being thrown. +For most algorithms, `cipher.update()` can be called multiple times with new +data until [`cipher.final()`][] is called. Some algorithms restrict calls to +`cipher.update()`. For example, [CCM mode][], [CBC-CTS mode][], [XTS mode][], +[AES key wrap modes][], and [SIV and GCM-SIV modes][] require the whole message +in a single call. Calling `cipher.update()` after [`cipher.final()`][] will +result in an error being thrown. ## Class: `Decipheriv` @@ -970,6 +978,10 @@ The [`crypto.createDecipheriv()`][] method is used to create `Decipheriv` instances. `Decipheriv` objects are not to be created directly using the `new` keyword. +The selected algorithm may impose additional restrictions on streaming and +calls to [`decipher.update()`][]. See [CCM mode][], [CBC-CTS mode][], +[XTS mode][], [AES key wrap modes][], and [SIV and GCM-SIV modes][]. + Example: Using `Decipheriv` objects as streams: ```mjs @@ -1267,8 +1279,8 @@ When data has been encrypted without standard block padding, calling `decipher.setAutoPadding(false)` will disable automatic padding to prevent [`decipher.final()`][] from checking for and removing padding. -Turning auto padding off will only work if the input data's length is a -multiple of the ciphers block size. +For block ciphers that use standard block padding, disabling it requires the +input data's length to be a multiple of the cipher's block size. The `decipher.setAutoPadding()` method must be called before [`decipher.final()`][]. @@ -1291,19 +1303,23 @@ changes: Updates the decipher with `data`. If the `inputEncoding` argument is given, the `data` argument is a string using the specified encoding. If the `inputEncoding` -argument is not given, `data` must be a [`Buffer`][]. If `data` is a -[`Buffer`][] then `inputEncoding` is ignored. +argument is not given, `data` must be a [`Buffer`][], `TypedArray`, or +`DataView`. If `data` is a [`Buffer`][], `TypedArray`, or `DataView`, then +`inputEncoding` is ignored. -The `outputEncoding` specifies the output format of the enciphered +The `outputEncoding` specifies the output format of the deciphered data. If the `outputEncoding` is specified, a string using the specified encoding is returned. If no `outputEncoding` is provided, a [`Buffer`][] is returned. When `outputEncoding` is specified, it must use the same encoding as previous calls to `decipher.update()`. -The `decipher.update()` method can be called multiple times with new data until -[`decipher.final()`][] is called. Calling `decipher.update()` after -[`decipher.final()`][] will result in an error being thrown. +For most algorithms, `decipher.update()` can be called multiple times with new +data until [`decipher.final()`][] is called. Some algorithms restrict calls to +`decipher.update()`. For example, [CCM mode][], [CBC-CTS mode][], [XTS mode][], +[AES key wrap modes][], and [SIV and GCM-SIV modes][] require the whole message +in a single call. Calling `decipher.update()` after [`decipher.final()`][] will +result in an error being thrown. Even if the underlying cipher implements authentication, the authenticity and integrity of the plaintext returned from this function may be uncertain at this @@ -3566,6 +3582,12 @@ operations. The specific constants currently defined are described in