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

/** @namespace primitives/derivation */
cryptolib.modules.primitives.derivation = function(box) {
  'use strict';

  box.primitives = box.primitives || {};
  box.primitives.derivation = {};
  /**
   * A module that holds derivation functionalities.
   *
   * @exports primitives/derivation
   */
  box.primitives.derivation.factory = {};

  var policies = {
    pbkdf: {
      provider: box.policies.primitives.derivation.pbkdf.provider,
      hash: box.policies.primitives.derivation.pbkdf.hash,
      saltLengthBytes: box.policies.primitives.derivation.pbkdf.saltLengthBytes,
      keyLengthBytes: box.policies.primitives.derivation.pbkdf.keyLengthBytes,
      iterations: box.policies.primitives.derivation.pbkdf.iterations
    }
  };

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

  /**
   * A factory class for generating {@link CryptoForgePBKDFSecretKeyGenerator}
   * objects.
   *
   * @class
   */
  box.primitives.derivation.factory.SecretKeyGeneratorFactory = function() {

  };

  box.primitives.derivation.factory.SecretKeyGeneratorFactory.prototype = {
    /**
     * Creates a generator for deriving passwords.
     *
     * @function
     * @returns {primitives/derivation.CryptoForgePBKDFSecretKeyGenerator} a
     *          {@link primitives/derivation.CryptoForgePBKDFSecretKeyGenerator}
     *          object.
     */
    createPBKDF: function() {
      var provider = policies.pbkdf.provider;

      try {
        if (provider === Config.primitives.derivation.pbkdf.provider.FORGE) {
          return new CryptoForgePBKDFSecretKeyGenerator();
        } else {
          throw new exceptions.CryptoLibException(
              'The specified provider ' + provider + ' is not supported.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'A CryptoPBKDFSecretKeyGenerator object could not be created.',
            error);
      }
    }
  };

  /**
   *
   * @class
   * @memberof primitives/derivation
   */
  function CryptoForgePBKDFSecretKeyGenerator() {
    validateHash(policies.pbkdf.hash);
    validateKeyLengthBytes(policies.pbkdf.keyLengthBytes);
    validateSaltLengthBytes(policies.pbkdf.saltLengthBytes);
    validateIterations(policies.pbkdf.iterations);

    this.keyLengthBytes = policies.pbkdf.keyLengthBytes;
  }

  CryptoForgePBKDFSecretKeyGenerator.prototype = {

    /**
     * @param {string}
     *            password the password, as a string of bytes in Base64
     *            encoded format.
     * @param {string}
     *            salt the salt, as a string of bytes in Base64 encoded
     *            format. Note that the salt length should coincide with the
     *            one required by the hash algorithm.
     *
     * @return {string} the derived key, as a string of bytes in Base64
     *         encoded format.
     */
    generateSecret: function(passwordB64, saltB64) {

      var password = converters.base64Decode(passwordB64);

      var salt = converters.base64Decode(saltB64);

      if (salt.length < policies.pbkdf.saltLengthBytes) {
        throw new exceptions.CryptoLibException(
            'The salt byte length ' + salt.length +
            ' is less than the minimum allowed salt length ' +
            policies.pbkdf.saltLengthBytes +
            ' set by the cryptographic policy.');
      }

      var derivedKey = sjcl.misc.pbkdf2(
          password, sjcl.codec.base64.toBits(saltB64),
          policies.pbkdf.iterations, this.keyLengthBytes * 8,
          hashCallback(policies.pbkdf.hash));

      return sjcl.codec.base64.fromBits(derivedKey);
    }
  };

  function hashCallback(algorithm) {
    if (algorithm === Config.primitives.derivation.pbkdf.hash.SHA256) {
      return null;
    } else {
      throw new exceptions.CryptoLibException(
          'Hash algorithm \'' + algorithm + '\' is unsupported');
    }
  }

  function validateHash(hash) {
    var supportedHashAlgorithms =
        [Config.primitives.derivation.pbkdf.hash.SHA256];

    if (supportedHashAlgorithms.indexOf(hash) < 0) {
      throw new exceptions.CryptoLibException(
          'The specified hash algorithm ' + hash +
          ' is not one of the supported options: ' + supportedHashAlgorithms);
    }
  }

  function validateSaltLengthBytes(saltLengthBytes) {
    var supportedSaltLengthBytes = [
      Config.primitives.derivation.pbkdf.saltLengthBytes.SL_20,
      Config.primitives.derivation.pbkdf.saltLengthBytes.SL_32
    ];

    if (typeof saltLengthBytes !== 'number' || saltLengthBytes % 1 !== 0 ||
        saltLengthBytes < 0) {
      throw new exceptions.CryptoLibException(
          'The specified salt byte length ' + saltLengthBytes +
          ' is not a positive integer.');
    }

    if (supportedSaltLengthBytes.indexOf(saltLengthBytes) < 0) {
      throw new exceptions.CryptoLibException(
          'The specified salt byte length ' + saltLengthBytes +
          ' is not one of the supported options: ' + supportedSaltLengthBytes);
    }
  }

  function validateKeyLengthBytes(keyLengthBytes) {
    var supportedKeyLengthBytes = [
      Config.primitives.derivation.pbkdf.keyLengthBytes.KL_16,
      Config.primitives.derivation.pbkdf.keyLengthBytes.KL_32
    ];

    if (typeof keyLengthBytes !== 'number' || keyLengthBytes % 1 !== 0 ||
        keyLengthBytes < 0) {
      throw new exceptions.CryptoLibException(
          'The specified key byte length ' + keyLengthBytes +
          ' is not a positive number.');
    }

    if (supportedKeyLengthBytes.indexOf(keyLengthBytes) < 0) {
      throw new exceptions.CryptoLibException(
          'The specified key byte length ' + keyLengthBytes +
          ' is not one of the supported options: ' + supportedKeyLengthBytes);
    }
  }

  function validateIterations(iterations) {
    var supportedIterations = [
      Config.primitives.derivation.pbkdf.iterations.I_1,
      Config.primitives.derivation.pbkdf.iterations.I_8000,
      Config.primitives.derivation.pbkdf.iterations.I_16000,
      Config.primitives.derivation.pbkdf.iterations.I_32000,
      Config.primitives.derivation.pbkdf.iterations.I_64000
    ];

    if (typeof iterations !== 'number' || iterations % 1 !== 0 ||
        iterations < 0) {
      throw new exceptions.CryptoLibException(
          'The specified number of iterations ' + iterations +
          ' is not a positive integer.');
    }

    if (supportedIterations.indexOf(iterations) < 0) {
      throw new exceptions.CryptoLibException(
          'The specified number of iterations ' + iterations +
          ' is not one of the supported options: ' + supportedIterations);
    }
  }
};
