From f3afdcc3dbc3adcf7c470341963a8a3b9a508fda Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 19 Jun 2026 12:18:36 +0200 Subject: [PATCH] crypto: move DEP0198 to End-of-Life Signed-off-by: Filip Skokan --- doc/api/crypto.md | 14 +++- doc/api/deprecations.md | 8 +- lib/internal/crypto/hash.js | 27 ------- src/crypto/crypto_hash.cc | 78 ++++++++++++------- ...st-crypto-default-shake-lengths-oneshot.js | 37 ++++++--- .../test-crypto-default-shake-lengths.js | 42 +++++++--- test/parallel/test-crypto-hash.js | 31 +++++--- test/parallel/test-crypto-oneshot-hash-xof.js | 26 +++++-- test/parallel/test-crypto-oneshot-hash.js | 25 +++++- test/parallel/test-crypto.js | 10 ++- 10 files changed, 193 insertions(+), 105 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index fe5c9468891cd1..13d688ed610d37 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3771,6 +3771,10 @@ and description of each available elliptic curve. -Type: Runtime +Type: End-of-Life -Creating SHAKE-128 and SHAKE-256 digests without an explicit `options.outputLength` is deprecated. +Creating SHAKE-128 and SHAKE-256 digests without an explicit +`options.outputLength` is no longer supported. ### DEP0199: `require('node:_http_*')` diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 01d653f9bd8d03..16834f169a5be0 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -3,7 +3,6 @@ const { FunctionPrototypeCall, ObjectSetPrototypeOf, - StringPrototypeReplace, StringPrototypeToLowerCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -73,25 +72,6 @@ const LazyTransform = require('internal/streams/lazy_transform'); const kState = Symbol('kState'); const kFinalized = Symbol('kFinalized'); -/** - * @param {string} name - * @returns {string} - */ -function normalizeAlgorithm(name) { - return StringPrototypeReplace(StringPrototypeToLowerCase(name), '-', ''); -} - -const maybeEmitDeprecationWarning = getDeprecationWarningEmitter( - 'DEP0198', - 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.', - undefined, - false, - (algorithm) => { - const normalized = normalizeAlgorithm(algorithm); - return normalized === 'shake128' || normalized === 'shake256'; - }, -); - const emitHmacDigestDeprecation = getDeprecationWarningEmitter( 'DEP0206', 'Calling Hmac.digest() more than once is deprecated.', @@ -117,9 +97,6 @@ function Hash(algorithm, options) { this[kState] = { [kFinalized]: false, }; - if (!isCopy && xofLen === undefined) { - maybeEmitDeprecationWarning(algorithm); - } FunctionPrototypeCall(LazyTransform, this, options); } @@ -334,10 +311,6 @@ function hash(algorithm, input, options) { outputLength += 0; } - if (outputLength === undefined) { - maybeEmitDeprecationWarning(algorithm); - } - return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(), input, normalized, encodingsMap[normalized], outputLength); } diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 20cadc610bace1..989f81302d198d 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -246,6 +246,39 @@ const EVP_MD* GetDigestImplementation(Environment* env, #endif } +void MarkInvalidXofLength() { + EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH); +} + +// DEP0198 EOL requires XOFs without an OpenSSL-defined default output length +// to fail when outputLength is omitted. OpenSSL 3.4 and later report a digest +// size of 0 for such XOFs, including SHAKE, which had weak historical defaults +// before OpenSSL 3.4. For older OpenSSL versions, identify those resolved +// EVP_MD values explicitly to keep the missing-outputLength error +// version-independent. +#if !OPENSSL_VERSION_PREREQ(3, 4) +bool IsShakeDigest(const EVP_MD* md) { +#if OPENSSL_VERSION_MAJOR >= 3 + return EVP_MD_is_a(md, "SHAKE128") || EVP_MD_is_a(md, "SHAKE256"); +#else + const char* name = OBJ_nid2sn(EVP_MD_type(md)); + return name != nullptr && + (strcmp(name, "SHAKE128") == 0 || strcmp(name, "SHAKE256") == 0); +#endif +} +#endif + +bool ShouldRejectMissingXofLength(const EVP_MD* md, size_t default_length) { + if (default_length == 0) return true; + +#if !OPENSSL_VERSION_PREREQ(3, 4) + return IsShakeDigest(md); +#else + static_cast(md); + return false; +#endif +} + // crypto.digest(algorithm, algorithmId, algorithmCache, // input, outputEncoding, outputEncodingId, outputLength) void Hash::OneShotDigest(const FunctionCallbackInfo& args) { @@ -287,18 +320,10 @@ void Hash::OneShotDigest(const FunctionCallbackInfo& args) { } else if (is_xof) { if (!args[6]->IsUndefined()) { output_length = args[6].As()->Value(); - } else if (output_length == 0) { - // This is to handle OpenSSL 3.4's breaking change in SHAKE128/256 - // default lengths - // TODO(@panva): remove this behaviour when DEP0198 is End-Of-Life - const char* name = OBJ_nid2sn(EVP_MD_type(md)); - if (name != nullptr) { - if (strcmp(name, "SHAKE128") == 0) { - output_length = 16; - } else if (strcmp(name, "SHAKE256") == 0) { - output_length = 32; - } - } + } else if (ShouldRejectMissingXofLength(md, output_length)) { + MarkInvalidXofLength(); + return ThrowCryptoError( + env, ERR_get_error(), "Digest method not supported"); } } @@ -387,6 +412,12 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { void Hash::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + Maybe xof_md_len = Nothing(); + if (!args[1]->IsUndefined()) { + CHECK(args[1]->IsUint32()); + xof_md_len = Just(args[1].As()->Value()); + } + const Hash* orig = nullptr; const EVP_MD* md = nullptr; if (args[0]->IsObject()) { @@ -397,12 +428,6 @@ void Hash::New(const FunctionCallbackInfo& args) { md = GetDigestImplementation(env, args[0], args[2], args[3]); } - Maybe xof_md_len = Nothing(); - if (!args[1]->IsUndefined()) { - CHECK(args[1]->IsUint32()); - xof_md_len = Just(args[1].As()->Value()); - } - Hash* hash = new Hash(env, args.This()); if (md == nullptr || !hash->HashInit(md, xof_md_len)) { return ThrowCryptoError(env, ERR_get_error(), @@ -423,18 +448,11 @@ bool Hash::HashInit(const EVP_MD* md, Maybe xof_md_len) { md_len_ = mdctx_.getDigestSize(); - // This is to handle OpenSSL 3.4's breaking change in SHAKE128/256 - // default lengths - // TODO(@panva): remove this behaviour when DEP0198 is End-Of-Life - if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() && md_len_ == 0) { - const char* name = OBJ_nid2sn(EVP_MD_type(md)); - if (name != nullptr) { - if (strcmp(name, "SHAKE128") == 0) { - md_len_ = 16; - } else if (strcmp(name, "SHAKE256") == 0) { - md_len_ = 32; - } - } + if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() && + ShouldRejectMissingXofLength(md, md_len_)) { + MarkInvalidXofLength(); + mdctx_.reset(); + return false; } if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) { diff --git a/test/parallel/test-crypto-default-shake-lengths-oneshot.js b/test/parallel/test-crypto-default-shake-lengths-oneshot.js index 4aff888c104eb2..7aa44ea2e7c20b 100644 --- a/test/parallel/test-crypto-default-shake-lengths-oneshot.js +++ b/test/parallel/test-crypto-default-shake-lengths-oneshot.js @@ -4,17 +4,34 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) - common.skip('not supported by BoringSSL'); +const { + getHashes, + hash, +} = require('crypto'); -const { hash } = require('crypto'); +if (!getHashes().includes('shake128')) + common.skip('unsupported shake128 test'); -common.expectWarning({ - DeprecationWarning: { - DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.', - } -}); +const assert = require('assert'); -{ - hash('shake128', 'test'); +const invalidXofLength = { + code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH', + name: 'Error', + message: /not XOF or invalid length/, +}; + +const shakeAlgorithms = [ + 'shake128', + 'SHAKE128', + 'shake256', + 'SHAKE256', +]; + +for (const algorithm of shakeAlgorithms) { + assert.throws(() => hash(algorithm, 'test'), invalidXofLength); + assert.throws(() => hash(algorithm, 'test', 'hex'), invalidXofLength); + assert.throws( + () => hash(algorithm, 'test', { outputEncoding: 'buffer' }), + invalidXofLength); + assert.throws(() => hash(algorithm, 'test', {}), invalidXofLength); } diff --git a/test/parallel/test-crypto-default-shake-lengths.js b/test/parallel/test-crypto-default-shake-lengths.js index 870468cbdc63df..8437d64a55f810 100644 --- a/test/parallel/test-crypto-default-shake-lengths.js +++ b/test/parallel/test-crypto-default-shake-lengths.js @@ -4,19 +4,39 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const crypto = require('crypto'); -if (!crypto.getHashes().includes('shake128')) { +const { + createHash, + getHashes, +} = require('crypto'); + +if (!getHashes().includes('shake128')) common.skip('unsupported shake128 test'); -} -const { createHash } = require('crypto'); +const assert = require('assert'); + +const invalidXofLength = { + code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH', + name: 'Error', + message: /not XOF or invalid length/, +}; -common.expectWarning({ - DeprecationWarning: { - DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.', - } -}); +const shakeAlgorithms = [ + 'shake128', + 'SHAKE128', + 'shake256', + 'SHAKE256', +]; -{ - createHash('shake128').update('test').digest(); +for (const algorithm of shakeAlgorithms) { + assert.throws(() => createHash(algorithm), invalidXofLength); + assert.throws(() => createHash(algorithm, null), invalidXofLength); + assert.throws(() => createHash(algorithm, {}), invalidXofLength); } + +const shake128 = createHash('shake128', { outputLength: 5 }); +const shake128Copy = shake128.copy({ outputLength: 5 }); + +assert.throws(() => shake128.copy(), invalidXofLength); +assert.throws(() => shake128.copy(null), invalidXofLength); +assert.throws(() => shake128.copy({}), invalidXofLength); +assert.throws(() => shake128Copy.copy(), invalidXofLength); diff --git a/test/parallel/test-crypto-hash.js b/test/parallel/test-crypto-hash.js index 79c20ea75983d5..44734e7aeb6f22 100644 --- a/test/parallel/test-crypto-hash.js +++ b/test/parallel/test-crypto-hash.js @@ -8,10 +8,6 @@ common.expectWarning({ DeprecationWarning: [ ['crypto.Hash constructor is deprecated.', 'DEP0179'], - ...(process.features.openssl_is_boringssl ? [] : [[ - 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.', - 'DEP0198', - ]]), ] }); @@ -194,16 +190,29 @@ assert.throws( // Test XOF hash functions and the outputLength option. if (!process.features.openssl_is_boringssl) { - // Default outputLengths. - assert.strictEqual(crypto.createHash('shake128').digest('hex'), - '7f9c2ba4e88f827d616045507605853e'); - assert.strictEqual(crypto.createHash('shake128', null).digest('hex'), + const invalidXofLength = { + code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH', + name: 'Error', + message: /not XOF or invalid length/, + }; + + assert.throws(() => crypto.createHash('shake128'), invalidXofLength); + assert.throws(() => crypto.createHash('shake128', null), invalidXofLength); + assert.throws(() => crypto.createHash('shake256'), invalidXofLength); + assert.throws(() => crypto.createHash('shake256', {}), invalidXofLength); + + assert.strictEqual(crypto.createHash('shake128', { outputLength: 16 }) + .digest('hex'), '7f9c2ba4e88f827d616045507605853e'); - assert.strictEqual(crypto.createHash('shake256').digest('hex'), + assert.strictEqual(crypto.createHash('shake256', { outputLength: 32 }) + .digest('hex'), '46b9dd2b0ba88d13233b3feb743eeb24' + '3fcd52ea62b81b82b50c27646ed5762f'); - assert.strictEqual(crypto.createHash('shake256', { outputLength: 0 }) - .copy() // Default outputLength. + const shake256 = crypto.createHash('shake256', { outputLength: 0 }); + assert.throws(() => shake256.copy(), invalidXofLength); + assert.throws(() => shake256.copy(null), invalidXofLength); + assert.throws(() => shake256.copy({}), invalidXofLength); + assert.strictEqual(shake256.copy({ outputLength: 32 }) .digest('hex'), '46b9dd2b0ba88d13233b3feb743eeb24' + '3fcd52ea62b81b82b50c27646ed5762f'); diff --git a/test/parallel/test-crypto-oneshot-hash-xof.js b/test/parallel/test-crypto-oneshot-hash-xof.js index b4363c31592763..656e7944396ed5 100644 --- a/test/parallel/test-crypto-oneshot-hash-xof.js +++ b/test/parallel/test-crypto-oneshot-hash-xof.js @@ -13,35 +13,47 @@ if (process.features.openssl_is_boringssl) { // Test XOF hash functions and the outputLength option. { - // Default outputLengths. + const invalidXofLength = { + code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH', + name: 'Error', + message: /not XOF or invalid length/, + }; + + assert.throws(() => crypto.hash('shake128', ''), invalidXofLength); + assert.throws(() => crypto.hash('shake128', '', 'hex'), invalidXofLength); + assert.throws( + () => crypto.hash('shake128', '', { outputEncoding: 'buffer' }), + invalidXofLength); + assert.throws(() => crypto.hash('shake256', ''), invalidXofLength); + assert.strictEqual( - crypto.hash('shake128', ''), + crypto.hash('shake128', '', { outputLength: 16 }), '7f9c2ba4e88f827d616045507605853e' ); assert.strictEqual( - crypto.hash('shake256', ''), + crypto.hash('shake256', '', { outputLength: 32 }), '46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f' ); // outputEncoding as an option. assert.strictEqual( - crypto.hash('shake128', '', { outputEncoding: 'base64url' }), + crypto.hash('shake128', '', { outputEncoding: 'base64url', outputLength: 16 }), 'f5wrpOiPgn1hYEVQdgWFPg' ); assert.strictEqual( - crypto.hash('shake256', '', { outputEncoding: 'base64url' }), + crypto.hash('shake256', '', { outputEncoding: 'base64url', outputLength: 32 }), 'RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8' ); assert.deepStrictEqual( - crypto.hash('shake128', '', { outputEncoding: 'buffer' }), + crypto.hash('shake128', '', { outputEncoding: 'buffer', outputLength: 16 }), Buffer.from('f5wrpOiPgn1hYEVQdgWFPg', 'base64url') ); assert.deepStrictEqual( - crypto.hash('shake256', '', { outputEncoding: 'buffer' }), + crypto.hash('shake256', '', { outputEncoding: 'buffer', outputLength: 32 }), Buffer.from('RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8', 'base64url') ); diff --git a/test/parallel/test-crypto-oneshot-hash.js b/test/parallel/test-crypto-oneshot-hash.js index f6ac8656c94e79..733c915e34b479 100644 --- a/test/parallel/test-crypto-oneshot-hash.js +++ b/test/parallel/test-crypto-oneshot-hash.js @@ -10,6 +10,17 @@ const crypto = require('crypto'); const fixtures = require('../common/fixtures'); const fs = require('fs'); +const invalidXofLengthCode = 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH'; + +function getXofOptions(method) { + try { + crypto.createHash(method); + } catch (err) { + if (err?.code !== invalidXofLengthCode) throw err; + return { outputLength: 16 }; + } +} + // Test errors for invalid arguments. [undefined, null, true, 1, () => {}, {}].forEach((invalid) => { assert.throws(() => { crypto.hash(invalid, 'test'); }, { code: 'ERR_INVALID_ARG_TYPE' }); @@ -31,12 +42,20 @@ const methods = crypto.getHashes(); const input = fs.readFileSync(fixtures.path('utf8_test_text.txt')); for (const method of methods) { + const xofOptions = getXofOptions(method); + for (const outputEncoding of ['buffer', 'hex', 'base64', undefined]) { - const oldDigest = crypto.createHash(method).update(input).digest(outputEncoding || 'hex'); - const digestFromBuffer = crypto.hash(method, input, outputEncoding); + const options = xofOptions === undefined || outputEncoding === undefined ? + xofOptions : { ...xofOptions, outputEncoding }; + const oldDigest = crypto.createHash(method, xofOptions) + .update(input) + .digest(outputEncoding || 'hex'); + const digestFromBuffer = crypto.hash( + method, input, options === undefined ? outputEncoding : options); assert.deepStrictEqual(digestFromBuffer, oldDigest, `different result from ${method} with encoding ${outputEncoding}`); - const digestFromString = crypto.hash(method, input.toString(), outputEncoding); + const digestFromString = crypto.hash( + method, input.toString(), options === undefined ? outputEncoding : options); assert.deepStrictEqual(digestFromString, oldDigest, `different result from ${method} with encoding ${outputEncoding}`); } diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js index 46f4571b33dfe8..1217ca37b03a2f 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -151,8 +151,14 @@ if (!process.features.openssl_is_boringssl) { } validateList(crypto.getHashes()); // Make sure all of the hashes are supported by OpenSSL -for (const algo of crypto.getHashes()) - crypto.createHash(algo); +for (const algo of crypto.getHashes()) { + try { + crypto.createHash(algo); + } catch (err) { + if (err?.code !== 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH') throw err; + crypto.createHash(algo, { outputLength: 0 }); + } +} // Assume that we have at least secp384r1. assert.notStrictEqual(crypto.getCurves().length, 0);