/*
 * 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('derivation module: ', function() {
  'use strict';

  it('should exist', function() {
    cryptolib('primitives.derivation', function(box) {
      expect(box.primitives.derivation).toBeDefined();
    });
  });

  it('should contain a PBKDF generator factory', function() {
    cryptolib('primitives.derivation', function(box) {
      var pbkdfGeneratorFactory =
          new box.primitives.derivation.factory.SecretKeyGeneratorFactory();
      expect(pbkdfGeneratorFactory).toBeDefined();
    });
  });

  describe('PBKDF generator factory', function() {
    it('should generate a secretKey.', function() {
      cryptolib(
          'primitives.derivation', 'commons.utils', 'primitives.securerandom',
          function(box) {
            var converters = new box.commons.utils.Converters();
            var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                            .SecretKeyGeneratorFactory();
            var secureRandomFactory =
                new box.primitives.securerandom.factory.SecureRandomFactory();

            var cryptoRandomBytes = secureRandomFactory.getCryptoRandomBytes();
            var cryptoPbkdfSecretKeyGenerator =
                pbkdfGeneratorFactory.createPBKDF();

            var encodedPassword = converters.base64Encode('scytl');

            var salt = cryptoRandomBytes.nextRandom(20);
            var saltB64 = converters.base64Encode(salt);

            var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
                encodedPassword, saltB64);

            expect(secretKeyB64).toBeDefined();

            expect(converters.base64Decode(secretKeyB64).length)
                .toBe(box.policies.primitives.derivation.pbkdf.keyLengthBytes);
          });
    });

    it('should generate a secretKey with hash SHA256 and salt length 32 bytes.',
       function() {
         cryptolib(
             'primitives.derivation', 'commons.utils',
             'primitives.securerandom', function() {
               var f = function(box) {
                 var converters = new box.commons.utils.Converters();
                 var pbkdfGeneratorFactory =
                     new box.primitives.derivation.factory
                         .SecretKeyGeneratorFactory();
                 var secureRandomFactory = new box.primitives.securerandom
                                               .factory.SecureRandomFactory();

                 var cryptoRandomBytes =
                     secureRandomFactory.getCryptoRandomBytes();
                 var cryptoPbkdfSecretKeyGenerator =
                     pbkdfGeneratorFactory.createPBKDF();

                 var encodedPassword = converters.base64Encode('scytl');

                 var salt = cryptoRandomBytes.nextRandom(32);
                 var saltB64 = converters.base64Encode(salt);

                 var secretKeyB64 =
                     cryptoPbkdfSecretKeyGenerator.generateSecret(
                         encodedPassword, saltB64);

                 expect(secretKeyB64).toBeDefined();

                 expect(converters.base64Decode(secretKeyB64).length)
                     .toBe(box.policies.primitives.derivation.pbkdf
                               .keyLengthBytes);
               };

               f.policies = {
                 primitives: {
                   secureRandom: {
                     provider: Config.primitives.secureRandom.provider.SCYTL
                   },
                   derivation: {
                     pbkdf: {
                       provider: 'Forge',
                       hash: 'SHA256',
                       saltLengthBytes: 32,
                       keyLengthBytes: 32,
                       iterations: 1
                     }
                   }
                 }
               };

               cryptolib(
                   'primitives.derivation', 'commons.utils',
                   'primitives.securerandom', f);
             });
       });

    it('should generate a secretKey that matches one created with another engine, if the policies and parameters are the same.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           var calculatedKey = 'vD80dK3PvPrQkALIY87oi7agHGoOCGGhvkZFuNASgL8=';
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var encodedPassword = converters.base64Encode('scytl');
           var saltB64 = 'CpHq+38iqF3Ls7j97qBQ2A9Diy8=';

           var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
               encodedPassword, saltB64);

           expect(secretKeyB64).toEqual(calculatedKey);
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should generate a secretKey that does not match one created with another engine, if the key lengths are different.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           var calculatedKey = 'vD80dK3PvPrQkALIY87oi7agHGoOCGGhvkZFuNASgL8=';
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var encodedPassword = converters.base64Encode('scytl');
           var saltB64 = 'CpHq+38iqF3Ls7j97qBQ2A9Diy8=';

           var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
               encodedPassword, saltB64);

           expect(secretKeyB64).not.toEqual(calculatedKey);
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 16,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should generate a secretKey that does not match one created with another engine, if the number of iterations are different.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           var calculatedKey = 'T5dDOwtsmPEnIYuCqUAldZZo4tO86FgcO7GAgDhaxYc=';
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var encodedPassword = converters.base64Encode('scytl');
           var saltB64 = 'CpHq+38iqF3Ls7j97qBQ2A9Diy8=';

           var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
               encodedPassword, saltB64);

           expect(secretKeyB64).not.toEqual(calculatedKey);
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 8000
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should generate a secretKey that does not match one created with another engine, if the password is different.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           var calculatedKey = 'T5dDOwtsmPEnIYuCqUAldZZo4tO86FgcO7GAgDhaxYc=';
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var encodedPassword = converters.base64Encode('Scytl');
           var saltB64 = 'CpHq+38iqF3Ls7j97qBQ2A9Diy8=';

           var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
               encodedPassword, saltB64);

           expect(secretKeyB64).not.toEqual(calculatedKey);
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should generate a secretKey that does not match one created with another engine, if the salt is different.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           var calculatedKey = 'T5dDOwtsmPEnIYuCqUAldZZo4tO86FgcO7GAgDhaxYc=';
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var encodedPassword = converters.base64Encode('scytl');
           var saltB64 = 'cpHq+38iqF3Ls7j97qBQ2A9Diy8=';

           var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
               encodedPassword, saltB64);

           expect(secretKeyB64).not.toEqual(calculatedKey);
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should generate a secretKey that does not match one created with another engine, if the hash algorithm is different.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var saltB64 = 'T4DtI/lcymMpBjJ7Ib9xpyS6xpuB5/+G68X3dT66eJs=';
           var generatedKeySHA1 =
               'Ol8LDensA6B3CNhguSJrcuAnHAaB1NnGL0VfgbzDi34=';

           var encodedPassword = converters.base64Encode('scytl');

           var secretKeyB64 = cryptoPbkdfSecretKeyGenerator.generateSecret(
               encodedPassword, saltB64);

           expect(secretKeyB64).not.toEqual(generatedKeySHA1);
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 32,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should throw an error when an unsupported provider is specified in the policy.',
       function() {
         var f = function(box) {
           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           expect(function() {
             pbkdfGeneratorFactory.createPBKDF();
           }).toThrow();
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'WRONG',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should throw an error when an unsupported hash algorithm is specified in the policy.',
       function() {
         var f = function(box) {
           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           expect(function() {
             pbkdfGeneratorFactory.createPBKDF();
           }).toThrow();
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'WRONG',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should throw an error when an unsupported minimum salt length is specified in the policy.',
       function() {
         var f = function(box) {
           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           expect(function() {
             pbkdfGeneratorFactory.createPBKDF();
           }).toThrow();
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 21,
                 keyLengthBytes: 32,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should throw an error when an unsupported key length is specified in the policy.',
       function() {
         var f = function(box) {
           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           expect(function() {
             pbkdfGeneratorFactory.createPBKDF();
           }).toThrow();
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 33,
                 iterations: 1
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should throw an error when an unsupported number of iterations is specified in the policy.',
       function() {
         var f = function(box) {
           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();

           expect(function() {
             pbkdfGeneratorFactory.createPBKDF();
           }).toThrow();
         };

         f.policies = {
           primitives: {
             derivation: {
               pbkdf: {
                 provider: 'Forge',
                 hash: 'SHA256',
                 saltLengthBytes: 20,
                 keyLengthBytes: 32,
                 iterations: 2
               }
             }
           }
         };

         cryptolib('primitives.derivation', 'commons.utils', f);
       });

    it('should throw an error when a salt with length less than the minimum specified in the policy is used.',
       function() {
         var f = function(box) {
           var converters = new box.commons.utils.Converters();

           var secureRandomFactory =
               new box.primitives.securerandom.factory.SecureRandomFactory();
           var randomByteGenerator = secureRandomFactory.getCryptoRandomBytes();
           var salt = randomByteGenerator.nextRandom(
               box.policies.primitives.derivation.pbkdf.saltLengthBytes - 1);
           var saltB64 = converters.base64Encode(salt);

           var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                           .SecretKeyGeneratorFactory();
           var cryptoPbkdfSecretKeyGenerator =
               pbkdfGeneratorFactory.createPBKDF();

           var encodedPassword = converters.base64Encode('scytl');

           expect(function() {
             cryptoPbkdfSecretKeyGenerator.generateSecret(
                 encodedPassword, saltB64);
           }).toThrow();
         };

         cryptolib(
             'primitives.securerandom', 'primitives.derivation',
             'commons.utils', f);
       });

    it('should produce the same results when using SJCL or Forge as the cryptographic service provider.',
       function() {
         cryptolib(
             'primitives.derivation', 'commons.utils',
             'primitives.securerandom', function(box) {
               var converters = new box.commons.utils.Converters();
               var pbkdfGeneratorFactory = new box.primitives.derivation.factory
                                               .SecretKeyGeneratorFactory();
               var secureRandomFactory = new box.primitives.securerandom.factory
                                             .SecureRandomFactory();

               var cryptoRandomBytes =
                   secureRandomFactory.getCryptoRandomBytes();
               var cryptoPbkdfSecretKeyGenerator =
                   pbkdfGeneratorFactory.createPBKDF();

               var password =
                   generateRandomString(Math.floor(Math.random() * 128) + 1);
               var passwordB64 = converters.base64Encode(password);

               var salt = cryptoRandomBytes.nextRandom(20);
               var saltB64 = converters.base64Encode(salt);

               var secretKeyFromSjclB64 =
                   cryptoPbkdfSecretKeyGenerator.generateSecret(
                       passwordB64, saltB64);

               var iterations =
                   cryptolibPolicies.primitives.derivation.pbkdf.iterations;
               var keyLengthBytes =
                   cryptolibPolicies.primitives.derivation.pbkdf.keyLengthBytes;

               var secretKeyFromForge = forge.pkcs5.pbkdf2(
                   password, salt, iterations, keyLengthBytes,
                   getHashGenerator(
                       cryptolibPolicies.primitives.derivation.pbkdf.hash,
                       Config.primitives.derivation.pbkdf.hash));

               var secretKeyFromForgeB64 =
                   converters.base64Encode(secretKeyFromForge);

               expect(secretKeyFromForgeB64).toBe(secretKeyFromSjclB64);
             });
       });
  });
});

function generateRandomString(length) {
  'use strict';

  var allowedChars =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,-./:;<=>?@[\]^_`{|}~';

  var str = '';
  for (var i = 0; i < length; i++) {
    str += allowedChars.charAt(Math.floor(Math.random() * allowedChars.length));
  }

  return str;
}

function getHashGenerator(algorithm, config) {
  'use strict';

  if (algorithm === config.SHA256) {
    return forge.sha256.create();
  } else {
    throw 'Hash algorithm \'' + algorithm + '\' is unsupported';
  }
}