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/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index d334d17c300b..8b707b4f89c4 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -7200,6 +7200,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 @@ -7227,22 +7300,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; @@ -7252,8 +7345,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) { diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 8c09ac5f165d..79f403788cf4 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -366,6 +366,7 @@ class DataPointer; class DHPointer; class ECKeyPointer; class EVPKeyPointer; +class MacCache; class EVPMacCtxPointer; class EVPMacPointer; class EVPMDCtxPointer; @@ -1887,6 +1888,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; @@ -1905,6 +1961,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); @@ -1941,6 +1999,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: diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 3555414444b3..429990b70289 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -7,8 +7,8 @@ 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'); @@ -2543,6 +2543,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)` + +> 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)` + +An invalid MAC algorithm was specified. + ### `ERR_CRYPTO_INVALID_MESSAGELEN` @@ -1125,6 +1135,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` @@ -4693,6 +4734,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/lib/crypto.js b/lib/crypto.js index ac4b0a33efb8..b44ae9de4e5d 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -112,6 +112,9 @@ const { Hmac, hash, } = require('internal/crypto/hash'); +const { + Mac, +} = require('internal/crypto/provider_mac'); const { X509Certificate, } = require('internal/crypto/x509'); @@ -119,6 +122,7 @@ const { getCiphers, getCurves, getHashes, + getMacs, setEngine, secureHeapUsed, } = require('internal/crypto/util'); @@ -170,6 +174,10 @@ function createHmac(hmac, key, options) { return new Hmac(hmac, key, options); } +function createMac(algorithm, key, options) { + return new Mac(algorithm, key, options); +} + function createSign(algorithm, options) { return new Sign(algorithm, options); } @@ -191,6 +199,7 @@ module.exports = { createECDH, createHash, createHmac, + createMac, createPrivateKey, createPublicKey, createSecretKey, @@ -204,6 +213,7 @@ module.exports = { getCurves, getDiffieHellman: createDiffieHellmanGroup, getHashes, + getMacs, hkdf, hkdfSync, pbkdf2, diff --git a/lib/internal/crypto/provider_mac.js b/lib/internal/crypto/provider_mac.js new file mode 100644 index 000000000000..f972ac00be21 --- /dev/null +++ b/lib/internal/crypto/provider_mac.js @@ -0,0 +1,262 @@ +'use strict'; + +const { + FunctionPrototypeCall, + ObjectSetPrototypeOf, + StringPrototypeIncludes, + StringPrototypeToLowerCase, + Symbol, +} = primordials; + +const { + Mac: _Mac, +} = internalBinding('crypto'); + +const { + getCachedMacId, + getMacCache, + kHandle, +} = require('internal/crypto/util'); + +const { + getKeyObjectHandle, + getKeyObjectType, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + normalizeEncoding, +} = require('internal/util'); + +const { + codes: { + ERR_CRYPTO_MAC_FINALIZED, + ERR_CRYPTO_MAC_UPDATE_FAILED, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + }, +} = require('internal/errors'); + +const { + validateEncoding, + validateObject, + validateString, + validateUint32, +} = require('internal/validators'); + +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); + +const LazyTransform = require('internal/streams/lazy_transform'); + +const kState = Symbol('kState'); +const kFinalized = Symbol('kFinalized'); + +function validateName(value, name) { + validateString(value, name); + if (value.length === 0 || StringPrototypeIncludes(value, '\0')) { + throw new ERR_INVALID_ARG_VALUE( + name, value, 'must be non-empty and contain no NUL bytes'); + } + return value; +} + +function normalizeBytes(value, name) { + if (!isArrayBufferView(value) && !isAnyArrayBuffer(value)) { + throw new ERR_INVALID_ARG_TYPE( + name, ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], value); + } + return value; +} + +function createHandle(algorithm, key, options) { + const name = validateName(algorithm, 'algorithm'); + let digest; + let cipher; + let iv; + let customization; + let salt; + let outputLength; + let hasExtendedOptions = false; + + if (options !== undefined) { + validateObject(options, 'options'); + + const digestOption = options.digest; + if (digestOption !== undefined) { + digest = validateName(digestOption, 'options.digest'); + } + const cipherOption = options.cipher; + if (cipherOption !== undefined) { + cipher = validateName(cipherOption, 'options.cipher'); + hasExtendedOptions = true; + } + const ivOption = options.iv; + if (ivOption !== undefined) { + iv = normalizeBytes(ivOption, 'options.iv'); + hasExtendedOptions = true; + } + const customizationOption = options.customization; + if (customizationOption !== undefined) { + customization = normalizeBytes( + customizationOption, 'options.customization'); + hasExtendedOptions = true; + } + const saltOption = options.salt; + if (saltOption !== undefined) { + salt = normalizeBytes(saltOption, 'options.salt'); + hasExtendedOptions = true; + } + const outputLengthOption = options.outputLength; + if (outputLengthOption !== undefined) { + outputLength = outputLengthOption; + validateUint32(outputLength, 'options.outputLength'); + outputLength += 0; + hasExtendedOptions = true; + } + } + + key = normalizeKey(key); + const id = getCachedMacId(name); + const cache = getMacCache(); + if (!hasExtendedOptions) { + return new _Mac(name, id, cache, key, digest); + } + return new _Mac( + name, + id, + cache, + key, + digest, + cipher, + iv, + customization, + salt, + outputLength, + ); +} + +function normalizeKey(key) { + if (isKeyObject(key)) { + if (getKeyObjectType(key) !== 'secret') { + throw new ERR_INVALID_ARG_TYPE( + 'key', ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'KeyObject'], key); + } + return getKeyObjectHandle(key); + } + if (!isArrayBufferView(key) && !isAnyArrayBuffer(key)) { + throw new ERR_INVALID_ARG_TYPE( + 'key', ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'KeyObject'], key); + } + return key; +} + +function normalizeOutputEncoding(outputEncoding) { + if (outputEncoding === undefined) return 'buffer'; + validateString(outputEncoding, 'outputEncoding'); + if (StringPrototypeToLowerCase(outputEncoding) === 'buffer') return 'buffer'; + const normalized = normalizeEncoding(outputEncoding); + if (normalized === undefined) { + throw new ERR_INVALID_ARG_VALUE('outputEncoding', outputEncoding); + } + return normalized; +} + +function normalizeInputEncoding(data, inputEncoding) { + if (inputEncoding === undefined) return undefined; + validateString(inputEncoding, 'inputEncoding'); + const normalized = normalizeEncoding(inputEncoding); + if (normalized === undefined) { + throw new ERR_INVALID_ARG_VALUE('inputEncoding', inputEncoding); + } + validateEncoding(data, normalized); + return normalized; +} + +function updateHandle(mac, data, encoding) { + const state = mac[kState]; + let updated; + try { + updated = mac[kHandle].update(data, encoding); + } catch (error) { + state[kFinalized] = true; + throw error; + } + if (!updated) { + state[kFinalized] = true; + throw new ERR_CRYPTO_MAC_UPDATE_FAILED(); + } +} + +function finalizeHandle(mac, outputEncoding) { + const state = mac[kState]; + state[kFinalized] = true; + return mac[kHandle].final(outputEncoding); +} + +function Mac(algorithm, key, options) { + if (!new.target) return new Mac(algorithm, key, options); + this[kHandle] = createHandle(algorithm, key, options); + this[kState] = { + [kFinalized]: false, + }; + FunctionPrototypeCall(LazyTransform, this, options); +} + +ObjectSetPrototypeOf(Mac.prototype, LazyTransform.prototype); +ObjectSetPrototypeOf(Mac, LazyTransform); + +Mac.prototype._transform = function _transform(chunk, encoding, callback) { + if (this[kState][kFinalized]) { + callback(new ERR_CRYPTO_MAC_FINALIZED()); + return; + } + try { + updateHandle(this, chunk, encoding); + } catch (error) { + callback(error); + return; + } + callback(); +}; + +Mac.prototype._flush = function _flush(callback) { + if (this[kState][kFinalized]) { + callback(new ERR_CRYPTO_MAC_FINALIZED()); + return; + } + try { + const result = finalizeHandle(this); + if (result.length !== 0) this.push(result); + } catch (error) { + callback(error); + return; + } + callback(); +}; + +Mac.prototype.update = function update(data, encoding) { + if (this[kState][kFinalized]) throw new ERR_CRYPTO_MAC_FINALIZED(); + if (typeof data === 'string') { + encoding = normalizeInputEncoding(data, encoding); + } else if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + 'data', ['string', 'Buffer', 'TypedArray', 'DataView'], data); + } + updateHandle(this, data, encoding); + return this; +}; + +Mac.prototype.final = function final(outputEncoding) { + if (this[kState][kFinalized]) throw new ERR_CRYPTO_MAC_FINALIZED(); + if (outputEncoding === undefined) return finalizeHandle(this); + outputEncoding = normalizeOutputEncoding(outputEncoding); + if (outputEncoding === 'buffer') return finalizeHandle(this); + return finalizeHandle(this, outputEncoding); +}; + +module.exports = { + Mac, +}; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 1de25e514793..f710d0b184f8 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -36,9 +36,11 @@ const { getCiphers: _getCiphers, getCurves: _getCurves, getHashes: _getHashes, + getMacs: _getMacs, setEngine: _setEngine, secureHeapUsed: _secureHeapUsed, getCachedAliases, + getCachedMacAliases, getOpenSSLSecLevelCrypto: getOpenSSLSecLevel, EVP_PKEY_ML_DSA_44, EVP_PKEY_ML_DSA_65, @@ -120,8 +122,12 @@ function toBuf(val, encoding) { } let _hashCache; +let _macCache; if (isBuildingSnapshot()) { - addSerializeCallback(() => { _hashCache = undefined; }); + addSerializeCallback(() => { + _hashCache = undefined; + _macCache = undefined; + }); } function getHashCache() { @@ -134,6 +140,16 @@ function getHashCache() { return _hashCache; } +function getMacCache() { + while (_macCache === undefined) { + const generation = getFipsCryptoGeneration(); + const cache = getCachedMacAliases(); + if (generation !== getFipsCryptoGeneration()) continue; + _macCache = cache; + } + return _macCache; +} + function cachedArrayByFipsGeneration(fn, onRefresh) { let result; let generation; @@ -164,6 +180,11 @@ function getCachedHashId(algorithm) { return result === undefined ? -1 : result; } +function getCachedMacId(algorithm) { + const result = getMacCache()[algorithm]; + return result === undefined ? -1 : result; +} + const getCiphers = cachedArrayByFipsGeneration( () => filterDuplicateStrings(_getCiphers())); const getHashes = cachedArrayByFipsGeneration( @@ -171,6 +192,11 @@ const getHashes = cachedArrayByFipsGeneration( () => { _hashCache = undefined; }); +const getMacs = cachedArrayByFipsGeneration( + () => filterDuplicateStrings(_getMacs(), true), + () => { + _macCache = undefined; + }); const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); @@ -1143,6 +1169,7 @@ module.exports = { getCurves, getDataViewOrTypedArrayBuffer, getHashes, + getMacs, getOptionalByteLength, emitOpenSSLEngineDeprecation, kHandle, @@ -1177,5 +1204,7 @@ module.exports = { secureHeapUsed, getCachedHashId, getHashCache, + getCachedMacId, + getMacCache, getOpenSSLSecLevel, }; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 438bde842d8b..221a40ecdf86 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1188,8 +1188,12 @@ E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError); E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', 'Invalid key object type %s, expected %s.', TypeError); +E('ERR_CRYPTO_INVALID_MAC', 'Invalid MAC: %s', TypeError); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error); +E('ERR_CRYPTO_MAC_FINALIZED', 'MAC already finalized', Error); +E('ERR_CRYPTO_MAC_NOT_SUPPORTED', 'MAC is not supported', Error); +E('ERR_CRYPTO_MAC_UPDATE_FAILED', 'MAC update failed', Error); E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); // Switch to TypeError. The current implementation does not seem right. diff --git a/node.gyp b/node.gyp index b99755575020..8cf405215249 100644 --- a/node.gyp +++ b/node.gyp @@ -398,6 +398,7 @@ 'src/crypto/crypto_kem.cc', 'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_kmac.cc', + 'src/crypto/crypto_mac.cc', 'src/crypto/crypto_turboshake.cc', 'src/crypto/crypto_random.cc', 'src/crypto/crypto_rsa.cc', @@ -415,6 +416,7 @@ 'src/crypto/crypto_dh.h', 'src/crypto/crypto_hmac.h', 'src/crypto/crypto_kmac.h', + 'src/crypto/crypto_mac.h', 'src/crypto/crypto_turboshake.h', 'src/crypto/crypto_rsa.h', 'src/crypto/crypto_spkac.h', diff --git a/src/crypto/README.md b/src/crypto/README.md index ad06cf989276..c9a349c7f3c5 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -43,6 +43,7 @@ following table: | `crypto_hkdf` | HKDF (Key derivation) implementation. | | `crypto_hmac` | HMAC implementations. | | `crypto_keys` | Utilities for using and generating secret, private, and public keys. | +| `crypto_mac` | Provider-generic MAC implementations. | | `crypto_pbkdf2` | PBKDF2 key / bit generation implementation. | | `crypto_rsa` | RSA Key Generation functions. | | `crypto_scrypt` | Scrypt key / bit generation implementation. | diff --git a/src/crypto/crypto_mac.cc b/src/crypto/crypto_mac.cc new file mode 100644 index 000000000000..7f43e760ccf7 --- /dev/null +++ b/src/crypto/crypto_mac.cc @@ -0,0 +1,656 @@ +#include "crypto/crypto_mac.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "node_errors.h" +#include "string_bytes.h" +#include "v8.h" + +#if OPENSSL_WITH_EVP_MAC +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace node { + +using v8::Array; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Int32; +using v8::Isolate; +using v8::Local; +using v8::LocalVector; +using v8::Name; +using v8::Null; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { + +#if OPENSSL_WITH_EVP_MAC +namespace { + +struct InitializedMac final { + ncrypto::EVPMacCtxPointer context; + size_t output_size = 0; + bool has_output_length = false; +}; + +struct MaybeCachedMac final { + EVP_MAC* cached_mac = nullptr; + ncrypto::EVPMacPointer mac; + int32_t cache_id = -1; + ncrypto::MacKind kind = ncrypto::MacKind::kOther; +}; + +bool MaybeThrowCryptoError(Environment* env, const char* message) { + const unsigned long error = ERR_get_error(); // NOLINT(runtime/int) + if (error == 0) return false; + ThrowCryptoError(env, error, message); + return true; +} + +void ThrowMacError(Environment* env, const char* message) { + if (!MaybeThrowCryptoError(env, message)) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, message); + } +} + +void ResetMacCache(Environment* env, + uint64_t generation, + Local algorithm_cache = Local()) { + ncrypto::MacCache* cache = env->provider_mac_cache.get(); + CHECK_NOT_NULL(cache); + if (!algorithm_cache.IsEmpty()) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + for (const auto& entry : cache->aliases()) { + if (algorithm_cache + ->Set(context, + OneByteString(isolate, entry.first), + Int32::New(isolate, -1)) + .IsNothing()) { + return; + } + } + } + cache->reset(generation); + env->supported_mac_algorithms.clear(); + env->supported_mac_algorithms_initialized = false; + env->mac_cache_generation = generation; +} + +bool SynchronizeMacCache(Environment* env, + Local algorithm_cache = Local()) { + const uint64_t generation = ncrypto::getFipsStateGeneration(); + if (env->mac_cache_generation == generation) return false; + ResetMacCache(env, generation, algorithm_cache); + return true; +} + +ncrypto::MacCache::Result GetCachedMacByID( + Environment* env, + int32_t id, + Local algorithm_cache = Local()) { + if (SynchronizeMacCache(env, algorithm_cache) || + env->provider_mac_cache == nullptr) { + return {}; + } + return env->provider_mac_cache->lookup(id, env->mac_cache_generation); +} + +MaybeCachedMac FetchAndMaybeCacheMac( + Environment* env, + const char* name, + Local algorithm_cache = Local()) { + SynchronizeMacCache(env, algorithm_cache); + if (env->isolate()->HasPendingException()) return {}; + ncrypto::MacCache* cache = env->provider_mac_cache.get(); + CHECK_NOT_NULL(cache); + const uint64_t generation = env->mac_cache_generation; + + if (auto cached = cache->lookup(name, generation); cached.mac != nullptr) { + return {cached.mac, {}, cached.id, cached.kind}; + } + + ncrypto::EVPMacPointer mac; + { + ncrypto::MarkPopErrorOnReturn mark_pop_error_on_return; + mac = ncrypto::EVPMacPointer::Fetch(name); + } + if (!mac) return {}; + + if (generation == ncrypto::getFipsStateGeneration()) { + auto cached = cache->insert(name, std::move(mac), generation); + if (cached.mac != nullptr) { + return {cached.mac, {}, cached.id, cached.kind}; + } + } + + const ncrypto::MacKind kind = ncrypto::MacCache::GetKind(mac.get()); + return {nullptr, std::move(mac), -1, kind}; +} + +EVP_MAC* GetMacImplementation(Environment* env, + Local algorithm, + Local cache_id_value, + Local algorithm_cache, + ncrypto::EVPMacPointer* mac_owner, + ncrypto::MacKind* kind) { + CHECK(algorithm->IsString()); + CHECK(cache_id_value->IsInt32()); + CHECK(algorithm_cache->IsObject()); + CHECK_NOT_NULL(mac_owner); + CHECK_NOT_NULL(kind); + + Local cache = algorithm_cache.As(); + const int32_t cache_id = cache_id_value.As()->Value(); + if (cache_id != -1) { + auto cached = GetCachedMacByID(env, cache_id, cache); + if (cached.mac != nullptr) { + *kind = cached.kind; + return cached.mac; + } + if (env->isolate()->HasPendingException()) return nullptr; + } + + Isolate* isolate = env->isolate(); + Utf8Value utf8(isolate, algorithm); + MaybeCachedMac result = FetchAndMaybeCacheMac(env, *utf8, cache); + if (env->isolate()->HasPendingException()) return nullptr; + if (result.cache_id != -1) { + if (cache + ->Set( + env->context(), algorithm, Int32::New(isolate, result.cache_id)) + .IsNothing()) { + return nullptr; + } + } + + if (result.cached_mac != nullptr) { + *kind = result.kind; + return result.cached_mac; + } + if (result.mac) { + *mac_owner = std::move(result.mac); + *kind = result.kind; + return mac_owner->get(); + } + return nullptr; +} + +bool IsSettableParameter(const ncrypto::EVPMacCtxPointer& context, + const char* name, + unsigned int type) { + const OSSL_PARAM* settable = context.getSettableParams(); + const OSSL_PARAM* descriptor = + settable == nullptr ? nullptr : OSSL_PARAM_locate_const(settable, name); + return descriptor != nullptr && descriptor->data_type == type; +} + +bool RequireSettableParameter(Environment* env, + const ncrypto::EVPMacCtxPointer& context, + Local algorithm, + const char* option, + const char* parameter, + unsigned int type) { + if (IsSettableParameter(context, parameter, type)) return true; + Utf8Value name(env->isolate(), algorithm); + THROW_ERR_INVALID_ARG_VALUE( + env, + "The property 'options.%s' is not supported by MAC %s", + option, + *name); + return false; +} + +bool InitializeMacContext(Environment* env, + const FunctionCallbackInfo& args, + InitializedMac* output) { + CHECK(args.Length() == 5 || args.Length() == 10); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsInt32()); + CHECK(args[2]->IsObject()); + + Isolate* isolate = env->isolate(); + ByteSource key = ByteSource::FromSecretKeyBytes(env, args[3]); + + std::optional digest; + if (!args[4]->IsUndefined()) { + CHECK(args[4]->IsString()); + digest.emplace(isolate, args[4]); + } + std::optional cipher; + if (!args[5]->IsUndefined()) { + CHECK(args[5]->IsString()); + cipher.emplace(isolate, args[5]); + } + + const bool has_iv = !args[6]->IsUndefined(); + ByteSource iv; + if (has_iv) iv = ByteSource::FromBuffer(args[6]); + const bool has_customization = !args[7]->IsUndefined(); + ByteSource customization; + if (has_customization) customization = ByteSource::FromBuffer(args[7]); + const bool has_salt = !args[8]->IsUndefined(); + ByteSource salt; + if (has_salt) salt = ByteSource::FromBuffer(args[8]); + + const bool has_output_length = !args[9]->IsUndefined(); + size_t output_length = 0; + if (has_output_length) { + CHECK(args[9]->IsUint32()); + output_length = args[9].As()->Value(); + } + + ncrypto::EVPMacPointer mac_owner; + ncrypto::MacKind kind = ncrypto::MacKind::kOther; + EVP_MAC* mac = + GetMacImplementation(env, args[0], args[1], args[2], &mac_owner, &kind); + if (mac == nullptr) { + if (env->isolate()->IsExecutionTerminating() || + env->isolate()->HasPendingException()) { + return false; + } + Utf8Value name(env->isolate(), args[0]); + THROW_ERR_CRYPTO_INVALID_MAC(env, "Invalid MAC: %s", *name); + return false; + } + + if (kind == ncrypto::MacKind::kHmac && !digest.has_value()) { + THROW_ERR_INVALID_ARG_VALUE( + env, "The property 'options.digest' is required for HMAC"); + return false; + } + if (kind == ncrypto::MacKind::kCmac && !cipher.has_value()) { + THROW_ERR_INVALID_ARG_VALUE( + env, "The property 'options.cipher' is required for CMAC"); + return false; + } + if (kind == ncrypto::MacKind::kGmac) { + if (!cipher.has_value()) { + THROW_ERR_INVALID_ARG_VALUE( + env, "The property 'options.cipher' is required for GMAC"); + return false; + } + if (!has_iv || iv.empty()) { + THROW_ERR_INVALID_ARG_VALUE( + env, "The property 'options.iv' must be non-empty for GMAC"); + return false; + } + } + + ncrypto::EVPMacCtxPointer context = ncrypto::EVPMacCtxPointer::New(mac); + if (!context) { + ThrowMacError(env, "Failed to create MAC context"); + return false; + } + + std::array params; + size_t count = 0; + if (digest.has_value()) { + if (!RequireSettableParameter(env, + context, + args[0], + "digest", + OSSL_MAC_PARAM_DIGEST, + OSSL_PARAM_UTF8_STRING)) { + return false; + } + params[count++] = OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, const_cast(digest->out()), 0); + } + if (cipher.has_value()) { + if (!RequireSettableParameter(env, + context, + args[0], + "cipher", + OSSL_MAC_PARAM_CIPHER, + OSSL_PARAM_UTF8_STRING)) { + return false; + } + params[count++] = OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_CIPHER, const_cast(cipher->out()), 0); + } + + unsigned char empty_parameter = 0; + auto add_bytes = [&](bool present, + const ByteSource& value, + const char* option, + const char* parameter) { + if (!present) return true; + if (!RequireSettableParameter(env, + context, + args[0], + option, + parameter, + OSSL_PARAM_OCTET_STRING)) { + return false; + } + void* data = value.empty() ? static_cast(&empty_parameter) + : const_cast(value.data()); + params[count++] = + OSSL_PARAM_construct_octet_string(parameter, data, value.size()); + return true; + }; + + if (!add_bytes(has_iv, iv, "iv", OSSL_MAC_PARAM_IV) || + !add_bytes(has_customization, + customization, + "customization", + OSSL_MAC_PARAM_CUSTOM) || + !add_bytes(has_salt, salt, "salt", OSSL_MAC_PARAM_SALT)) { + return false; + } + + if (has_output_length) { + if (output_length > Buffer::kMaxLength) { + env->isolate()->ThrowException(ERR_BUFFER_TOO_LARGE(env->isolate())); + return false; + } + if (!RequireSettableParameter(env, + context, + args[0], + "outputLength", + OSSL_MAC_PARAM_SIZE, + OSSL_PARAM_UNSIGNED_INTEGER)) { + return false; + } + params[count++] = + OSSL_PARAM_construct_size_t(OSSL_MAC_PARAM_SIZE, &output_length); + } + params[count] = OSSL_PARAM_construct_end(); + + if (!context.init(key, params.data())) { + ThrowMacError(env, "Failed to initialize MAC"); + return false; + } + + const size_t output_size = context.getSize(); + if (output_size > Buffer::kMaxLength) { + ERR_clear_error(); + env->isolate()->ThrowException(ERR_BUFFER_TOO_LARGE(env->isolate())); + return false; + } + if (has_output_length && output_size != output_length) { + ERR_clear_error(); + Utf8Value name(env->isolate(), args[0]); + THROW_ERR_INVALID_ARG_VALUE( + env, + "The property 'options.outputLength' was not honored by MAC %s", + *name); + return false; + } + if (!has_output_length && output_size == 0) { + ERR_clear_error(); + THROW_ERR_CRYPTO_OPERATION_FAILED(env, "MAC did not report an output size"); + return false; + } + + output->context = std::move(context); + output->output_size = output_size; + output->has_output_length = has_output_length; + return true; +} + +v8::MaybeLocal FinalizeMac(Environment* env, + ncrypto::EVPMacCtxPointer* context, + size_t output_size, + bool has_output_length, + enum encoding encoding) { + ncrypto::DataPointer result = context->final(output_size); + if (!result) { + ThrowMacError(env, "Failed to finalize MAC"); + context->reset(); + return {}; + } + context->reset(); + if (has_output_length && result.size() != output_size) { + THROW_ERR_CRYPTO_OPERATION_FAILED( + env, "MAC returned an unexpected output length"); + return {}; + } + if (encoding != BUFFER) { + return StringBytes::Encode(env->isolate(), + static_cast(result.get()), + result.size(), + encoding); + } + if (result.size() == 0) return Buffer::New(env, 0); + + ByteSource bytes = ByteSource::Allocated(result.release()); + return bytes.ToBuffer(env); +} + +void SaveMacName(const char* name, void* arg) { + if (name == nullptr) return; + const std::string_view view(name); + const bool is_dotted_decimal = + view.find('.') != std::string_view::npos && + std::all_of(view.begin(), view.end(), [](unsigned char c) { + return (c >= '0' && c <= '9') || c == '.'; + }); + if (is_dotted_decimal) return; + + std::string normalized(view); + std::transform(normalized.begin(), + normalized.end(), + normalized.begin(), + [](unsigned char c) { + if (c >= 'A' && c <= 'Z') { + return static_cast(c + ('a' - 'A')); + } + return static_cast(c); + }); + static_cast(arg)->supported_mac_algorithms.push_back( + std::move(normalized)); +} + +void SaveSupportedProviderMac(EVP_MAC* enumerated, void* arg) { + Environment* env = static_cast(arg); + const char* name = EVP_MAC_get0_name(enumerated); + if (name == nullptr) return; + + MaybeCachedMac result = FetchAndMaybeCacheMac(env, name); + EVP_MAC* fetched = + result.cached_mac != nullptr ? result.cached_mac : result.mac.get(); + if (fetched == nullptr) return; + EVP_MAC_names_do_all(fetched, SaveMacName, env); +} + +const std::vector& GetSupportedMacAlgorithms(Environment* env) { + while (true) { + SynchronizeMacCache(env); + const uint64_t generation = env->mac_cache_generation; + if (!env->supported_mac_algorithms_initialized) { + ncrypto::MarkPopErrorOnReturn mark_pop_error_on_return; + EVP_MAC_do_all_provided(nullptr, SaveSupportedProviderMac, env); + std::sort(env->supported_mac_algorithms.begin(), + env->supported_mac_algorithms.end()); + env->supported_mac_algorithms.erase( + std::unique(env->supported_mac_algorithms.begin(), + env->supported_mac_algorithms.end()), + env->supported_mac_algorithms.end()); + env->supported_mac_algorithms_initialized = true; + } + + const uint64_t current_generation = ncrypto::getFipsStateGeneration(); + if (generation == current_generation) { + return env->supported_mac_algorithms; + } + ResetMacCache(env, current_generation); + } +} + +} // namespace +#endif // OPENSSL_WITH_EVP_MAC + +#if OPENSSL_WITH_EVP_MAC +bool Mac::MacUpdate(const char* data, size_t length) { + if (!context_) return false; + if (length == 0) return true; + if (EVP_MAC_update(context_.get(), + reinterpret_cast(data), + length) == 1) { + return true; + } + MaybeThrowCryptoError(env(), "Failed to update MAC"); + context_.reset(); + return false; +} + +Mac::Mac(Environment* env, + Local wrap, + ncrypto::EVPMacCtxPointer&& context, + size_t output_size, + bool has_output_length) + : BaseObject(env, wrap), + context_(std::move(context)), + output_size_(output_size), + has_output_length_(has_output_length) { + MakeWeak(); +} +#else +Mac::Mac(Environment* env, Local wrap) : BaseObject(env, wrap) { + MakeWeak(); +} +#endif + +void Mac::MemoryInfo(MemoryTracker* tracker) const { +#if OPENSSL_WITH_EVP_MAC + tracker->TrackFieldWithSize("context", context_ ? kSizeOf_EVP_MAC_CTX : 0); +#else + static_cast(tracker); +#endif +} + +void Mac::Initialize(Environment* env, Local target) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Local t = NewFunctionTemplate(isolate, New); + t->InstanceTemplate()->SetInternalFieldCount(Mac::kInternalFieldCount); + SetProtoMethod(isolate, t, "update", MacUpdate); + SetProtoMethod(isolate, t, "final", MacFinal); + SetConstructorFunction(context, target, "Mac", t); + + SetMethodNoSideEffect(context, target, "getMacs", GetMacs); + SetMethodNoSideEffect( + context, target, "getCachedMacAliases", GetCachedAliases); +} + +void Mac::RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(MacUpdate); + registry->Register(MacFinal); + registry->Register(GetMacs); + registry->Register(GetCachedAliases); +} + +void Mac::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); +#if OPENSSL_WITH_EVP_MAC + CHECK(args.Length() == 5 || args.Length() == 10); + Environment* env = Environment::GetCurrent(args); + InitializedMac initialized; + if (!InitializeMacContext(env, args, &initialized)) return; + new Mac(env, + args.This(), + std::move(initialized.context), + initialized.output_size, + initialized.has_output_length); +#else + THROW_ERR_CRYPTO_MAC_NOT_SUPPORTED(Environment::GetCurrent(args), + "MAC is not supported"); +#endif +} + +void Mac::MacUpdate(const FunctionCallbackInfo& args) { +#if OPENSSL_WITH_EVP_MAC + Decode(args, + [](Mac* mac, + const FunctionCallbackInfo& args, + const char* data, + size_t length) { + args.GetReturnValue().Set(mac->MacUpdate(data, length)); + }); +#else + THROW_ERR_CRYPTO_MAC_NOT_SUPPORTED(Environment::GetCurrent(args), + "MAC is not supported"); +#endif +} + +void Mac::MacFinal(const FunctionCallbackInfo& args) { +#if OPENSSL_WITH_EVP_MAC + Mac* mac; + ASSIGN_OR_RETURN_UNWRAP(&mac, args.This()); + Environment* env = mac->env(); + if (!mac->context_) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, "MAC context is not initialized"); + return; + } + enum encoding encoding = BUFFER; + if (args.Length() >= 1) { + encoding = ParseEncoding(env->isolate(), args[0], BUFFER); + } + Local result; + if (FinalizeMac(env, + &mac->context_, + mac->output_size_, + mac->has_output_length_, + encoding) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +#else + THROW_ERR_CRYPTO_MAC_NOT_SUPPORTED(Environment::GetCurrent(args), + "MAC is not supported"); +#endif +} + +void Mac::GetMacs(const FunctionCallbackInfo& args) { +#if OPENSSL_WITH_EVP_MAC + Local context = args.GetIsolate()->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + Local result; + if (ToV8Value(context, GetSupportedMacAlgorithms(env)).ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +#else + args.GetReturnValue().Set(Array::New(args.GetIsolate(), 0)); +#endif +} + +void Mac::GetCachedAliases(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + LocalVector names(isolate); + LocalVector values(isolate); +#if OPENSSL_WITH_EVP_MAC + Environment* env = Environment::GetCurrent(args); + SynchronizeMacCache(env); + const auto& aliases = env->provider_mac_cache->aliases(); + names.reserve(aliases.size()); + values.reserve(aliases.size()); + for (const auto& [alias, id] : aliases) { + names.push_back(OneByteString(isolate, alias)); + values.push_back(Int32::New(isolate, id)); + } +#endif + Local result = Object::New( + isolate, Null(isolate), names.data(), values.data(), names.size()); + args.GetReturnValue().Set(result); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_mac.h b/src/crypto/crypto_mac.h new file mode 100644 index 000000000000..3223b1487107 --- /dev/null +++ b/src/crypto/crypto_mac.h @@ -0,0 +1,51 @@ +#ifndef SRC_CRYPTO_CRYPTO_MAC_H_ +#define SRC_CRYPTO_CRYPTO_MAC_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "base_object.h" +#include "crypto/crypto_util.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { + +class Mac final : public BaseObject { + public: + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Mac) + SET_SELF_SIZE(Mac) + + private: + static void New(const v8::FunctionCallbackInfo& args); + static void MacUpdate(const v8::FunctionCallbackInfo& args); + static void MacFinal(const v8::FunctionCallbackInfo& args); + static void GetMacs(const v8::FunctionCallbackInfo& args); + static void GetCachedAliases(const v8::FunctionCallbackInfo& args); + +#if OPENSSL_WITH_EVP_MAC + Mac(Environment* env, + v8::Local wrap, + ncrypto::EVPMacCtxPointer&& context, + size_t output_size, + bool has_output_length); + bool MacUpdate(const char* data, size_t length); + + ncrypto::EVPMacCtxPointer context_; + size_t output_size_ = 0; + bool has_output_length_ = false; +#else + Mac(Environment* env, v8::Local wrap); +#endif +}; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_MAC_H_ diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 62ae32d277d9..5344743dab27 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -50,6 +50,7 @@ namespace node::crypto { constexpr size_t kSizeOf_DH = 144; constexpr size_t kSizeOf_EC_KEY = 80; constexpr size_t kSizeOf_EVP_CIPHER_CTX = 168; +constexpr size_t kSizeOf_EVP_MAC_CTX = 16; constexpr size_t kSizeOf_EVP_MD_CTX = 48; constexpr size_t kSizeOf_EVP_PKEY = 72; constexpr size_t kSizeOf_EVP_PKEY_CTX = 80; diff --git a/src/env.cc b/src/env.cc index d679652818c9..7ec8c50b1aba 100644 --- a/src/env.cc +++ b/src/env.cc @@ -884,6 +884,9 @@ Environment::Environment(IsolateData* isolate_data, #if HAVE_OPENSSL && NCRYPTO_USE_OPENSSL3_PROVIDER provider_digest_cache = std::make_unique(); provider_cipher_cache = std::make_unique(); +#if OPENSSL_WITH_EVP_MAC + provider_mac_cache = std::make_unique(); +#endif #endif if (!is_main_thread()) { @@ -1142,6 +1145,9 @@ Environment::~Environment() { // environment-owned methods before unloading any addon DSOs. provider_digest_cache.reset(); provider_cipher_cache.reset(); +#if OPENSSL_WITH_EVP_MAC + provider_mac_cache.reset(); +#endif #endif // Dereference all addons that were loaded into this environment. for (binding::DLib& addon : loaded_addons_) { diff --git a/src/env.h b/src/env.h index 06ae22cc4bb2..826ed33f697e 100644 --- a/src/env.h +++ b/src/env.h @@ -71,6 +71,7 @@ namespace ncrypto { class CipherCache; class DigestCache; +class MacCache; } // namespace ncrypto namespace node { @@ -1098,6 +1099,10 @@ class Environment final : public MemoryRetainer { std::unique_ptr provider_digest_cache; std::unique_ptr provider_cipher_cache; std::vector supported_hash_algorithms; + uint64_t mac_cache_generation = 0; + std::unique_ptr provider_mac_cache; + std::vector supported_mac_algorithms; + bool supported_mac_algorithms_initialized = false; #endif // HAVE_OPENSSL v8::Global temporary_required_module_facade_original; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 7d52d835ca4c..e82167b1fb4e 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -48,6 +48,7 @@ namespace crypto { V(Hmac) \ V(Keygen) \ V(Keys) \ + V(Mac) \ V(NativeCryptoKey) \ V(NativeKeyObject) \ V(PBKDF2Job) \ diff --git a/src/node_crypto.h b/src/node_crypto.h index 3bb95cb340d2..f3e006e5fee8 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -48,6 +48,7 @@ #endif #include "crypto/crypto_keygen.h" #include "crypto/crypto_keys.h" +#include "crypto/crypto_mac.h" #include "crypto/crypto_pbkdf2.h" #include "crypto/crypto_pqc.h" #include "crypto/crypto_random.h" diff --git a/src/node_errors.h b/src/node_errors.h index 62cfba88f00d..cab1dc76dcc7 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -62,12 +62,15 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_CRYPTO_INVALID_KEYLEN, RangeError) \ V(ERR_CRYPTO_INVALID_KEYPAIR, RangeError) \ V(ERR_CRYPTO_INVALID_KEYTYPE, RangeError) \ + V(ERR_CRYPTO_INVALID_MAC, TypeError) \ V(ERR_CRYPTO_INVALID_MESSAGELEN, RangeError) \ V(ERR_CRYPTO_INVALID_SCRYPT_PARAMS, RangeError) \ V(ERR_CRYPTO_INVALID_STATE, Error) \ V(ERR_CRYPTO_INVALID_TAG_LENGTH, RangeError) \ V(ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, Error) \ V(ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE, Error) \ + V(ERR_CRYPTO_MAC_NOT_SUPPORTED, Error) \ + V(ERR_CRYPTO_MAC_UPDATE_FAILED, Error) \ V(ERR_CRYPTO_OPERATION_FAILED, Error) \ V(ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, RangeError) \ V(ERR_CRYPTO_UNKNOWN_CIPHER, Error) \ diff --git a/test/fixtures/snapshot/crypto-provider-mac-cache.js b/test/fixtures/snapshot/crypto-provider-mac-cache.js new file mode 100644 index 000000000000..ac401d8a979a --- /dev/null +++ b/test/fixtures/snapshot/crypto-provider-mac-cache.js @@ -0,0 +1,65 @@ +'use strict'; + +const assert = require('node:assert'); +const { + createMac, + getFips, + getMacs, + setFips, +} = require('node:crypto'); +const { setDeserializeMainFunction } = require('node:v8').startupSnapshot; + +const algorithm = 'poly1305'; +const key = Buffer.from( + '85d6be7857556d337f4452fe42d506a8' + + '0103808afb0db2fd4abff6af4149f51b', + 'hex', +); +const data = Buffer.from('Cryptographic Forum Research Group'); +const expected = 'a8061dc1305136c6c22b8baf0c0127a9'; + +setFips(0); +assert(getMacs().includes(algorithm)); +assert.strictEqual( + createMac(algorithm, key).update(data).final('hex'), + expected, +); + +setDeserializeMainFunction(() => { + // Resolve through the JavaScript alias cache before refreshing getMacs(). + // Startup snapshot serialization must not retain the build-time cache IDs. + assert.strictEqual( + createMac(algorithm, key).update(data).final('hex'), + expected, + ); + const expectedMacs = getMacs(); + assert(expectedMacs.includes(algorithm)); + const disposableMacs = getMacs(); + disposableMacs.length = 0; + assert.deepStrictEqual(getMacs(), expectedMacs); + assert.strictEqual( + createMac(algorithm, key).update(data).final('hex'), + expected, + ); + + let toggled = false; + try { + setFips(1); + toggled = getFips() === 1; + } catch { + // FIPS mode is optional; snapshot cache rebuilding is still covered. + } + if (toggled && !getMacs().includes(algorithm)) { + assert.throws(() => createMac(algorithm, key), { + code: 'ERR_CRYPTO_INVALID_MAC', + }); + } + setFips(0); + + assert(getMacs().includes(algorithm)); + assert.strictEqual( + createMac(algorithm, key).update(data).final('hex'), + expected, + ); + console.log('provider MAC cache snapshot: ok'); +}); 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/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 44cf62888e85..9b9cb59a8cba 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -644,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; @@ -828,6 +833,18 @@ export interface CryptoBinding { 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; @@ -936,6 +953,7 @@ export interface CryptoBinding { ]; getBundledRootCertificates(): string[]; getCachedAliases(): Record; + getCachedMacAliases(): Record; getCertificateCompressionAlgorithms(): string[]; getCipherInfo( nameOrNid: string | number, @@ -949,6 +967,7 @@ export interface CryptoBinding { getFipsCrypto(): 0 | 1; getFipsCryptoGeneration(): bigint; getHashes(): string[]; + getMacs(): string[]; isCryptoKey(key: unknown): boolean; isKeyObject(key: unknown): boolean; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots;