'use strict';

const common = require('../common');
if (!common.hasCrypto)
  common.skip('missing crypto');

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

const assert = require('assert');
const {
  createSecretKey,
  KeyObject,
  randomBytes,
  generateKeyPairSync,
} = require('crypto');

function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) {
  assert.strictEqual(cryptoKey instanceof CryptoKey, true);
  assert.strictEqual(cryptoKey.type, keyObject.type);
  assert.strictEqual(cryptoKey.algorithm.name, algorithm);
  assert.strictEqual(cryptoKey.extractable, extractable);
  assert.deepStrictEqual(cryptoKey.usages, usages);
  assert.strictEqual(keyObject.equals(KeyObject.from(cryptoKey)), true);
}

{
  for (const length of [128, 192, 256]) {
    const key = createSecretKey(randomBytes(length >> 3));
    const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW'];
    if (length === 256)
      algorithms.push('ChaCha20-Poly1305');

    for (const algorithm of algorithms) {
      const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt'];
      for (const extractable of [true, false]) {
        const cryptoKey = key.toCryptoKey(algorithm, extractable, usages);
        assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
        assert.strictEqual(cryptoKey.algorithm.length, algorithm !== 'ChaCha20-Poly1305' ? length : undefined);
      }
    }
  }
}

{
  const pbkdf2 = createSecretKey(randomBytes(16));
  const algorithm = 'PBKDF2';
  const usages = ['deriveBits'];
  assert.throws(() => pbkdf2.toCryptoKey(algorithm, true, usages), {
    name: 'SyntaxError',
    message: 'PBKDF2 keys are not extractable'
  });
  assert.throws(() => pbkdf2.toCryptoKey(algorithm, false, ['wrapKey']), {
    name: 'SyntaxError',
    message: 'Unsupported key usage for a PBKDF2 key'
  });
  const cryptoKey = pbkdf2.toCryptoKey(algorithm, false, usages);
  assertCryptoKey(cryptoKey, pbkdf2, algorithm, false, usages);
  assert.strictEqual(cryptoKey.algorithm.length, undefined);
}

{
  for (const length of [128, 192, 256]) {
    const hmac = createSecretKey(randomBytes(length >> 3));
    const algorithm = 'HMAC';
    const usages = ['sign', 'verify'];

    assert.throws(() => {
      createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm, hash: 'SHA-256' }, true, usages);
    }, {
      name: 'DataError',
      message: 'Zero-length key is not supported',
    });

    assert.throws(() => {
      hmac.toCryptoKey({
        name: algorithm,
        hash: 'SHA-256',
      }, true, []);
    }, {
      name: 'SyntaxError',
      message: 'Usages cannot be empty when importing a secret key.'
    });

    for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
      for (const extractable of [true, false]) {
        assert.throws(() => {
          hmac.toCryptoKey({ name: algorithm, hash: 'SHA-256', length: 0 }, true, usages);
        }, {
          name: 'DataError',
          message: 'HmacImportParams.length cannot be 0',
        });
        const cryptoKey = hmac.toCryptoKey({ name: algorithm, hash }, extractable, usages);
        assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages);
        assert.strictEqual(cryptoKey.algorithm.length, length);
      }
    }
  }
}

{
  const algorithms = ['Ed25519', 'X25519'];

  if (!process.features.openssl_is_boringssl) {
    algorithms.push('X448', 'Ed448');
  } else {
    common.printSkipMessage('Skipping unsupported Ed448/X448 test cases');
  }

  for (const algorithm of algorithms) {
    const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase());
    assert.throws(() => {
      publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []);
    }, {
      name: 'DataError',
      message: 'Invalid key type'
    });
    for (const key of [publicKey, privateKey]) {
      let usages;
      if (algorithm.startsWith('E')) {
        usages = key.type === 'public' ? ['verify'] : ['sign'];
      } else {
        usages = key.type === 'public' ? [] : ['deriveBits'];
      }
      for (const extractable of [true, false]) {
        const cryptoKey = key.toCryptoKey(algorithm, extractable, usages);
        assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
      }
    }
  }
}

{
  const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
  for (const key of [publicKey, privateKey]) {
    for (const algorithm of ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP']) {
      let usages;
      if (algorithm === 'RSA-OAEP') {
        usages = key.type === 'public' ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
      } else {
        usages = key.type === 'public' ? ['verify'] : ['sign'];
      }
      for (const extractable of [true, false]) {
        for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
          const cryptoKey = key.toCryptoKey({
            name: algorithm,
            hash
          }, extractable, usages);
          assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
          assert.strictEqual(cryptoKey.algorithm.hash.name, hash);
        }
      }
    }
  }
}

{
  for (const namedCurve of ['P-256', 'P-384', 'P-521']) {
    const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve });
    assert.throws(() => {
      privateKey.toCryptoKey({
        name: 'ECDH',
        namedCurve,
      }, true, []);
    }, {
      name: 'SyntaxError',
      message: 'Usages cannot be empty when importing a private key.'
    });
    assert.throws(() => {
      publicKey.toCryptoKey({
        name: 'ECDH',
        namedCurve: namedCurve === 'P-256' ? 'P-384' : 'P-256'
      }, true, []);
    }, {
      name: 'DataError',
      message: 'Named curve mismatch'
    });
    for (const key of [publicKey, privateKey]) {
      for (const algorithm of ['ECDH', 'ECDSA']) {
        let usages;
        if (algorithm === 'ECDH') {
          usages = key.type === 'public' ? [] : ['deriveBits'];
        } else {
          usages = key.type === 'public' ? ['verify'] : ['sign'];
        }
        for (const extractable of [true, false]) {
          const cryptoKey = key.toCryptoKey({
            name: algorithm,
            namedCurve
          }, extractable, usages);
          assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
          assert.strictEqual(cryptoKey.algorithm.namedCurve, namedCurve);
        }
      }
    }
  }
}

if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) {
  for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) {
    const { publicKey, privateKey } = generateKeyPairSync(name.toLowerCase());
    assert.throws(() => {
      privateKey.toCryptoKey(name, true, []);
    }, {
      name: 'SyntaxError',
      message: 'Usages cannot be empty when importing a private key.'
    });
    for (const key of [publicKey, privateKey]) {
      const usages = key.type === 'public' ? ['verify'] : ['sign'];
      for (const extractable of [true, false]) {
        const cryptoKey = key.toCryptoKey({ name }, extractable, usages);
        assertCryptoKey(cryptoKey, key, name, extractable, usages);
        assert.strictEqual(cryptoKey.algorithm.name, name);
      }
    }
  }
}

if (hasOpenSSL(3)) {
  for (const algorithm of ['KMAC128', 'KMAC256']) {
    const hmac = createSecretKey(randomBytes(32));
    const usages = ['sign', 'verify'];

    assert.throws(() => {
      createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm }, true, usages);
    }, {
      name: 'DataError',
      message: 'Zero-length key is not supported',
    });

    assert.throws(() => {
      hmac.toCryptoKey({
        name: algorithm,
      }, true, []);
    }, {
      name: 'SyntaxError',
      message: 'Usages cannot be empty when importing a secret key.'
    });

    for (const extractable of [true, false]) {
      assert.throws(() => {
        hmac.toCryptoKey({ name: algorithm, length: 0 }, true, usages);
      }, {
        name: 'DataError',
        message: 'KmacImportParams.length cannot be 0',
      });
      const cryptoKey = hmac.toCryptoKey({ name: algorithm }, extractable, usages);
      assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages);
      assert.strictEqual(cryptoKey.algorithm.length, 256);
    }
  }
}
