crypto: discover ciphers from OpenSSL providers

Enumerate usable ciphers and aliases from activated OpenSSL 3 providers
instead of maintaining lists of provider-only algorithms. Skip numeric
OID aliases and filter NULL, TLS composite, multiblock, and
encrypt-then-MAC implementations that the Cipher APIs cannot use.
Preserve the OpenSSL 1.1.1 and BoringSSL paths.

Expose CBC-CTS, SM4-GCM, SM4-CCM, SM4-XTS, and additional AES key wrap
implementations. Add `ctsMode` (CS1/CS2/CS3) and `xtsStandard`
(GB/IEEE) options for selecting provider CTS and SM4-XTS variants.

Keep ordinary cipher construction on the original binding and legacy
lookup paths.

Lazily cache successful provider fetches per Environment for
string initialization and `getCipherInfo()`.
Index entries by case-insensitive query, canonical, and alias names.
Deduplicate owners by provider and canonical identity. Return borrowed
pointers on warm hits.

Use the shared process-wide FIPS-state generation to invalidate
per-Environment cipher caches and refresh `getCiphers()` snapshots in
the main thread and workers. Existing cipher contexts retain their
implementation and can finish across a transition. Release provider
owners before unloading worker addon DSOs.

Enforce one-shot updates for CBC-CTS, AES key wrap, SIV/GCM-SIV, and
CCM decryption. Reject finalization without required input or CCM tags,
and defer authentication failures to `final()`. Document streaming and
XTS data-unit constraints.

Add known-answer vectors, option validation, provider round trips,
cache, worker, snapshot, FIPS transition, and construction benchmark
coverage.

