'use strict';

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

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

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

async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) {
  // Using a copy of plaintext to prevent tampering of the original
  plaintext = Buffer.from(plaintext);

  const key = await subtle.importKey(
    'raw',
    keyBuffer,
    { name: algorithm.name },
    false,
    ['encrypt', 'decrypt']);

  const output = await subtle.encrypt(algorithm, key, plaintext);
  plaintext[0] = 255 - plaintext[0];

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

  // Converting the returned ArrayBuffer into a Buffer right away,
  // so that the next line works
  const check = Buffer.from(await subtle.decrypt(algorithm, key, output));
  check[0] = 255 - check[0];

  assert.strictEqual(
    Buffer.from(check).toString('hex'),
    Buffer.from(plaintext).toString('hex'));
}

async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) {
  const key = await subtle.importKey(
    'raw',
    keyBuffer,
    { name: algorithm.name },
    false,
    ['decrypt']);

  return assert.rejects(subtle.encrypt(algorithm, key, plaintext), {
    message: /CryptoKey doesn't support encryption/
  });
}

async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) {
  const key = await subtle.importKey(
    'raw',
    keyBuffer,
    { name: algorithm.name },
    false,
    ['encrypt']);

  const output = await subtle.encrypt(algorithm, key, plaintext);

  return assert.rejects(subtle.decrypt(algorithm, key, output), {
    message: /CryptoKey doesn't support decryption/
  });
}

async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) {
  assert.notStrictEqual(algorithm.name, alg);
  const key = await subtle.importKey(
    'raw',
    keyBuffer,
    { name: alg },
    false,
    ['encrypt']);

  return assert.rejects(subtle.encrypt(algorithm, key, plaintext), {
    message: /CryptoKey doesn't match AlgorithmIdentifier/
  });
}

async function testDecrypt({ keyBuffer, algorithm, result }) {
  const key = await subtle.importKey(
    'raw',
    keyBuffer,
    { name: algorithm.name },
    false,
    ['encrypt', 'decrypt']);

  await subtle.decrypt(algorithm, key, result);
}

// Test aes-cbc vectors
{
  const {
    passing,
    failing,
    decryptionFailing
  } = require('../fixtures/crypto/aes_cbc')();

  (async function() {
    const variations = [];

    passing.forEach((vector) => {
      variations.push(testEncrypt(vector));
      variations.push(testEncryptNoEncrypt(vector));
      variations.push(testEncryptNoDecrypt(vector));
      variations.push(testEncryptWrongAlg(vector, 'AES-CTR'));
    });

    failing.forEach((vector) => {
      variations.push(assert.rejects(testEncrypt(vector), {
        message: /algorithm\.iv must contain exactly 16 bytes/
      }));
      variations.push(assert.rejects(testDecrypt(vector), {
        message: /algorithm\.iv must contain exactly 16 bytes/
      }));
    });

    decryptionFailing.forEach((vector) => {
      variations.push(assert.rejects(testDecrypt(vector), {
        name: 'OperationError'
      }));
    });

    await Promise.all(variations);
  })().then(common.mustCall());
}

// Test aes-ctr vectors
{
  const {
    passing,
    failing,
    decryptionFailing
  } = require('../fixtures/crypto/aes_ctr')();

  (async function() {
    const variations = [];

    passing.forEach((vector) => {
      variations.push(testEncrypt(vector));
      variations.push(testEncryptNoEncrypt(vector));
      variations.push(testEncryptNoDecrypt(vector));
      variations.push(testEncryptWrongAlg(vector, 'AES-CBC'));
    });

    // TODO(@jasnell): These fail for different reasons. Need to
    // make them consistent
    failing.forEach((vector) => {
      variations.push(assert.rejects(testEncrypt(vector), {
        message: /.*/
      }));
      variations.push(assert.rejects(testDecrypt(vector), {
        message: /.*/
      }));
    });

    decryptionFailing.forEach((vector) => {
      variations.push(assert.rejects(testDecrypt(vector), {
        name: 'OperationError'
      }));
    });

    await Promise.all(variations);
  })().then(common.mustCall());
}

// Test aes-gcm vectors
{
  const {
    passing,
    failing,
    decryptionFailing
  } = require('../fixtures/crypto/aes_gcm')();

  (async function() {
    const variations = [];

    passing.forEach((vector) => {
      variations.push(testEncrypt(vector));
      variations.push(testEncryptNoEncrypt(vector));
      variations.push(testEncryptNoDecrypt(vector));
      variations.push(testEncryptWrongAlg(vector, 'AES-CBC'));
    });

    failing.forEach((vector) => {
      variations.push(assert.rejects(testEncrypt(vector), {
        message: /is not a valid AES-GCM tag length/
      }));
      variations.push(assert.rejects(testDecrypt(vector), {
        message: /is not a valid AES-GCM tag length/
      }));
    });

    decryptionFailing.forEach((vector) => {
      variations.push(assert.rejects(testDecrypt(vector), {
        name: 'OperationError'
      }));
    });

    await Promise.all(variations);
  })().then(common.mustCall());
}

{
  (async function() {
    const secretKey = await subtle.generateKey(
      {
        name: 'AES-GCM',
        length: 256,
      },
      false,
      ['encrypt', 'decrypt'],
    );

    const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
    const aad = globalThis.crypto.getRandomValues(new Uint8Array(32));

    const encrypted = await subtle.encrypt(
      {
        name: 'AES-GCM',
        iv,
        additionalData: aad,
        tagLength: 128
      },
      secretKey,
      globalThis.crypto.getRandomValues(new Uint8Array(32))
    );

    await subtle.decrypt(
      {
        name: 'AES-GCM',
        iv,
        additionalData: aad,
        tagLength: 128,
      },
      secretKey,
      new Uint8Array(encrypted),
    );
  })().then(common.mustCall());
}
