Source: index.js

import sovrinDID from 'sovrin-did';
import axios from 'axios';
import CryptoJS from 'crypto-js';
import crypto from 'crypto';

/**
 *
 * Main SmartInvoice class representing interface to Smart Invoice API endpoint.
 * See below example how to use it:
 * <pre>
 *
 *    // ES6 project
 *    import SmartInvoice from 'smartinvoice-sdk';
 *
 *    // or below
 *    var SmartInvoice = require("smartinvoice-sdk").default
 *
 *    var identity = SmartInvoice.createIdentity();
 *    var host = "https://api.difacturo.com"
 *    var invitationCode = "getitfromus"
 *    var config = { host: host, invitationCode: invitationCode}
 *    var smartinvoice = new SmartInvoice(config, identity);
 *
 * </pre>
 *
 * @constructor
 * @param {Object} instanceConfig Json object with configuration for new instance
 * @param {Identity} userIdentity Sovrin identity object generate with createIdentity()
 * @return SmartInvoice {object} - The top level SmartInvoice object
 * @license MIT
 */
class SmartInvoice {
  /**
   * Generate new DID base identity
   * It include public and private key. Currently supported only Sovrin but in the feature
   * this would be extended to other DID Methods.
   *
   * Currently supported confguration:
   *    {
   *        host, // host of the api endpoint to which you want to connect
   *        invitationCode //invitation code required to connect to pilot network
   *    }
   *
   * @static
   * @return {Object} object including public and private key for the identity.
   */
  static createIdentity() {
    return sovrinDID.gen();
  }

  constructor(instanceConfig = {}, userIdentity) {
    this.config = instanceConfig;
    this.aesSecret = '';
    this.aesIv = '';
    this.identity = userIdentity;
    // TODO use autogenrated nonce and decouple authentication from encryption
    // TODO notice string must be exactly 24 bytes
    // Due to the sovrin lib we are using encryption with authentication where
    // we have to pass nonce
    // we would get rid of it as soon as we would find better lib for dealing with ecds keys
    this.nonce = Buffer.from('difacturdifacturdidifacr', 'ascii');

    this.http = axios.create({});
    this.http.interceptors.request.use(
      (config) => {
        const defaultConfig = config;
        if (this.jwt) {
          defaultConfig.headers.Authorization = `Bearer ${this.jwt}`;

          const tokenData = JSON.parse(
            Buffer.from(this.jwt.split('.')[1], 'base64').toString('binary'),
          );
          defaultConfig.baseURL = tokenData.orgEndpoint;
        }
        defaultConfig.headers['Content-Type'] = 'application/json';
        return defaultConfig;
      },
      error => Promise.reject(error),
    );
  }

  /**
   * Set/Get host for SmartInvoice API
   * Host is used only for calls which does not required JWT
   * For authenticated calls the host is taken by interceptor from JWT
   */
  set host(uri) {
    this.config.host = uri;
  }

  get host() {
    if (this.config === undefined || this.config.host === undefined) {
      throw Error('Host is not set check your config');
    }
    return this.config.host;
  }

  /**
   * Set/Get Json Web Token which is used to authenticate against endpoint from host variable
   * @param {String} JWT - json web token
   */
  set jwt(jwt) {
    this.config.jwt = jwt;
  }

  get jwt() {
    return this.config.jwt;
  }

  /**
   * Login user and get JWT token for next calls
   * @async
   * @return {Promise} axios promise and if success Json Web Token (JWT)
   */
  login() {
    const self = this;
    const userDID = `did:sov:${this.identity.did}`;
    const { invitationCode } = this.config;
    // TODO use identity keys for JWT
    let url = this.host;
    url += '/api/login?';
    return self.http
      .get(url, {
        params: {
          invitationCode,
          userDID,
        },
      })
      .then((res) => {
        self.jwt = res.data.token;
        return new Promise((resolve, reject) => {
          resolve(res);
        });
      })
      .catch((error) => {
        if (error.response.status === 401) {
          // user not reigsterd, try to register
          self.register().then(() => {
            // try to login once more
            self.login();
          });
        }
        return Promise.reject(error.response);
      });
  }

  /**
   * Allow to fetch document for the user within given time frame.
   *
   * @param {Timestamp} startTimestamp Miliseconds since 1900, from when we should get documents
   * @param {Timestamp} endTimestamp Miliseconds since 1900, until when we should look for documents
   */
  fetchDocuments(startTimestamp, endTimestamp) {
    const url = '/api/ddoc/transactions';
    return this.http.get(url, {
      params: {
        startTimestamp,
        endTimestamp,
      },
    });
  }

  /**
   * Forward existing document on the network to given receiver
   * @async
   * @param {String} receiverDID Decentralize identifier of the receiver
   * @param {String} dri Decentralize Resource Identifier of the forwarding file
   * @param {Object} payload Additional information which should be attached to the document
   */
  forwardDocument(receiverDID, dri, payload) {
    const self = this;
    self
      .encryptTransactionPayloadFor(receiverDID, dri, payload)
      .then((encryptedTransactionPayload) => {
        self.sendDocument(dri, encryptedTransactionPayload, receiverDID);
      });
  }

  /**
   * Send document via SmartInvoice platform to the receiver
   * @async
   * @param {String} receiverDID Decentralize identifier of the receiver
   * @param {File} file Document which should be sent
   * @param {Object} payload Additional information which should be attached to the document
   */
  sendTo(receiverDID, file, payload) {
    const self = this;
    self.encryptAndUploadDocument(file).then((response) => {
      const dri = response.data;
      self
        .encryptTransactionPayloadFor(receiverDID, dri, payload)
        .then((encryptedTransactionPayload) => {
          self.sendDocument(dri, encryptedTransactionPayload, receiverDID);
        });
    });
  }

  /**
   * Encrypt transaction payload for given receiver.
   * In most cases you don't have to use it directly as
   * [sendTo]{@link SmartInvoice#sendTo} is taking care of it.
   * @async
   * @ignore
   * @param {String} receiverDID DID of the receiver
   * @param {Object} payload JSON object with additional data
   * @return {Promise} promise with success result object with encryptedMessage
   *                   for sender and recevier
   */
  encryptTransactionPayloadFor(receiverDID, payload) {
    const self = this;
    const { signKey } = this.identity.secret;
    const userKeyPair = sovrinDID.getKeyPairFromSignKey(signKey);

    return this.fetchIdentityFor(receiverDID).then((response) => {
      const sharedSecret = sovrinDID.getSharedSecret(
        response.data.publicKey,
        userKeyPair.secretKey,
      );

      // Encrypting for self transaction
      const sharedSecretSender = sovrinDID.getSharedSecret(
        userKeyPair.publicKey,
        userKeyPair.secretKey,
      );

      const transactionPayload = {
        aes: {
          aesSecret: self.aesSecret,
          aesIV: self.aesIv,
        },
        payload,
      };

      const message = JSON.stringify(transactionPayload);

      const encryptedReceiverMessage = sovrinDID.encryptMessage(message, self.nonce, sharedSecret);

      const encryptedSenderMessage = sovrinDID.encryptMessage(
        message,
        self.nonce,
        sharedSecretSender,
      );

      return { encryptedReceiverMessage, encryptedSenderMessage };
    });
  }

  /**
   * Decrypt payload from transaction
   * @async
   * @param {String} senderDID Sender DID
   * @param {String} encryptedPayload encrypted payload from transaction
   * @return {Object} decrypted payload
   */
  decryptTransactionPayload(senderDID, encryptedPayload) {
    const self = this;
    const { signKey } = this.identity.secret;
    const userKeyPair = sovrinDID.getKeyPairFromSignKey(signKey);
    return this.fetchIdentityFor(senderDID).then((response) => {
      const sharedSecret = sovrinDID.getSharedSecret(
        response.data.publicKey,
        userKeyPair.secretKey,
      );
      return sovrinDID.decryptMessage(encryptedPayload, self.nonce, sharedSecret);
    });
  }

