@ 261106.10
current
#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 <openssl/evp.h>

#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
#include <openssl/digest.h>
#endif

#include <algorithm>
#include <climits>
#include <cstdio>
#include <string_view>
#include <utility>

namespace node {

using ncrypto::DataPointer;
using ncrypto::EVPMDCtxPointer;
using ncrypto::MarkPopErrorOnReturn;
using v8::ArrayBuffer;
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::Null;
using v8::Object;
using v8::String;
using v8::Uint32;
using v8::Uint8Array;
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->TraitTrackInline(digest_, "md");
}

#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
struct BoringSSLDigest {
  const EVP_MD* (*get)();
  const char* name;
};

constexpr BoringSSLDigest kBoringSSLDigests[] = {
    {EVP_md4, "md4"},
    {EVP_md5, "md5"},
    {EVP_sha1, "sha1"},
    {EVP_sha224, "sha224"},
    {EVP_sha256, "sha256"},
    {EVP_sha384, "sha384"},
    {EVP_sha512, "sha512"},
    {EVP_sha512_256, "sha512-256"},
};
#endif

void ResetHashCache(Environment* env,
                    uint64_t generation,
                    Local<Object> algorithm_cache = Local<Object>()) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  ncrypto::DigestCache* cache = env->provider_digest_cache.get();
  CHECK_NOT_NULL(cache);
  if (!algorithm_cache.IsEmpty()) {
    Isolate* isolate = env->isolate();
    Local<Context> context = isolate->GetCurrentContext();
    for (const auto& entry : cache->aliases()) {
      if (algorithm_cache
              ->Set(context,
                    OneByteString(isolate, entry.first),
                    Int32::New(isolate, -1))
              .IsNothing()) {
        return;
      }
    }
  }
  cache->reset(generation);
#endif
  env->supported_hash_algorithms.clear();
  env->hash_cache_generation = generation;
}

bool SynchronizeHashCache(Environment* env,
                          Local<Object> algorithm_cache = Local<Object>()) {
  const uint64_t generation = ncrypto::getFipsStateGeneration();
  if (env->hash_cache_generation == generation) return false;

  ResetHashCache(env, generation, algorithm_cache);
  return true;
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
const EVP_MD* GetCachedMDByID(Environment* env,
                              int32_t id,
                              Local<Object> algorithm_cache = Local<Object>()) {
  if (SynchronizeHashCache(env, algorithm_cache) ||
      env->provider_digest_cache == nullptr) {
    return nullptr;
  }
  return env->provider_digest_cache->lookup(id, env->hash_cache_generation)
      .digest;
}

struct MaybeCachedMD {
  const EVP_MD* cached_md = nullptr;
  ncrypto::Digest digest;
  int32_t cache_id = -1;
};

MaybeCachedMD FetchAndMaybeCacheMD(
    Environment* env,
    const char* search_name,
    Local<Object> algorithm_cache = Local<Object>(),
    const char* fetch_name = nullptr) {
  SynchronizeHashCache(env, algorithm_cache);
  if (env->isolate()->HasPendingException()) return {};
  ncrypto::DigestCache* cache = env->provider_digest_cache.get();
  CHECK_NOT_NULL(cache);
  const uint64_t generation = env->hash_cache_generation;
  const EVP_MD* legacy = nullptr;

  if (auto cached = cache->lookup(search_name, generation);
      cached.digest != nullptr) {
    return {cached.digest, cached.digest, cached.id};
  }

  if (fetch_name == nullptr) {
    legacy = ncrypto::getDigestByName(search_name);
    if (legacy != nullptr) {
      if (legacy == EVP_md_null()) return {};
      fetch_name = EVP_MD_get0_name(legacy);
      if (fetch_name == nullptr) return {nullptr, legacy, -1};
    } else {
      fetch_name = search_name;
    }
  }

  const ncrypto::Digest digest = ncrypto::Digest::Fetch(fetch_name);
  if (!digest) {
    return legacy == nullptr ? MaybeCachedMD{}
                             : MaybeCachedMD{nullptr, legacy, -1};
  }

  if (generation == ncrypto::getFipsStateGeneration()) {
    auto cached = cache->insert(search_name, digest.get(), generation);
    if (cached.digest != nullptr) {
      return {cached.digest, cached.digest, cached.id};
    }
  }

  return {nullptr, digest, -1};
}

void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md,
                                           const char* from,
                                           const char* to,
                                           void* arg) {
  if (!from) return;
  Environment* env = static_cast<Environment*>(arg);
  const ncrypto::Digest legacy = ncrypto::Digest::FromName(from);
  const char* canonical_name = legacy ? EVP_MD_get0_name(legacy) : nullptr;
  if (canonical_name == nullptr) return;

  auto result = FetchAndMaybeCacheMD(env, from, {}, canonical_name);
  if (result.cached_md || result.digest) {
    env->supported_hash_algorithms.push_back(from);
  }
}

