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

/**
 * Defines the generation and opening of digital envelopes.
 */
cryptolib.modules.digitalenvelope = function(box) {
  'use strict';

  box.digitalenvelope = box.digitalenvelope || {};
  box.digitalenvelope.factory = {};

  var policies = {
    symmetric: {
      secretkey: {
        encryption: {
          lengthBytes: box.policies.digitalenvelope.symmetric.secretkey
                           .encryption.lengthBytes
        },
        mac: {
          lengthBytes:
              box.policies.digitalenvelope.symmetric.secretkey.mac.lengthBytes
        }
      }
    }
  };

  var exceptions;
  var converters;
  var bitOperators;
  var secretKeyFactory;
  var macDigester;
  var symmetricCipher;
  var asymmetricCipher;

  var f = function(box) {
    exceptions = box.commons.exceptions;
    converters = new box.commons.utils.Converters();
    bitOperators = new box.commons.utils.BitOperators();
    secretKeyFactory = new box.symmetric.secretkey.factory.SecretKeyFactory();
    macDigester = (new box.symmetric.mac.factory.MacFactory()).create();
    symmetricCipher =
        (new box.symmetric.cipher.factory.SymmetricCipherFactory())
            .getCryptoSymmetricCipher();
    asymmetricCipher =
        (new box.asymmetric.cipher.factory.AsymmetricCipherFactory())
            .getCryptoAsymmetricCipher();
  };

  f.policies = {
    symmetric: {
      secretkey: {
        encryption: {
          lengthBytes: box.policies.digitalenvelope.symmetric.secretkey
                           .encryption.lengthBytes
        },
        mac: {
          lengthBytes:
              box.policies.digitalenvelope.symmetric.secretkey.mac.lengthBytes
        },
        secureRandom: {
          provider: box.policies.digitalenvelope.symmetric.secretkey
                        .secureRandom.provider
        }
      },
      cipher: {
        provider: box.policies.digitalenvelope.symmetric.cipher.provider,
        algorithm: {
          AES128_GCM: {
            name: box.policies.digitalenvelope.symmetric.cipher.algorithm
                      .AES128_GCM.name,
            keyLengthBytes: box.policies.digitalenvelope.symmetric.cipher
                                .algorithm.AES128_GCM.keyLengthBytes,
            tagLengthBytes: box.policies.digitalenvelope.symmetric.cipher
                                .algorithm.AES128_GCM.tagLengthBytes
          }
        },
        initializationVectorLengthBytes:
            box.policies.digitalenvelope.symmetric.cipher
                .initializationVectorLengthBytes,
        secureRandom: {
          provider: box.policies.digitalenvelope.symmetric.cipher.secureRandom
                        .provider
        }
      },
      mac: {
        hash: box.policies.digitalenvelope.symmetric.mac.hash,
        provider: box.policies.digitalenvelope.symmetric.mac.provider
      }
    },
    asymmetric: {
      cipher: {
        algorithm: box.policies.digitalenvelope.asymmetric.cipher.algorithm,
        secretKeyLengthBytes:
            box.policies.digitalenvelope.asymmetric.cipher.secretKeyLengthBytes,
        ivLengthBytes:
            box.policies.digitalenvelope.asymmetric.cipher.ivLengthBytes,
        tagLengthBytes:
            box.policies.digitalenvelope.asymmetric.cipher.tagLengthBytes,
        deriver: box.policies.digitalenvelope.asymmetric.cipher.deriver,
        hash: box.policies.digitalenvelope.asymmetric.cipher.hash,
        symmetricCipher:
            box.policies.digitalenvelope.asymmetric.cipher.symmetricCipher,
        provider: box.policies.digitalenvelope.asymmetric.cipher.provider,
        secureRandom: {
          provider: box.policies.digitalenvelope.asymmetric.cipher.secureRandom
                        .provider
        }
      }
    }
  };

  cryptolib('commons', 'symmetric', 'asymmetric.cipher', f);

  /**
   * Defines a factory for the creation of digital envelope generators and
   * openers.
   *
   * @class DigitalEnvelopeFactory
   * @memberof digitalenvelope
   */
  box.digitalenvelope.factory.DigitalEnvelopeFactory = function() {

  };

  box.digitalenvelope.factory.DigitalEnvelopeFactory.prototype = {

    /**
     * Obtains a new digital envelope generator.
     *
     * @function getDigitalEnvelopeGenerator
     * @return {DigitalEnvelopeGenerator} the digital envelope generator.
     */
    getDigitalEnvelopeGenerator: function() {

      try {
        return new box.digitalenvelope.DigitalEnvelopeGenerator();
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not obtain a digital envelope generator.', error);
      }
    },

    /**
     * Obtains new a digital envelope opener.
     *
     * @function getDigitalEnvelopeOpener
     * @return {DigitalEnvelopeOpener} the digital envelope opener.
     */
    getDigitalEnvelopeOpener: function() {

      try {
        return new box.digitalenvelope.DigitalEnvelopeOpener();
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not obtain a digital envelope opener.', error);
      }
    }
  };

  /**
   * Defines a digital envelope generator.
   *
   * @class DigitalEnvelopeGenerator
   * @memberof digitalenvelope
   */
  box.digitalenvelope.DigitalEnvelopeGenerator = function() {

  };

  box.digitalenvelope.DigitalEnvelopeGenerator.prototype = {

    /**
     * Generates a digital envelope for some data. The envelope can be
     * generated with one or more public keys, each of whose corresponding
     * private key can be used to open the envelope.
     *
     * @function generate
     * @param dataBase64
     *            {string} the data to store in the digital envelope, as a
     *            string in Base64 encoded format.
     * @param publicKeysPem
     *            {string[]} a list of one or more public keys used to
     *            generate the digital envelope, as an array of strings in
     *            PEM format.
     * @return {DigitalEnvelope} the generated digital envelope.
     */
    generate: function(dataBase64, publicKeysPem) {

      try {
        validateGeneratorInput(dataBase64, publicKeysPem);

        // Generate encryption secret key and use it to symmetrically encrypt
        // data.
        var secretKeyForEncryptionBase64 =
            secretKeyFactory.getCryptoSecretKeyGeneratorForEncryption()
                .generate();
        var encryptedDataBase64 =
            symmetricCipher.encrypt(secretKeyForEncryptionBase64, dataBase64);

        // Generate MAC secret key and use it to generate MAC of symmetrically
        // Base64 encryption of encrypted data.
        var secretKeyForMacBase64 =
            secretKeyFactory.getCryptoSecretKeyGeneratorForMac().generate();
        var encryptedDataBase64Base64 =
            converters.base64Encode(encryptedDataBase64);
        var macBase64 = macDigester.generate(
            secretKeyForMacBase64, [encryptedDataBase64Base64]);

        // Construct bit-wise concatenation of encryption and MAC secret keys.
        var secretKeyForEncryption =
            converters.base64Decode(secretKeyForEncryptionBase64);
        var secretKeyForMac = converters.base64Decode(secretKeyForMacBase64);
        var secretKeyConcatenation =
            secretKeyForEncryption.toString() + secretKeyForMac.toString();
        var secretKeyConcatenationBase64 =
            converters.base64Encode(secretKeyConcatenation);

        // Generate list of asymmetric encryptions of the secret key
        // concatenation, each encryption created using a different one of the
        // public keys provided as input for the digital envelope generation.
        var secretKeyEncryptions = [];
        for (var i = 0; i < publicKeysPem.length; i++) {
          var publicKeyPem = publicKeysPem[i];
          var encryptedSecretKeyConcatentionBase64 = asymmetricCipher.encrypt(
              publicKeyPem, secretKeyConcatenationBase64);
          secretKeyEncryptions.push(new SecretKeyEncryption(
              encryptedSecretKeyConcatentionBase64, publicKeyPem));
        }

        return new DigitalEnvelope(
            encryptedDataBase64, macBase64, secretKeyEncryptions);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Digital envelope could not be generated.', error);
      }
    }
  };

  /**
   * Defines a digital envelope opener.
   *
   * @class DigitalEnvelopeOpener
   * @memberof digitalenvelope
   */
  box.digitalenvelope.DigitalEnvelopeOpener = function() {

    this._getEncryptedSecretKeyConcatenation = function(
        secretKeyEncryptions, privateKeyPem) {

      // Extract modulus and public exponent from private key.
      var privateKey = box.forge.pki.privateKeyFromPem(privateKeyPem);
      var modulus = privateKey.n;
      var publicExponent = privateKey.e;

      for (var i = 0; i < secretKeyEncryptions.length; i++) {
        var publicKeyPem = secretKeyEncryptions[i].getPublicKey();
        var publicKey = box.forge.pki.publicKeyFromPem(publicKeyPem);

        if (publicKey.n.equals(modulus) && publicKey.e.equals(publicExponent)) {
          return secretKeyEncryptions[i].getEncryptedSecretKeyConcatenation();
        }
      }

      throw new exceptions.CryptoLibException(
          'Could not find an asymmetric encryption of the secret key concatenation that could be decrypted using the private key provided.');
    };
  };

  box.digitalenvelope.DigitalEnvelopeOpener.prototype = {

    /**
     * Opens a digital envelope and retrieves its data. The envelope can be
     * opened with any private key corresponding to a public key that was
     * used to generate the envelope.
     *
     * @function open
     * @param digitalEnvelope
     *            {DigitalEnvelope} the digital envelope to open.
     * @param privateKeyPem
     *            {string} the private key used to open the digital
     *            envelope, as a string in PEM format.
     * @return {string} the data stored in the digital envelope, as a string
     *         in Base64 encoded format.
     */
    open: function(digitalEnvelope, privateKeyPem) {

      try {
        validateOpenerInput(digitalEnvelope, privateKeyPem);

        // Retrieve data from digital envelope.
        var encryptedDataBase64 = digitalEnvelope.getEncryptedData();
        var macBase64 = digitalEnvelope.getMac();
        var secretKeyEncryptions = digitalEnvelope.getSecretKeyEncryptions();

        // Retrieve asymmetric encryption of bit-wise concatenation of
        // encryption and MAC secret keys that can be decrypted with private
        // key provided as input.
        var encryptedSecretKeyConcatenationBase64 =
            this._getEncryptedSecretKeyConcatenation(
                secretKeyEncryptions, privateKeyPem);

        // Asymmetrically decrypt bit-wise concatenation of encryption and MAC
        // secret keys.
        var secretKeyConcatenationBase64 = asymmetricCipher.decrypt(
            privateKeyPem, encryptedSecretKeyConcatenationBase64);
        var secretKeyConcatenation =
            converters.base64Decode(secretKeyConcatenationBase64);

        // Extract secret keys from bit-wise concatenation.
        var secretKeyForEncryptionLength =
            policies.symmetric.secretkey.encryption.lengthBytes;
        var secretKeyForMacLength =
            policies.symmetric.secretkey.mac.lengthBytes;
        var secretKeyForEncryption = bitOperators.extract(
            secretKeyConcatenation, 0, secretKeyForEncryptionLength);
        var secretKeyForEncryptionBase64 =
            converters.base64Encode(secretKeyForEncryption);
        var secretKeyForMac = bitOperators.extract(
            secretKeyConcatenation, secretKeyForEncryptionLength,
            secretKeyForEncryptionLength + secretKeyForMacLength);
        var secretKeyForMacBase64 = converters.base64Encode(secretKeyForMac);

        // Check integrity of digital envelope.
        var encryptedDataBase64Base64 =
            converters.base64Encode(encryptedDataBase64);
        if (!macDigester.verify(
                macBase64, secretKeyForMacBase64,
                [encryptedDataBase64Base64])) {
          throw new exceptions.CryptoLibException(
              'Integrity of digital envelope could not be verified.');
        }

        return symmetricCipher.decrypt(
            secretKeyForEncryptionBase64, encryptedDataBase64);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Digital envelope could not be opened.', error);
      }
    }
  };

  /**
   * Container class for a digital envelope.
   *
   * @class DigitalEnvelope
   * @param encryptedDataBase64
   *            {string} the symmetrically encrypted data, as a string in
   *            Base64 encoded format.
   * @param macBase64
   *            {string} the MAC of the symmetrically encrypted data, as a
   *            string in Base64 encoded format.
   * @param secretKeyEncryptions
   *            {SecretKeyEncryption[]} the list of asymmetric encryptions of
   *            the secret key concatenation, each encryption having been
   *            created using a different one of the public keys provided as
   *            input to the digital envelope generator.
   * @memberof digitalenvelope
   */
  function DigitalEnvelope(
      encryptedDataBase64, macBase64, secretKeyEncryptions) {
    this.encryptedDataBase64 = encryptedDataBase64;
    this.macBase64 = macBase64;
    this.secretKeyEncryptions = secretKeyEncryptions;

    var _encryptedSecretKeyConcatsBase64 = [];
    var _publicKeysPem = [];
    for (var i = 0; i < secretKeyEncryptions.length; i++) {
      _encryptedSecretKeyConcatsBase64[i] =
          secretKeyEncryptions[i].getEncryptedSecretKeyConcatenation();
      _publicKeysPem[i] = secretKeyEncryptions[i].getPublicKey();
    }

    this._getEncryptedSecretKeyConcatsBase64 = function() {
      return _encryptedSecretKeyConcatsBase64;
    };

    this._getPublicKeysPem = function() {
      return _publicKeysPem;
    };
  }

  DigitalEnvelope.prototype = {

    /**
     * @function getEncryptedData
     * @return {string} the symmetrically encrypted data, as a string in
     *         Base64 encoded format.
     */
    getEncryptedData: function() {

      return this.encryptedDataBase64;
    },

    /**
     * @function getMac
     * @return {string} the MAC of the symmetrically encrypted data, as a
     *         string in Base64 encoded format.
     */
    getMac: function() {

      return this.macBase64;
    },

    /**
     * @function getSecretKeyEncryptions
     * @return {SecretKeyEncryption[]} the list of asymmetric encryptions of
     *         the secret key concatenation.
     */
    getSecretKeyEncryptions: function() {

      return this.secretKeyEncryptions;
    },

    /**
     * Generates a string representation of the digital envelope.
     * <p>
     * Note: in order to permit interoperability between libraries, this
     * representation should match the equivalent representation in Java.
     *
     * @function stringify
     * @return {string} the string representation of the digital envelope.
     */
    stringify: function() {

      return JSON.stringify({
        digitalEnvelope: {
          encryptedDataBase64: this.encryptedDataBase64,
          macBase64: this.macBase64,
          encryptedSecretKeyConcatsBase64:
              this._getEncryptedSecretKeyConcatsBase64(),
          publicKeysPem: this._getPublicKeysPem()
        }
      });
    }
  };

  /**
   * Container class for the asymmetric encryption of the bit-wise
   * concatenation of the secret key used to symmetrically encrypt the data of
   * a digital envelope and the secret key used to generate an MAC for
   * checking the integrity of the symmetric encryption. The public key used
   * to perform the asymmetric encryption of the secret key concatenation is
   * also included.
   *
   * @class SecretKeyEncryption
   * @param encryptedSecretKeyConcatBase64
   *            {string} the asymmetric encryption of the secret key
   *            concatenation, using the public key provided as input, as a
   *            string in Base64 encoded format.
   * @param publicKeyPem
   *            {string} the public key, of type
   *            {@link java.security.PublicKey}, used to asymmetrically
   *            encrypt the secret key concatenation.
   * @memberof digitalenvelope
   */
  function SecretKeyEncryption(encryptedSecretKeyConcatBase64, publicKeyPem) {
    /**
     * @function getEncryptedSecretKeyConcatenationBase64
     * @return {string} the asymmetrically encrypted secret key
     *         concatenation, as a string in Base64 encoded format.
     */
    var getEncryptedSecretKeyConcatenation = function() {

      return encryptedSecretKeyConcatBase64;
    };

    /**
     * @function getPublicKey
     * @return {string} the public key used to asymmetrically encrypt the
     *         secret key concatenation, as a string in PEM format.
     */
    var getPublicKey = function() {

      return publicKeyPem;
    };

    return {
      getEncryptedSecretKeyConcatenation: getEncryptedSecretKeyConcatenation,
      getPublicKey: getPublicKey
    };
  }

  /**
   * Deserializes a digital envelope.
   *
   * @function deserialize
   * @param serializedDigitalEnvelope
   *            {string} the serialized digital envelope.
   * @return {DigitalEnvelope} the digital envelope after deserialization.
   * @memberof digitalenvelope
   */
  box.digitalenvelope.deserialize = function(serializedDigitalEnvelope) {

    validateDeserializerInput(serializedDigitalEnvelope);

    var digitalEnvelopeJson =
        JSON.parse(serializedDigitalEnvelope).digitalEnvelope;

    var encryptedDataBase64 = digitalEnvelopeJson.encryptedDataBase64;
    var macBase64 = digitalEnvelopeJson.macBase64;
    var encryptedSecretKeyConcatsBase64 =
        digitalEnvelopeJson.encryptedSecretKeyConcatsBase64;
    var publicKeysPem = digitalEnvelopeJson.publicKeysPem;

    var secretKeyEncryptions = [];
    for (var i = 0; i < encryptedSecretKeyConcatsBase64.length; i++) {
      secretKeyEncryptions.push(new SecretKeyEncryption(
          encryptedSecretKeyConcatsBase64[i], publicKeysPem[i]));
    }

    return new DigitalEnvelope(
        encryptedDataBase64, macBase64, secretKeyEncryptions);
  };

  function validateGeneratorInput(dataBase64, publicKeysPem) {
    if (typeof dataBase64 === 'undefined') {
      throw new exceptions.CryptoLibException(
          'Data provided to digital envelope generator is undefined.');
    }

    if (converters.base64Decode(dataBase64) === '') {
      throw new exceptions.CryptoLibException(
          'Data provided to digital envelope generator is empty.');
    }

    if (typeof dataBase64 !== 'string') {
      throw new exceptions.CryptoLibException(
          'Base64 encoded data provided to digital envelope generator is not a String.');
    }

    if (typeof publicKeysPem === 'undefined') {
      throw new exceptions.CryptoLibException(
          'Public key array provided to digital envelope generator is undefined.');
    }

    if (!(publicKeysPem instanceof Array)) {
      throw new exceptions.CryptoLibException(
          'Public key array provided to digital envelope generator is not of type Array.');
    }

    if (publicKeysPem.length === 0) {
      throw new exceptions.CryptoLibException(
          'Public key array provided to digital envelope generator is empty.');
    }
  }

  function validateOpenerInput(digitalEnvelope, privateKeyPem) {
    if (typeof digitalEnvelope === 'undefined') {
      throw new exceptions.CryptoLibException(
          'Digital envelope provided to digital envelope opener is undefined.');
    }

    if (!(digitalEnvelope instanceof Object)) {
      throw new exceptions.CryptoLibException(
          'Digital envelope provided to digital envelope generator is not of type Object.');
    }

    if (typeof privateKeyPem === 'undefined') {
      throw new exceptions.CryptoLibException(
          'Private key provided to digital envelope opener is undefined.');
    }

    if (typeof privateKeyPem !== 'string') {
      throw new exceptions.CryptoLibException(
          'Private key PEM provided to digital envelope opener is not of type String.');
    }
  }

  function validateDeserializerInput(serializedDigitalEnvelope) {
    if (typeof serializedDigitalEnvelope === 'undefined') {
      throw new exceptions.CryptoLibException(
          'Serialized object provided to digital envelope deserializer is undefined.');
    }

    if (typeof serializedDigitalEnvelope !== 'string') {
      throw new exceptions.CryptoLibException(
          'Serialized object provided to digital envelope deserializer is not of type String.');
    }

    if (serializedDigitalEnvelope.length === 0) {
      throw new exceptions.CryptoLibException(
          'Serialized object provided to digital envelope deserializer is empty.');
    }
  }
};
