'use strict';

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

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

const { hasOpenSSL } = require('../common/crypto');

const assert = require('assert');
const { subtle } = globalThis.crypto;
const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto');

{
  async function test() {
    const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32));
    await Promise.all([1, null, undefined, {}, []].map((format) =>
      assert.rejects(
        subtle.importKey(format, keyData, {}, false, ['wrapKey']), {
          code: 'ERR_INVALID_ARG_VALUE'
        })
    ));
    await assert.rejects(
      subtle.importKey('not valid', keyData, {}, false, ['wrapKey']), {
        code: 'ERR_INVALID_ARG_VALUE'
      });
    await assert.rejects(
      subtle.importKey('KeyObject', keyData, {}, false, ['wrapKey']), {
        message: /'KeyObject' is not a valid enum value of type KeyFormat/,
        code: 'ERR_INVALID_ARG_VALUE'
      });
    await assert.rejects(
      subtle.importKey('raw', 1, {}, false, ['deriveBits']), {
        code: 'ERR_INVALID_ARG_TYPE'
      });
    await assert.rejects(
      subtle.importKey('raw', keyData, {
        name: 'HMAC'
      }, false, ['sign', 'verify']), {
        code: 'ERR_MISSING_OPTION'
      });
    await assert.rejects(
      subtle.importKey('raw', keyData, {
        name: 'HMAC',
        hash: 'SHA-256',
        length: 384,
      }, false, ['sign', 'verify']), {
        name: 'DataError',
        message: 'Invalid key length'
      });
    await assert.rejects(
      subtle.importKey('raw', keyData, {
        name: 'HMAC',
        hash: 'SHA-256'
      }, false, ['deriveBits']), {
        name: 'SyntaxError',
        message: 'Unsupported key usage for HMAC key'
      });
    await assert.rejects(
      subtle.importKey('raw', keyData, {
        name: 'HMAC',
        hash: 'SHA-256',
        length: 0
      }, false, ['sign', 'verify']), {
        name: 'DataError',
        message: 'HmacImportParams.length cannot be 0'
      });
    await assert.rejects(
      subtle.importKey('raw', keyData, {
        name: 'HMAC',
        hash: 'SHA-256',
        length: 1
      }, false, ['sign', 'verify']), {
        name: 'NotSupportedError',
        message: 'Unsupported HmacImportParams.length'
      });
    await assert.rejects(
      subtle.importKey('jwk', null, {
        name: 'HMAC',
        hash: 'SHA-256',
      }, false, ['sign', 'verify']), {
        name: 'DataError',
        message: 'Invalid keyData'
      });
  }

  test().then(common.mustCall());
}

// Import/Export HMAC Secret Key
{
  async function test() {
    const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32));
    const key = await subtle.importKey(
      'raw',
      keyData, {
        name: 'HMAC',
        hash: 'SHA-256'
      }, true, ['sign', 'verify']);

    assert.strictEqual(key.algorithm, key.algorithm);
    assert.strictEqual(key.usages, key.usages);

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

    assert.deepStrictEqual(
      Buffer.from(keyData).toString('hex'),
      Buffer.from(raw).toString('hex'));

    const jwk = await subtle.exportKey('jwk', key);
    assert.deepStrictEqual(jwk.key_ops, ['sign', 'verify']);
    assert(jwk.ext);
    assert.strictEqual(jwk.kty, 'oct');
    assert.strictEqual(jwk.alg, 'HS256');

    assert.deepStrictEqual(
      Buffer.from(jwk.k, 'base64').toString('hex'),
      Buffer.from(raw).toString('hex'));

    await subtle.importKey(
      'jwk',
      jwk,
      {
        name: 'HMAC',
        hash: 'SHA-256'
      },
      true,
      ['sign', 'verify']);

    await subtle.importKey(
      'jwk',
      { ...jwk, alg: undefined },
      {
        name: 'HMAC',
        hash: 'SHA-256'
      },
      true,
      ['sign', 'verify']);

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, alg: 'HS384' },
        {
          name: 'HMAC',
          hash: 'SHA-256'
        },
        true,
        ['sign', 'verify']),
      { name: 'DataError', message: 'JWK "alg" does not match the requested algorithm' });

    await assert.rejects(
      subtle.importKey(
        'raw',
        keyData,
        {
          name: 'HMAC',
          hash: 'SHA-256'
        },
        true,
        [/* empty usages */]),
      { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' });
  }

  test().then(common.mustCall());
}

