Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v16.x backport] WebCryptoAPI fixes #47336

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/webcrypto.md
Expand Up @@ -2,6 +2,10 @@

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/46067

Check warning on line 6 in doc/api/webcrypto.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Arguments are now coerced and validated as per their WebIDL
definitions like in other Web Crypto API implementations.
- version: v16.17.0
pr-url: https://github.com/nodejs/node/pull/43310
description: Removed proprietary `'node.keyObject'` import/export format.
Expand Down
36 changes: 15 additions & 21 deletions lib/internal/crypto/aes.js
Expand Up @@ -32,7 +32,6 @@ const {
} = internalBinding('crypto');

const {
getArrayBufferOrView,
hasAnyNotIn,
jobPromise,
validateByteLength,
Expand Down Expand Up @@ -112,19 +111,16 @@ function getVariant(name, length) {
}

function asyncAesCtrCipher(mode, key, data, { counter, length }) {
counter = getArrayBufferOrView(counter, 'algorithm.counter');
validateByteLength(counter, 'algorithm.counter', 16);
// The length must specify an integer between 1 and 128. While
// there is no default, this should typically be 64.
if (typeof length !== 'number' ||
length <= 0 ||
length > kMaxCounterLength) {
if (length === 0 || length > kMaxCounterLength) {
throw lazyDOMException(
'AES-CTR algorithm.length must be between 1 and 128',
'OperationError');
}

return jobPromise(new AESCipherJob(
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
Expand All @@ -135,9 +131,8 @@ function asyncAesCtrCipher(mode, key, data, { counter, length }) {
}

function asyncAesCbcCipher(mode, key, data, { iv }) {
iv = getArrayBufferOrView(iv, 'algorithm.iv');
validateByteLength(iv, 'algorithm.iv', 16);
return jobPromise(new AESCipherJob(
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
Expand All @@ -147,7 +142,7 @@ function asyncAesCbcCipher(mode, key, data, { iv }) {
}

function asyncAesKwCipher(mode, key, data) {
return jobPromise(new AESCipherJob(
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
Expand All @@ -166,12 +161,9 @@ function asyncAesGcmCipher(
'OperationError'));
}

iv = getArrayBufferOrView(iv, 'algorithm.iv');
validateMaxBufferLength(iv, 'algorithm.iv');

if (additionalData !== undefined) {
additionalData =
getArrayBufferOrView(additionalData, 'algorithm.additionalData');
validateMaxBufferLength(additionalData, 'algorithm.additionalData');
}

Expand Down Expand Up @@ -201,7 +193,7 @@ function asyncAesGcmCipher(
break;
}

return jobPromise(new AESCipherJob(
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
Expand Down Expand Up @@ -282,24 +274,26 @@ async function aesImportKey(
break;
}
case 'jwk': {
if (keyData == null || typeof keyData !== 'object')
throw lazyDOMException('Invalid JWK keyData', 'DataError');
if (!keyData.kty)
throw lazyDOMException('Invalid keyData', 'DataError');

if (keyData.kty !== 'oct')
throw lazyDOMException('Invalid key type', 'DataError');
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');

if (usagesSet.size > 0 &&
keyData.use !== undefined &&
keyData.use !== 'enc') {
throw lazyDOMException('Invalid use type', 'DataError');
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
}

validateKeyOps(keyData.key_ops, usagesSet);

if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException('JWK is not extractable', 'DataError');
throw lazyDOMException(
'JWK "ext" Parameter and extractable mismatch',
'DataError');
}

const handle = new KeyObjectHandle();
Expand All @@ -309,10 +303,10 @@ async function aesImportKey(
validateKeyLength(length);

if (keyData.alg !== undefined) {
if (typeof keyData.alg !== 'string')
throw lazyDOMException('Invalid alg', 'DataError');
if (keyData.alg !== getAlgorithmName(algorithm.name, length))
throw lazyDOMException('Algorithm mismatch', 'DataError');
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}

keyObject = new SecretKeyObject(handle);
Expand Down
110 changes: 70 additions & 40 deletions lib/internal/crypto/cfrg.js
Expand Up @@ -18,7 +18,6 @@ const {
} = internalBinding('crypto');

const {
getArrayBufferOrView,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
Expand Down Expand Up @@ -53,7 +52,14 @@ function verifyAcceptableCfrgKeyUse(name, type, usages) {
case 'X25519':
// Fall through
case 'X448':
checkSet = ['deriveKey', 'deriveBits'];
switch (type) {
case 'private':
checkSet = ['deriveKey', 'deriveBits'];
break;
case 'public':
checkSet = [];
break;
}
break;
case 'Ed25519':
// Fall through
Expand All @@ -76,33 +82,32 @@ function verifyAcceptableCfrgKeyUse(name, type, usages) {

function createCFRGRawKey(name, keyData, isPublic) {
const handle = new KeyObjectHandle();
keyData = getArrayBufferOrView(keyData, 'keyData');

switch (name) {
case 'Ed25519':
case 'X25519':
if (keyData.byteLength !== 32) {
throw lazyDOMException(
`${name} raw keys must be exactly 32-bytes`);
`${name} raw keys must be exactly 32-bytes`, 'DataError');
}
break;
case 'Ed448':
if (keyData.byteLength !== 57) {
throw lazyDOMException(
`${name} raw keys must be exactly 57-bytes`);
`${name} raw keys must be exactly 57-bytes`, 'DataError');
}
break;
case 'X448':
if (keyData.byteLength !== 56) {
throw lazyDOMException(
`${name} raw keys must be exactly 56-bytes`);
`${name} raw keys must be exactly 56-bytes`, 'DataError');
}
break;
}

const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initEDRaw(name, keyData, keyType)) {
throw lazyDOMException('Failure to generate key object');
throw lazyDOMException('Invalid keyData', 'DataError');
}

return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle);
Expand Down Expand Up @@ -194,7 +199,7 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) {

function cfrgExportKey(key, format) {
emitExperimentalWarning(`The ${key.algorithm.name} Web Crypto API algorithm`);
return jobPromise(new ECKeyExportJob(
return jobPromise(() => new ECKeyExportJob(
kCryptoJobAsync,
format,
key[kKeyObject][kHandle]));
Expand All @@ -214,29 +219,40 @@ async function cfrgImportKey(
switch (format) {
case 'spki': {
verifyAcceptableCfrgKeyUse(name, 'public', usagesSet);
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki'
});
try {
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki'
});
} catch {
throw lazyDOMException(
'Invalid keyData', 'DataError');
}
break;
}
case 'pkcs8': {
verifyAcceptableCfrgKeyUse(name, 'private', usagesSet);
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8'
});
try {
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8'
});
} catch {
throw lazyDOMException(
'Invalid keyData', 'DataError');
}
break;
}
case 'jwk': {
if (keyData == null || typeof keyData !== 'object')
throw lazyDOMException('Invalid JWK keyData', 'DataError');
if (!keyData.kty)
throw lazyDOMException('Invalid keyData', 'DataError');
if (keyData.kty !== 'OKP')
throw lazyDOMException('Invalid key type', 'DataError');
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
if (keyData.crv !== name)
throw lazyDOMException('Subtype mismatch', 'DataError');
throw lazyDOMException(
'JWK "crv" Parameter and algorithm name mismatch', 'DataError');
const isPublic = keyData.d === undefined;

if (usagesSet.size > 0 && keyData.use !== undefined) {
Expand All @@ -254,38 +270,56 @@ async function cfrgImportKey(
break;
}
if (keyData.use !== checkUse)
throw lazyDOMException('Invalid use type', 'DataError');
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
}

validateKeyOps(keyData.key_ops, usagesSet);

if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException('JWK is not extractable', 'DataError');
throw lazyDOMException(
'JWK "ext" Parameter and extractable mismatch',
'DataError');
}

if (keyData.alg !== undefined) {
if (typeof keyData.alg !== 'string')
throw lazyDOMException('Invalid alg', 'DataError');
if (
(name === 'Ed25519' || name === 'Ed448') &&
keyData.alg !== 'EdDSA'
) {
throw lazyDOMException('Invalid alg', 'DataError');
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}
}

if (!isPublic && typeof keyData.x !== 'string') {
throw lazyDOMException('Invalid JWK', 'DataError');
}

verifyAcceptableCfrgKeyUse(
name,
isPublic ? 'public' : 'private',
usagesSet);
keyObject = createCFRGRawKey(

const publicKeyObject = createCFRGRawKey(
name,
Buffer.from(
isPublic ? keyData.x : keyData.d,
'base64'),
isPublic);
Buffer.from(keyData.x, 'base64'),
true);

if (isPublic) {
keyObject = publicKeyObject;
} else {
keyObject = createCFRGRawKey(
name,
Buffer.from(keyData.d, 'base64'),
false);

if (!createPublicKey(keyObject).equals(publicKeyObject)) {
throw lazyDOMException('Invalid JWK', 'DataError');
}
}
break;
}
case 'raw': {
Expand Down Expand Up @@ -314,16 +348,12 @@ function eddsaSignVerify(key, data, { name, context }, signature) {
if (key.type !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');

if (name === 'Ed448' && context !== undefined) {
context =
getArrayBufferOrView(context, 'algorithm.context');
if (context.byteLength !== 0) {
throw lazyDOMException(
'Non zero-length context is not yet supported.', 'NotSupportedError');
}
if (name === 'Ed448' && context?.byteLength) {
throw lazyDOMException(
'Non zero-length context is not yet supported.', 'NotSupportedError');
}

return jobPromise(new SignJob(
return jobPromise(() => new SignJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
Expand Down
15 changes: 5 additions & 10 deletions lib/internal/crypto/diffiehellman.js
Expand Up @@ -37,7 +37,6 @@ const {
validateInt32,
validateObject,
validateString,
validateUint32,
} = require('internal/validators');

const {
Expand All @@ -51,7 +50,6 @@ const {

const {
KeyObject,
isCryptoKey,
} = require('internal/crypto/keys');

const {
Expand Down Expand Up @@ -335,13 +333,6 @@ function deriveBitsECDH(name, publicKey, privateKey, callback) {
async function asyncDeriveBitsECDH(algorithm, baseKey, length) {
const { 'public': key } = algorithm;

// Null means that we're not asking for a specific number of bits, just
// give us everything that is generated.
if (length !== null)
validateUint32(length, 'length');
if (!isCryptoKey(key))
throw new ERR_INVALID_ARG_TYPE('algorithm.public', 'CryptoKey', key);

if (key.type !== 'public') {
throw lazyDOMException(
'algorithm.public must be a public key', 'InvalidAccessError');
Expand Down Expand Up @@ -377,7 +368,11 @@ async function asyncDeriveBitsECDH(algorithm, baseKey, length) {
key.algorithm.name === 'ECDH' ? baseKey.algorithm.namedCurve : baseKey.algorithm.name,
key[kKeyObject][kHandle],
baseKey[kKeyObject][kHandle], (err, bits) => {
if (err) return reject(err);
if (err) {
return reject(lazyDOMException(
'The operation failed for an operation-specific reason',
'OperationError'));
}
resolve(bits);
});
});
Expand Down