diff --git a/benchmark/crypto/create-cipheriv.js b/benchmark/crypto/create-cipheriv.js new file mode 100644 index 000000000000..7774e393b403 --- /dev/null +++ b/benchmark/crypto/create-cipheriv.js @@ -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'); +} diff --git a/benchmark/crypto/ecdh-compute-secret.js b/benchmark/crypto/ecdh-compute-secret.js new file mode 100644 index 000000000000..3061c5b2cc36 --- /dev/null +++ b/benchmark/crypto/ecdh-compute-secret.js @@ -0,0 +1,117 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('node:assert'); +const crypto = require('node:crypto'); + +const kCurve = 'prime256v1'; +const kPeerPoolSize = 32; +const scenarios = [ + 'first-after-generate', + 'full-lifecycle', + 'reused-local-same-peer', + 'reused-local-peer-pool', +]; + +const bench = common.createBenchmark(main, { + scenario: scenarios, + n: [5_000], +}, { + test: { scenario: 'first-after-generate', n: 1 }, +}); + +function generateContext() { + const context = crypto.createECDH(kCurve); + context.generateKeys(); + return context; +} + +function verifySecret(secret, local, peer) { + assert.deepStrictEqual(secret, peer.computeSecret(local.getPublicKey())); +} + +function firstAfterGenerate(n) { + const peer = generateContext(); + const peerPublicKey = peer.getPublicKey(); + const warmup = generateContext(); + warmup.computeSecret(peerPublicKey); + + const locals = Array.from({ length: n }, generateContext); + const secrets = new Array(n); + + bench.start(); + for (let i = 0; i < n; i++) + secrets[i] = locals[i].computeSecret(peerPublicKey); + bench.end(n); + + verifySecret(secrets[n - 1], locals[n - 1], peer); +} + +function fullLifecycle(n) { + const peer = generateContext(); + const peerPublicKey = peer.getPublicKey(); + const warmup = generateContext(); + warmup.computeSecret(peerPublicKey); + + const locals = new Array(n); + const secrets = new Array(n); + + bench.start(); + for (let i = 0; i < n; i++) { + const local = locals[i] = generateContext(); + secrets[i] = local.computeSecret(peerPublicKey); + } + bench.end(n); + + verifySecret(secrets[n - 1], locals[n - 1], peer); +} + +function reusedLocalSamePeer(n) { + const local = generateContext(); + const peer = generateContext(); + const peerPublicKey = peer.getPublicKey(); + local.computeSecret(peerPublicKey); + + const secrets = new Array(n); + + bench.start(); + for (let i = 0; i < n; i++) + secrets[i] = local.computeSecret(peerPublicKey); + bench.end(n); + + verifySecret(secrets[n - 1], local, peer); +} + +function reusedLocalPeerPool(n) { + const local = generateContext(); + const peers = Array.from( + { length: Math.min(n, kPeerPoolSize) }, + generateContext); + const peerPublicKeys = peers.map((peer) => peer.getPublicKey()); + local.computeSecret(peerPublicKeys[0]); + + const secrets = new Array(n); + + bench.start(); + for (let i = 0; i < n; i++) + secrets[i] = local.computeSecret(peerPublicKeys[i % peers.length]); + bench.end(n); + + const lastPeer = peers[(n - 1) % peers.length]; + verifySecret(secrets[n - 1], local, lastPeer); +} + +function main({ scenario, n }) { + switch (scenario) { + case 'first-after-generate': + return firstAfterGenerate(n); + case 'full-lifecycle': + return fullLifecycle(n); + case 'reused-local-same-peer': + return reusedLocalSamePeer(n); + case 'reused-local-peer-pool': + return reusedLocalPeerPool(n); + default: + throw new Error(`Unsupported scenario: ${scenario}`); + } +} diff --git a/benchmark/crypto/mac.js b/benchmark/crypto/mac.js new file mode 100644 index 000000000000..d1028fa414e6 --- /dev/null +++ b/benchmark/crypto/mac.js @@ -0,0 +1,254 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL } = require('../../test/common/crypto.js'); +const assert = require('node:assert'); +const { + createHmac, + createMac, + getMacs, +} = require('node:crypto'); + +if (!hasOpenSSL(3) || + process.features.openssl_is_boringssl || + typeof createMac !== 'function' || + typeof getMacs !== 'function') { + console.log('Skipping: generic MAC API requires OpenSSL >= 3'); + process.exit(0); +} + +const operations = [ + 'get-macs-cold', + 'get-macs-warm', + 'create-cold', + 'create-warm', + 'hmac-lifecycle', + 'mac-lifecycle', + 'mac-stream-lifecycle', + 'update', + 'stream', + 'final-buffer', + 'final-hex', +]; +const configurations = { + 'hmac-sha256': { + algorithm: 'HMAC', + key: Buffer.alloc(32, 0x42), + options: { digest: 'SHA256' }, + }, + 'kmac-128': { + algorithm: 'KMAC-128', + key: Buffer.alloc(32, 0x42), + options: { outputLength: 32 }, + }, +}; + +const bench = common.createBenchmark(main, { + operation: operations, + algorithm: Object.keys(configurations), + length: [0, 64, 4096], + n: [1, 10_000, 20_000, 500_000], +}, { + combinationFilter({ operation, algorithm, length, n }) { + if (operation === 'get-macs-cold') { + return algorithm === 'hmac-sha256' && length === 0 && n === 1; + } + if (operation === 'get-macs-warm') { + return algorithm === 'hmac-sha256' && length === 0 && n === 500_000; + } + if (operation === 'create-cold') + return length === 0 && n === 1; + if (operation === 'create-warm') + return length === 0 && n === 20_000; + if (operation === 'hmac-lifecycle') { + return algorithm === 'hmac-sha256' && n === 10_000; + } + if (operation === 'mac-lifecycle' || + operation === 'mac-stream-lifecycle') { + return n === 10_000; + } + if (operation === 'update' || operation === 'stream') { + return length === 64 && n === 500_000; + } + if (operation === 'final-buffer' || operation === 'final-hex') { + return algorithm === 'hmac-sha256' && + length === 64 && + n === 20_000; + } + return false; + }, + test: { + operation: ['create-cold'], + algorithm: ['hmac-sha256'], + length: [0], + n: [1], + }, +}); + +function main({ operation, algorithm, length, n }) { + const configuration = configurations[algorithm]; + const data = Buffer.alloc(length, 0x61); + + switch (operation) { + case 'get-macs-cold': + measureGetMacs(n, false); + break; + case 'get-macs-warm': + measureGetMacs(n, true); + break; + case 'create-cold': + measureCreate(configuration, n, false); + break; + case 'create-warm': + measureCreate(configuration, n, true); + break; + case 'hmac-lifecycle': + measureHmacLifecycle(configuration, data, n); + break; + case 'mac-lifecycle': + measureMacLifecycle(configuration, data, n); + break; + case 'mac-stream-lifecycle': + measureMacStreamLifecycle(configuration, data, n); + break; + case 'update': + measureUpdate(configuration, data, n); + break; + case 'stream': + measureStream(configuration, data, n); + break; + case 'final-buffer': + measureFinal(configuration, data, n); + break; + case 'final-hex': + measureFinal(configuration, data, n, 'hex'); + break; + default: + throw new Error(`unknown operation: ${operation}`); + } +} + +function measureGetMacs(n, warm) { + if (warm) + getMacs(); + + let result; + bench.start(); + for (let i = 0; i < n; ++i) + result = getMacs(); + bench.end(n); + + assert(Array.isArray(result)); +} + +function measureCreate({ algorithm, key, options }, n, warm) { + if (warm) + createMac(algorithm, key, options).final(); + + const contexts = new Array(n); + bench.start(); + for (let i = 0; i < n; ++i) + contexts[i] = createMac(algorithm, key, options); + bench.end(n); + + assert.strictEqual(typeof contexts[n - 1], 'object'); +} + +function measureHmacLifecycle({ key, options }, data, n) { + createHmac(options.digest, key).update(data).digest(); + + let result; + bench.start(); + for (let i = 0; i < n; ++i) + result = createHmac(options.digest, key).update(data).digest(); + bench.end(n); + + assert(Buffer.isBuffer(result)); +} + +function measureMacLifecycle({ algorithm, key, options }, data, n) { + createMac(algorithm, key, options).update(data).final(); + + let result; + bench.start(); + for (let i = 0; i < n; ++i) + result = createMac(algorithm, key, options).update(data).final(); + bench.end(n); + + assert(Buffer.isBuffer(result)); +} + +function measureMacStreamLifecycle({ algorithm, key, options }, data, n) { + const warmup = createMac(algorithm, key, options); + warmup.end(data); + warmup.read(); + + let result; + bench.start(); + for (let i = 0; i < n; ++i) { + const context = createMac(algorithm, key, options); + context.end(data); + result = context.read(); + } + bench.end(n); + + assert(Buffer.isBuffer(result)); +} + +function measureUpdate({ algorithm, key, options }, data, n) { + const warmup = createMac(algorithm, key, options); + warmup.update(data).final(); + + const context = createMac(algorithm, key, options); + bench.start(); + for (let i = 0; i < n; ++i) + context.update(data); + bench.end(n); + + assert(Buffer.isBuffer(context.final())); +} + +function measureStream({ algorithm, key, options }, data, n) { + const warmup = createMac(algorithm, key, options); + warmup.end(data); + warmup.read(); + + const context = createMac(algorithm, key, options); + bench.start(); + for (let i = 0; i < n; ++i) + context.write(data); + bench.end(n); + + context.end(); + assert(Buffer.isBuffer(context.read())); +} + +function measureFinal({ algorithm, key, options }, data, n, encoding) { + const warmup = createMac(algorithm, key, options).update(data); + if (encoding === undefined) + warmup.final(); + else + warmup.final(encoding); + + const contexts = new Array(n); + for (let i = 0; i < n; ++i) + contexts[i] = createMac(algorithm, key, options).update(data); + + let result; + if (encoding === undefined) { + bench.start(); + for (let i = 0; i < n; ++i) + result = contexts[i].final(); + bench.end(n); + } else { + bench.start(); + for (let i = 0; i < n; ++i) + result = contexts[i].final(encoding); + bench.end(n); + } + + if (encoding === undefined) + assert(Buffer.isBuffer(result)); + else + assert.strictEqual(typeof result, 'string'); +} diff --git a/benchmark/sqlite/sqlite-diagnostic-channel.js b/benchmark/sqlite/sqlite-diagnostic-channel.js new file mode 100644 index 000000000000..0610839653df --- /dev/null +++ b/benchmark/sqlite/sqlite-diagnostic-channel.js @@ -0,0 +1,42 @@ +'use strict'; +const common = require('../common.js'); +const sqlite = require('node:sqlite'); +const dc = require('node:diagnostics_channel'); +const assert = require('node:assert'); + +const bench = common.createBenchmark(main, { + n: [1e5], + mode: ['none', 'subscribed', 'unsubscribed'], +}); + +function main(conf) { + const { n, mode } = conf; + + const db = new sqlite.DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + const insert = db.prepare('INSERT INTO t VALUES (?)'); + + let subscriber; + if (mode === 'subscribed') { + subscriber = () => {}; + dc.subscribe('sqlite.db.query', subscriber); + } else if (mode === 'unsubscribed') { + subscriber = () => {}; + dc.subscribe('sqlite.db.query', subscriber); + dc.unsubscribe('sqlite.db.query', subscriber); + } + // mode === 'none': no subscription ever made + + let result; + bench.start(); + for (let i = 0; i < n; i++) { + result = insert.run(i); + } + bench.end(n); + + if (mode === 'subscribed') { + dc.unsubscribe('sqlite.db.query', subscriber); + } + + assert.ok(result !== undefined); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index b7bbaccfc3a5..b727bb06cdb9 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -14,6 +14,7 @@ #endif #include #include +#include #include #include #include @@ -130,6 +131,31 @@ struct OpenSSLBufferDeleter { }; using OpenSSLBufferPointer = std::unique_ptr; + +struct RsaOtherPrimeParamNames { + const char* factor; + const char* exponent; + const char* coefficient; +}; + +#define RSA_OTHER_PRIME_PARAM_NAMES(prime, coefficient) \ + { \ + OSSL_PKEY_PARAM_RSA_FACTOR #prime, OSSL_PKEY_PARAM_RSA_EXPONENT #prime, \ + OSSL_PKEY_PARAM_RSA_COEFFICIENT #coefficient \ + } + +constexpr std::array kRsaOtherPrimeParamNames = {{ + RSA_OTHER_PRIME_PARAM_NAMES(3, 2), + RSA_OTHER_PRIME_PARAM_NAMES(4, 3), + RSA_OTHER_PRIME_PARAM_NAMES(5, 4), + RSA_OTHER_PRIME_PARAM_NAMES(6, 5), + RSA_OTHER_PRIME_PARAM_NAMES(7, 6), + RSA_OTHER_PRIME_PARAM_NAMES(8, 7), + RSA_OTHER_PRIME_PARAM_NAMES(9, 8), + RSA_OTHER_PRIME_PARAM_NAMES(10, 9), +}}; + +#undef RSA_OTHER_PRIME_PARAM_NAMES #endif static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = @@ -508,23 +534,43 @@ DataPointer DataPointer::resize(size_t len) { } // ============================================================================ -bool isFipsEnabled() { - ClearErrorOnReturn clear_error_on_return; +namespace { +// This generation only coordinates cache invalidation. It does not make +// OpenSSL default property changes safe to race with crypto operations. +std::atomic fips_state_generation{0}; + +bool isFipsEnabledRaw() { #if OPENSSL_VERSION_MAJOR >= 3 return EVP_default_properties_is_fips_enabled(nullptr) == 1; #else return FIPS_mode() == 1; #endif } +} // namespace + +bool isFipsEnabled() { + ClearErrorOnReturn clear_error_on_return; + return isFipsEnabledRaw(); +} bool setFipsEnabled(bool enable, CryptoErrorList* errors) { - if (isFipsEnabled() == enable) return true; + const bool was_enabled = isFipsEnabled(); + if (was_enabled == enable) return true; ClearErrorOnReturn clearErrorOnReturn(errors); #if OPENSSL_VERSION_MAJOR >= 3 - return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; + const bool success = + EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; #else - return FIPS_mode_set(enable ? 1 : 0) == 1; + const bool success = FIPS_mode_set(enable ? 1 : 0) == 1; #endif + if (success && isFipsEnabledRaw() != was_enabled) { + fips_state_generation.fetch_add(1, std::memory_order_release); + } + return success; +} + +uint64_t getFipsStateGeneration() { + return fips_state_generation.load(std::memory_order_acquire); } bool testFipsEnabled() { @@ -3061,6 +3107,19 @@ EVPKeyPointer EVPKeyPointer::NewRSA(const Rsa& rsa) { bld.get(), OSSL_PKEY_PARAM_RSA_COEFFICIENT1, private_key.qi) != 1) { return {}; } + + const auto other_prime_infos = rsa.getOtherPrimeInfos(); + if (other_prime_infos.size() > kRsaOtherPrimeParamNames.size()) return {}; + for (size_t i = 0; i < other_prime_infos.size(); i++) { + const auto& info = other_prime_infos[i]; + const auto& names = kRsaOtherPrimeParamNames[i]; + if (info.r == nullptr || info.d == nullptr || info.t == nullptr || + OSSL_PARAM_BLD_push_BN(bld.get(), names.factor, info.r) != 1 || + OSSL_PARAM_BLD_push_BN(bld.get(), names.exponent, info.d) != 1 || + OSSL_PARAM_BLD_push_BN(bld.get(), names.coefficient, info.t) != 1) { + return {}; + } + } selection = EVP_PKEY_KEYPAIR; } @@ -4407,39 +4466,376 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) { // ============================================================================ -const Cipher Cipher::FromName(const char* name) { - return Cipher(EVP_get_cipherbyname(name)); +namespace { +constexpr char AsciiToLower(char c) { + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; } -const Cipher Cipher::FromNid(int nid) { - return Cipher(EVP_get_cipherbynid(nid)); +#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(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*>(arg)->emplace_back(name); +} +#endif +} // namespace + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +Cipher::Cipher(DeleteFnPtr cipher) + : cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {} +#endif + +size_t CaseInsensitiveNameHash::operator()( + std::string_view name) const noexcept { + size_t hash = 5381; + for (char c : name) hash = ((hash << 5) + hash) ^ AsciiToLower(c); + return hash; +} + +bool CaseInsensitiveNameEqual::operator()(std::string_view lhs, + std::string_view rhs) const noexcept { + if (lhs.size() != rhs.size()) return false; + for (size_t n = 0; n < lhs.size(); n++) { + if (AsciiToLower(lhs[n]) != AsciiToLower(rhs[n])) return false; + } + return true; +} + +DigestCache::Result DigestCache::lookup(const char* name, + uint64_t generation) const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation) return {}; + const auto it = aliases_.find(name); + if (it == aliases_.end()) return {}; + return lookup(it->second, generation); +#else + static_cast(name); + static_cast(generation); + return {}; +#endif +} + +DigestCache::Result DigestCache::insert(const char* name, + const EVP_MD* digest, + uint64_t generation) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation || name == nullptr || digest == nullptr) { + return {}; + } + + const char* canonical_name = EVP_MD_get0_name(digest); + const OSSL_PROVIDER* provider = EVP_MD_get0_provider(digest); + if (canonical_name == nullptr || provider == nullptr) return {}; + + for (size_t index = 0; index < digests_.size(); index++) { + const EVP_MD* cached = digests_[index].get(); + if (cached == nullptr) continue; + const char* cached_name = EVP_MD_get0_name(cached); + if (EVP_MD_get0_provider(cached) == provider && cached_name != nullptr && + CaseInsensitiveNameEqual()(cached_name, canonical_name)) { + const int32_t id = static_cast(first_id_ + index); + aliases_.insert_or_assign(name, id); + return {cached, id}; + } + } + + if (next_id_ == UINT32_MAX || + EVP_MD_up_ref(const_cast(digest)) != 1) { + return {}; + } + + digests_.emplace_back(const_cast(digest)); + const int32_t id = static_cast(next_id_++); + const size_t index = digests_.size() - 1; + + std::vector aliases; + EVP_MD_names_do_all(digests_[index].get(), PushAlgorithmAlias, &aliases); + for (const std::string& alias : aliases) aliases_.emplace(alias, id); + aliases_.insert_or_assign(name, id); + + return {digests_[index].get(), id}; +#else + static_cast(name); + static_cast(digest); + static_cast(generation); + return {}; +#endif +} + +void DigestCache::reset(uint64_t generation) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ == generation) return; + aliases_.clear(); + digests_.clear(); + first_id_ = next_id_; +#endif + generation_ = generation; +} + +const DigestCache::AliasMap& DigestCache::aliases() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return aliases_; +#else + static const AliasMap empty; + return empty; +#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(name); + static_cast(generation); + return nullptr; +#endif +} + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +const EVP_CIPHER* CipherCache::insert( + const char* name, + DeleteFnPtr&& 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 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 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()); + } else { + cipher_ = nullptr; + } + } +#endif +} + +Cipher& Cipher::operator=(const Cipher& other) { + if (this == &other) return *this; +#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()); + } else { + fetched_cipher_.reset(); + cipher_ = nullptr; + return *this; + } + } else { + fetched_cipher_.reset(); + } +#endif + cipher_ = other.cipher_; + return *this; +} + +const Cipher Cipher::FromName(const char* name, CipherCache* cache) { + const EVP_CIPHER* cipher = EVP_get_cipherbyname(name); + 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); + } + } + + MarkPopErrorOnReturn mark_pop_error_on_return; + DeleteFnPtr fetched( + EVP_CIPHER_fetch(nullptr, name, nullptr)); + if (!IsSupportedFetchedCipher(fetched.get())) return Cipher(); + + if (cache != nullptr && generation == getFipsStateGeneration()) { + if (const EVP_CIPHER* cached = + cache->insert(name, std::move(fetched), generation)) { + return Cipher(cached); + } + } + + return Cipher(std::move(fetched)); +#else + static_cast(cache); + return Cipher(); +#endif +} + +const Cipher Cipher::FromNid(int nid, CipherCache* cache) { + MarkPopErrorOnReturn mark_pop_error_on_return; + const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid); + if (cipher != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!IsSupportedLegacyCipher(cipher)) return Cipher(); +#endif + return Cipher(cipher); + } + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const char* name = OBJ_nid2sn(nid); + if (name != nullptr) return FromName(name, cache); +#else + static_cast(cache); +#endif + + return Cipher(); } 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 +const Cipher& GetPredefinedCipher() { + static const Cipher cipher = Cipher::FromNid(nid); + return cipher; +} +} // namespace + +const Cipher& Cipher::AES_128_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_KW() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_KW() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_KW() { + return GetPredefinedCipher(); +} #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(); +} + +const Cipher& Cipher::AES_192_OCB() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_OCB() { + return GetPredefinedCipher(); +} #endif -const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); +const Cipher& Cipher::CHACHA20_POLY1305() { + return GetPredefinedCipher(); +} bool Cipher::isGcmMode() const { if (!cipher_) return false; @@ -4461,11 +4857,38 @@ 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; } +bool Cipher::isSivMode() const { + if (!cipher_) return false; +#if OPENSSL_WITH_AES_SIV + return getMode() == EVP_CIPH_SIV_MODE; +#else + return false; +#endif +} + +bool Cipher::isGcmSivMode() const { + if (!cipher_) return false; +#if OPENSSL_WITH_AES_GCM_SIV + return getMode() == EVP_CIPH_GCM_SIV_MODE; +#else + return false; +#endif +} + bool Cipher::isStreamMode() const { if (!cipher_) return false; return getMode() == EVP_CIPH_STREAM_CIPHER; @@ -4520,6 +4943,14 @@ std::string_view Cipher::getModeLabel() const { return "ocb"; case EVP_CIPH_OFB_MODE: return "ofb"; +#if OPENSSL_WITH_AES_SIV + case EVP_CIPH_SIV_MODE: + return "siv"; +#endif +#if OPENSSL_WITH_AES_GCM_SIV + case EVP_CIPH_GCM_SIV_MODE: + return "gcm-siv"; +#endif case EVP_CIPH_WRAP_MODE: return "wrap"; case EVP_CIPH_XTS_MODE: @@ -4534,7 +4965,16 @@ const char* Cipher::getName() const { if (!cipher_) return {}; // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of // EVP_CIPHER_name(cipher) for compatibility with BoringSSL. - return OBJ_nid2sn(getNid()); + const int nid = getNid(); + if (nid != NID_undef) { + const char* name = OBJ_nid2sn(nid); + if (name != nullptr) return name; + } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVP_CIPHER_get0_name(cipher_); +#else + return {}; +#endif } bool Cipher::isSupportedAuthenticatedMode() const { @@ -4543,6 +4983,12 @@ bool Cipher::isSupportedAuthenticatedMode() const { case EVP_CIPH_GCM_MODE: #ifndef OPENSSL_NO_OCB case EVP_CIPH_OCB_MODE: +#endif +#if OPENSSL_WITH_AES_SIV + case EVP_CIPH_SIV_MODE: +#endif +#if OPENSSL_WITH_AES_GCM_SIV + case EVP_CIPH_GCM_SIV_MODE: #endif return true; case EVP_CIPH_STREAM_CIPHER: @@ -4621,11 +5067,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(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(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(standard); + return false; +#endif +} + int CipherCtxPointer::getBlockSize() const { if (!ctx_) return 0; return EVP_CIPHER_CTX_block_size(ctx_.get()); @@ -4651,11 +5143,39 @@ 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; } +bool CipherCtxPointer::isSivMode() const { + if (!ctx_) return false; +#if OPENSSL_WITH_AES_SIV + return getMode() == EVP_CIPH_SIV_MODE; +#else + return false; +#endif +} + +bool CipherCtxPointer::isGcmSivMode() const { + if (!ctx_) return false; +#if OPENSSL_WITH_AES_GCM_SIV + return getMode() == EVP_CIPH_GCM_SIV_MODE; +#else + return false; +#endif +} + bool CipherCtxPointer::isChaCha20Poly1305() const { if (!ctx_) return false; return getNid() == NID_chacha20_poly1305; @@ -5576,9 +6096,11 @@ DataPointer RSA_Cipher(const EVPKeyPointer& key, if (!key) return {}; EVPKeyCtxPointer ctx = key.newCtx(); + const Digest& mgf1_digest = + params.mgf1_digest != nullptr ? params.mgf1_digest : params.digest; if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) || - (params.digest != nullptr && (!ctx.setRsaOaepMd(params.digest) || - !ctx.setRsaMgf1Md(params.digest)))) { + (params.digest != nullptr && + (!ctx.setRsaOaepMd(params.digest) || !ctx.setRsaMgf1Md(mgf1_digest)))) { return {}; } @@ -5617,7 +6139,9 @@ DataPointer CipherImpl(const EVPKeyPointer& key, if (!key) return {}; EVPKeyCtxPointer ctx = key.newCtx(); if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) || - (params.digest != nullptr && !ctx.setRsaOaepMd(params.digest))) { + (params.digest != nullptr && !ctx.setRsaOaepMd(params.digest)) || + (params.mgf1_digest != nullptr && + !ctx.setRsaMgf1Md(params.mgf1_digest))) { return {}; } @@ -5650,6 +6174,11 @@ DataPointer CipherImpl(const EVPKeyPointer& key, } } // namespace +Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r, + BignumPointer&& d, + BignumPointer&& t) + : r(r.release()), d(d.release()), t(t.release()) {} + #if NCRYPTO_USE_OPENSSL3_PROVIDER namespace { int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) { @@ -5878,6 +6407,19 @@ Rsa::Rsa(const EVP_PKEY* pkey) : Rsa() { return; } + for (const auto& names : kRsaOtherPrimeParamNames) { + OtherPrimeInfoPointer info; + if (!GetOptionalPKeyBnParam(pkey, names.factor, &info.r) || + !GetOptionalPKeyBnParam(pkey, names.exponent, &info.d) || + !GetOptionalPKeyBnParam(pkey, names.coefficient, &info.t)) { + return; + } + + if (!info.r && !info.d && !info.t) break; + if (!info.r || !info.d || !info.t) return; + other_prime_infos_.push_back(std::move(info)); + } + if (type == EVP_PKEY_RSA_PSS) { MarkPopErrorOnReturn pop_errors; PssParams params; @@ -5916,6 +6458,35 @@ const Rsa::PrivateKey Rsa::getPrivateKey() const { #endif } +const Rsa::OtherPrimeInfos Rsa::getOtherPrimeInfos() const { + OtherPrimeInfos infos; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + infos.reserve(other_prime_infos_.size()); + for (const auto& info : other_prime_infos_) { + infos.push_back({info.r.get(), info.d.get(), info.t.get()}); + } +#elif NCRYPTO_USE_LEGACY_OPENSSL + if (rsa_ == nullptr) return infos; + const int count = RSA_get_multi_prime_extra_count(rsa_); + if (count <= 0) return infos; + + std::vector factors(count); + std::vector exponents(count); + std::vector coefficients(count); + if (RSA_get0_multi_prime_factors(rsa_, factors.data()) != 1 || + RSA_get0_multi_prime_crt_params( + rsa_, exponents.data(), coefficients.data()) != 1) { + return {}; + } + + infos.reserve(count); + for (int i = 0; i < count; i++) { + infos.push_back({factors[i], exponents[i], coefficients[i]}); + } +#endif + return infos; +} + const std::optional Rsa::getPssParams() const { #if NCRYPTO_USE_OPENSSL3_PROVIDER return pss_params_; @@ -6017,15 +6588,20 @@ bool Rsa::setPrivateKey(BignumPointer&& d, BignumPointer&& p, BignumPointer&& dp, BignumPointer&& dq, - BignumPointer&& qi) { + BignumPointer&& qi, + OtherPrimeInfoPointers&& other_prime_infos) { #if NCRYPTO_USE_OPENSSL3_PROVIDER if (!d || !q || !p || !dp || !dq || !qi) return false; + for (const auto& info : other_prime_infos) { + if (!info.r || !info.d || !info.t) return false; + } d_.reset(d.release()); q_.reset(q.release()); p_.reset(p.release()); dp_.reset(dp.release()); dq_.reset(dq.release()); qi_.reset(qi.release()); + other_prime_infos_ = std::move(other_prime_infos); rsa_ = n_ != nullptr && e_ != nullptr; return rsa_; #else @@ -6047,6 +6623,37 @@ bool Rsa::setPrivateKey(BignumPointer&& d, dp.release(); dq.release(); qi.release(); + +#if NCRYPTO_USE_LEGACY_OPENSSL + if (!other_prime_infos.empty()) { + std::vector factors; + std::vector exponents; + std::vector coefficients; + factors.reserve(other_prime_infos.size()); + exponents.reserve(other_prime_infos.size()); + coefficients.reserve(other_prime_infos.size()); + for (const auto& info : other_prime_infos) { + if (!info.r || !info.d || !info.t) return false; + factors.push_back(info.r.get()); + exponents.push_back(info.d.get()); + coefficients.push_back(info.t.get()); + } + if (RSA_set0_multi_prime_params(const_cast(rsa_), + factors.data(), + exponents.data(), + coefficients.data(), + static_cast(factors.size())) != 1) { + return false; + } + for (auto& info : other_prime_infos) { + info.r.release(); + info.d.release(); + info.t.release(); + } + } +#else + if (!other_prime_infos.empty()) return false; +#endif return true; #endif } @@ -6100,7 +6707,7 @@ struct CipherCallbackContext { void operator()(const char* name) { cb(name); } }; -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER template fetched( + fetch_type(nullptr, real_name, nullptr)); + if (!IsSupportedFetchedCipher(fetched.get())) return; - free_type(fetched); auto& cb = *(static_cast(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(c + ('a' - 'A')); + } + return static_cast(c); + }); + auto& cb = *(static_cast(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 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 void array_push_back(const TypeName* evp_ref, @@ -6156,7 +6799,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, #endif &context); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_CIPHER_do_all_provided(nullptr, array_push_back_provider, &context); +#endif #endif } @@ -6316,11 +6962,19 @@ EVP_MD_CTX* EVPMDCtxPointer::release() { return ctx_.release(); } -bool EVPMDCtxPointer::digestInit(const Digest& digest) { +bool EVPMDCtxPointer::digestInit(const EVP_MD* digest) { if (!ctx_) return false; return EVP_DigestInit_ex(ctx_.get(), digest, nullptr) > 0; } +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0) +bool EVPMDCtxPointer::digestInit(const EVP_MD* digest, + const OSSL_PARAM* params) { + if (!ctx_) return false; + return EVP_DigestInit_ex2(ctx_.get(), digest, params) > 0; +} +#endif + bool EVPMDCtxPointer::digestUpdate(const Buffer& in) { if (!ctx_) return false; return EVP_DigestUpdate(ctx_.get(), in.data, in.len) > 0; @@ -6669,6 +7323,79 @@ EVPMacPointer EVPMacPointer::Fetch(const char* algorithm) { return EVPMacPointer(EVP_MAC_fetch(nullptr, algorithm, nullptr)); } +MacKind MacCache::GetKind(EVP_MAC* mac) { + if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_HMAC)) return MacKind::kHmac; + if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_CMAC)) return MacKind::kCmac; + if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_GMAC)) return MacKind::kGmac; + return MacKind::kOther; +} + +MacCache::Result MacCache::lookup(const char* name, uint64_t generation) const { + if (generation_ != generation || name == nullptr) return {}; + const auto it = aliases_.find(name); + if (it == aliases_.end()) return {}; + return lookup(it->second, generation); +} + +MacCache::Result MacCache::insert(const char* name, + EVPMacPointer&& mac, + uint64_t generation) { + if (generation_ != generation || generation != getFipsStateGeneration() || + name == nullptr || mac == nullptr) { + return {}; + } + + const char* canonical_name = EVP_MAC_get0_name(mac.get()); + const OSSL_PROVIDER* provider = EVP_MAC_get0_provider(mac.get()); + if (canonical_name == nullptr || provider == nullptr) return {}; + + for (size_t index = 0; index < macs_.size(); index++) { + EVP_MAC* cached = macs_[index].mac.get(); + if (cached == nullptr) continue; + const char* cached_name = EVP_MAC_get0_name(cached); + if (EVP_MAC_get0_provider(cached) == provider && cached_name != nullptr && + CaseInsensitiveNameEqual()(cached_name, canonical_name)) { + if (generation != getFipsStateGeneration()) return {}; + const int32_t id = static_cast(first_id_ + index); + aliases_.insert_or_assign(name, id); + return {cached, id, macs_[index].kind}; + } + } + + if (next_id_ == UINT32_MAX) return {}; + + std::vector aliases; + { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (EVP_MAC_names_do_all(mac.get(), PushAlgorithmAlias, &aliases) != 1) { + return {}; + } + } + if (generation != getFipsStateGeneration()) return {}; + + const MacKind kind = GetKind(mac.get()); + macs_.push_back({std::move(mac), kind}); + const int32_t id = static_cast(next_id_++); + const size_t index = macs_.size() - 1; + + for (const std::string& alias : aliases) aliases_.emplace(alias, id); + aliases_.insert_or_assign(name, id); + + return {macs_[index].mac.get(), id, kind}; +} + +void MacCache::reset(uint64_t generation) { + if (generation_ == generation) return; + aliases_.clear(); + macs_.clear(); + first_id_ = next_id_; + generation_ = generation; +} + +const MacCache::AliasMap& MacCache::aliases() const { + return aliases_; +} + EVPMacCtxPointer::EVPMacCtxPointer(EVP_MAC_CTX* ctx) : ctx_(ctx) {} EVPMacCtxPointer::EVPMacCtxPointer(EVPMacCtxPointer&& other) noexcept @@ -6696,22 +7423,42 @@ EVP_MAC_CTX* EVPMacCtxPointer::release() { bool EVPMacCtxPointer::init(const Buffer& key, const OSSL_PARAM* params) { if (!ctx_) return false; - return EVP_MAC_init(ctx_.get(), - static_cast(key.data), - key.len, - params) == 1; + + static constexpr unsigned char kEmptyKey = 0; + const unsigned char* key_data = static_cast(key.data); + if (key_data == nullptr) { + if (key.len != 0) return false; + key_data = &kEmptyKey; + } + + return EVP_MAC_init(ctx_.get(), key_data, key.len, params) == 1; } bool EVPMacCtxPointer::update(const Buffer& data) { if (!ctx_) return false; + if (data.len == 0) return true; + if (data.data == nullptr) return false; return EVP_MAC_update(ctx_.get(), static_cast(data.data), data.len) == 1; } +size_t EVPMacCtxPointer::getSize() const { + return ctx_ ? EVP_MAC_CTX_get_mac_size(ctx_.get()) : 0; +} + +const OSSL_PARAM* EVPMacCtxPointer::getSettableParams() const { + return ctx_ ? EVP_MAC_CTX_settable_params(ctx_.get()) : nullptr; +} + DataPointer EVPMacCtxPointer::final(size_t length) { if (!ctx_) return {}; - auto buf = DataPointer::Alloc(length); + + // DataPointer uses a null allocation to represent failure. Retain a + // one-byte allocation for a successful zero-length result while passing the + // requested zero capacity to OpenSSL. A non-null output pointer is required + // to actually finalize; nullptr only queries the output length. + auto buf = DataPointer::Alloc(length == 0 ? 1 : length); if (!buf) return {}; size_t result_len = length; @@ -6721,8 +7468,9 @@ DataPointer EVPMacCtxPointer::final(size_t length) { length) != 1) { return {}; } + if (result_len > length) return {}; - return buf; + return buf.resize(result_len); } EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) { @@ -6846,7 +7594,10 @@ DataPointer xofHashDigest(const Buffer& buf, if (ctx.digestInit(md) != 1) { return {}; } - if (ctx.digestUpdate(reinterpret_cast&>(buf)) != 1) { + if (ctx.digestUpdate(Buffer{ + .data = buf.data, + .len = buf.len, + }) != 1) { return {}; } return ctx.digestFinal(output_length); @@ -6982,14 +7733,86 @@ size_t Digest::size() const { return EVP_MD_size(md_); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +Digest::Digest(DeleteFnPtr md) + : md_(md.get()), fetched_md_(std::move(md)) {} +#endif + +Digest::Digest(const Digest& other) : md_(other.md_) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (other.fetched_md_ != nullptr) { + if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) { + fetched_md_.reset(other.fetched_md_.get()); + } else { + md_ = nullptr; + } + } +#endif +} + +Digest& Digest::operator=(const Digest& other) { + if (this == &other) return *this; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (other.fetched_md_ != nullptr) { + if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) { + fetched_md_.reset(other.fetched_md_.get()); + } else { + fetched_md_.reset(); + md_ = nullptr; + return *this; + } + } else { + fetched_md_.reset(); + } +#endif + md_ = other.md_; + return *this; +} + const Digest Digest::MD5 = Digest(EVP_md5()); const Digest Digest::SHA1 = Digest(EVP_sha1()); const Digest Digest::SHA256 = Digest(EVP_sha256()); const Digest Digest::SHA384 = Digest(EVP_sha384()); const Digest Digest::SHA512 = Digest(EVP_sha512()); +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +bool IsSupportedDigest(const EVP_MD* md) { + if (md == nullptr || EVP_MD_is_a(md, "NULL")) return false; + + // OpenSSL currently crashes when ML-DSA-MU finalizes an empty input. Keep it + // unavailable until the provider implementation is fixed. + // https://github.com/openssl/openssl/issues/32445 + if (EVP_MD_is_a(md, "ML-DSA-MU")) return false; + + return true; +} +} // namespace +#endif + const Digest Digest::FromName(const char* name) { - return ncrypto::getDigestByName(name); + const EVP_MD* md = ncrypto::getDigestByName(name); + if (md != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (md == EVP_md_null()) return Digest(); +#endif + return Digest(md); + } + + return Fetch(name); +} + +const Digest Digest::Fetch(const char* name) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + MarkPopErrorOnReturn mark_pop_error_on_return; + DeleteFnPtr fetched( + EVP_MD_fetch(nullptr, name, nullptr)); + if (IsSupportedDigest(fetched.get())) { + return Digest(std::move(fetched)); + } +#endif + + return Digest(); } // ============================================================================ diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index f0e1e7451e4f..6b1edceed061 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -13,12 +13,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \ !defined(OPENSSL_NO_ENGINE) #include @@ -105,6 +108,18 @@ #define OPENSSL_WITH_EVP_MAC 0 #endif +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_AES_SIV 1 +#else +#define OPENSSL_WITH_AES_SIV 0 +#endif + +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_AES_GCM_SIV 1 +#else +#define OPENSSL_WITH_AES_GCM_SIV 0 +#endif + #if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2) #define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 1 #else @@ -351,6 +366,7 @@ class DataPointer; class DHPointer; class ECKeyPointer; class EVPKeyPointer; +class MacCache; class EVPMacCtxPointer; class EVPMacPointer; class EVPMDCtxPointer; @@ -385,9 +401,12 @@ class Digest final { static constexpr size_t MAX_SIZE = EVP_MAX_MD_SIZE; Digest() = default; Digest(const EVP_MD* md) : md_(md) {} - Digest(const Digest&) = default; - Digest& operator=(const Digest&) = default; + Digest(const Digest& other); + Digest& operator=(const Digest& other); inline Digest& operator=(const EVP_MD* md) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + fetched_md_.reset(); +#endif md_ = md; return *this; } @@ -406,9 +425,72 @@ class Digest final { static const Digest SHA512; static const Digest FromName(const char* name); + static const Digest Fetch(const char* name); private: const EVP_MD* md_ = nullptr; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit Digest(DeleteFnPtr md); + DeleteFnPtr fetched_md_; +#endif +}; + +struct CaseInsensitiveNameHash { + using is_transparent = void; + size_t operator()(std::string_view name) const noexcept; +}; + +struct CaseInsensitiveNameEqual { + using is_transparent = void; + bool operator()(std::string_view lhs, std::string_view rhs) const noexcept; +}; + +class DigestCache final { + public: + struct Result { + const EVP_MD* digest = nullptr; + int32_t id = -1; + }; + + using AliasMap = std::unordered_map; + + DigestCache() = default; + NCRYPTO_DISALLOW_COPY_AND_MOVE(DigestCache) + + Result lookup(const char* name, uint64_t generation) const; + inline Result lookup(int32_t id, uint64_t generation) const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation || id == -1) return {}; + const uint32_t unsigned_id = static_cast(id); + if (unsigned_id < first_id_) return {}; + const size_t index = unsigned_id - first_id_; + if (index >= digests_.size()) return {}; + return {digests_[index].get(), id}; +#else + static_cast(id); + static_cast(generation); + return {}; +#endif + } + Result insert(const char* name, const EVP_MD* digest, uint64_t generation); + void reset(uint64_t generation); + const AliasMap& aliases() const; + + private: + uint64_t generation_ = 0; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + using EVPMDPointer = DeleteFnPtr; + + // IDs are not reused across generations because JavaScript caches them + // independently in each Realm. + uint32_t first_id_ = 0; + uint32_t next_id_ = 0; + std::vector digests_; + AliasMap aliases_; +#endif }; // Computes a fixed-length digest. @@ -419,6 +501,32 @@ DataPointer xofHashDigest(const Buffer& 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&& cipher, + uint64_t generation); +#endif + + private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + using EVPCipherPointer = DeleteFnPtr; + + uint64_t generation_ = 0; + std::vector ciphers_; + std::unordered_map + aliases_; +#endif +}; + class Cipher final { public: static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH; @@ -437,9 +545,12 @@ class Cipher final { Cipher() = default; Cipher(const EVP_CIPHER* cipher) : cipher_(cipher) {} - Cipher(const Cipher&) = default; - Cipher& operator=(const Cipher&) = default; + Cipher(const Cipher& other); + Cipher& operator=(const Cipher& other); inline Cipher& operator=(const EVP_CIPHER* cipher) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + fetched_cipher_.reset(); +#endif cipher_ = cipher; return *this; } @@ -461,7 +572,10 @@ class Cipher final { bool isWrapMode() const; bool isCtrMode() const; bool isCcmMode() const; + bool isCtsMode() const; bool isOcbMode() const; + bool isSivMode() const; + bool isGcmSivMode() const; bool isStreamMode() const; bool isChaCha20Poly1305() const; @@ -472,8 +586,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; @@ -482,32 +596,29 @@ 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; Digest digest; + Digest mgf1_digest; const Buffer label; }; @@ -532,6 +643,10 @@ class Cipher final { private: const EVP_CIPHER* cipher_ = nullptr; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit Cipher(DeleteFnPtr cipher); + DeleteFnPtr fetched_cipher_; +#endif }; // ============================================================================ @@ -609,6 +724,23 @@ class Rsa final { const BIGNUM* dq; const BIGNUM* qi; }; + struct OtherPrimeInfo { + const BIGNUM* r; + const BIGNUM* d; + const BIGNUM* t; + }; + struct OtherPrimeInfoPointer { + OtherPrimeInfoPointer() = default; + OtherPrimeInfoPointer(BignumPointer&& r, + BignumPointer&& d, + BignumPointer&& t); + + DeleteFnPtr r; + DeleteFnPtr d; + DeleteFnPtr t; + }; + using OtherPrimeInfos = std::vector; + using OtherPrimeInfoPointers = std::vector; struct PssParams { std::string_view digest = "sha1"; std::optional mgf1_digest = "sha1"; @@ -617,6 +749,7 @@ class Rsa final { const PublicKey getPublicKey() const; const PrivateKey getPrivateKey() const; + const OtherPrimeInfos getOtherPrimeInfos() const; const std::optional getPssParams() const; bool setPublicKey(BignumPointer&& n, BignumPointer&& e); @@ -625,7 +758,8 @@ class Rsa final { BignumPointer&& p, BignumPointer&& dp, BignumPointer&& dq, - BignumPointer&& qi); + BignumPointer&& qi, + OtherPrimeInfoPointers&& other_prime_infos = {}); using CipherParams = Cipher::CipherParams; @@ -650,6 +784,7 @@ class Rsa final { DeleteFnPtr dp_; DeleteFnPtr dq_; DeleteFnPtr qi_; + OtherPrimeInfoPointers other_prime_infos_; std::optional pss_params_; #else OSSL3_CONST RSA* rsa_; @@ -918,7 +1053,9 @@ class CipherCtxPointer final { bool setIvLength(size_t length); bool setAeadTag(const Buffer& 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, @@ -931,7 +1068,11 @@ 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; bool isChaCha20Poly1305() const; bool update(const Buffer& in, @@ -1666,7 +1807,16 @@ class EVPMDCtxPointer final { void reset(EVP_MD_CTX* ctx = nullptr); EVP_MD_CTX* release(); - bool digestInit(const Digest& digest); + bool digestInit(const EVP_MD* digest); + inline bool digestInit(const Digest& digest) { + return digestInit(digest.get()); + } +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0) + bool digestInit(const EVP_MD* digest, const OSSL_PARAM* params); + inline bool digestInit(const Digest& digest, const OSSL_PARAM* params) { + return digestInit(digest.get(), params); + } +#endif bool digestUpdate(const Buffer& in); DataPointer digestFinal(size_t length); bool digestFinalInto(Buffer* buf); @@ -1758,6 +1908,61 @@ class EVPMacPointer final { DeleteFnPtr mac_; }; +enum class MacKind : uint8_t { + kOther, + kHmac, + kCmac, + kGmac, +}; + +class MacCache final { + public: + struct Result { + // Borrowed from the cache and valid until the cache is reset. Creating an + // EVP_MAC_CTX takes an independent reference to the method. + EVP_MAC* mac = nullptr; + int32_t id = -1; + MacKind kind = MacKind::kOther; + }; + + using AliasMap = std::unordered_map; + + MacCache() = default; + NCRYPTO_DISALLOW_COPY_AND_MOVE(MacCache) + + Result lookup(const char* name, uint64_t generation) const; + inline Result lookup(int32_t id, uint64_t generation) const { + if (generation_ != generation || id == -1) return {}; + const uint32_t unsigned_id = static_cast(id); + if (unsigned_id < first_id_) return {}; + const size_t index = unsigned_id - first_id_; + if (index >= macs_.size()) return {}; + return {macs_[index].mac.get(), id, macs_[index].kind}; + } + Result insert(const char* name, EVPMacPointer&& mac, uint64_t generation); + void reset(uint64_t generation); + const AliasMap& aliases() const; + static MacKind GetKind(EVP_MAC* mac); + + private: + struct Entry { + EVPMacPointer mac; + MacKind kind; + }; + + uint64_t generation_ = 0; + + // IDs are not reused across generations because JavaScript may cache them + // independently in each Realm. + uint32_t first_id_ = 0; + uint32_t next_id_ = 0; + std::vector macs_; + AliasMap aliases_; +}; + class EVPMacCtxPointer final { public: EVPMacCtxPointer() = default; @@ -1776,6 +1981,8 @@ class EVPMacCtxPointer final { bool init(const Buffer& key, const OSSL_PARAM* params = nullptr); bool update(const Buffer& data); + size_t getSize() const; + const OSSL_PARAM* getSettableParams() const; DataPointer final(size_t length); static EVPMacCtxPointer New(EVP_MAC* mac); @@ -1812,6 +2019,14 @@ class HMACCtxPointer final { }; #endif // OPENSSL_WITH_EVP_MAC +#if !OPENSSL_WITH_EVP_MAC +class MacCache final { + public: + MacCache() = default; + NCRYPTO_DISALLOW_COPY_AND_MOVE(MacCache) +}; +#endif + #ifndef OPENSSL_NO_ENGINE class EnginePointer final { public: @@ -1856,6 +2071,8 @@ bool isFipsEnabled(); bool setFipsEnabled(bool enabled, CryptoErrorList* errors); +uint64_t getFipsStateGeneration(); + bool testFipsEnabled(); // ============================================================================ diff --git a/doc/api/cli.md b/doc/api/cli.md index 801b017bc5a9..acbd1ff8bd7f 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -794,6 +794,17 @@ Enable [FIPS mode][] at startup. With OpenSSL 3, a configured provider named `fips` must be available and initialize successfully. With OpenSSL 1.1.1, Node.js must be built against a FIPS-capable OpenSSL. +### `--enable-fips-indicator-events` + + + +Publish OpenSSL FIPS indicator results to the +[`'crypto.fips.indicator'`][] diagnostics channel. This option requires OpenSSL +3.4 or later. It does not enable [FIPS mode][] or change whether an operation +is permitted. + ### `--enable-network-family-autoselection` Enable [FIPS mode][] at startup and prevent it from being disabled from script code. The same OpenSSL requirements as [`--enable-fips`][] apply. +An optional mode can be specified using `--force-fips=mode`: + +* `provider`: Preserve the OpenSSL FIPS provider's configured handling of + non-approved operations. This is the current default when the mode is + omitted. +* `strict`: Reject non-approved operations reported through the OpenSSL FIPS + indicator callback. This mode requires OpenSSL 3.4 or later. + +The `strict` mode only covers operations reported through the callback for +OpenSSL's default library context. It does not cover native addons that use +another `OSSL_LIB_CTX` or another copy of `libcrypto`, nor operation-specific +indicators that do not invoke the callback. + ### `--force-node-api-uncaught-exceptions-policy` The `node:crypto` module provides cryptographic functionality that includes a -set of wrappers for OpenSSL's hash, HMAC, cipher, decipher, sign, and verify -functions. +set of wrappers for OpenSSL's hash, message authentication code (MAC), cipher, +decipher, sign, verify, and key encapsulation mechanism (KEM) functions. ```mjs const { createHmac } = await import('node:crypto'); @@ -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 @@ -862,8 +866,8 @@ added: v1.0.0 --> * Returns: {Buffer} When using an authenticated encryption mode (`GCM`, `CCM`, - `OCB`, and `chacha20-poly1305` are currently supported), the - `cipher.getAuthTag()` method returns a + `OCB`, `SIV`, `GCM-SIV`, and `chacha20-poly1305` are currently + supported), the `cipher.getAuthTag()` method returns a [`Buffer`][] containing the _authentication tag_ that has been computed from the given data. @@ -885,14 +889,14 @@ added: v1.0.0 * `encoding` {string} The string encoding to use when `buffer` is a string. * Returns: {Cipheriv} The same `Cipheriv` instance for method chaining. -When using an authenticated encryption mode (`GCM`, `CCM`, `OCB`, and -`chacha20-poly1305` are -currently supported), the `cipher.setAAD()` method sets the value used for the -_additional authenticated data_ (AAD) input parameter. +When using an authenticated encryption mode (`GCM`, `CCM`, `OCB`, `SIV`, +`GCM-SIV`, and `chacha20-poly1305` are currently supported), the +`cipher.setAAD()` method sets the value used for the _additional authenticated +data_ (AAD) input parameter. -The `plaintextLength` option is optional for `GCM` and `OCB`. When using `CCM`, -the `plaintextLength` option must be specified and its value must match the -length of the plaintext in bytes. See [CCM mode][]. +The `plaintextLength` option is optional for `GCM`, `OCB`, `SIV`, and +`GCM-SIV`. When using `CCM`, the `plaintextLength` option must be specified and +its value must match the length of the plaintext in bytes. See [CCM mode][]. The `cipher.setAAD()` method must be called before [`cipher.update()`][]. @@ -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 @@ -1190,14 +1202,14 @@ changes: * `encoding` {string} String encoding to use when `buffer` is a string. * Returns: {Decipheriv} The same `Decipheriv` instance for method chaining. -When using an authenticated encryption mode (`GCM`, `CCM`, `OCB`, and -`chacha20-poly1305` are -currently supported), the `decipher.setAAD()` method sets the value used for the -_additional authenticated data_ (AAD) input parameter. +When using an authenticated encryption mode (`GCM`, `CCM`, `OCB`, `SIV`, +`GCM-SIV`, and `chacha20-poly1305` are currently supported), the +`decipher.setAAD()` method sets the value used for the _additional +authenticated data_ (AAD) input parameter. -The `options` argument is optional for `GCM`. When using `CCM`, the -`plaintextLength` option must be specified and its value must match the length -of the ciphertext in bytes. See [CCM mode][]. +The `options` argument is optional for `GCM`, `OCB`, `SIV`, and `GCM-SIV`. +When using `CCM`, the `plaintextLength` option must be specified and its value +must match the length of the ciphertext in bytes. See [CCM mode][]. The `decipher.setAAD()` method must be called before [`decipher.update()`][]. @@ -1232,18 +1244,18 @@ changes: * `encoding` {string} String encoding to use when `buffer` is a string. * Returns: {Decipheriv} The same `Decipheriv` instance for method chaining. -When using an authenticated encryption mode (`GCM`, `CCM`, `OCB`, and -`chacha20-poly1305` are -currently supported), the `decipher.setAuthTag()` method is used to pass in the -received _authentication tag_. If no tag is provided, or if the cipher text -has been tampered with, [`decipher.final()`][] will throw, indicating that the -cipher text should be discarded due to failed authentication. If the tag length -is invalid according to [NIST SP 800-38D][] or does not match the value of the +When using an authenticated encryption mode (`GCM`, `CCM`, `OCB`, `SIV`, +`GCM-SIV`, and `chacha20-poly1305` are currently supported), the +`decipher.setAuthTag()` method is used to pass in the received +_authentication tag_. If no tag is provided, or if the cipher text has been +tampered with, [`decipher.final()`][] will throw, indicating that the cipher +text should be discarded due to failed authentication. If the tag length is +invalid according to [NIST SP 800-38D][] or does not match the value of the `authTagLength` option, `decipher.setAuthTag()` will throw an error. The `decipher.setAuthTag()` method must be called before [`decipher.update()`][] -for `CCM` mode or before [`decipher.final()`][] for `GCM` and `OCB` modes and -`chacha20-poly1305`. +for `CCM`, `SIV`, and `GCM-SIV` modes or before [`decipher.final()`][] for +`GCM` and `OCB` modes and `chacha20-poly1305`. `decipher.setAuthTag()` can only be called once. Because the `node:crypto` module was originally designed to closely mirror @@ -1271,8 +1283,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()`][]. @@ -1295,19 +1307,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 @@ -2512,6 +2528,91 @@ Depending on the type of this `KeyObject`, this property is either `'secret'` for secret (symmetric) keys, `'public'` for public (asymmetric) keys or `'private'` for private (asymmetric) keys. +## Class: `Mac` + + + +* Extends: {stream.Transform} + +The `Mac` class computes message authentication codes using MAC +implementations supplied by OpenSSL providers. It can be used in one of two +ways: + +* As a [stream][] that is both readable and writable, where data is written and + one authentication tag is produced on the readable side when the writable + side ends; or +* By calling [`mac.update()`][] one or more times followed by [`mac.final()`][]. + +Instances of `Mac` are created using [`crypto.createMac()`][]. The `Mac` class +is not exported directly by the `node:crypto` module. + +Calling `mac.end()` without first writing data computes the authentication tag +for an empty message. If the selected MAC produces a zero-byte tag, such as +when a provider accepts `outputLength: 0`, the readable side ends without +emitting a data chunk because Node.js streams do not emit zero-length chunks. +When using `mac.final()` instead, it returns a zero-length [`Buffer`][] or an +empty encoded string. + +`mac.end()` and `mac.final()` are alternative terminal operations and must not +both be called on the same object. A `Mac` object cannot be used again after +either operation attempts finalization or after an underlying MAC update fails. + +Example: Using [`mac.update()`][] and [`mac.final()`][]: + +```mjs +const { createMac, randomBytes } = await import('node:crypto'); + +const key = randomBytes(16); +const mac = createMac('CMAC', key, { + cipher: 'AES-128-CBC', +}); + +mac.update('some data to authenticate'); +console.log(mac.final('hex')); +``` + +### `mac.final([outputEncoding])` + + + +* `outputEncoding` {string} The [encoding][] of the return value. +* Returns: {Buffer | string} + +Completes the MAC computation and returns the authentication tag. If +`outputEncoding` is omitted or is `'buffer'`, a [`Buffer`][] is returned. +Otherwise, a string is returned. + +To verify an authentication tag, compare equal-length [`Buffer`][] values using +[`crypto.timingSafeEqual()`][]. + +The `Mac` object cannot be used again after finalization is attempted, +including when finalization fails. Later calls to `mac.update()` or +`mac.final()` throw `ERR_CRYPTO_MAC_FINALIZED`. + +### `mac.update(data[, inputEncoding])` + + + +* `data` {string|Buffer|TypedArray|DataView} +* `inputEncoding` {string} The [encoding][] of the `data` string. +* Returns: {Mac} + +Updates the MAC with `data` and returns the `Mac` object so that calls can be +chained. When `data` is a string, `inputEncoding` defaults to `'utf8'`. When +`data` is a [`Buffer`][], `TypedArray`, or `DataView`, `inputEncoding` is +ignored. + +This method can be called multiple times before finalization. If an underlying +MAC update fails, the `Mac` object cannot be used again. Calling this method +after a previous underlying MAC update failure or after finalization throws +`ERR_CRYPTO_MAC_FINALIZED`. + ## Class: `Sign` + +> Stability: 1.2 - Release candidate + +* `algorithm` {string} The name of the MAC algorithm. +* `key` {ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} +* `options` {Object} [`stream.transform` options][] + * `digest` {string} The digest used by a MAC such as HMAC. + * `cipher` {string} The cipher used by a MAC such as CMAC or GMAC. + * `iv` {ArrayBuffer|Buffer|TypedArray|DataView} The initialization vector for + a MAC such as GMAC. + * `customization` {ArrayBuffer|Buffer|TypedArray|DataView} A customization + byte string for MACs that support it, such as KMAC. + * `salt` {ArrayBuffer|Buffer|TypedArray|DataView} A salt byte string for MACs + that support it, such as BLAKE2 MACs. + * `outputLength` {number} The requested provider output size in bytes. Must be + an unsigned 32-bit integer. Provider-specific restrictions also apply. +* Returns: {Mac} + +`algorithm` must be a non-empty provider MAC name. The MAC-specific properties +listed above are extensions to the standard [`stream.transform` options][] and +are passed only when the selected provider implementation advertises the +corresponding parameter with the expected type. A supplied MAC-specific option +that the selected implementation does not support causes an error. + +The following table summarizes the MAC-specific options accepted by MAC +implementations in OpenSSL's built-in providers. The `key` argument is required +for every MAC. The table lists only MAC-specific options; standard +[`stream.transform` options][] remain available for every family. + +| MAC family | Required options | Optional options | Notes | +| ---------- | --------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------- | +| HMAC | `digest` | None | | +| CMAC | `cipher` using CBC mode | None | | +| GMAC | `cipher` using GCM mode, non-empty `iv` | None | Requires a unique IV for every message authenticated with a given key. | +| KMAC | None | `customization`, `outputLength` | | +| BLAKE2 MAC | None | `customization`, `salt`, `outputLength` | | +| Poly1305 | None | None | Each key must be used for only one message. | +| SipHash | None | `outputLength` | | + +`outputLength` configures the output size of the provider MAC. It is never +implemented by computing a longer tag and truncating it. A value of `0` is +passed to the provider and is accepted only when that provider can initialize +and finalize the MAC with a zero-byte output. When `outputLength` is omitted, +the provider's default output size is used and must be nonzero. + +The `key` must contain bytes or be a [`KeyObject`][] of type `secret`. Key +length and other key requirements are determined by the selected provider +implementation. + +Available algorithms and their accepted parameters depend on the OpenSSL +version, loaded providers, and active default property query. Use +[`crypto.getMacs()`][] to list fetchable MAC names. A listed name can still +require options or a key with provider-specific properties. + ### `crypto.createPrivateKey(key)` * Returns: {string\[]} An array of the names of the supported hash algorithms, such as `'RSA-SHA256'`. Hash algorithms are also called "digest" algorithms. +This is the authoritative Node.js list of hash algorithms available to +[`crypto.createHash()`][] and [`crypto.hash()`][] in the current process. With +OpenSSL 3 or later, the list depends on the loaded providers and the default +property query in effect when the list is first generated. Some listed +algorithms can require API-specific options, such as `outputLength` for XOF +hash functions. + +A listed hash algorithm is not necessarily supported by APIs that combine a +digest with another cryptographic operation, such as HMAC, key derivation, or +signing. Those operations can apply additional restrictions. + ```mjs const { getHashes, @@ -4906,6 +5183,40 @@ const { console.log(getHashes()); // ['DSA', 'DSA-SHA', 'DSA-SHA1', ...] ``` +### `crypto.getMacs()` + + + +> Stability: 1.2 - Release candidate + +* Returns: {string\[]} A fresh array containing the sorted, lowercase names + and aliases of fetchable MAC implementations. + +Returns MAC names exposed by loaded OpenSSL providers that match the active +default property query. Duplicate names and numeric OID aliases are omitted. +On builds without OpenSSL `EVP_MAC` support, this function returns an empty +array. + +The returned names describe implementations that OpenSSL can fetch. They do not +guarantee that [`crypto.createMac()`][] can initialize the MAC without +additional options. A provider can require additional parameters or a key with +algorithm-specific properties, and it can expose parameters that this API does +not support. + +After a successful FIPS mode change made with [`crypto.setFips()`][], subsequent +calls reflect the new mode, and newly created `Mac` objects use it. Existing +`Mac` objects continue using the provider implementation selected when they +were created. + +```mjs +const { getMacs } = await import('node:crypto'); + +console.log(getMacs()); +// ['blake2bmac', 'blake2smac', 'cmac', 'gmac', 'hmac', ...] +``` + ### `crypto.getRandomValues(typedArray)` * `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey|URL} - * `oaepHash` {string} The hash function to use for OAEP padding and MGF1. - **Default:** `'sha1'` + * `oaepHash` {string} The hash function to use for OAEP padding and, unless + `mgf1Hash` is set, MGF1. **Default:** `'sha1'` + * `mgf1Hash` {string} The hash function to use for the MGF1 mask generation + function of OAEP padding. If not specified, the value of `oaepHash` is used. + This allows the OAEP digest and the MGF1 digest to differ. * `oaepLabel` {string|ArrayBuffer|Buffer|TypedArray|DataView} The label to use for OAEP padding. If not specified, no label is used. * `padding` {crypto.constants} An optional padding value defined in @@ -5319,6 +5670,10 @@ changes: Decrypts `buffer` with `privateKey`. `buffer` was previously encrypted using the corresponding public key, for example using [`crypto.publicEncrypt()`][]. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the active RSA implementation can impose additional restrictions on digests +used for OAEP or MGF1. + If `privateKey` is not a [`KeyObject`][], this function behaves as if `privateKey` had been passed to [`crypto.createPrivateKey()`][]. If it is an object, the `padding` property can be passed. Otherwise, this function uses @@ -5418,6 +5773,9 @@ be passed instead of a public key. + +> Stability: 1 - Experimental + +##### Event: `'crypto.fips.indicator'` + +* `operation` {string} The provider-defined operation type. +* `reason` {string} The provider-defined description of why the operation is + not approved. +* `blocked` {boolean} Whether an indicator callback blocked the operation. +* `count` {number} The number of matching pending indicator invocations + represented by this message. +* `dropped` {number} The number of additional indicator invocations dropped + before this message was delivered. + +Emitted when the OpenSSL FIPS provider used by Node.js detects an operation that +is not FIPS approved after the corresponding provider check was relaxed. Such +operations are possible when the provider is configured for backwards +compatibility. The `operation` and `reason` values come from the provider and +should be treated as opaque strings rather than stable enumerations. + +Start Node.js with [`--enable-fips-indicator-events`][] to enable this channel. +Without the option, subscribing does not install the OpenSSL callback and no +messages are published. + +Subscribing to the channel is observation-only and never changes the result of +an operation. Node.js preserves the result from any native indicator callback +installed before Node.js initializes its crypto support. When +[`--force-fips=strict`][] is used, Node.js rejects callback-indicated +non-approved operations whether or not indicator events are enabled or the +channel has subscribers. + +Messages are published asynchronously on the main thread because OpenSSL +indicators can originate from Workers or other threads. Only subscriptions on +the main thread receive messages. Delivery order relative to the originating +operation is not defined, and a message cannot be correlated with a particular +call or Worker. + +One cryptographic operation can invoke the OpenSSL indicator more than once. +Matching pending invocations are coalesced and reflected in `count`, which does +not necessarily represent a number of cryptographic operations. At most 256 +distinct messages are queued. Additional invocations are reported in `dropped` +on the first queued message. Queued messages do not keep the event loop active, +so this channel is best-effort diagnostics rather than an authoritative audit +log. + +This channel observes the default OpenSSL library context used by Node.js. It +does not observe native addons or other code that uses another `OSSL_LIB_CTX` +or another copy of `libcrypto`. It is active with OpenSSL 3.4 and later and is +not available with BoringSSL. A provider configured to reject a check directly, +including a pedantic OpenSSL FIPS provider, can reject an operation without +emitting an indicator. Receiving or not receiving a message does not establish +that Node.js or a cryptographic operation is FIPS validated. + +```mjs +import diagnosticsChannel from 'node:diagnostics_channel'; + +diagnosticsChannel.subscribe('crypto.fips.indicator', (message) => { + console.error('Non-approved cryptographic operation', message); +}); +``` + #### HTTP > Stability: 1 - Experimental @@ -1501,8 +1567,47 @@ added: v16.18.0 Emitted when a new thread is created. +#### SQLite + + + +> Stability: 1 - Experimental + +##### Event: `'sqlite.db.query'` + +* `sql` {string} The expanded SQL with bound parameter values substituted. + If expansion fails, the source SQL with unsubstituted placeholders is used + instead. +* `database` {DatabaseSync} The [`DatabaseSync`][] instance that executed the + statement. +* `duration` {number} SQLite's internal estimate of the statement run time in + nanoseconds. This reflects C-layer execution time only and does not include + JavaScript binding overhead such as argument marshaling or result-row + construction. + +Emitted after a SQL statement finishes executing against a [`DatabaseSync`][] +instance. This is a **profiling** event: it fires once per statement upon +completion and reports an estimated duration from SQLite's internal profiler. +It is not a distributed-tracing span. There is no corresponding start event, +no async context propagation, and no parent-span linkage. If you need +OpenTelemetry-compatible spans or async context propagation, wrap your SQLite +calls with a [`TracingChannel`][] at the JavaScript layer instead. + +Publishing is zero-overhead when there are no subscribers. + +No event is emitted for a statement that is abandoned mid-iteration and later +finalized, either explicitly through [`statement.close()`][] or when the +statement is garbage collected. Subscribers must not close the database or the +statement, since both are still in use while the event is being delivered; see +[`database.close()`][] and [`statement.close()`][]. + [TracingChannel Channels]: #tracingchannel-channels [`'uncaughtException'`]: process.md#event-uncaughtexception +[`--enable-fips-indicator-events`]: cli.md#--enable-fips-indicator-events +[`--force-fips=strict`]: cli.md#--force-fipsmode +[`DatabaseSync`]: sqlite.md#class-databasesync [`TracingChannel`]: #class-tracingchannel [`asyncEnd` event]: #asyncendevent [`asyncStart` event]: #asyncstartevent @@ -1511,6 +1616,7 @@ Emitted when a new thread is created. [`channel.subscribe(onMessage)`]: #channelsubscribeonmessage [`channel.unsubscribe(onMessage)`]: #channelunsubscribeonmessage [`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options +[`database.close()`]: sqlite.md#databaseclose [`diagnostics_channel.channel(name)`]: #diagnostics_channelchannelname [`diagnostics_channel.subscribe(name, onMessage)`]: #diagnostics_channelsubscribename-onmessage [`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels @@ -1520,5 +1626,6 @@ Emitted when a new thread is created. [`net.Server.listen()`]: net.md#serverlisten [`process.execve()`]: process.md#processexecvefile-args-env [`start` event]: #startevent +[`statement.close()`]: sqlite.md#statementclose [`worker_threads.locks`]: worker_threads.md#worker_threadslocks [context loss]: async_context.md#troubleshooting-context-loss diff --git a/doc/api/errors.md b/doc/api/errors.md index 9b81ad526084..7a1f31e1ee42 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -928,7 +928,7 @@ be called no more than one time per instance of a `Hash` object. ### `ERR_CRYPTO_HASH_UPDATE_FAILED` -[`hash.update()`][] failed for any reason. This should rarely, if ever, happen. +[`hash.update()`][] failed for an unspecified reason. @@ -1044,6 +1044,16 @@ An invalid key type was provided. The given crypto key object's type is invalid for the attempted operation. + + +### `ERR_CRYPTO_INVALID_MAC` + + + +An invalid MAC algorithm was specified. + ### `ERR_CRYPTO_INVALID_MESSAGELEN` @@ -1117,6 +1127,37 @@ added: v24.7.0 Attempted to use KEM operations while Node.js was not compiled with OpenSSL with KEM support. + + +### `ERR_CRYPTO_MAC_FINALIZED` + + + +An operation was attempted on a `Mac` object after finalization was attempted +or an underlying MAC update failed. + + + +### `ERR_CRYPTO_MAC_NOT_SUPPORTED` + + + +Node.js was built without support for the OpenSSL `EVP_MAC` API. + + + +### `ERR_CRYPTO_MAC_UPDATE_FAILED` + + + +[`mac.update()`][] failed for an unspecified reason. + ### `ERR_CRYPTO_OPERATION_FAILED` @@ -4545,7 +4586,7 @@ An error occurred trying to allocate memory. This should never happen. [`"imports"`]: packages.md#imports [`'uncaughtException'`]: process.md#event-uncaughtexception [`--disable-proto=throw`]: cli.md#--disable-protomode -[`--force-fips`]: cli.md#--force-fips +[`--force-fips`]: cli.md#--force-fipsmode [`--no-addons`]: cli.md#--no-addons [`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode [`BoundSocket`]: net.md#class-netboundsocket @@ -4591,6 +4632,7 @@ An error occurred trying to allocate memory. This should never happen. [`http`]: http.md [`https`]: https.md [`libuv Error handling`]: https://docs.libuv.org/en/v1.x/errors.html +[`mac.update()`]: crypto.md#macupdatedata-inputencoding [`net.Server`]: net.md#class-netserver [`net.Socket.write()`]: net.md#socketwritedata-encoding-callback [`net.Socket`]: net.md#class-netsocket diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 691b7e40fd37..2e3636cb4994 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -30,7 +30,9 @@ import sqlite from 'node:sqlite'; const sqlite = require('node:sqlite'); ``` -This module is only available under the `node:` scheme. +This module is only available under the `node:` scheme. SQL trace events can +be observed via the [`diagnostics_channel`][] module. See +[`'sqlite.db.query'`][] for details. The following example shows the basic usage of the `node:sqlite` module to open an in-memory database, write data to the database, and then read the data back. @@ -300,8 +302,8 @@ added: v22.5.0 Closes the database connection. An exception is thrown if the database is not open. An [`ERR_INVALID_STATE`][] error is thrown if the method is called while a statement is executing, such as inside a user-defined function, an aggregate -function, or an authorizer callback. This method is a wrapper around -[`sqlite3_close_v2()`][]. +function, an authorizer callback, or a [`'sqlite.db.query'`][] subscriber. This +method is a wrapper around [`sqlite3_close_v2()`][]. ### `database.loadExtension(path[, entryPoint])` @@ -428,6 +430,11 @@ wrapper around [`sqlite3_create_function_v2()`][]. * `callback` {Function|null} The authorizer function to set, or `null` to @@ -453,6 +460,31 @@ The callback must return one of the following constants: * `SQLITE_DENY` - Deny the operation (causes an error). * `SQLITE_IGNORE` - Ignore the operation (silently skip). +SQLite requires that the authorizer callback not modify the database connection +that invoked it, which includes preparing and stepping statements. Methods that +would do so throw an error with code `ERR_INVALID_STATE` while the callback is +on the stack, including `database.prepare()`, `database.exec()`, the execution +methods of that connection's statements, iterators, and tag stores, and +`database.setAuthorizer()` itself. Other connections remain usable. + +The callback can also be invoked from within `statement.run()`, +`statement.get()`, and similar methods, because SQLite may re-prepare a +statement during execution after a schema change. + +Separately, a statement that is currently being executed cannot be reentered. +Calling `statement.close()` on it would free the virtual machine that is +running, and re-running it through `statement.run()`, `statement.get()`, +`statement.all()`, `statement.iterate()`, `iterator.next()`, +`iterator.return()`, or the equivalent tag store methods would reset that +virtual machine mid-execution. All of these throw an `ERR_INVALID_STATE` error +instead. This applies to any callback SQLite invokes during execution, such as a +user-defined function. Other statements on the connection remain usable. + +Operations that touch no SQLite state stay available from the callback: +`sqlTagStore.clear()`, which only drops cached statements, and `next()` and +`return()` on an already-drained iterator, which keep returning +`{ done: true }`. + ```cjs const { DatabaseSync, constants } = require('node:sqlite'); const db = new DatabaseSync(':memory:'); @@ -1057,6 +1089,20 @@ returns an empty array. The prepared statement [parameters are bound][] using the values in `namedParameters` and `anonymousParameters`. See [Binding parameters][]. +### `statement.close()` + + + +Finalizes the prepared statement. An exception is thrown if the statement is +already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement +is currently executing, which happens when the method is called from a callback +that the statement itself triggered, such as a user-defined function, an +aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements +on the same connection can be finalized from such a callback. This method is a +wrapper around [`sqlite3_finalize()`][]. + ### `statement.columns()` + +Finalizes the prepared statement. If the prepared statement is already +finalized, then this is a no-op. An [`ERR_INVALID_STATE`][] error is thrown if +this statement is currently executing, under the same conditions as +[`statement.close()`][]. + ## Class: `SQLTagStore` + + +Current Realm + + +
+ + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/allow-resizable.html b/test/fixtures/wpt/webidl/ecmascript-binding/allow-resizable.html new file mode 100644 index 000000000000..54daa57bce67 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/allow-resizable.html @@ -0,0 +1,31 @@ + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/allow-shared.https.html b/test/fixtures/wpt/webidl/ecmascript-binding/allow-shared.https.html new file mode 100644 index 000000000000..b4cd1e37fd94 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/allow-shared.https.html @@ -0,0 +1,29 @@ + + +WebIDL [AllowShared] semantics + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/attributes-accessors-unique-function-objects.html b/test/fixtures/wpt/webidl/ecmascript-binding/attributes-accessors-unique-function-objects.html new file mode 100644 index 000000000000..167f55bcef7e --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/attributes-accessors-unique-function-objects.html @@ -0,0 +1,35 @@ + + +All attributes accessors are unique function objects + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/builtin-function-properties.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/builtin-function-properties.any.js new file mode 100644 index 000000000000..885bb441ead4 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/builtin-function-properties.any.js @@ -0,0 +1,23 @@ +"use strict"; + +test(() => { + const ownPropKeys = Reflect.ownKeys(Blob).slice(0, 3); + assert_array_equals(ownPropKeys, ["length", "name", "prototype"]); +}, 'Constructor property enumeration order of "length", "name", and "prototype"'); + +test(() => { + assert_own_property(Blob.prototype, "slice"); + + const ownPropKeys = Reflect.ownKeys(Blob.prototype.slice).slice(0, 2); + assert_array_equals(ownPropKeys, ["length", "name"]); +}, 'Method property enumeration order of "length" and "name"'); + +test(() => { + assert_own_property(Blob.prototype, "size"); + + const desc = Reflect.getOwnPropertyDescriptor(Blob.prototype, "size"); + assert_equals(typeof desc.get, "function"); + + const ownPropKeys = Reflect.ownKeys(desc.get).slice(0, 2); + assert_array_equals(ownPropKeys, ["length", "name"]); +}, 'Getter property enumeration order of "length" and "name"'); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/class-string-interface.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/class-string-interface.any.js new file mode 100644 index 000000000000..ee792d536838 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/class-string-interface.any.js @@ -0,0 +1,62 @@ +"use strict"; + +test(() => { + assert_own_property(Blob.prototype, Symbol.toStringTag); + + const propDesc = Object.getOwnPropertyDescriptor(Blob.prototype, Symbol.toStringTag); + assert_equals(propDesc.value, "Blob", "value"); + assert_equals(propDesc.configurable, true, "configurable"); + assert_equals(propDesc.enumerable, false, "enumerable"); + assert_equals(propDesc.writable, false, "writable"); +}, "@@toStringTag exists on the prototype with the appropriate descriptor"); + +test(() => { + assert_not_own_property(new Blob(), Symbol.toStringTag); +}, "@@toStringTag must not exist on the instance"); + +test(() => { + assert_equals(Object.prototype.toString.call(Blob.prototype), "[object Blob]"); +}, "Object.prototype.toString applied to the prototype"); + +test(() => { + assert_equals(Object.prototype.toString.call(new Blob()), "[object Blob]"); +}, "Object.prototype.toString applied to an instance"); + +test(t => { + assert_own_property(Blob.prototype, Symbol.toStringTag, "Precondition for this test: @@toStringTag on the prototype"); + + t.add_cleanup(() => { + Object.defineProperty(Blob.prototype, Symbol.toStringTag, { value: "Blob" }); + }); + + Object.defineProperty(Blob.prototype, Symbol.toStringTag, { value: "NotABlob" }); + assert_equals(Object.prototype.toString.call(Blob.prototype), "[object NotABlob]", "prototype"); + assert_equals(Object.prototype.toString.call(new Blob()), "[object NotABlob]", "instance"); +}, "Object.prototype.toString applied after modifying the prototype's @@toStringTag"); + +test(t => { + const instance = new Blob(); + assert_not_own_property(instance, Symbol.toStringTag, "Precondition for this test: no @@toStringTag on the instance"); + + Object.defineProperty(instance, Symbol.toStringTag, { value: "NotABlob" }); + assert_equals(Object.prototype.toString.call(instance), "[object NotABlob]"); +}, "Object.prototype.toString applied to the instance after modifying the instance's @@toStringTag"); + +// Chrome had a bug (https://bugs.chromium.org/p/chromium/issues/detail?id=793406) where if there +// was no @@toStringTag in the prototype, it would fall back to a magic class string. This tests +// that the bug is fixed. + +test(() => { + const instance = new Blob(); + Object.setPrototypeOf(instance, null); + + assert_equals(Object.prototype.toString.call(instance), "[object Object]"); +}, "Object.prototype.toString applied to a null-prototype instance"); + +// This test must be last. +test(() => { + delete Blob.prototype[Symbol.toStringTag]; + + assert_equals(Object.prototype.toString.call(Blob.prototype), "[object Object]", "prototype"); + assert_equals(Object.prototype.toString.call(new Blob()), "[object Object]", "instance"); +}, "Object.prototype.toString applied after deleting @@toStringTag"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/class-string-iterator-prototype-object.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/class-string-iterator-prototype-object.any.js new file mode 100644 index 000000000000..5ca549d69cff --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/class-string-iterator-prototype-object.any.js @@ -0,0 +1,51 @@ +"use strict"; + +const iteratorProto = Object.getPrototypeOf((new URLSearchParams()).entries()); + +test(() => { + assert_own_property(iteratorProto, Symbol.toStringTag); + + const propDesc = Object.getOwnPropertyDescriptor(iteratorProto, Symbol.toStringTag); + assert_equals(propDesc.value, "URLSearchParams Iterator", "value"); + assert_equals(propDesc.configurable, true, "configurable"); + assert_equals(propDesc.enumerable, false, "enumerable"); + assert_equals(propDesc.writable, false, "writable"); +}, "@@toStringTag exists with the appropriate descriptor"); + +test(() => { + assert_equals(Object.prototype.toString.call(iteratorProto), "[object URLSearchParams Iterator]"); +}, "Object.prototype.toString"); + +test(t => { + assert_own_property(iteratorProto, Symbol.toStringTag, "Precondition for this test: @@toStringTag exists"); + + t.add_cleanup(() => { + Object.defineProperty(iteratorProto, Symbol.toStringTag, { value: "URLSearchParams Iterator" }); + }); + + Object.defineProperty(iteratorProto, Symbol.toStringTag, { value: "Not URLSearchParams Iterator" }); + assert_equals(Object.prototype.toString.call(iteratorProto), "[object Not URLSearchParams Iterator]"); +}, "Object.prototype.toString applied after modifying @@toStringTag"); + +// Chrome had a bug (https://bugs.chromium.org/p/chromium/issues/detail?id=793406) where if there +// was no @@toStringTag, it would fall back to a magic class string. This tests that the bug is +// fixed. + +test(() => { + const iterator = (new URLSearchParams()).keys(); + assert_equals(Object.prototype.toString.call(iterator), "[object URLSearchParams Iterator]"); + + Object.setPrototypeOf(iterator, null); + assert_equals(Object.prototype.toString.call(iterator), "[object Object]"); +}, "Object.prototype.toString applied to a null-prototype instance"); + +test(t => { + const proto = Object.getPrototypeOf(iteratorProto); + t.add_cleanup(() => { + Object.setPrototypeOf(iteratorProto, proto); + }); + + Object.setPrototypeOf(iteratorProto, null); + + assert_equals(Object.prototype.toString.call(iteratorProto), "[object URLSearchParams Iterator]"); +}, "Object.prototype.toString applied after nulling the prototype"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/class-string-named-properties-object.window.js b/test/fixtures/wpt/webidl/ecmascript-binding/class-string-named-properties-object.window.js new file mode 100644 index 000000000000..a427a2f8142e --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/class-string-named-properties-object.window.js @@ -0,0 +1,23 @@ +"use strict"; + +const namedPropertiesObject = Object.getPrototypeOf(Window.prototype); + +test(() => { + assert_own_property(namedPropertiesObject, Symbol.toStringTag); + + const propDesc = Object.getOwnPropertyDescriptor(namedPropertiesObject, Symbol.toStringTag); + assert_equals(propDesc.value, "WindowProperties", "value"); + assert_equals(propDesc.configurable, true, "configurable"); + assert_equals(propDesc.enumerable, false, "enumerable"); + assert_equals(propDesc.writable, false, "writable"); +}, "@@toStringTag exists with the appropriate descriptor"); + +test(() => { + assert_equals(Object.prototype.toString.call(namedPropertiesObject), "[object WindowProperties]"); +}, "Object.prototype.toString"); + +// Chrome had a bug (https://bugs.chromium.org/p/chromium/issues/detail?id=793406) where if there +// was no @@toStringTag, it would fall back to a magic class string. Tests for this are present in +// the sibling class-string*.any.js tests. However, the named properties object always fails calls +// to [[DefineOwnProperty]] or [[SetPrototypeOf]] per the Web IDL spec, so there is no way to +// trigger the buggy behavior for it. diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/constructors.html b/test/fixtures/wpt/webidl/ecmascript-binding/constructors.html new file mode 100644 index 000000000000..61993a6200ed --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/constructors.html @@ -0,0 +1,132 @@ + + +Realm for constructed objects + + +
+ diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/default-iterator-object.html b/test/fixtures/wpt/webidl/ecmascript-binding/default-iterator-object.html new file mode 100644 index 000000000000..c7e9188521a2 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/default-iterator-object.html @@ -0,0 +1,27 @@ + + +Default iterator objects + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/default-toJSON-cross-realm.html b/test/fixtures/wpt/webidl/ecmascript-binding/default-toJSON-cross-realm.html new file mode 100644 index 000000000000..79c3097f339b --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/default-toJSON-cross-realm.html @@ -0,0 +1,26 @@ + + +Cross-realm [Default] toJSON() creates result object in its realm + + + + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constants.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constants.any.js index 9b6978783723..856d13681e60 100644 --- a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constants.any.js +++ b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constants.any.js @@ -1,4 +1,4 @@ -// META: global=window,dedicatedworker,shadowrealm +// META: global=window,dedicatedworker 'use strict'; diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-and-prototype.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-and-prototype.any.js index 011521652654..ca0fc84fc66a 100644 --- a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-and-prototype.any.js +++ b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-and-prototype.any.js @@ -1,4 +1,4 @@ -// META: global=window,dedicatedworker,shadowrealm +// META: global=window,dedicatedworker test(function() { assert_own_property(self, "DOMException", "property of global"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-behavior.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-behavior.any.js index c4ddabdafd45..10aad220b917 100644 --- a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-behavior.any.js +++ b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-constructor-behavior.any.js @@ -1,4 +1,4 @@ -// META: global=window,dedicatedworker,shadowrealm +// META: global=window,dedicatedworker 'use strict'; diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js index d1c86930d4cd..467bf054c3a9 100644 --- a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js +++ b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js @@ -1,4 +1,4 @@ -// META: global=window,dedicatedworker,shadowrealm +// META: global=window,dedicatedworker "use strict"; diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-is-error.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-is-error.any.js index 6f3097b222ed..dc39c978217b 100644 --- a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-is-error.any.js +++ b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-is-error.any.js @@ -1,4 +1,4 @@ -// META: global=window,dedicatedworker,shadowrealm +// META: global=window,dedicatedworker 'use strict'; diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-stack-accessor.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-stack-accessor.any.js new file mode 100644 index 000000000000..24f6fd486bdc --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/es-exceptions/DOMException-stack-accessor.any.js @@ -0,0 +1,80 @@ +// META: global=window,dedicatedworker + +// https://tc39.es/proposal-error-stack-accessor/ +// https://github.com/whatwg/webidl/pull/1421 + +"use strict"; + +test(() => { + const e = new DOMException("some message", "SyntaxError"); + assert_equals(typeof e.stack, "string", "stack must be a string"); +}, "new DOMException() has a stack property that is a string"); + +test(() => { + const e = new DOMException(); + assert_equals(typeof e.stack, "string", "stack must be a string"); +}, "new DOMException() with no arguments has a stack property that is a string"); + +if (typeof document !== "undefined") { + test(() => { + let caught; + try { + document.createElement(""); + } catch (e) { + caught = e; + } + assert_true(caught instanceof DOMException, "must be a DOMException"); + assert_equals(typeof caught.stack, "string", "stack must be a string"); + }, "thrown DOMException from DOM API has a stack property that is a string"); +} + +test(() => { + const e = new DOMException("some message", "SyntaxError"); + assert_false(e.hasOwnProperty("stack"), "stack must not be an own property of the instance"); +}, "DOMException instance does not have an own stack property"); + +test(() => { + assert_false(DOMException.prototype.hasOwnProperty("stack"), + "DOMException.prototype must not have an own stack property"); +}, "DOMException.prototype does not have an own stack property"); + +test(() => { + const desc = Object.getOwnPropertyDescriptor(Error.prototype, "stack"); + assert_not_equals(desc, undefined, "Error.prototype must have a stack property descriptor"); + assert_equals(typeof desc.get, "function", "stack must have a getter"); + assert_equals(typeof desc.set, "function", "stack must have a setter"); + assert_false(desc.enumerable, "stack must not be enumerable"); + assert_true(desc.configurable, "stack must be configurable"); +}, "Error.prototype.stack is an accessor property with correct attributes"); + +test(() => { + const getter = Object.getOwnPropertyDescriptor(Error.prototype, "stack").get; + const e = new DOMException("some message", "SyntaxError"); + const stack = getter.call(e); + assert_equals(typeof stack, "string", "getter must return a string for DOMException"); + assert_equals(stack, e.stack, "getter result must match e.stack"); +}, "Error.prototype.stack getter works on DOMException instances"); + +test(() => { + const setter = Object.getOwnPropertyDescriptor(Error.prototype, "stack").set; + const e = new DOMException("some message", "SyntaxError"); + setter.call(e, "custom stack"); + assert_true(e.hasOwnProperty("stack"), "after setter, stack must be an own property"); + assert_equals(e.stack, "custom stack", "own stack property must have the set value"); + + const desc = Object.getOwnPropertyDescriptor(e, "stack"); + assert_equals(desc.value, "custom stack", "must be a data property"); + assert_true(desc.writable, "must be writable"); + assert_true(desc.enumerable, "must be enumerable"); + assert_true(desc.configurable, "must be configurable"); +}, "Error.prototype.stack setter installs own data property on DOMException instances"); + +test(() => { + const setter = Object.getOwnPropertyDescriptor(Error.prototype, "stack").set; + // SetterThatIgnoresPrototypeProperties should not install a property on Error.prototype itself + const originalStack = Object.getOwnPropertyDescriptor(Error.prototype, "stack"); + setter.call(Error.prototype, "custom stack"); + const afterStack = Object.getOwnPropertyDescriptor(Error.prototype, "stack"); + assert_equals(typeof afterStack.get, "function", + "Error.prototype.stack must still be an accessor after calling setter on it"); +}, "Error.prototype.stack setter ignores Error.prototype itself"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/global-immutable-prototype.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/global-immutable-prototype.any.js new file mode 100644 index 000000000000..6291c3ae9356 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/global-immutable-prototype.any.js @@ -0,0 +1,25 @@ +// META: global=window,worker +// META: title=Immutability of the global prototype chain + +const objects = []; +setup(() => { + for (let object = self; object; object = Object.getPrototypeOf(object)) { + objects.push(object); + } +}); + +test(() => { + for (const object of objects) { + assert_throws_js(TypeError, () => { + Object.setPrototypeOf(object, {}); + }); + } +}, "Setting to a different prototype"); + +test(() => { + for (const object of objects) { + const expected = Object.getPrototypeOf(object); + Object.setPrototypeOf(object, expected); + assert_equals(Object.getPrototypeOf(object), expected); + } +}, "Setting to the same prototype"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html b/test/fixtures/wpt/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html new file mode 100644 index 000000000000..b9939b801cbd --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html @@ -0,0 +1,97 @@ + + +Cross-realm getter / setter / operation doesn't use lexical global object if |this| value is incompatible object / null / undefined + + + + + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/global-object-implicit-this-value.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/global-object-implicit-this-value.any.js new file mode 100644 index 000000000000..4c159c67519c --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/global-object-implicit-this-value.any.js @@ -0,0 +1,85 @@ +// META: global=window,worker + +// https://webidl.spec.whatwg.org/#dfn-attribute-getter (step 1.1.2.1) +// https://webidl.spec.whatwg.org/#dfn-attribute-setter (step 4.5.1) +// https://webidl.spec.whatwg.org/#dfn-create-operation-function (step 2.1.2.1) + +const notGlobalObject = Object.create(Object.getPrototypeOf(globalThis)); + +test(() => { + assert_throws_js(TypeError, () => { Object.create(globalThis).self; }); + assert_throws_js(TypeError, () => { getGlobalPropertyDescriptor("location").get.call(notGlobalObject); }); + assert_throws_js(TypeError, () => { Reflect.get(globalThis, "navigator", notGlobalObject); }); + assert_throws_js(TypeError, () => { new Proxy(globalThis, {}).onerror; }); +}, "Global object's getter throws when called on incompatible object"); + +test(() => { + assert_throws_js(TypeError, () => { Object.create(globalThis).origin = origin; }); + assert_throws_js(TypeError, () => { getGlobalPropertyDescriptor("onerror").set.call(notGlobalObject, onerror); }); + assert_throws_js(TypeError, () => { Reflect.set(globalThis, "onoffline", onoffline, notGlobalObject); }); + assert_throws_js(TypeError, () => { new Proxy(globalThis, {}).ononline = ononline; }); +}, "Global object's setter throws when called on incompatible object"); + +test(() => { + assert_throws_js(TypeError, () => { Object.create(globalThis).setInterval(() => {}, 100); }); + assert_throws_js(TypeError, () => { clearTimeout.call(notGlobalObject, () => {}); }); + assert_throws_js(TypeError, () => { Reflect.apply(btoa, notGlobalObject, [""]); }); + assert_throws_js(TypeError, () => { new Proxy(globalThis, {}).removeEventListener("foo", () => {}); }); +}, "Global object's operation throws when called on incompatible object"); + +if (typeof document !== "undefined") { + test(() => { + assert_throws_js(TypeError, () => { Object.getOwnPropertyDescriptor(window, "document").get.call(document.all); }); + }, "Global object's getter throws when called on incompatible object (document.all)"); + + test(() => { + assert_throws_js(TypeError, () => { Object.getOwnPropertyDescriptor(window, "name").set.call(document.all); }); + }, "Global object's setter throws when called on incompatible object (document.all)"); + + test(() => { + assert_throws_js(TypeError, () => { focus.call(document.all); }); + }, "Global object's operation throws when called on incompatible object (document.all)"); +} + +// An engine might have different code path for calling a function from outer scope to implement step 1.b.iii of https://tc39.es/ecma262/#sec-evaluatecall +const locationGetter = getGlobalPropertyDescriptor("location").get; +test(() => { + assert_equals(getGlobalPropertyDescriptor("self").get.call(null), self); + assert_equals((() => locationGetter())(), location); + assert_equals(Reflect.get(globalThis, "origin", null), origin); + assert_equals(Reflect.get(globalThis, "onoffline", undefined), onoffline); +}, "Global object's getter works when called on null / undefined"); + +test(() => { + const fn = () => {}; + + // origin is [Replaceable] + getGlobalPropertyDescriptor("origin").set.call(null, "foo"); + assert_equals(origin, "foo"); + getGlobalPropertyDescriptor("onerror").set.call(undefined, fn); + assert_equals(onerror, fn); + assert_true(Reflect.set(globalThis, "onoffline", fn, null)); + assert_equals(onoffline, fn); + + const ononlineSetter = getGlobalPropertyDescriptor("ononline").set; + (() => { ononlineSetter(fn); })(); + assert_equals(ononline, fn); +}, "Global object's setter works when called on null / undefined"); + +// An engine might have different code path for calling a function from outer scope to implement step 1.b.iii of https://tc39.es/ecma262/#sec-evaluatecall +const __addEventListener = addEventListener; +test(() => { + assert_equals(atob.call(null, ""), ""); + assert_equals(typeof (0, setInterval)(() => {}, 100), "number"); + + (() => { __addEventListener("foo", event => { event.preventDefault(); }); })(); + const __dispatchEvent = dispatchEvent; + (() => { assert_false(__dispatchEvent(new Event("foo", { cancelable: true }))); })(); +}, "Global object's operation works when called on null / undefined"); + +function getGlobalPropertyDescriptor(key) { + for (let obj = globalThis; obj; obj = Object.getPrototypeOf(obj)) { + const desc = Object.getOwnPropertyDescriptor(obj, key); + if (desc) return desc; + } +} diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/has-instance.html b/test/fixtures/wpt/webidl/ecmascript-binding/has-instance.html new file mode 100644 index 000000000000..caf0be472906 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/has-instance.html @@ -0,0 +1,26 @@ + + +instanceof behavior + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/interface-object-set-receiver.html b/test/fixtures/wpt/webidl/ecmascript-binding/interface-object-set-receiver.html new file mode 100644 index 000000000000..ca75a96bbad5 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/interface-object-set-receiver.html @@ -0,0 +1,37 @@ + + +window.Interface is defined on [[Set]] receiver + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/interface-object.html b/test/fixtures/wpt/webidl/ecmascript-binding/interface-object.html new file mode 100644 index 000000000000..132c61ddaed4 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/interface-object.html @@ -0,0 +1,28 @@ + + +Interface objects + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/interface-prototype-constructor-set-receiver.html b/test/fixtures/wpt/webidl/ecmascript-binding/interface-prototype-constructor-set-receiver.html new file mode 100644 index 000000000000..64a2da8eb2da --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/interface-prototype-constructor-set-receiver.html @@ -0,0 +1,36 @@ + + +Interface.prototype.constructor is defined on [[Set]] receiver + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/interface-prototype-object.html b/test/fixtures/wpt/webidl/ecmascript-binding/interface-prototype-object.html new file mode 100644 index 000000000000..299bcf926dc2 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/interface-prototype-object.html @@ -0,0 +1,15 @@ + + +Interface prototype objects + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/invalid-this-value-cross-realm.html b/test/fixtures/wpt/webidl/ecmascript-binding/invalid-this-value-cross-realm.html new file mode 100644 index 000000000000..0535115ac61f --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/invalid-this-value-cross-realm.html @@ -0,0 +1,45 @@ + + +Cross-realm getter / setter / operation doesn't use lexical global object to throw an error for incompatible |this| value + + + + + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/iterator-invalidation-foreach.html b/test/fixtures/wpt/webidl/ecmascript-binding/iterator-invalidation-foreach.html new file mode 100644 index 000000000000..9d2e3b9cb25c --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/iterator-invalidation-foreach.html @@ -0,0 +1,40 @@ + + +Behavior of iterators when modified within foreach + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/iterator-prototype-object.html b/test/fixtures/wpt/webidl/ecmascript-binding/iterator-prototype-object.html new file mode 100644 index 000000000000..7859c1e46ac4 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/iterator-prototype-object.html @@ -0,0 +1,47 @@ + + +Iterator prototype objects + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-callback-interface-object.html b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-callback-interface-object.html new file mode 100644 index 000000000000..627d29507f7d --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-callback-interface-object.html @@ -0,0 +1,69 @@ + + +Legacy callback interface objects + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-factor-function-subclass.window.js b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-factor-function-subclass.window.js new file mode 100644 index 000000000000..1fd64f41bb2e --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-factor-function-subclass.window.js @@ -0,0 +1,13 @@ +"use strict"; + +test(() => { + class CustomImage extends Image {} + var instance = new CustomImage(); + + assert_equals( + Object.getPrototypeOf(instance), CustomImage.prototype, + "Object.getPrototypeOf(instance) === CustomImage.prototype"); + + assert_true(instance instanceof CustomImage, "instance instanceof CustomImage"); + assert_true(instance instanceof HTMLImageElement, "instance instanceof HTMLImageElement"); +}, "[LegacyFactoryFunction] can be subclassed and correctly handles NewTarget"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js new file mode 100644 index 000000000000..fc5c48aca380 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js @@ -0,0 +1,6 @@ +"use strict"; + +test(() => { + const ownPropKeys = Reflect.ownKeys(Image).slice(0, 3); + assert_array_equals(ownPropKeys, ["length", "name", "prototype"]); +}, 'Legacy factory function property enumeration order of "length", "name", and "prototype"'); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/DefineOwnProperty.html b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/DefineOwnProperty.html new file mode 100644 index 000000000000..bd7ba19c1a90 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/DefineOwnProperty.html @@ -0,0 +1,165 @@ + + +Legacy platform objects [[DefineOwnProperty]] method + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/GetOwnProperty.html b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/GetOwnProperty.html new file mode 100644 index 000000000000..be3bcc61f0a3 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/GetOwnProperty.html @@ -0,0 +1,84 @@ + + +Legacy platform objects [[GetOwnProperty]] method + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/OwnPropertyKeys.html b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/OwnPropertyKeys.html new file mode 100644 index 000000000000..d33980517b1a --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/OwnPropertyKeys.html @@ -0,0 +1,65 @@ + + +Legacy platform objects [[OwnPropertyKeys]] method + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/Set.html b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/Set.html new file mode 100644 index 000000000000..1390b51cd03b --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/Set.html @@ -0,0 +1,94 @@ + + +Legacy platform objects [[Set]] method + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/helper.js b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/helper.js new file mode 100644 index 000000000000..01c1d00694eb --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/legacy-platform-object/helper.js @@ -0,0 +1,22 @@ +function assert_prop_desc_equals(object, property_key, expected) { + let actual = Object.getOwnPropertyDescriptor(object, property_key); + if (expected === undefined) { + assert_equals( + actual, undefined, + "(assert_prop_desc_equals: no property descriptor expected)"); + return; + } + for (p in actual) { + assert_true( + expected.hasOwnProperty(p), + "(assert_prop_desc_equals: property '" + p + "' is not expected)"); + assert_equals( + actual[p], expected[p], + "(assert_prop_desc_equals: property '" + p + "')"); + } + for (p in expected) { + assert_true( + actual.hasOwnProperty(p), + "(assert_prop_desc_equals: expected property '" + p + "' missing)"); + } +} diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/no-regexp-special-casing.any.js b/test/fixtures/wpt/webidl/ecmascript-binding/no-regexp-special-casing.any.js new file mode 100644 index 000000000000..4446dbf69c02 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/no-regexp-special-casing.any.js @@ -0,0 +1,47 @@ +"use strict"; +// RegExps used to be special-cased in Web IDL, but that was removed in +// https://github.com/heycam/webidl/commit/bbb2bde. These tests check that implementations no longer +// do any such special-casing. + +test(() => { + const regExp = new RegExp(); + regExp.message = "some message"; + + const errorEvent = new ErrorEvent("type", regExp); + + assert_equals(errorEvent.message, "some message"); +}, "Conversion to a dictionary works"); + +test(() => { + const messageChannel = new MessageChannel(); + const regExp = new RegExp(); + regExp[Symbol.iterator] = function* () { + yield messageChannel.port1; + }; + + const messageEvent = new MessageEvent("type", { ports: regExp }); + + assert_array_equals(messageEvent.ports, [messageChannel.port1]); +}, "Conversion to a sequence works"); + +promise_test(async () => { + const regExp = new RegExp(); + + const response = new Response(regExp); + + assert_equals(await response.text(), "/(?:)/"); +}, "Can convert a RegExp to a USVString"); + +test(() => { + let functionCalled = false; + + const regExp = new RegExp(); + regExp.handleEvent = () => { + functionCalled = true; + }; + + self.addEventListener("testevent", regExp); + self.dispatchEvent(new Event("testevent")); + + assert_true(functionCalled); +}, "Can be used as an object implementing a callback interface"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/observable-array-no-leak-of-internals.window.js b/test/fixtures/wpt/webidl/ecmascript-binding/observable-array-no-leak-of-internals.window.js new file mode 100644 index 000000000000..f93464005d01 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/observable-array-no-leak-of-internals.window.js @@ -0,0 +1,18 @@ +"use strict"; + +test(() => { + const observableArray = document.adoptedStyleSheets; + + let leaked_target = null; + let leaked_handler = null; + + let target_leaker = (target) => { leaked_target = target; return null; }; + Object.defineProperty(Object.prototype, "getPrototypeOf", {get: function() { + leaked_handler = this; + return target_leaker; + }}) + Object.getPrototypeOf(observableArray); + + assert_equals(leaked_target, null, "The proxy target leaked."); + assert_equals(leaked_handler, null, "The proxy handler leaked."); +}, "ObservableArray's internals won't leak"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/observable-array-ownkeys.window.js b/test/fixtures/wpt/webidl/ecmascript-binding/observable-array-ownkeys.window.js new file mode 100644 index 000000000000..29b537c4750a --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/observable-array-ownkeys.window.js @@ -0,0 +1,34 @@ +"use strict"; + +test(() => { + const observableArray = document.adoptedStyleSheets; + assert_array_equals( + Object.getOwnPropertyNames(observableArray), + ["length"], + "Initially only \"length\"."); + + observableArray["zzz"] = true; + observableArray["aaa"] = true; + assert_array_equals( + Object.getOwnPropertyNames(observableArray), + ["length", "zzz", "aaa"], + "Own properties whose key is a string have been added."); + + observableArray[0] = new CSSStyleSheet(); + observableArray[1] = new CSSStyleSheet(); + assert_array_equals( + Object.getOwnPropertyNames(observableArray), + ["0", "1", "length", "zzz", "aaa"], + "Own properties whose key is an array index have been added."); + + observableArray[Symbol.toStringTag] = "string_tag"; + observableArray[Symbol.toPrimitive] = "primitive"; + assert_array_equals( + Object.getOwnPropertyNames(observableArray), + ["0", "1", "length", "zzz", "aaa"], + "Own properties whose key is a symbol have been added (non-symbol)."); + assert_array_equals( + Object.getOwnPropertySymbols(observableArray), + [Symbol.toStringTag, Symbol.toPrimitive], + "Own properties whose key is a symbol have been added (symbol)."); +}, "ObservableArray's ownKeys trap"); diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/put-forwards.html b/test/fixtures/wpt/webidl/ecmascript-binding/put-forwards.html new file mode 100644 index 000000000000..7d99d65aa213 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/put-forwards.html @@ -0,0 +1,148 @@ + + +[PutForwards] behavior + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/replaceable-setter-throws-if-defineownproperty-fails.html b/test/fixtures/wpt/webidl/ecmascript-binding/replaceable-setter-throws-if-defineownproperty-fails.html new file mode 100644 index 000000000000..872bbff96042 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/replaceable-setter-throws-if-defineownproperty-fails.html @@ -0,0 +1,38 @@ + + +[Replaceable] setter throws TypeError if [[DefineOwnProperty]] fails + + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/sequence-conversion.html b/test/fixtures/wpt/webidl/ecmascript-binding/sequence-conversion.html new file mode 100644 index 000000000000..40764e9f5776 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/sequence-conversion.html @@ -0,0 +1,157 @@ + + +Sequence conversion + + + + + + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/setter-argument.html b/test/fixtures/wpt/webidl/ecmascript-binding/setter-argument.html new file mode 100644 index 000000000000..bfa4291b2365 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/setter-argument.html @@ -0,0 +1,176 @@ + + +Setter should treat no arguments as undefined + + + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/support/constructors-support.html b/test/fixtures/wpt/webidl/ecmascript-binding/support/constructors-support.html new file mode 100644 index 000000000000..3b2616170b1d --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/support/constructors-support.html @@ -0,0 +1,8 @@ + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/support/create-realm.js b/test/fixtures/wpt/webidl/ecmascript-binding/support/create-realm.js new file mode 100644 index 000000000000..45ded884fc1f --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/support/create-realm.js @@ -0,0 +1,12 @@ +"use strict"; + +function createRealm(t) { + return new Promise(resolve => { + const iframe = document.createElement("iframe"); + t.add_cleanup(() => { iframe.remove(); }); + iframe.onload = () => { resolve(iframe.contentWindow); }; + iframe.name = "dummy"; + iframe.src = "support/dummy-iframe.html"; + document.body.append(iframe); + }); +} diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/support/dummy-iframe.html b/test/fixtures/wpt/webidl/ecmascript-binding/support/dummy-iframe.html new file mode 100644 index 000000000000..3f773ae6f811 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/support/dummy-iframe.html @@ -0,0 +1,7 @@ + + +foo + + diff --git a/test/fixtures/wpt/webidl/ecmascript-binding/window-named-properties-object.html b/test/fixtures/wpt/webidl/ecmascript-binding/window-named-properties-object.html new file mode 100644 index 000000000000..cc4976890683 --- /dev/null +++ b/test/fixtures/wpt/webidl/ecmascript-binding/window-named-properties-object.html @@ -0,0 +1,284 @@ + + +Internal methods of Window's named properties object + + + + + + diff --git a/test/fixtures/wpt/webidl/idlharness.any.js b/test/fixtures/wpt/webidl/idlharness.any.js new file mode 100644 index 000000000000..164fa0e65c8d --- /dev/null +++ b/test/fixtures/wpt/webidl/idlharness.any.js @@ -0,0 +1,17 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: global=window,dedicatedworker + +"use strict"; + +idl_test( + ['webidl'], + [], + idl_array => { + idl_array.add_objects({ + DOMException: ['new DOMException()', + 'new DOMException("my message")', + 'new DOMException("my message", "myName")'] + }); + } +); diff --git a/test/parallel/test-cli-node-print-help.js b/test/parallel/test-cli-node-print-help.js index f42129eb1330..84704be2104a 100644 --- a/test/parallel/test-cli-node-print-help.js +++ b/test/parallel/test-cli-node-print-help.js @@ -27,7 +27,8 @@ function validateNodePrintHelp() { { compileConstant: HAVE_OPENSSL, flags: [ '--openssl-config=...', '--tls-cipher-list=...', '--use-bundled-ca', '--use-openssl-ca', '--use-system-ca', - '--enable-fips', '--force-fips' ] }, + '--enable-fips', '--enable-fips-indicator-events', + '--force-fips' ] }, { compileConstant: NODE_HAVE_I18N_SUPPORT, flags: [ '--icu-data-dir=...', 'NODE_ICU_DATA' ] }, { compileConstant: HAVE_INSPECTOR, diff --git a/test/parallel/test-crypto-aes-wrap.js b/test/parallel/test-crypto-aes-wrap.js index 951e93d728e3..b6561524dc92 100644 --- a/test/parallel/test-crypto-aes-wrap.js +++ b/test/parallel/test-crypto-aes-wrap.js @@ -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/); + } + } +} diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index 082e86a669b0..db1d12ae6711 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -29,7 +29,7 @@ const assert = require('assert'); const crypto = require('crypto'); const { inspect } = require('util'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); const isFipsEnabled = crypto.getFips(); @@ -62,8 +62,9 @@ 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); let options; if (isCCM || isOCB) @@ -77,6 +78,7 @@ for (const test of TEST_CASES) { plaintextLength: Buffer.from(test.plain, inputEncoding).length }; } + const aads = test.aads ?? (test.aad === undefined ? [] : [test.aad]); { const encrypt = crypto.createCipheriv(test.algo, @@ -84,8 +86,9 @@ for (const test of TEST_CASES) { Buffer.from(test.iv, 'hex'), options); - if (test.aad) - encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); + for (const aad of aads) { + encrypt.setAAD(Buffer.from(aad, 'hex'), aadOptions); + } let hex = encrypt.update(test.plain, inputEncoding, 'hex'); hex += encrypt.final('hex'); @@ -112,8 +115,9 @@ for (const test of TEST_CASES) { Buffer.from(test.iv, 'hex'), options); decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); - if (test.aad) - decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); + for (const aad of aads) { + decrypt.setAAD(Buffer.from(aad, 'hex'), aadOptions); + } const outputEncoding = test.plainIsHex ? 'hex' : 'ascii'; @@ -144,7 +148,7 @@ for (const test of TEST_CASES) { crypto.createCipheriv( test.algo, Buffer.from(test.key, 'hex'), - Buffer.alloc(0) + isSIV ? Buffer.alloc(1) : Buffer.alloc(0) ); }, errMessages.length); } @@ -203,6 +207,161 @@ for (const test of TEST_CASES) { } } +// SIV and GCM-SIV use fixed 16-byte authentication tags. +{ + for (const { algo, key, iv } of [ + { + algo: 'aes-128-siv', + key: Buffer.alloc(32), + iv: null + }, + { + algo: 'aes-128-gcm-siv', + key: Buffer.alloc(16), + iv: Buffer.alloc(12) + }, + ]) { + if (!ciphers.includes(algo)) { + common.printSkipMessage(`unsupported ${algo} test`); + continue; + } + + // OpenSSL 3.5 added support for zero-length SIV messages. + const supportsEmptyPlaintext = hasOpenSSL(3, 5); + + for (const authTagLength of [1, 15, 17]) { + assert.throws(() => { + crypto.createCipheriv(algo, key, iv, { authTagLength }); + }, errMessages.authTagLength); + + assert.throws(() => { + crypto.createDecipheriv(algo, key, iv, { authTagLength }); + }, errMessages.authTagLength); + } + + if (algo === 'aes-128-siv') { + const cipher = crypto.createCipheriv(algo, key, iv); + for (let i = 0; i < 126; i++) { + cipher.setAAD(Buffer.alloc(0)); + } + assert.throws(() => { + cipher.setAAD(Buffer.alloc(0)); + }, errMessages.state); + cipher.update(Buffer.alloc(1)); + cipher.final(); + } + + { + const cipher = crypto.createCipheriv(algo, key, iv); + cipher.update('a'); + assert.throws(() => { + cipher.update('b'); + }, /Trying to add data in unsupported state/); + } + + { + const cipher = crypto.createCipheriv(algo, key, iv); + const ciphertext = cipher.update('authenticated plaintext'); + assert.throws(() => { + cipher.setAAD(Buffer.from('too late')); + }, errMessages.state); + cipher.final(); + + const decipher = crypto.createDecipheriv(algo, key, iv); + decipher.setAuthTag(cipher.getAuthTag()); + const plaintext = decipher.update(ciphertext); + assert.throws(() => { + decipher.setAAD(Buffer.from('too late')); + }, errMessages.state); + assert.strictEqual( + Buffer.concat([plaintext, decipher.final()]).toString(), + 'authenticated plaintext'); + } + + { + const cipher = crypto.createCipheriv(algo, key, iv); + const ciphertext = cipher.update('authenticated plaintext'); + cipher.final(); + + const decipher = crypto.createDecipheriv(algo, key, iv); + decipher.update(ciphertext); + assert.throws(() => { + decipher.setAuthTag(cipher.getAuthTag()); + }, errMessages.state); + assert.throws(() => { + decipher.final(); + }, errMessages.auth); + } + + { + const cipher = crypto.createCipheriv(algo, key, iv); + assert.throws(() => { + cipher.final(); + }, errMessages.auth); + assert.throws(() => { + cipher.update('too late'); + }, errMessages.state); + assert.throws(() => { + cipher.final(); + }, errMessages.state); + } + + if (supportsEmptyPlaintext) { + const cipher = crypto.createCipheriv(algo, key, iv); + const ciphertext = Buffer.concat([ + cipher.update(Buffer.alloc(0)), + cipher.final(), + ]); + assert.strictEqual(ciphertext.length, 0); + + const decipher = crypto.createDecipheriv(algo, key, iv); + decipher.setAuthTag(cipher.getAuthTag()); + assert.throws(() => { + decipher.final(); + }, errMessages.auth); + assert.throws(() => { + decipher.update(Buffer.alloc(0)); + }, errMessages.state); + assert.throws(() => { + decipher.final(); + }, errMessages.state); + } + + if (supportsEmptyPlaintext) { + const cipher = crypto.createCipheriv(algo, key, iv); + const ciphertext = Buffer.concat([ + cipher.update(Buffer.alloc(0)), + cipher.final(), + ]); + + const decipher = crypto.createDecipheriv(algo, key, iv); + decipher.setAuthTag(cipher.getAuthTag()); + const plaintext = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + assert.strictEqual(plaintext.length, 0); + } + + { + const cipher = crypto.createCipheriv(algo, key, iv); + const ciphertext = Buffer.concat([ + cipher.update('authenticated plaintext'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + authTag[0] ^= 1; + + const decipher = crypto.createDecipheriv(algo, key, iv); + decipher.setAuthTag(authTag); + decipher.update(ciphertext); + assert.throws(() => { + decipher.final(); + }, /Unsupported state or unable to authenticate data/); + } + } +} + // Test that GCM can produce shorter authentication tags than 16 bytes. { const fullTag = '1debb47b2c91ba2cea16fad021703070'; @@ -803,6 +962,7 @@ if (!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, @@ -813,13 +973,61 @@ if (ciphers.includes('aes-128-ccm')) { const tag = cipher.getAuthTag(); assert.strictEqual(tag.length, 16); - const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { - authTagLength: 16, - }); - decipher.setAuthTag(tag); - decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); - decipher.update(new DataView(new ArrayBuffer(0))); - decipher.final(); + if (isFipsEnabled && hasOpenSSL3) { + assert.throws(() => crypto.createDecipheriv( + 'aes-128-ccm', key, nonce, { authTagLength: 16 }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + } else { + const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + decipher.setAuthTag(tag); + 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'); } diff --git a/test/parallel/test-crypto-cipherbase-options-fast-path.js b/test/parallel/test-crypto-cipherbase-options-fast-path.js new file mode 100644 index 000000000000..9649b61fa7c5 --- /dev/null +++ b/test/parallel/test-crypto-cipherbase-options-fast-path.js @@ -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' }); + } +} diff --git a/test/parallel/test-crypto-cipheriv-cbc-cts.js b/test/parallel/test-crypto-cipheriv-cbc-cts.js new file mode 100644 index 000000000000..c3447b2908b3 --- /dev/null +++ b/test/parallel/test-crypto-cipheriv-cbc-cts.js @@ -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/); +} diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 8801ddfe7023..672dc75446c1 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -84,6 +84,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 key = '123456789012345678901234'; @@ -160,6 +238,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)); diff --git a/test/parallel/test-crypto-cipheriv-xts.js b/test/parallel/test-crypto-cipheriv-xts.js new file mode 100644 index 000000000000..236ff578f8f8 --- /dev/null +++ b/test/parallel/test-crypto-cipheriv-xts.js @@ -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)); + } +} diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index c0d9f1b5c425..68668ae6a284 100644 --- a/test/parallel/test-crypto-dh-curves.js +++ b/test/parallel/test-crypto-dh-curves.js @@ -139,9 +139,15 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { ecdh4.setPrivateKey(ecdh1.getPrivateKey()); ecdh4.setPublicKey(ecdh1.getPublicKey()); + const ecdh4Secret = ecdh4.computeSecret(ecdh2.getPublicKey()); + assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), + ecdh4Secret); + assert.throws(() => { ecdh4.setPublicKey(ecdh3.getPublicKey()); }, { message: 'Failed to convert Buffer to EC_POINT' }); + assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), + ecdh4Secret); // Verify that we can use ECDH without having to use newly generated keys. const ecdh5 = crypto.createECDH('secp256k1'); @@ -185,6 +191,8 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { sharedSecret); assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); // Verify that we still have the same key pair as before the computation. assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); @@ -254,3 +262,31 @@ if (availableCurves.has('prime256v1') && availableHashes.has('sha256')) { '-----END EC PRIVATE KEY-----'; crypto.createSign('SHA256').sign(ecPrivateKey); } + +if (crypto.getFips() && hasOpenSSL(3) && availableCurves.has('secp256k1')) { + const originalFips = crypto.getFips(); + + try { + crypto.setFips(0); + const local = crypto.createECDH('secp256k1'); + const peer = crypto.createECDH('secp256k1'); + local.generateKeys(); + const peerPublicKey = peer.generateKeys(); + + local.computeSecret(peerPublicKey); + crypto.setFips(1); + assert.throws(() => local.computeSecret(peerPublicKey), { + code: 'ERR_CRYPTO_INVALID_KEYPAIR', + name: 'RangeError', + }); + + const installed = crypto.createECDH('secp256k1'); + installed.setPrivateKey(Buffer.from('cafebabe'.repeat(8), 'hex')); + assert.throws(() => installed.computeSecret(peerPublicKey), { + code: 'ERR_CRYPTO_INVALID_KEYPAIR', + name: 'RangeError', + }); + } finally { + crypto.setFips(originalFips); + } +} diff --git a/test/parallel/test-crypto-fips-indicator-strict.js b/test/parallel/test-crypto-fips-indicator-strict.js new file mode 100644 index 000000000000..cc65d0016269 --- /dev/null +++ b/test/parallel/test-crypto-fips-indicator-strict.js @@ -0,0 +1,153 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +if (process.features.openssl_is_boringssl) { + common.skip('BoringSSL does not support FIPS'); +} + +const assert = require('node:assert'); +const diagnosticsChannel = require('node:diagnostics_channel'); +const { once } = require('node:events'); +const { createHmac, subtle } = require('node:crypto'); +const { Worker } = require('node:worker_threads'); +const { + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); + +const channelName = 'crypto.fips.indicator'; +const mode = process.env.NODE_TEST_FIPS_FORCE_MODE; + +if (!hasOpenSSL(3, 4)) { + common.skip('OpenSSL 3.4 or later is required'); +} else if (!hasFIPS(3, 4)) { + common.skip('an active OpenSSL 3.4+ FIPS provider is required'); +} else if (mode === 'provider') { + assertSerializedMode(mode); + assert.strictEqual( + createHmac('sha256', Buffer.alloc(13)).digest().byteLength, 32); +} else if (mode === 'strict') { + assertSerializedMode(mode); + const subscriber = common.mustNotCall(); + diagnosticsChannel.subscribe(channelName, subscriber); + assert.throws( + () => createHmac('sha256', Buffer.alloc(13)).digest(), + { code: /^ERR_OSSL_/ }); + setImmediate(common.mustCall(() => { + assert.strictEqual( + diagnosticsChannel.unsubscribe(channelName, subscriber), true); + })); +} else if (mode === 'strict-events') { + assertSerializedMode('strict'); + testStrictEvents().then(common.mustCall()); +} else { + runParent(); +} + +function assertSerializedMode(expected) { + const { getOptionsAsFlagsFromBinding } = require('internal/options'); + assert.ok(getOptionsAsFlagsFromBinding().includes(`--force-fips=${expected}`)); +} + +function nextIndicator() { + const keepAlive = setInterval(common.mustNotCall(), 10_000); + const { promise, resolve } = Promise.withResolvers(); + const subscriber = common.mustCall((event, name) => { + assert.strictEqual(name, channelName); + clearInterval(keepAlive); + assert.strictEqual( + diagnosticsChannel.unsubscribe(channelName, subscriber), true); + resolve(event); + }); + diagnosticsChannel.subscribe(channelName, subscriber); + return promise; +} + +function runParent() { + try { + createHmac('sha256', Buffer.alloc(13)).digest(); + } catch (error) { + assert.match(error.code, /^ERR_OSSL_/); + common.printSkipMessage( + 'the FIPS provider rejects unapproved operations before signaling'); + return; + } + + for (const [args, childMode] of [ + [['--force-fips'], 'provider'], + [['--force-fips=provider'], 'provider'], + [['--force-fips=strict'], 'strict'], + [[ + '--force-fips=strict', + '--enable-fips-indicator-events', + ], 'strict-events'], + ]) { + spawnSyncAndExitWithoutError( + process.execPath, [...args, '--expose-internals', __filename], { + env: { ...process.env, NODE_TEST_FIPS_FORCE_MODE: childMode }, + }); + } +} + +async function testStrictEvents() { + const key = Buffer.alloc(13); + + assert.throws( + () => createHmac('sha256', key).digest(), + { code: /^ERR_OSSL_/ }); + + let eventPromise = nextIndicator(); + assert.throws( + () => createHmac('sha256', key).digest(), + { code: /^ERR_OSSL_/ }); + assert.deepStrictEqual(await eventPromise, { + operation: 'HMAC', + reason: 'keysize', + blocked: true, + count: 1, + dropped: 0, + }); + + const hmacKey = await subtle.importKey( + 'raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + eventPromise = nextIndicator(); + await assert.rejects( + subtle.sign('HMAC', hmacKey, Buffer.alloc(0)), + { name: 'OperationError' }); + const webCryptoEvent = await eventPromise; + assert.strictEqual(webCryptoEvent.operation, 'HMAC'); + assert.strictEqual(webCryptoEvent.reason, 'keysize'); + assert.strictEqual(webCryptoEvent.blocked, true); + + eventPromise = nextIndicator(); + const worker = new Worker(` + 'use strict'; + const { createHmac } = require('node:crypto'); + const { workerData } = require('node:worker_threads'); + createHmac('sha256', workerData.key).digest(); + `, { + eval: true, + workerData: { key }, + }); + const errorPromise = once(worker, 'error'); + const exitPromise = new Promise((resolve) => worker.on('exit', resolve)); + const [[error], workerEvent] = await Promise.all([ + errorPromise, + eventPromise, + ]); + assert.match(error.code, /^ERR_OSSL_/); + assert.deepStrictEqual(workerEvent, { + operation: 'HMAC', + reason: 'keysize', + blocked: true, + count: 1, + dropped: 0, + }); + const exitCode = await exitPromise; + assert.strictEqual(exitCode, 1); +} diff --git a/test/parallel/test-crypto-fips.js b/test/parallel/test-crypto-fips.js index 8b2e1c9a3649..ea20d895b1c8 100644 --- a/test/parallel/test-crypto-fips.js +++ b/test/parallel/test-crypto-fips.js @@ -10,10 +10,11 @@ if (process.features.openssl_is_boringssl) const assert = require('assert'); const spawnSync = require('child_process').spawnSync; const path = require('path'); +const { spawnSyncAndAssert } = require('../common/child_process'); const fixtures = require('../common/fixtures'); const { internalBinding } = require('internal/test/binding'); const { testFipsCrypto } = internalBinding('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); const FIPS_ENABLED = 1; const FIPS_DISABLED = 0; @@ -94,6 +95,45 @@ testHelper( 'require("crypto").getFips()', process.env); +// Explicit provider mode should preserve the behavior of bare --force-fips. +testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips=provider'], + testFipsCrypto() ? kNoFailure : kGenericUserError, + testFipsCrypto() ? FIPS_ENABLED : FIPS_FORCE_ERROR_STRING, + 'require("crypto").getFips()', + process.env); + +{ + spawnSyncAndAssert( + process.execPath, ['--force-fips=invalid', '-e', '0'], { + status: 9, + stderr: /invalid value for --force-fips; expected 'provider' or 'strict'/, + }); +} + +if (hasOpenSSL(3, 4)) { + testHelper( + testFipsCrypto() ? 'stdout' : 'stderr', + ['--force-fips=strict'], + testFipsCrypto() ? kNoFailure : kGenericUserError, + testFipsCrypto() ? FIPS_ENABLED : FIPS_FORCE_ERROR_STRING, + 'require("crypto").getFips()', + process.env); +} else { + spawnSyncAndAssert( + process.execPath, ['--enable-fips-indicator-events', '-e', '0'], { + status: 9, + stderr: /--enable-fips-indicator-events requires OpenSSL 3\.4 or later/, + }); + + spawnSyncAndAssert( + process.execPath, ['--force-fips=strict', '-e', '0'], { + status: 9, + stderr: /--force-fips=strict requires OpenSSL 3\.4 or later/, + }); +} + // By default FIPS should be off in both FIPS and non-FIPS builds // unless Node.js was configured using --shared-openssl in // which case it may be enabled by the system. diff --git a/test/parallel/test-crypto-getcipherinfo.js b/test/parallel/test-crypto-getcipherinfo.js index d55985aa3c7f..35eced3b296e 100644 --- a/test/parallel/test-crypto-getcipherinfo.js +++ b/test/parallel/test-crypto-getcipherinfo.js @@ -5,9 +5,12 @@ if (!common.hasCrypto) common.skip('missing crypto'); const { + createCipheriv, + createHash, getCiphers, getCipherInfo } = require('crypto'); +const { hasOpenSSL3 } = require('../common/crypto'); const assert = require('assert'); @@ -15,6 +18,48 @@ 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 }), { + code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH', + }); +} for (const cipher of ciphers) { const info = getCipherInfo(cipher); @@ -25,8 +70,10 @@ for (const cipher of ciphers) { continue; } assert(info); - const info2 = getCipherInfo(info.nid); - assert.deepStrictEqual(info, info2); + if (info.nid !== undefined) { + const info2 = getCipherInfo(info.nid); + assert.deepStrictEqual(info, info2); + } } const info = getCipherInfo('aes-128-cbc'); @@ -82,3 +129,57 @@ if (!process.features.openssl_is_boringssl) { } else { 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'); + assert.strictEqual(info.mode, 'siv'); + assert.strictEqual(info.keyLength, 32); + assert.strictEqual(info.ivLength, undefined); + assert(getCipherInfo('aes-128-siv', { ivLength: 0 })); + assert(!getCipherInfo('aes-128-siv', { ivLength: 1 })); +} else { + common.printSkipMessage('Skipping unsupported aes-128-siv test cases'); +} + +if (ciphers.includes('aes-128-gcm-siv')) { + const info = getCipherInfo('aes-128-gcm-siv'); + assert.strictEqual(info.name, 'aes-128-gcm-siv'); + assert.strictEqual(info.mode, 'gcm-siv'); + assert.strictEqual(info.keyLength, 16); + assert.strictEqual(info.ivLength, 12); + assert(getCipherInfo('aes-128-gcm-siv', { ivLength: 12 })); + assert(!getCipherInfo('aes-128-gcm-siv', { ivLength: 11 })); +} 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`); + } +} diff --git a/test/parallel/test-crypto-mac-cache-snapshot.js b/test/parallel/test-crypto-mac-cache-snapshot.js new file mode 100644 index 000000000000..1fd37367d2a8 --- /dev/null +++ b/test/parallel/test-crypto-mac-cache-snapshot.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL3 } = require('../common/crypto'); +if (!hasOpenSSL3 || process.features.openssl_is_boringssl) + common.skip('this test requires OpenSSL 3 EVP_MAC support'); + +const assert = require('node:assert'); +const { getMacs } = require('node:crypto'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); + +if (!getMacs().includes('poly1305')) + common.skip('Poly1305 is not supported'); + +const entry = fixtures.path('snapshot', 'crypto-provider-mac-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 MAC cache snapshot: ok/); diff --git a/test/parallel/test-crypto-mac-cache.js b/test/parallel/test-crypto-mac-cache.js new file mode 100644 index 000000000000..dfe99e943bb2 --- /dev/null +++ b/test/parallel/test-crypto-mac-cache.js @@ -0,0 +1,307 @@ +// 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 || process.features.openssl_is_boringssl) + common.skip('this test requires OpenSSL 3 EVP_MAC support'); + +const assert = require('node:assert'); +const { once } = require('node:events'); +const { + createMac, + getFips, + getMacs, + setFips, +} = require('node:crypto'); +const { getMacCache } = require('internal/crypto/util'); +const { internalBinding } = require('internal/test/binding'); +const { Worker } = require('node:worker_threads'); + +const binding = internalBinding('crypto'); +const algorithm = 'poly1305'; +const key = Buffer.from( + '85d6be7857556d337f4452fe42d506a8' + + '0103808afb0db2fd4abff6af4149f51b', + 'hex', +); +const data = Buffer.from('Cryptographic Forum Research Group'); +const expected = 'a8061dc1305136c6c22b8baf0c0127a9'; +const originalFips = getFips(); + +function getAliasId(aliases, name) { + const normalized = name.toLowerCase(); + for (const [alias, id] of Object.entries(aliases)) { + if (alias.toLowerCase() === normalized) return id; + } + return undefined; +} + +try { + setFips(0); +} catch { + common.skip('FIPS mode cannot be disabled'); +} +if (getFips() !== 0) + common.skip('FIPS mode cannot be disabled'); + +const initialMacs = getMacs(); +if (!initialMacs.includes(algorithm)) + common.skip(`${algorithm} is not supported`); + +let fipsMacs; +let canToggleFips = false; +const generationBeforeFipsProbe = binding.getFipsCryptoGeneration(); +try { + setFips(1); +} catch { + // FIPS mode is optional, so the non-FIPS cache checks below still run. + assert.strictEqual( + binding.getFipsCryptoGeneration(), + generationBeforeFipsProbe, + ); +} +if (getFips() === 1) { + fipsMacs = getMacs(); + canToggleFips = true; +} +try { + setFips(0); +} catch { + canToggleFips = false; +} + +const generation = binding.getFipsCryptoGeneration(); +setFips(0); +assert.strictEqual(binding.getFipsCryptoGeneration(), generation); + +const expectedMacs = getMacs(); +const disposableMacs = getMacs(); +assert.notStrictEqual(disposableMacs, expectedMacs); +disposableMacs.length = 0; +disposableMacs.push('not-a-real-mac'); +assert.deepStrictEqual(getMacs(), expectedMacs); + +const aliases = binding.getCachedMacAliases(); +const initialAlgorithmId = getAliasId(aliases, algorithm); +assert.strictEqual(typeof initialAlgorithmId, 'number'); + +const macCache = getMacCache(); +const cacheName = Object.keys(macCache).find( + (name) => name.toLowerCase() === algorithm, +); +assert(cacheName); +const descriptor = Object.getOwnPropertyDescriptor(macCache, cacheName); +assert(descriptor); +assert.strictEqual(descriptor.value, initialAlgorithmId); +const sentinel = new Error('mac cache setter'); +const throwsSentinel = (err) => err === sentinel; + +function installThrowingMacCacheEntry(id) { + Object.defineProperty(macCache, cacheName, { + __proto__: null, + configurable: true, + enumerable: descriptor.enumerable, + get() { return id; }, + set() { throw sentinel; }, + }); +} + +installThrowingMacCacheEntry(-1); +assert.throws(() => createMac(cacheName, key), throwsSentinel); +Object.defineProperty(macCache, cacheName, descriptor); + +// OpenSSL exposes two spellings for each KMAC implementation. They must map +// to the same cached EVP_MAC rather than consume separate cache entries. +const kmac128Id = getAliasId(aliases, 'kmac128'); +const kmac128HyphenatedId = getAliasId(aliases, 'kmac-128'); +if (kmac128Id === undefined || kmac128HyphenatedId === undefined) { + common.printSkipMessage('KMAC-128 aliases are not available'); +} else { + assert.strictEqual(kmac128Id, kmac128HyphenatedId); + const kmacAlgorithm = 'KMAC128'; + const hyphenatedAlgorithm = 'KMAC-128'; + const kmacOptions = { outputLength: 32 }; + const kmacKey = Buffer.alloc(32, 0x42); + const kmacData = Buffer.from('cache alias test'); + assert.deepStrictEqual( + createMac(kmacAlgorithm, kmacKey, kmacOptions) + .update(kmacData).final(), + createMac(hyphenatedAlgorithm, kmacKey, kmacOptions) + .update(kmacData).final(), + ); + const aliasesAfterUse = binding.getCachedMacAliases(); + assert.strictEqual(getAliasId(aliasesAfterUse, 'kmac128'), kmac128Id); + assert.strictEqual( + getAliasId(aliasesAfterUse, 'kmac-128'), + kmac128Id, + ); +} + +if (!canToggleFips || fipsMacs.includes(algorithm)) { + common.printSkipMessage('FIPS cache invalidation cannot be exercised'); + try { + setFips(originalFips); + } catch { + // The process is about to exit and FIPS support is optional. + } +} else { + const liveMac = createMac(algorithm, key).update(data); + const worker = new Worker(` + 'use strict'; + const { + createMac, + getFips, + getMacs, + } = require('node:crypto'); + const { internalBinding } = require('internal/test/binding'); + const { parentPort, workerData } = require('node:worker_threads'); + + function getAliasId(aliases, name) { + const normalized = name.toLowerCase(); + for (const [alias, id] of Object.entries(aliases)) { + if (alias.toLowerCase() === normalized) return id; + } + return undefined; + } + + const binding = internalBinding('crypto'); + const key = Buffer.from(workerData.key); + const data = Buffer.from(workerData.data); + const liveMac = createMac(workerData.algorithm, key).update(data); + getMacs(); + const initialAlgorithmId = getAliasId( + binding.getCachedMacAliases(), + workerData.algorithm, + ); + parentPort.postMessage({ + phase: 'warm', + algorithmId: initialAlgorithmId, + generation: binding.getFipsCryptoGeneration(), + }); + + parentPort.on('message', (phase) => { + if (phase === 'fips-on') { + let errorCode; + try { + createMac(workerData.algorithm, key); + } catch (error) { + errorCode = error.code; + } + const macs = getMacs(); + parentPort.postMessage({ + phase, + algorithmId: getAliasId( + binding.getCachedMacAliases(), + workerData.algorithm, + ), + errorCode, + fips: getFips(), + generation: binding.getFipsCryptoGeneration(), + hasAlgorithm: macs.includes(workerData.algorithm), + tag: liveMac.final('hex'), + }); + } else if (phase === 'fips-off') { + const macs = getMacs(); + parentPort.postMessage({ + phase, + algorithmId: getAliasId( + binding.getCachedMacAliases(), + workerData.algorithm, + ), + fips: getFips(), + generation: binding.getFipsCryptoGeneration(), + hasAlgorithm: macs.includes(workerData.algorithm), + tag: createMac(workerData.algorithm, key) + .update(data).final('hex'), + }); + } else { + parentPort.close(); + } + }); + `, { + eval: true, + workerData: { algorithm, data, key }, + }); + worker.on('error', common.mustNotCall()); + + (async () => { + const exitPromise = once(worker, 'exit'); + try { + const [warm] = await once(worker, 'message'); + assert.strictEqual(warm.phase, 'warm'); + assert.strictEqual(typeof warm.algorithmId, 'number'); + assert.strictEqual(warm.generation, generation); + + installThrowingMacCacheEntry(descriptor.value); + try { + setFips(1); + assert.throws(() => createMac(cacheName, key), throwsSentinel); + installThrowingMacCacheEntry(-1); + assert.throws(() => createMac(cacheName, key), throwsSentinel); + } finally { + Object.defineProperty(macCache, cacheName, descriptor); + } + const enabledGeneration = binding.getFipsCryptoGeneration(); + assert.strictEqual(enabledGeneration, generation + 1n); + assert.strictEqual(getFips(), 1); + assert(!getMacs().includes(algorithm)); + assert.strictEqual( + getAliasId(binding.getCachedMacAliases(), algorithm), + undefined, + ); + assert.throws(() => createMac(algorithm, key), { + code: 'ERR_CRYPTO_INVALID_MAC', + }); + assert.strictEqual(liveMac.final('hex'), expected); + + let responsePromise = once(worker, 'message'); + worker.postMessage('fips-on'); + const [enabled] = await responsePromise; + assert.strictEqual(enabled.phase, 'fips-on'); + assert.strictEqual(enabled.algorithmId, undefined); + assert.strictEqual(enabled.errorCode, 'ERR_CRYPTO_INVALID_MAC'); + assert.strictEqual(enabled.fips, 1); + assert.strictEqual(enabled.generation, enabledGeneration); + assert.strictEqual(enabled.hasAlgorithm, false); + assert.strictEqual(enabled.tag, expected); + + setFips(0); + const disabledGeneration = binding.getFipsCryptoGeneration(); + assert.strictEqual(disabledGeneration, enabledGeneration + 1n); + assert.strictEqual(getFips(), 0); + assert(getMacs().includes(algorithm)); + const restoredAlgorithmId = getAliasId( + binding.getCachedMacAliases(), + algorithm, + ); + assert.strictEqual(typeof restoredAlgorithmId, 'number'); + assert.notStrictEqual(restoredAlgorithmId, initialAlgorithmId); + assert.strictEqual( + createMac(algorithm, key).update(data).final('hex'), + expected, + ); + + responsePromise = once(worker, 'message'); + worker.postMessage('fips-off'); + const [disabled] = await responsePromise; + assert.strictEqual(disabled.phase, 'fips-off'); + assert.strictEqual(disabled.fips, 0); + assert.strictEqual(disabled.generation, disabledGeneration); + assert.strictEqual(disabled.hasAlgorithm, true); + assert.strictEqual(typeof disabled.algorithmId, 'number'); + assert.notStrictEqual(disabled.algorithmId, warm.algorithmId); + assert.strictEqual(disabled.tag, expected); + + worker.postMessage('done'); + const [code] = await exitPromise; + assert.strictEqual(code, 0); + } finally { + if (worker.threadId !== -1) await worker.terminate(); + setFips(originalFips); + } + })().then(common.mustCall()); +} diff --git a/test/parallel/test-crypto-mac-errors.js b/test/parallel/test-crypto-mac-errors.js new file mode 100644 index 000000000000..43e464bb737e --- /dev/null +++ b/test/parallel/test-crypto-mac-errors.js @@ -0,0 +1,180 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3) || process.features.openssl_is_boringssl) { + common.skip('OpenSSL 3 EVP_MAC support is required'); +} + +const assert = require('node:assert'); +const fixtures = require('../common/fixtures'); +const { + createMac, + createPublicKey, + createSecretKey, + getMacs, +} = require('node:crypto'); + +const key = Buffer.alloc(32, 0x42); +const data = Buffer.from('data'); +const availableMacs = new Set(getMacs()); + +function invalidType(fn) { + assert.throws(fn, { code: 'ERR_INVALID_ARG_TYPE' }); +} + +function invalidValue(fn) { + assert.throws(fn, { code: 'ERR_INVALID_ARG_VALUE' }); +} + +for (const algorithm of [undefined, null, 1, true, [], {}]) { + invalidType(() => createMac(algorithm, key)); +} + +for (const algorithm of ['', 'hmac\0sha256']) { + invalidValue(() => createMac(algorithm, key)); +} + +for (const [algorithm, options] of [ + ['hmac', { digest: 1 }], + ['cmac', { cipher: 1 }], + ['gmac', { iv: 'not a BufferSource' }], + ['kmac128', { customization: 'not a BufferSource' }], + ['blake2bmac', { salt: 'not a BufferSource' }], + ['kmac128', { outputLength: '32' }], +]) { + invalidType(() => createMac(algorithm, key, options)); +} + +for (const [algorithm, options] of [ + ['hmac', { digest: 'sha256\0sha512' }], + ['cmac', { cipher: 'aes-128-cbc\0aes-256-cbc' }], +]) { + invalidValue(() => createMac(algorithm, key, options)); +} + +for (const outputLength of [-1, 0.5, 2 ** 32, Infinity, NaN]) { + assert.throws( + () => createMac('kmac128', key, { outputLength }), + { code: 'ERR_OUT_OF_RANGE' }, + ); +} + +assert.throws( + () => createMac('definitely-not-a-mac', key), + { code: 'ERR_CRYPTO_INVALID_MAC' }, +); + +if (availableMacs.has('siphash')) { + assert.throws(() => createMac('siphash', Buffer.alloc(15)), (error) => { + assert.strictEqual(error.name, 'Error'); + assert.strictEqual(error.message, 'Failed to initialize MAC'); + assert.strictEqual(error.code, 'ERR_CRYPTO_OPERATION_FAILED'); + for (const property of [ + 'function', + 'library', + 'reason', + 'opensslErrorStack', + ]) { + assert.ok(!(property in error)); + } + return true; + }); +} + +if (availableMacs.has('hmac')) { + const algorithm = 'hmac'; + const options = { digest: 'sha256' }; + assert.throws( + () => createMac(algorithm, key, { digest: 'definitely-not-a-digest' }), + { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + library: 'digital envelope routines', + reason: 'unsupported', + }, + ); + const publicKey = createPublicKey(fixtures.readKey('rsa_public.pem')); + const cryptoKey = createSecretKey(key).toCryptoKey( + { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + for (const invalidKey of [ + undefined, + null, + 'key', + {}, + publicKey, + cryptoKey, + ]) { + invalidType(() => createMac(algorithm, invalidKey, options)); + } + + for (const invalidData of [ + undefined, + null, + 1, + true, + {}, + new ArrayBuffer(4), + ]) { + invalidType(() => createMac(algorithm, key, options).update(invalidData)); + } + + invalidType(() => createMac(algorithm, key, options).update('data', 1)); + invalidType(() => createMac(algorithm, key, options).final(1)); + invalidType(() => createMac(algorithm, key, 'hex')); + + invalidValue(() => createMac(algorithm, key, options) + .update('data', 'not-an-encoding')); + const invalidFinalEncoding = createMac(algorithm, key, options); + invalidValue(() => invalidFinalEncoding.final('not-an-encoding')); + assert.deepStrictEqual( + invalidFinalEncoding.update(data).final(), + createMac(algorithm, key, options).update(data).final(), + ); + invalidValue(() => createMac(algorithm, key, options).update('0', 'hex')); + + invalidValue(() => createMac('hmac', key)); + for (const extra of [ + { cipher: 'aes-128-cbc' }, + { iv: Buffer.alloc(12) }, + { customization: Buffer.alloc(0) }, + { salt: Buffer.alloc(16) }, + { outputLength: 16 }, + ]) { + invalidValue(() => createMac('hmac', key, { + ...options, + ...extra, + })); + } +} + +if (availableMacs.has('cmac')) { + invalidValue(() => createMac('cmac', key)); + invalidValue(() => createMac('cmac', key, { digest: 'sha256' })); + invalidValue(() => createMac('cmac', key, { + cipher: 'aes-256-cbc', + iv: Buffer.alloc(16), + })); +} + +if (availableMacs.has('gmac')) { + invalidValue(() => createMac('gmac', key)); + invalidValue(() => createMac('gmac', key, { cipher: 'aes-256-gcm' })); + invalidValue(() => createMac('gmac', key, { iv: Buffer.alloc(12) })); + invalidValue(() => createMac('gmac', key, { + cipher: 'aes-256-gcm', + iv: Buffer.alloc(12), + digest: 'sha256', + })); +} + +if (availableMacs.has('poly1305')) { + invalidValue(() => createMac('poly1305', key, { + customization: Buffer.alloc(0), + })); +} diff --git a/test/parallel/test-crypto-mac-unsupported.js b/test/parallel/test-crypto-mac-unsupported.js new file mode 100644 index 000000000000..68bb79f301e5 --- /dev/null +++ b/test/parallel/test-crypto-mac-unsupported.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const { hasOpenSSL3 } = require('../common/crypto'); + +if (hasOpenSSL3 && !process.features.openssl_is_boringssl) { + common.skip('this test requires a build without EVP_MAC support'); +} + +const assert = require('node:assert'); +const crypto = require('node:crypto'); + +const algorithm = 'hmac'; +const options = { digest: 'sha256' }; +const key = Buffer.from('key'); + +assert.strictEqual(typeof crypto.createMac, 'function'); +assert.strictEqual(typeof crypto.getMacs, 'function'); +assert.strictEqual(crypto.Mac, undefined); +assert.deepStrictEqual(crypto.getMacs(), []); +assert.throws(() => crypto.createMac(algorithm, key, options), { + code: 'ERR_CRYPTO_MAC_NOT_SUPPORTED', +}); +(async () => { + const esmCrypto = await import('node:crypto'); + assert.strictEqual(esmCrypto.createMac, crypto.createMac); + assert.strictEqual(esmCrypto.getMacs, crypto.getMacs); + assert.strictEqual(esmCrypto.Mac, undefined); +})().then(common.mustCall()); diff --git a/test/parallel/test-crypto-mac-vectors.js b/test/parallel/test-crypto-mac-vectors.js new file mode 100644 index 000000000000..ec9f0cffe877 --- /dev/null +++ b/test/parallel/test-crypto-mac-vectors.js @@ -0,0 +1,179 @@ +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3) || process.features.openssl_is_boringssl) { + common.skip('OpenSSL 3 EVP_MAC support is required'); +} + +const assert = require('node:assert'); +const { encodingsMap } = require('internal/util'); +const { + createMac, + getCiphers, + getMacs, +} = require('node:crypto'); + +const availableMacs = new Set(getMacs()); +const availableCiphers = new Set(getCiphers()); +const kmacVectors = require('../fixtures/crypto/kmac')(); +const gmacIVStorage = Uint8Array.from([ + 0xff, + ...Buffer.alloc(12), + 0xff, +]); +const blake2bSaltStorage = Uint8Array.from([ + 0xff, + ...Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex'), + 0xff, +]); +const blake2sSaltStorage = Uint8Array.from([ + 0xff, + ...Buffer.from('0001020304050607', 'hex'), + 0xff, +]); + +const vectors = [ + { + label: 'CMAC-AES-128', + algorithm: 'cmac', + options: { cipher: 'aes-128-cbc' }, + key: '2b7e151628aed2a6abf7158809cf4f3c', + data: '', + expected: 'bb1d6929e95937287fa37d129b756746', + cipher: 'aes-128-cbc', + }, + { + label: 'GMAC-AES-128', + algorithm: 'gmac', + options: { + cipher: 'aes-128-gcm', + iv: new DataView(gmacIVStorage.buffer, 1, 12), + }, + key: '00000000000000000000000000000000', + data: '', + expected: '58e2fccefa7e3061367f1d57a4e7455a', + cipher: 'aes-128-gcm', + }, + { + label: 'Poly1305', + algorithm: 'poly1305', + key: '85d6be7857556d337f4452fe42d506a8' + + '0103808afb0db2fd4abff6af4149f51b', + data: Buffer.from('Cryptographic Forum Research Group').toString('hex'), + expected: 'a8061dc1305136c6c22b8baf0c0127a9', + }, + { + label: 'SipHash-2-4', + algorithm: 'siphash', + options: { outputLength: 8 }, + key: '000102030405060708090a0b0c0d0e0f', + data: '', + expected: '310e0edd47db6f72', + }, + { + label: 'BLAKE2b MAC', + algorithm: 'blake2bmac', + options: { + outputLength: 32, + salt: new DataView(blake2bSaltStorage.buffer, 1, 16), + }, + key: '000102030405060708090a0b0c0d0e0f', + data: Buffer.from('abc').toString('hex'), + expected: '6e583b101a126f2d1fb6d1fff9834f3a' + + '0d0e23c17b902cca4f1a0d7abfb327fa', + }, + { + label: 'BLAKE2s MAC', + algorithm: 'blake2smac', + options: { + outputLength: 16, + salt: new DataView(blake2sSaltStorage.buffer, 1, 8), + }, + key: '000102030405060708090a0b0c0d0e0f', + data: Buffer.from('abc').toString('hex'), + expected: '18adff242af55a56c7b7646df6c3d9ba', + }, +]; + +for (const index of [0, 3]) { + const vector = kmacVectors[index]; + const algorithm = vector.algorithm.toLowerCase(); + const options = { + outputLength: vector.outputLength / 8, + }; + if (vector.customization !== undefined) { + const storage = Uint8Array.from([ + 0xff, + ...vector.customization, + 0xff, + ]); + options.customization = new DataView( + storage.buffer, 1, vector.customization.length); + } + vectors.push({ + label: vector.algorithm, + algorithm, + options, + key: vector.key.toString('hex'), + data: vector.data.toString('hex'), + expected: vector.expected.toString('hex'), + }); +} + +for (const vector of vectors) { + if (!availableMacs.has(vector.algorithm) || + (vector.cipher !== undefined && + !availableCiphers.has(vector.cipher))) { + common.printSkipMessage(`${vector.label} is not available`); + continue; + } + + const key = Buffer.from(vector.key, 'hex'); + const data = Buffer.from(vector.data, 'hex'); + const expected = Buffer.from(vector.expected, 'hex'); + assert.deepStrictEqual( + createMac(vector.algorithm, key, vector.options).update(data).final(), + expected, + ); +} + +if (availableMacs.has('kmac128')) { + const vector = kmacVectors[0]; + const algorithm = 'kmac128'; + const options = { outputLength: 0 }; + for (const outputEncoding of Object.keys(encodingsMap)) { + if (outputEncoding === 'buffer') continue; + assert.strictEqual( + createMac(algorithm, vector.key, options) + .update(vector.data) + .final(outputEncoding), + '', + ); + } + + for (const result of [ + createMac(algorithm, vector.key, options) + .update(vector.data) + .final(), + createMac(algorithm, vector.key, options) + .update(vector.data) + .final('buffer'), + ]) { + assert(Buffer.isBuffer(result)); + assert.deepStrictEqual(result, Buffer.alloc(0)); + } + + const streamed = createMac(algorithm, vector.key, options); + streamed.on('data', common.mustNotCall()); + streamed.on('end', common.mustCall()); + streamed.end(vector.data); +} diff --git a/test/parallel/test-crypto-mac.js b/test/parallel/test-crypto-mac.js new file mode 100644 index 000000000000..f41bd86509eb --- /dev/null +++ b/test/parallel/test-crypto-mac.js @@ -0,0 +1,225 @@ +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3) || process.features.openssl_is_boringssl) { + common.skip('OpenSSL 3 EVP_MAC support is required'); +} + +const assert = require('node:assert'); +const crypto = require('node:crypto'); +const { encodingsMap } = require('internal/util'); +const { + createHmac, + createMac, + createSecretKey, + getMacs, +} = crypto; +const { finished } = require('node:stream/promises'); +const { Transform } = require('node:stream'); + +assert.strictEqual(crypto.Mac, undefined); + +const firstMacs = getMacs(); +const secondMacs = getMacs(); + +assert.notStrictEqual(firstMacs, secondMacs); +assert.deepStrictEqual(firstMacs, [...firstMacs].sort()); +assert.strictEqual(firstMacs.length, new Set(firstMacs).size); +assert(firstMacs.every((name) => typeof name === 'string')); +assert(firstMacs.every((name) => name === name.toLowerCase())); +assert(firstMacs.every((name) => !/^\d+(?:\.\d+)+$/.test(name))); + +firstMacs.push('not-a-real-mac'); +assert(!getMacs().includes('not-a-real-mac')); + +const availableMacs = new Set(secondMacs); +if (!availableMacs.has('hmac')) { + common.printSkipMessage('HMAC is not available from the active providers'); +} else { + const algorithm = 'HMAC'; + const options = { digest: 'sha256' }; + const key = Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex'); + const data = Buffer.from('The quick brown fox jumps over the lazy dog'); + const expected = createHmac('sha256', key).update(data).digest(); + const expectedEmpty = createHmac('sha256', key).digest(); + const expectedEmptyKey = createHmac('sha256', Buffer.alloc(0)) + .update(data) + .digest(); + + assert.deepStrictEqual( + createMac(algorithm, key, options).update(data.toString()).final(), + expected, + ); + assert.deepStrictEqual( + createMac(algorithm, key, options).final(), + expectedEmpty, + ); + assert.deepStrictEqual( + createMac(algorithm, Buffer.alloc(0), options).update(data).final(), + expectedEmptyKey, + ); + + const nullPrototypeOptions = Object.assign({ __proto__: null }, options); + assert.deepStrictEqual( + createMac(algorithm, key, nullPrototypeOptions).update(data).final(), + expected, + ); + const inheritedUnknownOptions = Object.assign( + { __proto__: { unknown: true } }, options); + assert.deepStrictEqual( + createMac(algorithm, key, inheritedUnknownOptions).update(data).final(), + expected, + ); + assert.deepStrictEqual( + createMac(algorithm, key, { ...options, unknown: true }) + .update(data) + .final(), + expected, + ); + const incremental = createMac(algorithm, key, options); + assert(incremental instanceof Transform); + assert.strictEqual(incremental.update(data.subarray(0, 10)), incremental); + assert.strictEqual(incremental.update(Buffer.alloc(0)), incremental); + incremental.update(data.subarray(10)); + assert.deepStrictEqual(incremental.final(), expected); + assert.throws( + () => incremental.update(Buffer.alloc(0)), + { code: 'ERR_CRYPTO_MAC_FINALIZED' }, + ); + assert.throws( + () => incremental.final(), + { code: 'ERR_CRYPTO_MAC_FINALIZED' }, + ); + + // Input encodings are handled by update(), while final() accepts an output + // encoding. + assert.strictEqual( + createMac(algorithm, key, options) + .update(data.toString('hex'), 'hex') + .final('hex'), + expected.toString('hex'), + ); + assert.strictEqual( + createMac(algorithm, key, options) + .update(data.toString('base64'), 'base64') + .final('base64url'), + expected.toString('base64url'), + ); + for (const outputEncoding of Object.keys(encodingsMap)) { + if (outputEncoding === 'buffer') continue; + assert.strictEqual( + createMac(algorithm, key, options) + .update(data) + .final(outputEncoding), + expected.toString(outputEncoding), + ); + } + assert.deepStrictEqual( + createMac(algorithm, key, options) + .update(data, 'not-an-encoding') + .final(), + expected, + ); + const explicitBuffer = createMac(algorithm, key, options) + .update(data) + .final('buffer'); + assert(Buffer.isBuffer(explicitBuffer)); + assert.deepStrictEqual(explicitBuffer, expected); + + const encodedFinal = createMac(algorithm, key, options).update(data); + assert.strictEqual(encodedFinal.final('hex'), expected.toString('hex')); + assert.throws( + () => encodedFinal.update(data), + { code: 'ERR_CRYPTO_MAC_FINALIZED' }, + ); + assert.throws( + () => encodedFinal.final('hex'), + { code: 'ERR_CRYPTO_MAC_FINALIZED' }, + ); + // BufferSource keys and data must honor view offsets and lengths. + const keyStorage = Uint8Array.from([0xff, ...key, 0xff]); + const keyView = new Uint8Array(keyStorage.buffer, 1, key.length); + const keyDataView = new DataView(keyStorage.buffer, 1, key.length); + const dataStorage = Uint8Array.from([0xff, ...data, 0xff]); + const dataView = new DataView(dataStorage.buffer, 1, data.length); + assert.deepStrictEqual( + createMac(algorithm, keyView, options).update(dataView).final(), + expected, + ); + assert.deepStrictEqual( + createMac(algorithm, keyDataView, options).update(dataView).final(), + expected, + ); + + const arrayBufferKey = key.buffer.slice( + key.byteOffset, + key.byteOffset + key.byteLength, + ); + assert.deepStrictEqual( + createMac(algorithm, arrayBufferKey, options).update(data).final(), + expected, + ); + const secretKey = createSecretKey(key); + assert.deepStrictEqual( + createMac(algorithm, secretKey, options).update(data).final(), + expected, + ); + + (async () => { + const esmCrypto = await import('node:crypto'); + assert.strictEqual(esmCrypto.createMac, createMac); + assert.strictEqual(esmCrypto.getMacs, getMacs); + assert.strictEqual(esmCrypto.Mac, undefined); + + const emptyStream = createMac(algorithm, key, options); + const emptyChunks = []; + emptyStream.on( + 'data', common.mustCall((chunk) => emptyChunks.push(chunk), 1)); + const emptyFinished = finished(emptyStream); + emptyStream.end(); + await emptyFinished; + assert.deepStrictEqual(Buffer.concat(emptyChunks), expectedEmpty); + + const streamed = createMac(algorithm, key, { + ...options, + highWaterMark: 1, + }); + const chunks = []; + streamed.on('data', common.mustCall((chunk) => chunks.push(chunk), 1)); + assert.strictEqual(streamed.writableHighWaterMark, 1); + assert.strictEqual(streamed.readableHighWaterMark, 1); + const streamedFinished = finished(streamed); + streamed.write(data.subarray(0, 10)); + streamed.end(data.subarray(10)); + await streamedFinished; + assert.deepStrictEqual(Buffer.concat(chunks), expected); + assert.throws( + () => streamed.update(Buffer.alloc(0)), + { code: 'ERR_CRYPTO_MAC_FINALIZED' }, + ); + assert.throws( + () => streamed.final(), + { code: 'ERR_CRYPTO_MAC_FINALIZED' }, + ); + + // Direct finalization followed by stream finalization fails through the + // stream error path and does not emit a second tag. + const mixed = createMac(algorithm, key, options); + mixed.on('data', common.mustNotCall()); + const mixedFinished = finished(mixed); + assert.deepStrictEqual(mixed.update(data).final(), expected); + mixed.end(); + await assert.rejects(mixedFinished, { + code: 'ERR_CRYPTO_MAC_FINALIZED', + }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-crypto-provider-cipher-cache-snapshot.js b/test/parallel/test-crypto-provider-cipher-cache-snapshot.js new file mode 100644 index 000000000000..1afc5df8d94d --- /dev/null +++ b/test/parallel/test-crypto-provider-cipher-cache-snapshot.js @@ -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/); diff --git a/test/parallel/test-crypto-provider-cipher-cache.js b/test/parallel/test-crypto-provider-cipher-cache.js new file mode 100644 index 000000000000..bde9988480ed --- /dev/null +++ b/test/parallel/test-crypto-provider-cipher-cache.js @@ -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))); diff --git a/test/parallel/test-crypto-provider-hash-options.js b/test/parallel/test-crypto-provider-hash-options.js new file mode 100644 index 000000000000..609d00d7f7b0 --- /dev/null +++ b/test/parallel/test-crypto-provider-hash-options.js @@ -0,0 +1,387 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +if (Number(process.versions.openssl.split('.')[0]) < 4 || + process.features.openssl_is_boringssl) { + common.skip('OpenSSL 4 provider support is required'); +} + +const assert = require('node:assert'); +const { + createHash, + getHashes, + hash, +} = require('node:crypto'); +const { internalBinding } = require('internal/test/binding'); +const { + HashJob, + kCryptoJobSync, + kCryptoJobWebCrypto, +} = internalBinding('crypto'); + +const hashes = getHashes(); +const hashNames = new Map( + hashes.map((name) => [name.toLowerCase(), name]), +); + +let exercised = false; + +function findHash(...names) { + for (const name of names) { + const result = hashNames.get(name); + if (result !== undefined) return result; + } + return undefined; +} + +function testHashJob(args, expected) { + const { 0: err, 1: result } = new HashJob( + kCryptoJobSync, + ...args, + ).run(); + assert.strictEqual(err, undefined); + assert.deepStrictEqual(Buffer.from(result), expected); + + (async () => { + const asyncResult = await new HashJob( + kCryptoJobWebCrypto, + ...args, + ).run(); + assert.deepStrictEqual(Buffer.from(asyncResult), expected); + })().then(common.mustCall()); +} + +const cshakeVectors = [ + { + names: ['cshake-128', 'cshake128'], + shakeNames: ['shake128', 'shake-128'], + outputLength: 32, + input: Buffer.from('00010203', 'hex'), + expected: 'c1c36925b6409a04f1b504fcbca9d82b' + + '4017277cb5ed2b2065fc1d3814d5aaf5', + }, + { + names: ['cshake-256', 'cshake256'], + shakeNames: ['shake256', 'shake-256'], + outputLength: 64, + input: Buffer.from('00010203', 'hex'), + expected: 'd008828e2b80ac9d2218ffee1d070c48' + + 'b8e4c87bff32c9699d5b6896eee0edd1' + + '64020e2be0560858d9c00c037e34a96' + + '937c561a74c412bb4c746469527281c8c', + }, +]; + +for (const vector of cshakeVectors) { + const algorithm = findHash(...vector.names); + if (algorithm === undefined) { + common.printSkipMessage(`${vector.names[0]} is not available`); + continue; + } + + exercised = true; + + const options = { + outputLength: vector.outputLength, + customization: 'Email Signature', + }; + const streaming = createHash(algorithm, options) + .update(vector.input.subarray(0, 2)) + .update(vector.input.subarray(2)) + .digest('hex'); + const partial = createHash(algorithm, options) + .update(vector.input.subarray(0, 2)); + const copyOptionReads = []; + const copied = partial.copy({ + get outputLength() { + copyOptionReads.push('outputLength'); + return vector.outputLength; + }, + get functionName() { + copyOptionReads.push('functionName'); + return undefined; + }, + get customization() { + copyOptionReads.push('customization'); + return undefined; + }, + }) + .update(vector.input.subarray(2)) + .digest('hex'); + + assert.strictEqual(streaming, vector.expected); + assert.strictEqual(copied, vector.expected); + assert.deepStrictEqual(copyOptionReads, ['outputLength']); + assert.strictEqual(hash(algorithm, vector.input, options), vector.expected); + + // BufferSource parameters have the same semantics as their string form. + const bufferOptions = { + ...options, + customization: Buffer.from(options.customization), + }; + assert.strictEqual( + createHash(algorithm, bufferOptions).update(vector.input).digest('hex'), + vector.expected, + ); + assert.strictEqual( + hash(algorithm, vector.input, bufferOptions), + vector.expected, + ); + + // Without function-name and customization parameters, cSHAKE is SHAKE. + const withoutParameters = createHash(algorithm) + .update(vector.input) + .digest('hex'); + assert.strictEqual(hash(algorithm, vector.input), withoutParameters); + + // Explicit undefined parameters have the same semantics as omitted ones. + const undefinedOptions = { + outputLength: vector.outputLength, + functionName: undefined, + customization: undefined, + }; + assert.strictEqual( + createHash(algorithm, undefinedOptions).update(vector.input).digest('hex'), + withoutParameters, + ); + assert.strictEqual( + hash(algorithm, vector.input, undefinedOptions), + withoutParameters, + ); + + // Empty BufferSource parameters are still supplied to OpenSSL, but cSHAKE + // with two empty parameters is equivalent to SHAKE. + const emptyOptions = { + outputLength: vector.outputLength, + functionName: Buffer.alloc(0), + customization: new Uint8Array(0), + }; + assert.strictEqual( + createHash(algorithm, emptyOptions).update(vector.input).digest('hex'), + withoutParameters, + ); + assert.strictEqual( + hash(algorithm, vector.input, emptyOptions), + withoutParameters, + ); + const emptyFunctionNameOptions = { + outputLength: vector.outputLength, + functionName: Buffer.alloc(0), + }; + assert.strictEqual( + createHash(algorithm, emptyFunctionNameOptions) + .update(vector.input) + .digest('hex'), + withoutParameters, + ); + assert.strictEqual( + hash(algorithm, vector.input, emptyFunctionNameOptions), + withoutParameters, + ); + + const createHashOptionReads = []; + assert.strictEqual( + createHash(algorithm, { + get outputLength() { + createHashOptionReads.push('outputLength'); + return vector.outputLength; + }, + get functionName() { + createHashOptionReads.push('functionName'); + return undefined; + }, + get customization() { + createHashOptionReads.push('customization'); + return undefined; + }, + }).update(vector.input).digest('hex'), + withoutParameters, + ); + assert.deepStrictEqual( + createHashOptionReads, + ['outputLength', 'functionName', 'customization'], + ); + + const hashOptionReads = []; + assert.strictEqual( + hash(algorithm, vector.input, { + get outputLength() { + hashOptionReads.push('outputLength'); + return vector.outputLength; + }, + get outputEncoding() { + hashOptionReads.push('outputEncoding'); + return 'hex'; + }, + get functionName() { + hashOptionReads.push('functionName'); + return undefined; + }, + get customization() { + hashOptionReads.push('customization'); + return undefined; + }, + }), + withoutParameters, + ); + assert.deepStrictEqual( + hashOptionReads, + ['outputLength', 'outputEncoding', 'functionName', 'customization'], + ); + + for (const zeroLengthOptions of [ + { outputLength: 0 }, + { + outputLength: 0, + functionName: Buffer.alloc(0), + customization: new Uint8Array(0), + }, + ]) { + assert.deepStrictEqual( + createHash(algorithm, zeroLengthOptions).update(vector.input).digest(), + Buffer.alloc(0), + ); + assert.strictEqual( + hash(algorithm, vector.input, zeroLengthOptions), + '', + ); + } + + const shake = findHash(...vector.shakeNames); + if (shake !== undefined) { + assert.strictEqual( + withoutParameters, + createHash(shake, { outputLength: vector.outputLength }) + .update(vector.input) + .digest('hex'), + ); + } + + const namedOptions = { + outputLength: vector.outputLength, + functionName: 'KMAC', + customization: 'Node.js', + }; + let namedResult; + try { + namedResult = createHash(algorithm, namedOptions) + .update(vector.input) + .digest(); + } catch { + common.printSkipMessage( + `${algorithm} does not support the KMAC function name`, + ); + } + if (namedResult !== undefined) { + assert.deepStrictEqual( + hash(algorithm, vector.input, { + ...namedOptions, + outputEncoding: 'buffer', + }), + namedResult, + ); + assert.deepStrictEqual( + createHash(algorithm, { + ...namedOptions, + functionName: Buffer.from(namedOptions.functionName), + customization: new Uint8Array(Buffer.from(namedOptions.customization)), + }).update(vector.input).digest(), + namedResult, + ); + assert.deepStrictEqual( + hash(algorithm, vector.input, { + ...namedOptions, + functionName: Buffer.from(namedOptions.functionName), + customization: new Uint8Array(Buffer.from(namedOptions.customization)), + outputEncoding: 'buffer', + }), + namedResult, + ); + } + + for (const functionName of ['', 'TupleHash', 'ParallelHash', 'KMAC']) { + const functionOptions = { + outputLength: vector.outputLength, + functionName, + }; + let functionResult; + try { + functionResult = createHash(algorithm, functionOptions) + .update(vector.input) + .digest(); + } catch { + common.printSkipMessage( + `${algorithm} does not support the ${functionName} function name`, + ); + continue; + } + assert.deepStrictEqual( + functionResult, + hash(algorithm, vector.input, { + ...functionOptions, + outputEncoding: 'buffer', + }), + ); + } + + testHashJob([ + algorithm, + vector.input, + vector.outputLength * 8, + undefined, + Buffer.from(options.customization), + ], Buffer.from(vector.expected, 'hex')); + + for (const invalidOptions of [ + { functionName: 1 }, + { customization: {} }, + ]) { + const expected = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => createHash(algorithm, invalidOptions), expected); + assert.throws( + () => hash(algorithm, vector.input, invalidOptions), + expected, + ); + } + + for (const invalidOptions of [ + { functionName: 'KMAC\0' }, + { customization: 'Node\0js' }, + { customization: Buffer.from([0x61, 0x00, 0x62]) }, + ]) { + const expected = { code: 'ERR_INVALID_ARG_VALUE' }; + assert.throws(() => createHash(algorithm, invalidOptions), expected); + assert.throws( + () => hash(algorithm, vector.input, invalidOptions), + expected, + ); + } +} + +if (cshakeVectors.some(({ names }) => findHash(...names) !== undefined)) { + for (const mismatchedOptions of [ + { functionName: 'KMAC' }, + { customization: 'Node.js' }, + { functionName: Buffer.alloc(0) }, + { customization: new Uint8Array(0) }, + ]) { + assert.throws( + () => createHash('sha256', mismatchedOptions), + { message: 'Digest method not supported' }, + ); + assert.throws( + () => hash('sha256', Buffer.from('abc'), mismatchedOptions), + { message: 'Digest options are not supported' }, + ); + } +} + +if (!exercised) { + common.printSkipMessage('cSHAKE is not available'); +} diff --git a/test/parallel/test-crypto-provider-hashes.js b/test/parallel/test-crypto-provider-hashes.js new file mode 100644 index 000000000000..166efaa0d7fd --- /dev/null +++ b/test/parallel/test-crypto-provider-hashes.js @@ -0,0 +1,258 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const assert = require('node:assert'); +const { + createHash, + createHmac, + createSign, + createVerify, + generateKeyPair, + generateKeyPairSync, + getHashes, + hash, + hkdf, + hkdfSync, + pbkdf2, + pbkdf2Sync, + privateDecrypt, + publicEncrypt, + sign, + verify, +} = require('node:crypto'); +const { hasOpenSSL3 } = require('../common/crypto'); + +if (!hasOpenSSL3 || process.features.openssl_is_boringssl) { + common.skip('OpenSSL 3 provider support is required'); +} + +const { internalBinding } = require('internal/test/binding'); +const { + HashJob, + kCryptoJobSync, + kCryptoJobWebCrypto, +} = internalBinding('crypto'); + +const hashes = getHashes(); +const lowercaseHashes = hashes.map((name) => name.toLowerCase()); +const modifiedHashes = getHashes(); +modifiedHashes.length = 0; + +assert.deepStrictEqual(hashes, [...hashes].sort()); +assert.deepStrictEqual(getHashes(), hashes); +assert.strictEqual(new Set(lowercaseHashes).size, hashes.length); +if (lowercaseHashes.includes('sha1')) { + assert(hashes.includes('RSA-SHA1')); +} +assert(!lowercaseHashes.includes('null')); +assert(!lowercaseHashes.includes('ml-dsa-mu')); +assert(!hashes.some((name) => /^\d+(?:\.\d+)+$/.test(name))); + +for (const name of hashes) { + try { + createHash(name); + } catch (err) { + assert.strictEqual(err.code, 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH'); + createHash(name, { outputLength: 32 }); + } +} + +const input = Buffer.alloc(0); +assert.throws( + () => createHash('ml-dsa-mu'), + /Digest method not supported/, +); +assert.throws( + () => hash('ml-dsa-mu', input), + { message: 'Digest method ml-dsa-mu is not supported' }, +); + +const providerVectors = { + 'keccak-kmac-128': { + aliases: ['keccak-kmac-128', 'keccak-kmac128'], + expected: '83aa04c211dc19d16912571ed0a75130' + + 'd36aebd58562dd080c1ea84a8c7d73f7', + options: { outputLength: 32 }, + }, + 'keccak-256': { + aliases: ['keccak-256'], + expected: 'c5d2460186f7233c927e7db2dcc703c0' + + 'e500b653ca82273b7bfad8045d85a470', + }, + 'sha256-192': { + aliases: ['sha2-256/192', 'sha-256/192', 'sha256-192'], + expected: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c', + }, +}; + +function testHashVector({ aliases, expected, options }) { + for (const alias of aliases) { + if (!hashes.includes(alias)) continue; + assert(!hashes.includes(alias.toUpperCase())); + + for (const name of [alias, alias.toUpperCase()]) { + const streaming = createHash(name, options).update(input).digest('hex'); + assert.strictEqual(streaming, expected); + assert.strictEqual(hash(name, input, options), expected); + } + } +} + +// These digests are tested when the active provider advertises them. +for (const name of ['keccak-kmac-128', 'keccak-256', 'sha256-192']) { + const vector = providerVectors[name]; + if (vector.aliases.some((alias) => hashes.includes(alias))) { + testHashVector(vector); + } else { + common.printSkipMessage(`${name} is not available from the active provider`); + } +} + +const keccakKmacName = providerVectors['keccak-kmac-128'].aliases + .find((alias) => hashes.includes(alias)); +if (keccakKmacName !== undefined) { + (async () => { + const { expected } = providerVectors['keccak-kmac-128']; + const { 0: err, 1: syncResult } = new HashJob( + kCryptoJobSync, + keccakKmacName, + input, + 256, + ).run(); + assert.strictEqual(err, undefined); + assert.strictEqual(Buffer.from(syncResult).toString('hex'), expected); + + const result = await new HashJob( + kCryptoJobWebCrypto, + keccakKmacName, + input, + 256, + ).run(); + assert.strictEqual(Buffer.from(result).toString('hex'), expected); + })().then(common.mustCall()); +} + +if (hashes.includes('sha256-192')) { + const operationInput = Buffer.from('abc'); + + assert.strictEqual( + createHmac('sha256-192', 'key').update(operationInput).digest('hex'), + 'd7774e586190fa2d2f4d4be4bc86ccd459a9170d52c38809', + ); + + const hkdfExpected = 'ef23757b94b5e1e46c3f981d87828d7aeb0207733ab5c78' + + 'c60df321c9e8c88e0ad54b4eecfef8c258ccd'; + assert.strictEqual( + Buffer.from(hkdfSync('sha256-192', 'key', 'salt', 'info', 42)) + .toString('hex'), + hkdfExpected, + ); + hkdf( + 'sha256-192', + 'key', + 'salt', + 'info', + 42, + common.mustSucceed((result) => { + assert.strictEqual(Buffer.from(result).toString('hex'), hkdfExpected); + }), + ); + + const pbkdf2Expected = '1fee3dd5ea13d5b563d3cc88fbc6dcf7' + + '3497aeffc3b3e6358ab3d3d1aa2aa0ee'; + assert.strictEqual( + pbkdf2Sync('password', 'salt', 2, 32, 'sha256-192').toString('hex'), + pbkdf2Expected, + ); + pbkdf2( + 'password', + 'salt', + 2, + 32, + 'sha256-192', + common.mustSucceed((result) => { + assert.strictEqual(result.toString('hex'), pbkdf2Expected); + }), + ); + + const { privateKey: ecPrivateKey, publicKey: ecPublicKey } = + generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); + const signature = sign('sha256-192', operationInput, ecPrivateKey); + assert(verify('sha256-192', operationInput, ecPublicKey, signature)); + + const streamingSignature = createSign('sha256-192') + .update(operationInput) + .sign(ecPrivateKey); + const verifier = createVerify('sha256-192'); + verifier.update(operationInput); + assert(verifier.verify(ecPublicKey, streamingSignature)); + + sign( + 'sha256-192', + operationInput, + ecPrivateKey, + common.mustSucceed((asyncSignature) => { + verify( + 'sha256-192', + operationInput, + ecPublicKey, + asyncSignature, + common.mustSucceed((result) => assert(result)), + ); + }), + ); + + const { privateKey: rsaPrivateKey, publicKey: rsaPublicKey } = + generateKeyPairSync('rsa', { modulusLength: 2048 }); + const plaintext = Buffer.from('provider digest'); + + assert.throws( + () => sign('sha256-192', plaintext, rsaPrivateKey), + { code: 'ERR_OSSL_DIGEST_NOT_ALLOWED' }, + ); + assert.deepStrictEqual( + privateDecrypt( + { key: rsaPrivateKey, oaepHash: 'sha256-192' }, + publicEncrypt( + { key: rsaPublicKey, oaepHash: 'sha256-192' }, + plaintext, + ), + ), + plaintext, + ); + + const pssOptions = [ + { + hashAlgorithm: 'sha256-192', + modulusLength: 2048, + }, + { + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256-192', + modulusLength: 2048, + }, + ]; + const keyGenerationFailed = { message: 'Key generation job failed' }; + + for (const options of pssOptions) { + assert.throws( + () => generateKeyPairSync('rsa-pss', options), + keyGenerationFailed, + ); + generateKeyPair( + 'rsa-pss', + options, + common.mustCall((err, publicKey, privateKey) => { + assert.strictEqual(err?.message, keyGenerationFailed.message); + assert.strictEqual(publicKey, undefined); + assert.strictEqual(privateKey, undefined); + }), + ); + } +} diff --git a/test/parallel/test-crypto-rsa-multiprime-jwk.js b/test/parallel/test-crypto-rsa-multiprime-jwk.js new file mode 100644 index 000000000000..be434c76e828 --- /dev/null +++ b/test/parallel/test-crypto-rsa-multiprime-jwk.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { hasFIPS } = require('../common/crypto'); +const { + createPrivateKey, +} = require('crypto'); +const { subtle } = globalThis.crypto; + +if (process.features.openssl_is_boringssl) + common.skip('multi-prime RSA is not available with BoringSSL'); +if (hasFIPS()) + common.skip('multi-prime RSA is not available in FIPS mode'); + +const privateKey = createPrivateKey( + fixtures.readKey('rsa_private_2048_3_primes.pem')); +const pkcs8 = privateKey.export({ format: 'der', type: 'pkcs8' }); +const jwk = privateKey.export({ format: 'jwk' }); + +assert.strictEqual(jwk.oth.length, 1); +assert.deepStrictEqual(Object.keys(jwk.oth[0]), ['r', 'd', 't']); + +const importedKey = createPrivateKey({ key: jwk, format: 'jwk' }); +assert.deepStrictEqual(importedKey.export({ format: 'jwk' }), jwk); +assert.deepStrictEqual( + importedKey.export({ format: 'der', type: 'pkcs8' }), + pkcs8); + +for (const field of ['r', 'd', 't']) { + const invalidJwk = { + ...jwk, + oth: [{ ...jwk.oth[0] }], + }; + delete invalidJwk.oth[0][field]; + assert.throws( + () => createPrivateKey({ key: invalidJwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); +} + +(async () => { + const algorithm = { name: 'RSA-PSS', hash: 'SHA-256' }; + const cryptoKey = await subtle.importKey( + 'pkcs8', pkcs8, algorithm, true, ['sign']); + const exportedJwk = await subtle.exportKey('jwk', cryptoKey); + + const exportedKeyMaterial = { ...exportedJwk }; + delete exportedKeyMaterial.key_ops; + delete exportedKeyMaterial.ext; + delete exportedKeyMaterial.alg; + assert.deepStrictEqual(exportedKeyMaterial, jwk); + + const importedCryptoKey = await subtle.importKey( + 'jwk', exportedJwk, algorithm, true, ['sign']); + assert.deepStrictEqual( + Buffer.from(await subtle.exportKey('pkcs8', importedCryptoKey)), + pkcs8); +})().then(common.mustCall()); diff --git a/test/parallel/test-crypto-rsa-oaep-mgf1.js b/test/parallel/test-crypto-rsa-oaep-mgf1.js new file mode 100644 index 000000000000..7ba7f19f6b5c --- /dev/null +++ b/test/parallel/test-crypto-rsa-oaep-mgf1.js @@ -0,0 +1,149 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// Tests the `mgf1Hash` option of crypto.publicEncrypt() and +// crypto.privateDecrypt(), which allows the MGF1 digest of RSA-OAEP padding to +// differ from the OAEP message digest (`oaepHash`). This is required for +// interoperability with profiles such as XML Encryption's `rsa-oaep-mgf1p`, +// where the OAEP digest may be changed but MGF1 is fixed to SHA-1. + +const assert = require('assert'); +const crypto = require('crypto'); +const fixtures = require('../common/fixtures'); +const { hasFIPS } = require('../common/crypto'); + +const constants = crypto.constants; + +const publicKey = fixtures.readKey('rsa_public.pem', 'ascii'); +const privateKey = fixtures.readKey('rsa_private.pem', 'ascii'); + +const input = Buffer.from('the quick brown fox jumps over the lazy dog'); + +// A round-trip with mismatched OAEP and MGF1 digests must succeed when both +// sides agree on the digests. +{ + const encrypted = crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + mgf1Hash: 'sha1', + }, input); + + const decrypted = crypto.privateDecrypt({ + key: privateKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + mgf1Hash: 'sha1', + }, encrypted); + + assert.deepStrictEqual(decrypted, input); +} + +// mgf1Hash actually affects the padding: a ciphertext produced with +// oaepHash=sha256 and mgf1Hash=sha1 must NOT decrypt when MGF1 defaults to the +// OAEP digest (sha256), which is the pre-existing behavior. +{ + const encrypted = crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + mgf1Hash: 'sha1', + }, input); + + assert.throws(() => { + crypto.privateDecrypt({ + key: privateKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + // No mgf1Hash: MGF1 follows oaepHash (sha256) and must fail to unpad. + }, encrypted); + }, { + code: hasFIPS(3, 5) ? 'ERR_OSSL_EVP_PROVIDER_ASYM_CIPHER_FAILURE' : + 'ERR_OSSL_RSA_OAEP_DECODING_ERROR' + }); +} + +// Backward compatibility: omitting mgf1Hash on both sides keeps MGF1 == oaepHash +// (the historical behavior), so this round-trips, and setting mgf1Hash equal to +// oaepHash is equivalent to omitting it. +{ + const encrypted = crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + }, input); + + const decrypted = crypto.privateDecrypt({ + key: privateKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + mgf1Hash: 'sha256', + }, encrypted); + + assert.deepStrictEqual(decrypted, input); +} + +// The default oaepHash is sha1, so mgf1Hash defaults to sha1 as well. A +// ciphertext encrypted with all defaults must decrypt with an explicit +// mgf1Hash: 'sha1'. +{ + const encrypted = crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + }, input); + + const decrypted = crypto.privateDecrypt({ + key: privateKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + mgf1Hash: 'sha1', + }, encrypted); + + assert.deepStrictEqual(decrypted, input); +} + +// A few other digest combinations round-trip. +for (const [oaepHash, mgf1Hash] of [ + ['sha512', 'sha1'], + ['sha384', 'sha256'], + ['sha1', 'sha256'], +]) { + const encrypted = crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash, + mgf1Hash, + }, input); + + const decrypted = crypto.privateDecrypt({ + key: privateKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash, + mgf1Hash, + }, encrypted); + + assert.deepStrictEqual(decrypted, input); +} + +// mgf1Hash must be a string. +for (const mgf1Hash of [1, true, {}, [], null]) { + assert.throws(() => { + crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + mgf1Hash, + }, input); + }, { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// An unknown mgf1Hash digest name is rejected. +assert.throws(() => { + crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256', + mgf1Hash: 'not-a-real-digest', + }, input); +}, { code: 'ERR_OSSL_EVP_INVALID_DIGEST' }); diff --git a/test/parallel/test-diagnostics-channel-crypto-fips-indicator.js b/test/parallel/test-diagnostics-channel-crypto-fips-indicator.js new file mode 100644 index 000000000000..0d7366dd3438 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-crypto-fips-indicator.js @@ -0,0 +1,184 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const assert = require('node:assert'); +const diagnosticsChannel = require('node:diagnostics_channel'); +const { once } = require('node:events'); +const { Worker } = require('node:worker_threads'); +const { + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { + createHmac, + generateKeyPairSync, + sign, + subtle, +} = require('node:crypto'); + +const channelName = 'crypto.fips.indicator'; + +if (!hasOpenSSL(3, 4)) { + common.skip('OpenSSL 3.4 or later is required'); +} else if (!hasFIPS(3, 4)) { + common.skip('an active OpenSSL 3.4+ FIPS provider is required'); +} else if (!process.execArgv.includes('--enable-fips-indicator-events')) { + spawnSyncAndExitWithoutError( + process.execPath, + ['--enable-fips-indicator-events', __filename], + ); +} else { + run().then(common.mustCall()); +} + +function nextIndicator() { + const keepAlive = setInterval(common.mustNotCall(), 10_000); + const { promise, resolve } = Promise.withResolvers(); + const subscriber = common.mustCall((event, name) => { + assert.strictEqual(name, channelName); + clearInterval(keepAlive); + assert.strictEqual( + diagnosticsChannel.unsubscribe(channelName, subscriber), true); + resolve(event); + }); + diagnosticsChannel.subscribe(channelName, subscriber); + return promise; +} + +function testUnsubscribeDuringDrain(key, privateKey) { + const keepAlive = setInterval(common.mustNotCall(), 10_000); + const { promise, resolve } = Promise.withResolvers(); + const subscriber = common.mustCall(() => { + clearInterval(keepAlive); + assert.strictEqual( + diagnosticsChannel.unsubscribe(channelName, subscriber), true); + setImmediate(common.mustCall(resolve)); + }); + diagnosticsChannel.subscribe(channelName, subscriber); + createHmac('sha256', key).digest(); + sign('sha1', Buffer.alloc(0), privateKey); + return promise; +} + +async function testNoStaleIndicator(key) { + createHmac('sha256', key).digest(); + const subscriber = common.mustNotCall(); + diagnosticsChannel.subscribe(channelName, subscriber); + await new Promise((resolve) => setImmediate(resolve)); + assert.strictEqual( + diagnosticsChannel.unsubscribe(channelName, subscriber), true); +} + +async function run() { + const key = Buffer.alloc(13); + + const keepAlive = setInterval(common.mustNotCall(), 10_000); + const { promise, resolve: resolveProbe } = Promise.withResolvers(); + const probeSubscriber = (event) => { + clearInterval(keepAlive); + diagnosticsChannel.unsubscribe(channelName, probeSubscriber); + resolveProbe(event); + }; + diagnosticsChannel.subscribe(channelName, probeSubscriber); + + let output; + try { + output = createHmac('sha256', key).digest(); + } catch (error) { + clearInterval(keepAlive); + diagnosticsChannel.unsubscribe(channelName, probeSubscriber); + assert.match(error.code, /^ERR_OSSL_/); + common.printSkipMessage( + 'the FIPS provider rejects unapproved operations before signaling'); + return; + } + + assert.strictEqual(output.byteLength, 32); + assert.deepStrictEqual(await promise, { + operation: 'HMAC', + reason: 'keysize', + blocked: false, + count: 1, + dropped: 0, + }); + + await testNoStaleIndicator(key); + + let eventPromise = nextIndicator(); + createHmac('sha256', key).digest(); + createHmac('sha256', key).digest(); + assert.deepStrictEqual(await eventPromise, { + operation: 'HMAC', + reason: 'keysize', + blocked: false, + count: 2, + dropped: 0, + }); + + const secondSubscriber = common.mustCall(); + diagnosticsChannel.subscribe(channelName, secondSubscriber); + eventPromise = nextIndicator(); + createHmac('sha256', key).digest(); + await eventPromise; + assert.strictEqual( + diagnosticsChannel.unsubscribe(channelName, secondSubscriber), true); + + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); + await testUnsubscribeDuringDrain(key, privateKey); + + const hmacKey = await subtle.importKey( + 'raw', Buffer.alloc(13), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + eventPromise = nextIndicator(); + const [signature, hmacEvent] = await Promise.all([ + subtle.sign('HMAC', hmacKey, Buffer.alloc(0)), + eventPromise, + ]); + assert.strictEqual(signature.byteLength, 32); + assert.strictEqual(hmacEvent.operation, 'HMAC'); + assert.strictEqual(hmacEvent.reason, 'keysize'); + assert.strictEqual(hmacEvent.blocked, false); + + eventPromise = nextIndicator(); + const worker = new Worker(` + 'use strict'; + const diagnosticsChannel = require('node:diagnostics_channel'); + const { parentPort, workerData } = require('node:worker_threads'); + const { createHmac } = require('node:crypto'); + + let indicatorCount = 0; + diagnosticsChannel.subscribe('crypto.fips.indicator', () => { + indicatorCount++; + }); + const result = createHmac('sha256', workerData.key).digest(); + setImmediate(() => { + parentPort.postMessage({ + indicatorCount, + length: result.byteLength, + }); + }); + `, { + eval: true, + workerData: { key }, + }); + worker.on('error', common.mustNotCall()); + const exitPromise = once(worker, 'exit'); + const [[message], workerEvent] = await Promise.all([ + once(worker, 'message'), + eventPromise, + ]); + assert.deepStrictEqual(message, { indicatorCount: 0, length: 32 }); + assert.deepStrictEqual(workerEvent, { + operation: 'HMAC', + reason: 'keysize', + blocked: false, + count: 1, + dropped: 0, + }); + const [exitCode] = await exitPromise; + assert.strictEqual(exitCode, 0); +} diff --git a/test/parallel/test-http2-reset-buffered-read.js b/test/parallel/test-http2-reset-buffered-read.js new file mode 100644 index 000000000000..0f3cafc768cb --- /dev/null +++ b/test/parallel/test-http2-reset-buffered-read.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +// A clean peer reset must retain 26.x's buffered reads and readable 'end', +// rather than introducing the reset errors and immediate destroy from main. +const body = 'buffered response'; +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.on('error', common.mustNotCall()); + stream.resume(); + stream.respond(); + stream.write(body, common.mustCall(() => stream.close())); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const request = client.request({ ':method': 'POST' }); + request.write('request body'); + request.pause(); + request.read(0); + request.on('response', common.mustCall()); + request.on('error', common.mustNotCall()); + request.on('aborted', common.mustCall(() => { + setImmediate(common.mustCall(() => { + assert.strictEqual(request.destroyed, false); + assert.strictEqual(request.readableLength, Buffer.byteLength(body)); + let received = ''; + request.on('data', (chunk) => { received += chunk; }); + request.on('end', common.mustCall(() => { + assert.strictEqual(received, body); + })); + request.resume(); + })); + })); + request.on('close', common.mustCall(() => { + assert.strictEqual(request.readableEnded, true); + assert.strictEqual(request.rstCode, http2.constants.NGHTTP2_NO_ERROR); + client.close(); + server.close(); + })); +})); diff --git a/test/parallel/test-http2-reset-pending-write.js b/test/parallel/test-http2-reset-pending-write.js new file mode 100644 index 000000000000..ddb5dcd918ff --- /dev/null +++ b/test/parallel/test-http2-reset-pending-write.js @@ -0,0 +1,67 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +// A peer reset may leave a write unfinished. Preserve the clean readable +// end in 26.x, but do not wait for writable 'finish' to destroy the stream. +for (const compat of [false, true]) { + const server = http2.createServer(); + let client; + let request; + + function onRequest(readable, writable, stream) { + readable.on('error', common.mustNotCall()); + if (writable !== readable) + writable.on('error', common.mustNotCall()); + stream.on('error', common.mustNotCall()); + readable.resume(); + readable.on('end', common.mustCall()); + stream.on('aborted', common.mustCall()); + stream.on('finish', common.mustNotCall()); + if (compat) { + // The compat response retains its existing finish-on-close behavior. + writable.on('finish', common.mustCall()); + } + stream.on('close', common.mustCall(() => { + assert.strictEqual(stream.rstCode, http2.constants.NGHTTP2_NO_ERROR); + assert.strictEqual(stream.readableEnded, true); + assert.strictEqual(stream.writableFinished, false); + assert.strictEqual(stream.destroyed, true); + client.close(); + server.close(); + })); + + writable.write('first response', common.mustCall(() => { + // Model a write whose callback cannot finish after the peer resets. + // Start the reset only once the write is pending. + stream._write = common.mustCall(() => { + setImmediate(() => request.destroy()); + }); + writable.write('pending response'); + })); + } + + if (compat) { + server.on('request', common.mustCall((req, res) => { + onRequest(req, res, req.stream); + })); + } else { + server.on('stream', common.mustCall((stream) => { + stream.respond(); + onRequest(stream, stream, stream); + })); + } + + server.listen(0, common.mustCall(() => { + client = http2.connect(`http://localhost:${server.address().port}`); + request = client.request({ ':method': 'POST' }); + request.on('close', common.mustCall()); + request.on('error', common.mustNotCall()); + request.resume(); + request.write('request body'); + })); +} diff --git a/test/parallel/test-process-env-allowed-flags-are-documented.js b/test/parallel/test-process-env-allowed-flags-are-documented.js index 67483f6242cf..9bc3cc98199a 100644 --- a/test/parallel/test-process-env-allowed-flags-are-documented.js +++ b/test/parallel/test-process-env-allowed-flags-are-documented.js @@ -67,6 +67,7 @@ const conditionalOpts = [ '--secure-heap', '--secure-heap-min', '--enable-fips', + '--enable-fips-indicator-events', '--force-fips', ].includes(opt); } diff --git a/test/parallel/test-sqlite-authz.js b/test/parallel/test-sqlite-authz.js index 69c075a57e2e..f6020ce9047e 100644 --- a/test/parallel/test-sqlite-authz.js +++ b/test/parallel/test-sqlite-authz.js @@ -1,7 +1,7 @@ 'use strict'; -const { skipIfSQLiteMissing } = require('../common'); -skipIfSQLiteMissing(); +const common = require('../common'); +common.skipIfSQLiteMissing(); const assert = require('node:assert'); const { DatabaseSync, constants } = require('node:sqlite'); @@ -288,3 +288,329 @@ suite('DatabaseSync.prototype.setAuthorizer()', () => { }); }); }); + +// SQLite forbids an authorizer callback from modifying the connection that +// invoked it, which includes preparing and stepping statements. +// See https://www.sqlite.org/c3ref/set_authorizer.html. +suite('authorizer callback reentrancy', () => { + const expectedError = 'ERR_INVALID_STATE: database cannot be accessed ' + + 'from an authorizer callback'; + const steppingError = + 'ERR_INVALID_STATE: statement is already being executed'; + + // Calls each of `cases` from inside an authorizer callback, and returns a + // `name -> outcome` map of what each one threw. + const runInAuthorizer = (db, cases) => { + const outcomes = {}; + for (const [name, fn] of Object.entries(cases)) { + let ran = false; + db.setAuthorizer(() => { + if (!ran) { + ran = true; + try { + fn(); + outcomes[name] = 'did not throw'; + } catch (err) { + outcomes[name] = `${err.code}: ${err.message}`; + } + } + return constants.SQLITE_OK; + }); + db.exec('SELECT 1'); + db.setAuthorizer(null); + if (!ran) { + outcomes[name] = 'authorizer callback did not run'; + } + } + return outcomes; + }; + + // Builds the expected `name -> outcome` map for the given case names. + const allRejected = (cases) => Object.fromEntries( + Object.keys(cases).map((name) => [name, expectedError]), + ); + + it('rejects database methods', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + const cases = { + prepare: () => db.prepare('SELECT 1'), + exec: () => db.exec('SELECT 1'), + setAuthorizer: () => db.setAuthorizer(null), + createSession: () => db.createSession(), + applyChangeset: () => db.applyChangeset(new Uint8Array([1])), + createTagStore: () => db.createTagStore(), + serialize: () => db.serialize(), + function: () => db.function('noop', () => 1), + aggregate: () => db.aggregate('agg', { start: 0, step: (acc) => acc }), + enableLoadExtension: () => db.enableLoadExtension(false), + enableDefensive: () => db.enableDefensive(true), + limits: () => { db.limits.length = 100; }, + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + // loadExtension() checks that extension loading is enabled before reaching + // the authorizer guard, so it needs a database opened with allowExtension. + it('rejects loadExtension', () => { + const db = new DatabaseSync(':memory:', { allowExtension: true }); + db.enableLoadExtension(true); + db.exec('CREATE TABLE t (x INTEGER)'); + const cases = { + loadExtension: () => db.loadExtension('/nonexistent/extension'), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + // close() and deserialize() tear down the connection, so the pre-existing + // callback depth guard already rejects them with its own message. + it('rejects methods the callback depth guard already covers', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + const snapshot = db.serialize(); + const cases = { + close: () => db.close(), + deserialize: () => db.deserialize(snapshot), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), { + close: 'ERR_INVALID_STATE: database cannot be closed while in a callback', + deserialize: 'ERR_INVALID_STATE: database cannot be deserialized ' + + 'while in a callback', + }); + }); + + it('rejects statement methods', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const stmt = db.prepare('SELECT x FROM t'); + const cases = { + run: () => stmt.run(), + get: () => stmt.get(), + all: () => stmt.all(), + iterate: () => stmt.iterate(), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + // Only the statement being stepped is unsafe to finalize. Other statements + // on the connection have their own virtual machines, so finalizing them from + // a callback is allowed. + it('allows finalizing a statement that is not being executed', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const closeStmt = db.prepare('SELECT x FROM t'); + const disposeStmt = db.prepare('SELECT x FROM t'); + const cases = { + close: () => closeStmt.close(), + dispose: () => disposeStmt[Symbol.dispose](), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), { + close: 'did not throw', + dispose: 'did not throw', + }); + }); + + // Disposal is idempotent, so a statement that is already finalized must stay + // a no-op even inside a callback. Throwing here would turn a `using` scope's + // real exception into a SuppressedError. + it('allows disposing an already-finalized statement', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + const stmt = db.prepare('SELECT x FROM t'); + stmt.close(); + const cases = { dispose: () => stmt[Symbol.dispose]() }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), { + dispose: 'did not throw', + }); + }); + + it('rejects session changeset methods', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER PRIMARY KEY, y TEXT)'); + const session = db.createSession({ table: 't' }); + db.exec("INSERT INTO t VALUES (1, 'a')"); + const cases = { + changeset: () => session.changeset(), + patchset: () => session.patchset(), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + // A statement being re-prepared inside sqlite3_step() is the case that + // actually crashes, because that statement's VM is mid-execution. + it('rejects finalizing the statement being stepped', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const stmt = db.prepare('SELECT x FROM t'); + stmt.get(); + db.exec('ALTER TABLE t ADD COLUMN y INTEGER'); + + let outcome = 'authorizer callback did not run'; + let ran = false; + db.setAuthorizer(() => { + if (!ran) { + ran = true; + try { + stmt.close(); + outcome = 'did not throw'; + } catch (err) { + outcome = `${err.code}: ${err.message}`; + } + } + return constants.SQLITE_OK; + }); + + stmt.get(); + + assert.strictEqual(outcome, steppingError); + }); + + // Unlike an already-finalized statement, disposing the one being stepped + // would free the running virtual machine, so it throws. + it('rejects disposing the statement being stepped', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const stmt = db.prepare('SELECT x FROM t'); + stmt.get(); + db.exec('ALTER TABLE t ADD COLUMN y INTEGER'); + + let outcome = 'authorizer callback did not run'; + let ran = false; + db.setAuthorizer(() => { + if (!ran) { + ran = true; + try { + stmt[Symbol.dispose](); + outcome = 'did not throw'; + } catch (err) { + outcome = `${err.code}: ${err.message}`; + } + } + return constants.SQLITE_OK; + }); + + stmt.get(); + + assert.strictEqual(outcome, steppingError); + }); + + it('rejects iterator methods', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1), (2)'); + const iter = db.prepare('SELECT x FROM t').iterate(); + const cases = { + next: () => iter.next(), + return: () => iter.return(), + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + iter.return(); + }); + + // A drained iterator holds no SQLite state, so next() and return() stay + // available and remain idempotent inside a callback. + it('allows iterator methods on a drained iterator', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const iter = db.prepare('SELECT x FROM t').iterate(); + for (const row of iter) { + assert.ok(row); + } + const done = {}; + const cases = { + next: () => { done.next = iter.next().done; }, + return: () => { done.return = iter.return().done; }, + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), { + next: 'did not throw', + return: 'did not throw', + }); + assert.deepStrictEqual(done, { next: true, return: true }); + }); + + it('rejects tag store methods', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + const sql = db.createTagStore(10); + const cases = { + run: () => sql.run`SELECT 1`, + get: () => sql.get`SELECT 1`, + all: () => sql.all`SELECT 1`, + iterate: () => sql.iterate`SELECT 1`, + }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + }); + + // clear() only drops cached statements, so invalidating the cache after a + // schema change is allowed from the callback. + it('allows clearing a tag store', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const sql = db.createTagStore(10); + assert.strictEqual(sql.all`SELECT x FROM t`.length, 1); + assert.strictEqual(sql.size, 1); + const cases = { clear: () => sql.clear() }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), { + clear: 'did not throw', + }); + assert.strictEqual(sql.size, 0); + }); + + // A statement may be re-prepared during sqlite3_step() after a schema + // change, which invokes the authorizer without an explicit prepare() call. + it('rejects reentry when the authorizer runs during a re-prepare', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + const stmt = db.prepare('SELECT x FROM t'); + stmt.get(); + db.exec('ALTER TABLE t ADD COLUMN y INTEGER'); + + let outcome = 'authorizer callback did not run'; + let ran = false; + db.setAuthorizer(() => { + if (!ran) { + ran = true; + try { + db.prepare('SELECT 1'); + outcome = 'did not throw'; + } catch (err) { + outcome = `${err.code}: ${err.message}`; + } + } + return constants.SQLITE_OK; + }); + + stmt.get(); + + assert.strictEqual(outcome, expectedError); + }); + + it('allows access again after the authorizer returns', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + const cases = { prepare: () => db.prepare('SELECT 1') }; + + assert.deepStrictEqual(runInAuthorizer(db, cases), allRejected(cases)); + + db.setAuthorizer(() => constants.SQLITE_OK); + assert.deepStrictEqual(db.prepare('SELECT 1 AS v').get(), { __proto__: null, v: 1 }); + }); +}); diff --git a/test/parallel/test-sqlite-diagnostic-channel.js b/test/parallel/test-sqlite-diagnostic-channel.js new file mode 100644 index 000000000000..8b0776c969d7 --- /dev/null +++ b/test/parallel/test-sqlite-diagnostic-channel.js @@ -0,0 +1,237 @@ +// Flags: --expose-gc +'use strict'; + +const { mustCall, skipIfSQLiteMissing } = require('../common'); +skipIfSQLiteMissing(); + +const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); +const { DatabaseSync } = require('node:sqlite'); +const { suite, it } = require('node:test'); +const { gcUntil } = require('../common/gc'); + +suite('sqlite.db.query diagnostics channel', () => { + it('subscriber receives SQL string for exec() statements', (t) => { + const calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + + assert.strictEqual(calls.length, 2); + assert.strictEqual(calls[0].sql, 'CREATE TABLE t (x INTEGER)'); + assert.strictEqual(calls[1].sql, 'INSERT INTO t VALUES (1)'); + }); + + it('subscriber receives SQL string for prepared INSERT statements', (t) => { + let calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + calls = []; // reset after setup + + using stmt = db.prepare('INSERT INTO t VALUES (?)'); + stmt.run(42); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].sql, 'INSERT INTO t VALUES (42.0)'); + }); + + it('subscriber receives SQL string for prepared SELECT statements', (t) => { + let calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + calls = []; // reset after setup + + using stmt = db.prepare('SELECT x FROM t WHERE x = ?'); + stmt.get(1); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].sql, 'SELECT x FROM t WHERE x = 1.0'); + }); + + it('subscriber receives SQL string for prepared UPDATE statements', (t) => { + let calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + calls = []; // reset after setup + + using stmt = db.prepare('UPDATE t SET x = ? WHERE x = ?'); + stmt.run(2, 1); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].sql, 'UPDATE t SET x = 2.0 WHERE x = 1.0'); + }); + + it('subscriber receives SQL string for prepared DELETE statements', (t) => { + let calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + db.exec('INSERT INTO t VALUES (1)'); + calls = []; // reset after setup + + using stmt = db.prepare('DELETE FROM t WHERE x = ?'); + stmt.run(1); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].sql, 'DELETE FROM t WHERE x = 1.0'); + }); + + it('no calls received after unsubscribe', (t) => { + const calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + + db.exec('CREATE TABLE t (x INTEGER)'); + assert.strictEqual(calls.length, 1); + + dc.unsubscribe('sqlite.db.query', handler); + db.exec('INSERT INTO t VALUES (1)'); + assert.strictEqual(calls.length, 1); // No new calls after unsubscribe + }); + + it('falls back to source SQL when expansion fails', (t) => { + let calls = []; + using db = new DatabaseSync(':memory:', { limits: { length: 1000 } }); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x TEXT)'); + calls = []; // reset after setup + + using stmt = db.prepare('INSERT INTO t VALUES (?)'); + + const longValue = 'a'.repeat(977); + stmt.run(longValue); + + assert.strictEqual(calls.length, 1); + // Falls back to source SQL with unexpanded '?' placeholder + assert.strictEqual(calls[0].sql, 'INSERT INTO t VALUES (?)'); + }); + + it('database property identifies the correct database', (t) => { + const calls = []; + using db1 = new DatabaseSync(':memory:'); + using db2 = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db1.exec('CREATE TABLE t (x INTEGER)'); + db2.exec('CREATE TABLE t (x INTEGER)'); + + assert.strictEqual(calls.length, 2); + assert.strictEqual(calls[0].database, db1); + assert.strictEqual(calls[1].database, db2); + assert.notStrictEqual(calls[0].database, calls[1].database); + }); + + it('duration is a number', (t) => { + const calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(typeof calls[0].duration, 'number'); + }); + + it('duration is non-negative', (t) => { + const calls = []; + using db = new DatabaseSync(':memory:'); + + const handler = (msg) => calls.push(msg); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + db.exec('CREATE TABLE t (x INTEGER)'); + + assert.strictEqual(calls.length, 1); + assert.ok(calls[0].duration >= 0); + }); + + it('does not publish when an unfinished statement is collected', async (t) => { + let calls = 0; + const handler = () => calls++; + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + let collected = false; + const registry = new FinalizationRegistry(() => { collected = true; }); + + (() => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (x INTEGER)'); + for (let i = 0; i < 10; i++) { + db.exec(`INSERT INTO t VALUES (${i})`); + } + + const stmt = db.prepare('SELECT x FROM t'); + registry.register(stmt); + stmt.iterate().next(); // Leave the statement unfinished. + })(); + + calls = 0; // reset after setup + await gcUntil('unfinished statement is collected', () => collected); + + assert.strictEqual(calls, 0); + }); + + it('subscriber cannot close the database or statement', (t) => { + using db = new DatabaseSync(':memory:'); + + db.exec('CREATE TABLE t (x INTEGER)'); + using stmt = db.prepare('INSERT INTO t VALUES (?)'); + + const handler = mustCall(() => { + assert.throws(() => db.close(), { code: 'ERR_INVALID_STATE' }); + assert.throws(() => stmt.close(), { code: 'ERR_INVALID_STATE' }); + assert.throws(() => stmt[Symbol.dispose](), { + code: 'ERR_INVALID_STATE', + }); + }); + dc.subscribe('sqlite.db.query', handler); + t.after(() => dc.unsubscribe('sqlite.db.query', handler)); + + stmt.run(1); + + dc.unsubscribe('sqlite.db.query', handler); + assert.deepStrictEqual(db.prepare('SELECT x FROM t').all(), [ + { __proto__: null, x: 1 }, + ]); + }); +}); diff --git a/test/parallel/test-sqlite-named-parameters.js b/test/parallel/test-sqlite-named-parameters.js index 2fd6fb0da1c3..fd0a209b6b0b 100644 --- a/test/parallel/test-sqlite-named-parameters.js +++ b/test/parallel/test-sqlite-named-parameters.js @@ -109,6 +109,22 @@ suite('StatementSync.prototype.setAllowUnknownNamedParameters()', () => { message: /The "enabled" argument must be a boolean/, }); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); + stmt.close(); + t.assert.throws(() => { + stmt.setAllowUnknownNamedParameters(true); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('options.allowUnknownNamedParameters', () => { diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index c353c8035c5f..cf0e4daa45ca 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -67,6 +67,18 @@ suite('StatementSync.prototype.get()', () => { __proto__: null, key: 'key1', val: 'val1', }); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.get(); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.all()', () => { @@ -120,6 +132,18 @@ suite('StatementSync.prototype.all()', () => { { __proto__: null, key: 'key1', val: 'val1' }, ]); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.all(); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.iterate()', () => { @@ -286,6 +310,18 @@ suite('StatementSync.prototype.iterate()', () => { stmt2.get(); it.next(); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.iterate(); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.run()', () => { @@ -385,6 +421,18 @@ suite('StatementSync.prototype.run()', () => { const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?2)'); t.assert.deepStrictEqual(stmt.run(1, 2), { changes: 1, lastInsertRowid: 1 }); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.run(); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.sourceSQL', () => { @@ -399,6 +447,16 @@ suite('StatementSync.prototype.sourceSQL', () => { const stmt = db.prepare(sql); t.assert.strictEqual(stmt.sourceSQL, sql); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => stmt.sourceSQL, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.expandedSQL', () => { @@ -418,6 +476,16 @@ suite('StatementSync.prototype.expandedSQL', () => { ); t.assert.strictEqual(stmt.expandedSQL, expanded); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => stmt.expandedSQL, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.setReadBigInts()', () => { @@ -487,6 +555,18 @@ suite('StatementSync.prototype.setReadBigInts()', () => { [`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n, }); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.setReadBigInts(true); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.setReturnArrays()', () => { @@ -505,6 +585,18 @@ suite('StatementSync.prototype.setReturnArrays()', () => { message: /The "returnArrays" argument must be a boolean/, }); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.setReturnArrays(true); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('StatementSync.prototype.get() with array output', () => { @@ -723,6 +815,18 @@ suite('StatementSync.prototype.setAllowBareNamedParameters()', () => { message: /The "allowBareNamedParameters" argument must be a boolean/, }); }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.setAllowBareNamedParameters(true); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); }); suite('options.readBigInts', () => { @@ -969,3 +1073,71 @@ suite('options.allowBareNamedParameters', () => { ); }); }); + + +suite('StatementSync.prototype.close()', () => { + test('finalizes an open statement', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + const stmt = db.prepare('SELECT * FROM storage'); + t.assert.strictEqual(stmt.close(), undefined); + t.assert.throws(() => stmt.get(), { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); + + test('throws if the statement is already finalized', (t) => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt.close(); + t.assert.throws(() => { + stmt.close(); + }, { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); +}); + +suite('StatementSync.prototype[Symbol.dispose]()', () => { + test('finalizes an open statement', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + const stmt = db.prepare('SELECT * FROM storage'); + stmt[Symbol.dispose](); + t.assert.throws(() => stmt.get(), { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); + + test('does not throw on an already-finalized statement', () => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt[Symbol.dispose](); + stmt[Symbol.dispose](); + }); + + test('works with a using declaration', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + let captured; + { + using stmt = db.prepare('SELECT * FROM storage'); + captured = stmt; + t.assert.deepStrictEqual(stmt.all(), []); + } + t.assert.throws(() => captured.get(), { + code: 'ERR_INVALID_STATE', + message: /statement has been finalized/, + }); + }); + + test('closing the database after dispose does not double-finalize', () => { + using db = new DatabaseSync(':memory:'); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + stmt[Symbol.dispose](); + db.close(); + }); +}); diff --git a/test/parallel/test-sqlite-udf-close.js b/test/parallel/test-sqlite-udf-close.js index 86794029b457..cb11e50a7f7a 100644 --- a/test/parallel/test-sqlite-udf-close.js +++ b/test/parallel/test-sqlite-udf-close.js @@ -36,4 +36,207 @@ for (const method of ['all', 'get', 'run', 'iterate']) { assert.strictEqual(db.isOpen, true); db.close(); }); + + // Finalizing the statement being stepped frees the virtual machine that + // sqlite3_step() is still running, so this must throw rather than crash. + test(`statement.close() from a UDF during statement.${method}()`, () => { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE data (value INTEGER); + INSERT INTO data VALUES (1), (2), (3); + `); + + let statement; + db.function('close_stmt', (value) => { + statement.close(); + return value; + }); + + statement = db.prepare('SELECT close_stmt(value) FROM data'); + assert.throws(() => { + if (method === 'iterate') { + for (const row of statement.iterate()) { + assert.ok(row); + } + } else { + statement[method](); + } + }, { + code: 'ERR_INVALID_STATE', + message: 'statement is already being executed', + }); + + db.close(); + }); + + // Re-running the statement being stepped resets its virtual machine + // mid-execution, which is the same use-after-free as finalizing it. + for (const reentrant of ['run', 'get', 'all', 'iterate']) { + test(`statement.${reentrant}() from a UDF during ` + + `statement.${method}()`, () => { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE data (value INTEGER, padding TEXT); + INSERT INTO data VALUES (1, '${'x'.repeat(400)}'), + (2, '${'y'.repeat(400)}'), + (3, '${'z'.repeat(400)}'); + `); + + let statement; + let thrown; + db.function('reenter', (value) => { + if (thrown === undefined) { + try { + statement[reentrant](); + thrown = null; + } catch (err) { + thrown = err; + } + } + return value; + }); + + statement = db.prepare('SELECT reenter(value), padding FROM data'); + if (method === 'iterate') { + for (const row of statement.iterate()) { + assert.ok(row); + } + } else { + statement[method](); + } + + assert.ok(thrown, `${reentrant}() was not rejected`); + assert.strictEqual(thrown.code, 'ERR_INVALID_STATE'); + assert.strictEqual(thrown.message, 'statement is already being executed'); + + db.close(); + }); + } + + // Tag store methods resolve to a cached statement, which may be the one + // currently being stepped. Each reentrant method has its own guard, so all + // four are exercised. + for (const reentrant of ['run', 'get', 'all', 'iterate']) { + test(`tag store ${reentrant} reentry during statement.${method}()`, () => { + const db = new DatabaseSync(':memory:'); + const sql = db.createTagStore(10); + db.exec(` + CREATE TABLE data (value INTEGER, padding TEXT); + INSERT INTO data VALUES (1, '${'x'.repeat(400)}'), + (2, '${'y'.repeat(400)}'); + `); + + let thrown; + db.function('reenter_tag', (value) => { + if (thrown === undefined) { + try { + // The identical tagged literal resolves to the same cached + // statement that is mid-execution. + // All four reject at call time, iterate() included, so the + // result is never consumed. + // eslint-disable-next-line no-unused-expressions + sql[reentrant]`SELECT reenter_tag(value), padding FROM data`; + thrown = null; + } catch (err) { + thrown = err; + } + } + return value; + }); + + if (method === 'iterate') { + for (const row of sql.iterate`SELECT reenter_tag(value), padding FROM data`) { + assert.ok(row); + } + } else { + // eslint-disable-next-line no-unused-expressions + sql[method]`SELECT reenter_tag(value), padding FROM data`; + } + + assert.ok(thrown, `tag store ${reentrant} reentry was not rejected`); + assert.strictEqual(thrown.code, 'ERR_INVALID_STATE'); + assert.strictEqual(thrown.message, + 'statement is already being executed'); + + db.close(); + }); + } + + // A UDF may prepare and finalize its own helper statements. Only the + // statement being stepped is off limits. + test(`UDF finalizes its own statement during statement.${method}()`, () => { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE data (value INTEGER); + INSERT INTO data VALUES (1), (2), (3); + CREATE TABLE lookup (key INTEGER, label TEXT); + INSERT INTO lookup VALUES (1, 'one'), (2, 'two'), (3, 'three'); + `); + + db.function('lookup_label', (value) => { + const helper = db.prepare('SELECT label FROM lookup WHERE key = ?'); + const label = helper.get(value).label; + helper.close(); + return label; + }); + + const statement = db.prepare('SELECT lookup_label(value) AS l FROM data'); + if (method === 'iterate') { + const labels = []; + for (const row of statement.iterate()) { + labels.push(row.l); + } + assert.deepStrictEqual(labels, ['one', 'two', 'three']); + } else if (method === 'all') { + assert.deepStrictEqual(statement.all().map((r) => r.l), + ['one', 'two', 'three']); + } else if (method === 'get') { + assert.strictEqual(statement.get().l, 'one'); + } else { + statement.run(); + } + + db.close(); + }); +} + +// iterator.return() resets the statement it is iterating, and next() steps it +// again, so both reach the virtual machine that is mid-execution. +for (const op of ['next', 'return']) { + test(`iterator.${op}() from a UDF during iteration`, () => { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE data (value INTEGER, padding TEXT); + INSERT INTO data VALUES (1, '${'x'.repeat(400)}'), + (2, '${'y'.repeat(400)}'), + (3, '${'z'.repeat(400)}'); + `); + + let iterator; + let thrown; + db.function('reenter_iter', (value) => { + if (thrown === undefined && iterator !== undefined) { + try { + iterator[op](); + thrown = null; + } catch (err) { + thrown = err; + } + } + return value; + }); + + const statement = db.prepare( + 'SELECT reenter_iter(value) AS v, padding FROM data'); + iterator = statement.iterate(); + for (const row of iterator) { + assert.ok(row); + } + + assert.ok(thrown, `iterator.${op}() was not rejected`); + assert.strictEqual(thrown.code, 'ERR_INVALID_STATE'); + assert.strictEqual(thrown.message, 'statement is already being executed'); + + db.close(); + }); } diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index d2057d1f782e..539440ea7c31 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -639,6 +639,27 @@ async function testWrongKeyType( assert.deepStrictEqual(bits, new ArrayBuffer(0)); })().then(common.mustCall()); +// HKDF output is limited to 255 digest blocks. +(async function() { + const key = await crypto.subtle.importKey( + 'raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); + const algorithm = { + name: 'HKDF', + hash: 'SHA-256', + info: new Uint8Array(0), + salt: new Uint8Array(0), + }; + + const bits = await crypto.subtle.deriveBits(algorithm, key, 65280); + assert.strictEqual(bits.byteLength, 8160); + + await assert.rejects( + crypto.subtle.deriveBits(algorithm, key, 65288), { + name: 'OperationError', + message: 'length exceeds the maximum derived bit length', + }); +})().then(common.mustCall()); + // OpenSSL limits info to 1024 bytes (async function() { const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index 60d99319691b..cd71906f7c79 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -411,6 +411,41 @@ async function testImportRaw({ name, publicUsages }) { await Promise.all(tests); })().then(common.mustCall()); +// JWK key usage validation precedes `key_ops` validation. +(async function() { + for (const { name, publicUsages, privateUsages } of testVectors) { + const jwk = keyData[name].jwk; + const publicJwk = { + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x, + }; + const isKeyAgreement = name.startsWith('X'); + const invalidUsage = isKeyAgreement ? + privateUsages[0] : publicUsages[0]; + const invalidJwk = isKeyAgreement ? publicJwk : jwk; + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...invalidJwk, key_ops: [invalidUsage, invalidUsage] }, + { name }, + true, + [invalidUsage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + + const validUsage = privateUsages[0]; + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [validUsage, validUsage] }, + { name }, + true, + [validUsage]), + { name: 'DataError', message: 'Duplicate key operation' }); + } +})().then(common.mustCall()); + { const rsaPublic = crypto.createPublicKey( fixtures.readKey('rsa_public_2048.pem')); diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index a6990bc0e154..c025136b9745 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -408,6 +408,42 @@ async function testImportRaw({ name, publicUsages }, namedCurve) { await Promise.all(tests); })().then(common.mustCall()); +// JWK key usage validation precedes `key_ops` validation. +(async function() { + const jwk = keyData['P-256'].jwk; + const publicJwk = { + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x, + y: jwk.y, + }; + + for (const { name, publicUsages, privateUsages } of testVectors) { + const invalidUsage = name === 'ECDH' ? + privateUsages[0] : publicUsages[0]; + const invalidJwk = name === 'ECDH' ? publicJwk : jwk; + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...invalidJwk, key_ops: [invalidUsage, invalidUsage] }, + { name, namedCurve: 'P-256' }, + true, + [invalidUsage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + + const validUsage = privateUsages[0]; + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [validUsage, validUsage] }, + { name, namedCurve: 'P-256' }, + true, + [validUsage]), + { name: 'DataError', message: 'Duplicate key operation' }); + } +})().then(common.mustCall()); + // https://github.com/nodejs/node/issues/45859 (async function() { diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 5cafdfd41b27..ee6eaba7a36c 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -488,6 +488,40 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { }); })().then(common.mustCall()); +// JWK key usage validation precedes `key_ops` validation. +(async function() { + const privateJwk = keyData['ML-DSA-65'].jwk; + const publicJwk = { ...privateJwk, priv: undefined }; + + for (const [jwk, usage] of [ + [privateJwk, 'verify'], + [publicJwk, 'sign'], + ]) { + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [usage, usage] }, + 'ML-DSA-65', + true, + [usage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + } + + for (const [jwk, usage] of [ + [privateJwk, 'sign'], + [publicJwk, 'verify'], + ]) { + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [usage, usage] }, + 'ML-DSA-65', + true, + [usage]), + { name: 'DataError', message: /Duplicate key operation/ }); + } +})().then(common.mustCall()); + if (!process.features.openssl_is_boringssl) { (async function() { for (const { name, privateUsages } of testVectors) { diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index 32f072e555b1..0437b9ff1986 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -500,13 +500,29 @@ if (!process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported private key format test'); } -// Regression test: JWK `key_ops` validation must recognize ML-KEM operations -// (encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits) so that -// duplicate entries are rejected +// JWK key usage validation precedes `key_ops` validation. (async function() { - for (const op of ['encapsulateKey', 'encapsulateBits', - 'decapsulateKey', 'decapsulateBits']) { - const jwk = { ...keyData['ML-KEM-768'].jwk, key_ops: [op, op] }; + const privateJwk = keyData['ML-KEM-768'].jwk; + const encapsulationOps = ['encapsulateKey', 'encapsulateBits']; + const decapsulationOps = ['decapsulateKey', 'decapsulateBits']; + + for (const op of encapsulationOps) { + const jwk = { ...privateJwk, key_ops: [op, op] }; + await assert.rejects( + subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + } + + // Duplicate entries are still rejected when the requested usages are valid. + for (const op of encapsulationOps) { + const jwk = { ...privateJwk, priv: undefined, key_ops: [op, op] }; + await assert.rejects( + subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), + { name: 'DataError', message: /Duplicate key operation/ }); + } + + for (const op of decapsulationOps) { + const jwk = { ...privateJwk, key_ops: [op, op] }; await assert.rejects( subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), { name: 'DataError', message: /Duplicate key operation/ }); diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js index 9eb611533a77..fee2e910f05a 100644 --- a/test/parallel/test-webcrypto-export-import-rsa.js +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -647,6 +647,35 @@ const testVectors = [ await Promise.all(variations); })().then(common.mustCall()); +// Type-specific JWK usage validation precedes `key_ops` validation. +(async function() { + const privateJwk = keyData[1024].jwk; + + for (const { name, publicUsages, privateUsages } of testVectors) { + const algorithm = { name, hash: 'SHA-256' }; + const invalidUsage = publicUsages[0]; + const validUsage = privateUsages[0]; + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...privateJwk, key_ops: [invalidUsage, invalidUsage] }, + algorithm, + true, + [invalidUsage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...privateJwk, key_ops: [validUsage, validUsage] }, + algorithm, + true, + [validUsage]), + { name: 'DataError', message: 'Duplicate key operation' }); + } +})().then(common.mustCall()); + { const ecPublic = crypto.createPublicKey( fixtures.readKey('ec_p256_public.pem')); diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 989fdbb47616..86d480740bb2 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -12,9 +12,11 @@ const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { createSecretKey, + getFips, KeyObject, } = require('crypto'); const { subtle } = globalThis.crypto; +const rsaMinimumModulusLength = getFips() === 1 ? 2048 : 512; const { bigIntArrayToUnsignedBigInt } = require('internal/crypto/util'); @@ -414,7 +416,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { subtle.generateKey( { name, modulusLength, publicExponent: new Uint8Array([1, 1, 1, 1, 1]), hash }, true, usages), { - message: /The publicExponent must be equivalent to an unsigned 32-bit value/, + message: 'algorithm.publicExponent must fit in an unsigned 32-bit integer', name: 'OperationError', }); @@ -438,16 +440,30 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { }); })); - await Promise.all([[1], [1, 0, 0]].map((publicExponent) => { + await Promise.all([ + [[1], 'algorithm.publicExponent must be at least 3'], + [[1, 0, 0], 'algorithm.publicExponent must be odd'], + ].map(({ 0: publicExponent, 1: message }) => { return assert.rejects(subtle.generateKey({ name, modulusLength, publicExponent: new Uint8Array(publicExponent), hash }, true, usages), { + message, name: 'OperationError', }); })); + + await assert.rejects(subtle.generateKey({ + name, + modulusLength: rsaMinimumModulusLength - 1, + publicExponent: new Uint8Array([3]), + hash, + }, true, usages), { + message: `algorithm.modulusLength must be at least ${rsaMinimumModulusLength}`, + name: 'OperationError', + }); } const kTests = [ diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index 5c13561dc260..da5df46390bb 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -982,7 +982,8 @@ const keyLengthTargets = { function getSupportedAlgorithmOperations() { const algorithms = new Map(); for (const operation of Object.keys(kSupportedAlgorithms)) { - if (operation === 'get key length') + if (operation === 'get key length' || + operation === 'get shared key length') continue; for (const name of Object.keys(kSupportedAlgorithms[operation])) { if (!algorithms.has(name)) @@ -1015,6 +1016,7 @@ const operationOrder = [ const coveredOperations = new Set([ ...operationOrder, 'get key length', + 'get shared key length', ]); for (const operation of Object.keys(kSupportedAlgorithms)) { @@ -1023,6 +1025,15 @@ for (const operation of Object.keys(kSupportedAlgorithms)) { `missing prototype pollution operation coverage for ${operation}`); } +const sharedKeyLengthAlgorithms = + Object.keys(kSupportedAlgorithms['get shared key length'] ?? {}); +assert.deepStrictEqual( + sharedKeyLengthAlgorithms, + Object.keys(kSupportedAlgorithms.encapsulate ?? {})); +assert.deepStrictEqual( + sharedKeyLengthAlgorithms, + Object.keys(kSupportedAlgorithms.decapsulate ?? {})); + const supportedAlgorithms = getSupportedAlgorithmOperations(); for (const [name, operations] of supportedAlgorithms) { const fixture = fixtures.get(name); diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index 3c40139754be..b35e94df44c9 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -152,14 +152,12 @@ async function testVerify({ name, message: /Key algorithm mismatch/ }); - if (name === 'Ed448' && supportsContext) { + if (name === 'Ed448') { // Test failure when too long context await assert.rejects( - subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); } @@ -278,14 +276,12 @@ async function testSign({ name, message: /Key algorithm mismatch/ }); - if (name === 'Ed448' && supportsContext) { + if (name === 'Ed448') { // Test failure when too long context await assert.rejects( - subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); } } diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index b11e65ade791..67f90d2a0e53 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -101,11 +101,9 @@ async function testVerify({ name, // Test failure when too long context await assert.rejects( - subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); // Test failure when signature is altered @@ -209,11 +207,9 @@ async function testSign({ name, // Test failure when too long context await assert.rejects( - subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); } diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 0ccbf431f147..3f8a916846ce 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -205,22 +205,28 @@ async function testSaltLength(keyLength, hash, hLen) { const data = Buffer.from('Hello, world!'); const max = keyLength / 8 - hLen - 2; - const signature = await subtle.sign({ name: 'RSA-PSS', saltLength: max }, privateKey, data); - await assert.rejects( - subtle.sign({ name: 'RSA-PSS', saltLength: max + 1 }, privateKey, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause?.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause?.message, `The value of "algorithm.saltLength" is out of range. It must be >= 0 && <= ${max}. Received ${max + 1}`); - return true; - }); - await subtle.verify({ name: 'RSA-PSS', saltLength: max }, publicKey, signature, data); - await assert.rejects( - subtle.verify({ name: 'RSA-PSS', saltLength: max + 1 }, publicKey, signature, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause?.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause?.message, `The value of "algorithm.saltLength" is out of range. It must be >= 0 && <= ${max}. Received ${max + 1}`); - return true; - }); + const signature = await subtle.sign( + { name: 'RSA-PSS', saltLength: max }, privateKey, data); + assert.strictEqual(await subtle.verify( + { name: 'RSA-PSS', saltLength: max }, publicKey, signature, data), true); + + for (const saltLength of [max + 1, 0x7fffffff]) { + await assert.rejects( + subtle.sign({ name: 'RSA-PSS', saltLength }, privateKey, data), { + name: 'OperationError', + }); + assert.strictEqual(await subtle.verify( + { name: 'RSA-PSS', saltLength }, publicKey, signature, data), false); + } + + for (const saltLength of [0x80000000, 0xffffffff]) { + await assert.rejects( + subtle.sign({ name: 'RSA-PSS', saltLength }, privateKey, data), { + name: 'OperationError', + }); + assert.strictEqual(await subtle.verify( + { name: 'RSA-PSS', saltLength }, publicKey, signature, data), false); + } } (async function() { diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index 43d3ee03c8bc..d0a88f0057b8 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -61,21 +61,45 @@ function supportsRawSecret(alg) { return false; } -function supportsEncapsulatedRawSecret(alg) { +function getSharedKeyLength(alg) { + switch (alg?.name?.toLowerCase?.() ?? alg?.toLowerCase?.()) { + case 'ml-kem-512': + case 'ml-kem-768': + case 'ml-kem-1024': + return 256; + } +} + +function supportsEncapsulatedRawSecret(encapsulationAlgorithm, alg) { if (!supportsRawSecret(alg)) return false; - switch (alg?.name?.toLowerCase?.()) { + + const sharedKeyLength = getSharedKeyLength(encapsulationAlgorithm); + const name = alg?.name?.toLowerCase?.() ?? alg?.toLowerCase?.(); + if (name?.startsWith('aes')) { + return sharedKeyLength === 128 || + sharedKeyLength === 192 || + sharedKeyLength === 256; + } + + switch (name) { + case 'chacha20-poly1305': + return sharedKeyLength === 256; case 'hmac': + if (sharedKeyLength === 0) return false; + // Fall through case 'kmac128': case 'kmac256': - return typeof alg.length !== 'number' || Math.ceil(alg.length / 8) === 32; + return typeof alg !== 'object' || + typeof alg.length !== 'number' || + Math.ceil(alg.length / 8) * 8 === sharedKeyLength; default: - return true; + return sharedKeyLength !== undefined; } } for (const encap of vectors.encapsulateBits) { for (const imp of vectors.importKey) { - if (supportsEncapsulatedRawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(encap[1], imp[1])) { vectors.encapsulateKey.push([encap[0] && imp[0], encap[1], imp[1]]); } else { vectors.encapsulateKey.push([false, encap[1], imp[1]]); @@ -85,7 +109,7 @@ for (const encap of vectors.encapsulateBits) { for (const decap of vectors.decapsulateBits) { for (const imp of vectors.importKey) { - if (supportsEncapsulatedRawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(decap[1], imp[1])) { vectors.decapsulateKey.push([decap[0] && imp[0], decap[1], imp[1]]); } else { vectors.decapsulateKey.push([false, decap[1], imp[1]]); diff --git a/test/parallel/test-webcrypto-util.js b/test/parallel/test-webcrypto-util.js index 9763acfb71e0..c0c565a15f3d 100644 --- a/test/parallel/test-webcrypto-util.js +++ b/test/parallel/test-webcrypto-util.js @@ -27,8 +27,14 @@ const { bigIntArrayToUnsignedInt(new Uint8Array([1, 0, 1])), 65537); assert.strictEqual( - bigIntArrayToUnsignedInt(new Uint8Array([1, 0, 0, 0, 0])), - undefined); + bigIntArrayToUnsignedInt(new Uint8Array([0, 0, 1, 0, 1])), + 65537); + assert.throws( + () => bigIntArrayToUnsignedInt(new Uint8Array([1, 0, 0, 0, 0])), + { + name: 'OperationError', + message: 'algorithm.publicExponent must fit in an unsigned 32-bit integer', + }); } { diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 2a32b330f7d8..db856a75cca2 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -59,7 +59,10 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { skipSubtests( ['getPublicKey.tentative.https.any.js', /ml-(?:kem|dsa)/i], - ['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]); + [ + 'supports-modern.tentative.https.any.js', + /(?:ml-(?:kem|dsa)|(?:en|de)capsulateKey)/i, + ]); } if (process.features.openssl_is_boringssl) { diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index babf0464bb66..33607cbf2bda 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -86,6 +86,7 @@ const customTypesMap = { 'Hash': 'crypto.html#class-hash', 'Hmac': 'crypto.html#class-hmac', 'KeyObject': 'crypto.html#class-keyobject', + 'Mac': 'crypto.html#class-mac', 'Sign': 'crypto.html#class-sign', 'Verify': 'crypto.html#class-verify', 'crypto.constants': 'crypto.html#cryptoconstants', diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index cb66ac48fd82..eb40d33c513c 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -1,6 +1,8 @@ declare namespace InternalCryptoBinding { type Buffer = Uint8Array; - type ByteSource = string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView; + type BufferSource = ArrayBuffer | SharedArrayBuffer | ArrayBufferView; + type OptionalBufferSource = BufferSource | undefined; + type ByteSource = string | BufferSource; type OptionalByteSource = ByteSource | undefined; type JwkKey = Record; type KeyFormatDER = 0; @@ -300,6 +302,8 @@ declare namespace InternalCryptoBinding { algorithm: string, data: ByteSource, outputLength?: number, + functionName?: OptionalBufferSource, + customization?: OptionalBufferSource, ): CryptoJobForMode; } @@ -640,6 +644,11 @@ declare namespace InternalCryptoBinding { digest(encoding?: string): string | Buffer; } + interface MacHandle { + update(data: ByteSource, encoding?: string): boolean; + final(encoding?: string): string | Buffer; + } + interface CipherBaseHandle { update(data: ByteSource, inputEncoding?: string): Buffer; final(): Buffer; @@ -788,6 +797,7 @@ declare namespace InternalCryptoBinding { padding: number, oaepHash: string | undefined, oaepLabel: OptionalByteSource, + mgf1Hash: string | undefined, ] ) => Buffer; } @@ -826,6 +836,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, @@ -838,8 +850,22 @@ export interface CryptoBinding { xofLen?: number, algorithmId?: number, algorithmCache?: Record, + functionName?: InternalCryptoBinding.OptionalBufferSource, + customization?: InternalCryptoBinding.OptionalBufferSource, ) => InternalCryptoBinding.HashHandle; Hmac: new () => InternalCryptoBinding.HmacHandle; + Mac: new ( + algorithm: string, + algorithmId: number, + algorithmCache: Record, + key: InternalCryptoBinding.PreparedSecretKeyData, + digest?: string, + cipher?: string, + iv?: InternalCryptoBinding.OptionalBufferSource, + customization?: InternalCryptoBinding.OptionalBufferSource, + salt?: InternalCryptoBinding.OptionalBufferSource, + outputLength?: number, + ) => InternalCryptoBinding.MacHandle; KeyObjectHandle: new () => InternalCryptoBinding.KeyObjectHandle; SecureContext: new () => InternalCryptoBinding.SecureContextHandle; Sign: new () => InternalCryptoBinding.SignHandle; @@ -954,6 +980,7 @@ export interface CryptoBinding { ]; getBundledRootCertificates(): string[]; getCachedAliases(): Record; + getCachedMacAliases(): Record; getCertificateCompressionAlgorithms(): string[]; getCipherInfo( nameOrNid: string | number, @@ -965,7 +992,9 @@ export interface CryptoBinding { getCurves(): string[]; getExtraCACertificates(): string[]; getFipsCrypto(): 0 | 1; + getFipsCryptoGeneration(): bigint; getHashes(): string[]; + getMacs(): string[]; isCryptoKey(key: unknown): boolean; isKeyObject(key: unknown): boolean; isX509Certificate(value: unknown): boolean; @@ -982,6 +1011,8 @@ export interface CryptoBinding { outputEncoding: string, outputEncodingId?: number, outputLength?: number, + functionName?: InternalCryptoBinding.OptionalBufferSource, + customization?: InternalCryptoBinding.OptionalBufferSource, ): string | InternalCryptoBinding.Buffer; parseX509(data: InternalCryptoBinding.ByteSource): InternalCryptoBinding.X509CertificateHandle; privateDecrypt: InternalCryptoBinding.PublicKeyCipher; @@ -992,6 +1023,7 @@ export interface CryptoBinding { secureBuffer(length: number): Uint8Array | undefined; secureHeapUsed(): bigint | undefined; setEngine?(engine: string, flags: number): void; + setupFipsIndicatorChannel(): void; setFipsCrypto(fips: boolean | number): void; startLoadingCertificatesOffThread(): void; testFipsCrypto(): 0 | 1; diff --git a/typings/internalBinding/diagnostics_channel.d.ts b/typings/internalBinding/diagnostics_channel.d.ts index e6297d45ace0..90069e52712f 100644 --- a/typings/internalBinding/diagnostics_channel.d.ts +++ b/typings/internalBinding/diagnostics_channel.d.ts @@ -1,5 +1,7 @@ export interface DiagnosticsChannelBinding { subscribers: Uint32Array; + notifyChannelActive(index: number): void; + notifyChannelInactive(index: number): void; linkNativeChannel( callback: (name: string, index: number) => object | undefined, ): void;