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

crypto: use WebIDL converters in WebCryptoAPI #46067

Merged
merged 27 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bc436eb
crypto: use WebIDL converters in WebCryptoAPI
panva Jan 2, 2023
03c0459
add docs change entry
panva Jan 3, 2023
28a3c4f
make webidl require a noop after first use
panva Jan 3, 2023
c11a280
add note to the normalizeAlgorithm
panva Jan 3, 2023
c1f2da3
match normalizeAlgorithm case-insensitive definition
panva Jan 3, 2023
c5bab06
update unrecognized name message and assertions
panva Jan 3, 2023
2900c12
remove unused converter
panva Jan 3, 2023
976cd80
refactor makeException to have default code
panva Jan 3, 2023
b40e895
update toNumber to use makeException
panva Jan 3, 2023
7208755
carry context to toNumber exceptions
panva Jan 3, 2023
cc91875
normalize webidl error message sentences
panva Jan 3, 2023
30353de
add webidl.requiredArguments tests
panva Jan 3, 2023
cdfbb9d
add webidl.converters.boolean tests
panva Jan 3, 2023
1216371
add more converter tests
panva Jan 3, 2023
ffcd2fc
add more converter tests
panva Jan 3, 2023
4bed6b0
lint and doc change change
panva Jan 3, 2023
609f194
address some comments
panva Jan 3, 2023
c7af35a
address comments
panva Jan 3, 2023
39e40f0
address comments
panva Jan 3, 2023
91c8abd
address comments
panva Jan 3, 2023
833e286
remove more redundant checks
panva Jan 3, 2023
fcddb59
refactor JWK import validations
panva Jan 3, 2023
31b983c
upcoming linter rules fix
panva Jan 3, 2023
5d74979
webcrypto does not need defaultValue
panva Jan 4, 2023
44640fb
improve coverage
panva Jan 4, 2023
fb9d1c4
apply review feedback
panva Jan 5, 2023
42b9fb2
address more prototype pollution
panva Jan 5, 2023
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
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 coersed and validated as per their WebIDL
definitions like in other Web Crypto API implementations.
- version: v19.0.0
pr-url: https://github.com/nodejs/node/pull/44897
description: No longer experimental except for the `Ed25519`, `Ed448`,
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