'use strict';

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

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

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

const rsa_pkcs = require('../fixtures/crypto/rsa_pkcs');
const rsa_pss = require('../fixtures/crypto/rsa_pss');

async function testVerify({
  algorithm,
  hash,
  publicKeyBuffer,
  privateKeyBuffer,
  signature,
  plaintext,
}) {
  const [
    publicKey,
    noVerifyPublicKey,
    privateKey,
    hmacKey,
    ecdsaKeys,
  ] = await Promise.all([
    subtle.importKey(
      'spki',
      publicKeyBuffer,
      { name: algorithm.name, hash },
      false,
      ['verify']),
    subtle.importKey(
      'spki',
      publicKeyBuffer,
      { name: algorithm.name, hash },
      false,
      [ /* No usages */ ]),
    subtle.importKey(
      'pkcs8',
      privateKeyBuffer,
      { name: algorithm.name, hash },
      false,
      ['sign']),
    subtle.generateKey(
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']),
    subtle.generateKey(
      {
        name: 'ECDSA',
        namedCurve: 'P-521',
        hash: 'SHA-256',
      },
      false,
      ['sign']),
  ]);

  assert(await subtle.verify(algorithm, publicKey, signature, plaintext));

  // Test verification with altered buffers
  const copy = Buffer.from(plaintext);
  const sigcopy = Buffer.from(signature);
  const p = subtle.verify(algorithm, publicKey, sigcopy, copy);
  copy[0] = 255 - copy[0];
  sigcopy[0] = 255 - sigcopy[0];
  assert(await p);

  // Test failure when using wrong key
  await assert.rejects(
    subtle.verify(algorithm, privateKey, signature, plaintext), {
      message: /Unable to use this key to verify/
    });

  await assert.rejects(
    subtle.verify(algorithm, noVerifyPublicKey, signature, plaintext), {
      message: /Unable to use this key to verify/
    });

  // Test failure when using the wrong algorithms
  await assert.rejects(
    subtle.verify(algorithm, hmacKey, signature, plaintext), {
      message: /Key algorithm mismatch/
    });

  await assert.rejects(
    subtle.verify(algorithm, ecdsaKeys.publicKey, signature, plaintext), {
      message: /Key algorithm mismatch/
    });

  // Test failure when signature is altered
  {
    const copy = Buffer.from(signature);
    copy[0] = 255 - copy[0];
    assert(!(await subtle.verify(algorithm, publicKey, copy, plaintext)));
    assert(!(await subtle.verify(
      algorithm,
      publicKey,
      copy.slice(1),
      plaintext)));
  }

  // Test failure when data is altered
  {
    const copy = Buffer.from(plaintext);
    copy[0] = 255 - copy[0];
    assert(!(await subtle.verify(algorithm, publicKey, signature, copy)));
  }

  // Test failure when wrong hash is used
  {
    const otherhash = hash === 'SHA-1' ? 'SHA-256' : 'SHA-1';
    const keyWithOtherHash = await subtle.importKey(
      'spki',
      publicKeyBuffer,
      { name: algorithm.name, hash: otherhash },
      false,
      ['verify']);
    assert(!(await subtle.verify(algorithm, keyWithOtherHash, signature, plaintext)));
  }
}

async function testSign({
  algorithm,
  hash,
  publicKeyBuffer,
  privateKeyBuffer,
  signature,
  plaintext,
}) {
  const [
    publicKey,
    privateKey,
    hmacKey,
    ecdsaKeys,
  ] = await Promise.all([
    subtle.importKey(
      'spki',
      publicKeyBuffer,
      { name: algorithm.name, hash },
      false,
      ['verify']),
    subtle.importKey(
      'pkcs8',
      privateKeyBuffer,
      { name: algorithm.name, hash },
      false,
      ['sign']),
    subtle.generateKey(
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']),
    subtle.generateKey(
      {
        name: 'ECDSA',
        namedCurve: 'P-521',
        hash: 'SHA-256',
      },
      false,
      ['sign']),
  ]);

  {
    const sig = await subtle.sign(algorithm, privateKey, plaintext);
    assert.strictEqual(sig.byteLength, signature.byteLength);
    assert(await subtle.verify(algorithm, publicKey, sig, plaintext));
  }

  {
    const copy = Buffer.from(plaintext);
    const p = subtle.sign(algorithm, privateKey, copy);
    copy[0] = 255 - copy[0];
    const sig = await p;
    assert(await subtle.verify(algorithm, publicKey, sig, plaintext));
  }

  // Test failure when using wrong key
  await assert.rejects(
    subtle.sign(algorithm, publicKey, plaintext), {
      message: /Unable to use this key to sign/
    });

  // Test failure when using the wrong algorithms
  await assert.rejects(
    subtle.sign(algorithm, hmacKey, plaintext), {
      message: /Key algorithm mismatch/
    });

  await assert.rejects(
    subtle.sign(algorithm, ecdsaKeys.privateKey, plaintext), {
      message: /Key algorithm mismatch/
    });
}

async function testSaltLength(keyLength, hash, hLen) {
  const { publicKey, privateKey } = await subtle.generateKey({
    name: 'RSA-PSS',
    modulusLength: keyLength,
    publicExponent: new Uint8Array([1, 0, 1]),
    hash,
  }, false, ['sign', 'verify']);

  const data = Buffer.from('Hello, world!');
  const max = keyLength / 8 - hLen - 2;

  const signature = await subtle.sign({ name: 'RSA-PSS', saltLength: max }, privateKey, data);
  await assert.rejects(
    subtle.sign({ name: 'RSA-PSS', saltLength: max + 1 }, privateKey, data), (err) => {
      assert.strictEqual(err.name, 'OperationError');
      assert.strictEqual(err.cause?.code, 'ERR_OUT_OF_RANGE');
      assert.strictEqual(err.cause?.message, `The value of "algorithm.saltLength" is out of range. It must be >= 0 && <= ${max}. Received ${max + 1}`);
      return true;
    });
  await subtle.verify({ name: 'RSA-PSS', saltLength: max }, publicKey, signature, data);
  await assert.rejects(
    subtle.verify({ name: 'RSA-PSS', saltLength: max + 1 }, publicKey, signature, data), (err) => {
      assert.strictEqual(err.name, 'OperationError');
      assert.strictEqual(err.cause?.code, 'ERR_OUT_OF_RANGE');
      assert.strictEqual(err.cause?.message, `The value of "algorithm.saltLength" is out of range. It must be >= 0 && <= ${max}. Received ${max + 1}`);
      return true;
    });
}

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

  rsa_pkcs().forEach((vector) => {
    variations.push(testVerify(vector));
    variations.push(testSign(vector));
  });
  rsa_pss().forEach((vector) => {
    variations.push(testVerify(vector));
    variations.push(testSign(vector));
  });

  for (const keyLength of [1024, 2048]) {
    for (const [hash, hLen] of [
      ['SHA-1', 20],
      ['SHA-256', 32],
      ['SHA-384', 48],
      ['SHA-512', 64],
      ...(!process.features.openssl_is_boringssl ? [
        ['SHA3-256', 32],
        ['SHA3-384', 48],
        ['SHA3-512', 64],
      ] : []),
    ]) {
      variations.push(testSaltLength(keyLength, hash, hLen));
    }
  }

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