current commit
260925.24 · crypto: separate conversion from validation
// 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 };

function asIdlDictionary(value) {
  return { __proto__: null, ...value };
}

function assertIdlDictionary(actual, expected) {
  assert.deepStrictEqual(actual, asIdlDictionary(expected));
}

function assertJsonWebKey(actual, expected) {
  const idlDictionary = asIdlDictionary(expected);
  if (idlDictionary.oth !== undefined)
    idlDictionary.oth = idlDictionary.oth.map(asIdlDictionary);
  assert.deepStrictEqual(actual, idlDictionary);
}

// 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.`
    });
  }
}

// [EnforceRange] integer dictionary members
{
  const kOctetMax = 2 ** 8 - 1;
  const kUnsignedShortMax = 2 ** 16 - 1;
  const kUnsignedLongMax = 2 ** 32 - 1;
  const empty = Buffer.alloc(0);
  const rsaKeyGen = {
    name: 'RSA-PSS',
    modulusLength: 2048,
    publicExponent: new Uint8Array([1, 0, 1]),
  };
  const aesKeyParams = { name: 'AES-GCM', length: 128 };
  const hmacKeyParams = {
    name: 'HMAC',
    hash: 'SHA-256',
    length: 128,
  };
  const kmacKeyParams = { name: 'KMAC128', length: 128 };
  const cases = [
    ['RsaKeyGenParams', rsaKeyGen,
     { modulusLength: kUnsignedLongMax }],
    ['RsaHashedKeyGenParams', { ...rsaKeyGen, hash: 'SHA-256' },
     { modulusLength: kUnsignedLongMax }],
    ['AesKeyGenParams', aesKeyParams,
     { length: kUnsignedShortMax }],
    ['RsaPssParams', { name: 'RSA-PSS', saltLength: 20 },
     { saltLength: kUnsignedLongMax }],
    ['HmacKeyGenParams', hmacKeyParams,
     { length: kUnsignedLongMax }],
    ['HmacImportParams', hmacKeyParams,
     { length: kUnsignedLongMax }],
    ['CShakeParams', { name: 'cSHAKE128', outputLength: 256 },
     { outputLength: kUnsignedLongMax }],
    ['Pbkdf2Params', {
      name: 'PBKDF2',
      salt: empty,
      iterations: 1,
      hash: 'SHA-256',
    }, { iterations: kUnsignedLongMax }],
    ['AesDerivedKeyParams', aesKeyParams,
     { length: kUnsignedShortMax }],
    ['AeadParams', {
      name: 'AES-GCM',
      iv: Buffer.alloc(12),
      tagLength: 128,
    }, { tagLength: kOctetMax }],
    ['AesCtrParams', {
      name: 'AES-CTR',
      counter: Buffer.alloc(16),
      length: 128,
    }, { length: kOctetMax }],
    ['Argon2Params', {
      name: 'Argon2id',
      nonce: Buffer.alloc(8),
      parallelism: 1,
      memory: 8,
      passes: 1,
      version: 0x13,
    }, {
      parallelism: kUnsignedLongMax,
      memory: kUnsignedLongMax,
      passes: kUnsignedLongMax,
      version: kOctetMax,
    }],
    ['KmacKeyGenParams', kmacKeyParams,
     { length: kUnsignedLongMax }],
    ['KmacImportParams', kmacKeyParams,
     { length: kUnsignedLongMax }],
    ['KmacParams', { name: 'KMAC128', outputLength: 256 },
     { outputLength: kUnsignedLongMax }],
    ['KangarooTwelveParams', { name: 'KT128', outputLength: 256 },
     { outputLength: kUnsignedLongMax }],
    ['TurboShakeParams', {
      name: 'TurboSHAKE128',
      outputLength: 256,
      domainSeparation: 0x1f,
    }, {
      outputLength: kUnsignedLongMax,
      domainSeparation: kOctetMax,
    }],
  ];

  for (const [dictionary, base, members] of cases) {
    const converter = converters[dictionary];
    assertIdlDictionary(converter(base, opts), base);

    for (const [member, max] of Object.entries(members)) {
      assert.throws(
        () => converter({ ...base, [member]: -1 }, opts), {
          name: 'TypeError',
          code: 'ERR_OUT_OF_RANGE',
          message: `${prefix}: ${member} in ${context} is outside ` +
                   `the expected range of 0 to ${max}.`,
        });
    }
  }
}

