'use strict';

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

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

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

const keyData = {
  'Ed25519': {
    jwsAlg: 'Ed25519',
    spki: Buffer.from(
      '302a300506032b6570032100a054b618c12b26c8d43595a5c38dd2b0140b944a' +
      '151f75003278c2b6c58ec08f', 'hex'),
    pkcs8: Buffer.from(
      '302e020100300506032b657004220420d53150bdcd17b4d4b21ae756d4965639' +
      'd75b28f56ff9111b1f88326913e445bc', 'hex'),
    jwk: {
      kty: 'OKP',
      crv: 'Ed25519',
      x: 'oFS2GMErJsjUNZWlw43SsBQLlEoVH3UAMnjCtsWOwI8',
      d: '1TFQvc0XtNSyGudW1JZWOddbKPVv-REbH4gyaRPkRbw'
    }
  },
  'Ed448': {
    jwsAlg: 'Ed448',
    spki: Buffer.from(
      '3043300506032b6571033a0008cc38160c85bca5656ac4924af7ea97a9161b20' +
      '2528273dcb84afd2eeb99ac912a401b34ef15ef4d9486406a6eecc31e5909219' +
      'bd54866800', 'hex'),
    pkcs8: Buffer.from(
      '3047020100300506032b6571043b0439afd05b2fbb153b47c18dfa66baaed0de' +
      'fb4e88c651487cdee0fafc40fa3d048fe1cd145a44143243c0468166b5bc161a' +
      '82e3b904f3e2fcaaf9', 'hex'),
    jwk: {
      kty: 'OKP',
      crv: 'Ed448',
      x: 'CMw4FgyFvKVlasSSSvfql6kWGyAlKCc9y4Sv0u65mskSpAGzTvFe9NlIZAam7' +
         'swx5ZCSGb1UhmgA',
      d: 'r9BbL7sVO0fBjfpmuq7Q3vtOiMZRSHze4Pr8QPo9BI_hzRRaRBQyQ8BGgWa1v' +
         'BYaguO5BPPi_Kr5'
    }
  },
  'X25519': {
    jwsAlg: 'ECDH-ES',
    spki: Buffer.from(
      '302a300506032b656e032100f38d9f4e621a44e0428176a4c8a534b34f07f8db' +
      '30152f9ca0167aabf598fe65', 'hex'),
    pkcs8: Buffer.from(
      '302e020100300506032b656e04220420a8327850317b4b03a5a8b4e923413b1d' +
      'a4a642e0d6f7a72cf4d16a549e628a5f', 'hex'),
    jwk: {
      kty: 'OKP',
      crv: 'X25519',
      x: '842fTmIaROBCgXakyKU0s08H-NswFS-coBZ6q_WY_mU',
      d: 'qDJ4UDF7SwOlqLTpI0E7HaSmQuDW96cs9NFqVJ5iil8'
    }
  },
  'X448': {
    jwsAlg: 'ECDH-ES',
    spki: Buffer.from(
      '3042300506032b656f0339001d451c8c0c369a42eadfc2875cd44953caeb46c4' +
      '66dc86568280bfdbbb01f4709a1b0b1e0dd66cf7b11c84119ddc98890db72891' +
      '29e30da4', 'hex'),
    pkcs8: Buffer.from(
      '3046020100300506032b656f043a0438fc818f6546a81f963c27765dc1c05bfd' +
      'b169667e5e0cf45318ed1cb93872217ab0d9004e0c7dd0dcb00192f72039cc1a' +
      '1dff750ec31c8afb', 'hex'),
    jwk: {
      kty: 'OKP',
      crv: 'X448',
      x: 'HUUcjAw2mkLq38KHXNRJU8rrRsRm3IZWgoC_27sB9HCaGwseDdZs97EchBGd3' +
         'JiJDbcokSnjDaQ',
      d: '_IGPZUaoH5Y8J3ZdwcBb_bFpZn5eDPRTGO0cuThyIXqw2QBODH3Q3LABkvcgO' +
         'cwaHf91DsMcivs'
    }
  }
};

const testVectors = [
  {
    name: 'Ed25519',
    privateUsages: ['sign'],
    publicUsages: ['verify']
  },
  {
    name: 'X25519',
    privateUsages: ['deriveKey', 'deriveBits'],
    publicUsages: []
  },
];

if (!process.features.openssl_is_boringssl) {
  testVectors.push(
    {
      name: 'Ed448',
      privateUsages: ['sign'],
      publicUsages: ['verify']
    },
    {
      name: 'X448',
      privateUsages: ['deriveKey', 'deriveBits'],
      publicUsages: []
    },
  );
} else {
  common.printSkipMessage('Skipping unsupported Curve448 test cases');
}

async function testImportSpki({ name, publicUsages }, extractable) {
  const key = await subtle.importKey(
    'spki',
    keyData[name].spki,
    { name },
    extractable,
    publicUsages);
  assert.strictEqual(key.type, 'public');
  assert.strictEqual(key.extractable, extractable);
  assert.deepStrictEqual(key.usages, publicUsages);
  assert.deepStrictEqual(key.algorithm.name, name);
  assert.strictEqual(key.algorithm, key.algorithm);
  assert.strictEqual(key.usages, key.usages);

  if (extractable) {
    // Test the roundtrip
    const spki = await subtle.exportKey('spki', key);
    assert.strictEqual(
      Buffer.from(spki).toString('hex'),
      keyData[name].spki.toString('hex'));
  } else {
    await assert.rejects(
      subtle.exportKey('spki', key), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
  }

  // Bad usage
  await assert.rejects(
    subtle.importKey(
      'spki',
      keyData[name].spki,
      { name },
      extractable,
      ['wrapKey']),
    { message: /Unsupported key usage/ });
}

async function testImportPkcs8({ name, privateUsages }, extractable) {
  const key = await subtle.importKey(
    'pkcs8',
    keyData[name].pkcs8,
    { name },
    extractable,
    privateUsages);
  assert.strictEqual(key.type, 'private');
  assert.strictEqual(key.extractable, extractable);
  assert.deepStrictEqual(key.usages, privateUsages);
  assert.deepStrictEqual(key.algorithm.name, name);
  assert.strictEqual(key.algorithm, key.algorithm);
  assert.strictEqual(key.usages, key.usages);

  if (extractable) {
    // Test the roundtrip
    const pkcs8 = await subtle.exportKey('pkcs8', key);
    assert.strictEqual(
      Buffer.from(pkcs8).toString('hex'),
      keyData[name].pkcs8.toString('hex'));
  } else {
    await assert.rejects(
      subtle.exportKey('pkcs8', key), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
  }

  await assert.rejects(
    subtle.importKey(
      'pkcs8',
      keyData[name].pkcs8,
      { name },
      extractable,
      [/* empty usages */]),
    { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
}

async function testImportJwk({ name, publicUsages, privateUsages }, extractable) {

  const jwk = keyData[name].jwk;

  const tests = [
    subtle.importKey(
      'jwk',
      {
        kty: jwk.kty,
        crv: jwk.crv,
        x: jwk.x,
      },
      { name },
      extractable, publicUsages),
    subtle.importKey(
      'jwk',
      jwk,
      { name },
      extractable,
      privateUsages),
    subtle.importKey(
      'jwk',
      {
        alg: keyData[name].jwsAlg,
        kty: jwk.kty,
        crv: jwk.crv,
        x: jwk.x,
      },
      { name },
      extractable, publicUsages),
    subtle.importKey(
      'jwk',
      {
        ...jwk,
        alg: keyData[name].jwsAlg,
      },
      { name },
      extractable,
      privateUsages),
  ];

  // Test the deprecated "alg" value
  if (keyData[name].jwsAlg?.startsWith('Ed')) {
    tests.push(
      subtle.importKey(
        'jwk',
        {
          alg: 'EdDSA',
          kty: jwk.kty,
          crv: jwk.crv,
          x: jwk.x,
        },
        { name },
        extractable, publicUsages),
      subtle.importKey(
        'jwk',
        {
          ...jwk,
          alg: 'EdDSA',
        },
        { name },
        extractable,
        privateUsages),
    );
  }

  const [
    publicKey,
    privateKey,
  ] = await Promise.all(tests);

  assert.strictEqual(publicKey.type, 'public');
  assert.strictEqual(privateKey.type, 'private');
  assert.strictEqual(publicKey.extractable, extractable);
  assert.strictEqual(privateKey.extractable, extractable);
  assert.deepStrictEqual(publicKey.usages, publicUsages);
  assert.deepStrictEqual(privateKey.usages, privateUsages);
  assert.strictEqual(publicKey.algorithm.name, name);
  assert.strictEqual(privateKey.algorithm.name, name);
  assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
  assert.strictEqual(privateKey.usages, privateKey.usages);
  assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
  assert.strictEqual(publicKey.usages, publicKey.usages);

  if (extractable) {
    // Test the round trip
    const [
      pubJwk,
      pvtJwk,
    ] = await Promise.all([
      subtle.exportKey('jwk', publicKey),
      subtle.exportKey('jwk', privateKey),
    ]);

    assert.deepStrictEqual(pubJwk.key_ops, publicUsages);
    assert.strictEqual(pubJwk.ext, true);
    assert.strictEqual(pubJwk.kty, 'OKP');
    assert.strictEqual(pubJwk.x, jwk.x);
    assert.strictEqual(pubJwk.crv, jwk.crv);

    assert.deepStrictEqual(pvtJwk.key_ops, privateUsages);
    assert.strictEqual(pvtJwk.ext, true);
    assert.strictEqual(pvtJwk.kty, 'OKP');
    assert.strictEqual(pvtJwk.x, jwk.x);
    assert.strictEqual(pvtJwk.crv, jwk.crv);
    assert.strictEqual(pvtJwk.d, jwk.d);

    if (jwk.crv.startsWith('Ed')) {
      assert.strictEqual(pubJwk.alg, jwk.crv);
      assert.strictEqual(pvtJwk.alg, jwk.crv);
    } else {
      assert.strictEqual(pubJwk.alg, undefined);
      assert.strictEqual(pvtJwk.alg, undefined);
    }
  } else {
    await assert.rejects(
      subtle.exportKey('jwk', publicKey), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
    await assert.rejects(
      subtle.exportKey('jwk', privateKey), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
  }

  {
    const invalidUse = name.startsWith('X') ? 'sig' : 'enc';
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, use: invalidUse },
        { name },
        extractable,
        privateUsages),
      { message: 'Invalid JWK "use" Parameter' });
  }

  if (name.startsWith('Ed')) {
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { kty: jwk.kty, x: jwk.x, crv: jwk.crv, alg: 'foo' },
        { name },
        extractable,
        publicUsages),
      { message: 'JWK "alg" does not match the requested algorithm' });

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, alg: 'foo' },
        { name },
        extractable,
        privateUsages),
      { message: 'JWK "alg" does not match the requested algorithm' });
  }

  for (const crv of [undefined, name === 'Ed25519' ? 'Ed448' : 'Ed25519']) {
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { kty: jwk.kty, x: jwk.x, y: jwk.y, crv },
        { name },
        extractable,
        publicUsages),
      { message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' });

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, crv },
        { name },
        extractable,
        privateUsages),
      { message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' });
  }

  await assert.rejects(
    subtle.importKey(
      'jwk',
      { ...jwk },
      { name },
      extractable,
      [/* empty usages */]),
    { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });

  await assert.rejects(
    subtle.importKey(
      'jwk',
      { kty: jwk.kty, /* missing x */ crv: jwk.crv },
      { name },
      extractable,
      publicUsages),
    { name: 'DataError', message: 'Invalid keyData' });
}

