| #include "crypto/crypto_hash.h" |
| #include "async_wrap-inl.h" |
| #include "base_object-inl.h" |
| #include "env-inl.h" |
| #include "memory_tracker-inl.h" |
| #include "string_bytes.h" |
| #include "threadpoolwork-inl.h" |
| #include "v8.h" |
| |
| #include <cstdio> |
| |
| namespace node { |
| |
| using ncrypto::DataPointer; |
| using ncrypto::EVPMDCtxPointer; |
| using ncrypto::MarkPopErrorOnReturn; |
| using v8::Context; |
| using v8::FunctionCallbackInfo; |
| using v8::FunctionTemplate; |
| using v8::Int32; |
| using v8::Isolate; |
| using v8::Just; |
| using v8::JustVoid; |
| using v8::Local; |
| using v8::LocalVector; |
| using v8::Maybe; |
| using v8::MaybeLocal; |
| using v8::Name; |
| using v8::Nothing; |
| using v8::Object; |
| using v8::Uint32; |
| using v8::Value; |
| |
| namespace crypto { |
| Hash::Hash(Environment* env, Local<Object> wrap) : BaseObject(env, wrap) { |
| MakeWeak(); |
| } |
| |
| void Hash::MemoryInfo(MemoryTracker* tracker) const { |
| tracker->TrackFieldWithSize("mdctx", mdctx_ ? kSizeOf_EVP_MD_CTX : 0); |
| tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); |
| } |
| |
| #if OPENSSL_VERSION_MAJOR >= 3 |
| void PushAliases(const char* name, void* data) { |
| static_cast<std::vector<std::string>*>(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 = ncrypto::getDigestByName(search_name); |
| if (!implicit_md) return {nullptr, nullptr, -1}; |
| |
| const char* real_name = EVP_MD_get0_name(implicit_md); |
| 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<int32_t>(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 |
| // 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 {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<std::string> 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<int32_t>(id)}; |
| } |
| |
| void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md, |
| const char* from, |
| const char* to, |
| void* arg) { |
| if (!from) return; |
| Environment* env = static_cast<Environment*>(arg); |
| auto result = FetchAndMaybeCacheMD(env, from); |
| if (result.explicit_md) { |
| env->supported_hash_algorithms.push_back(from); |
| } |
| } |
| |
| #else |
| void SaveSupportedHashAlgorithms(const EVP_MD* md, |
| const char* from, |
| const char* to, |
| void* arg) { |
| if (!from) return; |
| Environment* env = static_cast<Environment*>(arg); |
| env->supported_hash_algorithms.push_back(from); |
| } |
| #endif // OPENSSL_VERSION_MAJOR >= 3 |
| |
| const std::vector<std::string>& GetSupportedHashAlgorithms(Environment* env) { |
| if (env->supported_hash_algorithms.empty()) { |
| MarkPopErrorOnReturn mark_pop_error_on_return; |
| #if 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; |
| } |
| |
| void Hash::GetHashes(const FunctionCallbackInfo<Value>& args) { |
| Local<Context> context = args.GetIsolate()->GetCurrentContext(); |
| Environment* env = Environment::GetCurrent(context); |
| const std::vector<std::string>& results = GetSupportedHashAlgorithms(env); |
| |
| Local<Value> ret; |
| if (ToV8Value(context, results).ToLocal(&ret)) { |
| args.GetReturnValue().Set(ret); |
| } |
| } |
| |
| void Hash::GetCachedAliases(const FunctionCallbackInfo<Value>& args) { |
| Isolate* isolate = args.GetIsolate(); |
| Local<Context> context = args.GetIsolate()->GetCurrentContext(); |
| Environment* env = Environment::GetCurrent(context); |
| size_t size = env->alias_to_md_id_map.size(); |
| LocalVector<Name> names(isolate); |
| LocalVector<Value> values(isolate); |
| #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)); |
| values.push_back(v8::Uint32::New(isolate, id)); |
| } |
| #else |
| CHECK(env->alias_to_md_id_map.empty()); |
| #endif |
| Local<Value> prototype = v8::Null(isolate); |
| Local<Object> result = |
| Object::New(isolate, prototype, names.data(), values.data(), size); |
| args.GetReturnValue().Set(result); |
| } |
| |
| const EVP_MD* GetDigestImplementation(Environment* env, |
| Local<Value> algorithm, |
| Local<Value> cache_id_val, |
| Local<Value> algorithm_cache) { |
| CHECK(algorithm->IsString()); |
| CHECK(cache_id_val->IsInt32()); |
| CHECK(algorithm_cache->IsObject()); |
| |
| #if OPENSSL_VERSION_MAJOR >= 3 |
| int32_t cache_id = cache_id_val.As<Int32>()->Value(); |
| if (cache_id != -1) { // Alias already cached, return the cached EVP_MD*. |
| return GetCachedMDByID(env, cache_id); |
| } |
| |
| // 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<Object>() |
| ->Set(isolate->GetCurrentContext(), |
| algorithm, |
| v8::Int32::New(isolate, result.cache_id)) |
| .IsNothing()) { |
| return nullptr; |
| } |
| } |
| |
| return result.explicit_md ? result.explicit_md : result.implicit_md; |
| #else |
| Utf8Value utf8(env->isolate(), algorithm); |
| return ncrypto::getDigestByName(*utf8); |
| #endif |
| } |
| |
| // crypto.digest(algorithm, algorithmId, algorithmCache, |
| // input, outputEncoding, outputEncodingId, outputLength) |
| void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) { |
| Environment* env = Environment::GetCurrent(args); |
| Isolate* isolate = env->isolate(); |
| CHECK_EQ(args.Length(), 7); |
| CHECK(args[0]->IsString()); // algorithm |
| CHECK(args[1]->IsInt32()); // algorithmId |
| CHECK(args[2]->IsObject()); // algorithmCache |
| CHECK(args[3]->IsString() || args[3]->IsArrayBufferView()); // input |
| CHECK(args[4]->IsString()); // outputEncoding |
| CHECK(args[5]->IsUint32() || args[5]->IsUndefined()); // outputEncodingId |
| CHECK(args[6]->IsUint32() || args[6]->IsUndefined()); // outputLength |
| |
| const EVP_MD* md = GetDigestImplementation(env, args[0], args[1], args[2]); |
| if (md == nullptr) [[unlikely]] { |
| Utf8Value method(isolate, args[0]); |
| std::string message = |
| "Digest method " + method.ToString() + " is not supported"; |
| return ThrowCryptoError(env, ERR_get_error(), message.c_str()); |
| } |
| |
| enum encoding output_enc = ParseEncoding(isolate, args[4], args[5], HEX); |
| |
| bool is_xof = (EVP_MD_flags(md) & EVP_MD_FLAG_XOF) != 0; |
| int output_length = EVP_MD_size(md); |
| |
| // This is to cause hash() to fail when an incorrect |
| // outputLength option was passed for a non-XOF hash function. |
| if (!is_xof && !args[6]->IsUndefined()) { |
| output_length = args[6].As<Uint32>()->Value(); |
| if (output_length != EVP_MD_size(md)) { |
| Utf8Value method(isolate, args[0]); |
| std::string message = |
| "Output length " + std::to_string(output_length) + " is invalid for "; |
| message += method.ToString() + ", which does not support XOF"; |
| return ThrowCryptoError(env, ERR_get_error(), message.c_str()); |
| } |
| } else if (is_xof) { |
| if (!args[6]->IsUndefined()) { |
| output_length = args[6].As<Uint32>()->Value(); |
| } else if (output_length == 0) { |
| // This is to handle OpenSSL 3.4's breaking change in SHAKE128/256 |
| // default lengths |
| // TODO(@panva): remove this behaviour when DEP0198 is End-Of-Life |
| const char* name = OBJ_nid2sn(EVP_MD_type(md)); |
| if (name != nullptr) { |
| if (strcmp(name, "SHAKE128") == 0) { |
| output_length = 16; |
| } else if (strcmp(name, "SHAKE256") == 0) { |
| output_length = 32; |
| } |
| } |
| } |
| } |
| |
| if (output_length == 0) { |
| if (output_enc == BUFFER) { |
| Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 0); |
| args.GetReturnValue().Set( |
| Buffer::New(isolate, ab, 0, 0).ToLocalChecked()); |
| } else { |
| args.GetReturnValue().Set(v8::String::Empty(isolate)); |
| } |
| return; |
| } |
| |
| DataPointer output = ([&]() -> DataPointer { |
| if (args[3]->IsString()) { |
| Utf8Value utf8(isolate, args[3]); |
| ncrypto::Buffer<const unsigned char> buf = { |
| .data = reinterpret_cast<const unsigned char*>(utf8.out()), |
| .len = utf8.length(), |
| }; |
| return is_xof ? ncrypto::xofHashDigest(buf, md, output_length) |
| : ncrypto::hashDigest(buf, md); |
| } |
| |
| ArrayBufferViewContents<unsigned char> input(args[3]); |
| ncrypto::Buffer<const unsigned char> buf = { |
| .data = reinterpret_cast<const unsigned char*>(input.data()), |
| .len = input.length(), |
| }; |
| return is_xof ? ncrypto::xofHashDigest(buf, md, output_length) |
| : ncrypto::hashDigest(buf, md); |
| })(); |
| |
| if (!output) [[unlikely]] { |
| return ThrowCryptoError(env, ERR_get_error()); |
| } |
| |
| Local<Value> ret; |
| if (StringBytes::Encode(env->isolate(), |
| static_cast<const char*>(output.get()), |
| output.size(), |
| output_enc) |
| .ToLocal(&ret)) { |
| args.GetReturnValue().Set(ret); |
| } |
| } |
| |
| void Hash::Initialize(Environment* env, Local<Object> target) { |
| Isolate* isolate = env->isolate(); |
| Local<Context> context = env->context(); |
| Local<FunctionTemplate> t = NewFunctionTemplate(isolate, New); |
| |
| t->InstanceTemplate()->SetInternalFieldCount(Hash::kInternalFieldCount); |
| |
| SetProtoMethod(isolate, t, "update", HashUpdate); |
| SetProtoMethod(isolate, t, "digest", HashDigest); |
| |
| SetConstructorFunction(context, target, "Hash", t); |
| |
| SetMethodNoSideEffect(context, target, "getHashes", GetHashes); |
| SetMethodNoSideEffect(context, target, "getCachedAliases", GetCachedAliases); |
| SetMethodNoSideEffect(context, target, "oneShotDigest", OneShotDigest); |
| |
| HashJob::Initialize(env, target); |
| } |
| |
| void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { |
| registry->Register(New); |
| registry->Register(HashUpdate); |
| registry->Register(HashDigest); |
| registry->Register(GetHashes); |
| registry->Register(GetCachedAliases); |
| registry->Register(OneShotDigest); |
| |
| HashJob::RegisterExternalReferences(registry); |
| } |
| |
| // new Hash(algorithm, algorithmId, xofLen, algorithmCache) |
| void Hash::New(const FunctionCallbackInfo<Value>& args) { |
| Environment* env = Environment::GetCurrent(args); |
| |
| const Hash* orig = nullptr; |
| const EVP_MD* md = nullptr; |
| if (args[0]->IsObject()) { |
| ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As<Object>()); |
| CHECK_NOT_NULL(orig); |
| md = orig->mdctx_.getDigest(); |
| } else { |
| md = GetDigestImplementation(env, args[0], args[2], args[3]); |
| } |
| |
| Maybe<unsigned int> xof_md_len = Nothing<unsigned int>(); |
| if (!args[1]->IsUndefined()) { |
| CHECK(args[1]->IsUint32()); |
| xof_md_len = Just<unsigned int>(args[1].As<Uint32>()->Value()); |
| } |
| |
| Hash* hash = new Hash(env, args.This()); |
| if (md == nullptr || !hash->HashInit(md, xof_md_len)) { |
| return ThrowCryptoError(env, ERR_get_error(), |
| "Digest method not supported"); |
| } |
| |
| if (orig != nullptr && !orig->mdctx_.copyTo(hash->mdctx_)) { |
| return ThrowCryptoError(env, ERR_get_error(), "Digest copy error"); |
| } |
| } |
| |
| bool Hash::HashInit(const EVP_MD* md, Maybe<unsigned int> xof_md_len) { |
| mdctx_ = EVPMDCtxPointer::New(); |
| if (!mdctx_.digestInit(md)) [[unlikely]] { |
| mdctx_.reset(); |
| return false; |
| } |
| |
| md_len_ = mdctx_.getDigestSize(); |
| |
| // This is to handle OpenSSL 3.4's breaking change in SHAKE128/256 |
| // default lengths |
| // TODO(@panva): remove this behaviour when DEP0198 is End-Of-Life |
| if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() && md_len_ == 0) { |
| const char* name = OBJ_nid2sn(EVP_MD_type(md)); |
| if (name != nullptr) { |
| if (strcmp(name, "SHAKE128") == 0) { |
| md_len_ = 16; |
| } else if (strcmp(name, "SHAKE256") == 0) { |
| md_len_ = 32; |
| } |
| } |
| } |
| |
| if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) { |
| // This is a little hack to cause createHash to fail when an incorrect |
| // hashSize option was passed for a non-XOF hash function. |
| if (!mdctx_.hasXofFlag()) [[unlikely]] { |
| EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH); |
| mdctx_.reset(); |
| return false; |
| } |
| md_len_ = xof_md_len.FromJust(); |
| } |
| |
| return true; |
| } |
| |
| bool Hash::HashUpdate(const char* data, size_t len) { |
| if (!mdctx_) return false; |
| return mdctx_.digestUpdate(ncrypto::Buffer<const void>{ |
| .data = data, |
| .len = len, |
| }); |
| } |
| |
| void Hash::HashUpdate(const FunctionCallbackInfo<Value>& args) { |
| Decode<Hash>(args, |
| [](Hash* hash, |
| const FunctionCallbackInfo<Value>& args, |
| const char* data, |
| size_t size) { |
| Environment* env = Environment::GetCurrent(args); |
| if (size > INT_MAX) [[unlikely]] |
| return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); |
| bool r = hash->HashUpdate(data, size); |
| args.GetReturnValue().Set(r); |
| }); |
| } |
| |
| void Hash::HashDigest(const FunctionCallbackInfo<Value>& args) { |
| Environment* env = Environment::GetCurrent(args); |
| |
| Hash* hash; |
| ASSIGN_OR_RETURN_UNWRAP(&hash, args.This()); |
| |
| enum encoding encoding = BUFFER; |
| if (args.Length() >= 1) { |
| encoding = ParseEncoding(env->isolate(), args[0], BUFFER); |
| } |
| |
| unsigned int len = hash->md_len_; |
| |
| // TODO(tniessen): SHA3_squeeze does not work for zero-length outputs on all |
| // platforms and will cause a segmentation fault if called. This workaround |
| // causes hash.digest() to correctly return an empty buffer / string. |
| // See https://github.com/openssl/openssl/issues/9431. |
| |
| if (!hash->digest_ && len > 0) { |
| // Some hash algorithms such as SHA3 do not support calling |
| // EVP_DigestFinal_ex more than once, however, Hash._flush |
| // and Hash.digest can both be used to retrieve the digest, |
| // so we need to cache it. |
| // See https://github.com/nodejs/node/issues/28245. |
| auto data = hash->mdctx_.digestFinal(len); |
| if (!data) [[unlikely]] { |
| return ThrowCryptoError(env, ERR_get_error()); |
| } |
| DCHECK(!data.isSecure()); |
| |
| hash->digest_ = ByteSource::Allocated(data.release()); |
| } |
| |
| Local<Value> ret; |
| if (StringBytes::Encode( |
| env->isolate(), hash->digest_.data<char>(), len, encoding) |
| .ToLocal(&ret)) { |
| args.GetReturnValue().Set(ret); |
| } |
| } |
| |
| HashConfig::HashConfig(HashConfig&& other) noexcept |
| : mode(other.mode), |
| in(std::move(other.in)), |
| digest(other.digest), |
| length(other.length) {} |
| |
| HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { |
| if (&other == this) return *this; |
| this->~HashConfig(); |
| return *new (this) HashConfig(std::move(other)); |
| } |
| |
| void HashConfig::MemoryInfo(MemoryTracker* tracker) const { |
| // If the Job is sync, then the HashConfig does not own the data. |
| if (mode == kCryptoJobAsync) |
| tracker->TrackFieldWithSize("in", in.size()); |
| } |
| |
| MaybeLocal<Value> HashTraits::EncodeOutput(Environment* env, |
| const HashConfig& params, |
| ByteSource* out) { |
| return out->ToArrayBuffer(env); |
| } |
| |
| Maybe<void> HashTraits::AdditionalConfig( |
| CryptoJobMode mode, |
| const FunctionCallbackInfo<Value>& args, |
| unsigned int offset, |
| HashConfig* params) { |
| Environment* env = Environment::GetCurrent(args); |
| |
| params->mode = mode; |
| |
| CHECK(args[offset]->IsString()); // Hash algorithm |
| Utf8Value digest(env->isolate(), args[offset]); |
| params->digest = ncrypto::getDigestByName(*digest); |
| if (params->digest == nullptr) [[unlikely]] { |
| THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", digest); |
| return Nothing<void>(); |
| } |
| |
| ArrayBufferOrViewContents<char> data(args[offset + 1]); |
| if (!data.CheckSizeInt32()) [[unlikely]] { |
| THROW_ERR_OUT_OF_RANGE(env, "data is too big"); |
| return Nothing<void>(); |
| } |
| params->in = mode == kCryptoJobAsync |
| ? data.ToCopy() |
| : data.ToByteSource(); |
| |
| unsigned int expected = EVP_MD_size(params->digest); |
| params->length = expected; |
| if (args[offset + 2]->IsUint32()) [[unlikely]] { |
| // length is expressed in terms of bits |
| params->length = |
| static_cast<uint32_t>(args[offset + 2] |
| .As<Uint32>()->Value()) / CHAR_BIT; |
| if (params->length != expected) { |
| if ((EVP_MD_flags(params->digest) & EVP_MD_FLAG_XOF) == 0) [[unlikely]] { |
| THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Digest method not supported"); |
| return Nothing<void>(); |
| } |
| } |
| } |
| |
| return JustVoid(); |
| } |
| |
| bool HashTraits::DeriveBits(Environment* env, |
| const HashConfig& params, |
| ByteSource* out, |
| CryptoJobMode mode) { |
| auto ctx = EVPMDCtxPointer::New(); |
| |
| if (!ctx.digestInit(params.digest) || !ctx.digestUpdate(params.in)) |
| [[unlikely]] { |
| return false; |
| } |
| |
| if (params.length > 0) [[likely]] { |
| auto data = ctx.digestFinal(params.length); |
| if (!data) [[unlikely]] |
| return false; |
| |
| DCHECK(!data.isSecure()); |
| *out = ByteSource::Allocated(data.release()); |
| } |
| |
| return true; |
| } |
| |
| } // namespace crypto |
| } // namespace node |