/*
 * 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 = Signer;

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

  var digester_;
  var padding_;
  var forgePrivateKey_;
  var updated_;

  function initForgeSigner() {
    if (policy.asymmetric.signer.algorithm !==
        Policy.options.asymmetric.signer.algorithm.RSA) {
      throw new Error(
          'Signer 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(
          'Signer 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(
              'Signer 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(
                'Signer PSS padding mask generation function hash algorithm \'' +
                policy.asymmetric.signer.padding.maskGenerator.hashAlgorithm +
                '\' is not supported.');
          }
        } else {
          throw new Error(
              'Signer 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(
            'Signer padding \'' + policy.asymmetric.signer.padding.name +
            '\' is not supported.');
      }
    }

    updated_ = false;
  }

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

  initForgeSigner();

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

  /**
   * Initializes the digital signer with the provided private key.
   *
   * @function init
   * @memberof Signer
   * @param {string}
   *            privateKeyPem The private key with which to initialize the
   *            digital signer, in PEM format.
   * @returns {Signer} A reference to this object, to facilitate method
   *          chaining.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.init = function(privateKeyPem) {
    validator.checkIsNonEmptyString(
        privateKeyPem,
        'Private key (PEM encoded) with which to initialize digital signer');

    forgePrivateKey_ = forge.pki.privateKeyFromPem(privateKeyPem);
    if (typeof forgePrivateKey_.sign === 'undefined') {
      throw new Error(
          'PEM encoding of private key with which to initialize digital signer is corrupt');
    }

    return this;
  };

  /**
   * Digitally signs 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 generated for the data provided to prior calls
   * to the method <code>update</code>. The signer will be automatically
   * reinitialized after this method completes. Before
   * using this method, the signer must have been initialized with a private
   * key, via the method {@link Signer.init}.
   *
   * @function sign
   * @memberof Signer
   * @param {Uint8Array|string}
   *            [data] Some data to sign. <b>NOTE:</b> Data of type
   *            <code>string</code> will be UTF-8 encoded.
   * @returns {Uint8Array} The digital signature.
   * @throws {Error}
   *             If the input data validation fails, the signer was not
   *             initialized, the signer was not updated with any data or the
   *             digital signing process fails.
   */
  this.sign = function(data) {
    if (typeof forgePrivateKey_ === 'undefined') {
      throw new Error(
          'Could not sign; Digital signer has not been initialized with any private key');
    }

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

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

    try {
      var signature = forgePrivateKey_.sign(digester_, padding_);

      digester_.start();
      updated_ = false;

      return codec.binaryDecode(signature);
    } catch (error) {
      throw new Error('Signature could not be generated; ' + error);
    }
  };

  /**
   * Updates the signer 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>sign</code>. Before
   * using this method, the signer must have been initialized with a private
   * key, via the method {@link Signer.init}.
   *
   * @function update
   * @memberof Signer
   * @param {Uint8Array|string}
   *            data The data with which to update the signer. <b>NOTE:</b>
   *            Data of type <code>string</code> will be UTF-8 encoded.
   * @returns {Signer} 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 forgePrivateKey_ === 'undefined') {
      throw new Error(
          'Could not update; Digital signer has not been initialized with any private key');
    }

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

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

    return this;
  };
}
