Skip to content

Commit

Permalink
crypto: use WebIDL converters in WebCryptoAPI
Browse files Browse the repository at this point in the history
WebCryptoAPI functions' arguments are now coersed and validated as per
their WebIDL definitions like in other Web Crypto API implementations.
This further improves interoperability with other implementations of
Web Crypto API.

PR-URL: #46067
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Backport-PR-URL: #46252
  • Loading branch information
panva authored and danielleadams committed May 17, 2023
1 parent 5dc0fee commit ab05294
Show file tree
Hide file tree
Showing 32 changed files with 2,028 additions and 420 deletions.
4 changes: 4 additions & 0 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/46067
description: Arguments are now coerced and validated as per their WebIDL
definitions like in other Web Crypto API implementations.
- version: v18.4.0
pr-url: https://github.com/nodejs/node/pull/43310
description: Removed proprietary `'node.keyObject'` import/export format.
Expand Down
28 changes: 11 additions & 17 deletions lib/internal/crypto/aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const {
} = internalBinding('crypto');

const {
getArrayBufferOrView,
hasAnyNotIn,
jobPromise,
validateByteLength,
Expand Down Expand Up @@ -112,13 +111,10 @@ 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');
Expand All @@ -135,7 +131,6 @@ 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(
kCryptoJobAsync,
Expand Down Expand Up @@ -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 @@ -281,24 +273,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 @@ -308,10 +302,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
37 changes: 17 additions & 20 deletions lib/internal/crypto/cfrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const {
} = internalBinding('crypto');

const {
getArrayBufferOrView,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
Expand Down Expand Up @@ -73,7 +72,6 @@ function verifyAcceptableCfrgKeyUse(name, isPublic, usages) {

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

switch (name) {
case 'Ed25519':
Expand Down Expand Up @@ -237,12 +235,13 @@ async function cfrgImportKey(
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 @@ -260,30 +259,32 @@ 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 keyData', 'DataError');
throw lazyDOMException('Invalid JWK', 'DataError');
}

verifyAcceptableCfrgKeyUse(
Expand All @@ -305,7 +306,7 @@ async function cfrgImportKey(
false);

if (!createPublicKey(keyObject).equals(publicKeyObject)) {
throw lazyDOMException('Invalid JWK keyData', 'DataError');
throw lazyDOMException('Invalid JWK', 'DataError');
}
}
break;
Expand Down Expand Up @@ -336,13 +337,9 @@ 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(
Expand Down
9 changes: 0 additions & 9 deletions lib/internal/crypto/diffiehellman.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const {
validateInt32,
validateObject,
validateString,
validateUint32,
} = require('internal/validators');

const {
Expand All @@ -48,7 +47,6 @@ const {

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

const {
Expand Down Expand Up @@ -320,13 +318,6 @@ function diffieHellman(options) {
async function ecdhDeriveBits(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
39 changes: 16 additions & 23 deletions lib/internal/crypto/ec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ const {
} = internalBinding('crypto');

const {
codes: {
ERR_MISSING_OPTION,
},
} = require('internal/errors');

const {
getArrayBufferOrView,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
Expand Down Expand Up @@ -76,7 +69,6 @@ function verifyAcceptableEcKeyUse(name, isPublic, usages) {

function createECPublicKeyRaw(namedCurve, keyData) {
const handle = new KeyObjectHandle();
keyData = getArrayBufferOrView(keyData, 'keyData');

if (!handle.initECRaw(kNamedCurveAliases[namedCurve], keyData)) {
throw lazyDOMException('Invalid keyData', 'DataError');
Expand Down Expand Up @@ -204,50 +196,53 @@ async function ecImportKey(
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 !== 'EC')
throw lazyDOMException('Invalid key type', 'DataError');
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
if (keyData.crv !== namedCurve)
throw lazyDOMException('Named curve mismatch', 'DataError');
throw lazyDOMException(
'JWK "crv" does not match the requested algorithm',
'DataError');

verifyAcceptableEcKeyUse(
name,
keyData.d === undefined,
usagesSet);

if (usagesSet.size > 0 && keyData.use !== undefined) {
if (algorithm.name === 'ECDSA' && keyData.use !== 'sig')
throw lazyDOMException('Invalid use type', 'DataError');
if (algorithm.name === 'ECDH' && keyData.use !== 'enc')
throw lazyDOMException('Invalid use type', 'DataError');
const checkUse = name === 'ECDH' ? 'enc' : 'sig';
if (keyData.use !== checkUse)
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 (algorithm.name === 'ECDSA' && keyData.alg !== undefined) {
if (typeof keyData.alg !== 'string')
throw lazyDOMException('Invalid alg', 'DataError');
let algNamedCurve;
switch (keyData.alg) {
case 'ES256': algNamedCurve = 'P-256'; break;
case 'ES384': algNamedCurve = 'P-384'; break;
case 'ES512': algNamedCurve = 'P-521'; break;
}
if (algNamedCurve !== namedCurve)
throw lazyDOMException('Named curve mismatch', 'DataError');
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}

const handle = new KeyObjectHandle();
const type = handle.initJwk(keyData, namedCurve);
if (type === undefined)
throw lazyDOMException('Invalid JWK keyData', 'DataError');
throw lazyDOMException('Invalid JWK', 'DataError');
keyObject = type === kKeyTypePrivate ?
new PrivateKeyObject(handle) :
new PublicKeyObject(handle);
Expand Down Expand Up @@ -289,8 +284,6 @@ function ecdsaSignVerify(key, data, { name, hash }, signature) {
if (key.type !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');

if (hash === undefined)
throw new ERR_MISSING_OPTION('algorithm.hash');
const hashname = normalizeHashName(hash.name);

return jobPromise(() => new SignJob(
Expand Down
12 changes: 2 additions & 10 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ const {
} = internalBinding('crypto');

const {
getArrayBufferOrView,
getDefaultEncoding,
getStringOption,
jobPromise,
normalizeAlgorithm,
normalizeHashName,
validateMaxBufferLength,
kHandle,
Expand Down Expand Up @@ -168,13 +166,8 @@ Hmac.prototype._transform = Hash.prototype._transform;
// Implementation for WebCrypto subtle.digest()

async function asyncDigest(algorithm, data) {
algorithm = normalizeAlgorithm(algorithm);
data = getArrayBufferOrView(data, 'data');
validateMaxBufferLength(data, 'data');

if (algorithm.length !== undefined)
validateUint32(algorithm.length, 'algorithm.length');

switch (algorithm.name) {
case 'SHA-1':
// Fall through
Expand All @@ -186,11 +179,10 @@ async function asyncDigest(algorithm, data) {
return jobPromise(() => new HashJob(
kCryptoJobAsync,
normalizeHashName(algorithm.name),
data,
algorithm.length));
data));
}

throw lazyDOMException('Unrecognized name.', 'NotSupportedError');
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}

module.exports = {
Expand Down
8 changes: 1 addition & 7 deletions lib/internal/crypto/hkdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const {
const { kMaxLength } = require('buffer');

const {
getArrayBufferOrView,
normalizeHashName,
toBuf,
validateByteSource,
Expand All @@ -45,7 +44,6 @@ const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
ERR_MISSING_OPTION,
},
hideStackFrames,
} = require('internal/errors');
Expand Down Expand Up @@ -140,11 +138,7 @@ function hkdfSync(hash, key, salt, info, length) {

const hkdfPromise = promisify(hkdf);
async function hkdfDeriveBits(algorithm, baseKey, length) {
const { hash } = algorithm;
const salt = getArrayBufferOrView(algorithm.salt, 'algorithm.salt');
const info = getArrayBufferOrView(algorithm.info, 'algorithm.info');
if (hash === undefined)
throw new ERR_MISSING_OPTION('algorithm.hash');
const { hash, salt, info } = algorithm;

if (length === 0)
throw lazyDOMException('length cannot be zero', 'OperationError');
Expand Down
Loading

0 comments on commit ab05294

Please sign in to comment.