struct ProviderHashNameContext {
  Environment* env;
};

void SaveSupportedProviderHashName(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<char>(c + ('a' - 'A'));
                   }
                   return static_cast<char>(c);
                 });

  auto* context = static_cast<ProviderHashNameContext*>(arg);
  auto result = FetchAndMaybeCacheMD(
      context->env, normalized_name.c_str(), {}, normalized_name.c_str());
  if (result.cached_md || result.digest) {
    context->env->supported_hash_algorithms.push_back(normalized_name);
  }
}

void SaveSupportedProviderHashAlgorithms(EVP_MD* md, void* arg) {
  ProviderHashNameContext context = {
      .env = static_cast<Environment*>(arg),
  };
  EVP_MD_names_do_all(md, SaveSupportedProviderHashName, &context);
}

#elif NCRYPTO_USE_BORINGSSL
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);
}
#else
#error "Unsupported crypto backend"
#endif  // NCRYPTO_USE_OPENSSL_PROVIDER

const std::vector<std::string>& GetSupportedHashAlgorithms(Environment* env) {
  while (true) {
    SynchronizeHashCache(env);
    const uint64_t generation = env->hash_cache_generation;
    if (env->supported_hash_algorithms.empty()) {
      MarkPopErrorOnReturn mark_pop_error_on_return;
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
      for (const auto& digest : kBoringSSLDigests) {
        static_cast<void>(digest.get);
        env->supported_hash_algorithms.emplace_back(digest.name);
      }
#elif NCRYPTO_USE_OPENSSL_PROVIDER
      // 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);
      EVP_MD_do_all_provided(nullptr, SaveSupportedProviderHashAlgorithms, env);
#elif NCRYPTO_USE_BORINGSSL
      EVP_MD_do_all_sorted(SaveSupportedHashAlgorithms, env);
#else
#error "Unsupported crypto backend"
#endif
#if NCRYPTO_USE_BORINGSSL
      // SHA-3 descriptors are not included in BoringSSL's name registry.
      for (const char* name : {"sha3-256", "sha3-384", "sha3-512"}) {
        env->supported_hash_algorithms.emplace_back(name);
      }
#endif
    }
    const uint64_t current_generation = ncrypto::getFipsStateGeneration();
    if (generation == current_generation) {
      return env->supported_hash_algorithms;
    }
    ResetHashCache(env, current_generation);
  }
}

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);
  SynchronizeHashCache(env);
  size_t size = 0;
  LocalVector<Name> names(isolate);
  LocalVector<Value> values(isolate);
#if NCRYPTO_USE_OPENSSL_PROVIDER
  const auto& aliases = env->provider_digest_cache->aliases();
  size = aliases.size();
  names.reserve(size);
  values.reserve(size);
  for (const auto& [alias, id] : aliases) {
    names.push_back(OneByteString(isolate, alias));
    values.push_back(Int32::New(isolate, id));
  }
