Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
crypto: allow length=0 for HKDF and PBKDF2 in SubtleCrypto.deriveBits
PR-URL: #55866
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Jason Zhang <xzha4350@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
  • Loading branch information
panva committed Jun 5, 2025
commit 7125d37c1dfcd3f53cd421f198b4823f6dcf1719
3 changes: 2 additions & 1 deletion lib/internal/crypto/hkdf.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayBuffer,
FunctionPrototypeCall,
} = primordials;

Expand Down Expand Up @@ -141,7 +142,7 @@ async function hkdfDeriveBits(algorithm, baseKey, length) {
const { hash, salt, info } = algorithm;

if (length === 0)
throw lazyDOMException('length cannot be zero', 'OperationError');
return new ArrayBuffer(0);
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
if (length % 8) {
Expand Down
7 changes: 3 additions & 4 deletions lib/internal/crypto/pbkdf2.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayBuffer,
FunctionPrototypeCall,
} = primordials;

Expand Down Expand Up @@ -98,10 +99,8 @@ async function pbkdf2DeriveBits(algorithm, baseKey, length) {
'iterations cannot be zero',
'OperationError');

const raw = baseKey[kKeyObject].export();

if (length === 0)
throw lazyDOMException('length cannot be zero', 'OperationError');
return new ArrayBuffer(0);
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
if (length % 8) {
Expand All @@ -113,7 +112,7 @@ async function pbkdf2DeriveBits(algorithm, baseKey, length) {
let result;
try {
result = await pbkdf2Promise(
raw, salt, iterations, length / 8, normalizeHashName(hash.name),
baseKey[kKeyObject].export(), salt, iterations, length / 8, normalizeHashName(hash.name),
);
} catch (err) {
throw lazyDOMException(
Expand Down
6 changes: 3 additions & 3 deletions test/fixtures/wpt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ Last update:
- interfaces: https://github.com/web-platform-tests/wpt/tree/df731dab88/interfaces
- performance-timeline: https://github.com/web-platform-tests/wpt/tree/17ebc3aea0/performance-timeline
- resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
- resources: https://github.com/web-platform-tests/wpt/tree/1e140d63ec/resources
- resources: https://github.com/web-platform-tests/wpt/tree/919874f84f/resources
- streams: https://github.com/web-platform-tests/wpt/tree/2bd26e124c/streams
- url: https://github.com/web-platform-tests/wpt/tree/67880a4eb8/url
- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi
- wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi
- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/6748a0a246/WebCryptoAPI
- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/b81831169b/WebCryptoAPI
- webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/a370aad338/webidl/ecmascript-binding/es-exceptions
- webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/e97fac4791/webmessaging/broadcastchannel

[Web Platform Tests]: https://github.com/web-platform-tests/wpt
[`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/main/docs/git-node.md#git-node-wpt
[`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/main/docs/git-node.md#git-node-wpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// META: title=WebCryptoAPI: CryptoKey.algorithm getter returns cached object

// https://w3c.github.io/webcrypto/#dom-cryptokey-algorithm
// https://github.com/servo/servo/issues/33908

promise_test(function() {
return self.crypto.subtle.generateKey(
{
name: "AES-CTR",
length: 256,
},
true,
["encrypt"],
).then(
function(key) {
let a = key.algorithm;
let b = key.algorithm;
assert_true(a === b);
},
function(err) {
assert_unreached("generateKey threw an unexpected error: " + err.toString());
}
);
}, "CryptoKey.algorithm getter returns cached object");
48 changes: 30 additions & 18 deletions test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
function define_tests_25519() {
return define_tests("X25519");
}

function define_tests_448() {
return define_tests("X448");
}

function define_tests() {
function define_tests(algorithmName) {
// May want to test prefixed implementations.
var subtle = self.crypto.subtle;

// Verify the derive functions perform checks against the all-zero value results,
// ensuring small-order points are rejected.
// https://www.rfc-editor.org/rfc/rfc7748#section-6.1
Object.keys(kSmallOrderPoint).forEach(function(algorithmName) {
{
kSmallOrderPoint[algorithmName].forEach(function(test) {
promise_test(async() => {
let derived;
Expand All @@ -28,15 +35,16 @@ function define_tests() {
assert_equals(derived, undefined, "Operation succeeded, but should not have.");
}, algorithmName + " key derivation checks for all-zero value result with a key of order " + test.order);
});
});
}

return importKeys(pkcs8, spki, sizes)
.then(function(results) {
publicKeys = results.publicKeys;
privateKeys = results.privateKeys;
noDeriveBitsKeys = results.noDeriveBitsKeys;
ecdhKeys = results.ecdhKeys;

Object.keys(sizes).forEach(function(algorithmName) {
{
// Basic success case
promise_test(function(test) {
return subtle.deriveBits({name: algorithmName, public: publicKeys[algorithmName]}, privateKeys[algorithmName], 8 * sizes[algorithmName])
Expand Down Expand Up @@ -101,11 +109,7 @@ function define_tests() {

// - wrong algorithm
promise_test(function(test) {
publicKey = publicKeys["X25519"];
if (algorithmName === "X25519") {
publicKey = publicKeys["X448"];
}
return subtle.deriveBits({name: algorithmName, public: publicKey}, privateKeys[algorithmName], 8 * sizes[algorithmName])
return subtle.deriveBits({name: algorithmName, public: ecdhKeys[algorithmName]}, privateKeys[algorithmName], 8 * sizes[algorithmName])
.then(function(derivation) {
assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
}, function(err) {
Expand Down Expand Up @@ -165,16 +169,17 @@ function define_tests() {
assert_equals(err.name, "OperationError", "Should throw correct error, not " + err.name + ": " + err.message);
});
}, algorithmName + " asking for too many bits");
});
}
});

function importKeys(pkcs8, spki, sizes) {
var privateKeys = {};
var publicKeys = {};
var noDeriveBitsKeys = {};
var ecdhPublicKeys = {};

var promises = [];
Object.keys(pkcs8).forEach(function(algorithmName) {
{
var operation = subtle.importKey("pkcs8", pkcs8[algorithmName],
{name: algorithmName},
false, ["deriveBits", "deriveKey"])
Expand All @@ -184,8 +189,8 @@ function define_tests() {
privateKeys[algorithmName] = null;
});
promises.push(operation);
});
Object.keys(pkcs8).forEach(function(algorithmName) {
}
{
var operation = subtle.importKey("pkcs8", pkcs8[algorithmName],
{name: algorithmName},
false, ["deriveKey"])
Expand All @@ -195,8 +200,8 @@ function define_tests() {
noDeriveBitsKeys[algorithmName] = null;
});
promises.push(operation);
});
Object.keys(spki).forEach(function(algorithmName) {
}
{
var operation = subtle.importKey("spki", spki[algorithmName],
{name: algorithmName},
false, [])
Expand All @@ -206,10 +211,17 @@ function define_tests() {
publicKeys[algorithmName] = null;
});
promises.push(operation);
});

}
{
var operation = subtle.importKey("spki", ecSPKI,
{name: "ECDH", namedCurve: "P-256"},
false, [])
.then(function(key) {
ecdhPublicKeys[algorithmName] = key;
});
}
return Promise.all(promises)
.then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, noDeriveBitsKeys: noDeriveBitsKeys}});
.then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, noDeriveBitsKeys: noDeriveBitsKeys, ecdhKeys: ecdhPublicKeys}});
}

// Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves
// META: script=cfrg_curves_bits_fixtures.js
// META: script=cfrg_curves_bits.js

// Define subtests from a `promise_test` to ensure the harness does not
// complete before the subtests are available. `explicit_done` cannot be used
// for this purpose because the global `done` function is automatically invoked
// by the WPT infrastructure in dedicated worker tests defined using the
// "multi-global" pattern.
promise_test(define_tests_25519, 'setup - define tests');
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// META: title=WebCryptoAPI: deriveBits() Using ECDH with CFRG Elliptic Curves
// META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves
// META: script=cfrg_curves_bits_fixtures.js
// META: script=cfrg_curves_bits.js

Expand All @@ -7,4 +7,4 @@
// for this purpose because the global `done` function is automatically invoked
// by the WPT infrastructure in dedicated worker tests defined using the
// "multi-global" pattern.
promise_test(define_tests, 'setup - define tests');
promise_test(define_tests_448, 'setup - define tests');

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 32 additions & 19 deletions test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
function define_tests_25519() {
return define_tests("X25519");
}

function define_tests_448() {
return define_tests("X448");
}

function define_tests() {
function define_tests(algorithmName) {
// May want to test prefixed implementations.
var subtle = self.crypto.subtle;

Expand All @@ -8,7 +15,7 @@ function define_tests() {
// https://www.rfc-editor.org/rfc/rfc7748#section-6.1
// TODO: The spec states that the check must be done on use, but there is discussion about doing it on import.
// https://github.com/WICG/webcrypto-secure-curves/pull/13
Object.keys(kSmallOrderPoint).forEach(function(algorithmName) {
{
kSmallOrderPoint[algorithmName].forEach(function(test) {
promise_test(async() => {
let derived;
Expand All @@ -32,10 +39,10 @@ function define_tests() {
assert_equals(derived, undefined, "Operation succeeded, but should not have.");
}, algorithmName + " deriveBits checks for all-zero value result with a key of order " + test.order);
});
});
}

// Ensure the keys generated by each algorithm are valid for key derivation.
Object.keys(sizes).forEach(function(algorithmName) {
{
promise_test(async() => {
let derived;
try {
Expand All @@ -46,15 +53,16 @@ function define_tests() {
}
assert_false (derived === undefined, "Key derivation failed.");
}, "Key derivation using a " + algorithmName + " generated keys.");
});
}

return importKeys(pkcs8, spki, sizes)
.then(function(results) {
publicKeys = results.publicKeys;
privateKeys = results.privateKeys;
noDeriveKeyKeys = results.noDeriveKeyKeys;
ecdhKeys = results.ecdhKeys;

Object.keys(sizes).forEach(function(algorithmName) {
{
// Basic success case
promise_test(function(test) {
return subtle.deriveKey({name: algorithmName, public: publicKeys[algorithmName]}, privateKeys[algorithmName], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
Expand Down Expand Up @@ -102,11 +110,7 @@ function define_tests() {

// - wrong algorithm
promise_test(function(test) {
publicKey = publicKeys["X25519"];
if (algorithmName === "X25519") {
publicKey = publicKeys["X448"];
}
return subtle.deriveKey({name: algorithmName, public: publicKey}, privateKeys[algorithmName], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
return subtle.deriveKey({name: algorithmName, public: ecdhKeys[algorithmName]}, privateKeys[algorithmName], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
.then(function(key) {return crypto.subtle.exportKey("raw", key);})
.then(function(exportedKey) {
assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
Expand Down Expand Up @@ -161,16 +165,17 @@ function define_tests() {
});
});
}, algorithmName + " public property value is a secret key");
});
}
});

function importKeys(pkcs8, spki, sizes) {
var privateKeys = {};
var publicKeys = {};
var noDeriveKeyKeys = {};
var ecdhPublicKeys = {};

var promises = [];
Object.keys(pkcs8).forEach(function(algorithmName) {
{
var operation = subtle.importKey("pkcs8", pkcs8[algorithmName],
{name: algorithmName},
false, ["deriveBits", "deriveKey"])
Expand All @@ -180,8 +185,8 @@ function define_tests() {
privateKeys[algorithmName] = null;
});
promises.push(operation);
});
Object.keys(pkcs8).forEach(function(algorithmName) {
}
{
var operation = subtle.importKey("pkcs8", pkcs8[algorithmName],
{name: algorithmName},
false, ["deriveBits"])
Expand All @@ -191,8 +196,8 @@ function define_tests() {
noDeriveKeyKeys[algorithmName] = null;
});
promises.push(operation);
});
Object.keys(spki).forEach(function(algorithmName) {
}
{
var operation = subtle.importKey("spki", spki[algorithmName],
{name: algorithmName},
false, [])
Expand All @@ -202,10 +207,18 @@ function define_tests() {
publicKeys[algorithmName] = null;
});
promises.push(operation);
});
}
{
var operation = subtle.importKey("spki", ecSPKI,
{name: "ECDH", namedCurve: "P-256"},
false, [])
.then(function(key) {
ecdhPublicKeys[algorithmName] = key;
});
}

return Promise.all(promises)
.then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, noDeriveKeyKeys: noDeriveKeyKeys}});
.then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, noDeriveKeyKeys: noDeriveKeyKeys, ecdhKeys: ecdhPublicKeys}});
}

// Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves
// META: script=cfrg_curves_bits_fixtures.js
// META: script=cfrg_curves_keys.js

// Define subtests from a `promise_test` to ensure the harness does not
// complete before the subtests are available. `explicit_done` cannot be used
// for this purpose because the global `done` function is automatically invoked
// by the WPT infrastructure in dedicated worker tests defined using the
// "multi-global" pattern.
promise_test(define_tests_25519, 'setup - define tests');
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
// for this purpose because the global `done` function is automatically invoked
// by the WPT infrastructure in dedicated worker tests defined using the
// "multi-global" pattern.
promise_test(define_tests, 'setup - define tests');
promise_test(define_tests_448, 'setup - define tests');
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// META: title=WebCryptoAPI: deriveKey() Using HKDF and PBKDF2 from an ECDH key
// META: script=derive_key_and_encrypt.js
// META: script=../util/helpers.js

// Test imported from WebKit's source, defined to check the impact of the
// 'Get Key Length' behavior of HKDF and PBKDF2, which should return 'null'
// in both cases, in the 'deriveKey' operation.
// https://bugs.webkit.org/show_bug.cgi?id=282096
promise_test(define_tests, 'setup - define tests');
Loading
Loading