From b89cd8d19a57ce7dcd040a74ecdb6fb13bea5766 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 28 Aug 2024 18:13:49 -0700 Subject: [PATCH] src: move hkdf, scrypto, pbkdf2 impl to ncrypto PR-URL: https://github.com/nodejs/node/pull/54651 Reviewed-By: Yagiz Nizipli Reviewed-By: Minwoo Jung --- deps/ncrypto/ncrypto.cc | 132 ++++++++++++++++++++++++++++ deps/ncrypto/ncrypto.h | 32 +++++++ src/crypto/crypto_hkdf.cc | 71 +++++---------- src/crypto/crypto_pbkdf2.cc | 39 ++++---- src/crypto/crypto_scrypt.cc | 58 ++++++------ test/parallel/test-crypto-scrypt.js | 2 +- 6 files changed, 231 insertions(+), 103 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index bf989369442084..eb3533bb4623b1 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #if OPENSSL_VERSION_MAJOR >= 3 @@ -1252,4 +1253,135 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, return out; } +// ============================================================================ +// KDF + +const EVP_MD* getDigestByName(const std::string_view name) { + return EVP_get_digestbyname(name.data()); +} + +bool checkHkdfLength(const EVP_MD* md, size_t length) { + // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as + // the output of the hash function. 255 is a hard limit because HKDF appends + // an 8-bit counter to each HMAC'd message, starting at 1. + static constexpr size_t kMaxDigestMultiplier = 255; + size_t max_length = EVP_MD_size(md) * kMaxDigestMultiplier; + if (length > max_length) return false; + return true; +} + +DataPointer hkdf(const EVP_MD* md, + const Buffer& key, + const Buffer& info, + const Buffer& salt, + size_t length) { + ClearErrorOnReturn clearErrorOnReturn; + + if (!checkHkdfLength(md, length) || + info.len > INT_MAX || + salt.len > INT_MAX) { + return {}; + } + + EVPKeyCtxPointer ctx = + EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr)); + if (!ctx || + !EVP_PKEY_derive_init(ctx.get()) || + !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) || + !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) { + return {}; + } + + std::string_view actual_salt; + static const char default_salt[EVP_MAX_MD_SIZE] = {0}; + if (salt.len > 0) { + actual_salt = {reinterpret_cast(salt.data), salt.len}; + } else { + actual_salt = {default_salt, static_cast(EVP_MD_size(md))}; + } + + // We do not use EVP_PKEY_HKDF_MODE_EXTRACT_AND_EXPAND because and instead + // implement the extraction step ourselves because EVP_PKEY_derive does not + // handle zero-length keys, which are required for Web Crypto. + // TODO: Once OpenSSL 1.1.1 support is dropped completely, and once BoringSSL + // is confirmed to support it, wen can hopefully drop this and use EVP_KDF + // directly which does support zero length keys. + unsigned char pseudorandom_key[EVP_MAX_MD_SIZE]; + unsigned pseudorandom_key_len = sizeof(pseudorandom_key); + + if (HMAC(md, + actual_salt.data(), + actual_salt.size(), + key.data, + key.len, + pseudorandom_key, + &pseudorandom_key_len) == nullptr) { + return {}; + } + if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) || + !EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), pseudorandom_key, pseudorandom_key_len)) { + return {}; + } + + auto buf = DataPointer::Alloc(length); + if (!buf) return {}; + + if (EVP_PKEY_derive(ctx.get(), static_cast(buf.get()), &length) <= 0) { + return {}; + } + + return buf; +} + +bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) { + return EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, nullptr, 0) == 1; +} + +DataPointer scrypt(const Buffer& pass, + const Buffer& salt, + uint64_t N, + uint64_t r, + uint64_t p, + uint64_t maxmem, + size_t length) { + ClearErrorOnReturn clearErrorOnReturn; + + if (pass.len > INT_MAX || + salt.len > INT_MAX) { + return {}; + } + + auto dp = DataPointer::Alloc(length); + if (dp && EVP_PBE_scrypt( + pass.data, pass.len, salt.data, salt.len, N, r, p, maxmem, + reinterpret_cast(dp.get()), length)) { + return dp; + } + + return {}; +} + +DataPointer pbkdf2(const EVP_MD* md, + const Buffer& pass, + const Buffer& salt, + uint32_t iterations, + size_t length) { + ClearErrorOnReturn clearErrorOnReturn; + + if (pass.len > INT_MAX || + salt.len > INT_MAX || + length > INT_MAX) { + return {}; + } + + auto dp = DataPointer::Alloc(length); + if (dp && PKCS5_PBKDF2_HMAC(pass.data, pass.len, salt.data, salt.len, + iterations, md, length, + reinterpret_cast(dp.get()))) { + return dp; + } + + return {}; +} + } // namespace ncrypto diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 9b6aecaaecd6f9..60bfce3ea8999e 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -588,6 +588,38 @@ BIOPointer ExportPublicKey(const char* input, size_t length); // The caller takes ownership of the returned Buffer Buffer ExportChallenge(const char* input, size_t length); +// ============================================================================ +// KDF + +const EVP_MD* getDigestByName(const std::string_view name); + +// Verify that the specified HKDF output length is valid for the given digest. +// The maximum length for HKDF output for a given digest is 255 times the +// hash size for the given digest algorithm. +bool checkHkdfLength(const EVP_MD* md, size_t length); + +DataPointer hkdf(const EVP_MD* md, + const Buffer& key, + const Buffer& info, + const Buffer& salt, + size_t length); + +bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem); + +DataPointer scrypt(const Buffer& pass, + const Buffer& salt, + uint64_t N, + uint64_t r, + uint64_t p, + uint64_t maxmem, + size_t length); + +DataPointer pbkdf2(const EVP_MD* md, + const Buffer& pass, + const Buffer& salt, + uint32_t iterations, + size_t length); + // ============================================================================ // Version metadata #define NCRYPTO_VERSION "0.0.1" diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc index 0dd9b42473ca73..aebb5d718dff9e 100644 --- a/src/crypto/crypto_hkdf.cc +++ b/src/crypto/crypto_hkdf.cc @@ -56,7 +56,7 @@ Maybe HKDFTraits::AdditionalConfig( CHECK(args[offset + 4]->IsUint32()); // Length Utf8Value hash(env->isolate(), args[offset]); - params->digest = EVP_get_digestbyname(*hash); + params->digest = ncrypto::getDigestByName(hash.ToStringView()); if (params->digest == nullptr) { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", *hash); return Nothing(); @@ -90,9 +90,7 @@ Maybe HKDFTraits::AdditionalConfig( // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as the // output of the hash function. 255 is a hard limit because HKDF appends an // 8-bit counter to each HMAC'd message, starting at 1. - constexpr size_t kMaxDigestMultiplier = 255; - size_t max_length = EVP_MD_size(params->digest) * kMaxDigestMultiplier; - if (params->length > max_length) { + if (!ncrypto::checkHkdfLength(params->digest, params->length)) { THROW_ERR_CRYPTO_INVALID_KEYLEN(env); return Nothing(); } @@ -104,53 +102,24 @@ bool HKDFTraits::DeriveBits( Environment* env, const HKDFConfig& params, ByteSource* out) { - EVPKeyCtxPointer ctx = - EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr)); - if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || - !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), params.digest) || - !EVP_PKEY_CTX_add1_hkdf_info( - ctx.get(), params.info.data(), params.info.size())) { - return false; - } - - // TODO(panva): Once support for OpenSSL 1.1.1 is dropped the whole - // of HKDFTraits::DeriveBits can be refactored to use - // EVP_KDF which does handle zero length key. - - std::string_view salt; - if (params.salt.size() != 0) { - salt = {params.salt.data(), params.salt.size()}; - } else { - static const char default_salt[EVP_MAX_MD_SIZE] = {0}; - salt = {default_salt, static_cast(EVP_MD_size(params.digest))}; - } - - // We do not use EVP_PKEY_HKDEF_MODE_EXTRACT_AND_EXPAND and instead implement - // the extraction step ourselves because EVP_PKEY_derive does not handle - // zero-length keys, which are required for Web Crypto. - unsigned char pseudorandom_key[EVP_MAX_MD_SIZE]; - unsigned int prk_len = sizeof(pseudorandom_key); - if (HMAC( - params.digest, - salt.data(), - salt.size(), - reinterpret_cast(params.key->GetSymmetricKey()), - params.key->GetSymmetricKeySize(), - pseudorandom_key, - &prk_len) == nullptr) { - return false; - } - if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) || - !EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), pseudorandom_key, prk_len)) { - return false; - } - - size_t length = params.length; - ByteSource::Builder buf(length); - if (EVP_PKEY_derive(ctx.get(), buf.data(), &length) <= 0) - return false; - - *out = std::move(buf).release(); + auto dp = ncrypto::hkdf(params.digest, + ncrypto::Buffer{ + .data = reinterpret_cast( + params.key->GetSymmetricKey()), + .len = params.key->GetSymmetricKeySize(), + }, + ncrypto::Buffer{ + .data = params.info.data(), + .len = params.info.size(), + }, + ncrypto::Buffer{ + .data = params.salt.data(), + .len = params.salt.size(), + }, + params.length); + if (!dp) return false; + + *out = ByteSource::Allocated(dp.release()); return true; } diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc index 963d0db6c62a45..89b48c8c8761cd 100644 --- a/src/crypto/crypto_pbkdf2.cc +++ b/src/crypto/crypto_pbkdf2.cc @@ -102,7 +102,7 @@ Maybe PBKDF2Traits::AdditionalConfig( } Utf8Value name(args.GetIsolate(), args[offset + 4]); - params->digest = EVP_get_digestbyname(*name); + params->digest = ncrypto::getDigestByName(name.ToStringView()); if (params->digest == nullptr) { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", *name); return Nothing(); @@ -111,27 +111,24 @@ Maybe PBKDF2Traits::AdditionalConfig( return Just(true); } -bool PBKDF2Traits::DeriveBits( - Environment* env, - const PBKDF2Config& params, - ByteSource* out) { - ByteSource::Builder buf(params.length); - +bool PBKDF2Traits::DeriveBits(Environment* env, + const PBKDF2Config& params, + ByteSource* out) { // Both pass and salt may be zero length here. - // The generated bytes are stored in buf, which is - // assigned to out on success. - - if (PKCS5_PBKDF2_HMAC(params.pass.data(), - params.pass.size(), - params.salt.data(), - params.salt.size(), - params.iterations, - params.digest, - params.length, - buf.data()) <= 0) { - return false; - } - *out = std::move(buf).release(); + auto dp = ncrypto::pbkdf2(params.digest, + ncrypto::Buffer{ + .data = params.pass.data(), + .len = params.pass.size(), + }, + ncrypto::Buffer{ + .data = params.salt.data(), + .len = params.salt.size(), + }, + params.iterations, + params.length); + + if (!dp) return false; + *out = ByteSource::Allocated(dp.release()); return true; } diff --git a/src/crypto/crypto_scrypt.cc b/src/crypto/crypto_scrypt.cc index a262a2be96d7c3..d0e17fd4ef0837 100644 --- a/src/crypto/crypto_scrypt.cc +++ b/src/crypto/crypto_scrypt.cc @@ -93,17 +93,11 @@ Maybe ScryptTraits::AdditionalConfig( params->p = args[offset + 4].As()->Value(); params->maxmem = args[offset + 5]->IntegerValue(env->context()).ToChecked(); - if (EVP_PBE_scrypt( - nullptr, - 0, - nullptr, - 0, - params->N, - params->r, - params->p, - params->maxmem, - nullptr, - 0) != 1) { + params->length = args[offset + 6].As()->Value(); + CHECK_GE(params->length, 0); + + if (!ncrypto::checkScryptParams( + params->N, params->r, params->p, params->maxmem)) { // Do not use CryptoErrorStore or ThrowCryptoError here in order to maintain // backward compatibility with ERR_CRYPTO_INVALID_SCRYPT_PARAMS. uint32_t err = ERR_peek_last_error(); @@ -118,9 +112,6 @@ Maybe ScryptTraits::AdditionalConfig( return Nothing(); } - params->length = args[offset + 6].As()->Value(); - CHECK_GE(params->length, 0); - return Just(true); } @@ -128,23 +119,30 @@ bool ScryptTraits::DeriveBits( Environment* env, const ScryptConfig& params, ByteSource* out) { - ByteSource::Builder buf(params.length); - - // Both the pass and salt may be zero-length at this point - - if (!EVP_PBE_scrypt(params.pass.data(), - params.pass.size(), - params.salt.data(), - params.salt.size(), - params.N, - params.r, - params.p, - params.maxmem, - buf.data(), - params.length)) { - return false; + // If the params.length is zero-length, just return an empty buffer. + // It's useless, yes, but allowed via the API. + if (params.length == 0) { + *out = ByteSource(); + return true; } - *out = std::move(buf).release(); + + auto dp = ncrypto::scrypt( + ncrypto::Buffer{ + .data = params.pass.data(), + .len = params.pass.size(), + }, + ncrypto::Buffer{ + .data = params.salt.data(), + .len = params.salt.size(), + }, + params.N, + params.r, + params.p, + params.maxmem, + params.length); + + if (!dp) return false; + *out = ByteSource::Allocated(dp.release()); return true; } diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index 61bd65fc92678c..338a19b0e88ad6 100644 --- a/test/parallel/test-crypto-scrypt.js +++ b/test/parallel/test-crypto-scrypt.js @@ -1,4 +1,4 @@ -// Flags: --expose-internals +// Flags: --expose-internals --no-warnings 'use strict'; const common = require('../common'); if (!common.hasCrypto)