Skip to content

Commit

Permalink
src: move hkdf, scrypto, pbkdf2 impl to ncrypto
Browse files Browse the repository at this point in the history
PR-URL: nodejs#54651
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Minwoo Jung <[email protected]>
  • Loading branch information
jasnell authored and tpoisseau committed Nov 21, 2024
1 parent ae2a197 commit 51db6db
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 103 deletions.
132 changes: 132 additions & 0 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
32 changes: 32 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
71 changes: 20 additions & 51 deletions src/crypto/crypto_hkdf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down Expand Up @@ -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>();
}
Expand All @@ -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;
}

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

*out = ByteSource::Allocated(dp.release());
return true;
}

Expand Down
39 changes: 18 additions & 21 deletions src/crypto/crypto_pbkdf2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand All @@ -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.

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();
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 (!dp) return false;
*out = ByteSource::Allocated(dp.release());
return true;
}

Expand Down
Loading

0 comments on commit 51db6db

Please sign in to comment.