'use strict';

const common = require('../common');

if (!common.hasCrypto)
  common.skip('missing crypto');

const assert = require('assert');
const { subtle } = globalThis.crypto;

const kTests = [
  {
    namedCurve: 'P-521',
    pkcs8: '3081ee020100301006072a8648ce3d020106052b810400230481d63081d302010' +
           '1044201a67ed321915a64aa359b7d648ddc2618fa8e8d1867e8f71830b10d25ed' +
           '2891faf12f3c7e75421a2ea264f9a915320d274fe1470742b984e96b98912081f' +
           'acd478da18189038186000400209d483f28666881c6641f3a126f400f51e46511' +
           '70fe678c75e85712e2868adc850824997bebf0bc82b43028a6d2ec1777ca45279' +
           'f7206a3ea8b5cd2073f493e45000cb54c3a5acaa268c56710428878d98b8afbf6' +
           '8a612153632846d807e92672698f1b9c611de7d38e34cd6c73889092c56e52d68' +
           '0f1dfd092b87ac8ef9ff3c8fb48',
    spki: '30819b301006072a8648ce3d020106052b81040023038186000400ee69f94715d7' +
          '01e9e2011333d4f4f96cba7d91f88b112baf75cf09cc1f8aca97618da9389822d2' +
          '9b6fe9996a61203ef752b771e8958fc4677bb3778565ab60d6ed00deab6761895b' +
          '935e3ad325fb8549e56f13786aa73f88a2ecfe40933473d8aef240c4dfd7d506f2' +
          '2cdd0e55558f3fbf05ebf7efef7a72d78f46469b8448f26e2712',
    result: '009c2bce57be80adab3b07385b8e5990eb7d6fdebdb01bf35371a4f6075e9d28',
  },
  {
    namedCurve: 'P-384',
    pkcs8: '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b02010' +
           '10430f871a5666589c14a5747263ef85b319cc023db6e35676c3d781eef8b055f' +
           'cfbe86fa0d06d056b5195fb1323af8de25b3a16403620004f11965df7dd4594d0' +
           '419c5086482a3b826b9797f9be0bd0d109c9e1e9989c1b9a92b8f269f98e17ad1' +
           '84ba73c1f79762af45af8141602642da271a6bb0ffeb0cb4478fcf707e661aa6d' +
           '6cdf51549c88c3f130be9e8201f6f6a09f4185aaf95c4',
    spki: '3076301006072a8648ce3d020106052b810400220362000491822dc2af59c18f5b' +
          '67f80df61a2603c2a8f0b3c0af822d63c279701a824560404401dde9a56ee52757' +
          'ea8bc748d4c82b5337b48d7b65583a3d572438880036bac6730f42ca5278966bd5' +
          'f21e86e21d30c5a6d0463ec513dd509ffcdcaf1ff5',
    result: 'e0bd6bce0aef8ca48838a6e2fcc57e67b9c5e8860c5f0be9dabec53e454e18a0',
  },
];

async function prepareKeys() {
  const keys = {};
  await Promise.all(
    kTests.map(async ({ namedCurve, size, pkcs8, spki, result }) => {
      const [
        privateKey,
        publicKey,
      ] = await Promise.all([
        subtle.importKey(
          'pkcs8',
          Buffer.from(pkcs8, 'hex'),
          {
            name: 'ECDH',
            namedCurve
          },
          true,
          ['deriveKey', 'deriveBits']),
        subtle.importKey(
          'spki',
          Buffer.from(spki, 'hex'),
          {
            name: 'ECDH',
            namedCurve
          },
          true,
          []),
      ]);
      keys[namedCurve] = {
        privateKey,
        publicKey,
        size,
        result,
      };
    }));
  return keys;
}

(async function() {
  const keys = await prepareKeys();
  const otherArgs = [
    { name: 'HMAC', hash: 'SHA-256', length: 256 },
    true,
    ['sign', 'verify']];

  await Promise.all(
    Object.keys(keys).map(async (namedCurve) => {
      const { result, privateKey, publicKey } = keys[namedCurve];

      {
        // Good parameters
        const key = await subtle.deriveKey({
          name: 'ECDH',
          public: publicKey
        }, privateKey, ...otherArgs);

        const raw = await subtle.exportKey('raw', key);

        assert.strictEqual(Buffer.from(raw).toString('hex'), result);
      }

      {
        // Non-multiple of 8 derived HMAC key length
        const key = await subtle.deriveKey({
          name: 'ECDH',
          public: publicKey
        }, privateKey, {
          name: 'HMAC',
          hash: 'SHA-256',
          length: 9
        }, true, ['sign', 'verify']);

        const raw = await subtle.exportKey('raw', key);
        const expected = Buffer.from(result.slice(0, 4), 'hex');
        expected[1] &= 0b10000000;

        assert.strictEqual(key.algorithm.length, 9);
        assert.deepStrictEqual(Buffer.from(raw), expected);
      }

      {
        // Case insensitivity
        const key = await subtle.deriveKey({
          name: 'eCdH',
          public: publicKey
        }, privateKey, {
          name: 'HmAc',
          hash: 'SHA-256',
          length: 256
        }, true, ['sign', 'verify']);

        const raw = await subtle.exportKey('raw', key);

        assert.strictEqual(Buffer.from(raw).toString('hex'), result);
      }
    }));

  // Error tests
  {
    // Missing public property
    await assert.rejects(
      subtle.deriveKey(
        { name: 'ECDH' },
        keys['P-384'].privateKey,
        ...otherArgs),
      { code: 'ERR_MISSING_OPTION' });
  }

  {
    // The public property is not a CryptoKey
    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: { message: 'Not a CryptoKey' }
        },
        keys['P-384'].privateKey,
        ...otherArgs),
      { code: 'ERR_INVALID_ARG_TYPE' });
  }

  {
    // Mismatched named curves
    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: keys['P-384'].publicKey
        },
        keys['P-521'].privateKey,
        ...otherArgs),
      { message: /Named curve mismatch/ });
  }

  {
    // Incorrect public key algorithm
    const { publicKey } = await subtle.generateKey(
      {
        name: 'ECDSA',
        namedCurve: 'P-521'
      },
      false,
      ['sign', 'verify']);

    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: publicKey
        },
        keys['P-521'].privateKey,
        ...otherArgs),
      { message: 'key algorithm mismatch' });
  }

  {
    // Private key does not have correct usages
    const privateKey = await subtle.importKey(
      'pkcs8',
      Buffer.from(kTests[0].pkcs8, 'hex'),
      {
        name: 'ECDH',
        namedCurve: 'P-521'
      }, false, ['deriveBits']);

    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: keys['P-521'].publicKey,
        },
        privateKey,
        ...otherArgs),
      { message: /baseKey does not have deriveKey usage/ });
  }

  {
    // Base key is not a private key
    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: keys['P-521'].publicKey
        },
        keys['P-521'].publicKey,
        ...otherArgs),
      { name: 'InvalidAccessError' });
  }

  {
    // Base key is not a private key
    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: keys['P-521'].privateKey
        },
        keys['P-521'].publicKey,
        ...otherArgs),
      { name: 'InvalidAccessError' });
  }

  {
    // Public is a secret key
    const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32));
    const key = await subtle.importKey(
      'raw',
      keyData,
      { name: 'AES-CBC', length: 256 },
      false, ['encrypt']);

    await assert.rejects(
      subtle.deriveKey(
        {
          name: 'ECDH',
          public: key
        },
        keys['P-521'].publicKey,
        ...otherArgs),
      { name: 'InvalidAccessError' });
  }
})().then(common.mustCall());
