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

/**
 * Defines Zero-Knowledge Proofs (ZKPs).
 */
cryptolib.modules.proofs.implementation = function(box) {
  'use strict';

  box.proofs = box.proofs || {};

  var exceptions, mathematical, utils, converters, secureRandomFactory,
      messageDigestFactory;

  var f = function(box) {
    exceptions = box.commons.exceptions;
    mathematical = box.commons.mathematical;
    utils = box.commons.utils;
    converters = new box.commons.utils.Converters();
    secureRandomFactory =
        new box.primitives.securerandom.factory.SecureRandomFactory();
    messageDigestFactory =
        new box.primitives.messagedigest.factory.MessageDigestFactory();
  };

  f.policies = {
    primitives: {
      messagedigest: {
        algorithm: box.policies.proofs.messagedigest.algorithm,
        provider: box.policies.proofs.messagedigest.provider
      },
      secureRandom: {provider: box.policies.proofs.secureRandom.provider}
    }
  };

  cryptolib(
      'commons.exceptions', 'commons.mathematical', 'primitives.messagedigest',
      'commons.utils', 'primitives.securerandom', f);

  /**
   * Schnorr proof generator.
   */
  box.proofs.SchnorrProofGenerator = function(voterID, electionID, group) {

    checkNotEmpty('voter ID', voterID);
    checkNotEmpty('election ID', electionID);
    checkDefined('group', group);

    var computationRules = [[[1, 1]]];
    var _voterID = voterID;
    var _electionID = electionID;
    var _progressMeter;
    var _numOutputs = 1;

    var buildListOfBaseElements = function() {

      var baseElements = [];
      baseElements.push(group.getGenerator());
      return baseElements;
    };

    var phiFunctionSchnorr = new box.proofs.PhiFunction(
        group, 1, _numOutputs, buildListOfBaseElements(), computationRules);
    var _prover = new box.proofs.Prover(group, phiFunctionSchnorr);

    this._buildListPublicValues = function(exponentiatedElement) {
      var publicList = [];
      publicList.push(exponentiatedElement);
      return publicList;
    };

    this._getProver = function() {
      return _prover;
    };

    this._buildListExponents = function(witness) {

      var exponentList = [];
      exponentList.push(witness);

      return exponentList;
    };

    this._buildDataString = function() {

      return 'SchnorrProof:VoterID=' + _voterID +
          'ElectionEventID=' + _electionID;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numOutputs, progressCallback, progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.SchnorrProofGenerator.prototype = {

    preCompute: function() {
      return this._getProver().preCompute(1, this._getProgressMeter());
    },

    generate: function(exponentiatedElement, witness, preComputedValues) {
      checkDefined('exponentiated element', exponentiatedElement);
      checkDefined('witness', witness);

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      return this._getProver().prove(
          this._buildListPublicValues(exponentiatedElement),
          this._buildListExponents(witness), this._buildDataString(),
          preComputedValues);
    }
  };

  /**
   * Exponentiation proof generator.
   */
  box.proofs.ExponentiationProofGenerator = function(baseElements, group) {

    checkDefined('base elements', baseElements);
    checkDefined('group', group);

    var buildComputationRules = function(k) {

      var rules = [];

      for (var i = 0; i < k; i++) {
        rules[i] = [];
        rules[i][0] = [];
        rules[i][0].push(i + 1);
        rules[i][0].push(1);
      }

      return rules;
    };

    var _numOutputs = baseElements.length;
    var phiFunctionExponentiation = new box.proofs.PhiFunction(
        group, 1, _numOutputs, baseElements,
        buildComputationRules(_numOutputs));

    var _prover = new box.proofs.Prover(group, phiFunctionExponentiation);

    var _progressMeter;

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numOutputs, progressCallback, progressPercentMinCheckInterval);
      return this;
    };

    this.check = function(exponentiatedElements) {
      if (exponentiatedElements.length !== _numOutputs) {
        throw new exceptions.CryptoLibException(
            'The number of exponentiated elements must be equal to the number of base elements');
      }
    };

    this._getProver = function() {
      return _prover;
    };
  };

  box.proofs.ExponentiationProofGenerator.prototype = {

    preCompute: function() {
      var EXPONENT_QUANTITY = 1;
      return this._getProver().preCompute(
          EXPONENT_QUANTITY, this._getProgressMeter());
    },

    generate: function(exponentiatedElements, exponent, preComputedValues) {

      checkDefined('exponentiatedElements', exponentiatedElements);
      checkDefined('exponent', exponent);
      this.check(exponentiatedElements);

      var exponents = [];
      exponents.push(exponent);

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      return this._getProver().prove(
          exponentiatedElements, exponents,
          box.EXPONENTIATION_PROOF_AUXILIARY_DATA, preComputedValues);
    }
  };

  /**
   * Plaintext proof generator.
   */
  box.proofs.PlaintextProofGenerator = function(publicKey, group) {

    checkDefined('public key', publicKey);
    checkDefined('group', group);

    var buildListOfBaseElements = function() {

      var list = [];
      list.push(group.getGenerator());
      list.addAll(publicKey.getGroupElementsArray());
      return list;
    };

    var buildComputationRules = function(k) {

      var rules = [];

      for (var i = 0; i < (k + 1); i++) {
        rules[i] = [];
        rules[i][0] = [];
        rules[i][0].push(i + 1);
        rules[i][0].push(1);
      }

      return rules;
    };

    var _publicKeyLength = publicKey.getGroupElementsArray().length;

    var numOutputs = _publicKeyLength + 1;
    var phiFunctionPlaintext = new box.proofs.PhiFunction(
        group, 1, numOutputs, buildListOfBaseElements(),
        buildComputationRules(_publicKeyLength));

    var _prover = new box.proofs.Prover(group, phiFunctionPlaintext);
    var _progressMeter;

    this._buildListPublicValues = function(ciphertext, plaintext) {

      var dividedCiphertext =
          mathematical.groupUtils.divide(ciphertext.getPhis(), plaintext);

      var publicValues = [];
      publicValues.push(ciphertext.getGamma());
      publicValues.addAll(dividedCiphertext);
      return publicValues;
    };

    this._getPublicKeyLength = function() {
      return _publicKeyLength;
    };

    this._getProver = function() {
      return _prover;
    };
    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _publicKeyLength + 1, progressCallback,
          progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.PlaintextProofGenerator.prototype = {

    preCompute: function() {
      return this._getProver().preCompute(1, this._getProgressMeter());
    },

    generate: function(ciphertext, plaintext, witness, preComputedValues) {

      checkDefined('ciphertext', ciphertext);
      checkDefined('plaintext', plaintext);
      checkDefined('witness', witness);

      var publicKeyLength = this._getPublicKeyLength();
      var numPhis = ciphertext.getPhis().length;

      if (publicKeyLength !== numPhis) {
        throw new exceptions.CryptoLibException(
            'The length of the ciphertext must be equal to the length of the public key + 1.');
      }

      if (publicKeyLength !== plaintext.length) {
        throw new exceptions.CryptoLibException(
            'The public key and the plaintext must have the same number of elements');
      }

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      var exponents = [];
      exponents.push(witness);

      return this._getProver().prove(
          this._buildListPublicValues(ciphertext, plaintext), exponents,
          box.PLAINTEXT_PROOF_AUXILIARY_DATA, preComputedValues);
    }
  };

  /**
   * Simple plaintext equality proof generator.
   */
  box.proofs.SimplePlaintextEqualityProofGenerator = function(
      primaryPublicKey, secondaryPublicKey, group) {

    checkDefined('primary public key', primaryPublicKey);
    checkDefined('secondary public key', secondaryPublicKey);
    checkDefined('group', group);

    var _primaryPublicKey = primaryPublicKey;
    var _secondaryPublicKey = secondaryPublicKey;
    var _group = group;

    var buildComputationRules = function(k) {

      var rules = [];
      var firstDimensionLength = k + 1;

      rules[0] = [];
      rules[0][0] = [];
      rules[0][0].push(1);
      rules[0][0].push(1);

      for (var i = 1; i < firstDimensionLength; i++) {
        rules[i] = [];

        rules[i][0] = [];
        rules[i][1] = [];

        rules[i][0].push(i + 1);
        rules[i][0].push(1);
        rules[i][1].push(i + 1 + k);
        rules[i][1].push(1);
      }

      return rules;
    };

    var buildSecondaryInvertedPublicKey = function() {

      var listInvertedElements = [];
      var keys = _secondaryPublicKey.getGroupElementsArray();

      for (var i = 0; i < keys.length; i++) {
        listInvertedElements.push(keys[i].invert());
      }

      return listInvertedElements;
    };

    var buildListOfBaseElements = function() {

      var invertedSecondaryKeyValues = buildSecondaryInvertedPublicKey();

      var baseElements = [];
      baseElements.push(group.getGenerator());
      baseElements.addAll(_primaryPublicKey.getGroupElementsArray());
      baseElements.addAll(invertedSecondaryKeyValues);
      return baseElements;
    };

    var primaryPublicKeyLength =
        primaryPublicKey.getGroupElementsArray().length;
    var secondaryPublicKeyLength =
        secondaryPublicKey.getGroupElementsArray().length;

    if (primaryPublicKeyLength !== secondaryPublicKeyLength) {
      throw new exceptions.CryptoLibException(
          'The number of elements in the primary and secondary public keys must be equal');
    }

    var _progressMeter;
    var _numKeyElements = primaryPublicKey.getGroupElementsArray().length;
    var numOutputs = _numKeyElements + 1;

    var rules = buildComputationRules(_numKeyElements);

    var phiFunctionSimplePlaintextEquality = new box.proofs.PhiFunction(
        group, 1, numOutputs, buildListOfBaseElements(), rules);

    var _prover =
        new box.proofs.Prover(_group, phiFunctionSimplePlaintextEquality);

    this._buildListExponents = function(witness) {

      var exponentList = [];
      exponentList.push(witness);
      return exponentList;
    };

    this._buildInputList = function(primaryCiphertext, secondaryCiphertext) {

      var dividedSubCiphertext = mathematical.groupUtils.divide(
          primaryCiphertext.getPhis(), secondaryCiphertext.getPhis());

      var generatedList = [];
      generatedList.push(secondaryCiphertext.getGamma());
      generatedList.addAll(dividedSubCiphertext);
      return generatedList;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numKeyElements * 2 + 1, progressCallback,
          progressPercentMinCheckInterval);
      return this;
    };

    this._getProver = function() {
      return _prover;
    };

    this._check = function(primaryCiphertextLength) {
      if (primaryCiphertextLength !== (primaryPublicKeyLength + 1)) {
        throw new exceptions.CryptoLibException(
            'The number of elements in the ciphertext must be equal to the number in the public key + 1');
      }
    };
  };

  box.proofs.SimplePlaintextEqualityProofGenerator.prototype = {

    preCompute: function() {
      return this._getProver().preCompute(1, this._getProgressMeter());
    },

    generate: function(
        primaryCiphertext, secondaryCiphertext, witness, preComputedValues) {

      checkDefined('primary ciphertext', primaryCiphertext);
      checkDefined('secondary ciphertext', secondaryCiphertext);
      checkDefined('witness', witness);

      var primaryCiphertextLength = primaryCiphertext.getPhis().length + 1;
      var secondaryCiphertextLength = secondaryCiphertext.getPhis().length + 1;

      if (primaryCiphertextLength !== secondaryCiphertextLength) {
        throw new exceptions.CryptoLibException(
            'The number of elements in the primary and secondary ciphertext must be equal');
      }

      this._check(primaryCiphertextLength);

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      return this._getProver().prove(
          this._buildInputList(primaryCiphertext, secondaryCiphertext),
          this._buildListExponents(witness),
          box.SIMPLE_PLAINTEXT_EQUALITY_PROOF_AUXILIARY_DATA,
          preComputedValues);
    }
  };

  /**
   * Plaintext equality proof generator.
   */
  box.proofs.PlaintextEqualityProofGenerator = function(
      primaryPublicKey, secondaryPublicKey, group) {

    checkDefined('primary public key', primaryPublicKey);
    checkDefined('secondary public key', secondaryPublicKey);
    checkDefined('group', group);

    var numPrimaryPublicKeyElements =
        primaryPublicKey.getGroupElementsArray().length;
    var numSecondaryPublicKeyElements =
        secondaryPublicKey.getGroupElementsArray().length;

    if (numPrimaryPublicKeyElements !== numSecondaryPublicKeyElements) {
      throw new exceptions.CryptoLibException(
          'The primary and secondary public keys contain different numbers of elements: ' +
          numPrimaryPublicKeyElements + ', ' + numSecondaryPublicKeyElements);
    }

    var _primaryPublicKey = primaryPublicKey;
    var _secondaryPublicKey = secondaryPublicKey;
    var _group = group;

    var _progressMeter;

    this._buildListExponents = function(primaryWitness, secondaryWitness) {

      var exponentList = [];
      exponentList.push(primaryWitness);
      exponentList.push(secondaryWitness);
      return exponentList;
    };

    this._buildInputList = function(primaryCiphertext, secondaryCiphertext) {

      var dividedSubCiphertext = mathematical.groupUtils.divide(
          primaryCiphertext.getPhis(), secondaryCiphertext.getPhis());

      var generatedList = [];
      generatedList.push(primaryCiphertext.getGamma());
      generatedList.push(secondaryCiphertext.getGamma());
      generatedList.addAll(dividedSubCiphertext);
      return generatedList;
    };

    function buildComputationRules(k) {
      var firstDimensionLength = k + 2;

      var rules = [];

      rules[0] = [];
      rules[0][0] = [];
      rules[0][0].push(1);
      rules[0][0].push(1);

      rules[1] = [];
      rules[1][0] = [];
      rules[1][0].push(1);
      rules[1][0].push(2);

      for (var i = 2; i < firstDimensionLength; i++) {
        rules[i] = [];

        rules[i][0] = [];
        rules[i][0].push(i);
        rules[i][0].push(1);

        rules[i][1] = [];
        rules[i][1].push(i + k);
        rules[i][1].push(2);
      }

      return rules;
    }

    function buildListOfBaseElements() {
      var invertedSecondaryKeyValues = buildSecondaryInvertedPublicKey();

      var baseElements = [];
      baseElements.push(group.getGenerator());
      baseElements.addAll(_primaryPublicKey.getGroupElementsArray());
      baseElements.addAll(invertedSecondaryKeyValues);
      return baseElements;
    }

    function buildSecondaryInvertedPublicKey() {
      var listInvertedElements = [];
      var keys = _secondaryPublicKey.getGroupElementsArray();

      for (var i = 0; i < keys.length; i++) {
        listInvertedElements.push(keys[i].invert());
      }

      return listInvertedElements;
    }

    var numKeyElements = primaryPublicKey.getGroupElementsArray().length;
    var _numOutputs = numKeyElements + 2;

    var rules = buildComputationRules(numKeyElements);

    var phiFunctionPlaintextEquality = new box.proofs.PhiFunction(
        group, 2, _numOutputs, buildListOfBaseElements(), rules);

    var _prover = new box.proofs.Prover(group, phiFunctionPlaintextEquality);

    this.getPrimaryPublicKey = function() {
      return _primaryPublicKey;
    };

    this.getSecondaryPublicKey = function() {
      return _secondaryPublicKey;
    };

    this.getGroup = function() {
      return _group;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          numKeyElements * 2 + 2, progressCallback,
          progressPercentMinCheckInterval);
      return this;
    };

    this._getProver = function() {
      return _prover;
    };

    this.check = function(numPrimaryCiphertextElements) {
      if (numPrimaryCiphertextElements !== (numPrimaryPublicKeyElements + 1)) {
        throw new exceptions.CryptoLibException(
            'The number of ciphertext elements is not equal to the number public key elements plus one.');
      }
    };
  };

  box.proofs.PlaintextEqualityProofGenerator.prototype = {

    preCompute: function() {
      return this._getProver().preCompute(2, this._getProgressMeter());
    },

    generate: function(
        primaryCiphertext, primaryWitness, secondaryCiphertext,
        secondaryWitness, preComputedValues) {

      checkDefined('primary ciphertext', primaryCiphertext);
      checkDefined('primary witness', primaryWitness);
      checkDefined('secondary ciphertext', secondaryCiphertext);
      checkDefined('secondary witness', secondaryWitness);

      var numPrimaryCiphertextElements = primaryCiphertext.getPhis().length + 1;
      var numSecondaryCiphertextElements =
          secondaryCiphertext.getPhis().length + 1;

      if (numPrimaryCiphertextElements !== numSecondaryCiphertextElements) {
        throw new exceptions.CryptoLibException(
            'The primary and secondary ciphertexts contain different numbers of elements: ' +
            numPrimaryCiphertextElements + ', ' +
            numSecondaryCiphertextElements);
      }
      this.check(numPrimaryCiphertextElements);

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      return this._getProver().prove(
          this._buildInputList(primaryCiphertext, secondaryCiphertext),
          this._buildListExponents(primaryWitness, secondaryWitness),
          box.PLAINTEXT_EQUALITY_PROOF_AUXILIARY_DATA, preComputedValues);
    }
  };

  /**
   * Plaintext exponent equality proof generator.
   */
  box.proofs.PlaintextExponentEqualityProofGenerator = function(
      baseElements, group) {

    var buildComputationRules = function(k) {

      var rules = [];
      var firstDimensionLength = k + 1;

      rules[0] = [];
      rules[0][0] = [];
      rules[0][0].push(1);
      rules[0][0].push(1);

      for (var i = 1; i < firstDimensionLength; i++) {
        rules[i] = [];

        rules[i][0] = [];
        rules[i][1] = [];

        rules[i][0].push(i + 1);
        rules[i][0].push(1);
        rules[i][1].push(i + 1 + k);
        rules[i][1].push(2);
      }

      return rules;
    };

    checkDefined('base elements', baseElements);
    checkDefined('group', group);

    var _numBaseElements = baseElements.length;

    var _progressMeter;

    var k = (_numBaseElements - 1) / 2;
    var numOutputs = k + 1;

    var phiFunctionPlaintextExponentEquality = new box.proofs.PhiFunction(
        group, 2, numOutputs, baseElements, buildComputationRules(k));

    var _prover =
        new box.proofs.Prover(group, phiFunctionPlaintextExponentEquality);

    this._buildListExponents = function(witness1, witness2) {

      var exponentList = [];
      exponentList.push(witness1);
      exponentList.push(witness2);
      return exponentList;
    };

    this._buildListPublicValues = function(ciphertext) {

      var list = [];
      list.push(ciphertext.getGamma());
      list.addAll(ciphertext.getPhis());
      return list;
    };

    this._getProver = function() {
      return _prover;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numBaseElements, progressCallback, progressPercentMinCheckInterval);
      return this;
    };

    this._checkLengthOf = function(ciphertext) {
      var k = ciphertext.getPhis().length;
      var expectedNumBaseElements = (2 * k) + 1;
      if (_numBaseElements !== expectedNumBaseElements) {
        throw new exceptions.CryptoLibException(
            'The lengths of the ciphertext and the base elements do not have the expected relationship.');
      }
    };
  };

  box.proofs.PlaintextExponentEqualityProofGenerator.prototype = {

    preCompute: function() {
      return this._getProver().preCompute(2, this._getProgressMeter());
    },

    generate: function(ciphertext, witness1, witness2, preComputedValues) {

      checkDefined('ciphertext', ciphertext);
      checkDefined('witness1', witness1);
      checkDefined('witness2', witness2);

      this._checkLengthOf(ciphertext);

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      return this._getProver().prove(
          this._buildListPublicValues(ciphertext),
          this._buildListExponents(witness1, witness2),
          box.PLAINTEXT_EXPONENT_EQUALITY_PROOF_AUXILIARY_DATA,
          preComputedValues);
    }
  };

  /**
   * OR-proof generator.
   */
  box.proofs.ORProofGenerator = function(publicKey, numElements, group) {

    checkPublicKey(publicKey);
    checkNumElements(numElements);
    checkGroup(group);

    var _publicKey = publicKey;
    var _numElements = numElements;
    var _group = group;
    var _numOutputs = _numElements * 2;
    var _progressMeter;

    function checkPublicKey(publicKey) {
      checkDefined('public key', publicKey);
      checkNotNull('public key', publicKey);

      var publicKeyLength = publicKey.getGroupElementsArray().length;
      if (publicKeyLength !== 1) {
        throw new exceptions.CryptoLibException(
            'Expected public key length: 1 ; Found: ' + publicKeyLength);
      }
    }

    function checkNumElements(numElements) {
      checkDefined('numElements', numElements);
      checkNotNull('numElements', numElements);

      if (numElements < 2) {
        throw new exceptions.CryptoLibException(
            'Expected number of elements to be greater than 1 ; Found: ' +
            numElements);
      }
    }

    function checkGroup(group) {
      checkDefined('group', group);
      checkNotNull('group', group);
    }

    function buildBaseElements() {
      var baseElements = [];
      baseElements.push(_group.getGenerator());
      baseElements.push(_publicKey.getGroupElementsArray()[0]);

      return baseElements;
    }

    function buildComputationRules(numOutputs) {
      var rules = [];

      var index1;
      var index2 = 0;
      for (var i = 0; i < numOutputs; i++) {
        index1 = 1 + (i % 2);
        if (i % 2 === 0) {
          index2++;
        }

        rules[i] = [];
        rules[i][0] = [];
        rules[i][0].push(index1);
        rules[i][0].push(index2);
      }

      return rules;
    }

    var phiFunctionOR = new box.proofs.PhiFunction(
        group, _numElements, _numOutputs, buildBaseElements(),
        buildComputationRules(_numOutputs));
    var _prover = new box.proofs.Prover(_group, phiFunctionOR);

    this.getNumElements = function() {
      return _numElements;
    };

    this.getProver = function() {
      return _prover;
    };

    this.buildPublicValues = function(ciphertext) {
      var publicValues = [];
      publicValues.push(ciphertext.getGamma());
      publicValues.push(ciphertext.getPhis()[0]);

      return publicValues;
    };

    this.getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numOutputs, progressCallback, progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.ORProofGenerator.prototype = {

    preCompute: function() {

      return this.getProver().preCompute(
          this.getNumElements(), this.getProgressMeter());
    },

    generate: function(
        ciphertext, witness, elements, index, data, preComputedValues) {

      checkCiphertext(ciphertext);
      checkWitness(witness);
      checkElements(elements, this.getNumElements());
      checkIndex(index, this.getNumElements());
      checkDefined('data', data);

      function checkCiphertext(ciphertext) {
        checkDefined('ciphertext', ciphertext);
        checkNotNull('ciphertext', ciphertext);

        var ciphertextLength = ciphertext.getPhis().length + 1;
        if (ciphertextLength !== 2) {
          throw new exceptions.CryptoLibException(
              'Expected ciphertext length: 2 ; Found: ' + ciphertextLength);
        }
      }

      function checkWitness(witness) {
        checkDefined('witness', witness);
        checkNotNull('witness', witness);
      }

      function checkElements(elements, numElementsExpected) {
        checkDefined('elements', elements);
        checkNotNull('elements', elements);

        var numElementsFound = elements.length;
        if (numElementsFound !== numElementsExpected) {
          throw new exceptions.CryptoLibException(
              'Expected number of elements: ' + numElementsExpected +
              ' ; Found: ' + numElementsFound);
        }
      }

      function checkIndex(index, numElements) {
        checkDefined('index', index);
        checkNotNull('index', index);

        if (index < 0) {
          throw new exceptions.CryptoLibException(
              'Expected index of encrypted element to be positive ; Found: ' +
              index);
        }

        if (index >= numElements) {
          throw new exceptions.CryptoLibException(
              'Expected index of encrypted element to be less than number of elements: ' +
              numElements + ' ; Found: ' + index);
        }
      }

      var publicValues = this.buildPublicValues(ciphertext);

      var privateValues = [];
      privateValues.push(witness);

      if (data === null) {
        data = '';
      }

      if (!preComputedValues) {
        preComputedValues = this.preCompute();
      }

      return this.getProver().prove(
          publicValues, privateValues, data, preComputedValues, elements,
          index);
    }
  };

  /**
   * Schnorr proof verifier.
   */
  box.proofs.SchnorrProofVerifier = function(
      exponentiatedElement, voterID, electionID, proof, group) {

    checkDefined('exponentiated element', exponentiatedElement);
    checkNotEmpty('voter ID', voterID);
    checkNotEmpty('election ID', electionID);
    checkDefined('proof', proof);
    checkDefined('group', group);

    var _exponentiatedElement = exponentiatedElement;
    var _voterID = voterID;
    var _electionID = electionID;
    var _proof = proof;
    var _group = group;

    var _progressMeter;

    this._buildListOfBaseElements = function() {

      var baseElements = [];
      baseElements.push(_group.getGenerator());
      return baseElements;
    };

    this._buildExponentiatedElementsList = function() {

      var exponentiatedElements = [];
      exponentiatedElements.push(_exponentiatedElement);
      return exponentiatedElements;
    };

    this._buildDataString = function() {
      return 'SchnorrProof:VoterID=' + _voterID +
          'ElectionEventID=' + _electionID;
    };

    var computationRules = [[[1, 1]]];

    var _numOutputs = 1;
    var phiFunction = new box.proofs.PhiFunction(
        _group, 1, _numOutputs, this._buildListOfBaseElements(),
        computationRules);

    var _verifier = new box.proofs.Verifier(_group, phiFunction);

    this._getProof = function() {
      return _proof;
    };

    this._getVerifier = function() {
      return _verifier;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numOutputs, progressCallback, progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.SchnorrProofVerifier.prototype = {

    verify: function() {
      return this._getVerifier().verify(
          this._buildExponentiatedElementsList(), this._getProof(),
          this._buildDataString(), this._getProgressMeter());
    }
  };

  /**
   * Exponentiation proof verifier.
   */
  box.proofs.ExponentiationProofVerifier = function(
      exponentiatedElements, baseElements, proof, group) {

    checkDefined('exponentiated elements', exponentiatedElements);
    checkDefined('base elements', baseElements);
    checkDefined('proof', proof);
    checkDefined('group', group);

    var _exponentiatedElements = exponentiatedElements;
    var _proof = proof;
    var _progressMeter;

    this._buildComputationRules = function(k) {

      var rules = [];

      for (var i = 0; i < k; i++) {
        rules[i] = [];
        rules[i][0] = [];
        rules[i][0].push(i + 1);
        rules[i][0].push(1);
      }

      return rules;
    };

    var _numOutputs = exponentiatedElements.length;

    var phiFunctionExponentiation = new box.proofs.PhiFunction(
        group, 1, _numOutputs, baseElements,
        this._buildComputationRules(_numOutputs));

    var _verifier = new box.proofs.Verifier(group, phiFunctionExponentiation);

    this._getProof = function() {
      return _proof;
    };

    this.getExponentiatedElements = function() {
      return _exponentiatedElements;
    };

    this._getVerifier = function() {
      return _verifier;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numOutputs, progressCallback, progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.ExponentiationProofVerifier.prototype = {

    verify: function() {
      return this._getVerifier().verify(
          this.getExponentiatedElements(), this._getProof(),
          box.EXPONENTIATION_PROOF_AUXILIARY_DATA, this._getProgressMeter());
    }
  };

  /**
   * Plaintext proof verifier.
   */
  box.proofs.PlaintextProofVerifier = function(publicKey, group) {

    checkDefined('public key', publicKey);
    checkDefined('group', group);

    var _publicKey = publicKey;
    var _group = group;

    var _progressMeter;

    this._buildListOfBaseElements = function() {

      var list = [];
      list.push(_group.getGenerator());
      list.addAll(_publicKey.getGroupElementsArray());

      return list;
    };

    this._buildListPublicValues = function(ciphertext, plaintext) {

      var dividedCiphertext =
          mathematical.groupUtils.divide(ciphertext.getPhis(), plaintext);

      var publicValues = [];
      publicValues.push(ciphertext.getGamma());
      publicValues.addAll(dividedCiphertext);
      return publicValues;
    };

    this._buildComputationRules = function(k) {

      var rules = [];

      for (var i = 0; i < (k + 1); i++) {
        rules[i] = [];
        rules[i][0] = [];
        rules[i][0].push(i + 1);
        rules[i][0].push(1);
      }

      return rules;
    };

    var _publicKeyLength = _publicKey.getGroupElementsArray().length;
    var numOutputs = _publicKeyLength + 1;

    var phiFunctionPlaintext = new box.proofs.PhiFunction(
        _group, 1, numOutputs, this._buildListOfBaseElements(),
        this._buildComputationRules(_publicKeyLength));

    var _verifier = new box.proofs.Verifier(_group, phiFunctionPlaintext);

    this.getPublicKey = function() {
      return _publicKey;
    };

    this.getGroup = function() {
      return _group;
    };

    this._getVerifier = function() {
      return _verifier;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _publicKeyLength + 1, progressCallback,
          progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.PlaintextProofVerifier.prototype = {

    verify: function(ciphertext, plaintext, proof) {

      checkDefined('ciphertext', ciphertext);
      checkDefined('plaintext', plaintext);
      checkDefined('proof', proof);

      return this._getVerifier().verify(
          this._buildListPublicValues(ciphertext, plaintext), proof,
          box.PLAINTEXT_PROOF_AUXILIARY_DATA, this._getProgressMeter());
    }
  };

  /**
   * Simple plaintext equality proof verifier.
   */
  box.proofs.SimplePlaintextEqualityProofVerifier = function(
      primaryCiphertext, primaryPublicKey, secondaryCiphertext,
      secondaryPublicKey, proof, group) {

    checkDefined('primary ciphertext', primaryCiphertext);
    checkDefined('primary public key', primaryPublicKey);
    checkDefined('secondary ciphertext', secondaryCiphertext);
    checkDefined('secondary public key', secondaryPublicKey);
    checkDefined('proof', proof);
    checkDefined('group', group);

    var numPrimaryCiphertextElements = primaryCiphertext.getPhis().length + 1;
    var numPrimaryPublicKeyElements =
        primaryPublicKey.getGroupElementsArray().length;
    var numSecondaryCiphertextElements =
        secondaryCiphertext.getPhis().length + 1;
    var numSecondaryPublicKeyElements =
        secondaryPublicKey.getGroupElementsArray().length;

    if (numPrimaryCiphertextElements !== numSecondaryCiphertextElements) {
      throw new exceptions.CryptoLibException(
          'The primary and secondary ciphertexts contain different numbers of elements: ' +
          numPrimaryCiphertextElements + ', ' + numSecondaryCiphertextElements);
    }

    if (numPrimaryPublicKeyElements !== numSecondaryPublicKeyElements) {
      throw new exceptions.CryptoLibException(
          'The primary and secondary public keys contain different numbers of elements: ' +
          numPrimaryPublicKeyElements + ', ' + numSecondaryPublicKeyElements);
    }

    if (numPrimaryCiphertextElements !== (numPrimaryPublicKeyElements + 1)) {
      throw new exceptions.CryptoLibException(
          'The number of ciphertext elements is not equal to the number public key elements plus one.');
    }

    var _primaryCiphertext = primaryCiphertext;
    var _primaryPublicKey = primaryPublicKey;
    var _secondaryCiphertext = secondaryCiphertext;
    var _secondaryPublicKey = secondaryPublicKey;
    var _proof = proof;

    var _progressMeter;

    function buildComputationRules(k) {
      var rules = [];
      var firstDimensionLength = k + 1;

      rules[0] = [];
      rules[0][0] = [];
      rules[0][0].push(1);
      rules[0][0].push(1);

      for (var i = 1; i < firstDimensionLength; i++) {
        rules[i] = [];

        rules[i][0] = [];
        rules[i][1] = [];

        rules[i][0].push(i + 1);
        rules[i][0].push(1);
        rules[i][1].push(i + 1 + k);
        rules[i][1].push(1);
      }

      return rules;
    }

    function buildListOfBaseElements() {
      var invertedSecondaryKeyValues = buildSecondaryInvertedPublicKey();

      var baseElements = [];
      baseElements.push(group.getGenerator());
      baseElements.addAll(_primaryPublicKey.getGroupElementsArray());
      baseElements.addAll(invertedSecondaryKeyValues);
      return baseElements;
    }

    function buildSecondaryInvertedPublicKey() {
      var listInvertedElements = [];
      var keys = _secondaryPublicKey.getGroupElementsArray();

      for (var i = 0; i < keys.length; i++) {
        listInvertedElements.push(keys[i].invert());
      }

      return listInvertedElements;
    }

    var _primarySubCiphertext = _primaryCiphertext.getPhis();
    var _secondarySubCiphertext = _secondaryCiphertext.getPhis();

    var _numKeyElements = _primaryPublicKey.getGroupElementsArray().length;
    var numOutputs = _numKeyElements + 1;

    var phiFunctionSimplePlaintextEquality = new box.proofs.PhiFunction(
        group, 1, numOutputs, buildListOfBaseElements(),
        buildComputationRules(_numKeyElements));

    var _verifier =
        new box.proofs.Verifier(group, phiFunctionSimplePlaintextEquality);

    this._buildInputList = function() {

      var dividedSubCiphertext = mathematical.groupUtils.divide(
          _primarySubCiphertext, _secondarySubCiphertext);

      var generatedList = [];
      generatedList.push(_primaryCiphertext.getGamma());
      generatedList.addAll(dividedSubCiphertext);
      return generatedList;
    };

    this._getProof = function() {
      return _proof;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numKeyElements * 2 + 1, progressCallback,
          progressPercentMinCheckInterval);
      return this;
    };

    this._getVerifier = function() {
      return _verifier;
    };
  };

  box.proofs.SimplePlaintextEqualityProofVerifier.prototype = {

    verify: function() {
      return this._getVerifier().verify(
          this._buildInputList(), this._getProof(),
          box.SIMPLE_PLAINTEXT_EQUALITY_PROOF_AUXILIARY_DATA,
          this._getProgressMeter());
    }
  };

  /**
   * Plaintext equality proof verifier.
   */
  box.proofs.PlaintextEqualityProofVerifier = function(
      primaryCiphertext, primaryPublicKey, secondaryCiphertext,
      secondaryPublicKey, proof, group) {

    checkDefined('primary ciphertext', primaryCiphertext);
    checkDefined('primary public key', primaryPublicKey);
    checkDefined('secondary ciphertext', secondaryCiphertext);
    checkDefined('secondary public key', secondaryPublicKey);
    checkDefined('proof', proof);
    checkDefined('group', group);

    var _primaryCiphertext = primaryCiphertext;
    var _primaryPublicKey = primaryPublicKey;
    var _secondaryCiphertext = secondaryCiphertext;
    var _secondaryPublicKey = secondaryPublicKey;
    var _proof = proof;

    var buildComputationRules = function(k) {

      var firstDimensionLength = k + 2;

      var rules = [];

      rules[0] = [];
      rules[0][0] = [];
      rules[0][0].push(1);
      rules[0][0].push(1);

      rules[1] = [];
      rules[1][0] = [];
      rules[1][0].push(1);
      rules[1][0].push(2);

      for (var i = 2; i < firstDimensionLength; i++) {
        rules[i] = [];

        rules[i][0] = [];
        rules[i][1] = [];

        rules[i][0].push(i);
        rules[i][0].push(1);
        rules[i][1].push(i + k);
        rules[i][1].push(2);
      }

      return rules;
    };

    function buildListOfBaseElements() {
      var invertedSecondaryKeyValues = buildSecondaryInvertedPublicKey();

      var baseElements = [];
      baseElements.push(group.getGenerator());
      baseElements.addAll(_primaryPublicKey.getGroupElementsArray());
      baseElements.addAll(invertedSecondaryKeyValues);
      return baseElements;
    }

    function buildSecondaryInvertedPublicKey() {
      var listInvertedElements = [];
      var keys = _secondaryPublicKey.getGroupElementsArray();

      for (var i = 0; i < keys.length; i++) {
        listInvertedElements.push(keys[i].invert());
      }

      return listInvertedElements;
    }

    var _progressMeter;

    var _primarySubCiphertext = _primaryCiphertext.getPhis();
    var _secondarySubCiphertext = _secondaryCiphertext.getPhis();

    var _numKeyElements = primaryPublicKey.getGroupElementsArray().length;
    var numOutputs = _numKeyElements + 2;

    var phiFunctionPlaintextEquality = new box.proofs.PhiFunction(
        group, 2, numOutputs, buildListOfBaseElements(),
        buildComputationRules(_numKeyElements));

    var _verifier =
        new box.proofs.Verifier(group, phiFunctionPlaintextEquality);

    this._buildInputList = function() {

      var dividedSubCiphertext = mathematical.groupUtils.divide(
          _primarySubCiphertext, _secondarySubCiphertext);

      var generatedList = [];
      generatedList.push(_primaryCiphertext.getGamma());
      generatedList.push(_secondaryCiphertext.getGamma());
      generatedList.addAll(dividedSubCiphertext);

      return generatedList;
    };

    this._getProof = function() {
      return _proof;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numKeyElements * 2 + 2, progressCallback,
          progressPercentMinCheckInterval);
      return this;
    };

    this._getVerifier = function() {
      return _verifier;
    };
  };

  box.proofs.PlaintextEqualityProofVerifier.prototype = {

    verify: function() {
      return this._getVerifier().verify(
          this._buildInputList(), this._getProof(),
          box.PLAINTEXT_EQUALITY_PROOF_AUXILIARY_DATA,
          this._getProgressMeter());
    }
  };

  /**
   * Plaintext exponent equality proof verifier.
   */
  box.proofs.PlaintextExponentEqualityProofVerifier = function(
      ciphertext, baseElements, proof, group) {

    checkDefined('ciphertext', ciphertext);
    checkDefined('base elements', baseElements);
    checkDefined('proof', proof);
    checkDefined('group', group);

    var _ciphertext = ciphertext;
    var _baseElements = baseElements;
    var _proof = proof;
    var _group = group;

    var _progressMeter;

    this._buildComputationRules = function(k) {

      var rules = [];
      var firstDimensionLength = k + 1;

      rules[0] = [];
      rules[0][0] = [];
      rules[0][0].push(1);
      rules[0][0].push(1);

      for (var i = 1; i < firstDimensionLength; i++) {
        rules[i] = [];

        rules[i][0] = [];
        rules[i][1] = [];

        rules[i][0].push(i + 1);
        rules[i][0].push(1);
        rules[i][1].push(i + 1 + k);
        rules[i][1].push(2);
      }

      return rules;
    };

    this._buildListPublicValues = function() {

      var list = [];
      list.push(_ciphertext.getGamma());
      list.addAll(_ciphertext.getPhis());
      return list;
    };

    var _k = _ciphertext.getPhis().length;
    var numOutputs = _k + 1;

    var phiFunctionPlaintextExponentEquality = new box.proofs.PhiFunction(
        _group, 2, numOutputs, _baseElements, this._buildComputationRules(_k));

    var _verifier =
        new box.proofs.Verifier(_group, phiFunctionPlaintextExponentEquality);

    this._getProof = function() {
      return _proof;
    };

    this._getVerifier = function() {
      return _verifier;
    };

    this._getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _k * 2 + 1, progressCallback, progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.PlaintextExponentEqualityProofVerifier.prototype = {

    verify: function() {
      return this._getVerifier().verify(
          this._buildListPublicValues(), this._getProof(),
          box.PLAINTEXT_EXPONENT_EQUALITY_PROOF_AUXILIARY_DATA,
          this._getProgressMeter());
    }
  };

  /**
   * OR-proof verifier.
   */
  box.proofs.ORProofVerifier = function(publicKey, numElements, group) {

    checkPublicKey(publicKey);
    checkNumElements(numElements);
    checkGroup(group);

    var _publicKey = publicKey;
    var _numElements = numElements;
    var _group = group;
    var _numOutputs = _numElements * 2;
    var _progressMeter;

    function checkPublicKey(publicKey) {
      checkDefined('public key', publicKey);
      checkNotNull('public key', publicKey);

      var publicKeyLength = publicKey.getGroupElementsArray().length;
      if (publicKeyLength !== 1) {
        throw new exceptions.CryptoLibException(
            'Expected public key length: 1 ; Found: ' + publicKeyLength);
      }
    }

    function checkNumElements(numElements) {
      checkDefined('numElements', numElements);
      checkNotNull('numElements', numElements);

      if (numElements < 2) {
        throw new exceptions.CryptoLibException(
            'Expected number of elements to be greater than 1 ; Found: ' +
            numElements);
      }
    }

    function checkGroup(group) {
      checkDefined('group', group);
      checkNotNull('group', group);
    }

    function buildBaseElements() {
      var baseElements = [];
      baseElements.push(_group.getGenerator());
      baseElements.push(_publicKey.getGroupElementsArray()[0]);

      return baseElements;
    }

    function buildComputationRules(numOutputs) {
      var rules = [];

      var index1;
      var index2 = 0;
      for (var i = 0; i < numOutputs; i++) {
        index1 = 1 + (i % 2);
        if (i % 2 === 0) {
          index2++;
        }

        rules[i] = [];
        rules[i][0] = [];
        rules[i][0].push(index1);
        rules[i][0].push(index2);
      }

      return rules;
    }

    var phiFunctionOR = new box.proofs.PhiFunction(
        _group, _numElements, _numOutputs, buildBaseElements(),
        buildComputationRules(_numOutputs));
    var _verifier = new box.proofs.Verifier(_group, phiFunctionOR);

    this.buildPublicValues = function(ciphertext) {
      var publicValues = [];
      publicValues.push(ciphertext.getGamma());
      publicValues.push(ciphertext.getPhis()[0]);

      return publicValues;
    };

    this.getNumElements = function() {
      return _numElements;
    };

    this.getVerifier = function() {
      return _verifier;
    };

    this.getProgressMeter = function() {
      return _progressMeter;
    };

    this.initProgressMeter = function(
        progressCallback, progressPercentMinCheckInterval) {
      _progressMeter = createProgressMeter(
          _numOutputs, progressCallback, progressPercentMinCheckInterval);
      return this;
    };
  };

  box.proofs.ORProofVerifier.prototype = {

    verify: function(ciphertext, elements, data, proof) {

      checkCiphertext(ciphertext);
      checkElements(elements, this.getNumElements());
      checkDefined('data', data);
      checkProof(proof);

      function checkCiphertext(ciphertext) {
        checkDefined('ciphertext', ciphertext);
        checkNotNull('ciphertext', ciphertext);

        var ciphertextLength = ciphertext.getPhis().length + 1;
        if (ciphertextLength !== 2) {
          throw new exceptions.CryptoLibException(
              'Expected ciphertext length: 2 ; Found: ' + ciphertextLength);
        }
      }

      function checkElements(elements, numElementsExpected) {
        checkDefined('elements', elements);
        checkNotNull('elements', elements);

        var numElementsFound = elements.length;
        if (numElementsFound !== numElementsExpected) {
          throw new exceptions.CryptoLibException(
              'Expected number of elements: ' + numElementsExpected +
              ' ; Found: ' + numElementsFound);
        }
      }

      function checkProof(proof) {
        checkDefined('proof', proof);
        checkNotNull('proof', proof);
      }

      if (data === null) {
        data = '';
      }

      return this.getVerifier().verify(
          this.buildPublicValues(ciphertext), proof, data, elements,
          this.getProgressMeter());
    }
  };

  /**
   * Proofs factory, can be used for creating proof generators and verifiers.
   */
  box.proofs.ProofsFactory = function() {

  };

  box.proofs.ProofsFactory.prototype = {

    createSchnorrProofGenerator: function(voterID, electionID, group) {
      return new box.proofs.SchnorrProofGenerator(voterID, electionID, group);
    },

    createExponentiationProofGenerator: function(baseElements, group) {
      return new box.proofs.ExponentiationProofGenerator(baseElements, group);
    },

    createPlaintextProofGenerator: function(publicKey, group) {
      return new box.proofs.PlaintextProofGenerator(publicKey, group);
    },

    createSimplePlaintextEqualityProofGenerator: function(
        primaryPublicKey, secondaryPublicKey, group) {
      return new box.proofs.SimplePlaintextEqualityProofGenerator(
          primaryPublicKey, secondaryPublicKey, group);
    },

    createPlaintextEqualityProofGenerator: function(
        primaryPublicKey, secondaryPublicKey, group) {
      return new box.proofs.PlaintextEqualityProofGenerator(
          primaryPublicKey, secondaryPublicKey, group);
    },

    createPlaintextExponentEqualityProofGenerator: function(
        baseElements, group) {
      return new box.proofs.PlaintextExponentEqualityProofGenerator(
          baseElements, group);
    },

    createORProofGenerator: function(publicKey, numElements, group) {
      return new box.proofs.ORProofGenerator(publicKey, numElements, group);
    },

    createSchnorrProofVerifier: function(
        element, voterID, electionEventID, proof, group) {
      return new box.proofs.SchnorrProofVerifier(
          element, voterID, electionEventID, proof, group);
    },

    createExponentiationProofVerifier: function(
        exponentiatedElements, baseElements, proof, group) {
      return new box.proofs.ExponentiationProofVerifier(
          exponentiatedElements, baseElements, proof, group);
    },

    createPlaintextProofVerifier: function(
        publicKey, ciphertext, plaintext, proof, group) {
      return new box.proofs.PlaintextProofVerifier(
          publicKey, ciphertext, plaintext, proof, group);
    },

    createSimplePlaintextEqualityProofVerifier: function(
        primaryCiphertext, primaryPublicKey, secondaryCiphertext,
        secondaryPublicKey, proof, group) {
      return new box.proofs.SimplePlaintextEqualityProofVerifier(
          primaryCiphertext, primaryPublicKey, secondaryCiphertext,
          secondaryPublicKey, proof, group);
    },

    createPlaintextEqualityProofVerifier: function(
        primaryCiphertext, primaryPublicKey, secondaryCiphertext,
        secondaryPublicKey, proof, group) {
      return new box.proofs.PlaintextEqualityProofVerifier(
          primaryCiphertext, primaryPublicKey, secondaryCiphertext,
          secondaryPublicKey, proof, group);
    },

    createPlaintextExponentEqualityProofVerifier: function(
        ciphertext, baseElements, proof, group) {
      return new box.proofs.PlaintextExponentEqualityProofVerifier(
          ciphertext, baseElements, proof, group);
    },

    createORProofVerifier: function(publicKey, numElements, group) {
      return new box.proofs.ORProofVerifier(publicKey, numElements, group);
    }
  };

  /**
   * Represents the PHI function of Maurer's Unified Zero-Knowledge Proof of
   * Knowledge (ZK-PoK) scheme.
   */
  box.proofs.PhiFunction = function(
      group, numInputs, numOutputs, baseElements, computationRules) {

    var validateComputationRules = function(
        numInputs, numOutputs, baseElements, computationRules) {

      // Validate that both of the values in each of the
      // pairs of rules have values in the correct ranges
      var numBaseElements = baseElements.length;
      for (var i = 0; i < numOutputs; i++) {
        for (var j = 0; j < computationRules[i].length; j++) {
          // Validate the first value of the pair
          var pairValue1 = computationRules[i][j][0];
          if ((pairValue1 < 1) || (pairValue1 > numBaseElements)) {
            throw new exceptions.CryptoLibException(
                'Invalid first value in index pair - should be in the range 1 to ' +
                numBaseElements + ', but it was ' + pairValue1);
          }

          // Validate the second value of the pair
          var pairValue2 = computationRules[i][j][1];
          if ((pairValue2 < 1) || (pairValue2 > numInputs)) {
            throw new exceptions.CryptoLibException(
                'Invalid second value in index pair - should be in the range 1 to ' +
                numInputs + ', but it was ' + pairValue2);
          }
        }
      }
    };

    checkDefined('group', group);
    checkIsPositive('number of inputs', numInputs);
    checkIsPositive('number of outputs', numOutputs);
    checkInitializedArray('base elements', baseElements);
    checkDefined('computation rules', computationRules);
    validateComputationRules(
        numInputs, numOutputs, baseElements, computationRules);

    var _numInputs = numInputs;
    var _numOutputs = numOutputs;
    var _baseElements = baseElements;
    var _computationRules = computationRules;

    /**
     * @return The number of inputs
     */
    this.getNumInputs = function() {
      return _numInputs;
    };

    /**
     * @return The number of outputs
     */
    this.getNumOutputs = function() {
      return _numOutputs;
    };

    /**
     * @return The list of base elements
     */
    this.getBaseElements = function() {
      return _baseElements;
    };

    /**
     * @return The computation rules.
     */
    this.getComputationRules = function() {
      return _computationRules;
    };
  };

  box.proofs.PhiFunction.prototype = {

    calculatePhi: function(inputs, progressMeter) {

      checkDefined('inputs', inputs);
      if (progressMeter === undefined) {
        progressMeter = new utils.ProgressMeter();
      }
      checkDefined('progressMeter', progressMeter);

      var computationsRules = this.getComputationRules();
      var baseElements = this.getBaseElements();

      var partialResult;
      var resultForThisListOfPairs;

      var result = [];
      var progress = 0;

      for (var i = 0; i < computationsRules.length; i++) {
        var numPairsInList = computationsRules[i].length;

        resultForThisListOfPairs =
            baseElements[computationsRules[i][0][0] - 1].exponentiate(
                inputs[computationsRules[i][0][1] - 1]);

        progressMeter.update(++progress);

        for (var j = 1; j < numPairsInList; j++) {
          var index1 = computationsRules[i][j][0];
          index1 = index1 - 1;
          var index2 = computationsRules[i][j][1];
          index2 = index2 - 1;

          partialResult = baseElements[index1].exponentiate(inputs[index2]);

          resultForThisListOfPairs =
              resultForThisListOfPairs.multiply(partialResult);

          progressMeter.update(++progress);
        }
        result.push(resultForThisListOfPairs);
      }

      return result;
    }
  };

  /**
   * Acts as a prover in a Zero-Knowledge Proof of Knowledge (ZK-PoK) exchange
   * using Maurer's unified PHI function.
   */
  box.proofs.Prover = function(group, phiFunction) {

    checkDefined('group', group);
    checkDefined('phiFunction', phiFunction);

    var _group = group;
    var _phiFunction = phiFunction;
    var _cryptoRandomInteger = secureRandomFactory.getCryptoRandomInteger();

    /**
     * @return Get the group.
     */
    this.getGroup = function() {
      return _group;
    };

    /**
     * @return Get the phi function.
     */
    this.getPhiFunction = function() {
      return _phiFunction;
    };

    this._calculateRandomExponents = function(numExponentsNeeded) {

      var exponentsArray = [];

      for (var i = 0; i < numExponentsNeeded; i++) {
        var randomExponent = mathematical.groupUtils.generateRandomExponent(
            _group, _cryptoRandomInteger);

        exponentsArray.push(randomExponent);
      }

      return exponentsArray;
    };

    this._generateHash = function(publicValues, phiOutputs, data) {

      var hashBuilder = new box.proofs.HashBuilder();

      var hashB64 =
          hashBuilder.buildHashForProof(publicValues, phiOutputs, data);

      var hash = converters.base64Decode(hashB64);
      var hashBytes = converters.bytesFromString(hash);
      hashBytes.unshift(0);

      var value = new box.forge.jsbn.BigInteger(hashBytes);

      return new mathematical.Exponent(_group.getQ(), value);
    };

    this._generateValuesList = function(
        privateValues, hash, randomExponents, index) {

      var proofValues = [];
      var proofValue;
      if (typeof index === 'undefined') {
        for (var i = 0; i < privateValues.length; i++) {
          proofValue = randomExponents[i].getValue().add(
              privateValues[i].getValue().multiply(hash.getValue()));

          proofValues.push(
              new mathematical.Exponent(_group.getQ(), proofValue));
        }
      } else {
        var numElements = randomExponents.length / 2;

        var updatedHash = hash;
        for (var j = 0; j < numElements; j++) {
          if (j === index) {
            for (var k = 0; k < numElements; k++) {
              if (k !== index) {
                updatedHash = updatedHash.subtract(randomExponents[k]);
              }
            }
            proofValues.push(updatedHash);
          } else {
            proofValues.push(randomExponents[j]);
          }
        }

        for (var m = numElements; m < (numElements * 2); m++) {
          if (m === (numElements + index)) {
            proofValue = randomExponents[m].getValue().add(
                privateValues[0].getValue().multiply(updatedHash.getValue()));

            proofValues.push(
                new mathematical.Exponent(_group.getQ(), proofValue));
          } else {
            proofValues.push(randomExponents[m]);
          }
        }
      }

      return proofValues;
    };

    this._updatePreComputedValues = function(
        preComputedValues, publicValues, elements, index) {

      var gamma = publicValues[0];
      var phi = publicValues[1];
      var numElements = elements.length;

      var challenges = this._calculateRandomExponents(numElements);

      var updatedExponents = [];
      updatedExponents.addAll(challenges);
      updatedExponents.addAll(preComputedValues.getExponents());

      var updatedPhiOutputs = [];
      var phiOutputs = preComputedValues.getPhiOutputs();
      for (var j = 0; j < numElements; j++) {
        var offset = 2 * j;
        if (j === index) {
          updatedPhiOutputs.push(phiOutputs[offset]);
          updatedPhiOutputs.push(phiOutputs[offset + 1]);
        } else {
          var gammaFactor = gamma.invert().exponentiate(challenges[j]);
          var phiFactor =
              (phi.invert().multiply(elements[j])).exponentiate(challenges[j]);
          updatedPhiOutputs.push(phiOutputs[offset].multiply(gammaFactor));
          updatedPhiOutputs.push(phiOutputs[offset + 1].multiply(phiFactor));
        }
      }

      return new box.proofs.ProofPreComputedValues(
          updatedExponents, updatedPhiOutputs);
    };
  };

  box.proofs.Prover.prototype = {

    /**
     * Pre-compute time-consuming values, so they can be used later for
     * method prove().
     */
    preCompute: function(privateValuesLength, progressMeter) {
      var randomExponents = this._calculateRandomExponents(privateValuesLength);

      var phiOutputs =
          this.getPhiFunction().calculatePhi(randomExponents, progressMeter);

      return new box.proofs.ProofPreComputedValues(randomExponents, phiOutputs);
    },

    /**
     * Performs prover steps in a ZK-PoK exchange using Maurer's unified PHI
     * function. This involves generating a proof, using the received inputs
     * and this Prover's internal fields.
     */
    prove: function(
        publicValues, privateValues, data, preComputedValues, elements, index) {

      checkDefined('public values', publicValues);
      checkDefined('private values', privateValues);
      checkDefined('data', data);

      if (!preComputedValues ||
          typeof preComputedValues.getPhiOutputs() === 'undefined' ||
          typeof preComputedValues.getExponents() === 'undefined') {
        throw new exceptions.CryptoLibException(
            'The precomputed values must be initialized');
      }

      if (typeof elements !== 'undefined') {
        preComputedValues = this._updatePreComputedValues(
            preComputedValues, publicValues, elements, index);
      }

      var hash = this._generateHash(
          publicValues, preComputedValues.getPhiOutputs(), data);

      var proofValues = this._generateValuesList(
          privateValues, hash, preComputedValues.getExponents(), index);

      return new box.proofs.Proof(hash, proofValues);
    }
  };

  /**
   * Acts as a verifier in a Zero-Knowledge Proof of Knowledge (ZK-PoK)
   * exchange using Maurer's unified PHI function.
   */
  box.proofs.Verifier = function(group, phiFunction) {

    checkDefined('group', group);
    checkDefined('phiFunction', phiFunction);

    var _group = group;
    var _phiFunction = phiFunction;

    /**
     * @return Get the group.
     */
    this.getGroup = function() {
      return _group;
    };

    /**
     * @return Get the phi function.
     */
    this.getPhiFunction = function() {
      return _phiFunction;
    };

    this._calculateComputedValues = function(
        publicValues, phiOutputs, proofHashValueOrElements, challenges) {

      var proofHashValue;
      var elements;
      if (!isArray(proofHashValueOrElements)) {
        proofHashValue = proofHashValueOrElements;
      } else {
        elements = proofHashValueOrElements;
      }

      var computedValues = [];
      if (typeof proofHashValue !== 'undefined') {
        for (var i = 0; i < phiOutputs.length; i++) {
          computedValues.push(phiOutputs[i].multiply(
              publicValues[i].exponentiate(proofHashValue.negate())));
        }
      } else {
        var gamma = publicValues[0];
        var phi = publicValues[1];

        for (var j = 0; j < challenges.length; j++) {
          var offset = 2 * j;
          var gammaFactor = gamma.invert().exponentiate(challenges[j]);
          var phiFactor =
              (phi.invert().multiply(elements[j])).exponentiate(challenges[j]);
          computedValues.push(phiOutputs[offset].multiply(gammaFactor));
          computedValues.push(phiOutputs[offset + 1].multiply(phiFactor));
        }
      }

      return computedValues;
    };

    this._calculateHash = function(publicValues, computedValues, data) {

      var hashBuilder = new box.proofs.HashBuilder();

      var hashB64 =
          hashBuilder.buildHashForProof(publicValues, computedValues, data);

      var hash = converters.base64Decode(hashB64);

      var hashBytes = converters.bytesFromString(hash);
      hashBytes.unshift(0);

      var value = new box.forge.jsbn.BigInteger(hashBytes);

      return new mathematical.Exponent(_group.getQ(), value);
    };
  };

  box.proofs.Verifier.prototype = {

    /**
     * Verifies the received input as being true or false.
     */
    verify: function(
        publicValues, proof, data, elementsOrProgressMeter, progressMeter) {

      checkDefined('public values', publicValues);
      checkDefined('proof', proof);
      checkDefined('data', data);

      var elements;
      if (!isArray(elementsOrProgressMeter)) {
        progressMeter = elementsOrProgressMeter;
      } else {
        elements = elementsOrProgressMeter;
      }

      var proofHashValue = proof.getHash();

      var phiOutputs;
      var computedValues;
      if (typeof elements === 'undefined') {
        phiOutputs = this.getPhiFunction().calculatePhi(
            proof.getValues(), progressMeter);
        computedValues = this._calculateComputedValues(
            publicValues, phiOutputs, proofHashValue);
      } else {
        var numElements = elements.length;
        var proofValues = proof.getValues();
        var challenges = proofValues.slice(0, numElements);
        var phiInputs = proofValues.slice(numElements, numElements * 2);

        phiOutputs =
            this.getPhiFunction().calculatePhi(phiInputs, progressMeter);
        computedValues = this._calculateComputedValues(
            publicValues, phiOutputs, elements, challenges);
      }

      var calculatedHash =
          this._calculateHash(publicValues, computedValues, data);

      return proofHashValue.equals(calculatedHash);
    }
  };

  /**
   * @class ProofPreComputedValues stores randomly created exponents and
   *        outputs of box.proofs.PhiFunction.calculatePhi().
   */
  box.proofs.ProofPreComputedValues = function(exponents, phiOutputs) {

    this.className = 'ProofPreComputedValues';
    var _exponents = exponents;
    var _phiOutputs = phiOutputs;

    /**
     * @return the exponents.
     */
    this.getExponents = function() {
      return _exponents;
    };

    /**
     * @return the phi function output.
     */
    this.getPhiOutputs = function() {
      return _phiOutputs;
    };
  };

  box.proofs.ProofPreComputedValues.prototype = {

    stringify: function() {
      var exponentValues = Array();
      var phiOutputValues = Array();

      for (var i = 0; i < this.getExponents().length; i++) {
        exponentValues[i] =
            converters.base64FromBigInteger(this.getExponents()[i].getValue());
      }

      for (var j = 0; j < this.getPhiOutputs().length; j++) {
        phiOutputValues[j] = converters.base64FromBigInteger(
            this.getPhiOutputs()[j].getElementValue());
      }

      return JSON.stringify({
        preComputed: {
          p: converters.base64FromBigInteger(this.getPhiOutputs()[0].getP()),
          q: converters.base64FromBigInteger(this.getPhiOutputs()[0].getQ()),
          exponents: exponentValues,
          phiOutputs: phiOutputValues
        }
      });
    }
  };

  /**
   * Represents a proof in a ZKP exchange.
   */
  box.proofs.Proof = function(hashValue, values) {

    checkDefined('hash value', hashValue);
    checkDefined('values', values);

    var _hashValue = hashValue;

    var _values = values;

    /**
     * @return Get the hash value of this proof.
     */
    this.getHash = function() {
      return _hashValue;
    };

    /**
     * @return Get the list of values of this proof.
     */
    this.getValues = function() {
      return _values;
    };
  };

  box.proofs.Proof.prototype = {

    /**
     * @return true if the given proof has the same hash and values.
     */
    equals: function(proof) {
      return proof.getHash().equals(this.getHash()) &&
          proof.getValues().equals(this.getValues());
    },

    /**
     * Displays a string representation of this proof JSON.
     * <p>
     * Note: in order to permit interoperability between libraries, this
     * representation should match the equivalent representation in Java.
     *
     * @function
     * @returns a string representation of this proof JSON.
     */
    stringify: function() {

      var values = Array();

      for (var i = 0; i < this.getValues().length; i++) {
        values[i] =
            converters.base64FromBigInteger(this.getValues()[i].getValue());
      }

      return JSON.stringify({
        zkProof: {
          q: converters.base64FromBigInteger(this.getHash().getQ()),
          hash: converters.base64FromBigInteger(this.getHash().getValue()),
          values: values
        }
      });
    }
  };

  /**
   * A class which provides utility methods for generating a hash from a set
   * of inputs.
   */
  box.proofs.HashBuilder = function() {

    /**
     * Convert an integer to a byte array
     */
    this._getInt64Bytes = function(x) {
      var bytes = [];
      var i = 8;
      do {
        bytes[--i] = x & (255);
        x = x >> 8;
      } while (i);

      return bytes;
    };

    /**
     * Converts an array to a String.
     */
    this._arrayElementValuesToString = function(groupElements) {

      var str = '';

      for (var i = 0; i < groupElements.length; i++) {
        if (groupElements[i].getElementValue() !== undefined) {
          str += groupElements[i].getElementValue();
        } else {
          throw new exceptions.CryptoLibException(
              'Group Element must implement getElementValue() method.');
        }
      }
      return str;
    };
  };

  box.proofs.HashBuilder.prototype = {

    /**
     * Generates a hash value based on the element values of publicValues
     * and generatedValues and the data.
     *
     * @function
     * @param publicValues
     *            {array} group elements representing public values.
     * @param generatedValues
     *            {array} group elements representing generated values.
     * @param data
     *            {string} some data encoded as a String.
     * @returns a generated hash code.
     * @throws CryptoLibException
     *             if any of the received objects has unsupported type.
     */
    buildHashForProof: function(publicValues, generatedValues, data) {

      checkInitializedArray('public values', publicValues);
      checkInitializedArray('generated values', generatedValues);

      var messageDigester = messageDigestFactory.getCryptoMessageDigest();

      messageDigester.start();

      messageDigester = update(
          messageDigester, this._arrayElementValuesToString(publicValues));
      messageDigester = update(
          messageDigester, this._arrayElementValuesToString(generatedValues));
      messageDigester = update(messageDigester, data);

      return messageDigester.digest();
    }
  };

  /**
   * Utility method that updates messageDigester with the provided string.
   */
  function update(messageDigester, str) {
    var stringRepresentationB64 = converters.base64Encode(str);
    var arrayToHash = [];
    arrayToHash.push(stringRepresentationB64);
    messageDigester.update(arrayToHash);
    return messageDigester;
  }

  /**
   * Utility method that checks that value is defined and not null and throws
   * related exception otherwise.
   */
  function checkDefined(key, value) {
    if (typeof value === 'undefined') {
      throw new exceptions.CryptoLibException(
          'The ' + key + ' must be defined');
    }
  }

  /**
   * Utility method that checks that value is initialized array and throws
   * related exception otherwise.
   */
  function checkInitializedArray(key, value) {
    checkDefined(key, value);

    if (!isArray(value)) {
      throw new exceptions.CryptoLibException(
          'The ' + key + ' must be an initialized array');
    }
  }

  /**
   * Utility method that checks that value is not null and throws related
   * exception otherwise.
   */
  function checkNotNull(key, value) {
    checkDefined(key, value);

    if (value === null) {
      throw new exceptions.CryptoLibException(
          'The ' + key + ' must not be null');
    }
  }

  /**
   * Utility method that checks that value is not empty and throws related
   * exception otherwise.
   */
  function checkNotEmpty(key, value) {
    checkDefined(key, value);

    if (value.length === 0) {
      throw new exceptions.CryptoLibException(
          'The ' + key + ' must not be empty');
    }
  }

  /**
   * Utility method that checks that value is positive and throws related
   * exception otherwise.
   */
  function checkIsPositive(key, value) {
    checkDefined(key, value);

    if (isNaN(value)) {
      throw new exceptions.CryptoLibException(
          'The ' + key + ' must be a number; Found ' + value);
    }

    if (value <= 0) {
      throw new exceptions.CryptoLibException(
          'The ' + key + ' must be positive; Found ' + value);
    }
  }

  /**
   * Utility method that checks that values are defined and throws related
   * exception otherwise.
   */
  var checkDefinedProgressParams = function(
      progressCallback, progressPercentMinCheckInterval) {
    if (typeof progressCallback !== 'undefined' && 
        typeof progressCallback !== 'function') {
        throw new exceptions.CryptoLibException(
            'The progress callback function provided as input is  not a function');
    }
    if (typeof progressPercentMinCheckInterval !== 'undefined' &&
        isNaN(progressPercentMinCheckInterval)) {
        throw new exceptions.CryptoLibException(
            'The progress percentage check interval provided as input is not a number');
    }
  };

  function isArray(a) {
    return (!!a) && (a.constructor === Array);
  }

  /**
   * Utility method that creates a new instance of ProgressMeter.
   */
  function createProgressMeter(
      progressMax, progressCallback, progressPercentMinCheckInterval) {
    checkDefinedProgressParams(
        progressCallback, progressPercentMinCheckInterval);
    return new utils.ProgressMeter(
        progressMax, progressCallback, progressPercentMinCheckInterval);
  }

  /**
   * Provides utility functionality for working with proofs.
   *
   * @class utils
   * @memberof proofs
   */
  box.proofs.utils = (function() {

    /**
     * Deserializes a Proof JSON string representation to a Proof object.
     *
     * @function
     * @memberof proofs.utils
     * @param jsonAsString
     *            {JSON} a JSON representation of a proof.
     * @returns a new Proof, created from the received string.
     */
    function deserializeProof(jsonAsString) {
      var proofJson = JSON.parse(jsonAsString).zkProof;

      var q = converters.base64ToBigInteger(proofJson.q);
      var hash = new box.commons.mathematical.Exponent(
          q, converters.base64ToBigInteger(proofJson.hash));
      var values = [];
      for (var i = 0; i < proofJson.values.length; i++) {
        values.push(new box.commons.mathematical.Exponent(
            q, converters.base64ToBigInteger(proofJson.values[i])));
      }

      return new box.proofs.Proof(hash, values);
    }

    /**
     * Deserializes a ProofPreComputedValues JSON string representation to a
     * ProofPreComputedValues object.
     *
     * @function
     * @memberof proofs.utils
     * @param jsonAsString
     *            {JSON} a JSON representation of a pre-computed value.
     * @returns a new ProofPreComputedValues, created from the received
     *          string.
     */
    function deserializeProofPreComputedValues(jsonAsString) {
      var proofPreComputedValuesJson = JSON.parse(jsonAsString).preComputed;
      var p = converters.base64ToBigInteger(proofPreComputedValuesJson.p);
      var q = converters.base64ToBigInteger(proofPreComputedValuesJson.q);

      var exponentValues = proofPreComputedValuesJson.exponents;
      var exponents = [];
      var value;
      for (var i = 0; i < exponentValues.length; i++) {
        value = converters.base64ToBigInteger(exponentValues[i]);
        exponents.push(new box.commons.mathematical.Exponent(q, value));
      }

      var phiOutputValues = proofPreComputedValuesJson.phiOutputs;
      var phiOutputs = [];
      for (var j = 0; j < phiOutputValues.length; j++) {
        value = converters.base64ToBigInteger(phiOutputValues[j]);
        phiOutputs.push(
            new box.commons.mathematical.ZpGroupElement(value, p, q));
      }

      return new box.proofs.ProofPreComputedValues(exponents, phiOutputs);
    }

    return {
      deserializeProof: deserializeProof,
      deserializeProofPreComputedValues: deserializeProofPreComputedValues
    };
  })();
};
