/*
 * 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 bitwise = require('scytl-bitwise');
var codec = require('scytl-codec');
var forge = require('node-forge');

module.exports = AsymmetricDecrypter;

/**
 * @class AsymmetricDecrypter
 * @classdesc The asymmetric decrypter API. To instantiate this object, use the
 *            method {@link AsymmetricCryptographyService.newDecrypter}.
 * @hideconstructor
 * @param {Policy}
 *            policy The cryptographic policy to use.
 */
function AsymmetricDecrypter(policy) {
  // PRIVATE ///////////////////////////////////////////////////////////////////

  var algorithm_;
  var oaepDigester_;
  var oeapMaskDigester_;
  var kemDeriver_;
  var forgePrivateKey_;

  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(
            '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(
            'RSA-KEM symmetric cipher algorithm \'' +
            algorithm.symmetricCipher + '\' is not supported.');
      }

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

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

  algorithm_ = policy.asymmetric.cipher.algorithm;

  initForgeCipher(algorithm_);

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

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

    forgePrivateKey_ = forge.pki.privateKeyFromPem(privateKey);
    if (typeof forgePrivateKey_.decrypt === 'undefined') {
      throw new Error(
          'PEM encoding of private key with which to initialize asymmetric decrypter is corrupt');
    }

    return this;
  };

  /**
   * Asymmetrically decrypts the provided data. Before using this method, the
   * decrypter must have been initialized with a private key, via the method
   * {@link AsymmetricDecrypter.init}.
   *
   * @function decrypt
   * @memberof AsymmetricDecrypter
   * @param {Uint8Array}
   *            encryptedData The encrypted data to decrypt.
   * @returns {Uint8Array} The decrypted data. <b>NOTE:</b> To retrieve data
   *          of type <code>string</code>, apply method
   *          <code>codec.utf8Decode</code> to result.
   * @throws {Error}
   *             If the input data validation fails, the decrypter was not
   *             initialized or the decryption process fails.
   */
  this.decrypt = function(encryptedData) {
    if (typeof forgePrivateKey_ === 'undefined') {
      throw new Error(
          'Asymmetric decrypter has not been initialized with any private key');
    }

    validator.checkIsInstanceOf(
        encryptedData, Uint8Array, 'Uint8Array',
        'Encrypted data to asymmetrically decrypt');

    try {
      var decryptedData;

      // Choose a decryption algorithm from the policy.
      switch (algorithm_.name) {
        case Policy.options.asymmetric.cipher.algorithm.RSA_OAEP.name:
          decryptedData = forgePrivateKey_.decrypt(
              encryptedData, algorithm_.name,
              {md: oaepDigester_, mgf1: {md: oeapMaskDigester_}});
          break;
        default:
          decryptedData = kemDecrypt(
              forgePrivateKey_, encryptedData, algorithm_, kemDeriver_);
      }

      return codec.binaryDecode(decryptedData);
    } catch (error) {
      throw new Error('Data could not be decrypted; ' + 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.');
  }
}

/**
 * Decrypt some encrypted data using the 'RSA-KEM' cipher algorithm.
 *
 * @function kemDecrypt
 * @memberof AsymmetricDecrypter
 * @private
 * @param {Object}
 *            privateKey The private key used for decrypting.
 * @param {Object}
 *            encryptedData The encrypted data to decrypt.
 * @param {Object}
 *            algorithm The cipher algorithm.
 * @returns {Object} The decrypted data.
 */
function kemDecrypt(privateKey, encryptedData, algorithm, deriver) {
  var encryptedDataParts = parseParts(privateKey, encryptedData, algorithm);

  //  var deriver = createDeriver(
  //      algorithm.keyDeriver.name, algorithm.keyDeriver.hashAlgorithm);

  // decrypt encapsulated secret key
  var kem = forge.kem.rsa.create(deriver);
  var key = kem.decrypt(
      privateKey, encryptedDataParts.encapsulation,
      algorithm.secretKeyLengthBytes);

  // decrypt some bytes
  var decipher = forge.cipher.createDecipher(algorithm.symmetricCipher, key);
  decipher.start({iv: encryptedDataParts.iv, tag: encryptedDataParts.tag});
  decipher.update(forge.util.createBuffer(encryptedDataParts.data));
  var result = decipher.finish();

  // result will be false if there was a failure
  if (result) {
    return decipher.output.getBytes();
  } else {
    throw new Error('Data could not be KEM decrypted.');
  }
}

/**
 * Parses the four parts of the data that are produced by the encrypt function,
 * and that are received by the decrypt function.
 * <p>
 * We know the length of three of these parts, and we know the total length of
 * the data, therefore we can parse all four of the parts.
 *
 * @function parseParts
 * @memberof AsymmetricDecrypter
 * @private
 * @param {Object}
 *            privateKey The private key used for decrypting.
 * @param {Object}
 *            encryptedData The encrypted data to decrypt.
 * @param {Object}
 *            algorithm The cipher algorithm. returns {Object} The object
 *            encapsulating the parsing results.
 */
function parseParts(privateKey, encryptedData, algorithm) {
  var ivLengthBytes = algorithm.ivLengthBytes;
  var tagLengthBytes = algorithm.tagLengthBytes;
  var encapsulationLengthBytes = privateKey.n.bitLength() / 8;

  var totalLength = encryptedData.length;
  var totalKnownLength =
      encapsulationLengthBytes + ivLengthBytes + tagLengthBytes;
  var encryptedDataLength = totalLength - totalKnownLength;

  var startIndexOfSecondPart = encapsulationLengthBytes;
  var startIndexOfThirdPart = startIndexOfSecondPart + ivLengthBytes;
  var startIndexOfFourthPart = startIndexOfThirdPart + encryptedDataLength;

  var encapsulation = bitwise.slice(encryptedData, 0, startIndexOfSecondPart);

  var iv = bitwise.slice(
      encryptedData, startIndexOfSecondPart, startIndexOfThirdPart);

  var data = bitwise.slice(
      encryptedData, startIndexOfThirdPart, startIndexOfFourthPart);

  var tag = bitwise.slice(encryptedData, startIndexOfFourthPart);

  return {
    encapsulation: codec.binaryEncode(encapsulation),
    iv: codec.binaryEncode(iv),
    data: codec.binaryEncode(data),
    tag: codec.binaryEncode(tag)
  };
}
