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

var cryptoPolicy = require('scytl-cryptopolicy');
var asymmetric = require('../lib/index');
var constants = require('../lib/constants');
var bitwise = require('scytl-bitwise');
var codec = require('scytl-codec');
var forge = require('node-forge');

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

  var PUBLIC_KEY_PEM =
      '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAglypZU45bnf2opnRI+Y51VqouBpvDT33xIB/OtbwKzwVpi+JrjBFtfk33tE9t/dSRs79CK94HRhCWcOiLa2qWPrjeZ9SBiEyScrhIvRZVBF41zBgwQNuRvJCsKmAqlZaFNJDZxEP4repmlBn1CfVFmfrXmOKqwP5F7l9ZtucveRzsfmF1yVPFkW8TMuB3YqMiyymyqHlS8ujCsu5I8tpgPbwuxdMOY94fNhSXrYkY8IuX1g1zdq/Z1jluOaR/UqK4UpnbuJaH/F0VgDNiWh6cTD0DFGEk0b70i5wU4Q3L/S6XZQRvSuADoCbhwBKuFL5pW5n865oLVb5S3wuVdWaGwIDAQAB-----END PUBLIC KEY-----';
  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 data_;
  var otherData1_;
  var otherData2_;
  var dataParts_;
  var service_;
  var signer_;
  var sigVerifier_;
  var encrypter_;
  var decrypter_;
  var signature_;
  var encryptedData_;

  beforeAll(function() {
    data_ = codec.utf8Encode(DATA);
    otherData1_ = codec.utf8Encode(OTHER_DATA_1);
    otherData2_ = codec.utf8Encode(OTHER_DATA_2);
    dataParts_ = [data_, otherData1_, otherData2_];
    service_ = asymmetric.newService();
    signer_ = service_.newSigner().init(PRIVATE_KEY_PEM);
    sigVerifier_ = service_.newSignatureVerifier().init(PUBLIC_KEY_PEM);
    encrypter_ = service_.newEncrypter().init(PUBLIC_KEY_PEM);
    decrypter_ = service_.newDecrypter().init(PRIVATE_KEY_PEM);
    signature_ = signer_.sign(data_);
    encryptedData_ = encrypter_.encrypt(data_);
  });

  describe('create an asymmetric cryptography service that should be able to ..', function() {
    it('create a new RsaPublicKey object', function() {
      var rsaPublicKey1 = service_.newRsaPublicKey({pem: PUBLIC_KEY_PEM});
      var rsaPublicKey2 =
          service_.newRsaPublicKey({n: rsaPublicKey1.n, e: rsaPublicKey1.e});

      expect(rsaPublicKey2.n).toEqual(rsaPublicKey1.n);
      expect(rsaPublicKey2.e).toEqual(rsaPublicKey1.e);

      var rsaPublicKey1Pem = rsaPublicKey1.toPem();
      var offset = rsaPublicKey1Pem.indexOf('\n') + 1;
      expect(rsaPublicKey1Pem.indexOf('\n', offset))
          .toBe(offset + constants.PEM_LINE_LENGTH + 1);

      var rsaPublicKey2Pem = rsaPublicKey1.toPem();
      offset = rsaPublicKey2Pem.indexOf('\n') + 1;
      expect(rsaPublicKey2Pem.indexOf('\n', offset))
          .toBe(offset + constants.PEM_LINE_LENGTH + 1);

      expect(removeNewLineChars(rsaPublicKey1Pem))
          .toBe(removeNewLineChars(PUBLIC_KEY_PEM));
      expect(removeNewLineChars(rsaPublicKey2Pem))
          .toBe(removeNewLineChars(PUBLIC_KEY_PEM));
    });

    it('create a new RsaPrivateKey object', function() {
      var rsaPrivateKey1 = service_.newRsaPrivateKey({pem: PRIVATE_KEY_PEM});
      var params = {
        n: rsaPrivateKey1.n,
        e: rsaPrivateKey1.e,
        d: rsaPrivateKey1.d,
        p: rsaPrivateKey1.p,
        q: rsaPrivateKey1.q,
        dP: rsaPrivateKey1.dP,
        dQ: rsaPrivateKey1.dQ,
        qInv: rsaPrivateKey1.qInv
      };

      var rsaPrivateKey2 = service_.newRsaPrivateKey(params);

      expect(rsaPrivateKey2.n).toEqual(rsaPrivateKey1.n);
      expect(rsaPrivateKey2.e).toEqual(rsaPrivateKey1.e);
      expect(rsaPrivateKey2.d).toEqual(rsaPrivateKey1.d);
      expect(rsaPrivateKey2.p).toEqual(rsaPrivateKey1.p);
      expect(rsaPrivateKey2.q).toEqual(rsaPrivateKey1.q);
      expect(rsaPrivateKey2.dP).toEqual(rsaPrivateKey1.dP);
      expect(rsaPrivateKey2.dQ).toEqual(rsaPrivateKey1.dQ);
      expect(rsaPrivateKey2.qInv).toEqual(rsaPrivateKey1.qInv);

      var rsaPrivateKey1Pem = rsaPrivateKey1.toPem();
      var offset = rsaPrivateKey1Pem.indexOf('\n') + 1;
      expect(rsaPrivateKey1Pem.indexOf('\n', offset))
          .toBe(offset + constants.PEM_LINE_LENGTH + 1);

      var rsaPrivateKey2Pem = rsaPrivateKey2.toPem();
      offset = rsaPrivateKey2Pem.indexOf('\n') + 1;
      expect(rsaPrivateKey2Pem.indexOf('\n', offset))
          .toBe(offset + constants.PEM_LINE_LENGTH + 1);

      expect(removeNewLineChars(rsaPrivateKey1Pem))
          .toBe(removeNewLineChars(PRIVATE_KEY_PEM));
      expect(removeNewLineChars(rsaPrivateKey2Pem))
          .toBe(removeNewLineChars(PRIVATE_KEY_PEM));
    });

    it('create a new KeyPair object', function() {
      var keyPair = service_.newKeyPair(PUBLIC_KEY_PEM, PRIVATE_KEY_PEM);

      expect(removeNewLineChars(keyPair.publicKey))
          .toBe(removeNewLineChars(PUBLIC_KEY_PEM));
      expect(removeNewLineChars(keyPair.privateKey))
          .toBe(removeNewLineChars(PRIVATE_KEY_PEM));
    });

    describe(
        'create a key pair generator that should be able to ..', function() {
          it('generate a new key pair', function() {
            // Note: Key size here is set to value lower than that allowed by
            // policy to keep test time reasonable.
            var policy = cryptoPolicy.newInstance();
            policy.asymmetric.keyPair.encryption.keyLengthBits = 128;
            var service = asymmetric.newService({policy: policy});
            var keyPairGenerator = service.newKeyPairGenerator();

            var keyPair = keyPairGenerator.next();
            var publicKey = keyPair.publicKey;
            var privateKey = keyPair.privateKey;
            expect(publicKey).toBeDefined();
            expect(publicKey).toMatch(/^-----BEGIN PUBLIC/);
            expect(privateKey).toBeDefined();
            expect(privateKey).toMatch(/^-----BEGIN RSA PRIVATE/);

            var modulus = forge.pki.publicKeyFromPem(publicKey).n;
            var keyLengthBits = modulus.toString(2).length;
            expect(keyLengthBits)
                .toBe(policy.asymmetric.keyPair.encryption.keyLengthBits);
          });
        });

    describe('create a signer/signature verifier pair that should be able to ..', function() {
      it('sign some data and verify the signature', function() {
        var signature = signer_.sign(data_);

        expect(sigVerifier_.verify(signature, data_)).toBeTruthy();
      });

      it('sign some data consisting of multiple parts', function() {
        // Call sign with no arguments.
        for (var i = 0; i < dataParts_.length; i++) {
          signer_.update(dataParts_[i]);
        }
        var signature = signer_.sign();

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

        // Call sign with argument consisting of last data part.
        for (var j = 0; j < (dataParts_.length - 1); j++) {
          signer_.update(dataParts_[j]);
        }
        signature = signer_.sign(otherData2_);

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

      it('sign some data consisting of multiple parts, using method chaining',
         function() {
           // Call sign with no arguments.
           var signature = signer_.update(data_)
                               .update(otherData1_)
                               .update(otherData2_)
                               .sign();

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

           // Call sign with argument consisting of last data part.
           signature =
               signer_.update(data_).update(otherData1_).sign(otherData2_);

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

      it('verify a signature of data consisting of multiple parts', function() {
        var signature =
            signer_.sign(bitwise.concatenate(data_, otherData1_, otherData2_));

        // Call verify with no arguments.
        for (var i = 0; i < dataParts_.length; i++) {
          sigVerifier_.update(dataParts_[i]);
        }
        var verified = sigVerifier_.verify(signature);
        expect(verified).toBeTruthy();

        // Call verify with argument consisting of last data part.
        for (var j = 0; j < (dataParts_.length - 1); j++) {
          sigVerifier_.update(dataParts_[j]);
        }
        verified = sigVerifier_.verify(signature, otherData2_);
        expect(verified).toBeTruthy();
      });

      it('verify a signature of data consisting of multiple parts, using method chaining',
         function() {
           var signature = signer_.sign(
               bitwise.concatenate(data_, otherData1_, otherData2_));

           // Calli verify with no arguments.
           var verified = sigVerifier_.update(data_)
                              .update(otherData1_)
                              .update(otherData2_)
                              .verify(signature);
           expect(verified).toBeTruthy();

           // Call verify with argument consisting of last data part.
           verified = sigVerifier_.update(data_)
                          .update(otherData1_)
                          .verify(signature, otherData2_);
           expect(verified).toBeTruthy();
         });

      it('sign some data of type string and verify the signature', function() {
        var signature =
            signer_.update(DATA).update(OTHER_DATA_1).sign(OTHER_DATA_2);

        var verified = sigVerifier_.update(DATA)
                           .update(OTHER_DATA_1)
                           .verify(signature, OTHER_DATA_2);

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

      it('sign some data and verify the signature, using RsaPrivateKey and RsaPublicKey objects',
         function() {
           var rsaPrivateKey =
               new service_.newRsaPrivateKey({pem: PRIVATE_KEY_PEM});
           var signer = service_.newSigner().init(rsaPrivateKey.toPem());

           var rsaPublicKey =
               new service_.newRsaPublicKey({pem: PUBLIC_KEY_PEM});
           var sigVerifier =
               service_.newSignatureVerifier().init(rsaPublicKey.toPem());

           var signature = signer.sign(data_);
           expect(sigVerifier.verify(signature, data_)).toBeTruthy();
         });

      it('throw an error when updating the signer with some data before the signer has been initialized with a private key',
         function() {
           var signer = service_.newSigner();

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

      it('throw an error when signing some data before the signer has been initialized with a private key',
         function() {
           var signer = service_.newSigner();

           expect(function() {
             signer.sign(data_);
           }).toThrow();
         });

      it('throw an error when updating the signature verifier with some data before the verifier has been initialized with a public key',
         function() {
           var sigVerifier = service_.newSignatureVerifier();

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

      it('throw an error when verifying a signature before the signature verifier has been initialized with a public key',
         function() {
           var sigVerifier = service_.newSignatureVerifier();

           expect(function() {
             sigVerifier.verify(signature_, data_);
           }).toThrow();
         });

      it('throw an error when signing without either providing data or having previously updated the signer with some data',
         function() {
           var signer = service_.newSigner().init(PRIVATE_KEY_PEM);

           expect(function() {
             signer.sign();
           }).toThrow();
         });

      it('throw an error when verifying a signature without either providing data or having previously updated the verifier with some data',
         function() {
           var sigVerifier =
               service_.newSignatureVerifier().init(PUBLIC_KEY_PEM);

           expect(function() {
             sigVerifier.verify(signature_);
           }).toThrow();
         });
    });

    describe(
        'create an encrypter/decrypter pair that should be able to ..',
        function() {
          it('encrypt and decrypt some data', function() {
            var encryptedData = encrypter_.encrypt(data_);
            var decryptedData = decrypter_.decrypt(encryptedData);

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

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

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

          it('encrypt and decrypt some data, using RsaPublicKey and RsaPrivateKey objects',
             function() {
               var rsaPublicKey =
                   new service_.newRsaPublicKey({pem: PUBLIC_KEY_PEM});
               var encrypter =
                   service_.newEncrypter().init(rsaPublicKey.toPem());

               var rsaPrivateKey =
                   new service_.newRsaPrivateKey({pem: PRIVATE_KEY_PEM});
               var decrypter =
                   service_.newDecrypter().init(rsaPrivateKey.toPem());

               var encryptedData = encrypter.encrypt(DATA);
               var decryptedData =
                   codec.utf8Decode(decrypter.decrypt(encryptedData));

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

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

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

          it('throw an error when decrypting before the decrypter has been initialized with a private key',
             function() {
               var decrypter = service_.newDecrypter();

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

function removeNewLineChars(str) {
  return str.replace(/(\r\n|\n|\r)/gm, '');
}