  /**
   * Private method for registering new user within DID directory.
   * In the future this would be reposnsibility of the partner.
   * @async
   * @ignore
   * @return {Promise} axios promise and if success http code 200
   */
  register() {
    let url = this.host;
    const { invitationCode } = this.config;
    const userDID = `did:sov:${this.identity.did}`;
    const userPublicKey = this.identity.encryptionPublicKey;
    url += '/api/register';
    return axios.post(url, {
      invitationCode,
      userDID,
      userPublicKey,
    });
  }

  /**
   * Private method for encrypting the document with generated AES key
   * @private
   * @ignore
   * @param {File} unencryptedFile File blob to be encrypted
   * @returns {String} encrypted file
   */
  // eslint-disable-next-line class-methods-use-this
  encryptWithAES(unencryptedFile) {
    this.aesSecret = crypto.randomBytes(16).toString('hex');
    this.aesIv = crypto.randomBytes(16).toString('hex');
    const encryptedFile = CryptoJS.AES.encrypt(unencryptedFile, this.aesSecret, {
      key: this.aesIv,
    }).toString();
    return encryptedFile;
  }

  /**
   * Private method for encrypting the document with generated AES key
   * Decrypt give payload with given AES key
   * @private
   * @ignore
   * @param {String} encryptedFile Encrypted file
   * @return {String} Decrypted File
   */
  decryptWithAES(encryptedFile) {
    const bytes = CryptoJS.AES.decrypt(encryptedFile, this.aesSecret, {
      key: this.aesIv,
    });
    return bytes.toString(CryptoJS.enc.Utf8);
  }

  /**
   * Fetch Public information about specific DID
   * @async
   * @private
   * @ignore
   * @param {String} did - Decentralized Identifier of the user.
   */
  fetchIdentityFor(did) {
    const url = `/api/did/${did}`;
    return this.http.get(url, {});
  }

  /**
   * Private method to upload file to decentralize storage and get DRI
   * Encrypt document and upload it to decentralize storage.
   * @async
   * @ignore
   * @private
   * @param {File} file Document Blob
   * @return {Promise} axios promies and if success file Store DRI
   *                   (decentralize resource identifier)
   */
  encryptAndUploadDocument(file) {
    const url = '/api/ddoc/upload';
    return this.http.post(url, {
      encryptedFile: this.encryptWithAES(file),
    });
  }

  /**
   * Private method to send document to the network
   * @ignore
   * @private
   * @param {String} dri Decentralized Resource Identifier
   * @param {Object} encryptedPayload Hash with encrypted payload for receiver and sender
   * @param {String} receiverDID DID of the receiver
   * @return {Promise} axios promies
   */
  sendDocument(dri, encryptedPayload, receiverDID) {
    const self = this;
    // TODO check if encryptedPayload is correct

    const encryptedReceiverPayload = Buffer.from(
      encryptedPayload.encryptedReceiverMessage,
      'binary',
    ).toString('base64');
    const encryptedSenderPayload = Buffer.from(
      encryptedPayload.encryptedSenderMessage,
      'binary',
    ).toString('base64');

    return this.http.post('/api/ddoc', {
      senderDID: self.identity.did,
      encryptedReceiverPayload,
      encryptedSenderPayload,
      receiverDID,
      dri,
    });
  }
}

/**
 * <p>SmartInvoice module</p>
 *
 * <p>
 * SmartInvoice module allow you to interact with Smart Invoice network.
 * See documentation for more details.
 * </p>
 * To include exported SmartInvoice class do this:
 * <pre>
 * import SmartInvoice from 'smartinvoice-sdk';
 * </pre>
 * or
 * <pre>
 * var SmartInvoice = require('smartinvoice-sdk');
 * </pre>
 * @module SmartInvoice
 */
export default SmartInvoice;