// Import/Export KMAC Secret Key
if (hasOpenSSL(3)) {
  async function test(name) {
    const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32));
    const key = await subtle.importKey(
      'raw-secret',
      keyData, name, true, ['sign', 'verify']);

    assert.strictEqual(key.algorithm, key.algorithm);
    assert.strictEqual(key.usages, key.usages);

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

    assert.deepStrictEqual(
      Buffer.from(keyData).toString('hex'),
      Buffer.from(raw).toString('hex'));

    const jwk = await subtle.exportKey('jwk', key);
    assert.deepStrictEqual(jwk.key_ops, ['sign', 'verify']);
    assert(jwk.ext);
    assert.strictEqual(jwk.kty, 'oct');
    assert.strictEqual(jwk.alg, `K${name.substring(4)}`);

    assert.deepStrictEqual(
      Buffer.from(jwk.k, 'base64').toString('hex'),
      Buffer.from(raw).toString('hex'));

    await subtle.importKey(
      'jwk',
      jwk,
      name,
      true,
      ['sign', 'verify']);

    await subtle.importKey(
      'jwk',
      { ...jwk, alg: undefined },
      name,
      true,
      ['sign', 'verify']);

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, alg: name === 'KMAC128' ? 'K256' : 'K128' },
        name,
        true,
        ['sign', 'verify']),
      { name: 'DataError', message: 'JWK "alg" does not match the requested algorithm' });

    await assert.rejects(
      subtle.importKey(
        'raw',
        keyData, name, true, ['sign', 'verify']),
      { name: 'NotSupportedError', message: `Unable to import ${name} using raw format` });

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

  test('KMAC128').then(common.mustCall());
  test('KMAC256').then(common.mustCall());
}

// Import/Export AES Secret Key
{
  async function test() {
    const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32));
    const key = await subtle.importKey(
      'raw',
      keyData, {
        name: 'AES-CTR',
        length: 256,
      }, true, ['encrypt', 'decrypt']);
    assert.strictEqual(key.algorithm, key.algorithm);
    assert.strictEqual(key.usages, key.usages);

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

    assert.deepStrictEqual(
      Buffer.from(keyData).toString('hex'),
      Buffer.from(raw).toString('hex'));

    const jwk = await subtle.exportKey('jwk', key);
    assert.deepStrictEqual(jwk.key_ops, ['encrypt', 'decrypt']);
    assert(jwk.ext);
    assert.strictEqual(jwk.kty, 'oct');

    assert.deepStrictEqual(
      Buffer.from(jwk.k, 'base64').toString('hex'),
      Buffer.from(raw).toString('hex'));

    await assert.rejects(
      subtle.importKey(
        'raw',
        keyData,
        {
          name: 'AES-CTR',
          length: 256,
        },
        true,
        [/* empty usages */]),
      { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' });
  }

  test().then(common.mustCall());
}

// Import/Export RSA Key Pairs
{
  async function test() {
    const { publicKey, privateKey } = await subtle.generateKey({
      name: 'RSA-PSS',
      modulusLength: 1024,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-384'
    }, true, ['sign', 'verify']);

    const [
      spki,
      pkcs8,
      publicJwk,
      privateJwk,
    ] = await Promise.all([
      subtle.exportKey('spki', publicKey),
      subtle.exportKey('pkcs8', privateKey),
      subtle.exportKey('jwk', publicKey),
      subtle.exportKey('jwk', privateKey),
    ]);

    assert(spki);
    assert(pkcs8);
    assert(publicJwk);
    assert(privateJwk);

    const [
      importedSpkiPublicKey,
      importedPkcs8PrivateKey,
      importedJwkPublicKey,
      importedJwkPrivateKey,
    ] = await Promise.all([
      subtle.importKey('spki', spki, {
        name: 'RSA-PSS',
        hash: 'SHA-384',
      }, true, ['verify']),
      subtle.importKey('pkcs8', pkcs8, {
        name: 'RSA-PSS',
        hash: 'SHA-384',
      }, true, ['sign']),
      subtle.importKey('jwk', publicJwk, {
        name: 'RSA-PSS',
        hash: 'SHA-384',
      }, true, ['verify']),
      subtle.importKey('jwk', privateJwk, {
        name: 'RSA-PSS',
        hash: 'SHA-384',
      }, true, ['sign']),
    ]);

    assert(importedSpkiPublicKey);
    assert(importedPkcs8PrivateKey);
    assert(importedJwkPublicKey);
    assert(importedJwkPrivateKey);
  }

  test().then(common.mustCall());
}

