Source: types/token.js

'use strict';

// Dependencies
const bitcoin = require('bitcoinjs-lib');
const schnorr = require('bip-schnorr');

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

/**
 * Implements a capability-based security token.
 */
class Token {
  /**
   * Create a new Fabric Token.
   * @param {Object} [settings] Configuration.
   * @returns {Token} The token instance.
   */
  constructor (settings = {}) {
    // TODO: determine rounding preference (secwise)
    this.created = Date.now();
    this.settings = Object.assign({
      capability: 'OP_0',
      issuer: null,
      subject: null,
      state: {
        status: 'READY'
      }
    }, settings);

    // Capability
    this.capability = this.settings.capability;
    this.ephemera = new Key();

    // Trust Chain
    this.issuer = this.settings.issuer ? this.settings.issuer : this.ephemera;
    this.subject = this.settings.subject ? this.settings.subject : this.ephemera.keypair.getPublic(true).encodeCompressed('hex');

    // ECDSA Signature
    this.signature = null;

    // State
    this._state = {
      content: this.settings.state
    };

    return this;
  }

  get state () {
    return JSON.parse(JSON.stringify(this._state.content));
  }

  static base64UrlEncode (input) {
    const base64 = Buffer.from(input, 'utf8').toString('base64');
    return base64.replace('+', '-').replace('/', '_').replace(/=+$/, '');
  }

  static base64UrlDecode (input) {
    input = input.replace(/-/g, '+').replace(/_/g, '/');

    while (input.length % 4) {
      input += '=';
    }

    return Buffer.from(input, 'base64').toString();
  }

  static fromString (input) {
    const parts = input.split('.');
    const headers = parts[0];
    const payload = parts[1];
    const signature = parts[2];
    const inner = Token.base64UrlDecode(payload);

    return new Token({
      capability: inner.cap,
      issuer: inner.iss,
      subject: inner.sub,
      state: inner.state,
      signature: signature
    });
  }

  toString () {
    // TODO: determine rounding preference (secwise)
    const utime = Math.floor(this.created / 1000);
    const issuer = this.issuer.keypair.getPublic(true).encodeCompressed('hex');
    const header = {
      alg: 'ES256K',
      iss: issuer,
      typ: 'JWT'
    };

    const payload = {
      cap: this.capability,
      iat: utime,
      iss: issuer,
      sub: this.subject,
      state: this.state
    };

    // TODO: reconcile with JWT spec
    // alternatively, since we're already breaking spec,
    // we can diverge again here.
    // Secret: HS256
    const secret = 'ffff';

    // Encodings
    const encodedHeader = Token.base64UrlEncode(JSON.stringify(header));
    const encodedPayload = Token.base64UrlEncode(JSON.stringify(payload));
    const signature = bitcoin.crypto.sha256(
      Buffer.from(`${encodedHeader}.${encodedPayload}.${secret}`)
    );

    return [
      encodedHeader,
      encodedPayload,
      Token.base64UrlEncode(signature.toString('hex'))
    ].join('.');
  }

  sign () {
    // Sign the capability using the private key
    const hash = bitcoin.crypto.sha256(this.capability);
    this.signature = schnorr.sign(this.issuer.privateKey, hash);
  }

  verify () {
    // Verify the signature using the public key
    const hash = bitcoin.crypto.sha256(this.capability);
    return schnorr.verify(this.issuer.publicKey, hash, this.signature);
  }

  add (other) {
    const combinedCapability = [this.capability, other.capability].join(' ');
    const combinedToken = new Token({
      capability: combinedCapability,
      issuer: this.issuer.publicKey
    });

    /* combinedToken.signature = schnorr.combine([
      this.signature,
      other.signature
    ]); */

    return combinedToken;
  }
}

module.exports = Token;