diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index dc7f635a480a49..86daccb1776438 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -919,6 +919,14 @@ async function wrapKey(format, key, wrappingKey, algorithm) { } catch { algorithm = normalizeAlgorithm(algorithm, 'encrypt'); } + + if (algorithm.name !== wrappingKey[kAlgorithm].name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + + if (!ArrayPrototypeIncludes(wrappingKey[kKeyUsages], 'wrapKey')) + throw lazyDOMException( + 'Unable to use this key to wrapKey', 'InvalidAccessError'); + let keyData = await FunctionPrototypeCall(exportKey, this, format, key); if (format === 'jwk') { @@ -997,6 +1005,13 @@ async function unwrapKey( unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey'); + if (unwrapAlgo.name !== unwrappingKey[kAlgorithm].name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + + if (!ArrayPrototypeIncludes(unwrappingKey[kKeyUsages], 'unwrapKey')) + throw lazyDOMException( + 'Unable to use this key to unwrapKey', 'InvalidAccessError'); + let keyData = await cipherOrWrap( kWebCryptoCipherDecrypt, unwrapAlgo, @@ -1030,12 +1045,12 @@ async function signVerify(algorithm, key, data, signature) { } algorithm = normalizeAlgorithm(algorithm, usage); - if (!ArrayPrototypeIncludes(key[kKeyUsages], usage) || - algorithm.name !== key[kAlgorithm].name) { + if (algorithm.name !== key[kAlgorithm].name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + + if (!ArrayPrototypeIncludes(key[kKeyUsages], usage)) throw lazyDOMException( - `Unable to use this key to ${usage}`, - 'InvalidAccessError'); - } + `Unable to use this key to ${usage}`, 'InvalidAccessError'); switch (algorithm.name) { case 'RSA-PSS': @@ -1120,18 +1135,6 @@ async function verify(algorithm, key, signature, data) { } async function cipherOrWrap(mode, algorithm, key, data, op) { - // We use a Node.js style error here instead of a DOMException because - // the WebCrypto spec is not specific what kind of error is to be thrown - // in this case. Both Firefox and Chrome throw simple TypeErrors here. - // The key algorithm and cipher algorithm must match, and the - // key must have the proper usage. - if (key[kAlgorithm].name !== algorithm.name || - !ArrayPrototypeIncludes(key[kKeyUsages], op)) { - throw lazyDOMException( - 'The requested operation is not valid for the provided key', - 'InvalidAccessError'); - } - // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. @@ -1182,6 +1185,14 @@ async function encrypt(algorithm, key, data) { }); algorithm = normalizeAlgorithm(algorithm, 'encrypt'); + + if (algorithm.name !== key[kAlgorithm].name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + + if (!ArrayPrototypeIncludes(key[kKeyUsages], 'encrypt')) + throw lazyDOMException( + 'Unable to use this key to encrypt', 'InvalidAccessError'); + return await cipherOrWrap( kWebCryptoCipherEncrypt, algorithm, @@ -1211,6 +1222,14 @@ async function decrypt(algorithm, key, data) { }); algorithm = normalizeAlgorithm(algorithm, 'decrypt'); + + if (algorithm.name !== key[kAlgorithm].name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + + if (!ArrayPrototypeIncludes(key[kKeyUsages], 'decrypt')) + throw lazyDOMException( + 'Unable to use this key to decrypt', 'InvalidAccessError'); + return await cipherOrWrap( kWebCryptoCipherDecrypt, algorithm, diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index e03be277f089c7..d7a7dca6584c16 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -49,7 +49,7 @@ async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { ['decrypt']); return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { - message: /The requested operation is not valid for the provided key/ + message: /Unable to use this key to encrypt/ }); } @@ -65,7 +65,7 @@ async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { const output = await subtle.encrypt(algorithm, key, plaintext); return assert.rejects(subtle.decrypt(algorithm, key, output), { - message: /The requested operation is not valid for the provided key/ + message: /Unable to use this key to decrypt/ }); } @@ -80,7 +80,23 @@ async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { ['encrypt']); return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { - message: /The requested operation is not valid for the provided key/ + message: /Key algorithm mismatch/ + }); +} + +async function testDecryptWrongAlg({ keyBuffer, algorithm, result }, alg) { + if (result === undefined) return; + assert.notStrictEqual(algorithm.name, alg); + const keyFormat = alg === 'AES-OCB' ? 'raw-secret' : 'raw'; + const key = await subtle.importKey( + keyFormat, + keyBuffer, + { name: alg }, + false, + ['decrypt']); + + return assert.rejects(subtle.decrypt(algorithm, key, result), { + message: /Key algorithm mismatch/ }); } @@ -112,6 +128,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { variations.push(testEncryptNoEncrypt(vector)); variations.push(testEncryptNoDecrypt(vector)); variations.push(testEncryptWrongAlg(vector, 'AES-CTR')); + variations.push(testDecryptWrongAlg(vector, 'AES-CTR')); }); failing.forEach((vector) => { @@ -149,6 +166,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { variations.push(testEncryptNoEncrypt(vector)); variations.push(testEncryptNoDecrypt(vector)); variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + variations.push(testDecryptWrongAlg(vector, 'AES-CBC')); }); // TODO(@jasnell): These fail for different reasons. Need to @@ -188,6 +206,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { variations.push(testEncryptNoEncrypt(vector)); variations.push(testEncryptNoDecrypt(vector)); variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + variations.push(testDecryptWrongAlg(vector, 'AES-CBC')); }); failing.forEach((vector) => { @@ -225,6 +244,7 @@ if (hasOpenSSL(3)) { variations.push(testEncryptNoEncrypt(vector)); variations.push(testEncryptNoDecrypt(vector)); variations.push(testEncryptWrongAlg(vector, 'AES-GCM')); + variations.push(testDecryptWrongAlg(vector, 'AES-GCM')); }); failing.forEach((vector) => { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 5362484288089d..0825027a7c3c02 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -48,7 +48,7 @@ async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { ['decrypt']); return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { - message: /The requested operation is not valid for the provided key/ + message: /Unable to use this key to encrypt/ }); } @@ -63,7 +63,7 @@ async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { const output = await subtle.encrypt(algorithm, key, plaintext); return assert.rejects(subtle.decrypt(algorithm, key, output), { - message: /The requested operation is not valid for the provided key/ + message: /Unable to use this key to decrypt/ }); } @@ -77,7 +77,22 @@ async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { ['encrypt']); return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { - message: /The requested operation is not valid for the provided key/ + message: /Key algorithm mismatch/ + }); +} + +async function testDecryptWrongAlg({ keyBuffer, algorithm, result }, alg) { + if (result === undefined) return; + assert.notStrictEqual(algorithm.name, alg); + const key = await subtle.importKey( + 'raw-secret', + keyBuffer, + { name: alg }, + false, + ['decrypt']); + + return assert.rejects(subtle.decrypt(algorithm, key, result), { + message: /Key algorithm mismatch/ }); } @@ -107,6 +122,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { variations.push(testEncryptNoEncrypt(vector)); variations.push(testEncryptNoDecrypt(vector)); variations.push(testEncryptWrongAlg(vector, 'AES-GCM')); + variations.push(testDecryptWrongAlg(vector, 'AES-GCM')); }); failing.forEach((vector) => { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js b/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js index e1a10023d2bcf8..ccd2c4565109c9 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js @@ -147,7 +147,7 @@ async function testEncryptionWrongKey({ algorithm, ['decrypt']); return assert.rejects( subtle.encrypt(algorithm, privateKey, plaintext), { - message: /The requested operation is not valid/ + message: /Unable to use this key to encrypt/ }); } @@ -167,7 +167,7 @@ async function testEncryptionBadUsage({ algorithm, ['decrypt']); return assert.rejects( subtle.encrypt(algorithm, publicKey, plaintext), { - message: /The requested operation is not valid/ + message: /Unable to use this key to encrypt/ }); } @@ -191,7 +191,7 @@ async function testDecryptionWrongKey({ ciphertext, return assert.rejects( subtle.decrypt(algorithm, publicKey, ciphertext), { - message: /The requested operation is not valid/ + message: /Unable to use this key to decrypt/ }); } @@ -215,7 +215,7 @@ async function testDecryptionBadUsage({ ciphertext, return assert.rejects( subtle.decrypt(algorithm, publicKey, ciphertext), { - message: /The requested operation is not valid/ + message: /Unable to use this key to decrypt/ }); } diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index a00c7d214bad99..c4ca52862fe094 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -43,14 +43,14 @@ const { subtle } = globalThis.crypto; name: 'RSA-OAEP', }, privateKey, buf), { name: 'InvalidAccessError', - message: 'The requested operation is not valid for the provided key' + message: 'Unable to use this key to encrypt' }); await assert.rejects(() => subtle.decrypt({ name: 'RSA-OAEP', }, publicKey, ciphertext), { name: 'InvalidAccessError', - message: 'The requested operation is not valid for the provided key' + message: 'Unable to use this key to decrypt' }); } @@ -88,14 +88,14 @@ if (!process.features.openssl_is_boringssl) { name: 'RSA-OAEP', }, privateKey, buf), { name: 'InvalidAccessError', - message: 'The requested operation is not valid for the provided key' + message: 'Unable to use this key to encrypt' }); await assert.rejects(() => subtle.decrypt({ name: 'RSA-OAEP', }, publicKey, ciphertext), { name: 'InvalidAccessError', - message: 'The requested operation is not valid for the provided key' + message: 'Unable to use this key to decrypt' }); } diff --git a/test/parallel/test-webcrypto-sign-verify-ecdsa.js b/test/parallel/test-webcrypto-sign-verify-ecdsa.js index 8fbf572ef5c64e..eb7814efa55698 100644 --- a/test/parallel/test-webcrypto-sign-verify-ecdsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ecdsa.js @@ -88,17 +88,17 @@ async function testVerify({ name, // Test failure when using the wrong algorithms await assert.rejects( subtle.verify({ name, hash }, hmacKey, signature, plaintext), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify({ name, hash }, rsaKeys.publicKey, signature, plaintext), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify({ name, hash }, okpKeys.publicKey, signature, plaintext), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); // Test failure when signature is altered @@ -210,17 +210,17 @@ async function testSign({ name, // Test failure when using the wrong algorithms await assert.rejects( subtle.sign({ name, hash }, hmacKey, plaintext), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign({ name, hash }, rsaKeys.privateKey, plaintext), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign({ name, hash }, okpKeys.privateKey, plaintext), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); } diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index 2c39216e4fb98b..1dfd3ddd76a6a0 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -101,17 +101,17 @@ async function testVerify({ name, // Test failure when using the wrong algorithms await assert.rejects( subtle.verify({ name, context }, hmacKey, signature, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify({ name, context }, rsaKeys.publicKey, signature, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify({ name, context }, ecKeys.publicKey, signature, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); if (name === 'Ed448' && supportsContext) { @@ -227,17 +227,17 @@ async function testSign({ name, // Test failure when using the wrong algorithms await assert.rejects( subtle.sign({ name, context }, hmacKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign({ name, context }, rsaKeys.privateKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign({ name, context }, ecKeys.privateKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); if (name === 'Ed448' && supportsContext) { diff --git a/test/parallel/test-webcrypto-sign-verify-hmac.js b/test/parallel/test-webcrypto-sign-verify-hmac.js index 5c2d8e6cd770ff..ac3841fad79ab8 100644 --- a/test/parallel/test-webcrypto-sign-verify-hmac.js +++ b/test/parallel/test-webcrypto-sign-verify-hmac.js @@ -62,7 +62,7 @@ async function testVerify({ hash, // Test failure when using the wrong algorithms await assert.rejects( subtle.verify({ name, hash }, rsaKeys.publicKey, signature, plaintext), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); // Test failure when signature is altered @@ -165,7 +165,7 @@ async function testSign({ hash, // Test failure when using the wrong algorithms await assert.rejects( subtle.sign({ name, hash }, rsaKeys.privateKey, plaintext), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); } diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index d41095e0893d97..008047630753b2 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -69,7 +69,7 @@ async function testVerify({ algorithm, // Test failure when using the wrong algorithms await assert.rejects( subtle.verify(signParams, keyPair.publicKey, expected, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); // Test failure when signature is altered @@ -177,7 +177,7 @@ async function testSign({ algorithm, // Test failure when using the wrong algorithms await assert.rejects( subtle.sign(signParams, keyPair.privateKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); } diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 092bf5b052e5f9..1ed74c2508f438 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -86,17 +86,17 @@ async function testVerify({ name, // Test failure when using the wrong algorithms await assert.rejects( subtle.verify({ name, context }, hmacKey, signature, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify({ name, context }, rsaKeys.publicKey, signature, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify({ name, context }, ecKeys.publicKey, signature, data), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); // Test failure when too long context @@ -194,17 +194,17 @@ async function testSign({ name, // Test failure when using the wrong algorithms await assert.rejects( subtle.sign({ name, context }, hmacKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign({ name, context }, rsaKeys.privateKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign({ name, context }, ecKeys.privateKey, data), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); // Test failure when too long context diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 7dd5637b12f702..0ccbf431f14740 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -82,12 +82,12 @@ async function testVerify({ // Test failure when using the wrong algorithms await assert.rejects( subtle.verify(algorithm, hmacKey, signature, plaintext), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.verify(algorithm, ecdsaKeys.publicKey, signature, plaintext), { - message: /Unable to use this key to verify/ + message: /Key algorithm mismatch/ }); // Test failure when signature is altered @@ -185,12 +185,12 @@ async function testSign({ // Test failure when using the wrong algorithms await assert.rejects( subtle.sign(algorithm, hmacKey, plaintext), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); await assert.rejects( subtle.sign(algorithm, ecdsaKeys.privateKey, plaintext), { - message: /Unable to use this key to sign/ + message: /Key algorithm mismatch/ }); } diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index bd788ec4ed8828..a0cbe2c324f786 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -378,3 +378,92 @@ function testWrapping(name, keys) { }); await Promise.all(variations); })().then(common.mustCall()); + +// Test that wrapKey/unwrapKey validate the wrapping/unwrapping key's +// algorithm and usage before proceeding. +// Spec: https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey +// Steps 9-10 (wrapping key checks) must precede step 12 (exportKey). +(async function() { + const hmacKey = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + + const ecKey = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'], + ); + + // Wrong algorithm: wrapping key is HMAC but algorithm says AES-GCM. + // Even though exporting ecKey.privateKey as 'spki' would also fail + // (wrong key type for spki), the wrapping key check must come first. + await assert.rejects( + subtle.wrapKey('spki', ecKey.privateKey, hmacKey, { + name: 'AES-GCM', + iv: new Uint8Array(12), + }), { + name: 'InvalidAccessError', + message: 'Key algorithm mismatch', + }); + + // Missing wrapKey usage: aesKey only has encrypt/decrypt, not wrapKey. + // Even though exporting ecKey.privateKey as 'spki' would also fail, + // the usage check must come first. + const aesKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, + true, + ['encrypt', 'decrypt'], + ); + + await assert.rejects( + subtle.wrapKey('spki', ecKey.privateKey, aesKey, { + name: 'AES-GCM', + iv: new Uint8Array(12), + }), { + name: 'InvalidAccessError', + message: 'Unable to use this key to wrapKey', + }); + + // Correct wrapping key algorithm and usage results in the expected + // exportKey error (not the wrapping key validation error). + const wrapKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, + true, + ['wrapKey'], + ); + + await assert.rejects( + subtle.wrapKey('spki', ecKey.privateKey, wrapKey, { + name: 'AES-GCM', + iv: new Uint8Array(12), + }), { + // exportKey('spki', privateKey) throws NotSupportedError + name: 'NotSupportedError', + }); + + // --- unwrapKey validation tests --- + + const ciphertext = new Uint8Array(32); // Dummy ciphertext + + // Wrong algorithm: unwrapping key is HMAC but algorithm says AES-GCM. + await assert.rejects( + subtle.unwrapKey('raw', ciphertext, hmacKey, { + name: 'AES-GCM', + iv: new Uint8Array(12), + }, { name: 'AES-GCM', length: 128 }, true, ['encrypt']), { + name: 'InvalidAccessError', + message: 'Key algorithm mismatch', + }); + + // Missing unwrapKey usage: aesKey only has encrypt/decrypt, not unwrapKey. + await assert.rejects( + subtle.unwrapKey('raw', ciphertext, aesKey, { + name: 'AES-GCM', + iv: new Uint8Array(12), + }, { name: 'AES-GCM', length: 128 }, true, ['encrypt']), { + name: 'InvalidAccessError', + message: 'Unable to use this key to unwrapKey', + }); +})().then(common.mustCall());