// 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.`
  });

  {
    const resizable = new ArrayBuffer(8, { maxByteLength: 16 });
    const view = new Uint8Array(resizable);

    const resizableError = {
      name: 'TypeError',
      code: 'ERR_INVALID_ARG_TYPE',
      message: `${prefix}: ${context} is backed by a resizable ` +
        'ArrayBuffer, which is not allowed.',
    };
    assert.throws(() => converters.BigInteger(view, opts), resizableError);
  }
}

// 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.`
  });

  {
    const resizable = new ArrayBuffer(8, { maxByteLength: 16 });
    const view = new Uint8Array(resizable);
    const dataView = new DataView(resizable);

    const resizableError = {
      name: 'TypeError',
      code: 'ERR_INVALID_ARG_TYPE',
      message: `${prefix}: ${context} is backed by a resizable ` +
        'ArrayBuffer, which is not allowed.',
    };
    assert.throws(
      () => converters.BufferSource(resizable, opts),
      resizableError);
    assert.throws(
      () => converters.BufferSource(view, opts),
      resizableError);
    assert.throws(
      () => converters.BufferSource(dataView, opts),
      resizableError);
  }
}

// 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: '' }] },
  ]) {
    assertJsonWebKey(converters.JsonWebKey(good), good);
    assertJsonWebKey(converters.JsonWebKey({ ...good, filtered: 'out' }), good);
  }
}

// KeyFormat
{
  for (const good of [
    'jwk',
    'spki',
    'pkcs8',
    'raw',
    'raw-public',
    'raw-seed',
    'raw-secret',
    'raw-private',
  ]) {
    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} '${bad}' is not a valid enum value of type KeyFormat.`,
    });
  }
}

// KeyUsage
{
  for (const good of [
    'encrypt',
    'decrypt',
    'sign',
    'verify',
    'deriveKey',
    'deriveBits',
    'wrapKey',
    'unwrapKey',
    'encapsulateBits',
    'decapsulateBits',
    'encapsulateKey',
    'decapsulateKey',
  ]) {
    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} '${bad}' is not a valid enum value of type KeyUsage.`,
    });
  }
}

// Algorithm
{
  const good = { name: 'RSA-PSS' };
  assertIdlDictionary(converters.Algorithm({ ...good, filtered: 'out' }, opts), good);

  assert.throws(() => converters.Algorithm({}, opts), {
    name: 'TypeError',
    code: 'ERR_MISSING_OPTION',
    message: `${prefix}: ${context} cannot 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]),
    },
  ]) {
    assertIdlDictionary(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} cannot 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' },
  ]) {
    assertIdlDictionary(converters.RsaHashedImportParams({ ...good, filtered: 'out' }, opts), good);
    assert.throws(() => converters.RsaHashedImportParams({ ...good, hash: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot be converted to 'RsaHashedImportParams' because 'hash' is required in 'RsaHashedImportParams'.`,
    });
  }
}

