-
Notifications
You must be signed in to change notification settings - Fork 31.3k
Migrate hkdf, pbkdf2, scrypt to OpenSSL EVP_KDF functions #57824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Review requested:
|
KdfCtxPointer KdfCtxPointer::FromName(std::string_view name) { | ||
auto kdf = EVP_KDF_fetch(nullptr, name.data(), nullptr); | ||
auto ctx = EVP_KDF_CTX_new(kdf); | ||
return KdfCtxPointer{ctx}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The KDF context stores a reference to the KDF, that's why we don't need to store it ourselves. It's also possible to fetch the KDF from the context if needed
public: | ||
using ParamType = | ||
std::variant<uint32_t, double, std::string, std::vector<char>>; | ||
using Params = std::map<std::string, ParamType>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this can be a vector of pairs and we assume no one will add the same param twice (it will take the value of the first match)
lib/internal/crypto/hkdf.js
Outdated
"HKDF", | ||
{ | ||
digest: hash, | ||
key: key.export({ format: 'buffer' }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about this. OpenSSL expects an octet string and manipulating a key object in C++ sounded complicated
lib/internal/crypto/hkdf.js
Outdated
const job = new KDFJob(kCryptoJobAsync, | ||
"HKDF", | ||
{ | ||
digest: hash, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be OK to rename the parameters to write digest,
here? I tried to not change the public interface at all
// if (!salt.CheckSizeInt32()) [[unlikely]] { | ||
// THROW_ERR_OUT_OF_RANGE(env, "salt is too large"); | ||
// return v8::Nothing<void>(); | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can still do these checks by checking if the param name is "pass" or "salt" but I think it's pointless
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, these changes will need to be ifdef guarded.
test/parallel/test-crypto-hkdf.js
Outdated
// assert.throws( | ||
// () => hkdfSync('sha512', 'a', '', '', 64 * 255 + 1), { | ||
// code: 'ERR_CRYPTO_INVALID_KEYLEN' | ||
// }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These aren't throwing from OpenSSL, not sure why they should throw here
I did indeed suggest this in #50353 (comment), but I am not sure if we have a timeline for when we will stop supporting OpenSSL 1.1.1... maybe @panva knows. From what I remember, EVP_KDF is only available in OpenSSL 3. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, we need to remember that Node.js has to continue to be able to support downlevel versions of openssl (dynamic lib) and boringssl (for electron). The ncrypto dependency in particular is used not only by Node.js but also Cloudflare workers and bun (and possibly others by now). Because of that, the original implementation here needs to be preserved and the new EVP_KDF
impl needs to be guarded with #ifdef
flags that enable it only when a supporting version of openssl is used.
The new implementations are likely best added in addition to rather than as a replacement for the original implementations.
I don't know the latest, @jasnell was proposing a swift downfall of 1.1.1 support but we clearly cannot do it for 24, so possibly in 25 it's deprecated and 26 it's gone? Refs #56733
👌 |
That's my current thinking yes. |
Cool, so I'll revert the removals and keep it alongside. When it's time to drop support, there's a large code surface to prune :) |
@@ -3080,6 +3080,69 @@ bool CipherCtxPointer::getAeadTag(size_t len, unsigned char* out) { | |||
|
|||
// ============================================================================ | |||
|
|||
KdfCtxPointer KdfCtxPointer::FromName(std::string_view name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need to be ifdef guarded so it only compiles when openssl/boringssl versions that support EVP_KDF
// } | ||
|
||
auto paramsObj = args[offset + 1].As<v8::Object>(); | ||
auto propertyNames = paramsObj->GetOwnPropertyNames(env->context()).ToLocalChecked(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've been working on moving as much as possible away from using ToLocalChecked()
. in code paths that will be hit at runtime in order to allow errors to propagate correctly. These should follow this pattern instead:
MaybeLocal<Array> propertyNames;
if (!paramsObj->GetOwnPropertyNames(env->context()).ToLocal(&propertyNames)) {
return Nothing<void>();
}
Here and below.
ArrayBufferOrViewContents<char> abv(value); | ||
params->kdfParams[name] = std::vector<char>(abv.data(), abv.data() + abv.size()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... this feels a bit dangerous to me... This essentially means that the parameter names are tied to the underlying openssl version. If openssl happens to change names for whatever reason then the public API also changes. I know that is unlikely but I think it's important to try not to tie ourselves too closely to a specific openssl version, especially considering that boringssl also needs to be supported in the mix here and they could actually choose different names just to be Extra Spicy if they wanted to.
It's rather more work to do but I think I'd prefer to define an intermediary struct that we control the naming of and have ncrypto map that to whatever openssl/boringssl version is being used. So something like...
// in ncrypto
struct KDFParams {
std::string_view pass;
std::string_view salt;
size_t iterations
// ... and so on
};
ncrypto::KDFParams params;
v8::Local<v8::Value> value;
if (!paramsObj->Get(context, env->pass_string()).ToLocal(&value)) {
return Nothing<void>();
}
Utf8Value str(isolate, value);
params.pass = *str;
// ...
ctx.setParams(params);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasnell that structure would have to support all parameters for all KDFs, is that the suggestion?
Alternatively, we can make a "translation" layer from Node to OpenSSL so the parameters are stable from Node independent of the OpenSSL backend
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either that or there would be separate structs for each KDF algorithm we support, with only the parameters for that algorithm. Yes, it's way more of a pain but definitely buffers more from differences across individual openssl and boringssl versions.
} | ||
|
||
if (!params->kdfCtx.setParams(params->kdfParams)) { | ||
if (uint32_t err = ERR_peek_last_error(); err != 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should be using ncrypto::ClearErrorOnReturn
or ncrypto::MarkPopErrorOnReturn
template<class... Ts> | ||
struct overloads : Ts... { using Ts::operator()...; }; | ||
|
||
void KDFConfig::MemoryInfo(MemoryTracker* tracker) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at MemoryRetainerTraits
... it was recently added to simplify tracking for types that cannot be modified to implement MemoryRetainer
https://github.com/nodejs/node/blob/main/src/memory_tracker.h#L141-L166 ... that said tho, I really think the params need to be modeled as an ncrypto defined struct that would make this easier.
Long ago in #50353 it was suggested to create a generic KDF function to support all functions depending on parameters, and it turns out that's how OpenSSL does it anyway. I created a generic interface to implement all KDF functions (currently hkdf, pbkdf2, scrypt, in the future argon2 when 3.5 is merged)
There's a few issues with the implementation currently: