/*
 * 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.symmetric = cryptolib.modules.symmetric || {};

/**
 * Defines the symmetric ciphers.
 *
 * @namespace symmetric/cipher
 */
cryptolib.modules.symmetric.cipher = function(box) {
  'use strict';

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

  /**
   * Allows a symmetric cipher factory to be obtained.
   *
   * @exports symmetric/cipher/factory
   */
  box.symmetric.cipher.factory = {};

  var policies = {
    cipher: {
      algorithm: box.policies.symmetric.cipher.algorithm,
      provider: box.policies.symmetric.cipher.provider,
      initializationVectorLengthBytes:
          box.policies.symmetric.cipher.initializationVectorLengthBytes
    }
  };

  for (var prop in policies.cipher.algorithm) {
    if (typeof prop !== 'undefined') {
      policies.cipher.algorithm = policies.cipher.algorithm[prop];
    }
  }

  var utils, exceptions, randomFactory;

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

  f.policies = {
    primitives: {
      secureRandom:
          {provider: box.policies.symmetric.cipher.secureRandom.provider}
    }
  };

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

  /**
   * Factory class that allows a symmetric cipher to be obtained.
   *
   * @class
   */
  box.symmetric.cipher.factory.SymmetricCipherFactory = function() {};

  box.symmetric.cipher.factory.SymmetricCipherFactory.prototype = {

    /**
     * Create a new symmetric cipher.
     * <p>
     * The particular cipher that is returned depends on the policies.
     *
     * @function
     * @returns a new symmetric cipher.
     */
    getCryptoSymmetricCipher: function() {

      var secureRandom;

      try {
        if (policies.cipher.provider ===
            Config.symmetric.cipher.provider.FORGE) {
          secureRandom = randomFactory.getCryptoRandomBytes();

          return this.getCryptoForgeSymmetricCipher(secureRandom);
        } else {
          throw new exceptions.CryptoLibException(
              'No suitable provider was provided.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'A CryptoSymmetricCipher could not be obtained.', error);
      }
    },

    /**
     * Create a new CryptoForgeSymmetricCipher.
     *
     * @function
     * @param secureRandom
     *            {CryptoScytlRandomBytes} a secure source of random bytes.
     * @returns {symmetric/cipher.CryptoForgeSymmetricCipher} a new
     *          CryptoForgeSymmetricCipher.
     */
    getCryptoForgeSymmetricCipher: function(secureRandom) {
      return new CryptoForgeSymmetricCipher(secureRandom);
    }
  };

  /**
   * @class
   * @param randomGenerator
   *            {CryptoScytlRandomBytes} a secure random bytes generator.
   * @memberof symmetric/cipher
   */
  function CryptoForgeSymmetricCipher(randomGenerator) {
    this.converters = new utils.Converters();
    this.bitOperators = new utils.BitOperators();
    this.randomGenerator = randomGenerator;
  }

  CryptoForgeSymmetricCipher.prototype = {

    /**
     * Encrypts some data.
     *
     * @function
     * @param data
     *            {String} Data to be encrypted, as string in Base64 encoded
     *            format.
     * @param keyB64
     *            {String} Key, as string in Base64 encoded format.
     * @returns Initialization vector concatenated with the encrypted data,
     *          as string in Base64 encoded format.
     */
    encrypt: function(keyB64, dataB64) {
      try {
        var key = this.converters.base64Decode(keyB64);
        var data = this.converters.base64Decode(dataB64);

        if (key.length !== policies.cipher.algorithm.keyLengthBytes) {
          throw new exceptions.CryptolibException(
              'Key length does not match the required by the algorithm.');
        }

        var initVec = this.randomGenerator.nextRandom(
            policies.cipher.initializationVectorLengthBytes);

        // Create a byte buffer for data.
        var dataBuffer = new box.forge.util.ByteBuffer(data);

        var cipher =
            box.forge.cipher.createCipher(policies.cipher.algorithm.name, key);

        // Only for the GCM mode
        var gcmAuthTagByteLength = policies.cipher.algorithm.tagLengthBytes;

        if (typeof gcmAuthTagBitLength !== 'undefined') {
          cipher.start({iv: initVec, tagLength: gcmAuthTagByteLength});
        } else {
          cipher.start({iv: initVec});
        }

        cipher.update(dataBuffer);
        cipher.finish();

        var encryptedData = cipher.output.getBytes().toString();

        if (typeof gcmAuthTagByteLength !== 'undefined') {
          var gcmAuthTag = cipher.mode.tag.getBytes();
          encryptedData = encryptedData + gcmAuthTag.toString();
        }

        var initVectorAndEncryptedData = initVec + encryptedData;
        var initVectorAndEncryptedDataBase64 =
            this.converters.base64Encode(initVectorAndEncryptedData);

        return initVectorAndEncryptedDataBase64;
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Data could not be symmetrically encrypted.', error);
      }
    },

    /**
     * Decrypts some encrypted data, using the initialization vector that is
     * provided
     *
     * @function
     * @param keyB64
     *            {String} the key as a string in Base64 encoded format.
     * @param initVectorAndEncryptedDataBase64
     *            {String} Initialization vector concatenated with the
     *            encrypted data, as string in Base64 encoded format.
     * @returns Decrypted data, as string in Base64 encoded format.
     */
    decrypt: function(keyB64, initVectorAndEncryptedDataBase64) {
      try {
        var key = this.converters.base64Decode(keyB64);

        if (key.length !== policies.cipher.algorithm.keyLengthBytes) {
          throw new exceptions.CryptolibException(
              'Key length does not match the required by the algorithm.');
        }

        var cipher = box.forge.cipher.createDecipher(
            policies.cipher.algorithm.name, key);

        var initVectorAndEncryptedData =
            this.converters.base64Decode(initVectorAndEncryptedDataBase64);
        var initVector = this.bitOperators.extract(
            initVectorAndEncryptedData, 0,
            policies.cipher.initializationVectorLengthBytes);
        var encryptedData = this.bitOperators.extract(
            initVectorAndEncryptedData,
            policies.cipher.initializationVectorLengthBytes,
            initVectorAndEncryptedData.length);

        // Only for the GCM mode
        var gcmAuthTagByteLength = policies.cipher.algorithm.tagLengthBytes;

        if (typeof gcmAuthTagByteLength !== 'undefined') {
          var offset = encryptedData.length - gcmAuthTagByteLength;

          var gcmAuthTag = this.bitOperators.extract(
              encryptedData, offset, encryptedData.length);

          encryptedData = this.bitOperators.extract(encryptedData, 0, offset);

          var gcmAuthTagBitLength =
              gcmAuthTagByteLength * box.NUMBER_OF_BITS_PER_BYTE;

          cipher.start({
            iv: initVector,
            tagLength: gcmAuthTagBitLength,
            tag: gcmAuthTag
          });

        } else {
          cipher.start({iv: initVector});
        }

        var encryptedDataBuffer = new box.forge.util.ByteBuffer(encryptedData);

        cipher.update(encryptedDataBuffer);
        cipher.finish();

        var outputBase64 =
            this.converters.base64Encode(cipher.output.getBytes().toString());
        return outputBase64;

      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Data could not be symmetrically decrypted.', error);
      }
    }
  };
};