src: move hkdf, scrypto, pbkdf2 impl to ncrypto

PR-URL: https://github.com/nodejs/node/pull/54651
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
This commit is contained in:
James M Snell 2024-08-28 18:13:49 -07:00
parent 16e6747c35
commit dcc2ed944f
6 changed files with 229 additions and 101 deletions

View File

@ -4,6 +4,7 @@
#include <openssl/dh.h>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/pkcs12.h>
#include <openssl/x509v3.h>
#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<const unsigned char>& key,
const Buffer<const unsigned char>& info,
const Buffer<const unsigned char>& 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<const char*>(salt.data), salt.len};
} else {
actual_salt = {default_salt, static_cast<unsigned>(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<unsigned char*>(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<const char>& pass,
const Buffer<const unsigned char>& 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<unsigned char*>(dp.get()), length)) {
return dp;
}
return {};
}
DataPointer pbkdf2(const EVP_MD* md,
const Buffer<const char>& pass,
const Buffer<const unsigned char>& 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<unsigned char*>(dp.get()))) {
return dp;
}
return {};
}
} // namespace ncrypto

View File

@ -588,6 +588,38 @@ BIOPointer ExportPublicKey(const char* input, size_t length);
// The caller takes ownership of the returned Buffer<char>
Buffer<char> 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<const unsigned char>& key,
const Buffer<const unsigned char>& info,
const Buffer<const unsigned char>& salt,
size_t length);
bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem);
DataPointer scrypt(const Buffer<const char>& pass,
const Buffer<const unsigned char>& salt,
uint64_t N,
uint64_t r,
uint64_t p,
uint64_t maxmem,
size_t length);
DataPointer pbkdf2(const EVP_MD* md,
const Buffer<const char>& pass,
const Buffer<const unsigned char>& salt,
uint32_t iterations,
size_t length);
// ============================================================================
// Version metadata
#define NCRYPTO_VERSION "0.0.1"

View File

@ -56,7 +56,7 @@ Maybe<bool> 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<bool>();
@ -90,9 +90,7 @@ Maybe<bool> 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<bool>();
}
@ -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<unsigned char>(), params.info.size())) {
return false;
}
auto dp = ncrypto::hkdf(params.digest,
ncrypto::Buffer<const unsigned char>{
.data = reinterpret_cast<const unsigned char*>(
params.key->GetSymmetricKey()),
.len = params.key->GetSymmetricKeySize(),
},
ncrypto::Buffer<const unsigned char>{
.data = params.info.data<const unsigned char>(),
.len = params.info.size(),
},
ncrypto::Buffer<const unsigned char>{
.data = params.salt.data<const unsigned char>(),
.len = params.salt.size(),
},
params.length);
if (!dp) 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<char>(), params.salt.size()};
} else {
static const char default_salt[EVP_MAX_MD_SIZE] = {0};
salt = {default_salt, static_cast<unsigned>(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<const unsigned char*>(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<unsigned char>(), &length) <= 0)
return false;
*out = std::move(buf).release();
*out = ByteSource::Allocated(dp.release());
return true;
}

View File

@ -102,7 +102,7 @@ Maybe<bool> 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<bool>();
@ -111,27 +111,24 @@ Maybe<bool> 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.
auto dp = ncrypto::pbkdf2(params.digest,
ncrypto::Buffer<const char>{
.data = params.pass.data<const char>(),
.len = params.pass.size(),
},
ncrypto::Buffer<const unsigned char>{
.data = params.salt.data<unsigned char>(),
.len = params.salt.size(),
},
params.iterations,
params.length);
if (PKCS5_PBKDF2_HMAC(params.pass.data<char>(),
params.pass.size(),
params.salt.data<unsigned char>(),
params.salt.size(),
params.iterations,
params.digest,
params.length,
buf.data<unsigned char>()) <= 0) {
return false;
}
*out = std::move(buf).release();
if (!dp) return false;
*out = ByteSource::Allocated(dp.release());
return true;
}

View File

@ -93,17 +93,11 @@ Maybe<bool> ScryptTraits::AdditionalConfig(
params->p = args[offset + 4].As<Uint32>()->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<Int32>()->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<bool> ScryptTraits::AdditionalConfig(
return Nothing<bool>();
}
params->length = args[offset + 6].As<Int32>()->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<char>(),
params.pass.size(),
params.salt.data<unsigned char>(),
params.salt.size(),
params.N,
params.r,
params.p,
params.maxmem,
buf.data<unsigned char>(),
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<const char>{
.data = params.pass.data<char>(),
.len = params.pass.size(),
},
ncrypto::Buffer<const unsigned char>{
.data = params.salt.data<unsigned char>(),
.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;
}

View File

@ -1,4 +1,4 @@
// Flags: --expose-internals
// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
if (!common.hasCrypto)