// RsaPssParams
{
  const good = { name: 'RSA-PSS', saltLength: 20 };
  assertIdlDictionary(converters.RsaPssParams({ ...good, filtered: 'out' }, opts), good);

  assert.throws(() => converters.RsaPssParams({ ...good, saltLength: undefined }, opts), {
    name: 'TypeError',
    code: 'ERR_MISSING_OPTION',
    message: `${prefix}: ${context} cannot 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) }]) {
    assertIdlDictionary(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' };
    assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good);

    assert.throws(() => converter({ ...good, namedCurve: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot 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' },
  ]) {
    assertIdlDictionary(converters.EcdsaParams({ ...good, filtered: 'out' }, opts), good);
    assert.throws(() => converters.EcdsaParams({ ...good, hash: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot 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: 32 },
      { name: 'HMAC', hash: 'SHA-1' },
      { name: 'HMAC', hash: 'SHA-1', length: 32 },
    ]) {
      assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good);
      assert.throws(() => converter({ ...good, hash: undefined }, opts), {
        name: 'TypeError',
        code: 'ERR_MISSING_OPTION',
        message: `${prefix}: ${context} cannot 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 };
    assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good);

    assert.throws(() => converter({ ...good, length: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot 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) },
  ]) {
    assertIdlDictionary(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} cannot 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) },
  ]) {
    assertIdlDictionary(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} cannot be converted to 'Pbkdf2Params' because '${required}' is required in 'Pbkdf2Params'.`,
      });
    }
  }
}

// Argon2Params
{
  const good = {
    name: 'Argon2id',
    memory: 8,
    nonce: Buffer.alloc(8),
    parallelism: 1,
    passes: 1,
  };

  assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good);

  assertIdlDictionary(
    converters.Argon2Params({
      ...good,
      associatedData: Buffer.alloc(0),
      secretValue: Buffer.alloc(0),
    }, opts),
    {
      ...good,
      associatedData: Buffer.alloc(0),
      secretValue: Buffer.alloc(0),
    });

  for (const required of ['memory', 'nonce', 'parallelism', 'passes']) {
    assert.throws(() => converters.Argon2Params({ ...good, [required]: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot be converted to 'Argon2Params' because '${required}' is required in 'Argon2Params'.`,
    });
  }

  const zeroPasses = converters.Argon2Params({ ...good, passes: 0 }, opts);
  assert.strictEqual(zeroPasses.passes, 0);
  assert.throws(() => webidl.validators.Argon2Params(zeroPasses), {
    name: 'OperationError',
    message: 'passes must be > 0',
  });
}

// AesCbcParams
{
  const good = { name: 'AES-CBC', iv: Buffer.alloc(16) };
  assertIdlDictionary(converters.AesCbcParams({ ...good, filtered: 'out' }, opts), good);

  assert.throws(() => converters.AesCbcParams({ ...good, iv: undefined }, opts), {
    name: 'TypeError',
    code: 'ERR_MISSING_OPTION',
    message: `${prefix}: ${context} cannot be converted to 'AesCbcParams' because 'iv' is required in 'AesCbcParams'.`,
  });
}

// AeadParams
{
  for (const good of [
    { name: 'AES-GCM', iv: Buffer.alloc(0) },
    { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64 },
    { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64, additionalData: Buffer.alloc(0) },
  ]) {
    assertIdlDictionary(converters.AeadParams({ ...good, filtered: 'out' }, opts), good);

    assert.throws(() => converters.AeadParams({ ...good, iv: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot be converted to 'AeadParams' because 'iv' is required in 'AeadParams'.`,
    });
  }
}

// AesCtrParams
{
  const good = { name: 'AES-CTR', counter: Buffer.alloc(16), length: 20 };
  assertIdlDictionary(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} cannot 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 };
    assertIdlDictionary(converters.EcdhKeyDeriveParams({ ...good, filtered: 'out' }, opts), good);

    assert.throws(() => converters.EcdhKeyDeriveParams({ ...good, public: undefined }, opts), {
      name: 'TypeError',
      code: 'ERR_MISSING_OPTION',
      message: `${prefix}: ${context} cannot be converted to 'EcdhKeyDeriveParams' because 'public' is required in 'EcdhKeyDeriveParams'.`,
    });
  }).then(common.mustCall());
}

// ContextParams
{
  for (const good of [
    { name: 'Ed448', context: new Uint8Array() },
    { name: 'Ed448' },
  ]) {
    assertIdlDictionary(converters.ContextParams({ ...good, filtered: 'out' }, opts), good);
  }
}

// Argon2Params
{
  const maxParallelism = 2 ** 24 - 1;
  const good = {
    name: 'Argon2id',
    nonce: Buffer.alloc(8),
    parallelism: maxParallelism,
    memory: 8 * maxParallelism,
    passes: 1,
  };
  assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good);

  const excessiveParallelism = converters.Argon2Params({
    ...good,
    parallelism: maxParallelism + 1,
    memory: 8 * (maxParallelism + 1),
  }, opts);
  assert.strictEqual(excessiveParallelism.parallelism, maxParallelism + 1);
  assert.throws(() => webidl.validators.Argon2Params(excessiveParallelism), {
    name: 'OperationError',
    message: 'parallelism must be > 0 and <= 16777215',
  });
}