// Import/Export EC Key Pairs
{
  async function test() {
    const { publicKey, privateKey } = await subtle.generateKey({
      name: 'ECDSA',
      namedCurve: 'P-384'
    }, true, ['sign', 'verify']);

    const [
      spki,
      pkcs8,
      publicJwk,
      privateJwk,
    ] = await Promise.all([
      subtle.exportKey('spki', publicKey),
      subtle.exportKey('pkcs8', privateKey),
      subtle.exportKey('jwk', publicKey),
      subtle.exportKey('jwk', privateKey),
    ]);

    assert(spki);
    assert(pkcs8);
    assert(publicJwk);
    assert(privateJwk);

    const [
      importedSpkiPublicKey,
      importedPkcs8PrivateKey,
      importedJwkPublicKey,
      importedJwkPrivateKey,
    ] = await Promise.all([
      subtle.importKey('spki', spki, {
        name: 'ECDSA',
        namedCurve: 'P-384'
      }, true, ['verify']),
      subtle.importKey('pkcs8', pkcs8, {
        name: 'ECDSA',
        namedCurve: 'P-384'
      }, true, ['sign']),
      subtle.importKey('jwk', publicJwk, {
        name: 'ECDSA',
        namedCurve: 'P-384'
      }, true, ['verify']),
      subtle.importKey('jwk', privateJwk, {
        name: 'ECDSA',
        namedCurve: 'P-384'
      }, true, ['sign']),
    ]);

    assert(importedSpkiPublicKey);
    assert(importedPkcs8PrivateKey);
    assert(importedJwkPublicKey);
    assert(importedJwkPrivateKey);
  }

  test().then(common.mustCall());
}

// SHA-3 hashes and JWK "alg"
if (!process.features.openssl_is_boringssl) {
  const rsa = fixtures.readKey('rsa_private_2048.pem');
  const privateKey = createPrivateKey(rsa);
  const publicKey = createPublicKey(privateKey);

  async function test(keyObject, algorithm, usages) {
    const key = keyObject.toCryptoKey(algorithm, true, usages);
    const jwk = await subtle.exportKey('jwk', key);
    assert.strictEqual(jwk.alg, undefined);
  }

  for (const hash of ['SHA3-256', 'SHA3-384', 'SHA3-512']) {
    for (const name of ['RSA-OAEP', 'RSA-PSS', 'RSASSA-PKCS1-v1_5']) {
      test(publicKey, { name, hash }, []).then(common.mustCall());
      test(privateKey, { name, hash }, [name === 'RSA-OAEP' ? 'unwrapKey' : 'sign']).then(common.mustCall());
    }

    test(createSecretKey(Buffer.alloc(32)), { name: 'HMAC', hash }, ['sign']);
  }

  {
    const jwk = createSecretKey(Buffer.alloc(16)).export({ format: 'jwk' });
    // This is rejected for SHA-2 but ignored for SHA-3
    // Otherwise, if the name attribute of hash is defined in another applicable specification:
    // Perform any key import steps defined by other applicable specifications, passing format,
    // jwk and hash and obtaining hash.
    jwk.alg = 'HS3-256';

    assert.rejects(subtle.importKey('jwk', jwk, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']), {
      name: 'DataError',
      message: 'JWK "alg" does not match the requested algorithm',
    }).then(common.mustCall());

    subtle.importKey('jwk', jwk, { name: 'HMAC', hash: 'SHA3-256' }, false, ['sign', 'verify']).then(common.mustCall());
  }
}

{
  const keyData = new Uint8Array(32);
  assert.rejects(
    subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false),
    { code: 'ERR_MISSING_ARGS' }
  ).then(common.mustCall());
}
