/*
 * 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 */
'use strict';

var validator = require('./input-validator');
var bitwise = require('scytl-bitwise');
var codec = require('scytl-codec');
var forge = require('node-forge');

module.exports = SymmetricCipher;

/**
 * @class SymmetricCipher
 * @classdesc The symmetric cipher API. To instantiate this object, use the
 *            method {@link SymmetricCryptographyService.newCipher}.
 * @hideconstructor
 * @param {Policy}
 *            policy The cryptographic policy to use.
 * @param {SecureRandomService}
 *            secureRandomService The secure random service to use.
 */
function SymmetricCipher(policy, secureRandomService) {
  var keyLengthBytes_ = policy.symmetric.cipher.algorithm.keyLengthBytes;
  var algorithm_ = policy.symmetric.cipher.algorithm.name;
  var tagLengthBytes_ = policy.symmetric.cipher.algorithm.tagLengthBytes;
  var ivLengthBytes_ = policy.symmetric.cipher.ivLengthBytes;

  var randomGenerator_ = secureRandomService.newRandomGenerator();

  var initialized_ = false;

  var cipher_;

  /**
   * Initializes the symmetric cipher with the provided secret key.
   *
   * @function init
   * @memberof SymmetricCipher
   * @param {Uint8Array}
   *            key The key with which to initialize the symmetric cipher.
   * @returns {SymmetricCipher} A reference to this object, to facilitate
   *          method chaining.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.init = function(key) {
    checkInitData(key);

    cipher_ = forge.cipher.createCipher(algorithm_, codec.binaryEncode(key));
    initialized_ = true;

    return this;
  };

  /**
   * Symmetrically encrypts some data. Before using this method, the cipher
   * must have been initialized with a secret key, via the method
   * {@link SymmetricCipher.init}.
   *
   * @function encrypt
   * @memberof SymmetricCipher
   * @param {Uint8Array}
   *            data The data to encrypt. <b>NOTE:</b> Data of type
   *            <code>string</code> will be UTF-8 encoded.
   * @returns {Uint8Array} The bitwise concatenation of the initialization
   *          vector and the encrypted data.
   * @throws {Error}
   *             If the input data validation fails, the cipher was not
   *             initialized or the encryption process fails.
   */
  this.encrypt = function(data) {
    if (!initialized_) {
      throw new Error(
          'Could not encrypt; Symmetric cipher was not initialized with any secret key');
    }

    if (typeof data === 'string') {
      data = codec.utf8Encode(data);
    }
    validator.checkIsInstanceOf(
        data, Uint8Array, 'Uint8Array', 'Data to symmetrically encrypt');

    try {
      var iv = codec.binaryEncode(randomGenerator_.nextBytes(ivLengthBytes_));

      // Create a byte buffer for data.
      var dataBuffer = new forge.util.ByteBuffer(codec.binaryEncode(data));

      // Only for the GCM mode
      var gcmAuthTagByteLength = tagLengthBytes_;

      if (typeof gcmAuthTagBitLength !== 'undefined') {
        cipher_.start({iv: iv, tagLength: gcmAuthTagByteLength});
      } else {
        cipher_.start({iv: iv});
      }

      cipher_.update(dataBuffer);
      cipher_.finish();

      var encryptedData = cipher_.output.getBytes().toString();

      if (typeof gcmAuthTagByteLength !== 'undefined') {
        var gcmAuthTag = cipher_.mode.tag.getBytes();
        encryptedData = encryptedData + gcmAuthTag.toString();
      }

      var initVectorAndEncryptedData = iv + encryptedData;

      return codec.binaryDecode(initVectorAndEncryptedData);
    } catch (error) {
      throw new Error(
          'Data could not be symmetrically encrypted: ' + error.message);
    }
  };

  /**
   * Symmetrically decrypts some data, using the initialization vector
   * provided with the encrypted data. Before using this method, the cipher
   * must have been initialized with a secret key, via the method
   * {@link SymmetricCipher.init}.
   *
   * @function decrypt
   * @memberof SymmetricCipher
   * @param {Uint8Array}
   *            initVectorAndEncryptedData The bitwise concatenation of the
   *            initialization vector and the encrypted data.
   * @returns {Uint8Array} The decrypted data. <b>NOTE:</b> To retrieve data
   *          of type <code>string</code>, apply method
   *          <code>codec.utf8Decode</code> to result.
   * @throws {Error}
   *             If the input data validation or the decryption process fails.
   */
  this.decrypt = function(initVectorAndEncryptedData) {
    if (!initialized_) {
      throw new Error(
          'Could not decrypt; Symmetric cipher was not initialized with any secret key');
    }

    validator.checkIsInstanceOf(
        initVectorAndEncryptedData, Uint8Array, 'Uint8Array',
        'Concatenation of initialization vector and encrypted data to symmetrically decrypt');

    try {
      var initVector =
          bitwise.slice(initVectorAndEncryptedData, 0, ivLengthBytes_);
      var encryptedData = bitwise.slice(
          initVectorAndEncryptedData, ivLengthBytes_,
          initVectorAndEncryptedData.length);

      // Only for the GCM mode
      var gcmAuthTagByteLength = tagLengthBytes_;

      if (typeof gcmAuthTagByteLength !== 'undefined') {
        var offset = encryptedData.length - gcmAuthTagByteLength;

        var gcmAuthTag =
            bitwise.slice(encryptedData, offset, encryptedData.length);

        encryptedData = bitwise.slice(encryptedData, 0, offset);

        var gcmAuthTagBitLength = gcmAuthTagByteLength * 8;

        cipher_.start({
          iv: codec.binaryEncode(initVector),
          tagLength: gcmAuthTagBitLength,
          tag: codec.binaryEncode(gcmAuthTag)
        });

      } else {
        cipher_.start({iv: initVector});
      }

      var encryptedDataBuffer =
          new forge.util.ByteBuffer(codec.binaryEncode(encryptedData));

      cipher_.update(encryptedDataBuffer);
      cipher_.finish();
      var decryptedData =
          codec.binaryDecode(cipher_.output.getBytes().toString());

      return decryptedData;
    } catch (error) {
      throw new Error('Data could not be symmetrically decrypted; ' + error);
    }
  };

  function checkInitData(key) {
    validator.checkIsInstanceOf(
        key, Uint8Array, 'Uint8Array',
        'Secret key with which to initialize symmetric cipher');

    if (key.length !== keyLengthBytes_) {
      throw new Error(
          'Expected secret key byte length ' + keyLengthBytes_ +
          ' ; Found: ' + key.length);
    }
  }
}
