node/test/parallel/test-webcrypto-webidl.js
Filip Skokan 3ef38c4bd7
crypto: use WebIDL converters in WebCryptoAPI
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: https://github.com/nodejs/node/pull/46067
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2023-01-17 08:57:58 +00:00

519 lines
17 KiB
JavaScript

// Flags: --expose-internals
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const webidl = require('internal/crypto/webidl');
const { subtle } = globalThis.crypto;
const { generateKeySync } = require('crypto');
const { converters } = webidl;
const prefix = "Failed to execute 'fn' on 'interface'";
const context = '1st argument';
const opts = { prefix, context };
// Required arguments.length
{
assert.throws(() => webidl.requiredArguments(0, 3, { prefix }), {
code: 'ERR_MISSING_ARGS',
name: 'TypeError',
message: `${prefix}: 3 arguments required, but only 0 present.`
});
assert.throws(() => webidl.requiredArguments(0, 1, { prefix }), {
code: 'ERR_MISSING_ARGS',
name: 'TypeError',
message: `${prefix}: 1 argument required, but only 0 present.`
});
// Does not throw when extra are added
webidl.requiredArguments(4, 3, { prefix });
}
// boolean
{
assert.strictEqual(converters.boolean(0), false);
assert.strictEqual(converters.boolean(NaN), false);
assert.strictEqual(converters.boolean(undefined), false);
assert.strictEqual(converters.boolean(null), false);
assert.strictEqual(converters.boolean(false), false);
assert.strictEqual(converters.boolean(''), false);
assert.strictEqual(converters.boolean(1), true);
assert.strictEqual(converters.boolean(Number.POSITIVE_INFINITY), true);
assert.strictEqual(converters.boolean(Number.NEGATIVE_INFINITY), true);
assert.strictEqual(converters.boolean('1'), true);
assert.strictEqual(converters.boolean('0'), true);
assert.strictEqual(converters.boolean('false'), true);
assert.strictEqual(converters.boolean(function() {}), true);
assert.strictEqual(converters.boolean(Symbol()), true);
assert.strictEqual(converters.boolean([]), true);
assert.strictEqual(converters.boolean({}), true);
}
// int conversion
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
{
for (const [converter, max] of [
[converters.octet, Math.pow(2, 8) - 1],
[converters['unsigned short'], Math.pow(2, 16) - 1],
[converters['unsigned long'], Math.pow(2, 32) - 1],
]) {
assert.strictEqual(converter(0), 0);
assert.strictEqual(converter(max), max);
assert.strictEqual(converter('' + 0), 0);
assert.strictEqual(converter('' + max), max);
assert.strictEqual(converter(3), 3);
assert.strictEqual(converter('' + 3), 3);
assert.strictEqual(converter(3.1), 3);
assert.strictEqual(converter(3.7), 3);
assert.strictEqual(converter(max + 1), 0);
assert.strictEqual(converter(max + 2), 1);
assert.throws(() => converter(max + 1, { ...opts, enforceRange: true }), {
name: 'TypeError',
code: 'ERR_OUT_OF_RANGE',
message: `${prefix}: ${context} is outside the expected range of 0 to ${max}.`,
});
assert.strictEqual(converter({}), 0);
assert.strictEqual(converter(NaN), 0);
assert.strictEqual(converter(false), 0);
assert.strictEqual(converter(true), 1);
assert.strictEqual(converter('1'), 1);
assert.strictEqual(converter('0'), 0);
assert.strictEqual(converter('{}'), 0);
assert.strictEqual(converter({}), 0);
assert.strictEqual(converter([]), 0);
assert.strictEqual(converter(function() {}), 0);
assert.throws(() => converter(Symbol(), opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is a Symbol and cannot be converted to a number.`
});
assert.throws(() => converter(0n, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is a BigInt and cannot be converted to a number.`
});
}
}
// DOMString
{
assert.strictEqual(converters.DOMString(1), '1');
assert.strictEqual(converters.DOMString(1n), '1');
assert.strictEqual(converters.DOMString(false), 'false');
assert.strictEqual(converters.DOMString(true), 'true');
assert.strictEqual(converters.DOMString(undefined), 'undefined');
assert.strictEqual(converters.DOMString(NaN), 'NaN');
assert.strictEqual(converters.DOMString({}), '[object Object]');
assert.strictEqual(converters.DOMString({ foo: 'bar' }), '[object Object]');
assert.strictEqual(converters.DOMString([]), '');
assert.strictEqual(converters.DOMString([1, 2]), '1,2');
assert.throws(() => converters.DOMString(Symbol(), opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is a Symbol and cannot be converted to a string.`
});
}
// object
{
for (const good of [{}, [], new Array(), function() {}]) {
assert.deepStrictEqual(converters.object(good), good);
}
for (const bad of [undefined, null, NaN, false, true, 0, 1, '', 'foo', Symbol(), 9n]) {
assert.throws(() => converters.object(bad, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is not an object.`
});
}
}
// Uint8Array
{
for (const good of [Buffer.alloc(0), new Uint8Array()]) {
assert.deepStrictEqual(converters.Uint8Array(good), good);
}
for (const bad of [new ArrayBuffer(), new SharedArrayBuffer(), [], null, 'foo', undefined, true]) {
assert.throws(() => converters.Uint8Array(bad, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is not an Uint8Array object.`
});
}
assert.throws(() => converters.Uint8Array(new Uint8Array(new SharedArrayBuffer()), opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is a view on a SharedArrayBuffer, which is not allowed.`
});
}
// BufferSource
{
for (const good of [
Buffer.alloc(0),
new Uint8Array(),
new ArrayBuffer(),
new DataView(new ArrayBuffer()),
new BigInt64Array(),
new BigUint64Array(),
new Float32Array(),
new Float64Array(),
new Int8Array(),
new Int16Array(),
new Int32Array(),
new Uint8ClampedArray(),
new Uint16Array(),
new Uint32Array(),
]) {
assert.deepStrictEqual(converters.BufferSource(good), good);
}
for (const bad of [new SharedArrayBuffer(), [], null, 'foo', undefined, true]) {
assert.throws(() => converters.BufferSource(bad, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.`
});
}
assert.throws(() => converters.BufferSource(new Uint8Array(new SharedArrayBuffer()), opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is a view on a SharedArrayBuffer, which is not allowed.`
});
}
// CryptoKey
{
subtle.generateKey({ name: 'AES-CBC', length: 128 }, false, ['encrypt']).then((key) => {
assert.deepStrictEqual(converters.CryptoKey(key), key);
}).then(common.mustCall());
for (const bad of [
generateKeySync('aes', { length: 128 }),
undefined, null, 1, {}, Symbol(), true, false, [],
]) {
assert.throws(() => converters.CryptoKey(bad, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: `${prefix}: ${context} is not of type CryptoKey.`
});
}
}
// AlgorithmIdentifier (Union for (object or DOMString))
{
assert.strictEqual(converters.AlgorithmIdentifier('foo'), 'foo');
assert.deepStrictEqual(converters.AlgorithmIdentifier({ name: 'foo' }), { name: 'foo' });
}
// JsonWebKey
{
for (const good of [
{},
{ use: 'sig' },
{ key_ops: ['sign'] },
{ ext: true },
{ oth: [] },
{ oth: [{ r: '', d: '', t: '' }] },
]) {
assert.deepStrictEqual(converters.JsonWebKey(good), good);
assert.deepStrictEqual(converters.JsonWebKey({ ...good, filtered: 'out' }), good);
}
}
// KeyFormat
{
for (const good of ['jwk', 'spki', 'pkcs8', 'raw']) {
assert.strictEqual(converters.KeyFormat(good), good);
}
for (const bad of ['foo', 1, false]) {
assert.throws(() => converters.KeyFormat(bad, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: `${prefix}: ${context} value '${bad}' is not a valid enum value of type KeyFormat.`,
});
}
}
// KeyUsage
{
for (const good of [
'encrypt',
'decrypt',
'sign',
'verify',
'deriveKey',
'deriveBits',
'wrapKey',
'unwrapKey',
]) {
assert.strictEqual(converters.KeyUsage(good), good);
}
for (const bad of ['foo', 1, false]) {
assert.throws(() => converters.KeyUsage(bad, opts), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: `${prefix}: ${context} value '${bad}' is not a valid enum value of type KeyUsage.`,
});
}
}
// Algorithm
{
const good = { name: 'RSA-PSS' };
assert.deepStrictEqual(converters.Algorithm({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.Algorithm({}, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'Algorithm' because 'name' is required in 'Algorithm'.`,
});
}
// RsaHashedKeyGenParams
{
for (const good of [
{
name: 'RSA-OAEP',
hash: { name: 'SHA-1' },
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
},
{
name: 'RSA-OAEP',
hash: 'SHA-1',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
},
]) {
assert.deepStrictEqual(converters.RsaHashedKeyGenParams({ ...good, filtered: 'out' }, opts), good);
for (const required of ['hash', 'publicExponent', 'modulusLength']) {
assert.throws(() => converters.RsaHashedKeyGenParams({ ...good, [required]: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'RsaHashedKeyGenParams' because '${required}' is required in 'RsaHashedKeyGenParams'.`,
});
}
}
}
// RsaHashedImportParams
{
for (const good of [
{ name: 'RSA-OAEP', hash: { name: 'SHA-1' } },
{ name: 'RSA-OAEP', hash: 'SHA-1' },
]) {
assert.deepStrictEqual(converters.RsaHashedImportParams({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.RsaHashedImportParams({ ...good, hash: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'RsaHashedImportParams' because 'hash' is required in 'RsaHashedImportParams'.`,
});
}
}
// RsaPssParams
{
const good = { name: 'RSA-PSS', saltLength: 20 };
assert.deepStrictEqual(converters.RsaPssParams({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.RsaPssParams({ ...good, saltLength: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'RsaPssParams' because 'saltLength' is required in 'RsaPssParams'.`,
});
}
// RsaOaepParams
{
for (const good of [{ name: 'RSA-OAEP' }, { name: 'RSA-OAEP', label: Buffer.alloc(0) }]) {
assert.deepStrictEqual(converters.RsaOaepParams({ ...good, filtered: 'out' }, opts), good);
}
}
// EcKeyImportParams, EcKeyGenParams
{
for (const name of ['EcKeyImportParams', 'EcKeyGenParams']) {
const { [name]: converter } = converters;
const good = { name: 'ECDSA', namedCurve: 'P-256' };
assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converter({ ...good, namedCurve: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to '${name}' because 'namedCurve' is required in '${name}'.`,
});
}
}
// EcdsaParams
{
for (const good of [
{ name: 'ECDSA', hash: { name: 'SHA-1' } },
{ name: 'ECDSA', hash: 'SHA-1' },
]) {
assert.deepStrictEqual(converters.EcdsaParams({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.EcdsaParams({ ...good, hash: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'EcdsaParams' because 'hash' is required in 'EcdsaParams'.`,
});
}
}
// HmacKeyGenParams, HmacImportParams
{
for (const name of ['HmacKeyGenParams', 'HmacImportParams']) {
const { [name]: converter } = converters;
for (const good of [
{ name: 'HMAC', hash: { name: 'SHA-1' } },
{ name: 'HMAC', hash: { name: 'SHA-1' }, length: 20 },
{ name: 'HMAC', hash: 'SHA-1' },
{ name: 'HMAC', hash: 'SHA-1', length: 20 },
]) {
assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converter({ ...good, hash: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to '${name}' because 'hash' is required in '${name}'.`,
});
}
}
}
// AesKeyGenParams, AesDerivedKeyParams
{
for (const name of ['AesKeyGenParams', 'AesDerivedKeyParams']) {
const { [name]: converter } = converters;
const good = { name: 'AES-CBC', length: 128 };
assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converter({ ...good, length: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to '${name}' because 'length' is required in '${name}'.`,
});
}
}
// HkdfParams
{
for (const good of [
{ name: 'HKDF', hash: { name: 'SHA-1' }, salt: Buffer.alloc(0), info: Buffer.alloc(0) },
{ name: 'HKDF', hash: 'SHA-1', salt: Buffer.alloc(0), info: Buffer.alloc(0) },
]) {
assert.deepStrictEqual(converters.HkdfParams({ ...good, filtered: 'out' }, opts), good);
for (const required of ['hash', 'salt', 'info']) {
assert.throws(() => converters.HkdfParams({ ...good, [required]: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'HkdfParams' because '${required}' is required in 'HkdfParams'.`,
});
}
}
}
// Pbkdf2Params
{
for (const good of [
{ name: 'PBKDF2', hash: { name: 'SHA-1' }, iterations: 5, salt: Buffer.alloc(0) },
{ name: 'PBKDF2', hash: 'SHA-1', iterations: 5, salt: Buffer.alloc(0) },
]) {
assert.deepStrictEqual(converters.Pbkdf2Params({ ...good, filtered: 'out' }, opts), good);
for (const required of ['hash', 'iterations', 'salt']) {
assert.throws(() => converters.Pbkdf2Params({ ...good, [required]: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'Pbkdf2Params' because '${required}' is required in 'Pbkdf2Params'.`,
});
}
}
}
// AesCbcParams
{
const good = { name: 'AES-CBC', iv: Buffer.alloc(0) };
assert.deepStrictEqual(converters.AesCbcParams({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.AesCbcParams({ ...good, iv: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'AesCbcParams' because 'iv' is required in 'AesCbcParams'.`,
});
}
// AesGcmParams
{
for (const good of [
{ name: 'AES-GCM', iv: Buffer.alloc(0) },
{ name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 16 },
{ name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 16, additionalData: Buffer.alloc(0) },
]) {
assert.deepStrictEqual(converters.AesGcmParams({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.AesGcmParams({ ...good, iv: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'AesGcmParams' because 'iv' is required in 'AesGcmParams'.`,
});
}
}
// AesCtrParams
{
const good = { name: 'AES-CTR', counter: Buffer.alloc(0), length: 20 };
assert.deepStrictEqual(converters.AesCtrParams({ ...good, filtered: 'out' }, opts), good);
for (const required of ['counter', 'length']) {
assert.throws(() => converters.AesCtrParams({ ...good, [required]: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'AesCtrParams' because '${required}' is required in 'AesCtrParams'.`,
});
}
}
// EcdhKeyDeriveParams
{
subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']).then((kp) => {
const good = { name: 'ECDH', public: kp.publicKey };
assert.deepStrictEqual(converters.EcdhKeyDeriveParams({ ...good, filtered: 'out' }, opts), good);
assert.throws(() => converters.EcdhKeyDeriveParams({ ...good, public: undefined }, opts), {
name: 'TypeError',
code: 'ERR_MISSING_OPTION',
message: `${prefix}: ${context} can not be converted to 'EcdhKeyDeriveParams' because 'public' is required in 'EcdhKeyDeriveParams'.`,
});
}).then(common.mustCall());
}
// Ed448Params
{
for (const good of [
{ name: 'Ed448', context: new Uint8Array() },
{ name: 'Ed448' },
]) {
assert.deepStrictEqual(converters.Ed448Params({ ...good, filtered: 'out' }, opts), good);
}
}