#endif
  Local<Value> prototype = 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,
    std::optional<ncrypto::Digest>& digest_owner) {
  CHECK(algorithm->IsString());
  CHECK(cache_id_val->IsInt32());
  CHECK(algorithm_cache->IsObject());
  DCHECK(!digest_owner.has_value());

#if NCRYPTO_USE_OPENSSL_PROVIDER
  Local<Object> cache = algorithm_cache.As<Object>();
  int32_t cache_id = cache_id_val.As<Int32>()->Value();
  if (cache_id != -1) {
    // Alias already cached, return the cached EVP_MD*.
    if (const EVP_MD* md = GetCachedMDByID(env, cache_id, cache)) return md;
    if (env->isolate()->HasPendingException()) return nullptr;
  }

  // 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, cache);
  if (env->isolate()->HasPendingException()) return nullptr;
  if (result.cache_id != -1) {
    // Add the alias to the JavaScript side to speed up the next lookup. The
    // native cache added it while inserting the implementation.
    if (algorithm_cache.As<Object>()
            ->Set(isolate->GetCurrentContext(),
                  algorithm,
                  Int32::New(isolate, result.cache_id))
            .IsNothing()) {
      return nullptr;
    }
  }

  if (result.cached_md != nullptr) return result.cached_md;
  if (result.digest) {
    digest_owner.emplace(result.digest);
    return digest_owner->get();
  }
  return nullptr;
#elif NCRYPTO_USE_BORINGSSL
  Utf8Value utf8(env->isolate(), algorithm);
  return ncrypto::getDigestByName(*utf8);
#else
#error "Unsupported crypto backend"
#endif
}

const ncrypto::Digest GetDigest(Environment* env, const char* name) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  MaybeCachedMD result = FetchAndMaybeCacheMD(env, name);
  if (result.cached_md != nullptr) {
    return ncrypto::Digest::Ref(result.cached_md);
  }
  return result.digest;
#elif NCRYPTO_USE_BORINGSSL
  return ncrypto::Digest::FromName(name);
#endif
}

const ncrypto::Digest GetDigestForJob(Environment* env,
                                      CryptoJobMode mode,
                                      const char* name) {
  if (mode == kCryptoJobSync) return GetDigest(env, name);
  return ncrypto::Digest::FromName(name);
}

ncrypto::HMACCtxPointer NewHmacContext(Environment* env,
                                       const ncrypto::Digest& md) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  SynchronizeHashCache(env);
  if (const auto* hmac_template = env->provider_digest_cache->getHmacTemplate(
          md, env->hash_cache_generation)) {
    if (auto ctx = hmac_template->clone()) return ctx;
  }
#endif
  return ncrypto::HMACCtxPointer::New();
}

void MarkInvalidXofLength() {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  ERR_raise(ERR_LIB_EVP, EVP_R_NOT_XOF_OR_INVALID_LENGTH);
#elif NCRYPTO_USE_BORINGSSL
  EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH);
#else
#error "Unsupported crypto backend"
#endif
}

// DEP0198 EOL requires XOFs without an OpenSSL-defined default output length
// to fail when outputLength is omitted. OpenSSL 3.4 and later report a digest
// size of 0 for such XOFs, including SHAKE, which had weak historical defaults
// before OpenSSL 3.4. For older OpenSSL versions, identify those resolved
// EVP_MD values explicitly to keep the missing-outputLength error
// version-independent.
#if !OPENSSL_VERSION_PREREQ(3, 4)
bool IsShakeDigest(const EVP_MD* md) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVP_MD_is_a(md, "SHAKE128") || EVP_MD_is_a(md, "SHAKE256");
#elif NCRYPTO_USE_BORINGSSL
  const char* name = OBJ_nid2sn(EVP_MD_type(md));
  return name != nullptr &&
         (strcmp(name, "SHAKE128") == 0 || strcmp(name, "SHAKE256") == 0);
#else
#error "Unsupported crypto backend"
#endif
}
#endif

bool ShouldRejectMissingXofLength(const EVP_MD* md, size_t default_length) {
  if (default_length == 0) return true;

#if !OPENSSL_VERSION_PREREQ(3, 4)
  return IsShakeDigest(md);
#else
  static_cast<void>(md);
  return false;
#endif
}