Fixes: https://github.com/nodejs/node/issues/43040
Fixes: https://github.com/nodejs/node/issues/64866
Refs: https://github.com/nodejs/node/issues/62982
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65484
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Filip Skokan authored and Node.js GitHub Bot committed 2026-08-25 23:11:06 +00:00
1 parent 025e88f981
commit e7a69c4992
26 files changed
+1801 -224

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
'use strict';
const common = require('../common.js');
const assert = require('node:assert');
const {
createCipheriv,
createDecipheriv,
getCiphers,
} = require('node:crypto');
const configurations = {
'aes-128-cbc': { keyLength: 16, ivLength: 16 },
'aes-128-gcm': { keyLength: 16, ivLength: 12 },
'aes-128-cbc-cts': { keyLength: 16, ivLength: 16 },
'aes-128-wrap-inv': { keyLength: 16, ivLength: 8 },
'aes128-wrap-inv': {
keyLength: 16,
ivLength: 8,
warmupCipher: 'aes-128-wrap-inv',
},
};
const ciphers = ['aes-128-cbc', 'aes-128-gcm'];
const availableCiphers = new Set(getCiphers());
for (const cipher of [
'aes-128-cbc-cts',
'aes-128-wrap-inv',
'aes128-wrap-inv',
]) {
if (availableCiphers.has(cipher)) {
ciphers.push(cipher);
}
}
const bench = common.createBenchmark(main, {
n: [1e5],
cipher: ciphers,
operation: ['encrypt', 'decrypt'],
});
function main({ n, cipher, operation }) {
const {
keyLength,
ivLength,
warmupCipher = cipher,
} = configurations[cipher];
const key = Buffer.alloc(keyLength);
const iv = Buffer.alloc(ivLength);
const results = new Array(n);
const method = operation === 'encrypt' ? createCipheriv : createDecipheriv;
const warmup = method(warmupCipher, key, iv);
assert.strictEqual(typeof warmup, 'object');
bench.start();
for (let i = 0; i < n; ++i) {
results[i] = method(cipher, key, iv);
}
bench.end(n);
assert.strictEqual(typeof results[n - 1], 'object');
}
+309 -79
View File
@@ -537,7 +537,7 @@ bool setFipsEnabled(bool enable, CryptoErrorList* errors) {
#else
const bool success = FIPS_mode_set(enable ? 1 : 0) == 1;
#endif
if (isFipsEnabledRaw() != was_enabled) {
if (success && isFipsEnabledRaw() != was_enabled) {
fips_state_generation.fetch_add(1, std::memory_order_release);
}
return success;
@@ -4433,6 +4433,40 @@ constexpr char AsciiToLower(char c) {
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
constexpr auto kUnsupportedCipherFlags =
EVP_CIPH_FLAG_CIPHER_WITH_MAC | EVP_CIPH_FLAG_TLS1_1_MULTIBLOCK;
bool HasUnsupportedCipherFlags(const EVP_CIPHER* cipher) {
return (EVP_CIPHER_get_flags(cipher) & kUnsupportedCipherFlags) != 0;
}
bool IsSupportedLegacyCipher(const EVP_CIPHER* cipher) {
return cipher != nullptr && cipher != EVP_enc_null() &&
!HasUnsupportedCipherFlags(cipher);
}
bool IsSupportedFetchedCipher(const EVP_CIPHER* cipher) {
if (cipher == nullptr || EVP_CIPHER_is_a(cipher, "NULL") ||
HasUnsupportedCipherFlags(cipher)) {
return false;
}
#ifdef OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC
int encrypt_then_mac = 0;
OSSL_PARAM params[] = {
OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC,
&encrypt_then_mac),
OSSL_PARAM_construct_end(),
};
if (EVP_CIPHER_get_params(const_cast<EVP_CIPHER*>(cipher), params) == 1 &&
encrypt_then_mac != 0) {
return false;
}
#endif
return true;
}
void PushAlgorithmAlias(const char* name, void* arg) {
if (name == nullptr) return;
static_cast<std::vector<std::string>*>(arg)->emplace_back(name);
@@ -4440,7 +4474,7 @@ void PushAlgorithmAlias(const char* name, void* arg) {
#endif
} // namespace
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
Cipher::Cipher(DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> cipher)
: cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {}
#endif
@@ -4541,8 +4575,63 @@ const DigestCache::AliasMap& DigestCache::aliases() const {
#endif
}
const EVP_CIPHER* CipherCache::lookup(const char* name, uint64_t generation) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (generation_ != generation) {
aliases_.clear();
ciphers_.clear();
generation_ = generation;
}
const auto it = aliases_.find(name);
if (it == aliases_.end()) return nullptr;
if (it->second >= ciphers_.size()) return nullptr;
return ciphers_[it->second].get();
#else
static_cast<void>(name);
static_cast<void>(generation);
return nullptr;
#endif
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
const EVP_CIPHER* CipherCache::insert(
const char* name,
DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free>&& cipher,
uint64_t generation) {
if (generation_ != generation || cipher == nullptr) return nullptr;
const char* canonical_name = EVP_CIPHER_get0_name(cipher.get());
const OSSL_PROVIDER* provider = EVP_CIPHER_get0_provider(cipher.get());
if (canonical_name != nullptr && provider != nullptr) {
for (size_t id = 0; id < ciphers_.size(); id++) {
const EVP_CIPHER* cached = ciphers_[id].get();
const char* cached_name = EVP_CIPHER_get0_name(cached);
if (EVP_CIPHER_get0_provider(cached) == provider &&
cached_name != nullptr &&
CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
aliases_.insert_or_assign(name, id);
return cached;
}
}
}
ciphers_.emplace_back(std::move(cipher));
const size_t id = ciphers_.size() - 1;
std::vector<std::string> aliases;
EVP_CIPHER_names_do_all(ciphers_[id].get(), PushAlgorithmAlias, &aliases);
for (const std::string& alias : aliases) {
aliases_.emplace(alias, id);
}
aliases_.insert_or_assign(name, id);
return ciphers_[id].get();
}
#endif
Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) {
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (other.fetched_cipher_ != nullptr) {
if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) {
fetched_cipher_.reset(other.fetched_cipher_.get());
@@ -4555,7 +4644,7 @@ Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) {
Cipher& Cipher::operator=(const Cipher& other) {
if (this == &other) return *this;
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (other.fetched_cipher_ != nullptr) {
if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) {
fetched_cipher_.reset(other.fetched_cipher_.get());
@@ -4572,40 +4661,59 @@ Cipher& Cipher::operator=(const Cipher& other) {
return *this;
}
const Cipher Cipher::FromName(const char* name) {
const Cipher Cipher::FromName(const char* name, CipherCache* cache) {
const EVP_CIPHER* cipher = EVP_get_cipherbyname(name);
if (cipher != nullptr) return Cipher(cipher);
if (cipher != nullptr) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (!IsSupportedLegacyCipher(cipher)) return Cipher();
#endif
return Cipher(cipher);
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
// A resolution that overlaps a FIPS transition may use either property
// state. The cache retains the generation observed here, so the first
// resolution begun after the transition clears any stale entries.
const uint64_t generation = getFipsStateGeneration();
if (cache != nullptr) {
if (const EVP_CIPHER* cached = cache->lookup(name, generation)) {
return Cipher(cached);
}
}
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
MarkPopErrorOnReturn mark_pop_error_on_return;
DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched(
EVP_CIPHER_fetch(nullptr, name, nullptr));
if (fetched == nullptr) return Cipher();
if (!IsSupportedFetchedCipher(fetched.get())) return Cipher();
const int mode = EVP_CIPHER_mode(fetched.get());
const bool is_siv_mode =
#if OPENSSL_WITH_AES_SIV
mode == EVP_CIPH_SIV_MODE ||
#endif
#if OPENSSL_WITH_AES_GCM_SIV
mode == EVP_CIPH_GCM_SIV_MODE ||
#endif
false;
if (is_siv_mode) return Cipher(std::move(fetched));
if (cache != nullptr && generation == getFipsStateGeneration()) {
if (const EVP_CIPHER* cached =
cache->insert(name, std::move(fetched), generation)) {
return Cipher(cached);
}
}
return Cipher();
return Cipher(std::move(fetched));
#else
static_cast<void>(cache);
return Cipher();
#endif
}
const Cipher Cipher::FromNid(int nid) {
const Cipher Cipher::FromNid(int nid, CipherCache* cache) {
const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid);
if (cipher != nullptr) return Cipher(cipher);
if (cipher != nullptr) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (!IsSupportedLegacyCipher(cipher)) return Cipher();
#endif
return Cipher(cipher);
}
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
const char* name = OBJ_nid2sn(nid);
if (name != nullptr) return FromName(name);
if (name != nullptr) return FromName(name, cache);
#else
static_cast<void>(cache);
#endif
return Cipher();
@@ -4615,27 +4723,79 @@ const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) {
return Cipher(GetCipherCtxCipher(ctx.get()));
}
const Cipher Cipher::EMPTY = Cipher();
const Cipher Cipher::AES_128_CBC = Cipher::FromNid(NID_aes_128_cbc);
const Cipher Cipher::AES_192_CBC = Cipher::FromNid(NID_aes_192_cbc);
const Cipher Cipher::AES_256_CBC = Cipher::FromNid(NID_aes_256_cbc);
const Cipher Cipher::AES_128_CTR = Cipher::FromNid(NID_aes_128_ctr);
const Cipher Cipher::AES_192_CTR = Cipher::FromNid(NID_aes_192_ctr);
const Cipher Cipher::AES_256_CTR = Cipher::FromNid(NID_aes_256_ctr);
const Cipher Cipher::AES_128_GCM = Cipher::FromNid(NID_aes_128_gcm);
const Cipher Cipher::AES_192_GCM = Cipher::FromNid(NID_aes_192_gcm);
const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm);
const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap);
const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap);
const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap);
namespace {
template <int nid>
const Cipher& GetPredefinedCipher() {
static const Cipher cipher = Cipher::FromNid(nid);
return cipher;
}
} // namespace
const Cipher& Cipher::AES_128_CBC() {
return GetPredefinedCipher<NID_aes_128_cbc>();
}
const Cipher& Cipher::AES_192_CBC() {
return GetPredefinedCipher<NID_aes_192_cbc>();
}
const Cipher& Cipher::AES_256_CBC() {
return GetPredefinedCipher<NID_aes_256_cbc>();
}
const Cipher& Cipher::AES_128_CTR() {
return GetPredefinedCipher<NID_aes_128_ctr>();
}
const Cipher& Cipher::AES_192_CTR() {
return GetPredefinedCipher<NID_aes_192_ctr>();
}
const Cipher& Cipher::AES_256_CTR() {
return GetPredefinedCipher<NID_aes_256_ctr>();
}
const Cipher& Cipher::AES_128_GCM() {
return GetPredefinedCipher<NID_aes_128_gcm>();
}
const Cipher& Cipher::AES_192_GCM() {
return GetPredefinedCipher<NID_aes_192_gcm>();
}
const Cipher& Cipher::AES_256_GCM() {
return GetPredefinedCipher<NID_aes_256_gcm>();
}
const Cipher& Cipher::AES_128_KW() {
return GetPredefinedCipher<NID_id_aes128_wrap>();
}
const Cipher& Cipher::AES_192_KW() {
return GetPredefinedCipher<NID_id_aes192_wrap>();
}
const Cipher& Cipher::AES_256_KW() {
return GetPredefinedCipher<NID_id_aes256_wrap>();
}
#ifndef OPENSSL_IS_BORINGSSL
const Cipher Cipher::AES_128_OCB = Cipher::FromNid(NID_aes_128_ocb);
const Cipher Cipher::AES_192_OCB = Cipher::FromNid(NID_aes_192_ocb);
const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb);
const Cipher& Cipher::AES_128_OCB() {
return GetPredefinedCipher<NID_aes_128_ocb>();
}
const Cipher& Cipher::AES_192_OCB() {
return GetPredefinedCipher<NID_aes_192_ocb>();
}
const Cipher& Cipher::AES_256_OCB() {
return GetPredefinedCipher<NID_aes_256_ocb>();
}
#endif
const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305);
const Cipher& Cipher::CHACHA20_POLY1305() {
return GetPredefinedCipher<NID_chacha20_poly1305>();
}
bool Cipher::isGcmMode() const {
if (!cipher_) return false;
@@ -4657,6 +4817,15 @@ bool Cipher::isCcmMode() const {
return getMode() == EVP_CIPH_CCM_MODE;
}
bool Cipher::isCtsMode() const {
if (!cipher_) return false;
#if NCRYPTO_USE_OPENSSL3_PROVIDER
return (EVP_CIPHER_get_flags(cipher_) & EVP_CIPH_FLAG_CTS) != 0;
#else
return false;
#endif
}
bool Cipher::isOcbMode() const {
if (!cipher_) return false;
return getMode() == EVP_CIPH_OCB_MODE;
@@ -4761,7 +4930,7 @@ const char* Cipher::getName() const {
const char* name = OBJ_nid2sn(nid);
if (name != nullptr) return name;
}
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
return EVP_CIPHER_get0_name(cipher_);
#else
return {};
@@ -4858,11 +5027,57 @@ bool CipherCtxPointer::setAeadTagLength(size_t length) {
ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr);
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
namespace {
// OSSL_CIPHER_PARAM_XTS_STANDARD is not defined by OpenSSL 3.0. Use its
// parameter name directly so custom 3.0 providers can advertise it too.
constexpr char kCipherParamXtsStandard[] = "xts_standard";
bool SetCipherCtxStringParam(EVP_CIPHER_CTX* ctx,
const char* key,
const char* value) {
if (ctx == nullptr || value == nullptr) return false;
const OSSL_PARAM* settable = EVP_CIPHER_CTX_settable_params(ctx);
const OSSL_PARAM* descriptor =
settable == nullptr ? nullptr : OSSL_PARAM_locate_const(settable, key);
if (descriptor == nullptr ||
descriptor->data_type != OSSL_PARAM_UTF8_STRING) {
return false;
}
OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(key, const_cast<char*>(value), 0),
OSSL_PARAM_END,
};
return EVP_CIPHER_CTX_set_params(ctx, params) == 1;
}
} // namespace
#endif
bool CipherCtxPointer::setCtsMode(const char* mode) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
return SetCipherCtxStringParam(ctx_.get(), OSSL_CIPHER_PARAM_CTS_MODE, mode);
#else
static_cast<void>(mode);
return false;
#endif
}
bool CipherCtxPointer::setPadding(bool padding) {
if (!ctx_) return false;
return EVP_CIPHER_CTX_set_padding(ctx_.get(), padding);
}
bool CipherCtxPointer::setXtsStandard(const char* standard) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
return SetCipherCtxStringParam(ctx_.get(), kCipherParamXtsStandard, standard);
#else
static_cast<void>(standard);
return false;
#endif
}
int CipherCtxPointer::getBlockSize() const {
if (!ctx_) return 0;
return EVP_CIPHER_CTX_block_size(ctx_.get());
@@ -4888,6 +5103,16 @@ bool CipherCtxPointer::isCcmMode() const {
return getMode() == EVP_CIPH_CCM_MODE;
}
bool CipherCtxPointer::isCtsMode() const {
if (!ctx_) return false;
return Cipher::FromCtx(*this).isCtsMode();
}
bool CipherCtxPointer::isXtsMode() const {
if (!ctx_) return false;
return getMode() == EVP_CIPH_XTS_MODE;
}
bool CipherCtxPointer::isWrapMode() const {
if (!ctx_) return false;
return getMode() == EVP_CIPH_WRAP_MODE;
@@ -6359,23 +6584,7 @@ struct CipherCallbackContext {
void operator()(const char* name) { cb(name); }
};
#if OPENSSL_WITH_AES_SIV
constexpr const char* kProviderOnlyAesSivCiphers[] = {
"aes-128-siv",
"aes-192-siv",
"aes-256-siv",
};
#endif
#if OPENSSL_WITH_AES_GCM_SIV
constexpr const char* kProviderOnlyAesGcmSivCiphers[] = {
"aes-128-gcm-siv",
"aes-192-gcm-siv",
"aes-256-gcm-siv",
};
#endif
#if OPENSSL_VERSION_MAJOR >= 3
#if NCRYPTO_USE_OPENSSL3_PROVIDER
template <class TypeName,
TypeName* fetch_type(OSSL_LIB_CTX*, const char*, const char*),
void free_type(TypeName*),
@@ -6399,13 +6608,49 @@ void array_push_back(const TypeName* evp_ref,
// instance if the algorithm is supported by the public OpenSSL APIs (some
// algorithms are used internally by OpenSSL and are also passed to this
// callback).
TypeName* fetched = fetch_type(nullptr, real_name, nullptr);
if (fetched == nullptr) return;
DeleteFnPtr<TypeName, free_type> fetched(
fetch_type(nullptr, real_name, nullptr));
if (!IsSupportedFetchedCipher(fetched.get())) return;
free_type(fetched);
auto& cb = *(static_cast<CipherCallbackContext*>(arg));
cb(from);
}
void array_push_back_provider_name(const char* name, void* arg) {
if (name == nullptr) return;
const std::string_view name_view(name);
const bool is_dotted_decimal =
name_view.find('.') != std::string_view::npos &&
std::all_of(name_view.begin(), name_view.end(), [](unsigned char c) {
return (c >= '0' && c <= '9') || c == '.';
});
if (is_dotted_decimal) return;
std::string normalized_name(name_view);
std::transform(normalized_name.begin(),
normalized_name.end(),
normalized_name.begin(),
[](unsigned char c) {
if (c >= 'A' && c <= 'Z') {
return static_cast<char>(c + ('a' - 'A'));
}
return static_cast<char>(c);
});
auto& cb = *(static_cast<CipherCallbackContext*>(arg));
cb(normalized_name.c_str());
}
void array_push_back_provider(EVP_CIPHER* cipher, void* arg) {
const char* name = EVP_CIPHER_get0_name(cipher);
if (name == nullptr) return;
DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched(
EVP_CIPHER_fetch(nullptr, name, nullptr));
if (!IsSupportedFetchedCipher(fetched.get())) return;
EVP_CIPHER_names_do_all(fetched.get(), array_push_back_provider_name, arg);
}
#else
template <class TypeName>
void array_push_back(const TypeName* evp_ref,
@@ -6431,7 +6676,7 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
}
#else
EVP_CIPHER_do_all_sorted(
#if OPENSSL_VERSION_MAJOR >= 3
#if NCRYPTO_USE_OPENSSL3_PROVIDER
array_push_back<EVP_CIPHER,
EVP_CIPHER_fetch,
EVP_CIPHER_free,
@@ -6441,23 +6686,8 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
array_push_back<EVP_CIPHER>,
#endif
&context);
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
auto maybe_push_provider_only_cipher = [&](const char* name) {
EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, name, nullptr);
if (cipher == nullptr) return;
EVP_CIPHER_free(cipher);
context.cb(name);
};
#endif
#if OPENSSL_WITH_AES_SIV
for (const char* name : kProviderOnlyAesSivCiphers) {
maybe_push_provider_only_cipher(name);
}
#endif
#if OPENSSL_WITH_AES_GCM_SIV
for (const char* name : kProviderOnlyAesGcmSivCiphers) {
maybe_push_provider_only_cipher(name);
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
EVP_CIPHER_do_all_provided(nullptr, array_push_back_provider, &context);
#endif
#endif
}
+55 -26
View File
@@ -20,6 +20,8 @@
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \
!defined(OPENSSL_NO_ENGINE)
#include <openssl/engine.h>
@@ -498,6 +500,32 @@ DataPointer xofHashDigest(const Buffer<const unsigned char>& data,
const EVP_MD* md,
size_t length);
class CipherCache final {
public:
CipherCache() = default;
NCRYPTO_DISALLOW_COPY_AND_MOVE(CipherCache)
const EVP_CIPHER* lookup(const char* name, uint64_t generation);
#if NCRYPTO_USE_OPENSSL3_PROVIDER
const EVP_CIPHER* insert(const char* name,
DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free>&& cipher,
uint64_t generation);
#endif
private:
#if NCRYPTO_USE_OPENSSL3_PROVIDER
using EVPCipherPointer = DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free>;
uint64_t generation_ = 0;
std::vector<EVPCipherPointer> ciphers_;
std::unordered_map<std::string,
size_t,
CaseInsensitiveNameHash,
CaseInsensitiveNameEqual>
aliases_;
#endif
};
class Cipher final {
public:
static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH;
@@ -519,7 +547,7 @@ class Cipher final {
Cipher(const Cipher& other);
Cipher& operator=(const Cipher& other);
inline Cipher& operator=(const EVP_CIPHER* cipher) {
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
fetched_cipher_.reset();
#endif
cipher_ = cipher;
@@ -543,6 +571,7 @@ class Cipher final {
bool isWrapMode() const;
bool isCtrMode() const;
bool isCcmMode() const;
bool isCtsMode() const;
bool isOcbMode() const;
bool isSivMode() const;
bool isGcmSivMode() const;
@@ -556,8 +585,8 @@ class Cipher final {
unsigned char* key,
unsigned char* iv) const;
static const Cipher FromName(const char* name);
static const Cipher FromNid(int nid);
static const Cipher FromName(const char* name, CipherCache* cache = nullptr);
static const Cipher FromNid(int nid, CipherCache* cache = nullptr);
static const Cipher FromCtx(const CipherCtxPointer& ctx);
using CipherNameCallback = std::function<void(const char* name)>;
@@ -566,28 +595,24 @@ class Cipher final {
// is able to do so.
static void ForEach(CipherNameCallback callback);
// Utilities to get various ciphers by type. If the underlying
// implementation does not support the requested cipher, then
// the result will be an empty Cipher object whose bool operator
// will return false.
static const Cipher EMPTY;
static const Cipher AES_128_CBC;
static const Cipher AES_192_CBC;
static const Cipher AES_256_CBC;
static const Cipher AES_128_CTR;
static const Cipher AES_192_CTR;
static const Cipher AES_256_CTR;
static const Cipher AES_128_GCM;
static const Cipher AES_192_GCM;
static const Cipher AES_256_GCM;
static const Cipher AES_128_KW;
static const Cipher AES_192_KW;
static const Cipher AES_256_KW;
static const Cipher AES_128_OCB;
static const Cipher AES_192_OCB;
static const Cipher AES_256_OCB;
static const Cipher CHACHA20_POLY1305;
// Lazily resolves common ciphers. If the underlying implementation does not
// support the requested cipher, the returned Cipher will be empty.
static const Cipher& AES_128_CBC();
static const Cipher& AES_192_CBC();
static const Cipher& AES_256_CBC();
static const Cipher& AES_128_CTR();
static const Cipher& AES_192_CTR();
static const Cipher& AES_256_CTR();
static const Cipher& AES_128_GCM();
static const Cipher& AES_192_GCM();
static const Cipher& AES_256_GCM();
static const Cipher& AES_128_KW();
static const Cipher& AES_192_KW();
static const Cipher& AES_256_KW();
static const Cipher& AES_128_OCB();
static const Cipher& AES_192_OCB();
static const Cipher& AES_256_OCB();
static const Cipher& CHACHA20_POLY1305();
struct CipherParams {
int padding;
@@ -617,7 +642,7 @@ class Cipher final {
private:
const EVP_CIPHER* cipher_ = nullptr;
#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
#if NCRYPTO_USE_OPENSSL3_PROVIDER
explicit Cipher(DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> cipher);
DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched_cipher_;
#endif
@@ -1007,7 +1032,9 @@ class CipherCtxPointer final {
bool setIvLength(size_t length);
bool setAeadTag(const Buffer<const char>& tag);
bool setAeadTagLength(size_t length);
bool setCtsMode(const char* mode);
bool setPadding(bool padding);
bool setXtsStandard(const char* standard);
bool init(const Cipher& cipher,
bool encrypt,
const unsigned char* key = nullptr,
@@ -1020,6 +1047,8 @@ class CipherCtxPointer final {
bool isGcmMode() const;
bool isOcbMode() const;
bool isCcmMode() const;
bool isCtsMode() const;
bool isXtsMode() const;
bool isWrapMode() const;
bool isSivMode() const;
bool isGcmSivMode() const;
+207 -60
View File
@@ -633,6 +633,10 @@ The [`crypto.createCipheriv()`][] method is
used to create `Cipheriv` instances. `Cipheriv` objects are not to be created
directly using the `new` keyword.
The selected algorithm may impose additional restrictions on streaming and
calls to [`cipher.update()`][]. See [CCM mode][], [CBC-CTS mode][], [XTS mode][],
[AES key wrap modes][], and [SIV and GCM-SIV modes][].
Example: Using `Cipheriv` objects as streams:
```mjs
@@ -905,14 +909,15 @@ added: v0.7.1
* `autoPadding` {boolean} **Default:** `true`
* Returns: {Cipheriv} The same `Cipheriv` instance for method chaining.
When using block encryption algorithms, the `Cipheriv` class will automatically
add padding to the input data to the appropriate block size. To disable the
default padding call `cipher.setAutoPadding(false)`.
When using block ciphers that use standard block padding, the `Cipheriv` class
will automatically add padding to the input data to the appropriate block size.
To disable the default padding call `cipher.setAutoPadding(false)`.
When `autoPadding` is `false`, the length of the entire input data must be a
multiple of the cipher's block size or [`cipher.final()`][] will throw an error.
Disabling automatic padding is useful for non-standard padding, for instance
using `0x0` instead of PKCS padding.
For block ciphers that use standard block padding, when `autoPadding` is
`false`, the length of the entire input data must be a multiple of the cipher's
block size or [`cipher.final()`][] will throw an error. Disabling automatic
padding is useful for non-standard padding, for instance using `0x0` instead of
PKCS padding.
The `cipher.setAutoPadding()` method must be called before
[`cipher.final()`][].
@@ -946,9 +951,12 @@ is specified, a string using the specified encoding is returned. If no
When `outputEncoding` is specified, it must use the same encoding as previous
calls to `cipher.update()`.
The `cipher.update()` method can be called multiple times with new data until
[`cipher.final()`][] is called. Calling `cipher.update()` after
[`cipher.final()`][] will result in an error being thrown.
For most algorithms, `cipher.update()` can be called multiple times with new
data until [`cipher.final()`][] is called. Some algorithms restrict calls to
`cipher.update()`. For example, [CCM mode][], [CBC-CTS mode][], [XTS mode][],
[AES key wrap modes][], and [SIV and GCM-SIV modes][] require the whole message
in a single call. Calling `cipher.update()` after [`cipher.final()`][] will
result in an error being thrown.
## Class: `Decipheriv`
@@ -970,6 +978,10 @@ The [`crypto.createDecipheriv()`][] method is
used to create `Decipheriv` instances. `Decipheriv` objects are not to be created
directly using the `new` keyword.
The selected algorithm may impose additional restrictions on streaming and
calls to [`decipher.update()`][]. See [CCM mode][], [CBC-CTS mode][],
[XTS mode][], [AES key wrap modes][], and [SIV and GCM-SIV modes][].
Example: Using `Decipheriv` objects as streams:
```mjs
@@ -1267,8 +1279,8 @@ When data has been encrypted without standard block padding, calling
`decipher.setAutoPadding(false)` will disable automatic padding to prevent
[`decipher.final()`][] from checking for and removing padding.
Turning auto padding off will only work if the input data's length is a
multiple of the ciphers block size.
For block ciphers that use standard block padding, disabling it requires the
input data's length to be a multiple of the cipher's block size.
The `decipher.setAutoPadding()` method must be called before
[`decipher.final()`][].
@@ -1291,19 +1303,23 @@ changes:
Updates the decipher with `data`. If the `inputEncoding` argument is given,
the `data`
argument is a string using the specified encoding. If the `inputEncoding`
argument is not given, `data` must be a [`Buffer`][]. If `data` is a
[`Buffer`][] then `inputEncoding` is ignored.
argument is not given, `data` must be a [`Buffer`][], `TypedArray`, or
`DataView`. If `data` is a [`Buffer`][], `TypedArray`, or `DataView`, then
`inputEncoding` is ignored.
The `outputEncoding` specifies the output format of the enciphered
The `outputEncoding` specifies the output format of the deciphered
data. If the `outputEncoding`
is specified, a string using the specified encoding is returned. If no
`outputEncoding` is provided, a [`Buffer`][] is returned.
When `outputEncoding` is specified, it must use the same encoding as previous
calls to `decipher.update()`.
The `decipher.update()` method can be called multiple times with new data until
[`decipher.final()`][] is called. Calling `decipher.update()` after
[`decipher.final()`][] will result in an error being thrown.
For most algorithms, `decipher.update()` can be called multiple times with new
data until [`decipher.final()`][] is called. Some algorithms restrict calls to
`decipher.update()`. For example, [CCM mode][], [CBC-CTS mode][], [XTS mode][],
[AES key wrap modes][], and [SIV and GCM-SIV modes][] require the whole message
in a single call. Calling `decipher.update()` after [`decipher.final()`][] will
result in an error being thrown.
Even if the underlying cipher implements authentication, the authenticity and
integrity of the plaintext returned from this function may be uncertain at this
@@ -3566,6 +3582,12 @@ operations. The specific constants currently defined are described in
<!-- YAML
added: v0.1.94
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65484
description: Additional ciphers available through OpenSSL providers (e.g.
SM4-GCM, SM4-CCM, SM4-XTS, CBC-CTS, and AES key-wrap variants)
are now supported. The `ctsMode` and `xtsStandard` options
were added.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63411
description: Ciphers in SIV and GCM-SIV modes are now supported.
@@ -3610,47 +3632,74 @@ changes:
* `algorithm` {string}
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject}
* `iv` {string|ArrayBuffer|Buffer|TypedArray|DataView|null}
* `options` {Object} [`stream.transform` options][]
* `options` {Object} [`stream.transform` options][] with these additional
properties:
* `authTagLength` {number} The authentication tag length in bytes. Its
requirements and default depend on the authenticated cipher, as described
below.
* `ctsMode` {string} The [CBC-CTS mode][] variant. One of `'CS1'`, `'CS2'`,
or `'CS3'`. The values are case-sensitive. **Default:** `'CS1'`.
* `encoding` {string} The [encoding][] to use when `key` is a string. This
option does not affect a string `iv`, which is always interpreted as UTF-8.
**Default:** `'utf8'`.
* `xtsStandard` {string} The standard used by `sm4-xts`. One of `'GB'` or
`'IEEE'`. The values are case-sensitive. **Default:** `'GB'`.
* Returns: {Cipheriv}
Creates and returns a `Cipheriv` object, with the given `algorithm`, `key` and
initialization vector (`iv`).
The `options` argument controls stream behavior and is optional except when a
cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][]. In GCM mode, the `authTagLength`
option is not required but can be used to set the length of the authentication
tag that will be returned by `getAuthTag()` and defaults to 16 bytes.
The `options` argument controls cipher-specific settings and stream behavior.
It is optional except when a cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`)
is used. In that case, the `authTagLength` option is required and specifies the
length of the authentication tag in bytes, see [CCM mode][]. In GCM mode, the
`authTagLength` option is not required but can be used to set the length of the
authentication tag that will be returned by `getAuthTag()` and defaults to 16
bytes.
For `SIV`, `GCM-SIV`, and `chacha20-poly1305`, the `authTagLength` option
defaults to 16 bytes. `SIV` and `GCM-SIV` only support 16-byte authentication
tags.
The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
recent OpenSSL releases, `openssl list -cipher-algorithms` will
display the available cipher algorithms.
The `ctsMode` and `xtsStandard` options configure parameters exposed by OpenSSL
providers. They are available only with OpenSSL 3.0 or later and a provider
that supports the corresponding parameter. `ctsMode` applies only to CBC-CTS
ciphers, and `xtsStandard` applies only to `sm4-xts`. Supplying either option
for an available cipher implementation that does not support it throws an
`ERR_CRYPTO_UNSUPPORTED_OPERATION` error. See [CBC-CTS mode][] and [XTS mode][]
for details.
The available algorithms depend on OpenSSL. [`crypto.getCiphers()`][] lists the
algorithms exposed by Node.js. On recent OpenSSL releases,
`openssl list -cipher-algorithms` displays the algorithms available to OpenSSL,
which can include algorithms that Node.js does not expose.
The `key` is the raw key used by the `algorithm` and `iv` is an
[initialization vector][]. Both arguments must be `'utf8'` encoded strings,
[Buffers][`Buffer`], `TypedArray`, or `DataView`s. The `key` may optionally be
a [`KeyObject`][] of type `secret`. If the cipher does not need
an initialization vector, `iv` may be `null`.
[initialization vector][]. Each may be a string, `ArrayBuffer`, [`Buffer`][],
`TypedArray`, or `DataView`. A string `key` is decoded using `options.encoding`,
which defaults to `'utf8'`; a string `iv` is always decoded as UTF-8. The `key`
may optionally be a [`KeyObject`][] of type `secret`. If the cipher does not
need an initialization vector, `iv` may be `null`.
When passing strings for `key` or `iv`, please consider
[caveats when using strings as inputs to cryptographic APIs][].
Initialization vectors should be unpredictable and unique; ideally, they will be
cryptographically random. They do not have to be secret: IVs are typically just
added to ciphertext messages unencrypted. It may sound contradictory that
something has to be unpredictable and unique, but does not have to be secret;
remember that an attacker must not be able to predict ahead of time what a
given IV will be.
Initialization vector requirements depend on the algorithm. For some
algorithms, an IV must be unpredictable and unique; for others, uniqueness
alone is sufficient, a fixed value is required, or no IV is used. Follow the
requirements for the selected algorithm. IVs typically do not have to be secret
and can be transmitted with the ciphertext.
### `crypto.createDecipheriv(algorithm, key, iv[, options])`
<!-- YAML
added: v0.1.94
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65484
description: Additional ciphers available through OpenSSL providers (e.g.
SM4-GCM, SM4-CCM, SM4-XTS, CBC-CTS, and AES key-wrap variants)
are now supported. The `ctsMode` and `xtsStandard` options
were added.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63411
description: Ciphers in SIV and GCM-SIV modes are now supported.
@@ -3691,40 +3740,59 @@ changes:
* `algorithm` {string}
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject}
* `iv` {string|ArrayBuffer|Buffer|TypedArray|DataView|null}
* `options` {Object} [`stream.transform` options][]
* `options` {Object} [`stream.transform` options][] with these additional
properties:
* `authTagLength` {number} The authentication tag length in bytes. Its
requirements and default depend on the authenticated cipher, as described
below.
* `ctsMode` {string} The [CBC-CTS mode][] variant. One of `'CS1'`, `'CS2'`,
or `'CS3'`. The values are case-sensitive. **Default:** `'CS1'`.
* `encoding` {string} The [encoding][] to use when `key` is a string. This
option does not affect a string `iv`, which is always interpreted as UTF-8.
**Default:** `'utf8'`.
* `xtsStandard` {string} The standard used by `sm4-xts`. One of `'GB'` or
`'IEEE'`. The values are case-sensitive. **Default:** `'GB'`.
* Returns: {Decipheriv}
Creates and returns a `Decipheriv` object that uses the given `algorithm`, `key`
and initialization vector (`iv`).
The `options` argument controls stream behavior and is optional except when a
cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`) is used. In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][].
For AES-GCM and `chacha20-poly1305`, the `authTagLength` option defaults to 16
bytes and must be set to a different value if a different length is used. For
`SIV` and `GCM-SIV`, the `authTagLength` option defaults to 16 bytes and only
16-byte authentication tags are supported.
The `options` argument controls cipher-specific settings and stream behavior.
It is optional except when a cipher in CCM or OCB mode (e.g. `'aes-128-ccm'`)
is used. In that case, the `authTagLength` option is required and specifies the
length of the authentication tag in bytes, see [CCM mode][]. For GCM and
`chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes and must be
set if a different length is used. For `SIV` and `GCM-SIV`, the `authTagLength`
option defaults to 16 bytes and only 16-byte authentication tags are supported.
The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
recent OpenSSL releases, `openssl list -cipher-algorithms` will
display the available cipher algorithms.
The `ctsMode` and `xtsStandard` options configure parameters exposed by OpenSSL
providers. They are available only with OpenSSL 3.0 or later and a provider
that supports the corresponding parameter. `ctsMode` applies only to CBC-CTS
ciphers, and `xtsStandard` applies only to `sm4-xts`. Supplying either option
for an available cipher implementation that does not support it throws an
`ERR_CRYPTO_UNSUPPORTED_OPERATION` error. See [CBC-CTS mode][] and [XTS mode][]
for details.
The available algorithms depend on OpenSSL. [`crypto.getCiphers()`][] lists the
algorithms exposed by Node.js. On recent OpenSSL releases,
`openssl list -cipher-algorithms` displays the algorithms available to OpenSSL,
which can include algorithms that Node.js does not expose.
The `key` is the raw key used by the `algorithm` and `iv` is an
[initialization vector][]. Both arguments must be `'utf8'` encoded strings,
[Buffers][`Buffer`], `TypedArray`, or `DataView`s. The `key` may optionally be
a [`KeyObject`][] of type `secret`. If the cipher does not need
an initialization vector, `iv` may be `null`.
[initialization vector][]. Each may be a string, `ArrayBuffer`, [`Buffer`][],
`TypedArray`, or `DataView`. A string `key` is decoded using `options.encoding`,
which defaults to `'utf8'`; a string `iv` is always decoded as UTF-8. The `key`
may optionally be a [`KeyObject`][] of type `secret`. If the cipher does not
need an initialization vector, `iv` may be `null`.
When passing strings for `key` or `iv`, please consider
[caveats when using strings as inputs to cryptographic APIs][].
Initialization vectors should be unpredictable and unique; ideally, they will be
cryptographically random. They do not have to be secret: IVs are typically just
added to ciphertext messages unencrypted. It may sound contradictory that
something has to be unpredictable and unique, but does not have to be secret;
remember that an attacker must not be able to predict ahead of time what a given
IV will be.
Initialization vector requirements depend on the algorithm. For some
algorithms, an IV must be unpredictable and unique; for others, uniqueness
alone is sufficient, a fixed value is required, or no IV is used. Follow the
requirements for the selected algorithm. IVs typically do not have to be secret
and can be transmitted with the ciphertext.
### `crypto.createDiffieHellman(prime[, primeEncoding][, generator][, generatorEncoding])`
@@ -6834,6 +6902,79 @@ try {
console.log(receivedPlaintext);
```
### CBC-CTS mode
For CBC ciphertext stealing (CBC-CTS) ciphers, the `ctsMode` option to
[`crypto.createCipheriv()`][] or [`crypto.createDecipheriv()`][] selects the
variant:
* `'CS1'` is the default. For block-aligned input, its output is the same as CBC
mode.
* `'CS2'` is also the same as CBC for block-aligned input. For input with a
partial final block, it swaps the final full and partial ciphertext blocks
relative to CS1.
* `'CS3'` is the Kerberos 5 variant. It uses the CS2 ordering for a partial
final block and swaps the final two ciphertext blocks even for block-aligned
input.
Encryption and decryption must use the same variant. The option is available
only with CBC-CTS provider ciphers on OpenSSL 3.0 or later.
Applications which use this mode must adhere to these restrictions:
* The plaintext or ciphertext must be at least one block long.
* The ciphertext has the same length as the plaintext.
* `cipher.update()` or `decipher.update()` must be called exactly once with all
input data. Stream methods such as `write(data)`, `end(data)`, or `pipe()` may
fail because CBC-CTS does not accept multiple data updates.
* `cipher.final()` or `decipher.final()` must still be called to complete the
operation.
* `crypto.getCipherInfo()` reports the base mode as `'cbc'`.
### XTS mode
XTS ciphers operate on independently tweakable data units. A `Cipheriv` or
`Decipheriv` instance represents one complete data unit, and its `iv` argument
provides the 16-byte tweak. Use a new instance with the appropriate positional
tweak for each different logical data unit.
Applications which use XTS mode must adhere to these restrictions:
* The plaintext or ciphertext must be at least one 16-byte block. Its length
does not have to be a multiple of 16 bytes because XTS uses ciphertext
stealing for a final partial block.
* The ciphertext has the same length as the plaintext.
* `cipher.update()` or `decipher.update()` must be called exactly once with all
input data. Stream methods such as `write(data)`, `end(data)`, or `pipe()` may
fail because XTS does not accept multiple data updates.
* `cipher.final()` or `decipher.final()` must still be called to complete the
operation.
For `sm4-xts`, the `xtsStandard` option to [`crypto.createCipheriv()`][] or
[`crypto.createDecipheriv()`][] selects either the default `'GB'` variant from
GB/T 17964-2021 or the `'IEEE'` variant from IEEE Std 1619-2007. Encryption and
decryption must use the same variant. The option is available only for
`sm4-xts`; it does not apply to AES-XTS ciphers. OpenSSL's default provider
supports `sm4-xts` in OpenSSL 3.2 or later.
### AES key wrap modes
AES key wrap (`AES-WRAP`) and AES key wrap with padding (`AES-WRAP-PAD`)
ciphers operate on a complete key-data value rather than on an incremental byte
stream. The inverse-transform variants have the same processing restrictions.
Applications which use an AES key wrap cipher must adhere to these
restrictions:
* `cipher.update()` or `decipher.update()` must be called exactly once with the
complete, non-empty input value.
* Do not use AES key wrap ciphers as generic [`stream.Transform`][] streams.
Methods such as `write(data)`, `end(data)`, and `pipe()` can split one value
across multiple updates, with each update being treated as a separate wrap or
unwrap operation.
* `cipher.final()` or `decipher.final()` must still be called to complete the
operation.
### SIV and GCM-SIV modes
`SIV`[^openssl30] and `GCM-SIV`[^openssl32] are supported [AEAD algorithms][]
@@ -7259,6 +7400,8 @@ See the [list of SSL OP Flags][] for details.
[^openssl35]: Requires OpenSSL >= 3.5
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
[AES key wrap modes]: #aes-key-wrap-modes
[CBC-CTS mode]: #cbc-cts-mode
[CCM mode]: #ccm-mode
[CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532
[Caveats]: #support-for-weak-or-compromised-algorithms
@@ -7288,7 +7431,9 @@ See the [list of SSL OP Flags][] for details.
[RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt
[SIV and GCM-SIV modes]: #siv-and-gcm-siv-modes
[Web Crypto API documentation]: webcrypto.md
[XTS mode]: #xts-mode
[`--allow-openssl-store`]: cli.md#--allow-openssl-store
[`--enable-fips`]: cli.md#--enable-fips
[`--force-fips`]: cli.md#--force-fips
@@ -7318,6 +7463,7 @@ See the [list of SSL OP Flags][] for details.
[`crypto.createVerify()`]: #cryptocreateverifyalgorithm-options
[`crypto.generateKey()`]: #cryptogeneratekeytype-options-callback
[`crypto.generateKeyPair()`]: #cryptogeneratekeypairtype-options-callback
[`crypto.getCiphers()`]: #cryptogetciphers
[`crypto.getCurves()`]: #cryptogetcurves
[`crypto.getDiffieHellman()`]: #cryptogetdiffiehellmangroupname
[`crypto.getFips()`]: #cryptogetfips
@@ -7350,6 +7496,7 @@ See the [list of SSL OP Flags][] for details.
[`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist
[`sign.sign()`]: #signsignprivatekey-outputencoding
[`sign.update()`]: #signupdatedata-inputencoding
[`stream.Transform`]: stream.md#class-streamtransform
[`stream.Writable` options]: stream.md#new-streamwritableoptions
[`stream.transform` options]: stream.md#new-streamtransformoptions
[`util.promisify()`]: util.md#utilpromisifyoriginal
+30 -2
View File
@@ -33,6 +33,7 @@ const {
const {
validateEncoding,
validateOneOf,
validateUint32,
validateObject,
validateString,
@@ -60,6 +61,9 @@ const { normalizeEncoding, kEmptyObject } = require('internal/util');
const { StringDecoder } = require('string_decoder');
const kCtsModes = ['CS1', 'CS2', 'CS3'];
const kXtsStandards = ['GB', 'IEEE'];
function rsaFunctionFor(method, defaultPadding, keyType) {
const keyName = keyType === 'private' ? 'privateKey' : undefined;
return (key, buffer) => {
@@ -119,8 +123,32 @@ function getUIntOption(options, key) {
}
function createCipherBase(cipher, credential, options, isEncrypt, iv) {
const authTagLength = getUIntOption(options, 'authTagLength');
this[kHandle] = new CipherBase(isEncrypt, cipher, credential, iv, authTagLength);
let authTagLength = -1;
let ctsMode;
let xtsStandard;
if (options != null) {
authTagLength = getUIntOption(options, 'authTagLength');
ctsMode = getStringOption(options, 'ctsMode') ?? undefined;
xtsStandard = getStringOption(options, 'xtsStandard') ?? undefined;
if (ctsMode !== undefined)
validateOneOf(ctsMode, 'options.ctsMode', kCtsModes);
if (xtsStandard !== undefined)
validateOneOf(xtsStandard, 'options.xtsStandard', kXtsStandards);
}
if (ctsMode === undefined && xtsStandard === undefined) {
this[kHandle] = new CipherBase(
isEncrypt, cipher, credential, iv, authTagLength);
} else {
this[kHandle] = new CipherBase(
isEncrypt,
cipher,
credential,
iv,
authTagLength,
ctsMode,
xtsStandard);
}
this._decoder = null;
FunctionPrototypeCall(LazyTransform, this, options);
+2 -1
View File
@@ -164,7 +164,8 @@ function getCachedHashId(algorithm) {
return result === undefined ? -1 : result;
}
const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers()));
const getCiphers = cachedArrayByFipsGeneration(
() => filterDuplicateStrings(_getCiphers()));
const getHashes = cachedArrayByFipsGeneration(
() => filterDuplicateStrings(_getHashes()),
() => {
+15 -15
View File
@@ -13,15 +13,15 @@ namespace node::crypto {
constexpr unsigned kNoAuthTagLength = static_cast<unsigned>(-1);
#define VARIANTS_COMMON(V) \
V(CTR_128, AES_CTR_Cipher, ncrypto::Cipher::AES_128_CTR) \
V(CTR_192, AES_CTR_Cipher, ncrypto::Cipher::AES_192_CTR) \
V(CTR_256, AES_CTR_Cipher, ncrypto::Cipher::AES_256_CTR) \
V(CBC_128, AES_Cipher, ncrypto::Cipher::AES_128_CBC) \
V(CBC_192, AES_Cipher, ncrypto::Cipher::AES_192_CBC) \
V(CBC_256, AES_Cipher, ncrypto::Cipher::AES_256_CBC) \
V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM) \
V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM) \
V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM) \
V(CTR_128, AES_CTR_Cipher, ncrypto::Cipher::AES_128_CTR()) \
V(CTR_192, AES_CTR_Cipher, ncrypto::Cipher::AES_192_CTR()) \
V(CTR_256, AES_CTR_Cipher, ncrypto::Cipher::AES_256_CTR()) \
V(CBC_128, AES_Cipher, ncrypto::Cipher::AES_128_CBC()) \
V(CBC_192, AES_Cipher, ncrypto::Cipher::AES_192_CBC()) \
V(CBC_256, AES_Cipher, ncrypto::Cipher::AES_256_CBC()) \
V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM()) \
V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM()) \
V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM()) \
VARIANTS_KW(V)
#ifdef OPENSSL_IS_BORINGSSL
@@ -33,16 +33,16 @@ constexpr unsigned kNoAuthTagLength = static_cast<unsigned>(-1);
V(KW_256, AES_KW_Cipher, static_cast<const EVP_CIPHER*>(nullptr))
#else
#define VARIANTS_KW(V) \
V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW) \
V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \
V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW)
V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW()) \
V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW()) \
V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW())
#endif
#if OPENSSL_WITH_AES_OCB
#define VARIANTS_OCB(V) \
V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \
V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \
V(OCB_256, AES_Cipher, ncrypto::Cipher::AES_256_OCB)
V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB()) \
V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB()) \
V(OCB_256, AES_Cipher, ncrypto::Cipher::AES_256_OCB())
#else
#define VARIANTS_OCB(V)
#endif
+1 -1
View File
@@ -105,7 +105,7 @@ Maybe<void> ChaCha20Poly1305CipherTraits::AdditionalConfig(
ChaCha20Poly1305CipherConfig* params) {
Environment* env = Environment::GetCurrent(args);
params->cipher = ncrypto::Cipher::CHACHA20_POLY1305;
params->cipher = ncrypto::Cipher::CHACHA20_POLY1305();
#ifndef OPENSSL_IS_BORINGSSL
// On BoringSSL, ChaCha20-Poly1305 is not exposed via the EVP_CIPHER registry
+72 -24
View File
@@ -75,10 +75,10 @@ void GetCipherInfo(const FunctionCallbackInfo<Value>& args) {
const auto cipher = ([&] {
if (args[0]->IsString()) {
Utf8Value name(env->isolate(), args[0]);
return Cipher::FromName(*name);
return Cipher::FromName(*name, env->provider_cipher_cache.get());
} else {
int nid = args[0].As<Int32>()->Value();
return Cipher::FromNid(nid);
return Cipher::FromNid(nid, env->provider_cipher_cache.get());
}
})();
@@ -227,7 +227,7 @@ CipherBase::CipherBase(Environment* env, Local<Object> wrap, CipherKind kind)
auth_tag_state_(kAuthTagUnknown),
auth_tag_len_(kNoAuthTagLength),
pending_auth_failed_(false),
has_siv_update_(false),
has_one_shot_update_(false),
siv_aad_components_(0) {
MakeWeak();
}
@@ -311,7 +311,7 @@ void CipherBase::RegisterExternalReferences(
void CipherBase::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 5);
CHECK(args.Length() == 5 || args.Length() == 7);
CipherBase* cipher =
new CipherBase(env, args.This(), args[0]->IsTrue() ? kCipher : kDecipher);
@@ -343,7 +343,25 @@ void CipherBase::New(const FunctionCallbackInfo<Value>& args) {
auth_tag_len = kNoAuthTagLength;
}
cipher->InitIv(*cipher_type, key_buf, iv_buf, auth_tag_len);
if (args.Length() == 5) {
cipher->InitIv(
*cipher_type, key_buf, iv_buf, auth_tag_len, nullptr, nullptr);
return;
}
CHECK(args[5]->IsString() || args[5]->IsUndefined());
CHECK(args[6]->IsString() || args[6]->IsUndefined());
const Utf8Value cts_mode(env->isolate(),
args[5]->IsString() ? args[5] : Local<Value>());
const Utf8Value xts_standard(env->isolate(),
args[6]->IsString() ? args[6] : Local<Value>());
cipher->InitIv(*cipher_type,
key_buf,
iv_buf,
auth_tag_len,
args[5]->IsString() ? *cts_mode : nullptr,
args[6]->IsString() ? *xts_standard : nullptr);
}
void CipherBase::CommonInit(const char* cipher_type,
@@ -352,7 +370,9 @@ void CipherBase::CommonInit(const char* cipher_type,
int key_len,
const unsigned char* iv,
int iv_len,
unsigned int auth_tag_len) {
unsigned int auth_tag_len,
const char* cts_mode,
const char* xts_standard) {
MarkPopErrorOnReturn mark_pop_error_on_return;
CHECK(!ctx_);
ctx_ = CipherCtxPointer::New();
@@ -372,6 +392,18 @@ void CipherBase::CommonInit(const char* cipher_type,
"Failed to initialize cipher");
}
if (cts_mode != nullptr && !ctx_.setCtsMode(cts_mode)) {
ctx_.reset();
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env(), "%s does not support the ctsMode option", cipher_type);
}
if (xts_standard != nullptr && !ctx_.setXtsStandard(xts_standard)) {
ctx_.reset();
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env(), "%s does not support the xtsStandard option", cipher_type);
}
if (cipher.isSupportedAuthenticatedMode()) {
CHECK_GE(iv_len, 0);
if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len)) {
@@ -394,11 +426,14 @@ void CipherBase::CommonInit(const char* cipher_type,
void CipherBase::InitIv(const char* cipher_type,
const ByteSource& key_buf,
const ArrayBufferOrViewContents<unsigned char>& iv_buf,
unsigned int auth_tag_len) {
unsigned int auth_tag_len,
const char* cts_mode,
const char* xts_standard) {
HandleScope scope(env()->isolate());
MarkPopErrorOnReturn mark_pop_error_on_return;
auto cipher = Cipher::FromName(cipher_type);
auto cipher =
Cipher::FromName(cipher_type, env()->provider_cipher_cache.get());
if (!cipher) return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env());
const int expected_iv_len = cipher.getIvLength();
@@ -436,14 +471,15 @@ void CipherBase::InitIv(const char* cipher_type,
return THROW_ERR_CRYPTO_INVALID_IV(env());
}
CommonInit(
cipher_type,
cipher,
key_buf.data<unsigned char>(),
key_buf.size(),
iv_buf.data(),
iv_buf.size(),
auth_tag_len);
CommonInit(cipher_type,
cipher,
key_buf.data<unsigned char>(),
key_buf.size(),
iv_buf.data(),
iv_buf.size(),
auth_tag_len,
cts_mode,
xts_standard);
}
bool CipherBase::InitAuthenticated(const char* cipher_type,
@@ -563,7 +599,7 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
}
if ((cipher->ctx_.isSivMode() || cipher->ctx_.isGcmSivMode()) &&
cipher->has_siv_update_) {
cipher->has_one_shot_update_) {
return args.GetReturnValue().Set(false);
}
@@ -598,7 +634,7 @@ bool CipherBase::SetAAD(
if (!ctx_ || !IsAuthenticatedMode())
return false;
const bool is_siv = ctx_.isSivMode();
if ((is_siv || ctx_.isGcmSivMode()) && has_siv_update_) return false;
if ((is_siv || ctx_.isGcmSivMode()) && has_one_shot_update_) return false;
if (is_siv && siv_aad_components_ >= kMaxSivAADComponents) return false;
MarkPopErrorOnReturn mark_pop_error_on_return;
@@ -659,12 +695,20 @@ CipherBase::UpdateResult CipherBase::Update(
if (!ctx_ || len > INT_MAX) return kErrorState;
MarkPopErrorOnReturn mark_pop_error_on_return;
if (ctx_.isCcmMode() && !CheckCCMMessageLength(len)) {
const bool is_ccm_mode = ctx_.isCcmMode();
const bool is_ccm_decipher = kind_ == kDecipher && is_ccm_mode;
if (is_ccm_mode && !CheckCCMMessageLength(len)) {
return kErrorMessageSize;
}
const bool is_siv = ctx_.isSivMode() || ctx_.isGcmSivMode();
if (is_siv && has_siv_update_) {
const bool is_cts = ctx_.isCtsMode();
const bool is_xts = ctx_.isXtsMode();
const bool is_wrap = ctx_.isWrapMode();
const bool is_one_shot =
is_ccm_decipher || is_siv || is_cts || is_xts || is_wrap;
if (is_one_shot && has_one_shot_update_) {
return kErrorState;
}
@@ -694,8 +738,8 @@ CipherBase::UpdateResult CipherBase::Update(
bool r = ctx_.update(
buffer, static_cast<unsigned char*>((*out)->Data()), &buf_len);
if (is_siv) {
has_siv_update_ = true;
if (is_ccm_decipher || is_siv || ((is_cts || is_xts || is_wrap) && r)) {
has_one_shot_update_ = true;
}
// When in CCM mode, EVP_CipherUpdate will fail if the authentication tag is
@@ -773,7 +817,10 @@ void CipherBase::SetAutoPadding(const FunctionCallbackInfo<Value>& args) {
bool CipherBase::Final(std::unique_ptr<BackingStore>* out) {
if (!ctx_) return false;
if ((ctx_.isSivMode() || ctx_.isGcmSivMode()) && !has_siv_update_) {
const bool is_one_shot = ctx_.isSivMode() || ctx_.isGcmSivMode() ||
ctx_.isCtsMode() || ctx_.isXtsMode() ||
ctx_.isWrapMode();
if (is_one_shot && !has_one_shot_update_) {
ctx_.reset();
return false;
}
@@ -796,7 +843,8 @@ bool CipherBase::Final(std::unique_ptr<BackingStore>* out) {
// EVP_CipherFinal_ex must not be called and will fail.
bool ok;
if (kind_ == kDecipher && ctx_.isCcmMode()) {
ok = !pending_auth_failed_;
ok = auth_tag_state_ == kAuthTagSetByUser && has_one_shot_update_ &&
!pending_auth_failed_;
*out = ArrayBuffer::NewBackingStore(env()->isolate(), 0);
} else {
int out_len = (*out)->ByteLength();
+7 -3
View File
@@ -51,11 +51,15 @@ class CipherBase : public BaseObject {
int key_len,
const unsigned char* iv,
int iv_len,
unsigned int auth_tag_len);
unsigned int auth_tag_len,
const char* cts_mode,
const char* xts_standard);
void InitIv(const char* cipher_type,
const ByteSource& key_buf,
const ArrayBufferOrViewContents<unsigned char>& iv_buf,
unsigned int auth_tag_len);
unsigned int auth_tag_len,
const char* cts_mode,
const char* xts_standard);
bool InitAuthenticated(const char* cipher_type,
int iv_len,
unsigned int auth_tag_len);
@@ -87,7 +91,7 @@ class CipherBase : public BaseObject {
unsigned int auth_tag_len_;
char auth_tag_[ncrypto::Cipher::MAX_AUTH_TAG_LENGTH];
bool pending_auth_failed_;
bool has_siv_update_;
bool has_one_shot_update_;
unsigned int siv_aad_components_;
int max_message_size_;
};
+7 -4
View File
@@ -2507,9 +2507,11 @@ int SecureContext::TicketKeyCallback(SSL* ssl,
ArrayBufferViewContents<unsigned char> aes_key(aes.As<ArrayBufferView>());
if (enc) {
EVP_EncryptInit_ex(ectx, Cipher::AES_128_CBC, nullptr, aes_key.data(), iv);
EVP_EncryptInit_ex(
ectx, Cipher::AES_128_CBC(), nullptr, aes_key.data(), iv);
} else {
EVP_DecryptInit_ex(ectx, Cipher::AES_128_CBC, nullptr, aes_key.data(), iv);
EVP_DecryptInit_ex(
ectx, Cipher::AES_128_CBC(), nullptr, aes_key.data(), iv);
}
return r;
@@ -2532,7 +2534,8 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl,
memcpy(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_));
if (!ncrypto::CSPRNG(iv, 16) ||
EVP_EncryptInit_ex(
ectx, Cipher::AES_128_CBC, nullptr, sc->ticket_key_aes_, iv) <= 0 ||
ectx, Cipher::AES_128_CBC(), nullptr, sc->ticket_key_aes_, iv) <=
0 ||
!InitTicketHmac(
hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_))) {
return -1;
@@ -2546,7 +2549,7 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl,
}
if (EVP_DecryptInit_ex(
ectx, Cipher::AES_128_CBC, nullptr, sc->ticket_key_aes_, iv) <= 0 ||
ectx, Cipher::AES_128_CBC(), nullptr, sc->ticket_key_aes_, iv) <= 0 ||
!InitTicketHmac(
hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_))) {
return -1;
+2
View File
@@ -883,6 +883,7 @@ Environment::Environment(IsolateData* isolate_data,
thread_name_(thread_name) {
#if HAVE_OPENSSL && NCRYPTO_USE_OPENSSL3_PROVIDER
provider_digest_cache = std::make_unique<ncrypto::DigestCache>();
provider_cipher_cache = std::make_unique<ncrypto::CipherCache>();
#endif
if (!is_main_thread()) {
@@ -1140,6 +1141,7 @@ Environment::~Environment() {
// Provider methods can contain callbacks into native addons. Release the
// environment-owned methods before unloading any addon DSOs.
provider_digest_cache.reset();
provider_cipher_cache.reset();
#endif
// Dereference all addons that were loaded into this environment.
for (binding::DLib& addon : loaded_addons_) {
+2
View File
@@ -69,6 +69,7 @@
#include <vector>
namespace ncrypto {
class CipherCache;
class DigestCache;
} // namespace ncrypto
@@ -1095,6 +1096,7 @@ class Environment final : public MemoryRetainer {
#if HAVE_OPENSSL
uint64_t hash_cache_generation = 0;
std::unique_ptr<ncrypto::DigestCache> provider_digest_cache;
std::unique_ptr<ncrypto::CipherCache> provider_cipher_cache;
std::vector<std::string> supported_hash_algorithms;
#endif // HAVE_OPENSSL
+73 -7
View File
@@ -10,7 +10,13 @@ if (!hasOpenSSL3) {
common.skip('this test requires OpenSSL 3.x');
}
const assert = require('node:assert');
const { createHash, getCiphers, getHashes } = require('node:crypto');
const {
createCipheriv,
createDecipheriv,
createHash,
getCiphers,
getHashes,
} = require('node:crypto');
const { debuglog } = require('node:util');
const { getProviders } = require(`./build/${common.buildType}/binding`);
@@ -20,7 +26,22 @@ const { getProviders } = require(`./build/${common.buildType}/binding`);
// supported by the provider.
const providers = {
'default': {
ciphers: ['des3-wrap'],
ciphers: [
{
name: 'aes-128-cbc-cts',
keyLength: 16,
ivLength: 16,
plaintextLength: 32,
unavailableCode: 'ERR_CRYPTO_UNKNOWN_CIPHER',
},
{
name: 'des3-wrap',
keyLength: 24,
ivLength: null,
plaintextLength: 16,
unavailableCode: 'ERR_OSSL_EVP_UNSUPPORTED',
},
],
hashes: [
'sha512-256',
...['keccak-kmac-128', 'keccak-kmac128']
@@ -28,7 +49,22 @@ const providers = {
],
},
'legacy': {
ciphers: ['blowfish', 'idea'],
ciphers: [
{
name: 'blowfish',
keyLength: 16,
ivLength: 8,
plaintextLength: 16,
unavailableCode: 'ERR_OSSL_EVP_UNSUPPORTED',
},
{
name: 'idea',
keyLength: 16,
ivLength: 8,
plaintextLength: 16,
unavailableCode: 'ERR_OSSL_EVP_UNSUPPORTED',
},
],
hashes: ['md4', 'whirlpool'],
},
};
@@ -60,12 +96,36 @@ function createSupportedHash(hash) {
}
}
function assertCipherRoundTrip({
name,
keyLength,
ivLength,
plaintextLength,
}) {
const key = Buffer.alloc(keyLength, 0x42);
const iv = ivLength === null ? null : Buffer.alloc(ivLength, 0x24);
const plaintext = Buffer.alloc(plaintextLength, 0x61);
const cipher = createCipheriv(name, key, iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
const decipher = createDecipheriv(name, key, iv);
const received = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
assert.deepStrictEqual(received, plaintext);
}
function testProviderPresent(provider) {
debug(`Checking '${provider}' is present`);
assertArrayIncludes(getProviders(), provider, 'Loaded providers');
for (const cipher of providers[provider].ciphers || []) {
debug(`Checking '${cipher}' cipher is available`);
assertArrayIncludes(getCiphers(), cipher, 'Available ciphers');
debug(`Checking '${cipher.name}' cipher is available`);
assertArrayIncludes(getCiphers(), cipher.name, 'Available ciphers');
assertCipherRoundTrip(cipher);
}
for (const hash of providers[provider].hashes || []) {
debug(`Checking '${hash}' hash is available`);
@@ -78,8 +138,14 @@ function testProviderAbsent(provider) {
debug(`Checking '${provider}' is absent`);
assertArrayDoesNotInclude(getProviders(), provider, 'Loaded providers');
for (const cipher of providers[provider].ciphers || []) {
debug(`Checking '${cipher}' cipher is unavailable`);
assertArrayDoesNotInclude(getCiphers(), cipher, 'Available ciphers');
const { name, keyLength, ivLength, unavailableCode } = cipher;
debug(`Checking '${name}' cipher is unavailable`);
assertArrayDoesNotInclude(getCiphers(), name, 'Available ciphers');
const key = Buffer.alloc(keyLength, 0x42);
const iv = ivLength === null ? null : Buffer.alloc(ivLength, 0x24);
assert.throws(() => createCipheriv(name, key, iv), {
code: unavailableCode,
});
}
for (const hash of providers[provider].hashes || []) {
debug(`Checking '${hash}' hash is unavailable`);
+32
View File
@@ -52,6 +52,38 @@ module.exports = [
ct: '0eaccb',
tag: '93da9bb81333aee0c785b240d319719d', tampered: false },
// RFC 8998, Appendix A.1
{ algo: 'sm4-gcm',
key: '0123456789abcdeffedcba9876543210',
iv: '00001234567800000000abcd',
plain: 'aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb' +
'ccccccccccccccccdddddddddddddddd' +
'eeeeeeeeeeeeeeeeffffffffffffffff' +
'eeeeeeeeeeeeeeeeaaaaaaaaaaaaaaaa',
plainIsHex: true,
aad: 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
ct: '17f399f08c67d5ee19d0dc9969c4bb7d' +
'5fd46fd3756489069157b282bb200735' +
'd82710ca5c22f0ccfa7cbf93d496ac15' +
'a56834cbcf98c397b4024a2691233b8d',
tag: '83de3541e4c2b58177e065a9bf7b62ec', tampered: false },
// RFC 8998, Appendix A.2
{ algo: 'sm4-ccm',
key: '0123456789abcdeffedcba9876543210',
iv: '00001234567800000000abcd',
plain: 'aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb' +
'ccccccccccccccccdddddddddddddddd' +
'eeeeeeeeeeeeeeeeffffffffffffffff' +
'eeeeeeeeeeeeeeeeaaaaaaaaaaaaaaaa',
plainIsHex: true,
aad: 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
ct: '48af93501fa62adbcd414cce6034d895' +
'dda1bf8f132f042098661572e7483094' +
'fd12e518ce062c98acee28d95df4416b' +
'ed31a2f04476c18bb40c84a74b97dc5b',
tag: '16842d4fa186f56ab33256971fa110f4', tampered: false },
{ algo: 'aes-128-gcm',
key: '6970787039613669314d623455536234',
iv: '583673497131313748307652', plain: 'Hello World!',
+55
View File
@@ -0,0 +1,55 @@
'use strict';
const assert = require('assert');
const {
createHash,
createCipheriv,
getCipherInfo,
getCiphers,
getHashes,
setFips,
} = require('crypto');
const { setDeserializeMainFunction } = require('v8').startupSnapshot;
const algorithm = 'aes-128-cbc-cts';
const key = Buffer.alloc(16);
const iv = Buffer.alloc(16);
const legacyCipher = 'blowfish';
const legacyHash = 'md4';
setFips(0);
assert(getCiphers().includes(algorithm));
assert(getCiphers().includes(legacyCipher));
assert(getHashes().includes(legacyHash));
assert(getCipherInfo(algorithm));
createCipheriv(algorithm, key, iv);
createHash(legacyHash).digest();
setDeserializeMainFunction(() => {
assert(getCiphers().includes(algorithm));
assert(!getCiphers().includes(legacyCipher));
assert(!getHashes().includes(legacyHash));
assert(getCipherInfo(algorithm));
createCipheriv(algorithm, key, iv);
assert.throws(
() => createCipheriv(legacyCipher, key, Buffer.alloc(8)),
{ code: 'ERR_OSSL_EVP_UNSUPPORTED' },
);
assert.throws(
() => createHash(legacyHash),
{ code: 'ERR_OSSL_EVP_UNSUPPORTED' },
);
setFips(1);
assert(!getCiphers().includes(algorithm));
assert.strictEqual(getCipherInfo(algorithm), undefined);
assert.throws(() => createCipheriv(algorithm, key, iv), {
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
});
setFips(0);
assert(getCiphers().includes(algorithm));
assert(getCipherInfo(algorithm));
createCipheriv(algorithm, key, iv);
console.log('provider crypto caches snapshot: ok');
});
+143
View File
@@ -63,3 +63,146 @@ const key3 = Buffer.from('29c9eab5ed5ad44134a1437fe2e673b4d88a5b7c72e68454fea087
const msg = decipher.update(cipher.update(text, 'utf8'), 'buffer', 'utf8');
assert.strictEqual(msg, text, `${algorithm} test case failed`);
});
const kwIV = Buffer.alloc(8, 0xa6);
const kwpIV = Buffer.from('a65959a6', 'hex');
// NIST SP 800-38F known-answer vectors.
[
{
algorithm: 'aes-128-wrap',
key: '000102030405060708090a0b0c0d0e0f',
plaintext: '00112233445566778899aabbccddeeff',
ciphertext: '1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5',
iv: kwIV,
},
{
algorithm: 'aes-192-wrap',
key: '000102030405060708090a0b0c0d0e0f1011121314151617',
plaintext: '00112233445566778899aabbccddeeff',
ciphertext: '96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d',
iv: kwIV,
},
{
algorithm: 'aes-256-wrap',
key: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
plaintext: '00112233445566778899aabbccddeeff',
ciphertext: '64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7',
iv: kwIV,
},
{
algorithm: 'aes-128-wrap-pad',
key: '6decf10a1caf8e3b80c7a4be8c9c84e8',
plaintext: '49',
ciphertext: '01a7d657fc4a5b216f261cca4d052c2b',
iv: kwpIV,
},
{
algorithm: 'aes-192-wrap-pad',
key: '9ca11078baebc1597a68ce2fe3fc79a201626575252b8860',
plaintext: '76',
ciphertext: '866bc0ae30e290bb20a0dab31a6e7165',
iv: kwpIV,
},
{
algorithm: 'aes-256-wrap-pad',
key: '95da2700ca6fd9a52554ee2a8df1386f5b94a1a60ed8a4aef60a8d61ab5f225a',
plaintext: 'd1',
ciphertext: '06ba7ae6f3248cfdcf267507fa001bc4',
iv: kwpIV,
},
{
algorithm: 'aes-128-wrap-inv',
key: 'e88ba734ea243480a6129366753b58eb',
plaintext: 'd140ac16a44c1c2b3f47037ea8898a3e',
ciphertext: '600861ee14320006f0ae55c46d5e1ebf3303751df7f038df',
iv: kwIV,
},
{
algorithm: 'aes-192-wrap-inv',
key: '370c715135b44eb3773b1aff833bcd28b59aee866d4a36b3',
plaintext: 'eae0f60f1cf33d5b75869e84c764a04e',
ciphertext: 'ea4ba4add8add19950ca491d109ffa08f90312693055677a',
iv: kwIV,
},
{
algorithm: 'aes-256-wrap-inv',
key: 'de982f7c871f78e37462e2f48a62eecb2da81a10799c6ebf2bee8c786b624b0e',
plaintext: 'ecafc437d9f1643c7645c2416c14c003',
ciphertext: 'aec02ddb3f6de1f99103c6042dfc9001eb3cf56d9c2a11f7',
iv: kwIV,
},
{
algorithm: 'aes-128-wrap-pad-inv',
key: '1c321a356b0ee25e30de2d618c1facbe',
plaintext: '42',
ciphertext: '3ddf22da3080a1a5252574c76f833790',
iv: kwpIV,
},
{
algorithm: 'aes-192-wrap-pad-inv',
key: 'fe3fe235bb36dcf03f01cbf32cc98a3abf10ab3d608d3b30',
plaintext: '1d2b7fc29991bafaf7',
ciphertext: 'c11afb3c0de263dfb9b672a5f81fe0b9acfe9c407691f332',
iv: kwpIV,
},
{
algorithm: 'aes-256-wrap-pad-inv',
key: '148a3fa618a6998c30b9f0f67922354a3747f2fa2e4d2e0b7af9582d6f548fee',
plaintext: '441125592acf9e5208dcd558a7ac0034d15530dbad7a2913963da0cbf60aa3',
ciphertext: '23f26a9476829885055694062c89b86399e8d6125509c9e88bb0a5b5113f4bfc8d34a62cba3c9eee',
iv: kwpIV,
},
].forEach(({ algorithm, key, plaintext, ciphertext, iv }) => {
if (!crypto.getCiphers().includes(algorithm)) {
common.printSkipMessage(`Skipping unsupported ${algorithm} test case`);
return;
}
const keyBuffer = Buffer.from(key, 'hex');
const plaintextBuffer = Buffer.from(plaintext, 'hex');
const expected = Buffer.from(ciphertext, 'hex');
const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv);
const actual = Buffer.concat([
cipher.update(plaintextBuffer),
cipher.final(),
]);
assert.deepStrictEqual(actual, expected, `${algorithm} wrap failed`);
const decipher = crypto.createDecipheriv(algorithm, keyBuffer, iv);
const unwrapped = Buffer.concat([
decipher.update(actual),
decipher.final(),
]);
assert.deepStrictEqual(
unwrapped, plaintextBuffer, `${algorithm} unwrap failed`);
});
{
const algorithm = crypto.getCiphers().includes('aes-128-wrap-inv') ?
'aes-128-wrap-inv' : 'aes128-wrap';
if (!crypto.getCiphers().includes(algorithm)) {
common.printSkipMessage(`Skipping unsupported ${algorithm} state tests`);
} else {
const key = Buffer.from('e88ba734ea243480a6129366753b58eb', 'hex');
const iv = Buffer.alloc(8, 0xa6);
const plaintextParts = [Buffer.alloc(16), Buffer.alloc(16, 1)];
const wrappedParts = plaintextParts.map((plaintext) => {
const cipher = crypto.createCipheriv(algorithm, key, iv);
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
});
for (const [create, inputParts] of [
[crypto.createCipheriv, plaintextParts],
[crypto.createDecipheriv, wrappedParts],
]) {
const withoutUpdate = create(algorithm, key, iv);
assert.throws(() => withoutUpdate.final(), /Unsupported state/);
const multipleUpdates = create(algorithm, key, iv);
multipleUpdates.update(inputParts[0]);
assert.throws(() => multipleUpdates.update(inputParts[1]),
/Trying to add data in unsupported state/);
}
}
}
+43 -1
View File
@@ -63,7 +63,7 @@ for (const test of TEST_CASES) {
continue;
}
const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo);
const isCCM = /^(?:aes-(?:128|192|256)|sm4)-ccm$/.test(test.algo);
const isOCB = /^aes-(128|192|256)-ocb$/.test(test.algo);
const isSIV = /^aes-(128|192|256)-siv$/.test(test.algo);
@@ -963,6 +963,7 @@ if (!fips3 && !process.features.openssl_is_boringssl) {
if (ciphers.includes('aes-128-ccm')) {
const key = crypto.randomBytes(16);
const nonce = crypto.randomBytes(13);
const authError = /Unsupported state or unable to authenticate data/;
const cipher = crypto.createCipheriv('aes-128-ccm', key, nonce, {
authTagLength: 16,
@@ -986,6 +987,47 @@ if (ciphers.includes('aes-128-ccm')) {
decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 });
decipher.update(new DataView(new ArrayBuffer(0)));
decipher.final();
const invalidTag = Buffer.from(tag);
invalidTag[0] ^= 0xff;
{
const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, {
authTagLength: 16,
});
decipher.setAuthTag(tag);
decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 });
assert.throws(() => decipher.final(), authError);
}
{
const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, {
authTagLength: 16,
});
decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 });
assert.throws(() => decipher.final(), authError);
}
{
const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, {
authTagLength: 16,
});
decipher.setAuthTag(invalidTag);
decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 });
decipher.update(Buffer.alloc(0));
assert.throws(() => decipher.final(), authError);
}
{
const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, {
authTagLength: 16,
});
decipher.setAuthTag(tag);
decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 });
decipher.update(Buffer.alloc(0));
assert.throws(() => decipher.update(Buffer.alloc(0)), errMessages.state);
decipher.final();
}
}
} else {
common.printSkipMessage('Skipping unsupported aes-128-ccm test');
@@ -0,0 +1,130 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
createCipheriv,
createDecipheriv,
} = require('crypto');
const key = Buffer.alloc(16);
const gcmIv = Buffer.alloc(12);
const cbcIv = Buffer.alloc(16);
for (const create of [createCipheriv, createDecipheriv]) {
// None of these supply either of the extended cipher options.
create('aes-128-gcm', key, gcmIv);
create('aes-128-gcm', key, gcmIv, null);
create('aes-128-gcm', key, gcmIv, {});
create('aes-128-gcm', key, gcmIv, undefined);
create('aes-128-gcm', key, gcmIv, { authTagLength: 16 });
for (const options of [
{ ctsMode: null },
{ ctsMode: undefined },
{ xtsStandard: null },
{ xtsStandard: undefined },
{ ctsMode: null, xtsStandard: undefined },
]) {
create('aes-128-gcm', key, gcmIv, options);
}
const accesses = [];
create('aes-128-gcm', key, gcmIv, {
get authTagLength() {
accesses.push('authTagLength');
return 16;
},
get ctsMode() {
accesses.push('ctsMode');
return null;
},
get xtsStandard() {
accesses.push('xtsStandard');
return undefined;
},
});
assert.deepStrictEqual(
accesses,
['authTagLength', 'ctsMode', 'xtsStandard']);
const extendedAccesses = [];
assert.throws(
() => create('aes-128-cbc', key, cbcIv, {
get authTagLength() {
extendedAccesses.push('authTagLength');
return null;
},
get ctsMode() {
extendedAccesses.push('ctsMode');
return 'CS1';
},
get xtsStandard() {
extendedAccesses.push('xtsStandard');
return null;
},
}),
{ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' });
assert.deepStrictEqual(
extendedAccesses,
['authTagLength', 'ctsMode', 'xtsStandard']);
const invalidAuthTagAccesses = [];
assert.throws(() => create('aes-128-cbc', key, cbcIv, {
get authTagLength() {
invalidAuthTagAccesses.push('authTagLength');
return -2;
},
get ctsMode() {
invalidAuthTagAccesses.push('ctsMode');
return undefined;
},
}), { code: 'ERR_INVALID_ARG_VALUE' });
assert.deepStrictEqual(invalidAuthTagAccesses, ['authTagLength']);
const invalidCtsTypeAccesses = [];
assert.throws(() => create('aes-128-cbc', key, cbcIv, {
get ctsMode() {
invalidCtsTypeAccesses.push('ctsMode');
return 1;
},
get xtsStandard() {
invalidCtsTypeAccesses.push('xtsStandard');
return undefined;
},
}), { code: 'ERR_INVALID_ARG_TYPE' });
assert.deepStrictEqual(invalidCtsTypeAccesses, ['ctsMode']);
const invalidCtsValueAccesses = [];
assert.throws(() => create('aes-128-cbc', key, cbcIv, {
get ctsMode() {
invalidCtsValueAccesses.push('ctsMode');
return 'CS4';
},
get xtsStandard() {
invalidCtsValueAccesses.push('xtsStandard');
return undefined;
},
}), { code: 'ERR_INVALID_ARG_VALUE' });
assert.deepStrictEqual(
invalidCtsValueAccesses,
['ctsMode', 'xtsStandard']);
assert.throws(
() => create('aes-128-cbc', key, cbcIv, { xtsStandard: 'GB' }),
{ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' });
for (const ctsMode of ['', 'CS4']) {
assert.throws(
() => create('aes-128-cbc', key, cbcIv, { ctsMode }),
{ code: 'ERR_INVALID_ARG_VALUE' });
}
for (const xtsStandard of ['', 'IEEE-1619']) {
assert.throws(
() => create('aes-128-cbc', key, cbcIv, { xtsStandard }),
{ code: 'ERR_INVALID_ARG_VALUE' });
}
}
@@ -0,0 +1,123 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
createCipheriv,
createDecipheriv,
getCiphers,
} = require('crypto');
const algorithm = 'aes-128-cbc-cts';
const key = Buffer.from('636869636b656e207465726979616b69', 'hex');
const iv = Buffer.alloc(16);
for (const create of [createCipheriv, createDecipheriv]) {
for (const ctsMode of ['cs1', 'CS4', '']) {
assert.throws(
() => create('aes-128-cbc', key, iv, { ctsMode }),
{ code: 'ERR_INVALID_ARG_VALUE' });
}
for (const ctsMode of [1, true, {}]) {
assert.throws(
() => create('aes-128-cbc', key, iv, { ctsMode }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}
assert.throws(
() => create('aes-128-cbc', key, iv, { ctsMode: 'CS1' }),
{ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' });
}
if (!getCiphers().includes(algorithm)) {
common.printSkipMessage(`unsupported ${algorithm}`);
return;
}
// OpenSSL AES-128-CBC-CTS CS1 test vector.
const plaintext = Buffer.from('4920776f756c64206c696b652074686520',
'hex');
const expected = Buffer.from('97c6353568f2bf8cb4d8a580362da7ff7f',
'hex');
const cipher = createCipheriv(algorithm, key, iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
assert.deepStrictEqual(ciphertext, expected);
const decipher = createDecipheriv(algorithm, key, iv);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
assert.deepStrictEqual(decrypted, plaintext);
const vectors = [
{
plaintext,
ciphertext: {
CS1: expected,
CS2: Buffer.from('c6353568f2bf8cb4d8a580362da7ff7f97', 'hex'),
CS3: Buffer.from('c6353568f2bf8cb4d8a580362da7ff7f97', 'hex'),
},
},
{
plaintext: Buffer.from(
'4920776f756c64206c696b6520746865' +
'2047656e6572616c2047617527732043', 'hex'),
ciphertext: {
CS1: Buffer.from(
'97687268d6ecccc0c07b25e25ecfe584' +
'39312523a78662d5be7fcbcc98ebf5a8', 'hex'),
CS2: Buffer.from(
'97687268d6ecccc0c07b25e25ecfe584' +
'39312523a78662d5be7fcbcc98ebf5a8', 'hex'),
CS3: Buffer.from(
'39312523a78662d5be7fcbcc98ebf5a8' +
'97687268d6ecccc0c07b25e25ecfe584', 'hex'),
},
},
];
for (const { plaintext, ciphertext } of vectors) {
for (const ctsMode of ['CS1', 'CS2', 'CS3']) {
const cipher = createCipheriv(algorithm, key, iv, { ctsMode });
const encrypted = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
assert.deepStrictEqual(encrypted, ciphertext[ctsMode]);
const decipher = createDecipheriv(algorithm, key, iv, { ctsMode });
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
assert.deepStrictEqual(decrypted, plaintext);
}
}
const tooShort = createCipheriv(algorithm, key, iv);
assert.throws(() => tooShort.update(Buffer.alloc(15)),
/Trying to add data in unsupported state/);
assert.deepStrictEqual(Buffer.concat([
tooShort.update(plaintext),
tooShort.final(),
]), expected);
for (const [create, input] of [
[createCipheriv, plaintext],
[createDecipheriv, expected],
]) {
const withoutUpdate = create(algorithm, key, iv);
assert.throws(() => withoutUpdate.final(), /Unsupported state/);
const multipleUpdates = create(algorithm, key, iv);
multipleUpdates.update(input.subarray(0, 16));
assert.throws(() => multipleUpdates.update(input.subarray(16)),
/Trying to add data in unsupported state/);
}
@@ -85,6 +85,84 @@ function testCipher3(key, iv) {
`encryption/decryption with key ${key} and iv ${iv}`);
}
function testSm4Xts() {
const aesKey = Buffer.alloc(16);
const aesIv = Buffer.alloc(16);
for (const create of [crypto.createCipheriv, crypto.createDecipheriv]) {
for (const xtsStandard of ['gb', 'IEEE-1619', '']) {
assert.throws(
() => create('aes-128-cbc', aesKey, aesIv, { xtsStandard }),
{ code: 'ERR_INVALID_ARG_VALUE' });
}
for (const xtsStandard of [1, true, {}]) {
assert.throws(
() => create('aes-128-cbc', aesKey, aesIv, { xtsStandard }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}
assert.throws(
() => create('aes-128-cbc', aesKey, aesIv, { xtsStandard: 'GB' }),
{ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' });
}
if (!crypto.getCiphers().includes('sm4-xts')) {
common.printSkipMessage('unsupported sm4-xts test');
return;
}
// GB/T 17964-2021
const key = Buffer.from(
'2B7E151628AED2A6ABF7158809CF4F3C' +
'000102030405060708090A0B0C0D0E0F', 'hex');
const iv = Buffer.from('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF', 'hex');
const plaintext = Buffer.from(
'6BC1BEE22E409F96E93D7E117393172A' +
'AE2D8A571E03AC9C9EB76FAC45AF8E51' +
'30C81C46A35CE411E5FBC1191A0A52EF' +
'F69F2445DF4F9B17', 'hex');
const vectors = [
{
options: undefined,
ciphertext: Buffer.from(
'E9538251C71D7B80BBE4483FEF497BD1' +
'2C5C581BD6242FC51E08964FB4F60FDB' +
'0BA42F63499279213D318D2C11F6886E' +
'903BE7F93A1B3479', 'hex'),
},
{
options: { xtsStandard: 'GB' },
ciphertext: Buffer.from(
'E9538251C71D7B80BBE4483FEF497BD1' +
'2C5C581BD6242FC51E08964FB4F60FDB' +
'0BA42F63499279213D318D2C11F6886E' +
'903BE7F93A1B3479', 'hex'),
},
{
options: { xtsStandard: 'IEEE' },
ciphertext: Buffer.from(
'E9538251C71D7B80BBE4483FEF497BD1' +
'B3DB1A3E60408C575D63FF7DB39F8326' +
'0869F9E2585FEC9F0B863BF8FD784B86' +
'27D16C0DB6D2CFC7', 'hex'),
},
];
for (const { options, ciphertext } of vectors) {
const cipher = crypto.createCipheriv('sm4-xts', key, iv, options);
const encrypted = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
assert.deepStrictEqual(encrypted, ciphertext);
const decipher = crypto.createDecipheriv('sm4-xts', key, iv, options);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
assert.deepStrictEqual(decrypted, plaintext);
}
}
{
const Cipheriv = crypto.Cipheriv;
const algorithm = fips3 ? 'aes-128-cbc' : 'des-ede3-cbc';
@@ -167,6 +245,7 @@ if (!isFipsEnabled) {
testCipher3(Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'),
Buffer.from('A6A6A6A6A6A6A6A6', 'hex'));
}
testSm4Xts();
// Zero-sized IV or null should be accepted in ECB mode.
crypto.createCipheriv('aes-128-ecb', Buffer.alloc(16), Buffer.alloc(0));
+71
View File
@@ -0,0 +1,71 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
createCipheriv,
createDecipheriv,
getCiphers,
} = require('crypto');
const iv = Buffer.from('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', 'hex');
const plaintext = Buffer.from(
'000102030405060708090a0b0c0d0e0f10', 'hex');
const cases = [
{
algorithm: 'aes-128-xts',
key: Buffer.from(
'000102030405060708090a0b0c0d0e0f' +
'101112131415161718191a1b1c1d1e1f', 'hex'),
},
{
algorithm: 'sm4-xts',
key: Buffer.from(
'2b7e151628aed2a6abf7158809cf4f3c' +
'000102030405060708090a0b0c0d0e0f', 'hex'),
},
];
for (const { algorithm, key } of cases) {
if (!getCiphers().includes(algorithm)) {
common.printSkipMessage(`unsupported ${algorithm} test`);
continue;
}
const cipher = createCipheriv(algorithm, key, iv);
const ciphertext = cipher.update(plaintext);
assert.strictEqual(ciphertext.length, plaintext.length);
assert.deepStrictEqual(cipher.final(), Buffer.alloc(0));
const decipher = createDecipheriv(algorithm, key, iv);
assert.deepStrictEqual(decipher.update(ciphertext), plaintext);
assert.deepStrictEqual(decipher.final(), Buffer.alloc(0));
for (const [create, input, expected] of [
[createCipheriv, plaintext, ciphertext],
[createDecipheriv, ciphertext, plaintext],
]) {
const withoutUpdate = create(algorithm, key, iv);
assert.throws(() => withoutUpdate.final(), /Unsupported state/);
const failedUpdate = create(algorithm, key, iv);
assert.throws(() => failedUpdate.update(Buffer.alloc(15)),
/Trying to add data in unsupported state/);
assert.throws(() => failedUpdate.final(), /Unsupported state/);
const retry = create(algorithm, key, iv);
assert.throws(() => retry.update(Buffer.alloc(15)),
/Trying to add data in unsupported state/);
assert.deepStrictEqual(retry.update(input), expected);
assert.deepStrictEqual(retry.final(), Buffer.alloc(0));
const oneUpdate = create(algorithm, key, iv);
assert.deepStrictEqual(oneUpdate.update(input), expected);
assert.throws(() => oneUpdate.update(Buffer.alloc(16)),
/Trying to add data in unsupported state/);
assert.deepStrictEqual(oneUpdate.final(), Buffer.alloc(0));
}
}
+68 -1
View File
@@ -5,11 +5,12 @@ if (!common.hasCrypto)
common.skip('missing crypto');
const {
createCipheriv,
createHash,
getCiphers,
getCipherInfo,
} = require('crypto');
const { hasFIPS } = require('../common/crypto');
const { hasFIPS, hasOpenSSL3 } = require('../common/crypto');
const assert = require('assert');
@@ -17,6 +18,42 @@ const ciphers = getCiphers();
assert.strictEqual(getCipherInfo(-1), undefined);
assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined);
if (hasOpenSSL3) {
assert.deepStrictEqual(
ciphers.filter((cipher) => cipher.includes('cbc-hmac')), []);
for (const cipher of [
'null',
'aes-128-cbc-hmac-sha1',
'aes-256-cbc-hmac-sha1',
'aes-128-cbc-hmac-sha256',
'aes-256-cbc-hmac-sha256',
'aes-128-cbc-hmac-sha1-etm',
'aes-192-cbc-hmac-sha1-etm',
'aes-256-cbc-hmac-sha1-etm',
'aes-128-cbc-hmac-sha256-etm',
'aes-192-cbc-hmac-sha256-etm',
'aes-256-cbc-hmac-sha256-etm',
'aes-128-cbc-hmac-sha512-etm',
'aes-192-cbc-hmac-sha512-etm',
'aes-256-cbc-hmac-sha512-etm',
]) {
assert(!ciphers.includes(cipher));
assert.strictEqual(getCipherInfo(cipher), undefined);
assert.throws(
() => createCipheriv(cipher, Buffer.alloc(16), Buffer.alloc(16)), {
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
});
}
}
if (ciphers.includes('aes-128-wrap-inv')) {
const alias = 'aes128-wrap-inv';
assert(ciphers.includes(alias));
assert.deepStrictEqual(getCipherInfo(alias),
getCipherInfo('aes-128-wrap-inv'));
}
assert(!ciphers.some((cipher) => /^\d+(?:\.\d+)+$/.test(cipher)));
if (!process.features.openssl_is_boringssl) {
// A failed provider fetch must not contaminate the OpenSSL error queue.
assert.throws(() => createHash('sha256', { outputLength: 28 }), {
@@ -96,6 +133,19 @@ if (hasFIPS(3)) {
common.printSkipMessage('Skipping unsupported aes-128-ocb test cases');
}
if (ciphers.includes('aes-128-cbc-cts')) {
const info = getCipherInfo('aes-128-cbc-cts');
assert.strictEqual(info.name, 'aes-128-cbc-cts');
assert.strictEqual(info.mode, 'cbc');
assert.strictEqual(info.keyLength, 16);
assert.strictEqual(info.blockSize, 16);
assert.strictEqual(info.ivLength, 16);
assert(getCipherInfo('aes-128-cbc-cts', { ivLength: 16 }));
assert(!getCipherInfo('aes-128-cbc-cts', { ivLength: 15 }));
} else {
common.printSkipMessage('Skipping unsupported aes-128-cbc-cts test cases');
}
if (ciphers.includes('aes-128-siv')) {
const info = getCipherInfo('aes-128-siv');
assert.strictEqual(info.name, 'aes-128-siv');
@@ -119,3 +169,20 @@ if (ciphers.includes('aes-128-gcm-siv')) {
} else {
common.printSkipMessage('Skipping unsupported aes-128-gcm-siv test cases');
}
for (const [name, mode, keyLength, ivLength] of [
['sm4-gcm', 'gcm', 16, 12],
['sm4-ccm', 'ccm', 16, 12],
['sm4-xts', 'xts', 32, 16],
]) {
if (ciphers.includes(name)) {
const info = getCipherInfo(name);
assert.strictEqual(info.name, name);
assert.strictEqual(info.mode, mode);
assert.strictEqual(info.nid, undefined);
assert.strictEqual(info.keyLength, keyLength);
assert.strictEqual(info.ivLength, ivLength);
} else {
common.printSkipMessage(`Skipping unsupported ${name} test cases`);
}
}
@@ -0,0 +1,28 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { hasOpenSSL3 } = require('../common/crypto');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
if (!hasOpenSSL3)
common.skip('this test requires OpenSSL 3.x');
const entry = fixtures.path('snapshot', 'crypto-provider-cipher-cache.js');
const buildEnv = {
OPENSSL_CONF: fixtures.path(
'openssl3-conf', 'legacy_provider_enabled.cnf'),
};
const runEnv = {
OPENSSL_CONF: fixtures.path('openssl3-conf', 'default_only.cnf'),
};
tmpdir.refresh();
buildSnapshot(entry, buildEnv);
const { stdout } = runWithSnapshot(undefined, runEnv);
assert.match(stdout, /provider crypto caches snapshot: ok/);
@@ -0,0 +1,183 @@
// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL3 } = require('../common/crypto');
if (!hasOpenSSL3)
common.skip('this test requires OpenSSL 3.x');
const assert = require('assert');
const {
createCipheriv,
getCipherInfo,
getCiphers,
getFips,
getHashes,
setFips,
} = require('crypto');
const { internalBinding } = require('internal/test/binding');
const { isMainThread, Worker } = require('worker_threads');
if (!isMainThread)
common.skip('crypto.setFips() is not supported in workers');
const algorithm = 'camellia-128-cbc-cts';
const hashAlgorithm = 'md5';
const originalFips = getFips();
setFips(0);
if (!getCiphers().includes(algorithm)) {
common.skip(`${algorithm} is not supported`);
}
assert(getHashes().includes(hashAlgorithm));
const binding = internalBinding('crypto');
const generation = binding.getFipsCryptoGeneration();
setFips(0);
assert.strictEqual(binding.getFipsCryptoGeneration(), generation);
const ciphers = getCiphers();
ciphers.length = 0;
assert(getCiphers().includes(algorithm));
const info = getCipherInfo(algorithm);
assert(info);
assert.deepStrictEqual(getCipherInfo(algorithm.toUpperCase()), info);
assert.deepStrictEqual(getCipherInfo(algorithm), info);
assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined);
assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined);
const key = Buffer.alloc(16);
const iv = Buffer.alloc(16);
const plaintext = Buffer.alloc(32);
const liveCipher = createCipheriv(algorithm, key, iv);
const worker = new Worker(`
'use strict';
const {
createHash,
createCipheriv,
getCipherInfo,
getCiphers,
getHashes,
} = require('crypto');
const { internalBinding } = require('internal/test/binding');
const { parentPort, workerData } = require('worker_threads');
const binding = internalBinding('crypto');
const key = Buffer.from(workerData.key);
const iv = Buffer.from(workerData.iv);
const plaintext = Buffer.from(workerData.plaintext);
const liveCipher = createCipheriv(workerData.algorithm, key, iv);
getHashes();
getCiphers();
getCipherInfo(workerData.algorithm);
parentPort.postMessage({
phase: 'warm',
generation: binding.getFipsCryptoGeneration(),
});
parentPort.on('message', (phase) => {
if (phase === 'fips-on') {
let errorCode;
try {
createCipheriv(workerData.algorithm, key, iv);
} catch (error) {
errorCode = error.code;
}
const output = Buffer.concat([
liveCipher.update(plaintext),
liveCipher.final(),
]);
parentPort.postMessage({
phase,
errorCode,
generation: binding.getFipsCryptoGeneration(),
hasCipher: getCiphers().includes(workerData.algorithm),
hasHash: getHashes().includes(workerData.hashAlgorithm),
hasInfo: getCipherInfo(workerData.algorithm) !== undefined,
outputLength: output.length,
});
} else if (phase === 'fips-off') {
const cipher = createCipheriv(workerData.algorithm, key, iv);
const hash = createHash(workerData.hashAlgorithm).digest('hex');
const output = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
parentPort.postMessage({
phase,
generation: binding.getFipsCryptoGeneration(),
hasHash: getHashes().includes(workerData.hashAlgorithm),
hasCipher: getCiphers().includes(workerData.algorithm),
hasInfo: getCipherInfo(workerData.algorithm) !== undefined,
hash,
outputLength: output.length,
});
} else {
parentPort.close();
}
});
`, {
eval: true,
workerData: { algorithm, hashAlgorithm, key, iv, plaintext },
});
let enabledGeneration;
worker.on('message', common.mustCall((message) => {
if (message.phase === 'warm') {
assert.strictEqual(message.generation, generation);
setFips(1);
enabledGeneration = binding.getFipsCryptoGeneration();
assert.strictEqual(enabledGeneration, generation + 1n);
assert(!getCiphers().includes(algorithm));
assert(!getHashes().includes(hashAlgorithm));
assert.strictEqual(getCipherInfo(algorithm), undefined);
assert.throws(() => createCipheriv(algorithm, key, iv), {
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
});
const output = Buffer.concat([
liveCipher.update(plaintext),
liveCipher.final(),
]);
assert.strictEqual(output.length, plaintext.length);
worker.postMessage('fips-on');
} else if (message.phase === 'fips-on') {
assert.strictEqual(message.generation, enabledGeneration);
assert.strictEqual(message.hasCipher, false);
assert.strictEqual(message.hasHash, false);
assert.strictEqual(message.hasInfo, false);
assert.strictEqual(message.errorCode, 'ERR_CRYPTO_UNKNOWN_CIPHER');
assert.strictEqual(message.outputLength, plaintext.length);
setFips(0);
assert.strictEqual(
binding.getFipsCryptoGeneration(), enabledGeneration + 1n);
assert(getHashes().includes(hashAlgorithm));
assert(getCiphers().includes(algorithm));
assert(getCipherInfo(algorithm));
worker.postMessage('fips-off');
} else {
assert.strictEqual(message.phase, 'fips-off');
assert.strictEqual(
message.generation, binding.getFipsCryptoGeneration());
assert.strictEqual(message.hasCipher, true);
assert.strictEqual(message.hasHash, true);
assert.strictEqual(message.hasInfo, true);
assert.strictEqual(
message.hash,
'd41d8cd98f00b204e9800998ecf8427e',
);
assert.strictEqual(message.outputLength, plaintext.length);
worker.postMessage('done');
setFips(originalFips);
}
}, 3));
worker.on('error', common.mustNotCall());
worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0)));
+2
View File
@@ -810,6 +810,8 @@ export interface CryptoBinding {
credential: InternalCryptoBinding.PreparedSecretKeyData,
iv: InternalCryptoBinding.ByteSource | null,
authTagLength?: number,
ctsMode?: 'CS1' | 'CS2' | 'CS3',
xtsStandard?: 'GB' | 'IEEE',
) => InternalCryptoBinding.CipherBaseHandle;
DiffieHellman: new (
sizeOrKey: number | InternalCryptoBinding.ByteSource,