/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
/* jshint node:true */
'use strict';

var Policy = require('scytl-cryptopolicy');
var validator = require('./input-validator');
var codec = require('scytl-codec');
var forge = require('node-forge');

module.exports = SignatureVerifier;

/**
 * @class SignatureVerifier
 * @classdesc The digital signature verifier API. To instantiate this object,
 *            use the method
 *            {@link AsymmetricCryptographyService.newSignatureVerifier}.
 * @hideconstructor
 * @param {Policy}
 *            policy The cryptographic policy to use.
 * @param {SecureRandomService}
 *            secureRandomService The secure random service to use.
 */
function SignatureVerifier(policy, secureRandomService) {
  // PRIVATE ///////////////////////////////////////////////////////////////////

  var digester_;
  var padding_;
  var forgePublicKey_;
  var updated_;

  function initForgeSigner() {
    if (policy.asymmetric.signer.algorithm !==
        Policy.options.asymmetric.signer.algorithm.RSA) {
      throw new Error(
          'Signature verifier algorithm \'' +
          policy.asymmetric.signer.algorithm + '\' is not supported.');
    }

    if (policy.asymmetric.signer.hashAlgorithm ===
        Policy.options.asymmetric.signer.hashAlgorithm.SHA256) {
      digester_ = forge.md.sha256.create();
    } else if (
        policy.asymmetric.signer.hashAlgorithm ===
        Policy.options.asymmetric.signer.hashAlgorithm.SHA512_224) {
      digester_ = forge.md.sha512.sha224.create();
    } else {
      throw new Error(
          'Signature verifier hash algorithm \'' +
          policy.asymmetric.signer.hashAlgorithm + '\' is not supported.');
    }
    digester_.start();

    if (typeof policy.asymmetric.signer.padding !== 'undefined') {
      if (policy.asymmetric.signer.padding.name ===
          Policy.options.asymmetric.signer.padding.PSS.name) {
        var paddingMd;
        if (policy.asymmetric.signer.padding.hashAlgorithm ===
            Policy.options.asymmetric.signer.padding.PSS.hashAlgorithm.SHA256) {
          paddingMd = forge.md.sha256.create();
        } else if (
            policy.asymmetric.signer.padding.hashAlgorithm ===
            Policy.options.asymmetric.signer.padding.PSS.hashAlgorithm
                .SHA512_224) {
          paddingMd = forge.md.sha512.sha224.create();
        } else {
          throw new Error(
              'Signature verifier PSS padding hash algorithm \'' +
              policy.asymmetric.signer.padding.hashAlgorithm +
              '\' is not supported.');
        }

        var paddingMgf;
        if (policy.asymmetric.signer.padding.maskGenerator.name ===
            Policy.options.asymmetric.signer.padding.PSS.maskGenerator.MGF1
                .name) {
          if (policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm ===
              Policy.options.asymmetric.signer.padding.PSS.maskGenerator.MGF1
                  .hashAlgorithm.SHA256) {
            paddingMgf = forge.mgf.mgf1.create(forge.md.sha256.create());
          } else if (
              policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm ===
              Policy.options.asymmetric.signer.padding.PSS.maskGenerator.MGF1
                  .hashAlgorithm.SHA512_224) {
            paddingMgf = forge.mgf.mgf1.create(forge.md.sha512.sha224.create());
          } else {
            throw new Error(
                'Signature verifier PSS padding mask generation function hash algorithm \'' +
                policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm +
                '\' is not supported.');
          }
        } else {
          throw new Error(
              'Signature verifier PSS padding mask generation function \'' +
              policy.asymmetric.signer.padding.maskGenerator.name +
              '\' is not supported.');
        }

        padding_ = forge.pss.create({
          md: paddingMd,
          mgf: paddingMgf,
          saltLength: policy.asymmetric.signer.padding.saltLengthBytes,
          prng: secureRandomService.newRandomGenerator()
        });
      } else {
        throw new Error(
            'Signature verifier padding \'' +
            policy.asymmetric.signer.padding.name + '\' is not supported.');
      }
    }

    updated_ = false;
  }

  // CONSTRUCTOR ///////////////////////////////////////////////////////////////

  initForgeSigner();

  // PUBLIC ////////////////////////////////////////////////////////////////////

  /**
   * Initializes the signature verifier with the provided public key.
   *
   * @function init
   * @memberof SignatureVerifier
   * @param {string}
   *            publicKey The public key with which to initialize the
   *            signature verifier, in PEM format.
   * @returns {SignatureVerifier} A reference to this object, to facilitate
   *          method chaining.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.init = function(publicKey) {
    validator.checkIsNonEmptyString(
        publicKey,
        'Public key (PEM encoded) with which to initialize signature verifier');

    forgePublicKey_ = forge.pki.publicKeyFromPem(publicKey);
    if (typeof forgePublicKey_.verify === 'undefined') {
      throw new Error(
          'PEM encoding of public key with which to initialize signature verifier is corrupt');
    }

    return this;
  };

  /**
   * Verifies the digital signature of the provided data. If there were any
   * prior calls to the method <code>update</code>, then the provided data
   * will be bitwise appended to the data provided to those calls. If no data
   * is provided here the signature will only be verified for the data
   * provided to prior calls to the method <code>update</code>. The
   * signature verifier will be automatically reinitialized after this method
   * completes. Before using this method, the signature verifier must have
   * been initialized with a public key, via the method
   * {@link SignatureVerifier.init}.
   *
   * @function verify
   * @memberof SignatureVerifier
   * @param {Uint8Array}
   *            signature The digital signature to verify.
   * @param {Uint8Array|string}
   *            [data] Some data that was digitally signed. <b>NOTE:</b> Data
   *            of type <code>string</code> will be UTF-8 encoded.
   * @returns {boolean} <code>true</code> if the signature was verified,
   *          <code>false</code> otherwise.
   * @throws {Error}
   *             If the input data validation fails, the signature verifier
   *             was not initialized, the signature verifier was not updated
   *             with any data or the signature verification process fails.
   */
  this.verify = function(signature, data) {
    if (typeof forgePublicKey_ === 'undefined') {
      throw new Error(
          'Could not verify signature; Signature verifier has not been initialized with any public key');
    }

    validator.checkIsInstanceOf(
        signature, Uint8Array, 'Uint8Array', 'Digital signature to verify');

    if (typeof data !== 'undefined') {
      if (typeof data === 'string') {
        data = codec.utf8Encode(data);
      }
      validator.checkIsInstanceOf(
          data, Uint8Array, 'Uint8Array',
          'Data provided to signature verifier');
      this.update(data);
    } else if (!updated_) {
      throw new Error(
          'Attempt to verify a signature without either providing data as input or having made a previous call to method \'update\'');
    }

    var publicExponentFound = forgePublicKey_.e.toString();
    var publicExponentExpected =
        policy.asymmetric.signer.publicExponent.toString();
    if (publicExponentFound !== publicExponentExpected) {
      throw new Error(
          'Expected public key for verifying signature to have same public exponent as crytographic policy: ' +
          publicExponentExpected + ' ; Found ' + publicExponentFound);
    }

    try {
      var verified = forgePublicKey_.verify(
          digester_.digest().getBytes(), codec.binaryEncode(signature),
          padding_);

      digester_.start();
      updated_ = false;

      return verified;
    } catch (error) {
      throw new Error('Signature could not be verified; ' + error);
    }
  };

  /**
   * Updates the signature verifier with the provided data. The data will be
   * internally bitwise concatenated to any data provided during previous
   * calls to this method, after the last call to the method
   * <code>verify</code>. Before using this method, the signature verifier
   * must have been initialized with a public key, via the method
   * {@link SignatureVerifier.init}.
   *
   * @function update
   * @memberof SignatureVerifier
   * @param {Uint8Array|string}
   *            data The data with which to update the signature verifier.
   *            <b>NOTE:</b> Data of type <code>string</code> will be UTF-8
   *            encoded.
   * @returns {SignatureVerifier} A reference to this object, to facilitate
   *          method chaining.
   * @throws {Error}
   *             If the input data validation fails or the update process
   *             fails.
   */
  this.update = function(data) {
    if (typeof forgePublicKey_ === 'undefined') {
      throw new Error(
          'Could not update; Signature verifier has not been initialized with any public key');
    }

    if (typeof data === 'string') {
      data = codec.utf8Encode(data);
    }
    validator.checkIsInstanceOf(
        data, Uint8Array, 'Uint8Array',
        'Data with which to update signature verifier');

    try {
      digester_.update(codec.binaryEncode(data));
      updated_ = true;
    } catch (error) {
      throw new Error(
          'Signature verifier could not be updated: ' + error.message);
    }

    return this;
  };
}
