From a1e07c6dfafc8a85c60cdc9a88695b53fc1454f8 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sun, 3 Dec 2023 19:11:54 +0100 Subject: [PATCH 1/3] crypto: use EVP_MD_fetch and cache EVP_MD for hashes On OpenSSL 3, migrate from EVP_get_digestbyname() to EVP_MD_fetch() to get the implementation and use a per-Environment cache for it. The EVP_MDs are freed during Environment cleanup. Drive-by: declare the smart pointer for EVP_MD_CTX as EVPMDCtxPointer instead of EVPMDPointer to avoid confusion with EVP_MD pointers. --- src/crypto/README.md | 2 +- src/crypto/crypto_hash.cc | 27 ++++++++++++++++++++++++--- src/crypto/crypto_hash.h | 2 +- src/crypto/crypto_sig.cc | 8 ++++---- src/crypto/crypto_sig.h | 2 +- src/crypto/crypto_util.h | 2 +- src/env.h | 10 ++++++++++ 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/crypto/README.md b/src/crypto/README.md index ceefda03976ba9..86bfb7e47824c8 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -79,7 +79,7 @@ using SSLPointer = DeleteFnPtr; using PKCS8Pointer = DeleteFnPtr; using EVPKeyPointer = DeleteFnPtr; using EVPKeyCtxPointer = DeleteFnPtr; -using EVPMDPointer = DeleteFnPtr; +using EVPMDCtxPointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; using ECPointer = DeleteFnPtr; using BignumPointer = DeleteFnPtr; diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 12dd5de3bd7de7..d1c97da0a33a51 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -52,6 +52,28 @@ void Hash::GetHashes(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ctx.ToJSArray()); } +const EVP_MD* GetDigestImplementation(Environment* env, + const Utf8Value& hash_type) { +#if OPENSSL_VERSION_MAJOR >= 3 + std::string hash_type_str = hash_type.ToString(); + auto it = env->evp_md_cache.find(hash_type_str); + if (it == env->evp_md_cache.end()) { + EVP_MD* explicit_md = EVP_MD_fetch(nullptr, hash_type_str.c_str(), nullptr); + if (explicit_md != nullptr) { + env->evp_md_cache.emplace(hash_type_str, explicit_md); + return explicit_md; + } else { + // We'll do a fallback. + ERR_clear_error(); + } + } else { + return it->second.get(); + } +#endif // OPENSSL_VERSION_MAJOR >= 3 + // EVP_MD_fetch failed, fallback to EVP_get_digestbyname. + return EVP_get_digestbyname(*hash_type); +} + void Hash::Initialize(Environment* env, Local target) { Isolate* isolate = env->isolate(); Local context = env->context(); @@ -94,7 +116,7 @@ void Hash::New(const FunctionCallbackInfo& args) { md = EVP_MD_CTX_md(orig->mdctx_.get()); } else { const Utf8Value hash_type(env->isolate(), args[0]); - md = EVP_get_digestbyname(*hash_type); + md = GetDigestImplementation(env, hash_type); } Maybe xof_md_len = Nothing(); @@ -284,7 +306,7 @@ bool HashTraits::DeriveBits( Environment* env, const HashConfig& params, ByteSource* out) { - EVPMDPointer ctx(EVP_MD_CTX_new()); + EVPMDCtxPointer ctx(EVP_MD_CTX_new()); if (UNLIKELY(!ctx || EVP_DigestInit_ex(ctx.get(), params.digest, nullptr) <= 0 || @@ -357,6 +379,5 @@ void InternalVerifyIntegrity(const v8::FunctionCallbackInfo& args) { args.GetReturnValue().Set(rc.FromMaybe(Local())); } } - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 2d17c3510ed214..8bbf5dd06ec596 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -34,7 +34,7 @@ class Hash final : public BaseObject { Hash(Environment* env, v8::Local wrap); private: - EVPMDPointer mdctx_ {}; + EVPMDCtxPointer mdctx_{}; unsigned int md_len_ = 0; ByteSource digest_; }; diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index e38bc9eec083ad..5b1acf2677d1b6 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -73,7 +73,7 @@ bool ApplyRSAOptions(const ManagedEVPPKey& pkey, } std::unique_ptr Node_SignFinal(Environment* env, - EVPMDPointer&& mdctx, + EVPMDCtxPointer&& mdctx, const ManagedEVPPKey& pkey, int padding, Maybe pss_salt_len) { @@ -391,7 +391,7 @@ Sign::SignResult Sign::SignFinal( if (!mdctx_) return SignResult(kSignNotInitialised); - EVPMDPointer mdctx = std::move(mdctx_); + EVPMDCtxPointer mdctx = std::move(mdctx_); if (!ValidateDSAParameters(pkey.get())) return SignResult(kSignPrivateKey); @@ -511,7 +511,7 @@ SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey, unsigned char m[EVP_MAX_MD_SIZE]; unsigned int m_len; *verify_result = false; - EVPMDPointer mdctx = std::move(mdctx_); + EVPMDCtxPointer mdctx = std::move(mdctx_); if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) return kSignPublicKey; @@ -696,7 +696,7 @@ bool SignTraits::DeriveBits( const SignConfiguration& params, ByteSource* out) { ClearErrorOnReturn clear_error_on_return; - EVPMDPointer context(EVP_MD_CTX_new()); + EVPMDCtxPointer context(EVP_MD_CTX_new()); EVP_PKEY_CTX* ctx = nullptr; switch (params.mode) { diff --git a/src/crypto/crypto_sig.h b/src/crypto/crypto_sig.h index 1a4cda42272e51..633201473e4645 100644 --- a/src/crypto/crypto_sig.h +++ b/src/crypto/crypto_sig.h @@ -42,7 +42,7 @@ class SignBase : public BaseObject { SET_SELF_SIZE(SignBase) protected: - EVPMDPointer mdctx_; + EVPMDCtxPointer mdctx_; }; class Sign : public SignBase { diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 1ce5f35a70a7c8..c946ef964860e9 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -62,7 +62,7 @@ using SSLPointer = DeleteFnPtr; using PKCS8Pointer = DeleteFnPtr; using EVPKeyPointer = DeleteFnPtr; using EVPKeyCtxPointer = DeleteFnPtr; -using EVPMDPointer = DeleteFnPtr; +using EVPMDCtxPointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; using ECPointer = DeleteFnPtr; using BignumPointer = DeleteFnPtr; diff --git a/src/env.h b/src/env.h index 244b8828c73a18..997a2b02ed2cfc 100644 --- a/src/env.h +++ b/src/env.h @@ -49,6 +49,10 @@ #include "uv.h" #include "v8.h" +#if HAVE_OPENSSL +#include +#endif + #include #include #include @@ -1018,6 +1022,12 @@ class Environment : public MemoryRetainer { kExitInfoFieldCount }; +#if OPENSSL_VERSION_MAJOR >= 3 + // We declare another alias here to avoid having to include crypto_util.h + using EVPMDPointer = DeleteFnPtr; + std::unordered_map evp_md_cache; +#endif // OPENSSL_VERSION_MAJOR + private: // V8 has changed the constructor of exceptions, support both APIs before Node // updates to V8 12.1. From 8245383424762082ef91933fe6cab80ae72633b9 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 4 Dec 2023 05:39:44 +0100 Subject: [PATCH 2/3] fixup! crypto: use EVP_MD_fetch and cache EVP_MD for hashes --- lib/internal/crypto/hash.js | 39 +++++++++++- src/crypto/crypto_hash.cc | 117 ++++++++++++++++++++++++------------ src/env.h | 4 +- 3 files changed, 119 insertions(+), 41 deletions(-) diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 4ed97034f612fb..abf277347db2c1 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -11,6 +11,7 @@ const { HashJob, Hmac: _Hmac, kCryptoJobAsync, + getHashes, } = internalBinding('crypto'); const { @@ -56,16 +57,50 @@ const LazyTransform = require('internal/streams/lazy_transform'); const kState = Symbol('kState'); const kFinalized = Symbol('kFinalized'); +const { + namespace: { + isBuildingSnapshot, + addSerializeCallback, + addDeserializeCallback, + }, +} = require('internal/v8/startup_snapshot'); + +let algorithmMap; +function initializeAlgorithmMap() { + if (algorithmMap === undefined) { + algorithmMap = { __proto__: null }; + const hashes = getHashes(); + for (let i = 0; i < hashes.length; ++i) { + algorithmMap[[hashes[i]]] = i; + } + if (isBuildingSnapshot()) { + // For dynamic linking, clear the map. + addSerializeCallback(() => { algorithmMap = undefined; }); + addDeserializeCallback(initializeAlgorithmMap); + } + } +} + +function getAlgorithmId(algorithm) { + initializeAlgorithmMap(); + const result = algorithmMap[algorithm]; + return result === undefined ? -1 : result; +} + +initializeAlgorithmMap(); + function Hash(algorithm, options) { if (!new.target) return new Hash(algorithm, options); - if (!(algorithm instanceof _Hash)) + const isCopy = algorithm instanceof _Hash; + if (!isCopy) validateString(algorithm, 'algorithm'); const xofLen = typeof options === 'object' && options !== null ? options.outputLength : undefined; if (xofLen !== undefined) validateUint32(xofLen, 'options.outputLength'); - this[kHandle] = new _Hash(algorithm, xofLen); + const algorithmId = isCopy ? -1 : getAlgorithmId(algorithm); + this[kHandle] = new _Hash(algorithm, algorithmId, xofLen); this[kState] = { [kFinalized]: false, }; diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index d1c97da0a33a51..b8914b62ccf784 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -14,6 +14,7 @@ namespace node { using v8::Context; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; +using v8::Int32; using v8::Isolate; using v8::Just; using v8::Local; @@ -34,44 +35,86 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); } -void Hash::GetHashes(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - MarkPopErrorOnReturn mark_pop_error_on_return; - CipherPushContext ctx(env); - EVP_MD_do_all_sorted( +void CacheSupportedHashAlgorithms(const EVP_MD* md, + const char* from, + const char* to, + void* arg) { + if (!from) return; + #if OPENSSL_VERSION_MAJOR >= 3 - array_push_back, -#else - array_push_back, -#endif - &ctx); - args.GetReturnValue().Set(ctx.ToJSArray()); + const EVP_MD* implicit_md = EVP_get_digestbyname(from); + if (!implicit_md) return; + const char* real_name = EVP_MD_get0_name(implicit_md); + if (!real_name) return; + // EVP_*_fetch() does not support alias names, so we need to pass it the + // real/original algorithm name. + // We use EVP_*_fetch() as a filter here because it will only return an + // instance if the algorithm is supported by the public OpenSSL APIs (some + // algorithms are used internally by OpenSSL and are also passed to this + // callback). + EVP_MD* explicit_md = EVP_MD_fetch(nullptr, real_name, nullptr); + if (!explicit_md) return; +#endif // OPENSSL_VERSION_MAJOR >= 3 + + Environment* env = static_cast(arg); + env->supported_hash_algorithms.push_back(from); + +#if OPENSSL_VERSION_MAJOR >= 3 + env->evp_md_cache.emplace_back(explicit_md); +#endif // OPENSSL_VERSION_MAJOR >= 3 } -const EVP_MD* GetDigestImplementation(Environment* env, - const Utf8Value& hash_type) { +const std::vector& GetSupportedHashAlgorithms(Environment* env) { + if (env->supported_hash_algorithms.empty()) { + MarkPopErrorOnReturn mark_pop_error_on_return; + std::vector results; + EVP_MD_do_all_sorted(CacheSupportedHashAlgorithms, env); #if OPENSSL_VERSION_MAJOR >= 3 - std::string hash_type_str = hash_type.ToString(); - auto it = env->evp_md_cache.find(hash_type_str); - if (it == env->evp_md_cache.end()) { - EVP_MD* explicit_md = EVP_MD_fetch(nullptr, hash_type_str.c_str(), nullptr); - if (explicit_md != nullptr) { - env->evp_md_cache.emplace(hash_type_str, explicit_md); - return explicit_md; - } else { - // We'll do a fallback. - ERR_clear_error(); - } - } else { - return it->second.get(); + CHECK_EQ(env->supported_hash_algorithms.size(), env->evp_md_cache.size()); + CHECK_GT(env->supported_hash_algorithms.size(), 0); +#endif // OPENSSL_VERSION_MAJOR >= 3 + } + return env->supported_hash_algorithms; +} + +void Hash::GetHashes(const FunctionCallbackInfo& args) { + Local context = args.GetIsolate()->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + const std::vector& results = GetSupportedHashAlgorithms(env); + + Local ret; + if (ToV8Value(context, results).ToLocal(&ret)) { + args.GetReturnValue().Set(ret); } +} + +const EVP_MD* GetDigestImplementation(Environment* env, + Local algorithm, + Local algorithm_id) { + CHECK(algorithm->IsString()); + CHECK(algorithm_id->IsInt32()); + int32_t id = algorithm_id.As()->Value(); + + const std::vector& algorithms = GetSupportedHashAlgorithms(env); + std::string algorithm_str; + + if (id != -1) { +#if OPENSSL_VERSION_MAJOR >= 3 + CHECK_LT(static_cast(id), algorithms.size()); + auto& ptr = env->evp_md_cache[id]; + CHECK_NOT_NULL(ptr.get()); + return ptr.get(); +#else + algorithm_str = algorithms[id]; #endif // OPENSSL_VERSION_MAJOR >= 3 - // EVP_MD_fetch failed, fallback to EVP_get_digestbyname. - return EVP_get_digestbyname(*hash_type); + } + + if (algorithm_str.empty()) { // It could be unsupported algorithms. + Utf8Value utf8(env->isolate(), algorithm); + algorithm_str = utf8.ToString(); + } + const EVP_MD* implicit_md = EVP_get_digestbyname(algorithm_str.c_str()); + return implicit_md; } void Hash::Initialize(Environment* env, Local target) { @@ -110,19 +153,17 @@ void Hash::New(const FunctionCallbackInfo& args) { const Hash* orig = nullptr; const EVP_MD* md = nullptr; - if (args[0]->IsObject()) { ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As()); md = EVP_MD_CTX_md(orig->mdctx_.get()); } else { - const Utf8Value hash_type(env->isolate(), args[0]); - md = GetDigestImplementation(env, hash_type); + md = GetDigestImplementation(env, args[0], args[1]); } Maybe xof_md_len = Nothing(); - if (!args[1]->IsUndefined()) { - CHECK(args[1]->IsUint32()); - xof_md_len = Just(args[1].As()->Value()); + if (!args[2]->IsUndefined()) { + CHECK(args[2]->IsUint32()); + xof_md_len = Just(args[2].As()->Value()); } Hash* hash = new Hash(env, args.This()); diff --git a/src/env.h b/src/env.h index 997a2b02ed2cfc..70dd05aacabe96 100644 --- a/src/env.h +++ b/src/env.h @@ -1025,9 +1025,11 @@ class Environment : public MemoryRetainer { #if OPENSSL_VERSION_MAJOR >= 3 // We declare another alias here to avoid having to include crypto_util.h using EVPMDPointer = DeleteFnPtr; - std::unordered_map evp_md_cache; + std::vector evp_md_cache; #endif // OPENSSL_VERSION_MAJOR + std::vector supported_hash_algorithms; + private: // V8 has changed the constructor of exceptions, support both APIs before Node // updates to V8 12.1. From e476dfc0c001cb32ba4543d53a04fd0257e60155 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sun, 10 Dec 2023 02:15:21 +0100 Subject: [PATCH 3/3] fixup! crypto: use EVP_MD_fetch and cache EVP_MD for hashes --- lib/internal/crypto/hash.js | 41 ++------- lib/internal/crypto/util.js | 27 ++++++ src/crypto/crypto_hash.cc | 174 +++++++++++++++++++++++++++--------- src/crypto/crypto_hash.h | 1 + src/env.h | 6 +- 5 files changed, 169 insertions(+), 80 deletions(-) diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index abf277347db2c1..f3072d61dd0be0 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -11,7 +11,6 @@ const { HashJob, Hmac: _Hmac, kCryptoJobAsync, - getHashes, } = internalBinding('crypto'); const { @@ -20,6 +19,8 @@ const { normalizeHashName, validateMaxBufferLength, kHandle, + getCachedHashId, + getHashCache, } = require('internal/crypto/util'); const { @@ -57,38 +58,6 @@ const LazyTransform = require('internal/streams/lazy_transform'); const kState = Symbol('kState'); const kFinalized = Symbol('kFinalized'); -const { - namespace: { - isBuildingSnapshot, - addSerializeCallback, - addDeserializeCallback, - }, -} = require('internal/v8/startup_snapshot'); - -let algorithmMap; -function initializeAlgorithmMap() { - if (algorithmMap === undefined) { - algorithmMap = { __proto__: null }; - const hashes = getHashes(); - for (let i = 0; i < hashes.length; ++i) { - algorithmMap[[hashes[i]]] = i; - } - if (isBuildingSnapshot()) { - // For dynamic linking, clear the map. - addSerializeCallback(() => { algorithmMap = undefined; }); - addDeserializeCallback(initializeAlgorithmMap); - } - } -} - -function getAlgorithmId(algorithm) { - initializeAlgorithmMap(); - const result = algorithmMap[algorithm]; - return result === undefined ? -1 : result; -} - -initializeAlgorithmMap(); - function Hash(algorithm, options) { if (!new.target) return new Hash(algorithm, options); @@ -99,8 +68,10 @@ function Hash(algorithm, options) { options.outputLength : undefined; if (xofLen !== undefined) validateUint32(xofLen, 'options.outputLength'); - const algorithmId = isCopy ? -1 : getAlgorithmId(algorithm); - this[kHandle] = new _Hash(algorithm, algorithmId, xofLen); + // Lookup the cached ID from JS land because it's faster than decoding + // the string in C++ land. + const algorithmId = isCopy ? -1 : getCachedHashId(algorithm); + this[kHandle] = new _Hash(algorithm, xofLen, algorithmId, getHashCache()); this[kState] = { [kFinalized]: false, }; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 45a236a1130249..d756b067798a57 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -29,6 +29,7 @@ const { getHashes: _getHashes, setEngine: _setEngine, secureHeapUsed: _secureHeapUsed, + getCachedAliases, } = internalBinding('crypto'); const { getOptionValue } = require('internal/options'); @@ -66,6 +67,13 @@ const { lazyDOMException, } = require('internal/util'); +const { + namespace: { + isBuildingSnapshot, + addSerializeCallback, + }, +} = require('internal/v8/startup_snapshot'); + const { isDataView, isArrayBufferView, @@ -87,6 +95,23 @@ function toBuf(val, encoding) { return val; } +let _hashCache; +function getHashCache() { + if (_hashCache === undefined) { + _hashCache = getCachedAliases(); + if (isBuildingSnapshot()) { + // For dynamic linking, clear the map. + addSerializeCallback(() => { _hashCache = undefined; }); + } + } + return _hashCache; +} + +function getCachedHashId(algorithm) { + const result = getHashCache()[algorithm]; + return result === undefined ? -1 : result; +} + const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers())); const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes())); const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); @@ -574,4 +599,6 @@ module.exports = { getStringOption, getUsagesUnion, secureHeapUsed, + getCachedHashId, + getHashCache, }; diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index b8914b62ccf784..2a709126544833 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -20,6 +20,7 @@ using v8::Just; using v8::Local; using v8::Maybe; using v8::MaybeLocal; +using v8::Name; using v8::Nothing; using v8::Object; using v8::Uint32; @@ -35,17 +36,37 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); } -void CacheSupportedHashAlgorithms(const EVP_MD* md, - const char* from, - const char* to, - void* arg) { - if (!from) return; - #if OPENSSL_VERSION_MAJOR >= 3 - const EVP_MD* implicit_md = EVP_get_digestbyname(from); - if (!implicit_md) return; +void PushAliases(const char* name, void* data) { + static_cast*>(data)->push_back(name); +} + +EVP_MD* GetCachedMDByID(Environment* env, size_t id) { + CHECK_LT(id, env->evp_md_cache.size()); + EVP_MD* result = env->evp_md_cache[id].get(); + CHECK_NOT_NULL(result); + return result; +} + +struct MaybeCachedMD { + EVP_MD* explicit_md = nullptr; + const EVP_MD* implicit_md = nullptr; + int32_t cache_id = -1; +}; + +MaybeCachedMD FetchAndMaybeCacheMD(Environment* env, const char* search_name) { + const EVP_MD* implicit_md = EVP_get_digestbyname(search_name); + if (!implicit_md) return {nullptr, nullptr, -1}; + const char* real_name = EVP_MD_get0_name(implicit_md); - if (!real_name) return; + if (!real_name) return {nullptr, implicit_md, -1}; + + auto it = env->alias_to_md_id_map.find(real_name); + if (it != env->alias_to_md_id_map.end()) { + size_t id = it->second; + return {GetCachedMDByID(env, id), implicit_md, static_cast(id)}; + } + // EVP_*_fetch() does not support alias names, so we need to pass it the // real/original algorithm name. // We use EVP_*_fetch() as a filter here because it will only return an @@ -53,26 +74,56 @@ void CacheSupportedHashAlgorithms(const EVP_MD* md, // algorithms are used internally by OpenSSL and are also passed to this // callback). EVP_MD* explicit_md = EVP_MD_fetch(nullptr, real_name, nullptr); - if (!explicit_md) return; -#endif // OPENSSL_VERSION_MAJOR >= 3 + if (!explicit_md) return {nullptr, implicit_md, -1}; + // Cache the EVP_MD* fetched. + env->evp_md_cache.emplace_back(explicit_md); + size_t id = env->evp_md_cache.size() - 1; + + // Add all the aliases to the map to speed up next lookup. + std::vector aliases; + EVP_MD_names_do_all(explicit_md, PushAliases, &aliases); + for (const auto& alias : aliases) { + env->alias_to_md_id_map.emplace(alias, id); + } + env->alias_to_md_id_map.emplace(search_name, id); + + return {explicit_md, implicit_md, static_cast(id)}; +} + +void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md, + const char* from, + const char* to, + void* arg) { + if (!from) return; Environment* env = static_cast(arg); - env->supported_hash_algorithms.push_back(from); + auto result = FetchAndMaybeCacheMD(env, from); + if (result.explicit_md) { + env->supported_hash_algorithms.push_back(from); + } +} -#if OPENSSL_VERSION_MAJOR >= 3 - env->evp_md_cache.emplace_back(explicit_md); -#endif // OPENSSL_VERSION_MAJOR >= 3 +#else +void SaveSupportedHashAlgorithms(const EVP_MD* md, + const char* from, + const char* to, + void* arg) { + if (!from) return; + Environment* env = static_cast(arg); + env->supported_hash_algorithms.push_back(from); } +#endif // OPENSSL_VERSION_MAJOR >= 3 const std::vector& GetSupportedHashAlgorithms(Environment* env) { if (env->supported_hash_algorithms.empty()) { MarkPopErrorOnReturn mark_pop_error_on_return; - std::vector results; - EVP_MD_do_all_sorted(CacheSupportedHashAlgorithms, env); #if OPENSSL_VERSION_MAJOR >= 3 - CHECK_EQ(env->supported_hash_algorithms.size(), env->evp_md_cache.size()); - CHECK_GT(env->supported_hash_algorithms.size(), 0); -#endif // OPENSSL_VERSION_MAJOR >= 3 + // Since we'll fetch the EVP_MD*, cache them along the way to speed up + // later lookups instead of throwing them away immediately. + EVP_MD_do_all_sorted(SaveSupportedHashAlgorithmsAndCacheMD, env); +#else + EVP_MD_do_all_sorted(SaveSupportedHashAlgorithms, env); +#endif } return env->supported_hash_algorithms; } @@ -88,33 +139,67 @@ void Hash::GetHashes(const FunctionCallbackInfo& args) { } } +void Hash::GetCachedAliases(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = args.GetIsolate()->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + std::vector> names; + std::vector> values; + size_t size = env->alias_to_md_id_map.size(); +#if OPENSSL_VERSION_MAJOR >= 3 + names.reserve(size); + values.reserve(size); + for (auto& [alias, id] : env->alias_to_md_id_map) { + names.push_back(OneByteString(isolate, alias.c_str(), alias.size())); + values.push_back(v8::Uint32::New(isolate, id)); + } +#else + CHECK(env->alias_to_md_id_map.empty()); +#endif + Local prototype = v8::Null(isolate); + Local result = + Object::New(isolate, prototype, names.data(), values.data(), size); + args.GetReturnValue().Set(result); +} + const EVP_MD* GetDigestImplementation(Environment* env, Local algorithm, - Local algorithm_id) { + Local cache_id_val, + Local algorithm_cache) { CHECK(algorithm->IsString()); - CHECK(algorithm_id->IsInt32()); - int32_t id = algorithm_id.As()->Value(); - - const std::vector& algorithms = GetSupportedHashAlgorithms(env); - std::string algorithm_str; + CHECK(cache_id_val->IsInt32()); + CHECK(algorithm_cache->IsObject()); - if (id != -1) { #if OPENSSL_VERSION_MAJOR >= 3 - CHECK_LT(static_cast(id), algorithms.size()); - auto& ptr = env->evp_md_cache[id]; - CHECK_NOT_NULL(ptr.get()); - return ptr.get(); -#else - algorithm_str = algorithms[id]; -#endif // OPENSSL_VERSION_MAJOR >= 3 + int32_t cache_id = cache_id_val.As()->Value(); + if (cache_id != -1) { // Alias already cached, return the cached EVP_MD*. + return GetCachedMDByID(env, cache_id); } - if (algorithm_str.empty()) { // It could be unsupported algorithms. - Utf8Value utf8(env->isolate(), algorithm); - algorithm_str = utf8.ToString(); + // Only decode the algorithm when we don't have it cached to avoid + // unnecessary overhead. + Isolate* isolate = env->isolate(); + Utf8Value utf8(isolate, algorithm); + + auto result = FetchAndMaybeCacheMD(env, *utf8); + if (result.cache_id != -1) { + // Add the alias to both C++ side and JS side to speedup the lookup + // next time. + env->alias_to_md_id_map.emplace(*utf8, result.cache_id); + if (algorithm_cache.As() + ->Set(isolate->GetCurrentContext(), + algorithm, + v8::Int32::New(isolate, result.cache_id)) + .IsNothing()) { + return nullptr; + } } - const EVP_MD* implicit_md = EVP_get_digestbyname(algorithm_str.c_str()); - return implicit_md; + + return result.explicit_md ? result.explicit_md : result.implicit_md; +#else + Utf8Value utf8(env->isolate(), algorithm); + return EVP_get_digestbyname(*utf8); +#endif } void Hash::Initialize(Environment* env, Local target) { @@ -130,6 +215,7 @@ void Hash::Initialize(Environment* env, Local target) { SetConstructorFunction(context, target, "Hash", t); SetMethodNoSideEffect(context, target, "getHashes", GetHashes); + SetMethodNoSideEffect(context, target, "getCachedAliases", GetCachedAliases); HashJob::Initialize(env, target); @@ -142,12 +228,14 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(HashUpdate); registry->Register(HashDigest); registry->Register(GetHashes); + registry->Register(GetCachedAliases); HashJob::RegisterExternalReferences(registry); registry->Register(InternalVerifyIntegrity); } +// new Hash(algorithm, algorithmId, xofLen, algorithmCache) void Hash::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -157,13 +245,13 @@ void Hash::New(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As()); md = EVP_MD_CTX_md(orig->mdctx_.get()); } else { - md = GetDigestImplementation(env, args[0], args[1]); + md = GetDigestImplementation(env, args[0], args[2], args[3]); } Maybe xof_md_len = Nothing(); - if (!args[2]->IsUndefined()) { - CHECK(args[2]->IsUint32()); - xof_md_len = Just(args[2].As()->Value()); + if (!args[1]->IsUndefined()) { + CHECK(args[1]->IsUint32()); + xof_md_len = Just(args[1].As()->Value()); } Hash* hash = new Hash(env, args.This()); diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 8bbf5dd06ec596..a90acc895b97b2 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -25,6 +25,7 @@ class Hash final : public BaseObject { bool HashUpdate(const char* data, size_t len); static void GetHashes(const v8::FunctionCallbackInfo& args); + static void GetCachedAliases(const v8::FunctionCallbackInfo& args); protected: static void New(const v8::FunctionCallbackInfo& args); diff --git a/src/env.h b/src/env.h index 70dd05aacabe96..ee8534bb1a7b6e 100644 --- a/src/env.h +++ b/src/env.h @@ -1022,13 +1022,15 @@ class Environment : public MemoryRetainer { kExitInfoFieldCount }; +#if HAVE_OPENSSL #if OPENSSL_VERSION_MAJOR >= 3 // We declare another alias here to avoid having to include crypto_util.h using EVPMDPointer = DeleteFnPtr; std::vector evp_md_cache; -#endif // OPENSSL_VERSION_MAJOR - +#endif // OPENSSL_VERSION_MAJOR >= 3 + std::unordered_map alias_to_md_id_map; std::vector supported_hash_algorithms; +#endif // HAVE_OPENSSL private: // V8 has changed the constructor of exceptions, support both APIs before Node