/*
 * 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
 */
cryptolib.modules.asymmetric = cryptolib.modules.asymmetric || {};
/** @namespace asymmetric/signer */
cryptolib.modules.asymmetric.signer = function(box) {
  'use strict';

  box.asymmetric = box.asymmetric || {};
  box.asymmetric.signer = {};

  /**
   * A module that holds asymmetric signature functionalities.
   *
   * @exports asymmetric/signer/factory
   */
  box.asymmetric.signer.factory = {};

  var policies = {
    algorithm: box.policies.asymmetric.signer.algorithm,
    provider: box.policies.asymmetric.signer.provider,
    hash: box.policies.asymmetric.signer.hash,
    padding: box.policies.asymmetric.signer.padding,
    publicExponent: box.policies.asymmetric.signer.publicExponent
  };

  var converters, exceptions, randomFactory;

  var f = function(box) {
    converters = new box.commons.utils.Converters();
    exceptions = box.commons.exceptions;
    randomFactory =
        new box.primitives.securerandom.factory.SecureRandomFactory();
  };

  f.policies = {
    primitives: {
      secureRandom:
          {provider: box.policies.asymmetric.signer.secureRandom.provider}
    }
  };

  cryptolib('commons', 'primitives.securerandom', f);

  /**
   * A factory class for creating a digital signer.
   *
   * @class
   */
  box.asymmetric.signer.factory.SignerFactory = function() {};

  box.asymmetric.signer.factory.SignerFactory.prototype = {
    getCryptoSigner: function() {
      try {
        if (policies.provider === Config.asymmetric.signer.provider.FORGE) {
          var secureRandom = randomFactory.getCryptoRandomBytes();

          return this.getCryptoForgeSigner(secureRandom);
        } else {
          throw new exceptions.CryptoLibException(
              'No suitable provider for the signer was provided.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'A CryptoSigner could not be obtained.', error);
      }
    },
    /**
     * @function
     * @return {asymmetric/signer.CryptoForgeSigner}
     */
    getCryptoForgeSigner: function(secureRandom) {
      return new CryptoForgeSigner(secureRandom);
    }
  };

  /**
   * A digital signer.
   *
   * @class
   * @memberof asymmetric/signer
   * @param secureRandom
   */
  function CryptoForgeSigner(secureRandom) {
    this.digester = null;
    this.secureRandom = secureRandom;

    try {
      if (policies.algorithm !== Config.asymmetric.signer.algorithm.RSA) {
        throw new exceptions.CryptoLibException(
            'The given algorithm is not supported');
      }

      if (policies.hash === Config.asymmetric.signer.hash.SHA256) {
        this.digester = forge.md.sha256.create();
      } else {
        var errorMessage = 'Message digester type \'' + policies.hash +
            '\' not recognized by signer.';
        throw new exceptions.CryptoLibException(errorMessage);
      }

      if (typeof(policies.padding) !== 'undefined' && 
    	  policies.padding.PSS.name === Config.asymmetric.signer.padding.PSS.name) {
        var paddingHash;
        if (policies.padding.PSS.hash ===
            Config.asymmetric.signer.padding.PSS.hash.SHA256) {
          paddingHash = forge.md.sha256.create();
        }

        var paddingMgf;
        if (policies.padding.PSS.mask.MGF1.name ===
            Config.asymmetric.signer.padding.PSS.mask.MGF1.name &&
            policies.padding.PSS.mask.MGF1.hash ===
            Config.asymmetric.signer.padding.PSS.mask.MGF1.hash.SHA256) {
          var mgfHash = forge.md.sha256.create();
          paddingMgf = forge.mgf.mgf1.create(mgfHash);
        }

        this.padding = forge.pss.create({
          md: paddingHash,
          mgf: paddingMgf,
          saltLength: policies.padding.PSS.saltLengthBytes,
          prng: this.secureRandom
        });
      }
    } catch (error) {
      throw new exceptions.CryptoLibException(
          'CryptoForgeSigner could not be created.', error);
    }
  }

  CryptoForgeSigner.prototype = {
    /**
     * Digitally signs some data.
     *
     * @function
     * @param privateKeyPem
     *            Private key, as string in PEM format.
     * @param arrayDataBase64
     *            Data to sign, as an array of string in Base64 encoded
     *            format.
     * @returns signature, in Base64 encoded format.
     */
    sign: function(privateKeyPem, arrayDataBase64) {
      try {
        var privateKey = forge.pki.privateKeyFromPem(privateKeyPem);

        if (privateKey.e.toString() !== policies.publicExponent.toString()) {
          throw new exceptions.CryptoLibException(
              'Private key has not been generated with the required exponent : ' +
              policies.publicExponent);
        }

        if (!arrayDataBase64 || arrayDataBase64.length < 1) {
          throw new exceptions.CryptoLibException(
              'The array of data in Base64 should contain at least one element.');
        }

        this.digester.start();
        var dataB64;
        var data;
        for (var i = 0; i < arrayDataBase64.length; i++) {
          dataB64 = arrayDataBase64[i];

          if (dataB64 === undefined || dataB64 === null) {
            throw new exceptions.CryptoLibException(
                'The input data contained an element that was not defined or empty.');
          }

          data = converters.base64Decode(dataB64);
          this.digester.update(data);
        }

        var signature = privateKey.sign(this.digester, this.padding);

        return converters.base64Encode(signature);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Signature could not be generated.', error);
      }
    },

    /**
     * Verifies the digital signature of some data.
     *
     * @function
     *
     * @param signatureB64
     *            Signature, as string in Base64 encoded format.
     * @param publicKeyPem
     *            Public key, as string in PEM format.
     * @param arrayDataBase64
     *            Data that was signed, as an array of string in Base64
     *            encoded format.
     *
     * @returns boolean indicating whether signature verification was
     *          successful.
     */
    verify: function(signatureB64, publicKeyPem, arrayDataBase64) {
      try {
        var publicKey = forge.pki.publicKeyFromPem(publicKeyPem);

        if (publicKey.e.toString() !== policies.publicExponent.toString()) {
          throw new exceptions.CryptoLibException(
              'Public key has not been generated with the required exponent : ' +
              policies.publicExponent);
        }

        if (!arrayDataBase64 || arrayDataBase64.length < 1) {
          throw new exceptions.CryptoLibException(
              'The array of data in Base64 should contain at least one element.');
        }

        this.digester.start();
        var dataB64;

        for (var i = 0; i < arrayDataBase64.length; i++) {
          dataB64 = arrayDataBase64[i];

          if (dataB64 === undefined || dataB64 === null) {
            throw new exceptions.CryptoLibException(
                'The input data contained an element that was not defined or empty.');
          }

          var data = converters.base64Decode(dataB64);
          this.digester.update(data);
        }

        var signature = converters.base64Decode(signatureB64);

        return publicKey.verify(
            this.digester.digest().getBytes(), signature, this.padding);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Signature could not be verified.', error);
      }
    }
  };
};
