/*
 * 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 MACs of the module.
 *
 * @namespace symmetric/mac
 */
cryptolib.modules.symmetric.mac = function(box) {
  'use strict';

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

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

  var policies = {
    mac: {
      hash: box.policies.symmetric.mac.hash,
      provider: box.policies.symmetric.mac.provider
    }
  };

  var converters;
  var exceptions;

  cryptolib('commons', function(box) {
    converters = new box.commons.utils.Converters();
    exceptions = box.commons.exceptions;
  });

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

  box.symmetric.mac.factory.MacFactory.prototype = {

    /**
     * Create a new MAC.
     * <p>
     * The particular MAC that is returned depends on the policies.
     *
     * @function
     * @returns a new MAC.
     */
    create: function() {
      try {
        if (policies.mac.provider === Config.symmetric.mac.provider.FORGE) {
          return this.createCryptoForgeMac();
        } else {
          throw new exceptions.CryptoLibException(
              'No suitable provider was provided.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'A CryptoMac could not be obtained.', error);
      }
    },

    /**
     * Create a new CryptoForgeMac.
     *
     * @function
     * @returns {symmetric/mac.CryptoForgeMac} a new CryptoForgeMac.
     */
    createCryptoForgeMac: function() {
      return new CryptoForgeMac();
    }
  };

  /**
   * Represents a MAC.
   *
   * @class
   * @memberof symmetric/mac
   */
  function CryptoForgeMac() {
    this.hmacDigester = forge.hmac.create();
  }

  CryptoForgeMac.prototype = {

    /**
     * Generates MAC for some data.
     *
     * @function
     * @param secretKeyBase64
     *            {String} Secret key, as string in Base64 encoded format
     * @param arrayDataBase64
     *            {String} The input data for the MAC, as an array of string
     *            in Base64 encoded format. It is assumed that the array is
     *            not empty.
     * @returns The generated MAC, in Base64 encoded format.
     */
    generate: function(secretKeyBase64, arrayDataBase64) {

      var secretKey = converters.base64Decode(secretKeyBase64);
      var secretKeyBuffer = new forge.util.ByteBuffer(secretKey);

      try {
        this.hmacDigester.start(policies.mac.hash, secretKeyBuffer);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'MAC message digester could not be initialized.', error);
      }

      try {
        if (arrayDataBase64.length < 1) {
          throw new exceptions.CryptoLibException(
              'The array of data should have at least one element');
        }

        var dataB64;
        for (var i = 0; i < arrayDataBase64.length; i++) {
          dataB64 = arrayDataBase64[i];
          var data = converters.base64Decode(dataB64);
          this.hmacDigester.update(data);
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'MAC message digester could not be updated.', error);
      }

      try {
        var hmacMessageDigest = this.hmacDigester.digest();

        return converters.base64Encode(hmacMessageDigest.getBytes());
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'MAC message digest could not be generated.', error);
      }
    },

    /**
     * Verify that a given MAC is indeed the MAC for the given data, using
     * the given secret key.
     *
     * @function
     * @param macBase64
     *            {String} The MAC to be verified, as string in Base64
     *            encoded format.
     * @param secretKeyBase64
     *            {String} Secret key, as string in Base64 encoded format
     * @param arrayDataBase64
     *            {String} The input data for the MAC, as an array of string
     *            in Base64 encoded format. It is assumed that the array is
     *            not empty.
     *
     * @returns True if the MAC is the MAC of the given data and SecretKey,
     *          false otherwise.
     */
    verify: function(macBase64, secretKeyBase64, arrayDataBase64) {
      if (typeof macBase64 === 'undefined' || macBase64 === null) {
        return false;
      }

      var macBase64ToVerify = this.generate(secretKeyBase64, arrayDataBase64);

      if (macBase64.length !== macBase64ToVerify.length) {
        return false;
      }

      var equals = true;
      for (var i = 0; i < macBase64ToVerify.length; i++) {
        if (macBase64ToVerify.charCodeAt(i) !== macBase64.charCodeAt(i)) {
          equals = false;
        }
      }
      return equals;
    }
  };
};