void OneShotDigestWithMD(Environment* env,
                         const FunctionCallbackInfo<Value>& args,
                         const EVP_MD* md,
                         const CShakeOptions* options) {
  Isolate* isolate = env->isolate();
  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;
  OPENSSL_SIZE_T 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 (ShouldRejectMissingXofLength(md, output_length)) {
      MarkInvalidXofLength();
      return ThrowCryptoError(
          env, ERR_get_error(), "Digest method not supported");
    }
  }

  auto return_empty_output = [&]() {
    if (output_enc == BUFFER) {
      Local<Uint8Array> u8;
      if (Buffer::New(isolate, ArrayBuffer::New(isolate, 0), 0, 0)
              .ToLocal(&u8)) {
        args.GetReturnValue().Set(u8);
      }
    } else {
      args.GetReturnValue().Set(String::Empty(isolate));
    }
  };

  const bool has_digest_options = options != nullptr && !options->empty();
  if (output_length == 0 && !has_digest_options) {
    return return_empty_output();
  }

  if (!has_digest_options) {
    DataPointer output = ([&]() -> DataPointer {
      if (args[3]->IsString()) {
        Utf8Value utf8(isolate, args[3]);
        ncrypto::Buffer<const unsigned char> input = {
            .data = reinterpret_cast<const unsigned char*>(utf8.out()),
            .len = static_cast<size_t>(utf8.length()),
        };
        return is_xof ? ncrypto::xofHashDigest(input, md, output_length)
                      : ncrypto::hashDigest(input, md);
      }

      ArrayBufferViewContents<unsigned char> input(args[3]);
      ncrypto::Buffer<const unsigned char> buffer = {
          .data = input.data(),
          .len = input.length(),
      };
      return is_xof ? ncrypto::xofHashDigest(buffer, md, output_length)
                    : ncrypto::hashDigest(buffer, 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);
    }
    return;
  }

  EVPMDCtxPointer ctx = EVPMDCtxPointer::New();
  if (!options->Initialize(&ctx, md)) {
    return ThrowCryptoError(
        env, ERR_get_error(), "Digest options are not supported");
  }

  const bool updated = [&]() {
    if (args[3]->IsString()) {
      Utf8Value input(isolate, args[3]);
      return ctx.digestUpdate(ncrypto::Buffer<const void>{
          .data = input.out(),
          .len = static_cast<size_t>(input.length()),
      });
    }

    ArrayBufferViewContents<unsigned char> input(args[3]);
    return ctx.digestUpdate(ncrypto::Buffer<const void>{
        .data = input.data(),
        .len = input.length(),
    });
  }();
  if (!updated) return ThrowCryptoError(env, ERR_get_error());
  if (output_length == 0) return return_empty_output();
  DataPointer output = ctx.digestFinal(output_length);

  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);
  }
}

// crypto.digest(algorithm, algorithmId, algorithmCache, input, outputEncoding,
//               outputEncodingId, outputLength[, functionName, customization])
void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args.Length() == 7 || args.Length() == 9);
  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

  if (args.Length() == 7) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
    const int32_t cache_id = args[1].As<Int32>()->Value();
    if (cache_id != -1) {
      if (const EVP_MD* md =
              GetCachedMDByID(env, cache_id, args[2].As<Object>())) {
        return OneShotDigestWithMD(env, args, md, nullptr);
      }
      if (env->isolate()->HasPendingException()) return;
    }
#elif NCRYPTO_USE_BORINGSSL
    Utf8Value utf8(env->isolate(), args[0]);
    return OneShotDigestWithMD(
        env, args, ncrypto::getDigestByName(*utf8), nullptr);
#else
#error "Unsupported crypto backend"
#endif
  }

  if (args.Length() == 9) {
    CShakeOptions options;
    if (GetCShakeOptions(args, 7, &options).IsNothing()) return;

    std::optional<ncrypto::Digest> digest_owner;
    const EVP_MD* md =
        GetDigestImplementation(env, args[0], args[1], args[2], digest_owner);
    if (env->isolate()->HasPendingException()) return;
    return OneShotDigestWithMD(env, args, md, &options);
  }

  std::optional<ncrypto::Digest> digest_owner;
  const EVP_MD* md =
      GetDigestImplementation(env, args[0], args[1], args[2], digest_owner);
  if (env->isolate()->HasPendingException()) return;
  OneShotDigestWithMD(env, args, md, nullptr);
}

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, xofLen, algorithmId, algorithmCache[, functionName,
//          customization])
void Hash::New(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  CHECK(args.Length() == 4 || args.Length() == 6);

  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());
  }

