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

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

  var algorithm_;
  var oaepDigester_;
  var oeapMaskDigester_;
  var kemDeriver_;
  var randomGenerator_;
  var forgePublicKey_;

  function initForgeCipher(algorithm) {
    if (algorithm.name ===
        Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.name) {
      oaepDigester_ = getRsaOaepHash(algorithm.hashAlgorithm);

      if (algorithm.maskGenerator.name ===
          Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.maskGenerator.MGF1
              .name) {
        oeapMaskDigester_ =
            getRsaOaepMaskHash(algorithm.maskGenerator.hashAlgorithm);
      } else {
        throw new Error(
            'Asymmetric encrypter RSA-OAEP mask generation function \'' +
            algorithm.maskGenerator.name + '\' is not supported.');
      }
    } else if (
        algorithm.name ===
        Policy.options.asymmetric.cipher.algorithm.RSA_KEM.name) {
      if (algorithm.symmetricCipher !==
          Policy.options.asymmetric.cipher.algorithm.RSA_KEM.symmetricCipher
              .AES_GCM) {
        throw new Error(
            'Asymmetric encrypter RSA-KEM symmetric cipher algorithm \'' +
            algorithm.symmetricCipher + '\' is not supported.');
      }

      kemDeriver_ = createDeriver(
          algorithm.keyDeriver.name, algorithm.keyDeriver.hashAlgorithm);
    } else {
      throw new Error(
          'Asymmetric encryption algorithm \'' + algorithm.name +
          '\' is not supported.');
    }
  }

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

  algorithm_ = policy.asymmetric.cipher.algorithm;
  randomGenerator_ = secureRandomService.newRandomGenerator();

  initForgeCipher(algorithm_);

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

  /**
   * Initializes the asymmetric encrypter with the provided public key.
   *
   * @function init
   * @memberof AsymmetricEncrypter
   * @param {string}
   *            publicKey The public key with which to initialize the
   *            asymmetric encrypter, in PEM format.
   * @returns {AsymmetricEncrypter} 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 asymmetric encrypter');

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

    return this;
  };

  /**
   * Asymmetrically encrypts the provided data. If the cipher algorithm is set
   * to <code>RSA-KEM</code> in the cryptographic policy being used, then
   * the output of this function will be in the form
   * <code>[Encapsulation][IV][Encrypted Data][Tag]</code>. Before using
   * this method, the encrypter must have been initialized with a public key,
   * via the method {@link AsymmetricEncrypter.init}.
   *
   * @function encrypt
   * @memberof AsymmetricEncrypter
   * @param {Uint8Array|string}
   *            data The data to encrypt. <b>NOTE:</b> Data of type
   *            <code>string</code> will be UTF-8 encoded.
   * @returns {Uint8Array} The encrypted data.
   * @throws {Error}
   *             If the input data validation fails, the encrypter was not
   *             initialized or the encryption process fails.
   */
  this.encrypt = function(data) {
    if (typeof forgePublicKey_ === 'undefined') {
      throw new Error(
          'Asymmetric encrypter has not been initialized with any public key');
    }

    if (typeof data === 'string') {
      data = codec.utf8Encode(data);
    }
    validator.checkIsInstanceOf(
        data, Uint8Array, 'Uint8Array', 'Data to asymmetrically encrypt');

    try {
      var output;

      // Choose the encryption algorithm from the policy.
      switch (algorithm_.name) {
        case Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.name:
          output = forgePublicKey_.encrypt(
                codec.binaryEncode(data), algorithm_.name, {
                  md: oaepDigester_,
                  mgf1: {md: oeapMaskDigester_},
                  seed: codec.binaryEncode(secureRandomService.nextSeed())
                });
          break;
        default:
          output = kemEncrypt(
              forgePublicKey_, codec.binaryEncode(data), algorithm_,
              kemDeriver_, randomGenerator_);
          break;
      }

      return codec.binaryDecode(output);
    } catch (error) {
      throw new Error('Data could not be encrypted; ' + error);
    }
  };
}

// Note: at the moment, the options that are being used with the RSA_OAEP
// algorithm are hardcoded (for encrypting and decrypting). This could be
// improved so that these are read from the properties file. Doing this will
// mean any that existing properties files (used by consumers of the
// library) will become invalid (if the consumer uses RSA_OAEP) as they wont
// have the mandatory new properties.
function getRsaOaepHash(hashAlgorithm) {
  if (hashAlgorithm ===
      Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.hashAlgorithm
          .SHA256) {
    return forge.md.sha256.create();
  } else {
    throw new Error(
        'RSA-OAEP cipher hash algorithm \'' + hashAlgorithm +
        '\' is not supported.');
  }
}

function getRsaOaepMaskHash(hashAlgorithm) {
  // For interoperability purposes, the MGF1 hash function must
  // remain as SHA-1.
  if (hashAlgorithm ===
      Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.maskGenerator.MGF1
          .hashAlgorithm.SHA1) {
    return forge.md.sha1.create();
  } else {
    throw new Error(
        'RSA-OAEP cipher mask generation function hash algorithm \'' +
        hashAlgorithm + '\' is not supported.');
  }
}

function createDigester(hashAlgorithm) {
  if (hashAlgorithm ===
      Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver
          .hashAlgorithm.SHA256) {
    return forge.md.sha256.create();
  } else if (
      hashAlgorithm ===
      Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver
          .hashAlgorithm.SHA512_224) {
    return forge.md.sha512.sha224.create();
  } else {
    throw new Error(
        'RSA-KEM cipher key derivation hash algorithm \'' + hashAlgorithm +
        '\' is not supported.');
  }
}

function createDeriver(deriverName, hashAlgorithm) {
  var hash = createDigester(hashAlgorithm);

  switch (deriverName) {
    case Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver.name
        .KDF1:
      return new forge.kem.kdf1(hash);
    case Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver.name
        .KDF2:
      return new forge.kem.kdf2(hash);
    case Policy.options.asymmetric.cipher.algorithm.RSA_KEM.keyDeriver.name
        .MGF1:
      return new forge.mgf.mgf1.create(hash);
    default:
      throw new Error(
          'RSA-KEM cipher key derivation function \'' + deriverName +
          '\' is not supported.');
  }
}

/**
 * Encrypts some data using the 'RSA-KEM' cipher algorithm.
 *
 * @function kemEncrypt
 * @memberof AsymmetricEncrypter
 * @private
 * @param {Object}
 *            publicKey The public key to use for encrypting
 * @param {Object}
 *            data The data to encrypt.
 * @param {Object}
 *            algorithm The cipher algorithm.
 * @param {Object}
 *            randomGenerator The random generator to use.
 * @returns {Object} The encrypted data.
 */
function kemEncrypt(publicKey, data, algorithm, deriver, randomGenerator) {
  // generate and encapsulate secret key
  var kem = forge.kem.rsa.create(deriver);
  var result = kem.encrypt(publicKey, algorithm.secretKeyLengthBytes);

  var iv =
      codec.binaryEncode(randomGenerator.nextBytes(algorithm.ivLengthBytes));

  var cipher = forge.cipher.createCipher(algorithm.symmetricCipher, result.key);
  cipher.start({iv: iv});
  cipher.update(forge.util.createBuffer(data));
  cipher.finish();

  var encryptedData = cipher.output.getBytes();
  var tag = cipher.mode.tag.getBytes();

  return result.encapsulation.toString() + iv.toString() +
      encryptedData.toString() + tag.toString();
}
