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

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

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

  var policies = {
    cipher: {
      algorithm: box.policies.asymmetric.cipher.algorithm,
      secretKeyLengthBytes: box.policies.asymmetric.cipher.secretKeyLengthBytes,
      ivLengthBytes: box.policies.asymmetric.cipher.ivLengthBytes,
      tagLengthBytes: box.policies.asymmetric.cipher.tagLengthBytes,
      deriver: box.policies.asymmetric.cipher.deriver,
      hash: box.policies.asymmetric.cipher.hash,
      symmetricCipher: box.policies.asymmetric.cipher.symmetricCipher,
      provider: box.policies.asymmetric.cipher.provider
    }
  };

  var utils, converters, exceptions, randomFactory;

  var f = function(box) {
    utils = box.commons.utils;
    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.cipher.secureRandom.provider}
    }
  };

  cryptolib('commons', 'primitives.securerandom', f);
  /** @class */
  box.asymmetric.cipher.factory.AsymmetricCipherFactory = function() {};

  box.asymmetric.cipher.factory.AsymmetricCipherFactory.prototype = {

    /**
     * @function Gets an asymmetric cipher.
     * @returns {asymmetric/cipher.CryptoForgeAsymmetricCipher}
     */
    getCryptoAsymmetricCipher: function() {
      try {
        if (policies.cipher.provider ===
            Config.asymmetric.cipher.provider.FORGE) {
          var secureRandomBytes = randomFactory.getCryptoRandomBytes();
          return this.getCryptoForgeAsymmetricCipher(secureRandomBytes);
        } else {
          throw new exceptions.CryptoLibException(
              'No suitable provider for the asymmetric cipher was provided.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'A CryptoAsymmetricCipher could not be obtained.', error);
      }
    },

    /**
     * @function Gets a Forge asymmetric cipher.
     * @param secureRandom
     *            {CryptoScytlRandomBytes} a source of random bytes.
     * @returns {asymmetric/cipher.CryptoForgeAsymmetricCipher}
     */
    getCryptoForgeAsymmetricCipher: function(secureRandom) {
      return new CryptoForgeAsymmetricCipher(secureRandom);
    }
  };

  /**
   * Defines a Forge asymmetric cipher
   *
   * @class
   * @param secureRandomBytes
   *            {CryptoScytlRandomBytes} a source of random bytes.
   * @memberof asymmetric/cipher
   */
  function CryptoForgeAsymmetricCipher(secureRandomBytes) {
    if (!secureRandomBytes) {
      throw new exceptions.CryptoLibException(
          'The received PRNG was not valid');
    }

    this.bitOperators = new utils.BitOperators();

    this.secureRandomBytes = secureRandomBytes;

    // We would like to use the SCYTL PRNG directly in the ciphers that are
    // created. However it is currently not possible to pass a PRNG to the
    // Forge library when creating or using a cipher. However, in some cases,
    // it is possible to pass a seed (created using the SCYTL PRNG) to FORGE
    // that is then used to seed the FORGE internal PRNG.
    this.encodingSeedLength = 32;

    try {
      if ((policies.cipher.algorithm.name !==
           Config.asymmetric.cipher.algorithm.RSA_OAEP.name) &&
          (policies.cipher.algorithm.name !==
           Config.asymmetric.cipher.algorithm.RSA_KEM.name)) {
        throw new exceptions.CryptoLibException(
            'The specified algorithm is not supported.');
      }
    } catch (error) {
      throw new exceptions.CryptoLibException(
          'CryptoForgeAsymmetricCipher could not be created.', error);
    }

    /**
     * Parses the four parts of the data that is produced by the encrypt
     * function, and that is 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 out all four of the parts.
     */
    this._parseParts = function(privateKey, encryptedData) {

      var ivLengthBytes = policies.cipher.ivLengthBytes;
      var tagLengthBytes = policies.cipher.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 =
          this.bitOperators.extract(encryptedData, 0, startIndexOfSecondPart);

      var iv = this.bitOperators.extract(
          encryptedData, startIndexOfSecondPart, startIndexOfThirdPart);

      var data = this.bitOperators.extract(
          encryptedData, startIndexOfThirdPart, startIndexOfFourthPart);

      var tag =
          this.bitOperators.extract(encryptedData, startIndexOfFourthPart);

      return {encapsulation: encapsulation, iv: iv, data: data, tag: tag};
    };

    this._validateInputs = function(keyPem, data) {

      if (!keyPem) {
        throw new exceptions.CryptoLibException(
            'The received key was not initialized.');
      }

      if (!data) {
        throw new exceptions.CryptoLibException(
            'The received data was not initialized.');
      }
    };

    // 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.
    this._getRsaOaepHash = function() {
      return box.forge.md.sha256.create();
    };

    //
    this._getRsaOaepMaskHash = function() {
      // For interoperability purposes, the MGF1 hash function must
      // remain as SHA-1.
      return box.forge.md.sha1.create();
    };

    this._getRsaOaepEncodingOptions = function() {

      var encodingOptions = {
        md: this._getRsaOaepHash(),
        mgf1: {md: this._getRsaOaepMaskHash()},
        seed: this.secureRandomBytes.nextRandom(this.encodingSeedLength)
      };

      return encodingOptions;
    };

    this._getRsaOaepDecodingOptions = function() {

      var decodingOptions = {
        md: this._getRsaOaepHash(),
        mgf1: {md: this._getRsaOaepMaskHash()}
      };

      return decodingOptions;
    };

    this._determineAndCreateHash = function(requestedHash) {

      if (requestedHash ===
          Config.asymmetric.cipher.algorithm.RSA_KEM.deriver.messagedigest
              .algorithm.SHA256) {
        return forge.md.sha256.create();
      } else {
        throw new exceptions.CryptoLibException(
            'Unsupported hash function specified.');
      }
    };

    this._determineAndCreateDeriver = function(requestedDeriver) {
      var hash = this._determineAndCreateHash(policies.cipher.hash);

      if (requestedDeriver ===
          Config.asymmetric.cipher.algorithm.RSA_KEM.deriver.name.KDF1) {
        return new forge.kem.kdf1(hash);
      } else if (
          requestedDeriver ===
          Config.asymmetric.cipher.algorithm.RSA_KEM.deriver.name.KDF2) {
        return new forge.kem.kdf2(hash);
      } else if (
          requestedDeriver ===
          Config.asymmetric.cipher.algorithm.RSA_KEM.deriver.name.MGF1) {
        return new forge.mgf.mgf1.create(hash);
      } else {
        throw new exceptions.CryptoLibException(
            'Unsupported deriver function specified.');
      }
    };
  }

  CryptoForgeAsymmetricCipher.prototype = {

    /**
     * Encrypts some data.
     * <p>
     * If the algorithm is RSA-KEM, then the output from this function will
     * be the base64 encoding of the following data:
     * <p>
     * [Encapsulation][IV][Encrypted Data][Tag]
     *
     * @function
     * @param publicKeyPem
     *            {string} public key, as string in PEM format.
     * @param dataBase64
     *            {string} data to be encrypted, as string in Base64 encoded
     *            format.
     * @returns encrypted data, as string in Base 64 encoded format..
     */
    encrypt: function(publicKeyPem, dataBase64) {
      this._validateInputs(publicKeyPem, dataBase64);

      try {
        var publicKey = box.forge.pki.publicKeyFromPem(publicKeyPem);

        var data = converters.base64Decode(dataBase64);

        var output;

        if (policies.cipher.algorithm.name ===
            Config.asymmetric.cipher.algorithm.RSA_OAEP.name) {
          var encodingOptions = this._getRsaOaepEncodingOptions();

          output = publicKey.encrypt(
              data, policies.cipher.algorithm.name, encodingOptions);

        } else if (
            policies.cipher.algorithm.name ===
            Config.asymmetric.cipher.algorithm.RSA_KEM.name) {
          var secretKeyLengthBytes = policies.cipher.secretKeyLengthBytes;
          var ivLengthBytes = policies.cipher.ivLengthBytes;
          var symmetricCipher = policies.cipher.symmetricCipher;

          var deriver =
              this._determineAndCreateDeriver(policies.cipher.deriver);

          // generate and encapsulate secret key
          var kem = forge.kem.rsa.create(deriver);
          var result = kem.encrypt(publicKey, secretKeyLengthBytes);

          var iv = forge.random.getBytesSync(ivLengthBytes);
          var cipher = forge.cipher.createCipher(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();

          output = result.encapsulation.toString() + iv.toString() +
              encryptedData.toString() + tag.toString();

        } else {
          throw new exceptions.CryptoLibException(
              'The specified algorithm is not supported.');
        }

        return converters.base64Encode(output);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'CryptoForgeAsymmetricCipher, data could not be encrypted.', error);
      }
    },

    /**
     * Decrypts some encrypted data.
     *
     * @function
     * @param privateKeyPem
     *            {string} private key, as string in PEM format.
     * @param encryptedDataB64
     *            {string} encrypted data, as string in Base64 encoded
     *            format.
     * @returns decrypted data, as string in Base64 encoded format.
     */
    decrypt: function(privateKeyPem, encryptedDataBase64) {

      this._validateInputs(privateKeyPem, encryptedDataBase64);

      try {
        var privateKey = box.forge.pki.privateKeyFromPem(privateKeyPem);

        var encryptedData = converters.base64Decode(encryptedDataBase64);

        var decryptedData;

        if (policies.cipher.algorithm.name ===
            Config.asymmetric.cipher.algorithm.RSA_OAEP.name) {
          var decodingOptions = this._getRsaOaepDecodingOptions();

          decryptedData = privateKey.decrypt(
              encryptedData, policies.cipher.algorithm.name, decodingOptions);

        } else if (
            policies.cipher.algorithm.name ===
            Config.asymmetric.cipher.algorithm.RSA_KEM.name) {
          var symmetricCipher = policies.cipher.symmetricCipher;
          var secretKeyLengthBytes = policies.cipher.secretKeyLengthBytes;

          var encryptedDataParts = this._parseParts(privateKey, encryptedData);
          var deriver =
              this._determineAndCreateDeriver(policies.cipher.deriver);

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

          // decrypt some bytes
          var decipher = forge.cipher.createDecipher(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) {
            decryptedData = decipher.output.getBytes();
          } else {
            throw new exceptions.CryptoLibException(
                'Error while decrypting data.');
          }
        } else {
          throw new exceptions.CryptoLibException(
              'The specified algorithm is not supported.');
        }

        return converters.base64Encode(decryptedData);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'CryptoForgeAsymmetricCipher, data could not be decrypted.', error);
      }
    }
  };
};