#if NCRYPTO_USE_OPENSSL_PROVIDER
  // This is the common path after the first lookup. Avoid constructing a
  // digest owner when the Environment already owns the cached implementation.
  if (args.Length() == 4 && args[0]->IsString()) {
    CHECK(args[2]->IsInt32());
    const int32_t cache_id = args[2].As<Int32>()->Value();
    if (cache_id != -1) {
      if (const EVP_MD* md =
              GetCachedMDByID(env, cache_id, args[3].As<Object>())) {
        Hash* hash = new Hash(env, args.This());
        if (!hash->HashInit(md, xof_md_len)) {
          return ThrowCryptoError(
              env, ERR_get_error(), "Digest method not supported");
        }
        return;
      }
      if (env->isolate()->HasPendingException()) return;
    }
  }
#endif

  const Hash* orig = nullptr;
  std::optional<ncrypto::Digest> digest_owner;
  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], digest_owner);
    if (env->isolate()->HasPendingException()) return;
  }

  Hash* hash = new Hash(env, args.This());
  if (args.Length() == 4) {
    if (md == nullptr || !hash->HashInit(md, xof_md_len)) {
      return ThrowCryptoError(
          env, ERR_get_error(), "Digest method not supported");
    }
  } else {
    CShakeOptions options;
    if (GetCShakeOptions(args, 4, &options).IsNothing()) return;
    if (md == nullptr || !hash->HashInit(md, options, 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* digest, Maybe<unsigned int> xof_md_len) {
  mdctx_ = EVPMDCtxPointer::New();
  if (!mdctx_.digestInit(digest)) [[unlikely]] {
    mdctx_.reset();
    return false;
  }

  md_len_ = mdctx_.getDigestSize();
  if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() &&
      ShouldRejectMissingXofLength(digest, md_len_)) {
    MarkInvalidXofLength();
    mdctx_.reset();
    return false;
  }

  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]] {
      MarkInvalidXofLength();
      mdctx_.reset();
      return false;
    }
    md_len_ = xof_md_len.FromJust();
  }

  return true;
}

bool Hash::HashInit(const EVP_MD* digest,
                    const CShakeOptions& options,
                    Maybe<unsigned int> xof_md_len) {
  mdctx_ = EVPMDCtxPointer::New();
  if (!options.Initialize(&mdctx_, digest)) [[unlikely]] {
    mdctx_.reset();
    return false;
  }

  md_len_ = mdctx_.getDigestSize();
  if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() &&
      ShouldRejectMissingXofLength(digest, md_len_)) {
    MarkInvalidXofLength();
    mdctx_.reset();
    return false;
  }

  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]] {
      MarkInvalidXofLength();
      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
    : in(std::move(other.in)),
      digest(other.digest),
      options(std::move(other.options)),
      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 {
  tracker->TraitTrackInline(in, "in");
  if (options.has_value()) {
    tracker->TrackField("options", *options);
  }
}

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);

  CHECK(args[offset]->IsString());  // Hash algorithm
  Utf8Value digest(env->isolate(), args[offset]);
  params->digest = ncrypto::Digest::FromName(*digest);
  if (!params->digest) [[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 = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource();

  if (static_cast<unsigned int>(args.Length()) > offset + 3) {
    params->options.emplace();
    if (GetCShakeOptions(args, offset + 3, &*params->options).IsNothing()) {
      return Nothing<void>();
    }
  }

  unsigned int expected = EVP_MD_size(params->digest.get());
  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.get()) & 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,
                            CryptoErrorStore* errors) {
  auto ctx = EVPMDCtxPointer::New();

  const bool initialized =
      params.options.has_value()
          ? params.options->Initialize(&ctx, params.digest.get())
          : ctx.digestInit(params.digest.get());
  if (!initialized || !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