/*
 * 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
 */
/* jshint node:true, jasmine:true */
'use strict';

var cryptoPolicy = require('scytl-cryptopolicy');
var symmetric = require('../lib/index');
var secureRandom = require('scytl-securerandom');
var bitwise = require('scytl-bitwise');
var codec = require('scytl-codec');

describe('The symmetric cryptography module should be able to ...', function() {
  var DATA = 'anystring';
  var OTHER_DATA_1 = DATA + 'a';
  var OTHER_DATA_2 = DATA + 'b';

  var data_;
  var otherData1_;
  var otherData2_;
  var dataParts_;
  var service_;
  var policy_ = cryptoPolicy.newInstance();
  var keyGenerator_;
  var encryptionKey_;
  var macKey_;
  var cipher_;
  var macHandler_;
  var encryptedData_;
  var mac_;

  beforeAll(function() {
    data_ = codec.utf8Encode(DATA);
    otherData1_ = codec.utf8Encode(OTHER_DATA_1);
    otherData2_ = codec.utf8Encode(OTHER_DATA_2);
    dataParts_ = [data_, otherData1_, otherData2_];
    service_ = symmetric.newService();
    policy_ = cryptoPolicy.newInstance();
    keyGenerator_ = service_.newKeyGenerator();
    encryptionKey_ = keyGenerator_.nextEncryptionKey();
    macKey_ = keyGenerator_.nextMacKey();
    cipher_ = service_.newCipher().init(encryptionKey_);
    macHandler_ = service_.newMacHandler().init(macKey_);
    encryptedData_ = cipher_.encrypt(data_);
    mac_ = macHandler_.generate(data_);
  });

  describe('create a symmetric cryptography service that should be able to ..', function() {
    describe('create a secret key generator that should be able to', function() {
      it('generate a secret key for encryption', function() {
        expect(encryptionKey_.length)
            .toBe(policy_.symmetric.secretKey.encryption.lengthBytes);
      });

      it('generate a secret key for mac generation', function() {
        expect(macKey_.length)
            .toBe(policy_.symmetric.secretKey.mac.lengthBytes);
      });

      it('generate secret keys, using a specified cryptographic policy',
         function() {
           var policy = cryptoPolicy.newInstance();
           policy.symmetric.secretKey.encryption.lengthBytes =
               cryptoPolicy.options.symmetric.secretKey.encryption.lengthBytes
                   .KL_32;
           policy.symmetric.secretKey.mac.lengthBytes =
               cryptoPolicy.options.symmetric.secretKey.mac.lengthBytes.KL_64;
           var service = symmetric.newService({policy: policy});
           var keyGenerator = service.newKeyGenerator();

           var encryptionKey = keyGenerator.nextEncryptionKey();
           expect(encryptionKey.length)
               .toBe(policy.symmetric.secretKey.encryption.lengthBytes);

           var macKey = keyGenerator.nextMacKey();
           expect(macKey.length)
               .toEqual(policy.symmetric.secretKey.mac.lengthBytes);
         });

      it('generate secret keys, using a specified secure random service object',
         function() {
           var secureRandomService = secureRandom.newService();
           var service =
               symmetric.newService({secureRandomService: secureRandomService});
           var keyGenerator = service.newKeyGenerator();

           var encryptionKey = keyGenerator.nextEncryptionKey();
           expect(encryptionKey.length)
               .toBe(policy_.symmetric.secretKey.encryption.lengthBytes);

           var macKey = keyGenerator.nextMacKey();
           expect(macKey.length)
               .toBe(policy_.symmetric.secretKey.mac.lengthBytes);
         });
    });

    describe('create a symmetric cipher that should be able to ..', function() {
      it('encrypt and decrypt some data', function() {
        var encryptedData = cipher_.encrypt(data_);
        var decryptedData = cipher_.decrypt(encryptedData);

        expect(decryptedData).toEqual(data_);
      });

      it('encrypt and decrypt some data of type string', function() {
        var encryptedData = cipher_.encrypt(DATA);
        var decryptedData = codec.utf8Decode(cipher_.decrypt(encryptedData));

        expect(decryptedData).toEqual(DATA);
      });

      it('encrypt and decrypt some data, using a specified cryptographic policy',
         function() {
           var policy = cryptoPolicy.newInstance();
           policy.symmetric.secretKey.encryption.lengthBytes =
               cryptoPolicy.options.symmetric.secretKey.encryption.lengthBytes
                   .KL_32;
           policy.symmetric.cipher.algorithm.keyLengthBytes =
               cryptoPolicy.options.symmetric.cipher.algorithm.AES_GCM
                   .keyLengthBytes.KL_32;
           var service = symmetric.newService({policy: policy});

           var key = service.newKeyGenerator().nextEncryptionKey();
           var cipher = service.newCipher().init(key);

           var encryptedData = cipher.encrypt(data_);
           var decryptedData = cipher.decrypt(encryptedData);

           expect(decryptedData).toEqual(data_);
         });

      it('encrypt and decrypt some data, using a specified secure random service object',
         function() {
           var secureRandomService = secureRandom.newService();
           var service =
               symmetric.newService({secureRandomService: secureRandomService});

           var keyGenerator = service.newKeyGenerator();
           var key = keyGenerator.nextEncryptionKey();
           var cipher = service.newCipher().init(key);

           var encryptedData = cipher.encrypt(data_);
           var decryptedData = cipher.decrypt(encryptedData);

           expect(decryptedData).toEqual(data_);
         });

      it('encrypt and decrypt data that contains characters and spaces',
         function() {
           var data = codec.utf8Encode('text with some spaces');

           var encryptedData = cipher_.encrypt(data);
           var decryptedData = cipher_.decrypt(encryptedData);

           expect(decryptedData).toEqual(data);
         });

      it('encrypt and decrypt data that only contains spaces', function() {
        var data = codec.utf8Encode('    ');

        var encryptedData = cipher_.encrypt(data);
        var decryptedData = cipher_.decrypt(encryptedData);

        expect(decryptedData).toEqual(data);
      });

      it('decrypt some data that was encrypted with the AES-GCM algorithm, using Java.',
         function() {
           var data_java = codec.utf8Encode('mUm9EQC8mrDw');
           var encryptionKey_java =
               codec.base64Decode('v3VagUVMuZJ+b+uFSjqGTw==');
           var initVector_java = codec.base64Decode('LGF26O6hR2fgUsHd');
           var encryptedDataJava_java =
               codec.base64Decode('Cvpz8/L+mTv2eFvYpxNoxt+/KJwtAOzG7hl9Dw==');

           var initVectorAndEncryptedData =
               bitwise.concatenate(initVector_java, encryptedDataJava_java);

           var cipher = service_.newCipher().init(encryptionKey_java);
           var decryptedData_java = cipher.decrypt(initVectorAndEncryptedData);

           expect(decryptedData_java).toEqual(data_java);
         });

      it('throw an error when initializing a cipher, using a secret key with a length that differs from that of the cipher',
         function() {
           var policy = cryptoPolicy.newInstance();
           policy.symmetric.secretKey.encryption.lengthBytes =
               cryptoPolicy.options.symmetric.secretKey.encryption.lengthBytes
                   .KL_16;
           policy.symmetric.cipher.algorithm.keyLengthBytes =
               cryptoPolicy.options.symmetric.cipher.algorithm.AES_GCM
                   .keyLengthBytes.KL_32;
           var service = symmetric.newService({policy: policy});
           var key = service.newKeyGenerator().nextEncryptionKey();
           var cipher = service.newCipher();

           expect(function() {
             cipher.init(key);
           }).toThrow();
         });

      it('throw an error when encrypting some data before the cipher has been initialized with a secret key',
         function() {
           var cipher = service_.newCipher();

           expect(function() {
             cipher.encrypt(data_);
           }).toThrow();
         });

      it('throw an error when decrypting some encrypted data before the cipher has been initialized with a secret key',
         function() {
           var cipher = service_.newCipher();

           expect(function() {
             cipher.decrypt(encryptedData_);
           }).toThrow();
         });
    });

    describe('create a MAC handler that should be able to ..', function() {
      it('generate and verify a MAC', function() {
        var mac = macHandler_.generate(data_);
        var verified = macHandler_.verify(mac, data_);

        expect(verified).toBeTruthy();
      });

      it('generate a MAC from multiple data parts', function() {
        for (var i = 0; i < dataParts_.length; i++) {
          macHandler_.update(dataParts_[i]);
        }
        var mac = macHandler_.generate();

        var verified = macHandler_.verify(
            mac, bitwise.concatenate(data_, otherData1_, otherData2_));
        expect(verified).toBeTruthy();

        for (var j = 0; j < (dataParts_.length - 1); j++) {
          macHandler_.update(dataParts_[j]);
        }
        mac = macHandler_.generate(otherData2_);

        verified = macHandler_.verify(
            mac, bitwise.concatenate(data_, otherData1_, otherData2_));
        expect(verified).toBeTruthy();
      });

      it('generate a MAC from multiple data parts, using method chaining',
         function() {
           var mac = macHandler_.update(data_)
                         .update(otherData1_)
                         .update(otherData2_)
                         .generate();

           var verified = macHandler_.verify(
               mac, bitwise.concatenate(data_, otherData1_, otherData2_));
           expect(verified).toBeTruthy();

           mac = macHandler_.update(data_)
                     .update(otherData1_)
                     .generate(otherData2_);

           verified = macHandler_.verify(
               mac, bitwise.concatenate(data_, otherData1_, otherData2_));
           expect(verified).toBeTruthy();
         });

      it('verify a MAC generated from multiple data parts', function() {
        var mac = macHandler_.generate(
            bitwise.concatenate(data_, otherData1_, otherData2_));

        for (var i = 0; i < dataParts_.length; i++) {
          macHandler_.update(dataParts_[i]);
        }
        var verified = macHandler_.verify(mac);
        expect(verified).toBeTruthy();

        for (var j = 0; j < (dataParts_.length - 1); j++) {
          macHandler_.update(dataParts_[j]);
        }
        verified = macHandler_.verify(mac, otherData2_);
        expect(verified).toBeTruthy();
      });

      it('verify a MAC generated from multiple data parts, using method chaining',
         function() {
           var mac = macHandler_.generate(
               bitwise.concatenate(data_, otherData1_, otherData2_));

           var verified = macHandler_.update(data_)
                              .update(otherData1_)
                              .update(otherData2_)
                              .verify(mac);
           expect(verified).toBeTruthy();

           verified = macHandler_.update(data_)
                          .update(otherData1_)
                          .verify(mac, otherData2_);
           expect(verified).toBeTruthy();
         });

      it('generate the expected MAC when using data of type string',
         function() {
           var mac1 = macHandler_.update(data_)
                          .update(otherData1_)
                          .generate(otherData2_);

           var mac2 = macHandler_.update(DATA)
                          .update(OTHER_DATA_1)
                          .generate(OTHER_DATA_2);

           expect(mac2).toEqual(mac1);
         });

      it('verify a MAC when using data of type string', function() {
        var mac =
            macHandler_.update(data_).update(otherData1_).generate(otherData2_);

        var verified = macHandler_.update(DATA)
                           .update(OTHER_DATA_1)
                           .verify(mac, OTHER_DATA_2);

        expect(verified).toBeTruthy();
      });

      it('be automatically reinitialized after MAC generation', function() {
        var mac1 = macHandler_.generate(data_);

        var mac2 = macHandler_.generate(data_);

        expect(mac1).toEqual(mac2);
      });

      it('generate a MAC, using a specified cryptographic policy', function() {
        var policy = cryptoPolicy.newInstance();
        policy.symmetric.mac.hashAlgorithm =
            cryptoPolicy.options.symmetric.mac.hashAlgorithm.SHA512_224;
        var service = symmetric.newService({policy: policy});

        var key = service.newKeyGenerator().nextMacKey();
        var MacHandler = service.newMacHandler().init(key);

        var mac = MacHandler.generate(data_);
        var verified = MacHandler.verify(mac, data_);

        expect(verified).toBeTruthy();
        expect(mac.length).toBe(28);
      });

      it('generate a MAC, using a specified secure random service object',
         function() {
           var secureRandomService = secureRandom.newService();
           var service =
               symmetric.newService({secureRandomService: secureRandomService});

           var key = service.newKeyGenerator().nextMacKey();
           var MacHandler = service.newMacHandler().init(key);

           var mac = MacHandler.generate(data_);

           var verified = MacHandler.verify(mac, data_);

           expect(verified).toBeTruthy();
         });

      it('throw an error when updating a MAC before the MAC handler has been initialized with a secret key',
         function() {
           var macHandler = service_.newMacHandler();

           expect(function() {
             macHandler.update(data_);
           }).toThrow();
         });

      it('throw an error when generating a MAC before the MAC handler has been initialized with a secret key',
         function() {
           var macHandler = service_.newMacHandler();

           expect(function() {
             macHandler.generate(data_);
           }).toThrow();
         });

      it('throw an error when verifying a MAC before the MAC handler has been initialized with a secret key',
         function() {
           var macHandler = service_.newMacHandler();

           expect(function() {
             macHandler.verify(mac_, data_);
           }).toThrow();
         });

      it('throw an error when generating a MAC without either providing data or having previously updated the MAC handler with some data',
         function() {
           var macHandler = service_.newMacHandler().init(macKey_);

           expect(function() {
             macHandler.generate();
           }).toThrow();
         });

      it('throw an error when verifynig a MAC without either providing data or having previously updated the MAC handler with some data',
         function() {
           var macHandler = service_.newMacHandler().init(macKey_);

           expect(function() {
             macHandler.verify(mac_);
           }).toThrow();
         });
    });

    it('throw an error when initializing a cipher, using an algorithm that is not supported by the cryptographic policy',
       function() {
         var policy = cryptoPolicy.newInstance();
         policy.symmetric.secretKey.encryption.lengthBytes =
             cryptoPolicy.options.symmetric.secretKey.encryption.lengthBytes
                 .KL_16;
         policy.symmetric.cipher.algorithm.name = 'Wrong algorithm';
         var service = symmetric.newService({policy: policy});
         var key = service.newKeyGenerator().nextEncryptionKey();
         var cipher = service.newCipher();

         expect(function() {
           cipher.init(key);
         }).toThrow();
       });
  });
});
