/*
 * 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
 */
describe('Homomorphic service...', function() {
  'use strict';

  var MAX_EXPONENT_BYTES = 2;
  var NUM_GROUP_ELEMENTS = 2;
  var NUM_SINGLE_GROUP_ELEMENTS = 5;
  var G_STR = '2';
  var P_STR = '23';
  var Q_STR = '11';
  var ELEMENT_1_STR = '2';
  var ELEMENT_2_STR = '4';
  var EXPONENT_1_STR = '1';
  var EXPONENT_2_STR = '2';
  var P_LARGE_STR =
      '22588801568735561413035633152679913053449200833478689904902877673687016391844561133376032309307885537704777240609087377993341380751697605235541131273868440070920362148431866829787784445019147999379498503693247429579480289226602748397335327890884464685051682703709742724121783217827040722415360103179289160056581759372475845985438977307323570530753362027145384124771826114651710264766437273044759690955051982839684910462609395741692689616014805965573558015387956017183286848440036954926101719205598449898400180082053755864070690174202432196678045052744337832802051787273056312757384654145455745603262082348042780103679';
  var Q_LARGE_STR =
      '11294400784367780706517816576339956526724600416739344952451438836843508195922280566688016154653942768852388620304543688996670690375848802617770565636934220035460181074215933414893892222509573999689749251846623714789740144613301374198667663945442232342525841351854871362060891608913520361207680051589644580028290879686237922992719488653661785265376681013572692062385913057325855132383218636522379845477525991419842455231304697870846344808007402982786779007693978008591643424220018477463050859602799224949200090041026877932035345087101216098339022526372168916401025893636528156378692327072727872801631041174021390051839';
  var ELEMENT_1_LARGE_STR =
      '8008650650117182055231947796743510595354735701773678127718063162818861428894482760351040442014062206238677757186380515026469824595870224783896120627455841205517047520359887236518609328142965980653129280864098892996273964740090483597375107437139254412471355470548193136206727124381287524666926794272344682267309880188101372142121235759763117992716726731820970068223350454790098901032607754554402031099573358614926437378953856998041036773132081351057601842441454945388068039731483320814325657715553744820319265534754357498649904400594493834709761403358903505501640705996603619499187129497007221246019054462834616450362';
  var ELEMENT_2_LARGE_STR =
      '11633656285653208443919333339470607846301429932894308783457570029107286147936455427826899303856584843544699293627942560893304455970515082299498680026759665698080990494885992738624255552297867735585728427220214607212173788862760053487198296321130486607904876481443447070538549541504734530736112052175501932133880661864111558469545768002897528612084034669926036569715266022136440126595140089621370735885927146016552642348282420224106653601826235849885956583427798077317966671703306684815012981357245529568157986211578924579799810305712551845266909314082456551659795959581952216679219662533353585637849875985836675331118';
  var EXPONENT_1_LARGE_STR =
      '2310427452287266604038074581830299484831755686607108413899820257798165596547227594518989253178264380941033095268581246346863002280809127490132335557543545847332023813499454886051935043363376517658050024793517465349562106228739828060169613880190064891580719513382177220752409776280150551117185655930834371803832590064152233876708731218969195345490200219499344738664373593917843352485459832349992908261365788172930979823955846952949156058247067965643074532679047274706566088888021355575983368719069681293419346162608049979977471053590745499346591475991673763640645139343249862033377744682868210594034732021779460567412';
  var EXPONENT_2_LARGE_STR =
      '7533608948657130117198364792462971241599837777629317286887202126604432685322243335715285630236984093165224740099563982076007648728594011814311120331313200895892632245003875959905589367483155794698953057915454892457379023058909518440394905695255170407948701433659375513788930419544151891518689331636455316691827048845766063478445484089208223819510250476623436782404385896353292653750744111841510466992101967325081746747885221256692886521245630131046183525331085171958919416648097910028887247120874015121272894655357197879292253781160838729753609784455892951965051119314203469298666741439497193677501351952497150273389';

  var _initialized = false;
  var _elGamalPublicKey;
  var _elGamalPrivateKey;
  var _elGamalEncrypter;
  var _elGamalDecrypter;
  var _elGamalKeyPair;
  var _zpSubgroup;
  var _zpGroupElements;
  var _elGamalPublicKeyLarge;
  var _elGamalPrivateKeyLarge;
  var _elGamalEncrypterLarge;
  var _elGamalDecrypterLarge;
  var _zpGroupElementsLarge;
  var _zpGroupElementsLargeNonQR;
  var _singleZpGroupElements;
  var _elGamalEncrypterUsingSeed;
  var _elGamalRandomDecrypter;

  beforeEach(function() {
    if (!_initialized) {
      cryptolib(
          'commons.utils', 'commons.mathematical', 'homomorphic.service',
          function(box) {
            var converters = new box.commons.utils.Converters();

            var gB64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(G_STR));
            var pB64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(P_STR));
            var qB64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(Q_STR));
            var element1B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(ELEMENT_1_STR));
            var element2B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(ELEMENT_2_STR));
            var exponent1B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(EXPONENT_1_STR));
            var exponent2B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(EXPONENT_2_STR));
            var elGamalPublicKeyJson = '{"publicKey":{"zpSubgroup":{"g":"' +
                gB64 + '","p":"' + pB64 + '","q":"' + qB64 +
                '"},"elements":["' + element1B64 + '","' + element2B64 + '"]}}';
            var elGamalPrivateKeyJson = '{"privateKey":{"zpSubgroup":{"g":"' +
                gB64 + '","p":"' + pB64 + '","q":"' + qB64 +
                '"},"exponents":["' + exponent1B64 + '","' + exponent2B64 +
                '"]}}';
            var elGamalPublicKeyJsonB64 =
                converters.base64Encode(elGamalPublicKeyJson);
            var elGamalPrivateKeyJsonB64 =
                converters.base64Encode(elGamalPrivateKeyJson);

            _elGamalPublicKey = box.homomorphic.service.createElGamalPublicKey(
                elGamalPublicKeyJsonB64);
            _elGamalPrivateKey =
                box.homomorphic.service.createElGamalPrivateKey(
                    elGamalPrivateKeyJsonB64);

            _elGamalKeyPair = new box.homomorphic.service.createElGamalKeyPair(
                _elGamalPublicKey, _elGamalPrivateKey);

            _elGamalEncrypter =
                box.homomorphic.service.createEncrypter(_elGamalPublicKey);
            _elGamalDecrypter =
                box.homomorphic.service.createDecrypter(_elGamalPrivateKey);

            var g = new box.forge.jsbn.BigInteger(G_STR);
            var p = new box.forge.jsbn.BigInteger(P_STR);
            var q = new box.forge.jsbn.BigInteger(Q_STR);
            _zpSubgroup = new box.commons.mathematical.ZpSubgroup(g, p, q);
            _zpGroupElements =
                generateGroupElements(_zpSubgroup, NUM_GROUP_ELEMENTS);

            gB64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(G_STR));
            pB64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(P_LARGE_STR));
            qB64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(Q_LARGE_STR));
            element1B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(ELEMENT_1_LARGE_STR));
            element2B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(ELEMENT_2_LARGE_STR));
            exponent1B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(EXPONENT_1_LARGE_STR));
            exponent2B64 = converters.base64FromBigInteger(
                new forge.jsbn.BigInteger(EXPONENT_2_LARGE_STR));
            var elGamalPublicKeyLargeJson =
                '{"publicKey":{"zpSubgroup":{"g":"' + gB64 + '","p":"' + pB64 +
                '","q":"' + qB64 + '"},"elements":["' + element1B64 + '","' +
                element2B64 + '"]}}';
            var elGamalPrivateKeyLargeJson =
                '{"privateKey":{"zpSubgroup":{"g":"' + gB64 + '","p":"' + pB64 +
                '","q":"' + qB64 + '"},"exponents":["' + exponent1B64 + '","' +
                exponent2B64 + '"]}}';
            var elGamalPublicKeyLargeJsonB64 =
                converters.base64Encode(elGamalPublicKeyLargeJson);
            var elGamalPrivateKeyLargeJsonB64 =
                converters.base64Encode(elGamalPrivateKeyLargeJson);

            _elGamalPublicKeyLarge =
                box.homomorphic.service.createElGamalPublicKey(
                    elGamalPublicKeyLargeJsonB64);
            _elGamalPrivateKeyLarge =
                box.homomorphic.service.createElGamalPrivateKey(
                    elGamalPrivateKeyLargeJsonB64);

            _elGamalEncrypterLarge =
                box.homomorphic.service.createEncrypter(_elGamalPublicKeyLarge);
            _elGamalDecrypterLarge = box.homomorphic.service.createDecrypter(
                _elGamalPrivateKeyLarge);

            g = new box.forge.jsbn.BigInteger(G_STR);
            p = new box.forge.jsbn.BigInteger(P_LARGE_STR);
            q = new box.forge.jsbn.BigInteger(Q_LARGE_STR);
            var zpSubgroup = new box.commons.mathematical.ZpSubgroup(g, p, q);
            _zpGroupElementsLarge =
                generateGroupElements(zpSubgroup, NUM_GROUP_ELEMENTS);

            g = new box.forge.jsbn.BigInteger(G_STR);
            p = new box.forge.jsbn.BigInteger(P_LARGE_STR);
            q = new box.forge.jsbn.BigInteger(Q_LARGE_STR)
                    .subtract(box.forge.jsbn.BigInteger.ONE);
            zpSubgroup = new box.commons.mathematical.ZpSubgroup(g, p, q);
            _zpGroupElementsLargeNonQR =
                generateGroupElements(zpSubgroup, NUM_GROUP_ELEMENTS);

            g = new box.forge.jsbn.BigInteger(G_STR);
            p = new box.forge.jsbn.BigInteger(P_LARGE_STR);
            q = new box.forge.jsbn.BigInteger(Q_LARGE_STR);
            zpSubgroup = new box.commons.mathematical.ZpSubgroup(g, p, q);
            _singleZpGroupElements =
                generateGroupElements(zpSubgroup, NUM_SINGLE_GROUP_ELEMENTS);
          });

      _initialized = true;
    }

    cryptolib('primitives.service', 'homomorphic.service', function(box) {
      // Generate seed and define callback function to initialize random
      // integer generator with seed.
      var seedHex = cryptoPRNG.generateRandomSeedInHex();
      var cryptoRandomInteger;
      var initRandomGenerator = function(box) {
        cryptoRandomInteger = box.primitives.service.getCryptoRandomInteger();
      };

      // Create new encrypter that uses random integer generator initialized
      // with seed.
      initRandomGenerator.prng = cryptoPRNG.createPRNGFromSeed(seedHex);
      cryptolib('primitives.service', initRandomGenerator);
      _elGamalEncrypterUsingSeed = box.homomorphic.service.createEncrypter(
          _elGamalPublicKeyLarge, cryptoRandomInteger);

      // Create new decrypter that uses random integer generator initialized
      // with seed.
      initRandomGenerator.prng = cryptoPRNG.createPRNGFromSeed(seedHex);
      cryptolib('primitives.service', initRandomGenerator);
      _elGamalRandomDecrypter = box.homomorphic.service.createRandomDecrypter(
          _elGamalPublicKeyLarge, cryptoRandomInteger);
    });
  });

  describe('should be able to ..', function() {
    it('create an ElGamal key pair', function() {
      validateElGamalPublicKey(_elGamalKeyPair.getPublicKey());
      validateElGamalPrivateKey(_elGamalKeyPair.getPrivateKey());
    });

    it('and create an ElGamal encrypter', function() {
      validateElGamalPublicKey(_elGamalEncrypter.getElGamalPublicKey());
    });

    it('and create an ElGamal decrypter', function() {
      validateElGamalPrivateKey(_elGamalDecrypter.getElGamalPrivateKey());
    });

    it('and deserialize an ElGamal public key', function() {
      expect(_elGamalPublicKey.getGroup()
                 .getGenerator()
                 .getElementValue()
                 .toString())
          .toEqual(G_STR);
      expect(_elGamalPublicKey.getGroup().getP().toString()).toEqual(P_STR);
      expect(_elGamalPublicKey.getGroup().getQ().toString()).toEqual(Q_STR);

      expect(_elGamalPublicKey.getGroupElementsArray().length).toEqual(2);
      expect(_elGamalPublicKey.getGroupElementsArray()[0]
                 .getElementValue()
                 .toString())
          .toEqual(ELEMENT_1_STR);
      expect(_elGamalPublicKey.getGroupElementsArray()[1]
                 .getElementValue()
                 .toString())
          .toEqual(ELEMENT_2_STR);
    });

    it('and deserialize an ElGamal private key', function() {
      expect(_elGamalPrivateKey.getGroup()
                 .getGenerator()
                 .getElementValue()
                 .toString())
          .toEqual(G_STR);
      expect(_elGamalPrivateKey.getGroup().getP().toString()).toEqual(P_STR);
      expect(_elGamalPrivateKey.getGroup().getQ().toString()).toEqual(Q_STR);

      expect(_elGamalPrivateKey.getExponentsArray().length).toEqual(2);
      expect(_elGamalPrivateKey.getExponentsArray()[0].getValue().toString())
          .toEqual(EXPONENT_1_STR);
      expect(_elGamalPrivateKey.getExponentsArray()[1].getValue().toString())
          .toEqual(EXPONENT_2_STR);
    });

    it('and ElGamal encrypt and decrypt some group elements', function() {
      var encryptedGroupElements =
          _elGamalEncrypter.encryptGroupElements(_zpGroupElements);

      var decryptedGroupElements =
          _elGamalDecrypter.decrypt(encryptedGroupElements, true);

      expect(decryptedGroupElements.length).toBe(_zpGroupElements.length);
      for (var i = 0; i < decryptedGroupElements.length; i++) {
        expect(decryptedGroupElements[i].equals(_zpGroupElements[i]))
            .toBeTruthy();
      }
    });

    it('and ElGamal encrypt and decrypt some group elements, using pre-computed encryption values',
       function() {
         var preComputedValues = _elGamalEncrypter.preCompute();
         var encryptedGroupElements = _elGamalEncrypter.encryptGroupElements(
             _zpGroupElements, preComputedValues);

         var decryptedGroupElements =
             _elGamalDecrypter.decrypt(encryptedGroupElements, true);

         expect(decryptedGroupElements.length).toBe(_zpGroupElements.length);
         for (var i = 0; i < decryptedGroupElements.length; i++) {
           expect(decryptedGroupElements[i].equals(_zpGroupElements[i]))
               .toBeTruthy();
         }
       });

    it('and ElGamal encrypt and decrypt some group elements, using encryption values generated with a short exponent',
       function() {
         cryptolib(function(box) {
           var encryptedGroupElements =
               _elGamalEncrypterLarge.encryptGroupElements(
                   _zpGroupElementsLarge, true);
           expect(encryptedGroupElements.getR().getValue().bitLength())
               .not.toBeGreaterThan(box.SHORT_EXPONENT_BIT_LENGTH);

           var decryptedGroupElements =
               _elGamalDecrypterLarge.decrypt(encryptedGroupElements, true);

           expect(decryptedGroupElements.length)
               .toBe(_zpGroupElementsLarge.length);
           for (var i = 0; i < decryptedGroupElements.length; i++) {
             expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some group elements, using pre-computed encryption values generated with a short exponent',
       function() {
         cryptolib(function(box) {
           var preComputedValues = _elGamalEncrypterLarge.preCompute(true);
           var encryptedGroupElements =
               _elGamalEncrypterLarge.encryptGroupElements(
                   _zpGroupElementsLarge, preComputedValues);
           expect(encryptedGroupElements.getR().getValue().bitLength())
               .not.toBeGreaterThan(box.SHORT_EXPONENT_BIT_LENGTH);

           var decryptedGroupElements =
               _elGamalDecrypterLarge.decrypt(encryptedGroupElements, true);

           expect(decryptedGroupElements.length)
               .toBe(_zpGroupElementsLarge.length);
           for (var i = 0; i < decryptedGroupElements.length; i++) {
             expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some group elements, using a random seed',
       function() {
         var encryptedGroupElements =
             _elGamalEncrypterUsingSeed.encryptGroupElements(
                 _zpGroupElementsLarge);

         var decryptedGroupElements =
             _elGamalRandomDecrypter.decrypt(encryptedGroupElements, true);

         expect(decryptedGroupElements.length)
             .toBe(_zpGroupElementsLarge.length);
         for (var i = 0; i < decryptedGroupElements.length; i++) {
           expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
               .toBeTruthy();
         }
       });

    it('and ElGamal encrypt and decrypt some group elements, using a random seed and pre-computed encryption values',
       function() {
         var preComputedValues = _elGamalEncrypterUsingSeed.preCompute();
         var encryptedGroupElements = _elGamalEncrypter.encryptGroupElements(
             _zpGroupElementsLarge, preComputedValues);

         var decryptedGroupElements =
             _elGamalRandomDecrypter.decrypt(encryptedGroupElements, true);

         expect(decryptedGroupElements.length)
             .toBe(_zpGroupElementsLarge.length);
         for (var i = 0; i < decryptedGroupElements.length; i++) {
           expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
               .toBeTruthy();
         }
       });

    it('and ElGamal encrypt and decrypt some group elements, using a random seed and encryption values generated with a short exponent',
       function() {
         cryptolib(function(box) {
           var encryptedGroupElements =
               _elGamalEncrypterUsingSeed.encryptGroupElements(
                   _zpGroupElementsLarge, true);
           expect(encryptedGroupElements.getR().getValue().bitLength())
               .not.toBeGreaterThan(box.SHORT_EXPONENT_BIT_LENGTH);

           var decryptedGroupElements = _elGamalRandomDecrypter.decrypt(
               encryptedGroupElements, true, true);

           expect(decryptedGroupElements.length)
               .toBe(_zpGroupElementsLarge.length);
           for (var i = 0; i < decryptedGroupElements.length; i++) {
             expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some group elements, using a random seed and pre-computed encryption values generated with a short exponent',
       function() {
         cryptolib(function(box) {
           var preComputedValues = _elGamalEncrypterUsingSeed.preCompute(true);
           var encryptedGroupElements =
               _elGamalEncrypterUsingSeed.encryptGroupElements(
                   _zpGroupElementsLarge, preComputedValues);
           expect(encryptedGroupElements.getR().getValue().bitLength())
               .not.toBeGreaterThan(box.SHORT_EXPONENT_BIT_LENGTH);

           var decryptedGroupElements = _elGamalRandomDecrypter.decrypt(
               encryptedGroupElements, true, true);

           expect(decryptedGroupElements.length)
               .toBe(_zpGroupElementsLarge.length);
           for (var i = 0; i < decryptedGroupElements.length; i++) {
             expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some single group elements, using a random seed',
       function() {
         var encryptedGroupElementArray = [];
         for (var i = 0; i < _singleZpGroupElements.length; i++) {
           var encryptedGroupElement =
               _elGamalEncrypterUsingSeed.encryptGroupElements(
                   [_singleZpGroupElements[i]]);
           encryptedGroupElementArray.push(encryptedGroupElement);
         }

         var decryptedGroupElementArray = [];
         for (var j = 0; j < encryptedGroupElementArray.length; j++) {
           var decryptedGroupElement = _elGamalRandomDecrypter.decrypt(
               encryptedGroupElementArray[j], true)[0];
           decryptedGroupElementArray.push(decryptedGroupElement);
         }

         expect(decryptedGroupElementArray.length)
             .toBe(_singleZpGroupElements.length);
         for (var k = 0; k < decryptedGroupElementArray.length; k++) {
           expect(
               decryptedGroupElementArray[k].equals(_singleZpGroupElements[k]))
               .toBeTruthy();
         }
       });

    it('and ElGamal encrypt and decrypt some single group elements, using a random seed and pre-computed encryption values',
       function() {
         var preComputedValuesArray = [];
         for (var i = 0; i < _singleZpGroupElements.length; i++) {
           var preComputedValues = _elGamalEncrypterUsingSeed.preCompute();
           preComputedValuesArray.push(preComputedValues);
         }
         var encryptedGroupElementArray = [];
         for (var j = 0; j < _singleZpGroupElements.length; j++) {
           var encryptedGroupElement =
               _elGamalEncrypterUsingSeed.encryptGroupElements(
                   [_singleZpGroupElements[j]], preComputedValuesArray[j]);
           encryptedGroupElementArray.push(encryptedGroupElement);
         }

         var decryptedGroupElementArray = [];
         for (var k = 0; k < encryptedGroupElementArray.length; k++) {
           var decryptedGroupElement = _elGamalRandomDecrypter.decrypt(
               encryptedGroupElementArray[k], true)[0];
           decryptedGroupElementArray.push(decryptedGroupElement);
         }

         expect(decryptedGroupElementArray.length)
             .toBe(_singleZpGroupElements.length);
         for (var l = 0; l < decryptedGroupElementArray.length; l++) {
           expect(
               decryptedGroupElementArray[l].equals(_singleZpGroupElements[l]))
               .toBeTruthy();
         }
       });

    it('and ElGamal encrypt and decrypt some single group elements, using a random seed and encryption values generated with a short exponent',
       function() {
         cryptolib(function(box) {
           var encryptedGroupElementArray = [];
           for (var i = 0; i < _singleZpGroupElements.length; i++) {
             var encryptedGroupElement =
                 _elGamalEncrypterUsingSeed.encryptGroupElements(
                     [_singleZpGroupElements[i]], true);
             expect(encryptedGroupElement.getR().getValue().bitLength())
                 .not.toBeGreaterThan(box.SHORT_EXPONENT_BIT_LENGTH);
             encryptedGroupElementArray.push(encryptedGroupElement);
           }

           var decryptedGroupElementArray = [];
           for (var j = 0; j < encryptedGroupElementArray.length; j++) {
             var decryptedGroupElement = _elGamalRandomDecrypter.decrypt(
                 encryptedGroupElementArray[j], true, true)[0];
             decryptedGroupElementArray.push(decryptedGroupElement);
           }

           expect(decryptedGroupElementArray.length)
               .toBe(_singleZpGroupElements.length);
           for (var k = 0; k < decryptedGroupElementArray.length; k++) {
             expect(decryptedGroupElementArray[k].equals(
                        _singleZpGroupElements[k]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some single group elements, using a random seed and pre-computed encryption values generated with a short exponent',
       function() {
         cryptolib(function(box) {
           var preComputedValuesArray = [];
           for (var i = 0; i < _singleZpGroupElements.length; i++) {
             var preComputedValues =
                 _elGamalEncrypterUsingSeed.preCompute(true);
             preComputedValuesArray.push(preComputedValues);
           }
           var encryptedGroupElementArray = [];
           for (var j = 0; j < _singleZpGroupElements.length; j++) {
             var encryptedGroupElement =
                 _elGamalEncrypterUsingSeed.encryptGroupElements(
                     [_singleZpGroupElements[j]], preComputedValuesArray[j]);
             expect(encryptedGroupElement.getR().getValue().bitLength())
                 .not.toBeGreaterThan(box.SHORT_EXPONENT_BIT_LENGTH);
             encryptedGroupElementArray.push(encryptedGroupElement);
           }

           var decryptedGroupElementArray = [];
           for (var k = 0; k < encryptedGroupElementArray.length; k++) {
             var decryptedGroupElement = _elGamalRandomDecrypter.decrypt(
                 encryptedGroupElementArray[k], true, true)[0];
             decryptedGroupElementArray.push(decryptedGroupElement);
           }

           expect(decryptedGroupElementArray.length)
               .toBe(_singleZpGroupElements.length);
           for (var l = 0; l < decryptedGroupElementArray.length; l++) {
             expect(decryptedGroupElementArray[l].equals(
                        _singleZpGroupElements[l]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some group elements, using serialized encryption values',
       function() {
         cryptolib('homomorphic.cipher', function(box) {
           var encryptedGroupElements =
               _elGamalEncrypter.encryptGroupElements(_zpGroupElements);

           var encryptedGroupElementsJson =
               encryptedGroupElements.getElGamalComputationValues().stringify();
           var encryptedGroupElementsFromJson =
               box.homomorphic.cipher.deserializeElGamalComputationValues(
                   encryptedGroupElementsJson);

           var decryptedGroupElements =
               _elGamalDecrypter.decrypt(encryptedGroupElementsFromJson, true);

           expect(decryptedGroupElements.length).toBe(_zpGroupElements.length);
           for (var i = 0; i < decryptedGroupElements.length; i++) {
             expect(decryptedGroupElements[i].equals(_zpGroupElements[i]))
                 .toBeTruthy();
           }
         });
       });

    it('and ElGamal encrypt and decrypt some group elements, using serialized pre-computed encryption values',
       function() {
         cryptolib('homomorphic.cipher', function(box) {
           var preComputedValues = _elGamalEncrypter.preCompute();

           var preComputedValuesJson =
               preComputedValues.getElGamalComputationValues().stringify();
           var preComputedValuesFromJson =
               box.homomorphic.cipher.deserializeElGamalComputationValues(
                   preComputedValuesJson);
           var gamma = preComputedValuesFromJson.getGamma();
           var phis = preComputedValuesFromJson.getPhis();
           preComputedValuesFromJson =
               new box.homomorphic.cipher.ElGamalEncrypterValues(
                   null, gamma, phis);

           var encryptedGroupElements = _elGamalEncrypter.encryptGroupElements(
               _zpGroupElements, preComputedValuesFromJson);

           var decryptedGroupElements =
               _elGamalDecrypter.decrypt(encryptedGroupElements, true);

           expect(decryptedGroupElements.length).toBe(_zpGroupElements.length);
           for (var i = 0; i < decryptedGroupElements.length; i++) {
             expect(decryptedGroupElements[i].equals(_zpGroupElements[i]))
                 .toBeTruthy();
           }
         });
       });

    it('and throw an exception when attempting to ElGamal encrypt, using a short exponent, for a group that is not of type quadratic residue',
       function() {
         expect(function() {
           _elGamalEncrypterLarge.encryptGroupElements(
               _zpGroupElementsLargeNonQR, true);
         }).toThrow();
       });

    it('and throw an exception when attempting to ElGamal encrypt, using a short exponent, for a group whose order is smaller than this exponent',
       function() {
         expect(function() {
           _elGamalEncrypter.encryptGroupElements(_zpGroupElements, true);
         }).toThrow();
       });

    it('and fail to decrypt some encrypted elements when using a random seed but not reinitializing the random generator with the seed',
       function() {
         var encryptedGroupElements =
             _elGamalEncrypterUsingSeed.encryptGroupElements(
                 _zpGroupElementsLarge);

         // Decryption should be successful the first time.
         var decryptedGroupElements =
             _elGamalRandomDecrypter.decrypt(encryptedGroupElements, true);
         expect(decryptedGroupElements.length)
             .toBe(_zpGroupElementsLarge.length);
         for (var i = 0; i < decryptedGroupElements.length; i++) {
           expect(decryptedGroupElements[i].equals(_zpGroupElementsLarge[i]))
               .toBeTruthy();
         }

         // Decryption should fail the second time since the random generator
         // was not re-initialized with the seed first.
         decryptedGroupElements =
             _elGamalRandomDecrypter.decrypt(encryptedGroupElements, true);
         expect(decryptedGroupElements.length)
             .toBe(_zpGroupElementsLarge.length);
         for (var j = 0; j < decryptedGroupElements.length; j++) {
           expect(decryptedGroupElements[j].equals(_zpGroupElementsLarge[j]))
               .toBeFalsy();
         }
       });
  });

  function generateRandomExponent(zpSubgroup) {
    var exponent;
    cryptolib('commons.mathematical', function(box) {
      do {
        var randomInt = new box.forge.jsbn.BigInteger(
            box.forge.random.getBytes(MAX_EXPONENT_BYTES));
        exponent =
            new box.commons.mathematical.Exponent(zpSubgroup.getQ(), randomInt);
      } while (exponent.getValue().equals(box.forge.jsbn.BigInteger.ZERO));
    });

    return exponent;
  }

  function generateGroupElements(zpSubgroup, numElements) {
    var element;
    var groupElements = [];
    for (var i = 0; i < numElements; i++) {
      element = zpSubgroup.getGenerator().exponentiate(
          generateRandomExponent(zpSubgroup));
      groupElements.push(element);
    }

    return groupElements;
  }

  function validateElGamalPublicKey(elGamalPublicKey) {
    expect(elGamalPublicKey.getGroup().equals(_zpSubgroup)).toBeTruthy();

    var keyElements = elGamalPublicKey.getGroupElementsArray();
    expect(keyElements.length).toBe(NUM_GROUP_ELEMENTS);
    expect(keyElements[0].getElementValue().toString()).toEqual(ELEMENT_1_STR);
    expect(keyElements[1].getElementValue().toString()).toEqual(ELEMENT_2_STR);
  }

  function validateElGamalPrivateKey(elGamalPrivateKey) {
    expect(elGamalPrivateKey.getGroup().equals(_zpSubgroup)).toBeTruthy();

    var keyExponents = elGamalPrivateKey.getExponentsArray();
    expect(keyExponents.length).toBe(NUM_GROUP_ELEMENTS);
    expect(keyExponents[0].getValue().toString()).toEqual(EXPONENT_1_STR);
    expect(keyExponents[1].getValue().toString()).toEqual(EXPONENT_2_STR);
  }
});
