Source: types/session.js

'use strict'

// Constants
const {
  LIGHTNING_PROTOCOL_H_INIT,
  LIGHTNING_PROTOCOL_PROLOGUE
} = require('../constants');

// Dependencies
const BN = require('bn.js');
const struct = require('struct');
const crypto = require('crypto');

// Fabric Types
const Entity = require('./entity');
const Key = require('./key');

/**
 * The {@link Session} type describes a connection between {@link Peer}
 * objects, and includes its own lifecycle.
 */
class Session extends Entity {
  /**
   * Creates a new {@link Session}.
   * @param {Object} settings 
   */
  constructor (settings = {}) {
    super(settings);

    this.settings = Object.assign({
      ephemeral: true,
      initiator: null,
      recipient: null
    }, settings);

    // Session Key
    this.key = this._getOddKey();
    this.derived = null;

    // Internal State
    this._state = {
      clock: 0,
      meta: {
        messages: 0,
        received: 0,
        sent: 0
      }
    };

    // Protocol Components
    this.components = {};

    // Map of messages
    this.store = {};

    // List of Session messages
    this.messages = [];

    // Status flag
    this.status = 'initialized';

    return this;
  }

  get id () {
    return this.key.id;
  }

  get clock () {
    return this._state.clock;
  }

  set clock (value) {
    this._state.clock = value;
  }

  get state () {
    return Object.assign({}, this._state);
  }

  get meta () {
    return this.state.meta;
  }

  set meta (value) {
    this.state.meta = value;
  }

  TypedMessage (type, data) {
    const message = struct()
      .charsnt('type', 64) // 64 B
      .charsnt('data', Math.pow(2, 22)); // 4 MB

    // Allocate memory
    message.allocate();

    message.fields.type = type;
    message.fields.data = data;

    return message;
  }

  fingerprint (buffer) {
    if (!(buffer instanceof Buffer)) throw new Error('Input must be a buffer.');
    return this.hash(buffer).digest('hex');
  }

  hash (buffer) {
    if (!(buffer instanceof Buffer)) throw new Error('Input must be a buffer.');
    return crypto.createHash('sha256').update(buffer);
  }

  // TODO: implement
  encrypt (data) {
    return data;
  }

  // TODO: implement
  decrypt (data) {
    return data;
  }

  _getEvenKey () {
    let key = new Key();
    let num = new BN(key.public.encode('hex'), 16);
    if (!num.isEven()) return this._getEvenKey();
    return key;
  }

  _getOddKey () {
    let key = new Key();
    let num = new BN(key.public.encode('hex'), 16);
    if (!num.isOdd()) return this._getOddKey();
    return key;
  }

  /**
   * Opens the {@link Session} for interaction.
   */
  async start () {
    this.status = 'starting';

    const target = new Key({ public: this.settings.recipient });
    this.derived = this.key.keypair.derive(target.public);

    const key = new BN(this.key.public.encode('hex'), 16);
    const start = this.TypedMessage('SessionStart', key.toString(10));

    await this._appendMessage(start.buffer());

    this.components.h = this.hash(Buffer.from(LIGHTNING_PROTOCOL_H_INIT, 'ascii'));
    this.components.ck = this.hash(Buffer.from(LIGHTNING_PROTOCOL_H_INIT, 'ascii'));

    this.components.h.update(Buffer.from(LIGHTNING_PROTOCOL_PROLOGUE, 'ascii'));

    this.status = 'started';
    return this;
  }

  /**
   * Closes the {@link Session}, preventing further interaction.
   */
  async stop () {
    this.status = 'stopping';
    this.status = 'stopped';
    return this;
  }

  async commit () {
    if (!this.key) throw new Error('No key for session!');
    let signature = this.key._sign(this.state);
    return Buffer.from(signature).toString('hex');
  }

  async _appendMessage (message) {
    this.clock++;

    const id = this.fingerprint(message);

    if (!this.settings.ephemeral) {
      this.store[id] = message;
    }

    this.messages.push(id);
    this.meta.messages = this.messages.length;

    let signature = await this.commit();

    this.emit('message', {
      type: 'AddMessage',
      data: { id, signature }
    });

    return signature;
  }
}

module.exports = Session;