diff --git a/lib/crypto.js b/lib/crypto.js index ac4b0a33efb8ab..8df917bb7708b9 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -121,6 +121,7 @@ const { getHashes, setEngine, secureHeapUsed, + evictCipherHashCache, } = require('internal/crypto/util'); const Certificate = require('internal/crypto/certificate'); const { @@ -263,6 +264,7 @@ function setFips(val) { throw new ERR_WORKER_UNSUPPORTED_OPERATION('Calling crypto.setFips()'); } setFipsCrypto(val); + evictCipherHashCache(); } } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 181d07a07cf00e..6222affab9a872 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -6,6 +6,7 @@ const { ArrayFrom, ArrayPrototypeIncludes, ArrayPrototypePush, + ArrayPrototypeSlice, BigInt, DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, @@ -125,8 +126,25 @@ function getCachedHashId(algorithm) { return result === undefined ? -1 : result; } -const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers())); -const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes())); +let _ciphersCache; +function getCiphers() { + if (_ciphersCache === undefined) + _ciphersCache = filterDuplicateStrings(_getCiphers()); + return ArrayPrototypeSlice(_ciphersCache); +} + +let _hashesCache; +function getHashes() { + if (_hashesCache === undefined) + _hashesCache = filterDuplicateStrings(_getHashes()); + return ArrayPrototypeSlice(_hashesCache); +} + +function evictCipherHashCache() { + _ciphersCache = undefined; + _hashesCache = undefined; +} + const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); function setEngine(id, flags) { @@ -143,6 +161,7 @@ function setEngine(id, flags) { throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); if (!_setEngine(id, flags)) throw new ERR_CRYPTO_ENGINE_UNKNOWN(id); + evictCipherHashCache(); } const getArrayBufferOrView = hideStackFrames((buffer, name, encoding) => { @@ -855,6 +874,7 @@ module.exports = { getCurves, getDataViewOrTypedArrayBuffer, getHashes, + evictCipherHashCache, kHandle, kKeyObject, setEngine, diff --git a/test/parallel/test-crypto-ciphers-hashes-fips-cache.js b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js new file mode 100644 index 00000000000000..220f34e905e3fe --- /dev/null +++ b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js @@ -0,0 +1,74 @@ +// Flags: --expose-internals +'use strict'; + +// Verify that getCiphers() and getHashes() reflect the current FIPS state +// rather than returning a stale cached snapshot from before setFips() was +// called. Regression test for https://github.com/nodejs/node/issues/62982. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { internalBinding } = require('internal/test/binding'); +const { testFipsCrypto } = internalBinding('crypto'); + +if (!testFipsCrypto()) + common.skip('FIPS not supported in this build'); + +const assert = require('assert'); +const { getCiphers, getHashes, setFips, getFips } = require('crypto'); + +const initialFips = getFips(); + +// Ensure FIPS is off so we can capture the full algorithm lists as a baseline, +// regardless of whether the system has FIPS on by default. +if (initialFips) + setFips(false); + +const ciphersWithoutFips = getCiphers(); +const hashesWithoutFips = getHashes(); + +assert.ok(ciphersWithoutFips.length > 0, 'expected at least one cipher'); +assert.ok(hashesWithoutFips.length > 0, 'expected at least one hash'); + +// Switch to FIPS mode; the lists must be re-derived, not served from cache. +setFips(true); +assert.strictEqual(getFips(), 1); + +const ciphersWithFips = getCiphers(); +const hashesWithFips = getHashes(); + +// FIPS mode restricts the visible algorithm set — the lists must shrink +// (or at minimum differ; some platforms expose only FIPS algorithms by +// default, but in that case the full list can't be larger than the FIPS one). +assert.ok( + ciphersWithFips.length <= ciphersWithoutFips.length, + `Expected FIPS cipher list (${ciphersWithFips.length}) to be no larger ` + + `than the full list (${ciphersWithoutFips.length})` +); +assert.ok( + hashesWithFips.length <= hashesWithoutFips.length, + `Expected FIPS hash list (${hashesWithFips.length}) to be no larger ` + + `than the full list (${hashesWithoutFips.length})` +); + +// Every FIPS-mode algorithm must also appear in the non-FIPS list. +for (const cipher of ciphersWithFips) { + assert.ok( + ciphersWithoutFips.includes(cipher), + `FIPS cipher '${cipher}' missing from the non-FIPS list` + ); +} +for (const hash of hashesWithFips) { + assert.ok( + hashesWithoutFips.includes(hash), + `FIPS hash '${hash}' missing from the non-FIPS list` + ); +} + +// Turn FIPS back off; the cache must be evicted so the full lists come back. +setFips(false); +assert.strictEqual(getFips(), 0); + +assert.deepStrictEqual(getCiphers(), ciphersWithoutFips); +assert.deepStrictEqual(getHashes(), hashesWithoutFips);