mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
025e88f981
commit
e7a69c4992
26 files changed
+1801
-224
No files matched your search
@@ -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');
|
||||
}
|
||||
Vendored
+309
-79
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+55
-26
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Vendored
+32
@@ -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!',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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/);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
Vendored
+2
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user