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;