/*
 * 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 pbkdf = require('../lib/index');
var codec = require('scytl-codec');
var forge = require('node-forge');

describe('The PBKDF module should be able to ...', function() {
  var PASSWORD = 'Test';
  var SALT = 'TestTestTestTestTestTestTestTest';

  var salt_;
  var tooSmallsalt_;
  var deriver_;
  var key_;
  var policy_;

  beforeAll(function() {
    salt_ = codec.utf8Encode(SALT);
    tooSmallsalt_ = codec.utf8Encode('1');
    deriver_ = pbkdf.newService().newDeriver();
    key_ = deriver_.derive(PASSWORD, salt_);
  });

  beforeEach(function() {
    policy_ = cryptoPolicy.newInstance();
  });

  describe('create a PBKDF service that should be able to ..', function() {
    describe('create a PBKDF deriver that should be able to', function() {
      it('derive a key of the expected length, when using the default cryptographic policy.',
         function() {
           var key = deriver_.derive(PASSWORD, salt_);
           expect(key).toBeDefined();

           expect(key.length)
               .toBe(policy_.primitives.keyDerivation.pbkdf.keyLengthBytes);
         });

      it('derive a key of the expected length, when using a specified cryptographic policy.',
         function() {
           policy_.primitives.keyDerivation.pbkdf.keyLengthBytes =
               cryptoPolicy.options.primitives.keyDerivation.pbkdf
                   .keyLengthBytes.KL_32;
           var pbkdfService = pbkdf.newService({policy: policy_});

           var key = pbkdfService.newDeriver().derive(PASSWORD, salt_);
           expect(key).toBeDefined();

           expect(key.length)
               .toBe(cryptoPolicy.options.primitives.keyDerivation.pbkdf
                         .keyLengthBytes.KL_32);
         });

      it('derive the same key every time, for a given cryptographic policy.',
         function() {
           var key = deriver_.derive(PASSWORD, salt_);

           expect(key).toBeDefined();
           expect(key).toEqual(key_);
         });

      it('derive the expected key if salt is provided as string instead of bytes',
         function() {
           var key = deriver_.derive(PASSWORD, SALT);

           expect(key).toBeDefined();
           expect(key).toEqual(key_);
         });

      it('derive different keys when different numbers of iterations are specified.',
         function() {
           policy_.primitives.keyDerivation.pbkdf.numIterations =
               cryptoPolicy.options.primitives.keyDerivation.pbkdf.numIterations
                   .I_1;
           var pbkdfService = pbkdf.newService({policy: policy_});

           var key = pbkdfService.newDeriver().derive(PASSWORD, salt_);

           expect(key).toBeDefined();
           expect(key).not.toEqual(key_);
         });

      it('derive different keys when different passwords are used.',
         function() {
           var key = deriver_.derive(PASSWORD + 'a', salt_);

           expect(key).toBeDefined();
           expect(key).not.toEqual(key_);
         });

      it('derive different keys when different salts are used.', function() {
        var key = deriver_.derive(PASSWORD, codec.utf8Encode(SALT + 'a'));

        expect(key).toBeDefined();
        expect(key).not.toEqual(key_);
      });

      it('derive the same key for the SJCL and Forge cryptographic service providers, given a random password.',
         function() {
           var password =
               generateRandomString(Math.floor(Math.random() * 128) + 1);

           // Derive key using SJCL.
           var derivedKeyFromSjcl = deriver_.derive(password, salt_);

           // Derive key using Forge.
           var numIterations =
               policy_.primitives.keyDerivation.pbkdf.numIterations;
           var keyLengthBytes =
               policy_.primitives.keyDerivation.pbkdf.keyLengthBytes;
           var derivedKeyFromForgeBinaryEncoded = forge.pkcs5.pbkdf2(
               codec.binaryEncode(codec.utf8Encode(password)),
               codec.binaryEncode(salt_), numIterations, keyLengthBytes,
               getHashGenerator(
                   policy_.primitives.keyDerivation.pbkdf.hashAlgorithm,
                   cryptoPolicy.options.primitives.keyDerivation.pbkdf
                       .hashAlgorithm));
           var derivedKeyFromForge =
               codec.binaryDecode(derivedKeyFromForgeBinaryEncoded);

           expect(derivedKeyFromForge).toEqual(derivedKeyFromSjcl);
         });

      it('throw an error when a salt length less than the minimum value specified in the cryptographic policy is used',
         function() {
           expect(function() {
             deriver_.derive(PASSWORD, tooSmallsalt_);
           }).toThrow();
         });
    });
  });
});

function generateRandomString(length) {
  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) {
  if (algorithm === config.SHA256) {
    return forge.sha256.create();
  } else {
    throw 'Hash algorithm \'' + algorithm + '\' is unsupported';
  }
}
