crypto: decorate async crypto job errors with OpenSSL error details

PR-URL: https://github.com/nodejs/node/pull/62348
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Filip Skokan authored and GitHub committed 2026-03-28 05:24:34 +00:00
1 parent e78ccd8017
commit 74509b166a
32 files changed
+443 -80

No files matched your search

+6 -2
View File
@@ -124,7 +124,8 @@ Maybe<void> Argon2Traits::AdditionalConfig(
bool Argon2Traits::DeriveBits(Environment* env,
const Argon2Config& config,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
// If the config.length is zero-length, just return an empty buffer.
// It's useless, yes, but allowed via the API.
if (config.keylen == 0) {
@@ -144,7 +145,10 @@ bool Argon2Traits::DeriveBits(Environment* env,
config.ad,
config.type);
if (!dp) return false;
if (!dp) {
errors->Insert(NodeCryptoError::ARGON2_FAILED);
return false;
}
DCHECK(!dp.isSecure());
*out = ByteSource::Allocated(dp.release());
return true;
+2 -1
View File
@@ -59,7 +59,8 @@ struct Argon2Traits final {
static bool DeriveBits(Environment* env,
const Argon2Config& config,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const Argon2Config& config,
+2 -7
View File
@@ -504,16 +504,11 @@ MaybeLocal<Value> DHBitsTraits::EncodeOutput(Environment* env,
bool DHBitsTraits::DeriveBits(Environment* env,
const DHBitsConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
auto dp = DHPointer::stateless(params.private_key.GetAsymmetricKey(),
params.public_key.GetAsymmetricKey());
if (!dp) {
bool can_throw = mode == CryptoJobMode::kCryptoJobSync;
if (can_throw) {
unsigned long err = ERR_get_error(); // NOLINT(runtime/int)
if (err) ThrowCryptoError(env, err, "diffieHellman failed");
}
return false;
}
+2 -1
View File
@@ -83,7 +83,8 @@ struct DHBitsTraits final {
static bool DeriveBits(Environment* env,
const DHBitsConfig& params,
ByteSource* out_,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const DHBitsConfig& params,
+5 -2
View File
@@ -433,7 +433,8 @@ Maybe<void> ECDHBitsTraits::AdditionalConfig(
bool ECDHBitsTraits::DeriveBits(Environment* env,
const ECDHBitsConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
size_t len = 0;
const auto& m_privkey = params.private_.GetAsymmetricKey();
const auto& m_pubkey = params.public_.GetAsymmetricKey();
@@ -464,8 +465,10 @@ bool ECDHBitsTraits::DeriveBits(Environment* env,
const EC_KEY* public_key = m_pubkey;
const auto group = ECKeyPointer::GetGroup(private_key);
if (group == nullptr)
if (group == nullptr) {
errors->Insert(NodeCryptoError::ECDH_FAILED);
return false;
}
CHECK(ECKeyPointer::Check(private_key));
CHECK(ECKeyPointer::Check(public_key));
+2 -1
View File
@@ -80,7 +80,8 @@ struct ECDHBitsTraits final {
static bool DeriveBits(Environment* env,
const ECDHBitsConfig& params,
ByteSource* out_,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const ECDHBitsConfig& params,
+2 -1
View File
@@ -540,7 +540,8 @@ Maybe<void> HashTraits::AdditionalConfig(
bool HashTraits::DeriveBits(Environment* env,
const HashConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
auto ctx = EVPMDCtxPointer::New();
if (!ctx.digestInit(params.digest) || !ctx.digestUpdate(params.in))
+2 -1
View File
@@ -73,7 +73,8 @@ struct HashTraits final {
static bool DeriveBits(Environment* env,
const HashConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const HashConfig& params,
+6 -2
View File
@@ -100,7 +100,8 @@ Maybe<void> HKDFTraits::AdditionalConfig(
bool HKDFTraits::DeriveBits(Environment* env,
const HKDFConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
auto dp = ncrypto::hkdf(params.digest,
ncrypto::Buffer<const unsigned char>{
.data = reinterpret_cast<const unsigned char*>(
@@ -116,7 +117,10 @@ bool HKDFTraits::DeriveBits(Environment* env,
.len = params.salt.size(),
},
params.length);
if (!dp) return false;
if (!dp) {
errors->Insert(NodeCryptoError::HKDF_FAILED);
return false;
}
DCHECK(!dp.isSecure());
*out = ByteSource::Allocated(dp.release());
+2 -1
View File
@@ -45,7 +45,8 @@ struct HKDFTraits final {
static bool DeriveBits(Environment* env,
const HKDFConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const HKDFConfig& params,
+2 -1
View File
@@ -231,7 +231,8 @@ Maybe<void> HmacTraits::AdditionalConfig(
bool HmacTraits::DeriveBits(Environment* env,
const HmacConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
auto ctx = HMACCtxPointer::New();
ncrypto::Buffer<const void> key_buf{
+2 -1
View File
@@ -76,7 +76,8 @@ struct HmacTraits final {
static bool DeriveBits(Environment* env,
const HmacConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const HmacConfig& params,
+17 -16
View File
@@ -51,12 +51,12 @@ namespace {
bool DoKEMEncapsulate(Environment* env,
const EVPKeyPointer& public_key,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
auto result = ncrypto::KEM::Encapsulate(public_key);
if (!result) {
if (mode == kCryptoJobSync) {
THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform encapsulation");
}
errors->Insert(NodeCryptoError::ENCAPSULATION_FAILED);
errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
return false;
}
@@ -68,10 +68,8 @@ bool DoKEMEncapsulate(Environment* env,
auto data = ncrypto::DataPointer::Alloc(total_len);
if (!data) {
if (mode == kCryptoJobSync) {
THROW_ERR_CRYPTO_OPERATION_FAILED(env,
"Failed to allocate output buffer");
}
errors->Insert(NodeCryptoError::ALLOCATION_FAILED);
errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
return false;
}
@@ -97,14 +95,14 @@ bool DoKEMDecapsulate(Environment* env,
const EVPKeyPointer& private_key,
const ByteSource& ciphertext,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
ncrypto::Buffer<const void> ciphertext_buf{ciphertext.data(),
ciphertext.size()};
auto shared_key = ncrypto::KEM::Decapsulate(private_key, ciphertext_buf);
if (!shared_key) {
if (mode == kCryptoJobSync) {
THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform decapsulation");
}
errors->Insert(NodeCryptoError::DECAPSULATION_FAILED);
errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
return false;
}
@@ -137,11 +135,12 @@ Maybe<void> KEMEncapsulateTraits::AdditionalConfig(
bool KEMEncapsulateTraits::DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
Mutex::ScopedLock lock(params.key.mutex());
const auto& public_key = params.key.GetAsymmetricKey();
return DoKEMEncapsulate(env, public_key, out, mode);
return DoKEMEncapsulate(env, public_key, out, mode, errors);
}
MaybeLocal<Value> KEMEncapsulateTraits::EncodeOutput(
@@ -218,11 +217,13 @@ Maybe<void> KEMDecapsulateTraits::AdditionalConfig(
bool KEMDecapsulateTraits::DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
Mutex::ScopedLock lock(params.key.mutex());
const auto& private_key = params.key.GetAsymmetricKey();
return DoKEMDecapsulate(env, private_key, params.ciphertext, out, mode);
return DoKEMDecapsulate(
env, private_key, params.ciphertext, out, mode, errors);
}
MaybeLocal<Value> KEMDecapsulateTraits::EncodeOutput(
+4 -2
View File
@@ -48,7 +48,8 @@ struct KEMEncapsulateTraits final {
static bool DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const KEMConfiguration& params,
@@ -71,7 +72,8 @@ struct KEMDecapsulateTraits final {
static bool DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const KEMConfiguration& params,
+3 -1
View File
@@ -122,7 +122,8 @@ Maybe<void> KmacTraits::AdditionalConfig(
bool KmacTraits::DeriveBits(Environment* env,
const KmacConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
if (params.length == 0) {
*out = ByteSource();
return true;
@@ -133,6 +134,7 @@ bool KmacTraits::DeriveBits(Environment* env,
size_t key_size = params.key.GetSymmetricKeySize();
if (key_size == 0) {
errors->Insert(NodeCryptoError::KMAC_FAILED);
return false;
}
+2 -1
View File
@@ -51,7 +51,8 @@ struct KmacTraits final {
static bool DeriveBits(Environment* env,
const KmacConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const KmacConfig& params,
+6 -2
View File
@@ -113,7 +113,8 @@ Maybe<void> PBKDF2Traits::AdditionalConfig(
bool PBKDF2Traits::DeriveBits(Environment* env,
const PBKDF2Config& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
// Both pass and salt may be zero length here.
auto dp = ncrypto::pbkdf2(params.digest,
ncrypto::Buffer<const char>{
@@ -127,7 +128,10 @@ bool PBKDF2Traits::DeriveBits(Environment* env,
params.iterations,
params.length);
if (!dp) return false;
if (!dp) {
errors->Insert(NodeCryptoError::PBKDF2_FAILED);
return false;
}
DCHECK(!dp.isSecure());
*out = ByteSource::Allocated(dp.release());
return true;
+2 -1
View File
@@ -58,7 +58,8 @@ struct PBKDF2Traits final {
static bool DeriveBits(Environment* env,
const PBKDF2Config& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const PBKDF2Config& params,
+6 -3
View File
@@ -68,7 +68,8 @@ Maybe<void> RandomBytesTraits::AdditionalConfig(
bool RandomBytesTraits::DeriveBits(Environment* env,
const RandomBytesConfig& params,
ByteSource* unused,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
return ncrypto::CSPRNG(params.buffer, params.size);
}
@@ -155,7 +156,8 @@ Maybe<void> RandomPrimeTraits::AdditionalConfig(
bool RandomPrimeTraits::DeriveBits(Environment* env,
const RandomPrimeConfig& params,
ByteSource* unused,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
return params.prime.generate(
BignumPointer::PrimeConfig{
.bits = params.bits,
@@ -194,7 +196,8 @@ Maybe<void> CheckPrimeTraits::AdditionalConfig(
bool CheckPrimeTraits::DeriveBits(Environment* env,
const CheckPrimeConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
int ret = params.candidate.isPrime(params.checks, getPrimeCheckCallback(env));
if (ret < 0) [[unlikely]]
return false;
+6 -3
View File
@@ -35,7 +35,8 @@ struct RandomBytesTraits final {
static bool DeriveBits(Environment* env,
const RandomBytesConfig& params,
ByteSource* out_,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const RandomBytesConfig& params,
@@ -70,7 +71,8 @@ struct RandomPrimeTraits final {
static bool DeriveBits(Environment* env,
const RandomPrimeConfig& params,
ByteSource* out_,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const RandomPrimeConfig& params,
@@ -104,7 +106,8 @@ struct CheckPrimeTraits final {
static bool DeriveBits(Environment* env,
const CheckPrimeConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const CheckPrimeConfig& params,
+6 -2
View File
@@ -116,7 +116,8 @@ Maybe<void> ScryptTraits::AdditionalConfig(
bool ScryptTraits::DeriveBits(Environment* env,
const ScryptConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
// 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) {
@@ -139,7 +140,10 @@ bool ScryptTraits::DeriveBits(Environment* env,
params.maxmem,
params.length);
if (!dp) return false;
if (!dp) {
errors->Insert(NodeCryptoError::SCRYPT_FAILED);
return false;
}
DCHECK(!dp.isSecure());
*out = ByteSource::Allocated(dp.release());
return true;
+2 -1
View File
@@ -60,7 +60,8 @@ struct ScryptTraits final {
static bool DeriveBits(Environment* env,
const ScryptConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const ScryptConfig& params,
+4 -7
View File
@@ -688,8 +688,8 @@ Maybe<void> SignTraits::AdditionalConfig(
bool SignTraits::DeriveBits(Environment* env,
const SignConfiguration& params,
ByteSource* out,
CryptoJobMode mode) {
bool can_throw = mode == CryptoJobMode::kCryptoJobSync;
CryptoJobMode mode,
CryptoErrorStore* errors) {
auto context = EVPMDCtxPointer::New();
if (!context) [[unlikely]]
return false;
@@ -699,7 +699,8 @@ bool SignTraits::DeriveBits(Environment* env,
params.context_string.size() > 0);
if (has_context && !SupportsContextString(key)) {
if (can_throw) crypto::CheckThrow(env, SignBase::Error::ContextUnsupported);
errors->Insert(NodeCryptoError::CONTEXT_UNSUPPORTED);
errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
return false;
}
@@ -728,7 +729,6 @@ bool SignTraits::DeriveBits(Environment* env,
})();
if (!ctx.has_value()) [[unlikely]] {
if (can_throw) crypto::CheckThrow(env, SignBase::Error::Init);
return false;
}
@@ -742,7 +742,6 @@ bool SignTraits::DeriveBits(Environment* env,
: std::nullopt;
if (!ApplyRSAOptions(key, *ctx, padding, salt_length)) {
if (can_throw) crypto::CheckThrow(env, SignBase::Error::PrivateKey);
return false;
}
@@ -751,7 +750,6 @@ bool SignTraits::DeriveBits(Environment* env,
if (key.isOneShotVariant()) {
auto data = context.signOneShot(params.data);
if (!data) [[unlikely]] {
if (can_throw) crypto::CheckThrow(env, SignBase::Error::PrivateKey);
return false;
}
DCHECK(!data.isSecure());
@@ -759,7 +757,6 @@ bool SignTraits::DeriveBits(Environment* env,
} else {
auto data = context.sign(params.data);
if (!data) [[unlikely]] {
if (can_throw) crypto::CheckThrow(env, SignBase::Error::PrivateKey);
return false;
}
DCHECK(!data.isSecure());
+2 -1
View File
@@ -143,7 +143,8 @@ struct SignTraits final {
static bool DeriveBits(Environment* env,
const SignConfiguration& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const SignConfiguration& params,
+4 -2
View File
@@ -472,7 +472,8 @@ Maybe<void> TurboShakeTraits::AdditionalConfig(
bool TurboShakeTraits::DeriveBits(Environment* env,
const TurboShakeConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
CHECK_GT(params.output_length, 0);
char* buf = MallocOpenSSL<char>(params.output_length);
@@ -585,7 +586,8 @@ Maybe<void> KangarooTwelveTraits::AdditionalConfig(
bool KangarooTwelveTraits::DeriveBits(Environment* env,
const KangarooTwelveConfig& params,
ByteSource* out,
CryptoJobMode mode) {
CryptoJobMode mode,
CryptoErrorStore* errors) {
CHECK_GT(params.output_length, 0);
char* buf = MallocOpenSSL<char>(params.output_length);
+4 -2
View File
@@ -42,7 +42,8 @@ struct TurboShakeTraits final {
static bool DeriveBits(Environment* env,
const TurboShakeConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const TurboShakeConfig& params,
@@ -86,7 +87,8 @@ struct KangarooTwelveTraits final {
static bool DeriveBits(Environment* env,
const KangarooTwelveConfig& params,
ByteSource* out,
CryptoJobMode mode);
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(
Environment* env, const KangarooTwelveConfig& params, ByteSource* out);
+24 -2
View File
@@ -230,7 +230,9 @@ void GetOpenSSLSecLevelCrypto(const FunctionCallbackInfo<Value>& args) {
void CryptoErrorStore::Capture() {
errors_.clear();
primary_openssl_error_ = 0;
while (const uint32_t err = ERR_get_error()) {
if (primary_openssl_error_ == 0) primary_openssl_error_ = err;
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
errors_.emplace_back(buf);
@@ -297,6 +299,12 @@ MaybeLocal<Value> cryptoErrorListToException(Environment* env,
return exception;
}
namespace error {
v8::Maybe<void> Decorate(Environment* env,
v8::Local<v8::Object> obj,
unsigned long err); // NOLINT(runtime/int)
} // namespace error
MaybeLocal<Value> CryptoErrorStore::ToException(
Environment* env,
Local<String> exception_string) const {
@@ -321,10 +329,10 @@ MaybeLocal<Value> CryptoErrorStore::ToException(
Local<Value> exception_v = Exception::Error(exception_string);
CHECK(!exception_v.IsEmpty());
CHECK(exception_v->IsObject());
Local<Object> exception = exception_v.As<Object>();
if (!Empty()) {
CHECK(exception_v->IsObject());
Local<Object> exception = exception_v.As<Object>();
Local<Value> stack;
if (!ToV8Value(env->context(), errors_).ToLocal(&stack) ||
exception->Set(env->context(), env->openssl_error_stack(), stack)
@@ -333,6 +341,20 @@ MaybeLocal<Value> CryptoErrorStore::ToException(
}
}
if (primary_openssl_error_ != 0) {
if (error::Decorate(env, exception, primary_openssl_error_).IsNothing()) {
return MaybeLocal<Value>();
}
} else if (node_error_code_ != nullptr) {
if (exception
->Set(env->context(),
env->code_string(),
OneByteString(env->isolate(), node_error_code_))
.IsNothing()) {
return MaybeLocal<Value>();
}
}
return exception_v;
}
+28 -11
View File
@@ -72,13 +72,23 @@ void Decode(const v8::FunctionCallbackInfo<v8::Value>& args,
}
}
#define NODE_CRYPTO_ERROR_CODES_MAP(V) \
V(CIPHER_JOB_FAILED, "Cipher job failed") \
V(DERIVING_BITS_FAILED, "Deriving bits failed") \
V(ENGINE_NOT_FOUND, "Engine \"%s\" was not found") \
V(INVALID_KEY_TYPE, "Invalid key type") \
V(KEY_GENERATION_JOB_FAILED, "Key generation job failed") \
V(OK, "Ok") \
#define NODE_CRYPTO_ERROR_CODES_MAP(V) \
V(ALLOCATION_FAILED, "Failed to allocate output buffer") \
V(ARGON2_FAILED, "Argon2 derivation failed") \
V(CIPHER_JOB_FAILED, "Cipher job failed") \
V(CONTEXT_UNSUPPORTED, "Context parameter is unsupported") \
V(DECAPSULATION_FAILED, "Decapsulation failed") \
V(DERIVING_BITS_FAILED, "Deriving bits failed") \
V(ECDH_FAILED, "ECDH key agreement failed") \
V(ENCAPSULATION_FAILED, "Encapsulation failed") \
V(ENGINE_NOT_FOUND, "Engine \"%s\" was not found") \
V(HKDF_FAILED, "HKDF derivation failed") \
V(INVALID_KEY_TYPE, "Invalid key type") \
V(KEY_GENERATION_JOB_FAILED, "Key generation job failed") \
V(KMAC_FAILED, "KMAC derivation failed") \
V(OK, "Ok") \
V(PBKDF2_FAILED, "PBKDF2 derivation failed") \
V(SCRYPT_FAILED, "scrypt derivation failed")
enum class NodeCryptoError {
#define V(CODE, DESCRIPTION) CODE,
@@ -115,12 +125,16 @@ struct CryptoErrorStore final : public MemoryRetainer {
Environment* env,
v8::Local<v8::String> exception_string = v8::Local<v8::String>()) const;
void SetNodeErrorCode(const char* code) { node_error_code_ = code; }
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(CryptoErrorStore)
SET_SELF_SIZE(CryptoErrorStore)
private:
std::vector<std::string> errors_;
unsigned long primary_openssl_error_ = 0; // NOLINT(runtime/int)
const char* node_error_code_ = nullptr;
};
template <typename... Args>
@@ -411,14 +425,17 @@ class DeriveBitsJob final : public CryptoJob<DeriveBitsTraits> {
void DoThreadPoolWork() override {
ncrypto::ClearErrorOnReturn clear_error_on_return;
CryptoErrorStore* errors = CryptoJob<DeriveBitsTraits>::errors();
if (!DeriveBitsTraits::DeriveBits(AsyncWrap::env(),
*CryptoJob<DeriveBitsTraits>::params(),
&out_,
this->mode())) {
CryptoErrorStore* errors = CryptoJob<DeriveBitsTraits>::errors();
errors->Capture();
if (errors->Empty())
this->mode(),
errors)) {
if (errors->Empty()) errors->Capture();
if (errors->Empty()) {
errors->Insert(NodeCryptoError::DERIVING_BITS_FAILED);
errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
}
return;
}
success_ = true;
@@ -154,13 +154,16 @@ MCowBQYDK2VuAyEA6pwGRbadNQAI/tYN8+/p/0/hbsdHfOEGr1ADiLVk/Gc=
const signature = crypto.randomBytes(16);
let expected = /no default digest/;
let expectedCode = 'ERR_OSSL_EVP_NO_DEFAULT_DIGEST';
if (hasOpenSSL3 || process.features.openssl_is_boringssl) {
expected = /operation[\s_]not[\s_]supported[\s_]for[\s_]this[\s_]keytype/i;
expectedCode = 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE';
}
crypto.verify(undefined, data, untrustedKey, signature, common.mustCall((err) => {
assert.ok(err);
assert.match(err.message, expected);
assert.strictEqual(err.code, expectedCode);
}));
}
@@ -171,5 +174,6 @@ MCowBQYDK2VuAyEA6pwGRbadNQAI/tYN8+/p/0/hbsdHfOEGr1ADiLVk/Gc=
crypto.sign('sha512', 'message', privateKey, common.mustCall((err) => {
assert.ok(err);
assert.match(err.message, /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i);
assert.match(err.code, /^ERR_OSSL_.*DIGEST_TOO_BIG_FOR_RSA_KEY$/);
}));
}
@@ -141,6 +141,7 @@ for (const [params1, params2] of list) {
assert.ok(err);
assert.strictEqual(err.name, 'Error');
assert.match(err.message, hasOpenSSL3 ? /mismatching domain parameters/ : /different parameters/);
assert.match(err.code, /^ERR_OSSL_.*(?:MISMATCHING_DOMAIN_PARAMETERS|DIFFERENT_PARAMETERS)$/);
}));
}
@@ -199,6 +200,7 @@ crypto.diffieHellman({
assert.ok(err);
assert.strictEqual(err.name, 'Error');
assert.match(err.message, hasOpenSSL3 ? /mismatching domain parameters/ : /different parameters/);
assert.match(err.code, /^ERR_OSSL_.*(?:MISMATCHING_DOMAIN_PARAMETERS|DIFFERENT_PARAMETERS)$/);
}));
test(crypto.generateKeyPairSync('x448'),
@@ -216,5 +218,6 @@ test(crypto.generateKeyPairSync('x25519'),
assert.ok(err);
assert.strictEqual(err.name, 'Error');
assert.match(err.message, hasOpenSSL3 ? /failed during derivation/ : /Deriving bits failed/);
assert.strictEqual(err.code, hasOpenSSL3 ? 'ERR_OSSL_FAILED_DURING_DERIVATION' : 'ERR_CRYPTO_OPERATION_FAILED');
}));
}
+3 -2
View File
@@ -224,7 +224,7 @@ for (const [name, {
{
const { ciphertext } = crypto.encapsulate(publicKey);
assert.throws(() => crypto.decapsulate(wrongPrivateKey, ciphertext), {
message: /Failed to (initialize|perform) decapsulation/,
message: /Decapsulation failed/,
code: 'ERR_CRYPTO_OPERATION_FAILED',
});
}
@@ -234,7 +234,8 @@ for (const [name, {
crypto.encapsulate(publicKey, common.mustSucceed(({ ciphertext }) => {
crypto.decapsulate(wrongPrivateKey, ciphertext, common.mustCall((err) => {
assert(err);
assert.strictEqual(err.message, 'Deriving bits failed');
assert.match(err.message, /Decapsulation failed/);
assert.strictEqual(err.code, 'ERR_CRYPTO_OPERATION_FAILED');
}));
}));
}
@@ -0,0 +1,278 @@
'use strict';
// Tests that sync and async crypto operations produce errors with matching
// properties. This validates that both code paths go through the same
// CryptoErrorStore-based error creation rather than diverging.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
const assert = require('assert');
const crypto = require('crypto');
const fixtures = require('../common/fixtures');
function getError(fn) {
let err;
assert.throws(fn, (e) => { err = e; return true; });
return err;
}
// Compare error properties between sync and async errors.
function assertErrorMatch(syncErr, asyncErr, label) {
assert(asyncErr instanceof Error, `${label}: expected async Error`);
assert.strictEqual(asyncErr.message, syncErr.message,
`${label}: message mismatch`);
assert.strictEqual(asyncErr.code, syncErr.code,
`${label}: code mismatch`);
// OpenSSL error decoration properties
for (const prop of ['library', 'reason']) {
assert.strictEqual(asyncErr[prop], syncErr[prop],
`${label}: ${prop} mismatch`);
}
}
const data = Buffer.from('test data');
// === crypto.sign / crypto.verify ===
// Sign: ed25519 with explicit digest (OpenSSL error at signInit)
{
const privateKey = crypto.createPrivateKey(
fixtures.readKey('ed25519_private.pem'));
const syncErr = getError(() => crypto.sign('sha256', data, privateKey));
assert(syncErr.code);
assert(syncErr.message);
crypto.sign('sha256', data, privateKey, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'sign: ed25519 + sha256');
}));
}
// Sign: context on key that does not support it (CONTEXT_UNSUPPORTED)
{
const privateKey = crypto.createPrivateKey(
fixtures.readKey('rsa_private.pem'));
const keyOpts = { key: privateKey, context: Buffer.alloc(1) };
const syncErr = getError(() => crypto.sign('sha256', data, keyOpts));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_OPERATION_FAILED');
assert.match(syncErr.message, /Context parameter is unsupported/);
crypto.sign('sha256', data, keyOpts, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'sign: RSA + context');
}));
}
// Sign: RSA key too small for digest (OpenSSL error)
{
const { privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 512,
});
const syncErr = getError(() => crypto.sign('sha512', data, privateKey));
assert.match(syncErr.message, /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i);
crypto.sign('sha512', data, privateKey, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'sign: RSA 512 + sha512');
}));
}
// Sign: illegal RSA padding mode (OpenSSL error)
{
const privateKey = crypto.createPrivateKey(
fixtures.readKey('rsa_private.pem'));
const keyOpts = {
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
};
const syncErr = getError(() => crypto.sign('sha256', data, keyOpts));
assert.match(syncErr.message,
/illegal[\s_]or[\s_]unsupported[\s_]padding[\s_]mode/i);
crypto.sign('sha256', data, keyOpts, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'sign: RSA + OAEP padding');
}));
}
// Verify: X25519 key (not a signing key - OpenSSL error)
{
const publicKey = crypto.createPublicKey(
fixtures.readKey('x25519_public.pem'));
const sig = Buffer.alloc(64);
const syncErr = getError(() =>
crypto.verify(null, data, publicKey, sig));
assert(syncErr.message);
crypto.verify(null, data, publicKey, sig, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'verify: X25519 key');
}));
}
// === crypto.diffieHellman ===
// DH: Mismatched EC curves (OpenSSL error)
{
const alicePrivate = crypto.createPrivateKey(
fixtures.readKey('ec_p256_private.pem'));
const bobPublic = crypto.createPublicKey(
fixtures.readKey('ec_p384_public.pem'));
const syncErr = getError(() =>
crypto.diffieHellman({
privateKey: alicePrivate,
publicKey: bobPublic,
}));
assert(syncErr.message);
crypto.diffieHellman({
privateKey: alicePrivate,
publicKey: bobPublic,
}, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'DH: mismatched EC curves');
}));
}
// DH: X25519 with all-zero public key (OpenSSL error)
{
const privateKey = crypto.createPrivateKey(
fixtures.readKey('x25519_private.pem'));
const publicKey = crypto.createPublicKey(
'-----BEGIN PUBLIC KEY-----\n' +
'MCowBQYDK2VuAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n' +
'-----END PUBLIC KEY-----');
const syncErr = getError(() =>
crypto.diffieHellman({ publicKey, privateKey }));
assert(syncErr.message);
crypto.diffieHellman({ publicKey, privateKey },
common.mustCall((asyncErr) => {
assertErrorMatch(
syncErr, asyncErr, 'DH: X25519 zero key');
}));
}
// DH: Mismatched DH group params (OpenSSL error)
{
const alice = crypto.generateKeyPairSync('dh', { group: 'modp5' });
const bob = crypto.generateKeyPairSync('dh', { group: 'modp18' });
const syncErr = getError(() =>
crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}));
assert(syncErr.message);
crypto.diffieHellman({
privateKey: alice.privateKey,
publicKey: bob.publicKey,
}, common.mustCall((asyncErr) => {
assertErrorMatch(syncErr, asyncErr, 'DH: mismatched DH groups');
}));
}
// === crypto.encapsulate / crypto.decapsulate ===
if (hasOpenSSL(3)) {
// KEM: Decapsulate with wrong private key type
{
const rsaPublicKey = crypto.createPublicKey(
fixtures.readKey('rsa_public.pem'));
const x25519Private = crypto.createPrivateKey(
fixtures.readKey('x25519_private.pem'));
const { ciphertext } = crypto.encapsulate(rsaPublicKey);
const syncErr = getError(() =>
crypto.decapsulate(x25519Private, ciphertext));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_OPERATION_FAILED');
assert.match(syncErr.message, /Decapsulation failed/);
crypto.decapsulate(x25519Private, ciphertext,
common.mustCall((asyncErr) => {
assertErrorMatch(
syncErr, asyncErr, 'KEM: wrong key decapsulate');
}));
}
}
// === crypto.hkdf / crypto.hkdfSync ===
// HKDF: Invalid digest (AdditionalConfig error - should throw in both paths)
{
const key = crypto.randomBytes(32);
const salt = Buffer.alloc(0);
const info = Buffer.alloc(0);
const syncErr = getError(() =>
crypto.hkdfSync('nonexistent', key, salt, info, 64));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_INVALID_DIGEST');
// Async hkdf also throws synchronously for setup errors
const asyncErr = getError(() =>
crypto.hkdf('nonexistent', key, salt, info, 64, common.mustNotCall()));
assertErrorMatch(syncErr, asyncErr, 'HKDF: invalid digest');
}
// HKDF: keylen too large (AdditionalConfig error)
{
const key = crypto.randomBytes(32);
const salt = Buffer.alloc(0);
const info = Buffer.alloc(0);
// sha256 output is 32 bytes, max HKDF output is 255 * 32 = 8160
const tooLarge = 255 * 32 + 1;
const syncErr = getError(() =>
crypto.hkdfSync('sha256', key, salt, info, tooLarge));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_INVALID_KEYLEN');
const asyncErr = getError(() =>
crypto.hkdf('sha256', key, salt, info, tooLarge, common.mustNotCall()));
assertErrorMatch(syncErr, asyncErr, 'HKDF: keylen too large');
}
// === crypto.pbkdf2 / crypto.pbkdf2Sync ===
// PBKDF2: Invalid digest (AdditionalConfig error)
{
const syncErr = getError(() =>
crypto.pbkdf2Sync('password', 'salt', 1, 32, 'nonexistent'));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_INVALID_DIGEST');
// pbkdf2 also throws synchronously for setup errors
const asyncErr = getError(() =>
crypto.pbkdf2('password', 'salt', 1, 32, 'nonexistent',
common.mustNotCall()));
assertErrorMatch(syncErr, asyncErr, 'PBKDF2: invalid digest');
}
// === crypto.scrypt / crypto.scryptSync ===
// Scrypt: Invalid params (AdditionalConfig error sent to OpenSSL)
{
const syncErr = getError(() =>
crypto.scryptSync('password', 'salt', 64, { N: 3 }));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS');
const asyncErr = getError(() =>
crypto.scrypt('password', 'salt', 64, { N: 3 },
common.mustNotCall()));
assertErrorMatch(syncErr, asyncErr, 'Scrypt: N not power of 2');
}
{
const syncErr = getError(() =>
crypto.scryptSync('password', 'salt', 64, { N: 1 }));
assert.strictEqual(syncErr.code, 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS');
const asyncErr = getError(() =>
crypto.scrypt('password', 'salt', 64, { N: 1 },
common.mustNotCall()));
assertErrorMatch(syncErr, asyncErr, 'Scrypt: N=1');
}