/*!
 * aead.js - aead for bcoin
 * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('assert');
const ChaCha20 = require('./chacha20');
const Poly1305 = require('./poly1305');

/**
 * AEAD (used for bip151)
 * @alias module:crypto.AEAD
 * @constructor
 * @see https://github.com/openssh/openssh-portable
 * @see https://tools.ietf.org/html/rfc7539#section-2.8
 */

function AEAD() {
  if (!(this instanceof AEAD))
    return new AEAD();

  this.chacha20 = new ChaCha20();
  this.poly1305 = new Poly1305();
  this.aadLen = 0;
  this.cipherLen = 0;
  this.polyKey = null;
}

/**
 * Initialize the AEAD with a key and iv.
 * @param {Buffer} key
 * @param {Buffer} iv - IV / packet sequence number.
 */

AEAD.prototype.init = function init(key, iv) {
  const polyKey = Buffer.allocUnsafe(32);
  polyKey.fill(0);

  this.chacha20.init(key, iv);
  this.chacha20.encrypt(polyKey);
  this.poly1305.init(polyKey);

  // We need to encrypt a full block
  // to get the cipher in the correct state.
  this.chacha20.encrypt(Buffer.allocUnsafe(32));

  // Counter should be one.
  assert(this.chacha20.getCounter() === 1);

  // Expose for debugging.
  this.polyKey = polyKey;

  this.aadLen = 0;
  this.cipherLen = 0;
};

/**
 * Update the aad (will be finalized
 * on an encrypt/decrypt call).
 * @param {Buffer} aad
 */

AEAD.prototype.aad = function aad(data) {
  assert(this.cipherLen === 0, 'Cannot update aad.');
  this.poly1305.update(data);
  this.aadLen += data.length;
};

/**
 * Encrypt a piece of data.
 * @param {Buffer} data
 */

AEAD.prototype.encrypt = function encrypt(data) {
  if (this.cipherLen === 0)
    this.pad16(this.aadLen);

  this.chacha20.encrypt(data);
  this.poly1305.update(data);
  this.cipherLen += data.length;

  return data;
};

/**
 * Decrypt a piece of data.
 * @param {Buffer} data
 */

AEAD.prototype.decrypt = function decrypt(data) {
  if (this.cipherLen === 0)
    this.pad16(this.aadLen);

  this.cipherLen += data.length;
  this.poly1305.update(data);
  this.chacha20.encrypt(data);

  return data;
};

/**
 * Authenticate data without decrypting.
 * @param {Buffer} data
 */

AEAD.prototype.auth = function auth(data) {
  if (this.cipherLen === 0)
    this.pad16(this.aadLen);

  this.cipherLen += data.length;
  this.poly1305.update(data);

  return data;
};

/**
 * Finalize the aead and generate a MAC.
 * @returns {Buffer} MAC
 */

AEAD.prototype.finish = function finish() {
  const len = Buffer.allocUnsafe(16);
  let lo, hi;

  // The RFC says these are supposed to be
  // uint32le, but their own fucking test
  // cases fail unless they are uint64le's.
  lo = this.aadLen % 0x100000000;
  hi = (this.aadLen - lo) / 0x100000000;
  len.writeUInt32LE(lo, 0, true);
  len.writeUInt32LE(hi, 4, true);

  lo = this.cipherLen % 0x100000000;
  hi = (this.cipherLen - lo) / 0x100000000;
  len.writeUInt32LE(lo, 8, true);
  len.writeUInt32LE(hi, 12, true);

  if (this.cipherLen === 0)
    this.pad16(this.aadLen);

  this.pad16(this.cipherLen);
  this.poly1305.update(len);

  return this.poly1305.finish();
};

/**
 * Pad a chunk before updating mac.
 * @private
 * @param {Number} size
 */

AEAD.prototype.pad16 = function pad16(size) {
  size %= 16;

  if (size === 0)
    return;

  const pad = Buffer.allocUnsafe(16 - size);
  pad.fill(0);

  this.poly1305.update(pad);
};

/*
 * Expose
 */

module.exports = AEAD;
