node-stone
Version:
A library for creating, validating and verifying stones
420 lines (359 loc) • 12.9 kB
JavaScript
/**
* Node Stone allows javascript developers to create, verify and sign
* stones according to the ellcry's standards.
*/
var StoneObj = {};
var _ = require('lodash');
var utility = require('./utility');
var Validator = require("./validate");
var ursa = require("ursa");
var base64 = require("base64url");
var blockNames = ["meta", "ownership", "attributes", "embeds"];
var Promise = require('bluebird');
module.exports = StoneObj;
/**
* Initialize a new Stone object. Validation of blocks is not performed.
* So a stone created using this method may not be valid. Use the validator
* to perform necessary checks.
*
* @param {object} data new stone object
* @return {object} new Stone instance
*/
function newStone(data) {
var data = data || {};
var stone = new StoneObj.Stone();
stone.meta = data.meta || {};
stone.ownership = data.ownership || {};
stone.attributes = data.attributes || {};
stone.embeds = data.embeds || {};
return stone;
}
/**
* Base 64 encode a string
* @param {string} str string to encode
* @return {string} base64 string
*/
function toB64(str) {
return base64.encode(str);
}
/**
* Decode base 64 string to ascii string
* @param {string} b64Str base 64 string
* @return {string} corresponding ascii string
*/
function fromB64(b64Str) {
return base64.decode(b64Str)
}
/**
* Given a valid json string representation of a stone, validate it and
* return a stone object.
* @param {string} str json representation of a stone
* @return {object} a stone object if successful, otherwise an Error object
*/
function loadJSON(str) {
try {
var stoneJSON = JSON.parse(str);
var err = Validator.validate(stoneJSON);
return (err instanceof Error) ? err : newStone(stoneJSON);
} catch(e){
return new Error("failed to load. JSON string is malformed");
}
}
/**
* Stone object
*/
StoneObj.Stone = function Stone() {
this.meta = {};
this.ownership = {};
this.attributes = {};
this.embeds = {};
this.signatures = {};
}
/**
* Sign a block. The signing process takes the value of a block and signs
* it using JWS. The signature generated is included in the
* `signatures` block. If a block is empty or unknown, an error is returned
*
* @param {string} blockName name of block to sign
* @param {string} privateKey RSA private key for signing
* @return {string|Error} signature if successful, otherwise Error
*/
StoneObj.Stone.prototype.sign = function (blockName, privateKey) {
return new Promise(function(resolve, reject){
var signature = null;
// private key is required
if (!privateKey || !_.trim(privateKey).length) return reject(new Error("private key is required for signing"));
// load private key
if (!utility.isValidRSAPrivatekKey(privateKey)) return reject(new Error("private key is invalid"));
// check if block name is valid
if (_.indexOf(blockNames, blockName) === -1) return reject(new Error("block unknown"));
// cannot sign empty block
if (_.isEmpty(this[blockName])) return reject(new Error("cannot sign empty block"));
// generate signature
utility.createRSASig(privateKey, JSON.stringify(this[blockName])).then(function(signature){
this.signatures[blockName] = signature;
resolve(signature);
}.bind(this)).catch(reject);
}.bind(this));
}
/**
* Verifies the signature of a block. It will compute the signature of a
* block and compare it with the signature stored in the signatures block.
* It will return null when verification is passed or when the block to check is empty.
* An instance of Error is returned when verification fails.
*
* @param {string} blockName block name
* @param {string} publicKey public key for verification
* @return {Error|null} null is returned if successful, otherwise an Error object
*/
StoneObj.Stone.prototype.verify = function (blockName, publicKey) {
return new Promise(function(resolve, reject){
// public key is required
if (!publicKey || !_.trim(publicKey).length) return reject(new Error("public key is required for verifying"));
// check key validity
if (!utility.isValidRSAPublickKey(publicKey)) return reject(new Error("public key is invalid"));
// check if block name is valid
if (_.indexOf(blockNames, blockName) === -1) return reject(new Error("block unknown"));
// ensure block has signature
if (!this.hasSignature(blockName)) return reject(new Error("block `"+blockName+"` has no signature"));
// verify an RSA signature
utility.verifyRSASig(publicKey, this.signatures[blockName]).then(function(result){
return (result.verified) ? resolve(true) : resolve(false);
}).catch(reject);
}.bind(this));
}
/**
* Returns a JSON representation of the stone instance
* @return {object} json object
*/
StoneObj.Stone.prototype.toJSON = function() {
return {
meta: _.clone(this.meta),
ownership: _.cloneDeep(this.ownership),
attributes: _.cloneDeep(this.attributes),
embeds: _.cloneDeep(this.embeds),
signatures: _.clone(this.signatures)
}
}
/**
* Checks if the stone instance current state is considered valid
* @return {boolean} true if valid, otherwise false
*/
StoneObj.Stone.prototype.isValid = function() {
return !(Validator.validate(this.toJSON()) instanceof Error)
}
/**
* Encode a base64 url equivalent of the instance signatures
* @return {string} base 64 encoded representation of the instance
*/
StoneObj.Stone.prototype.encode = function() {
var stoneJSON = JSON.stringify(this.toJSON().signatures);
return toB64(stoneJSON);
}
/**
* Create an identical instance
* @return {Stone} new stone object
*/
StoneObj.Stone.prototype.clone = function() {
var stoneJSON = this.toJSON();
return StoneObj.load(stoneJSON);
}
/**
* Checks if the instance content in the ownership block
* @return {Boolean} true if content is available, otherwise false
*/
StoneObj.Stone.prototype.hasOwnership = function() {
return !_.isEmpty(this.ownership);
}
/**
* Checks if the instance content in the attributes block
* @return {Boolean} true if content is available, otherwise false
*/
StoneObj.Stone.prototype.hasAttributes = function() {
return !_.isEmpty(this.attributes);
}
/**
* Checks if the instance content in the embeds block
* @return {Boolean} true if content is available, otherwise false
*/
StoneObj.Stone.prototype.hasEmbeds = function() {
return !_.isEmpty(this.embeds);
}
/**
* Check whether a block has a signature
* @param {string} blockName block name
* @return {Boolean} true if signature exists, otherwise false
*/
StoneObj.Stone.prototype.hasSignature = function (blockName) {
return (this.signatures[blockName]) ? true : false;
}
/**
* Initialize meta block with new value. New value
* is validated and signed.
* @param {object} meta meta information
* @param {string} privateKey private key for signing
*/
StoneObj.Stone.prototype.addMeta = function (meta, privateKey) {
return new Promise(function(resolve, reject){
var result = Validator.validateMetaBlock(meta);
if (result instanceof Error) return reject(result);
this.meta = meta;
return this.sign("meta", privateKey).then(resolve).catch(reject);
}.bind(this));
}
/**
* Initialize ownership block with new value. New value
* is validated and signed. Meta.id must be set, otherwise an errror is returned.
*
* @param {object} ownership ownership information
* @param {string} privateKey private key for signing
*/
StoneObj.Stone.prototype.addOwnership = function (ownership, privateKey) {
return new Promise(function(resolve, reject){
if (!this.meta.id || ("string" === typeof this.meta.id && !_.trim(this.meta.id).length)) {
return reject(new Error("meta.id is not set"));
}
var result = Validator.validateOwnershipBlock(ownership, this.meta.id);
if (result instanceof Error) return reject(result);
this.ownership = ownership;
return this.sign("ownership", privateKey).then(resolve).catch(reject);
}.bind(this));
}
/**
* Initialize attributes block with new value. New value
* is validated and signed. Meta.id must be set, otherwise an errror is returned.
* @param {object} attributes attributes information
* @param {string} privateKey private key for signing
*/
StoneObj.Stone.prototype.addAttributes = function (attributes, privateKey) {
return new Promise(function(resolve, reject){
if (!this.meta.id || ("string" === typeof this.meta.id && !_.trim(this.meta.id).length)) {
return reject(new Error("meta.id is not set"));
}
var result = Validator.validateAttributesBlock(attributes, this.meta.id);
if (result instanceof Error) return reject(result);
this.attributes = attributes;
return this.sign("attributes", privateKey).then(resolve).catch(reject);
}.bind(this));
}
/**
* Updates the embeds block with a new value. The valu must be a valid
* embeds block. Meta.id must be set, otherwise an errror is returned.
*
* @param {object} embeds embeds information
* @param {string} privateKey private key for signing
*/
StoneObj.Stone.prototype.addEmbed = function (embeds, privateKey) {
return new Promise(function(resolve, reject){
if (!this.meta.id || ("string" === typeof this.meta.id && !_.trim(this.meta.id).length)) {
return reject(new Error("meta.id is not set"));
}
var result = Validator.validateEmbedsBlock(embeds, this.meta.id);
if (result instanceof Error) return reject(result);
this.embeds = embeds;
return this.sign("embeds", privateKey).then(resolve).catch(reject);
}.bind(this));
}
/**
* Create a new stone with a valid meta block that
* is also signed.
*
* @param {object} meta meta block value
* @param {string} privateKey issuer private
* @return {Promise} [description]
*/
StoneObj.create = function (meta, privateKey) {
return new Promise(function(resolve, reject){
// private key is required
if (!privateKey || !_.trim(privateKey).length) return reject(new Error("private key is required for signing"));
// create new stone
var stone = newStone();
// validate meta block
var err = Validator.validateMetaBlock(meta);
if (err instanceof Error) return reject(err);
// assign meta to stone.meta
stone.meta = meta;
// sign meta block
stone.sign("meta", privateKey).then(function(signature){
resolve(stone);
}).catch(reject);
});
}
/**
* Load a stone from a json object.
* This function will not attempt to sign any stone blocks.
* String begining with "{" will be passed to JSON.parse.
*
* @param {string|object} val a str
* @return {object} a stone object or an Error object
*/
StoneObj.load = function(val) {
// load a json string
if ("string" === typeof val) {
var stoneStr = _.trim(val);
if (stoneStr.length == 0) return new Error("cannot load empty string");
if (stoneStr[0] === "{") return loadJSON(stoneStr);
}
// load a json object
if (_.isPlainObject(val)) {
var val = _.cloneDeep(val);
var err = Validator.validate(val);
return (err instanceof Error) ? err : newStone(val);
}
return new Error("unsupported parameter");
}
/**
* Given an encoded signed stone, it will attempt to
* decode and load the underlying stone object.
* @param {string} val base64 url encoded stone
* @return {[type]} [description]
*/
StoneObj.decode = function(val) {
var signatures = null;
var signedStone = fromB64(val);
var curBlock = null;
var stone = newStone();
try {
// parse signed stone signatures object to json
signatures = JSON.parse(signedStone);
// process meta signature if available
if (signatures.meta) {
var result = utility.getJWSPayload(signatures.meta);
if (result instanceof Error) return result;
curBlock = "meta";
var block = JSON.parse(result);
stone.meta = block;
stone.signatures.meta = signatures.meta;
}
// process ownership signature if available
if (signatures.ownership) {
var result = utility.getJWSPayload(signatures.ownership);
if (result instanceof Error) return result;
curBlock = "ownership";
var block = JSON.parse(result);
stone.ownership = block;
stone.signatures.ownership = signatures.ownership;
}
// process attributes signature if available
if (signatures.attributes) {
var result = utility.getJWSPayload(signatures.attributes);
if (result instanceof Error) return result;
curBlock = "attributes";
var block = JSON.parse(result);
stone.attributes = block;
stone.signatures.attributes = signatures.attributes;
}
// process embeds signature if available
if (signatures.embeds) {
var result = utility.getJWSPayload(signatures.embeds);
if (result instanceof Error) return result;
curBlock = "embeds";
var block = JSON.parse(result);
stone.embeds = block;
stone.signatures.embeds = signatures.embeds;
}
return stone;
} catch(e){
return new Error("failed to decode" + ((!curBlock) ? "" : (": invalid " + curBlock + " signature")))
}
}