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

var BigInteger = forge.jsbn.BigInteger;

describe('The secure random module should be able to ...', function() {
  var TEST_SEED = new Uint8Array([18, 58, 188]);

  var PRIVATE_KEY_PEM =
      '-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAglypZU45bnf2opnRI+Y51VqouBpvDT33xIB/OtbwKzwVpi+JrjBFtfk33tE9t/dSRs79CK94HRhCWcOiLa2qWPrjeZ9SBiEyScrhIvRZVBF41zBgwQNuRvJCsKmAqlZaFNJDZxEP4repmlBn1CfVFmfrXmOKqwP5F7l9ZtucveRzsfmF1yVPFkW8TMuB3YqMiyymyqHlS8ujCsu5I8tpgPbwuxdMOY94fNhSXrYkY8IuX1g1zdq/Z1jluOaR/UqK4UpnbuJaH/F0VgDNiWh6cTD0DFGEk0b70i5wU4Q3L/S6XZQRvSuADoCbhwBKuFL5pW5n865oLVb5S3wuVdWaGwIDAQABAoIBAC/tn34Wf3kE9BGeGc1oFLVDaqqdVVz5/oEpeR2J7q0GnzMFYUpAhzC7WvY52cYsUPyll1Q9Jx0TUTmteo/uvKWQQFfz4nVMeS+2PoXabolBDzuWlsv/1eiRo0FOYHa/3siu8YcQN9X0DpAkpbfTmT1uoZOHZ3EuucMmOFu7vGn38Grw8bSxpR0uvTtnb8ygC+aB51y38RMyhzQQanrM8FMeAfDAy6IB0Yo7b0c50Cxa6Ax4nqn9LXyGakr5WeAMkgTIOA/GId9SZD4e5eRpq+628pOeR4O9datFltgl6r1+A4ii2VrJsDqeatGtODlX6KRKqwFHoGIa2TjgSZLuorECgYEAxeSZDOOgFsI5mB7RkRzZaQ9znJ15sgdyZiAFZAOUah4hSGdAXNAnZTlrdacduXEu3EfkpuPToX7xZSv5FRYwfBwMwCLeytlGLPjQzWejZGbo4+KqgzWb9fECDYVtDPlJ/+yLih9nt67BHweJKxYydl18rVigdVyy22X86NijSykCgYEAqKPUrXZAo+TJvmTw4tgsibJgvXBYBhmsej8mGNQw+Nyp2gV28sgm61ifIeXKS8teq+MFwGA6cHQedbsCqhMHokdhESZmlbWxhSFLihQcewBxwvrBwbaxI23yXRzwMewznZFL032PpcbqrmwFmcSSEZ3nmbvTH6ShqLW+pzDNp6MCgYBQLzdgxJ7qedqSa/JohTMG4e7rh9d2rpPJE7J7ewPZF8pOpx+qO+Gqn2COdJ+Ts2vUcAETKn9nEaPIZc/wnmQY9dioxbhWo0FPGaaphBPtq9Ez/XUv4zoFppk5V1X/isdUPsmvttf00oeIBiqrXbwmv+yz5JRn2Z7TTXjz9Ev+OQKBgQCUuoCMRzl1EgcXIqEL/0kwW6BUEqufHa9u1Ri9Vw6lvL8T6DPipMEmWK9nzuid9gtVns/ovTVtDgv7GuabplLaPQePf4WDzY11c0rSyS/hDyBFrK+LL5uEOqhAlJAGB2HyOj1clWVF+GvrTpuV5LZKUS/79pmZU7G7QCaX/0Ow7wKBgC/kDH7cmWQnWvvJ5izrx/7PogQVPOLELeUIGLu/hjsSdDKiFCxCUZ948+9NuG+DnpXDWzw//r8mPBRRGGsqFws5Aipp7yjQ3kRDCCzGelPCVhHyfmKqA+8ewXPulKS3/wIyHIvaXmsuAtTfurHtpRyzjKmCBK1Y6WQ3trIXvo7s-----END RSA PRIVATE KEY-----';

  var PIN_ENTROPY = 100;

  var service_;
  var randomGenerator_;

  beforeEach(function() {
    service_ = secureRandom.newService();

    randomGenerator_ = service_.newRandomGenerator();
  });

  describe('create a secure random service that should be able to ..', function() {
    describe(
        'create a secure random generator that should be able to', function() {
          it('generate 10 random bytes', function() {
            var numBytes = 10;
            var bytes = randomGenerator_.nextBytes(numBytes);
            expect(bytes).toBeDefined();
            expect(bytes.length).not.toBeGreaterThan(numBytes);
          });

          it('generate a specified number of random bytes', function() {
            var numBytes = 16;
            var bytes = randomGenerator_.nextBytes(numBytes);
            expect(bytes).toBeDefined();
            expect(bytes.numBytes).not.toBeGreaterThan(numBytes);
          });

          it('generate a random BigInteger of a specified maximum bit length',
             function() {
               for (var maxNumBits = 1; maxNumBits <= 20; maxNumBits++) {
                 var randomBigInteger =
                     randomGenerator_.nextBigInteger(maxNumBits);

                 expect(randomBigInteger).not.toBeLessThan(0);
                 expect(randomBigInteger.bitLength())
                     .not.toBeGreaterThan(maxNumBits);
               }
             });

          it('generate a random BigInteger of a specified number of digits',
             function() {
               var maxValue;

               for (var numDigits = 1; numDigits <= 10; numDigits++) {
                 maxValue = Math.pow(10, numDigits);

                 var randomBigInteger =
                     randomGenerator_.nextBigIntegerByDigits(numDigits);

                 expect(randomBigInteger.compareTo(BigInteger.ZERO))
                     .toBeGreaterThan(0);
                 expect(randomBigInteger.compareTo(
                            new BigInteger(maxValue.toString())))
                     .toBeLessThan(0);
               }
             });

          it('be created by specifying a maximum allowed number of bytes',
             function() {
               var maxNumBytes = constants.SECURE_RANDOM_MAX_NUM_BYTES + 1;
               var randomGenerator_ =
                   service_.newRandomGenerator({maxNumBytes: maxNumBytes});

               var randomBytes = randomGenerator_.nextBytes(maxNumBytes);
               expect(randomBytes).toBeDefined();
               expect(randomBytes.length).not.toBeGreaterThan(maxNumBytes);

               expect(function() {
                 randomGenerator_.nextBytes(maxNumBytes + 1);
               }).toThrow();
             });

          it('be created by specifying maximum allowed number of digits',
             function() {
               var maxNumDigits = constants.SECURE_RANDOM_MAX_NUM_BYTES + 1;
               var randomGenerator_ =
                   service_.newRandomGenerator({maxNumDigits: maxNumDigits});

               var randomBigInteger =
                   randomGenerator_.nextBigIntegerByDigits(maxNumDigits);
               expect(randomBigInteger).toBeDefined();
               expect(randomBigInteger.compareTo(BigInteger.ZERO))
                   .toBeGreaterThan(0);
               expect(randomBigInteger.compareTo(
                          new BigInteger('10').pow(maxNumDigits)))
                   .toBeLessThan(0);

               expect(function() {
                 randomGenerator_.nextBigIntegerByDigits(maxNumDigits + 1);
               }).toThrow();
             });

          it('throw an exception when a negative input argument is passed',
             function() {
               expect(function() {
                 randomGenerator_.nextBytes(-1);
               }).toThrow();

               expect(function() {
                 randomGenerator_.nextBigInteger(-1);
               }).toThrow();

               expect(function() {
                 randomGenerator_.nextBigIntegerByDigits(-1);
               }).toThrow();
             });

          it('throw an exception when a non-numeric input argument is passed',
             function() {
               expect(function() {
                 randomGenerator_.nextBytes('abc');
               }).toThrow();

               expect(function() {
                 randomGenerator_.nextBigInteger('abc');
               }).toThrow();

               expect(function() {
                 randomGenerator_.nextBigIntegerByDigits('abc');
               }).toThrow();
             });

          it('throw an exception when a too-large input argument is passed',
             function() {
               expect(function() {
                 randomGenerator_.nextBytes(
                     constants.SECURE_RANDOM_MAX_NUM_BYTES + 1);
               }).toThrow();

               expect(function() {
                 randomGenerator_.nextBigIntegerByDigits(
                     constants.SECURE_RANDOM_MAX_NUM_DIGITS + 1);
               }).toThrow();
             });
        });

    it('initialize its PRNG from a provided seed', function() {
      var service = secureRandom.newService(
          {prngSeed: TEST_SEED, noDefaultCollectors: true});
      var randomGenerator = service.newRandomGenerator();

      expect(randomGenerator.nextBigIntegerByDigits(2).toString())
          .toEqual('21');
      expect(randomGenerator.nextBigIntegerByDigits(4).toString())
          .toEqual('2950');
      expect(randomGenerator.nextBigIntegerByDigits(16).toString())
          .toEqual('5574593524320719');
    });

    it('generate a random seed to instantiate other secure random services',
       function() {
         var seed = service_.nextSeed();
         expect(seed.length)
             .toBe(constants.SECURE_RANDOM_DEFAULT_NUM_SEED_BYTES);

         seed = service_.nextSeed(16);
         expect(seed.length).toBe(16);

         var service1 = secureRandom.newService(
             {prngSeed: seed, noDefaultCollectors: true});
         var randomGenerator1 = service1.newRandomGenerator();
         var randomInt1 = randomGenerator1.nextBigIntegerByDigits(2);

         var service2 = secureRandom.newService(
             {prngSeed: seed, noDefaultCollectors: true});
         var randomGenerator2 = service2.newRandomGenerator();
         var randomInt2 = randomGenerator2.nextBigIntegerByDigits(2);

         expect(randomInt1).toEqual(randomInt2);
       });

    it('create two different secure random generators that use the same PRNG',
       function() {
         var service = secureRandom.newService(
             {prngSeed: TEST_SEED, noDefaultCollectors: true});
         var randomGenerator1 = service.newRandomGenerator();
         var randomGenerator2 = service.newRandomGenerator();

         expect(randomGenerator1.nextBigIntegerByDigits(2).toString())
             .toEqual('21');
         expect(randomGenerator1.nextBigIntegerByDigits(4).toString())
             .toEqual('2950');
         expect(randomGenerator1.nextBigIntegerByDigits(16).toString())
             .toEqual('5574593524320719');

         expect(randomGenerator2.nextBigIntegerByDigits(2).toString())
             .toEqual('12');
         expect(randomGenerator2.nextBigIntegerByDigits(4).toString())
             .toEqual('6576');
         expect(randomGenerator2.nextBigIntegerByDigits(16).toString())
             .toEqual('2672997009079990');
       });

    it('start and stop the entropy collection manually', function() {
      var callbackCalled = false;
      var callback = function() {
        callbackCalled = true;
      };
      service_.startEntropyCollection(callback);
      var maxEntropyCollected = service_.stopEntropyCollection();

      expect(callbackCalled).toBeTruthy();
      expect(typeof maxEntropyCollected).toBe('boolean');
    });

    it('measure the entropy collected as a percentage of the maximum entropy required',
       function() {
         var service = secureRandom.newService({noDefaultCollectors: true});
         service.startEntropyCollection();
         service.stopEntropyCollection();

         expect(service.getEntropyPercentage()).toBe(0);

         service = secureRandom.newService();
         service.startEntropyCollection();
         service.stopEntropyCollection();

         expect(service.getEntropyPercentage()).toBeGreaterThan(0);
         expect(service.getEntropyPercentage()).toBeLessThan(101);
       });

    it('collect some entropy with the maximum entropy counter option set',
       function() {
         var service = secureRandom.newService({maxEntropyCounter: 150});

         expect(service.getEntropyPercentage()).toBeGreaterThan(0);
         expect(service.getEntropyPercentage()).toBeLessThan(101);
       });

    it('collect some entropy with the hash updater interval option set',
       function() {
         var service = secureRandom.newService({hashUpdaterInterval: 500});

         expect(service.getEntropyPercentage()).toBeGreaterThan(0);
         expect(service.getEntropyPercentage()).toBeLessThan(101);
       });

    it('collect some entropy using private key data', function() {
      var privateKeyData = {
        privateKey: PRIVATE_KEY_PEM,
        pinEntropy: PIN_ENTROPY
      };

      var service = secureRandom.newService(
          {noDefaultCollectors: true, privateKeyData: privateKeyData});

      expect(service.getEntropyPercentage()).toBeGreaterThan(0);
      expect(service.getEntropyPercentage()).toBeLessThan(101);
    });

    it('generate bytes with the Forge-compliant bytes generation method',
       function() {
         var service = secureRandom.newService(
             {prngSeed: TEST_SEED, noDefaultCollectors: true});
         var randomGenerator = service.newRandomGenerator();

         expect(codec.base64Encode(
                    codec.binaryDecode(randomGenerator.getBytesSync(2))))
             .toEqual('k5U=');
         expect(codec.base64Encode(
                    codec.binaryDecode(randomGenerator.getBytesSync(4))))
             .toEqual('FQJ+/Q==');
         expect(codec.base64Encode(
                    codec.binaryDecode(randomGenerator.getBytesSync(6))))
             .toEqual('o9f6kc4U');
       });
  });
});