async function testImportRaw({ name, publicUsages }) {
  const jwk = keyData[name].jwk;

  const rawKeyData = Buffer.from(jwk.x, 'base64url');
  const publicKey = await subtle.importKey(
    'raw',
    rawKeyData,
    { name },
    true, publicUsages);

  assert.strictEqual(publicKey.type, 'public');
  assert.deepStrictEqual(publicKey.usages, publicUsages);
  assert.strictEqual(publicKey.algorithm.name, name);
  assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
  assert.strictEqual(publicKey.usages, publicKey.usages);

  // Test raw export round-trip
  const exported = await subtle.exportKey('raw', publicKey);
  assert.deepStrictEqual(Buffer.from(exported), rawKeyData);
}

(async function() {
  const tests = [];
  for (const vector of testVectors) {
    for (const extractable of [true, false]) {
      tests.push(testImportSpki(vector, extractable));
      tests.push(testImportPkcs8(vector, extractable));
      tests.push(testImportJwk(vector, extractable));
    }
    tests.push(testImportRaw(vector));
  }
  await Promise.all(tests);
})().then(common.mustCall());

{
  const rsaPublic = crypto.createPublicKey(
    fixtures.readKey('rsa_public_2048.pem'));
  const rsaPrivate = crypto.createPrivateKey(
    fixtures.readKey('rsa_private_2048.pem'));

  for (const [name, publicUsages, privateUsages] of [
    ['Ed25519', ['verify'], ['sign']],
    ['X25519', [], ['deriveBits']],
  ]) {
    assert.rejects(subtle.importKey(
      'spki',
      rsaPublic.export({ format: 'der', type: 'spki' }),
      { name },
      true, publicUsages), { message: /Invalid key type/ }).then(common.mustCall());
    assert.rejects(subtle.importKey(
      'pkcs8',
      rsaPrivate.export({ format: 'der', type: 'pkcs8' }),
      { name },
      true, privateUsages), { message: /Invalid key type/ }).then(common.mustCall());
  }
}
