Commit 36b76ca2eec for nodejs
commit 36b76ca2eeceaf29ae7a1a14182d12818b0f4242
Author: Filip Skokan <panva.ip@gmail.com>
Date: Tue Sep 22 22:27:08 2026 +0200
crypto: handle PBKDF2 iteration limits
Report OperationError for unsupported iteration counts and return an
empty result before invoking the backend when no output is requested.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66237
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js
index f42ce3bbd13..b9f6b59c85b 100644
--- a/lib/internal/crypto/pbkdf2.js
+++ b/lib/internal/crypto/pbkdf2.js
@@ -16,6 +16,7 @@ const {
} = internalBinding('crypto');
const {
+ isInt32,
validateFunction,
validateInt32,
validateString,
@@ -98,7 +99,7 @@ function check(password, salt, iterations, keylen, digest) {
return { password, salt, iterations, keylen, digest };
}
-function validatePbkdf2DeriveBitsLength(length) {
+function validatePbkdf2DeriveBits({ iterations }, length) {
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
@@ -107,10 +108,16 @@ function validatePbkdf2DeriveBitsLength(length) {
'length must be a multiple of 8',
'OperationError');
}
+ if (iterations === 0)
+ throw lazyDOMException('iterations cannot be zero', 'OperationError');
+ if (length !== 0 && !isInt32(iterations)) {
+ throw lazyDOMException(
+ 'iterations exceeds the implementation limit', 'OperationError');
+ }
}
function pbkdf2DeriveBits(algorithm, baseKey, length) {
- validatePbkdf2DeriveBitsLength(length);
+ validatePbkdf2DeriveBits(algorithm, length);
const { iterations, hash, salt } = algorithm;
if (length === 0)
@@ -129,5 +136,5 @@ module.exports = {
pbkdf2,
pbkdf2Sync,
pbkdf2DeriveBits,
- validatePbkdf2DeriveBitsLength,
+ validatePbkdf2DeriveBits,
};
diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js
index 73ae08fa1e1..ae45d8a9ec2 100644
--- a/lib/internal/crypto/webcrypto.js
+++ b/lib/internal/crypto/webcrypto.js
@@ -1937,7 +1937,7 @@ function check(op, alg, length) {
}
if (normalizedAlgorithm.name === 'PBKDF2') {
- require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length);
+ require('internal/crypto/pbkdf2').validatePbkdf2DeriveBits(normalizedAlgorithm, length);
}
if (StringPrototypeStartsWith(normalizedAlgorithm.name, 'Argon2')) {
diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js
index 5b3fc8d605b..1d35ce69f56 100644
--- a/lib/internal/crypto/webidl.js
+++ b/lib/internal/crypto/webidl.js
@@ -14,7 +14,6 @@ const {
lazyDOMException,
} = require('internal/util');
const {
- isInt32,
isUint32,
} = require('internal/validators');
const {
@@ -468,15 +467,6 @@ converters.Pbkdf2Params = createDictionaryConverter(
key: 'iterations',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
- validator: (V, dict) => {
- if (V === 0)
- throw lazyDOMException('iterations cannot be zero', 'OperationError');
- if (!isInt32(V)) {
- throw lazyDOMException(
- 'iterations exceeds the implementation limit',
- 'NotSupportedError');
- }
- },
required: true,
},
{
diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js
index 99de1e6ba6d..ee4de1c26a2 100644
--- a/test/parallel/test-webcrypto-derivebits.js
+++ b/test/parallel/test-webcrypto-derivebits.js
@@ -136,7 +136,7 @@ const rejectsXCurves = hasFIPS(3, 5);
salt: new Uint8Array([2]),
iterations: 2 ** 31,
}, key, 8),
- { name: 'NotSupportedError' });
+ { name: 'OperationError' });
}
test().then(common.mustCall());
diff --git a/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js b/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js
new file mode 100644
index 00000000000..168ee83a641
--- /dev/null
+++ b/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js
@@ -0,0 +1,21 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { subtle } = globalThis.crypto;
+
+(async () => {
+ const key = await subtle.importKey('raw', new Uint8Array(16), 'PBKDF2', false, ['deriveBits']);
+ for (const iterations of [2 ** 31, 2 ** 32 - 1]) {
+ const algorithm = { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations };
+ assert.strictEqual((await subtle.deriveBits(algorithm, key, 0)).byteLength, 0);
+ assert.strictEqual(SubtleCrypto.supports('deriveBits', algorithm, 0), true);
+ assert.strictEqual(SubtleCrypto.supports('deriveBits', algorithm, 8), false);
+ await assert.rejects(subtle.deriveBits(algorithm, key, 8), { name: 'OperationError' });
+ }
+ const algorithm = { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations: 0 };
+ await assert.rejects(subtle.deriveBits(algorithm, key, 0), { name: 'OperationError' });
+})().then(common.mustCall());