This commit is contained in:
wltsmrz
2014-05-28 18:22:54 -07:00
parent bdb299e085
commit eca5ac7611
6 changed files with 825 additions and 687 deletions

View File

@@ -1,36 +1,45 @@
var RippleTxt = require('./rippletxt');
var request = require('superagent'); var request = require('superagent');
var RippleTxt = require('./rippletxt').RippleTxt;
function AuthInfo () { function AuthInfo() {
this.rippleTxt = new RippleTxt; this.rippleTxt = new RippleTxt;
} };
/** /**
* Get auth info for a given username * Get auth info for a given username
*
* @param {string} domain - Domain which hosts the user's info * @param {string} domain - Domain which hosts the user's info
* @param {string} username - Username who's info we are retreiving * @param {string} username - Username who's info we are retreiving
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
AuthInfo.prototype.get = function (domain, username, fn) {
var self = this;
self.rippleTxt.get(domain, function(err, txt){
if (err) return fn(err);
processTxt(txt)
});
function processTxt(txt) {
if (!txt.authinfo_url) return fn(new Error("Authentication is not supported on "+domain));
var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url;
url += "?domain="+domain+"&username="+username;
request.get(url, function(err, resp){
if (err || resp.error) return fn(new Error("Authentication info server unreachable"));
fn(null, resp.body);
});
}
}
module.exports = AuthInfo; AuthInfo.prototype.get = function(domain, username, fn) {
var self = this;
self.rippleTxt.get(domain, function(err, txt) {
if (err) {
fn(err);
} else {
processTxt(txt)
}
});
function processTxt(txt) {
if (!txt.authinfo_url) {
return fn(new Error('Authentication is not supported on ' + domain));
}
var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url;
url += '?domain='+domain+'&username='+username;
request.get(url, function(err, resp){
if (err || resp.error) {
fn(new Error('Authentication info server unreachable'));
} else {
fn(null, resp.body);
}
});
};
};
exports.AuthInfo = AuthInfo;

View File

@@ -1,124 +1,136 @@
var crypt = require('./crypt'), var request = require('superagent');
request = require('superagent'), var extend = require('extend');
extend = require("extend"); var crypt = require('./crypt').Crypt;
//Blob object class //Blob object class
var BlobObj = function (url, id, key) { function BlobObj(url, id, key) {
this.url = url; this.url = url;
this.id = id; this.id = id;
this.key = key; this.key = key;
this.data = {}; this.data = { };
}; };
// Blob operations // Blob operations
// Do NOT change the mapping of existing ops // Do NOT change the mapping of existing ops
BlobObj.ops = { BlobObj.ops = {
// Special // Special
"noop" : 0, noop: 0,
// Simple ops // Simple ops
"set" : 16, set: 16,
"unset" : 17, unset: 17,
"extend" : 18, extend: 18,
// Meta ops // Meta ops
"push" : 32, push: 32,
"pop" : 33, pop: 33,
"shift" : 34, shift: 34,
"unshift" : 35, unshift: 35,
"filter" : 36 filter: 36
}; };
BlobObj.opsReverseMap = []; BlobObj.opsReverseMap = [ ];
for (var name in BlobObj.ops) {
for (var name in BlobObj.ops) {
BlobObj.opsReverseMap[BlobObj.ops[name]] = name; BlobObj.opsReverseMap[BlobObj.ops[name]] = name;
} }
/**
/*
* Initialize a new blob object * Initialize a new blob object
*
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
BlobObj.prototype.init = function (fn) {
BlobObj.prototype.init = function(fn) {
var self = this, url; var self = this, url;
if (self.url.indexOf("://") === -1) self.url = "http://" + url;
if (self.url.indexOf('://') === -1) {
self.url = 'http://' + url;
}
url = self.url + '/v1/blob/' + self.id; url = self.url + '/v1/blob/' + self.id;
request.get(url, function(err, resp){
if (err || !resp.body || resp.body.result !== 'success') request.get(url, function(err, resp) {
return fn(new Error("Could not retrieve blob")); if (err || !resp.body || resp.body.result !== 'success') {
return fn(new Error('Could not retrieve blob'));
self.revision = resp.body.revision; }
self.encrypted_secret = resp.body.encrypted_secret;
self.revision = resp.body.revision;
if (!self.decrypt(resp.body.blob)) { self.encrypted_secret = resp.body.encrypted_secret;
return fn(new Error("Error while decrypting blob"));
if (!self.decrypt(resp.body.blob)) {
return fn(new Error('Error while decrypting blob'));
}
//Apply patches
if (resp.body.patches && resp.body.patches.length) {
var successful = true;
resp.body.patches.forEach(function(patch) {
successful = successful && self.applyEncryptedPatch(patch);
});
if (successful) {
self.consolidate();
} }
}
//Apply patches
if (resp.body.patches && resp.body.patches.length) {
var successful = true;
resp.body.patches.forEach(function (patch) {
successful = successful && self.applyEncryptedPatch(patch);
});
if (successful) self.consolidate(); //return with newly decrypted blob
} fn(null, self);
}).timeout(8000);
};
fn(null, self);//return with newly decrypted blob /**
}).timeout(8000);
}
/*
* Consolidate - * Consolidate -
* Consolidate patches as a new revision * Consolidate patches as a new revision
*
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
BlobObj.prototype.consolidate = function (fn) {
// Callback is optional
if ("function" !== typeof fn) fn = function(){};
console.log("client: blob: consolidation at revision", this.revision); BlobObj.prototype.consolidate = function(fn) {
// Callback is optional
if (typeof fn !== 'function') {
fn = function(){};
}
//console.log('client: blob: consolidation at revision', this.revision);
var encrypted = this.encrypt(); var encrypted = this.encrypt();
var config = { var config = {
method : 'POST', method: 'POST',
url : this.url + '/v1/blob/consolidate', url: this.url + '/v1/blob/consolidate',
dataType : 'json', dataType: 'json',
data : { data: {
blob_id : this.id, blob_id: this.id,
data : encrypted, data: encrypted,
revision : this.revision revision: this.revision
}, },
}; };
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
request.post(signed.url) request.post(signed.url)
.send(signed.data) .send(signed.data)
.end(function(err, resp) { .end(function(err, resp) {
// XXX Add better error information to exception
// XXX Add better error information to exception if (err) {
if (err) return fn(new Error("Failed to consolidate blob - XHR error")); fn(new Error('Failed to consolidate blob - XHR error'));
else if (resp.body && resp.body.result === 'success') return fn(null, resp.body); } else if (resp.body && resp.body.result === 'success') {
else return fn(new Error("Failed to consolidate blob")); fn(null, resp.body);
} else {
fn(new Error('Failed to consolidate blob'));
}
}); });
}; };
/**
/*
* ApplyEncryptedPatch - * ApplyEncryptedPatch -
* save changes from a downloaded patch to the blob * save changes from a downloaded patch to the blob
*
* @param {string} patch - encrypted patch string * @param {string} patch - encrypted patch string
*/ */
BlobObj.prototype.applyEncryptedPatch = function (patch)
{ BlobObj.prototype.applyEncryptedPatch = function(patch) {
try { try {
var params = JSON.parse(crypt.decrypt(this.key, patch)); var params = JSON.parse(crypt.decrypt(this.key, patch));
var op = params.shift(); var op = params.shift();
@@ -128,55 +140,55 @@ BlobObj.prototype.applyEncryptedPatch = function (patch)
this.revision++; this.revision++;
return true; return true;
} catch (err) { } catch (err) {
console.log("client: blob: failed to apply patch:", err.toString()); //console.log('client: blob: failed to apply patch:', err.toString());
console.log(err.stack); //console.log(err.stack);
return false; return false;
} }
} };
/** /**
* Encrypt secret with unlock key * Encrypt secret with unlock key
*
* @param {string} secretUnlockkey * @param {string} secretUnlockkey
*/ */
BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) {
BlobObj.prototype.encryptSecret = function(secretUnlockKey, secret) {
return crypt.encrypt(secretUnlockKey, secret); return crypt.encrypt(secretUnlockKey, secret);
}; };
/** /**
* Decrypt secret with unlock key * Decrypt secret with unlock key
*
* @param {string} secretUnlockkey * @param {string} secretUnlockkey
*/ */
BlobObj.prototype.decryptSecret = function (secretUnlockKey) {
BlobObj.prototype.decryptSecret = function(secretUnlockKey) {
return crypt.decrypt(secretUnlockKey, this.encrypted_secret); return crypt.decrypt(secretUnlockKey, this.encrypted_secret);
}; };
/** /**
* Decrypt blob with crypt key * Decrypt blob with crypt key
*
* @param {string} data - encrypted blob data * @param {string} data - encrypted blob data
*/ */
BlobObj.prototype.decrypt = function (data) {
BlobObj.prototype.decrypt = function(data) {
try { try {
this.data = JSON.parse(crypt.decrypt(this.key, data)); this.data = JSON.parse(crypt.decrypt(this.key, data));
return this; return this;
} catch (e) { } catch (e) {
console.log("client: blob: decryption failed", e.toString()); //console.log('client: blob: decryption failed', e.toString());
console.log(e.stack); //console.log(e.stack);
return false; return false;
} }
}; };
/** /**
* Encrypt blob with crypt key * Encrypt blob with crypt key
*/ */
BlobObj.prototype.encrypt = function()
{ BlobObj.prototype.encrypt = function() {
// Filter Angular metadata before encryption // Filter Angular metadata before encryption
// if ('object' === typeof this.data && // if ('object' === typeof this.data &&
// 'object' === typeof this.data.contacts) // 'object' === typeof this.data.contacts)
@@ -185,68 +197,67 @@ BlobObj.prototype.encrypt = function()
return crypt.encrypt(this.key, JSON.stringify(this.data)); return crypt.encrypt(this.key, JSON.stringify(this.data));
}; };
/** /**
* Encrypt recovery key * Encrypt recovery key
*
* @param {string} secret * @param {string} secret
* @param {string} blobDecryptKey * @param {string} blobDecryptKey
*/ */
BlobObj.prototype.encryptBlobCrypt = function (secret, blobDecryptKey) {
BlobObj.prototype.encryptBlobCrypt = function(secret, blobDecryptKey) {
var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret);
return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey); return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey);
}; };
/** /**
* Decrypt recovery key * Decrypt recovery key
*
* @param {string} secret * @param {string} secret
*/ */
BlobObj.prototype.decryptBlobCrypt = function (secret) {
BlobObj.prototype.decryptBlobCrypt = function(secret) {
var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret);
return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key);
}; };
/**** Blob updating functions ****/ /**** Blob updating functions ****/
/** /**
* Set blob element * Set blob element
*/ */
BlobObj.prototype.set = function (pointer, value, fn) {
BlobObj.prototype.set = function(pointer, value, fn) {
this.applyUpdate('set', pointer, [value]); this.applyUpdate('set', pointer, [value]);
this.postUpdate('set', pointer, [value], fn); this.postUpdate('set', pointer, [value], fn);
}; };
/** /**
* Remove blob element * Remove blob element
*/ */
BlobObj.prototype.unset = function (pointer, fn) {
BlobObj.prototype.unset = function(pointer, fn) {
this.applyUpdate('unset', pointer, []); this.applyUpdate('unset', pointer, []);
this.postUpdate('unset', pointer, [], fn); this.postUpdate('unset', pointer, [], fn);
}; };
/** /**
* Extend blob object * Extend blob object
*/ */
BlobObj.prototype.extend = function (pointer, value, fn) {
BlobObj.prototype.extend = function(pointer, value, fn) {
this.applyUpdate('extend', pointer, [value]); this.applyUpdate('extend', pointer, [value]);
this.postUpdate('extend', pointer, [value], fn); this.postUpdate('extend', pointer, [value], fn);
}; };
/** /**
* Prepend blob array * Prepend blob array
*/ */
BlobObj.prototype.unshift = function (pointer, value, fn) {
BlobObj.prototype.unshift = function(pointer, value, fn) {
this.applyUpdate('unshift', pointer, [value]); this.applyUpdate('unshift', pointer, [value]);
this.postUpdate('unshift', pointer, [value], fn); this.postUpdate('unshift', pointer, [value], fn);
}; };
/** /**
* Filter the row(s) from an array. * Filter the row(s) from an array.
* *
@@ -255,49 +266,48 @@ BlobObj.prototype.unshift = function (pointer, value, fn) {
* *
* The subcommands can be any commands with the pointer parameter left out. * The subcommands can be any commands with the pointer parameter left out.
*/ */
BlobObj.prototype.filter = function (pointer, field, value, subcommands, callback) { BlobObj.prototype.filter = function(pointer, field, value, subcommands, callback) {
var params = Array.prototype.slice.apply(arguments); var params = Array.prototype.slice.apply(arguments);
if ("function" === typeof params[params.length-1]) { if (typeof params[params.length - 1] === 'function') {
callback = params.pop(); callback = params.pop();
} }
params.shift(); params.shift();
// Normalize subcommands to minimize the patch size // Normalize subcommands to minimize the patch size
params = params.slice(0, 2).concat(normalizeSubcommands(params.slice(2), true)); params = params.slice(0, 2).concat(normalizeSubcommands(params.slice(2), true));
this.applyUpdate('filter', pointer, params); this.applyUpdate('filter', pointer, params);
this.postUpdate('filter', pointer, params, callback); this.postUpdate('filter', pointer, params, callback);
}; };
/** /**
* Apply udpdate to the blob * Apply udpdate to the blob
*/ */
BlobObj.prototype.applyUpdate = function (op, path, params) {
BlobObj.prototype.applyUpdate = function(op, path, params) {
// Exchange from numeric op code to string // Exchange from numeric op code to string
if ("number" === typeof op) { if (typeof op === 'number') {
op = BlobObj.opsReverseMap[op]; op = BlobObj.opsReverseMap[op];
} }
if ("string" !== typeof op) {
throw new Error("Blob update op code must be a number or a valid op id string"); if (typeof op !== 'string') {
throw new Error('Blob update op code must be a number or a valid op id string');
} }
// Separate each step in the "pointer" // Separate each step in the 'pointer'
var pointer = path.split("/"); var pointer = path.split('/');
var first = pointer.shift(); var first = pointer.shift();
if (first !== "") {
throw new Error("Invalid JSON pointer: "+path); if (first !== '') {
throw new Error('Invalid JSON pointer: '+path);
} }
this._traverse(this.data, pointer, path, op, params); this._traverse(this.data, pointer, path, op, params);
}; };
//for applyUpdate function //for applyUpdate function
BlobObj.prototype._traverse = function (context, pointer, BlobObj.prototype._traverse = function(context, pointer, originalPointer, op, params) {
originalPointer, op, params) {
var _this = this; var _this = this;
var part = _this.unescapeToken(pointer.shift()); var part = _this.unescapeToken(pointer.shift());
@@ -305,16 +315,15 @@ BlobObj.prototype._traverse = function (context, pointer,
if (part === '-') { if (part === '-') {
part = context.length; part = context.length;
} else if (part % 1 !== 0 && part >= 0) { } else if (part % 1 !== 0 && part >= 0) {
throw new Error("Invalid pointer, array element segments must be " + throw new Error('Invalid pointer, array element segments must be a positive integer, zero or '-'');
"a positive integer, zero or '-'");
} }
} else if ("object" !== typeof context) { } else if (typeof context !== 'object') {
return null; return null;
} else if (!context.hasOwnProperty(part)) { } else if (!context.hasOwnProperty(part)) {
// Some opcodes create the path as they're going along // Some opcodes create the path as they're going along
if (op === "set") { if (op === 'set') {
context[part] = {}; context[part] = {};
} else if (op === "unshift") { } else if (op === 'unshift') {
context[part] = []; context[part] = [];
} else { } else {
return null; return null;
@@ -322,139 +331,133 @@ BlobObj.prototype._traverse = function (context, pointer,
} }
if (pointer.length !== 0) { if (pointer.length !== 0) {
return this._traverse(context[part], pointer, return this._traverse(context[part], pointer, originalPointer, op, params);
originalPointer, op, params);
} }
switch (op) { switch (op) {
case "set": case 'set':
context[part] = params[0]; context[part] = params[0];
break; break;
case "unset": case 'unset':
if (Array.isArray(context)) { if (Array.isArray(context)) {
context.splice(part, 1); context.splice(part, 1);
} else { } else {
delete context[part]; delete context[part];
} }
break; break;
case "extend": case 'extend':
if ("object" !== typeof context[part]) { if (typeof context[part] !== 'object') {
throw new Error("Tried to extend a non-object"); throw new Error('Tried to extend a non-object');
} }
extend(true, context[part], params[0]); extend(true, context[part], params[0]);
break; break;
case "unshift": case 'unshift':
if ("undefined" === typeof context[part]) { if (typeof context[part] === 'undefined') {
context[part] = []; context[part] = [ ];
} else if (!Array.isArray(context[part])) { } else if (!Array.isArray(context[part])) {
throw new Error("Operator 'unshift' must be applied to an array."); throw new Error('Operator "unshift" must be applied to an array.');
} }
context[part].unshift(params[0]); context[part].unshift(params[0]);
break; break;
case "filter": case 'filter':
if (Array.isArray(context[part])) { if (Array.isArray(context[part])) {
context[part].forEach(function (element, i) { context[part].forEach(function(element, i) {
if ("object" === typeof element && if (typeof element === 'object' && element.hasOwnProperty(params[0]) && element[params[0]] === params[1]) {
element.hasOwnProperty(params[0]) && var subpointer = originalPointer + '/' + i;
element[params[0]] === params[1]) { var subcommands = normalizeSubcommands(params.slice(2));
var subpointer = originalPointer+"/"+i;
var subcommands = normalizeSubcommands(params.slice(2));
subcommands.forEach(function (subcommand) { subcommands.forEach(function(subcommand) {
var op = subcommand[0]; var op = subcommand[0];
var pointer = subpointer+subcommand[1]; var pointer = subpointer + subcommand[1];
_this.applyUpdate(op, pointer, subcommand.slice(2)); _this.applyUpdate(op, pointer, subcommand.slice(2));
}); });
} }
}); });
} }
break; break;
default: default:
throw new Error("Unsupported op "+op); throw new Error('Unsupported op '+op);
} }
}; };
BlobObj.prototype.escapeToken = function(token) {
BlobObj.prototype.escapeToken = function (token) { return token.replace(/[~\/]/g, function(key) {
return token.replace(/[~\/]/g, function (key) { return key === "~" ? "~0" : "~1"; }); return key === '~' ? '~0' : '~1';
});
}; };
BlobObj.prototype.unescapeToken = function(str) { BlobObj.prototype.unescapeToken = function(str) {
return str.replace(/~./g, function(m) { return str.replace(/~./g, function(m) {
switch (m) { switch (m) {
case "~0": case '~0':
return "~"; return '~';
case "~1": case '~1':
return "/"; return '/';
} }
throw("Invalid tilde escape: " + m); throw new Error('Invalid tilde escape: ' + m);
}); });
}; };
/** /**
* Sumbit update to blob vault * Sumbit update to blob vault
*/ */
BlobObj.prototype.postUpdate = function (op, pointer, params, fn) {
// Callback is optional
if ("function" !== typeof fn) fn = function(){};
if ("string" === typeof op) { BlobObj.prototype.postUpdate = function(op, pointer, params, fn) {
// Callback is optional
if (typeof fn !== 'function') {
fn = function(){};
}
if (typeof op === 'string') {
op = BlobObj.ops[op]; op = BlobObj.ops[op];
} }
if ("number" !== typeof op) {
throw new Error("Blob update op code must be a number or a valid op id string"); if (typeof op !== 'number') {
} throw new Error('Blob update op code must be a number or a valid op id string');
if (op < 0 || op > 255) {
throw new Error("Blob update op code out of bounds");
} }
console.log("client: blob: submitting update", BlobObj.opsReverseMap[op], pointer, params); if (op < 0 || op > 255) {
throw new Error('Blob update op code out of bounds');
}
//console.log('client: blob: submitting update', BlobObj.opsReverseMap[op], pointer, params);
params.unshift(pointer); params.unshift(pointer);
params.unshift(op); params.unshift(op);
var config = { var config = {
method : 'POST', method: 'POST',
url : this.url + '/v1/blob/patch', url: this.url + '/v1/blob/patch',
dataType : 'json', dataType: 'json',
data : { data: {
blob_id : this.id, blob_id: this.id,
patch : crypt.encrypt(this.key, JSON.stringify(params)) patch: crypt.encrypt(this.key, JSON.stringify(params))
} }
}; };
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
request.post(signed.url) request.post(signed.url)
.send(signed.data) .send(signed.data)
.end(function(err, resp) { .end(function(err, resp) {
if (err) if (err) {
return fn(new Error("Patch could not be saved - XHR error")); fn(new Error('Patch could not be saved - XHR error'));
else if (!resp.body || resp.body.result !== 'success') } else if (!resp.body || resp.body.result !== 'success') {
return fn(new Error("Patch could not be saved - bad result")); fn(new Error('Patch could not be saved - bad result'));
} else {
return fn(null, resp.body); fn(null, resp.body);
}
}); });
}; };
/***** helper functions *****/ /***** helper functions *****/
function normalizeSubcommands(subcommands, compress) { function normalizeSubcommands(subcommands, compress) {
// Normalize parameter structure // Normalize parameter structure
if ("number" === typeof subcommands[0] || if (/(number|string)/.test(typeof subcommands[0])) {
"string" === typeof subcommands[0]) {
// Case 1: Single subcommand inline // Case 1: Single subcommand inline
subcommands = [subcommands]; subcommands = [subcommands];
} else if (subcommands.length === 1 && } else if (subcommands.length === 1 && Array.isArray(subcommands[0]) && /(number|string)/.test(subcommands[0][0])) {
Array.isArray(subcommands[0]) &&
("number" === typeof subcommands[0][0] ||
"string" === typeof subcommands[0][0])) {
// Case 2: Single subcommand as array // Case 2: Single subcommand as array
// (nothing to do) // (nothing to do)
} else if (Array.isArray(subcommands[0])) { } else if (Array.isArray(subcommands[0])) {
@@ -463,16 +466,19 @@ function normalizeSubcommands(subcommands, compress) {
} }
// Normalize op name and convert strings to numeric codes // Normalize op name and convert strings to numeric codes
subcommands = subcommands.map(function (subcommand) { subcommands = subcommands.map(function(subcommand) {
if ("string" === typeof subcommand[0]) { if (typeof subcommand[0] === 'string') {
subcommand[0] = BlobObj.ops[subcommand[0]]; subcommand[0] = BlobObj.ops[subcommand[0]];
} }
if ("number" !== typeof subcommand[0]) {
throw new Error("Invalid op in subcommand"); if (typeof subcommand[0] !== 'number') {
throw new Error('Invalid op in subcommand');
} }
if ("string" !== typeof subcommand[1]) {
throw new Error("Invalid path in subcommand"); if (typeof subcommand[1] !== 'string') {
throw new Error('Invalid path in subcommand');
} }
return subcommand; return subcommand;
}); });
@@ -486,53 +492,63 @@ function normalizeSubcommands(subcommands, compress) {
} else { } else {
return subcommands; return subcommands;
} }
} };
/***** blob client methods ****/ /***** blob client methods ****/
/** /**
* Blob object class * Blob object class
*/ */
module.exports.Blob = BlobObj
exports.Blob = BlobObj
/** /**
* Get ripple name for a given address * Get ripple name for a given address
*/ */
module.exports.getRippleName = function (url, address, fn) {
exports.getRippleName = function(url, address, fn) {
if (!crypt.isValidAddress(address)) return fn (new Error("Invalid ripple address")); if (!crypt.isValidAddress(address)) {
return fn (new Error('Invalid ripple address'));
}
request.get(url + '/v1/user/' + address, function(err, resp){ request.get(url + '/v1/user/' + address, function(err, resp){
if (err) return fn(new Error("Unable to access vault sever")); if (err) {
else if (resp.body && resp.body.username) return fn(null, resp.body.username); fn(new Error('Unable to access vault sever'));
else if (resp.body && resp.body.exists === false) return fn (new Error("No ripple name for this address")); } else if (resp.body && resp.body.username) {
else return fn(new Error("Unable to determine if ripple name exists")); fn(null, resp.body.username);
}); } else if (resp.body && resp.body.exists === false) {
} fn (new Error('No ripple name for this address'));
} else {
fn(new Error('Unable to determine if ripple name exists'));
}
});
};
/**
/* * Retrive a blob with url, id and key
* Retrive a blob with url, id and key
*/ */
module.exports.get = function (url, id, crypt, fn) {
exports.get = function(url, id, crypt, fn) {
var blob = new BlobObj(url, id, crypt); var blob = new BlobObj(url, id, crypt);
blob.init(fn); blob.init(fn);
} };
/**
/*
* Verify email address * Verify email address
*/ */
module.exports.verify = function (url, username, token, fn) {
exports.verify = function(url, username, token, fn) {
url += '/v1/user/' + username + '/verify/' + token; url += '/v1/user/' + username + '/verify/' + token;
request.get(url, function(err, resp){ request.get(url, function(err, resp){
if (err) return fn(err); if (err) {
else if (resp.body && resp.body.result === 'success') return fn(null, data); fn(err);
else return fn(new Error("Failed to verify the account")); } else if (resp.body && resp.body.result === 'success') {
}); fn(null, data);
} } else {
fn(new Error('Failed to verify the account'));
}
});
};
/** /**
* Create a blob object * Create a blob object
@@ -547,51 +563,55 @@ module.exports.verify = function (url, username, token, fn) {
* @param {object} options.oldUserBlob * @param {object} options.oldUserBlob
* @param {function} fn * @param {function} fn
*/ */
module.exports.create = function (options, fn)
{ exports.create = function(options, fn) {
var blob = new BlobObj(options.url, options.id, options.crypt);
var blob = new BlobObj(options.url, options.id, options.crypt);
blob.revision = 0; blob.revision = 0;
blob.data = {
auth_secret : crypt.createSecret(8), blob.data = {
account_id : crypt.getAddress(options.masterkey), auth_secret: crypt.createSecret(8),
email : options.email, account_id: crypt.getAddress(options.masterkey),
contacts : [], email: options.email,
created : (new Date()).toJSON() contacts: [],
created: (new Date()).toJSON()
}; };
blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey); blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey);
// Migration // Migration
if (options.oldUserBlob) { if (options.oldUserBlob) {
blob.data.contacts = options.oldUserBlob.data.contacts; blob.data.contacts = options.oldUserBlob.data.contacts;
} }
//post to the blob vault to create //post to the blob vault to create
var config = { var config = {
method : "POST", method: 'POST',
url : options.url + '/v1/user', url: options.url + '/v1/user',
data : { data: {
blob_id : options.id, blob_id: options.id,
username : options.username, username: options.username,
address : blob.data.account_id, address: blob.data.account_id,
auth_secret : blob.data.auth_secret, auth_secret: blob.data.auth_secret,
data : blob.encrypt(), data: blob.encrypt(),
email : options.email, email: options.email,
hostlink : options.activateLink, hostlink: options.activateLink,
encrypted_blobdecrypt_key : blob.encryptBlobCrypt(options.masterkey, options.crypt), encrypted_blobdecrypt_key: blob.encryptBlobCrypt(options.masterkey, options.crypt),
encrypted_secret : blob.encrypted_secret encrypted_secret: blob.encrypted_secret
} }
}; };
var signed = crypt.signRequestAsymmetric(config, options.masterkey, blob.data.account_id, options.id); var signed = crypt.signRequestAsymmetric(config, options.masterkey, blob.data.account_id, options.id);
request.post(signed) request.post(signed)
.send(signed.data) .send(signed.data)
.end(function(err, resp) { .end(function(err, resp) {
if (err) return fn(err); if (err) {
else if (resp.body && resp.body.result === 'success') return fn(null, blob,resp.body); fn(err);
else return fn(new Error("Could not create blob")); } else if (resp.body && resp.body.result === 'success') {
}); fn(null, blob,resp.body);
} } else {
fn(new Error('Could not create blob'));
}
});
};

View File

@@ -1,34 +1,36 @@
var sjcl = require('./utils').sjcl, var parser = require('url');
base = require('./base').Base, var querystring = require('querystring');
UInt160 = require('./uint160').UInt160, var extend = require('extend');
message = require('./message'), var request = require('superagent');
request = require('superagent'), var sjcl = require('./utils').sjcl;
extend = require("extend"), var base = require('./base').Base;
parser = require("url"); var UInt160 = require('./uint160').UInt160;
var message = require('./message');
var cryptConfig = { var cryptConfig = {
cipher : "aes", cipher: 'aes',
mode : "ccm", mode: 'ccm',
ts : 64, // tag length ts: 64, // tag length
ks : 256, // key size ks: 256, // key size
iter : 1000 // iterations (key derivation) iter: 1000 // iterations (key derivation)
}; };
var Crypt = {}; var Crypt = { };
/** /**
* Full domain hash based on SHA512 * Full domain hash based on SHA512
*/ */
function fdh(data, bytelen)
{ function fdh(data, bytelen) {
var bitlen = bytelen << 3; var bitlen = bytelen << 3;
if (typeof data === "string") { if (typeof data === 'string') {
data = sjcl.codec.utf8String.toBits(data); data = sjcl.codec.utf8String.toBits(data);
} }
// Add hashing rounds until we exceed desired length in bits // Add hashing rounds until we exceed desired length in bits
var counter = 0, output = []; var counter = 0, output = [];
while (sjcl.bitArray.bitLength(output) < bitlen) { while (sjcl.bitArray.bitLength(output) < bitlen) {
var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data)); var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data));
output = sjcl.bitArray.concat(output, hash); output = sjcl.bitArray.concat(output, hash);
@@ -39,132 +41,137 @@ function fdh(data, bytelen)
output = sjcl.bitArray.clamp(output, bitlen); output = sjcl.bitArray.clamp(output, bitlen);
return output; return output;
} };
/** /**
* This is a function to derive different hashes from the same key. * This is a function to derive different hashes from the same key.
* Each hash is derived as HMAC-SHA512HALF(key, token). * Each hash is derived as HMAC-SHA512HALF(key, token).
*
* @param {string} key * @param {string} key
* @param {string} hash * @param {string} hash
*/ */
function keyHash(key, token) { function keyHash(key, token) {
var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512); var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256)); return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256));
} };
/****** exposed functions ******/ /****** exposed functions ******/
/** /**
* KEY DERIVATION FUNCTION * KEY DERIVATION FUNCTION
* *
* This service takes care of the key derivation, i.e. converting low-entropy * This service takes care of the key derivation, i.e. converting low-entropy
* secret into higher entropy secret via either computationally expensive * secret into higher entropy secret via either computationally expensive
* processes or peer-assisted key derivation (PAKDF). * processes or peer-assisted key derivation (PAKDF).
*
* @param {object} opts * @param {object} opts
* @param {string} purpose - Key type/purpose * @param {string} purpose - Key type/purpose
* @param {string} username * @param {string} username
* @param {string} secret - Also known as passphrase/password * @param {string} secret - Also known as passphrase/password
* @param {function} fn * @param {function} fn
*/ */
Crypt.derive = function (opts, purpose, username, secret, fn) {
Crypt.derive = function(opts, purpose, username, secret, fn) {
var tokens; var tokens;
if (purpose=='login') tokens = ['id', 'crypt'];
else tokens = ['unlock'];
var iExponent = new sjcl.bn(String(opts.exponent)), if (purpose === 'login') {
iModulus = new sjcl.bn(String(opts.modulus)), tokens = ['id', 'crypt'];
iAlpha = new sjcl.bn(String(opts.alpha)); } else {
tokens = ['unlock'];
}
var publicInfo = "PAKDF_1_0_0:"+opts.host.length+":"+opts.host+ var iExponent = new sjcl.bn(String(opts.exponent));
":"+username.length+":"+username+ var iModulus = new sjcl.bn(String(opts.modulus));
":"+purpose.length+":"+purpose+ var iAlpha = new sjcl.bn(String(opts.alpha));
":",
publicSize = Math.ceil(Math.min((7+iModulus.bitLength()) >>> 3, 256)/8), var publicInfo = [ 'PAKDF_1_0_0', opts.host.length, opts.host, username.length, username, purpose.length, purpose ].join(':') + ':';
publicHash = fdh(publicInfo, publicSize), var publicSize = Math.ceil(Math.min((7 + iModulus.bitLength()) >>> 3, 256) / 8);
publicHex = sjcl.codec.hex.fromBits(publicHash), var publicHash = fdh(publicInfo, publicSize);
iPublic = new sjcl.bn(String(publicHex)).setBitM(0), var publicHex = sjcl.codec.hex.fromBits(publicHash);
secretInfo = publicInfo+":"+secret.length+":"+secret+":", var iPublic = new sjcl.bn(String(publicHex)).setBitM(0);
secretSize = (7+iModulus.bitLength()) >>> 3, var secretInfo = [ publicInfo, secret.length, secret ].join(':') + ':';
secretHash = fdh(secretInfo, secretSize), var secretSize = (7 + iModulus.bitLength()) >>> 3;
secretHex = sjcl.codec.hex.fromBits(secretHash), var secretHash = fdh(secretInfo, secretSize);
iSecret = new sjcl.bn(String(secretHex)).mod(iModulus); var secretHex = sjcl.codec.hex.fromBits(secretHash);
var iSecret = new sjcl.bn(String(secretHex)).mod(iModulus);
if (iSecret.jacobi(iModulus) !== 1) { if (iSecret.jacobi(iModulus) !== 1) {
iSecret = iSecret.mul(iAlpha).mod(iModulus); iSecret = iSecret.mul(iAlpha).mod(iModulus);
} }
var iRandom; var iRandom;
for (;;) { for (;;) {
iRandom = sjcl.bn.random(iModulus, 0); iRandom = sjcl.bn.random(iModulus, 0);
if (iRandom.jacobi(iModulus) === 1) if (iRandom.jacobi(iModulus) === 1) {
break; break;
}
} }
var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus), var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus);
iSignreq = iSecret.mulmod(iBlind, iModulus), var iSignreq = iSecret.mulmod(iBlind, iModulus);
signreq = sjcl.codec.hex.fromBits(iSignreq.toBits()); var signreq = sjcl.codec.hex.fromBits(iSignreq.toBits());
request.post(opts.url) request.post(opts.url)
.send({ .send({ info: publicInfo, signreq: signreq })
info : publicInfo, .end(function(err, resp) {
signreq : signreq if (err || !resp) {
}).end(function(err, resp) { return fn(new Error('Could not query PAKDF server ' + opts.host));
}
if (err || !resp) return fn(new Error("Could not query PAKDF server "+opts.host));
var data = resp.body || resp.text ? JSON.parse(resp.text) : {};
var data = resp.body || resp.text ? JSON.parse(resp.text) : {};
if (data.result !== 'success') {
if (!data.result=='success') return fn(new Error("Could not query PAKDF server "+opts.host)); return fn(new Error('Could not query PAKDF server '+opts.host));
}
var iSignres = new sjcl.bn(String(data.signres));
iRandomInv = iRandom.inverseMod(iModulus), var iSignres = new sjcl.bn(String(data.signres));
iSigned = iSignres.mulmod(iRandomInv, iModulus), var iRandomInv = iRandom.inverseMod(iModulus);
key = iSigned.toBits(), var iSigned = iSignres.mulmod(iRandomInv, iModulus);
result = {}; var key = iSigned.toBits();
var result = { };
tokens.forEach(function (token) {
result[token] = keyHash(key, token); tokens.forEach(function(token) {
result[token] = keyHash(key, token);
});
fn(null, result);
}); });
};
fn (null, result);
});
}
/** /**
* Imported from ripple-client * Imported from ripple-client
*
*/ */
Crypt.RippleAddress = (function () {
Crypt.RippleAddress = (function() {
function append_int(a, i) { function append_int(a, i) {
return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff) return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff)
} };
function firstHalfOfSHA512(bytes) { function firstHalfOfSHA512(bytes) {
return sjcl.bitArray.bitSlice( return sjcl.bitArray.bitSlice(
sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)), sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)),
0, 256 0, 256
); );
} };
function SHA256_RIPEMD160(bits) { function SHA256_RIPEMD160(bits) {
return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
} };
return function (seed) { return function(seed) {
this.seed = base.decode_check(33, seed); this.seed = base.decode_check(33, seed);
if (!this.seed) { if (!this.seed) {
throw "Invalid seed." throw new Error('Invalid seed.');
} }
this.getAddress = function (seq) { this.getAddress = function(seq) {
seq = seq || 0; seq = seq || 0;
var private_gen, public_gen, i = 0; var private_gen, public_gen, i = 0;
do { do {
private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i))); private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i)));
i++; i++;
@@ -174,6 +181,7 @@ Crypt.RippleAddress = (function () {
var sec; var sec;
i = 0; i = 0;
do { do {
sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i))); sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i)));
i++; i++;
@@ -188,11 +196,12 @@ Crypt.RippleAddress = (function () {
/** /**
* Encrypt data * Encrypt data
* @params {string} key *
* @params {string} data * @param {string} key
* @param {string} data
*/ */
Crypt.encrypt = function(key, data)
{ Crypt.encrypt = function(key, data) {
key = sjcl.codec.hex.toBits(key); key = sjcl.codec.hex.toBits(key);
var opts = extend(true, {}, cryptConfig); var opts = extend(true, {}, cryptConfig);
@@ -206,23 +215,23 @@ Crypt.encrypt = function(key, data)
encryptedBits = sjcl.bitArray.concat(encryptedBits, ciphertext); encryptedBits = sjcl.bitArray.concat(encryptedBits, ciphertext);
return sjcl.codec.base64.fromBits(encryptedBits); return sjcl.codec.base64.fromBits(encryptedBits);
} };
/** /**
* Decrypt data * Decrypt data
* @params {string} key *
* @params {string} data * @param {string} key
* @param {string} data
*/ */
Crypt.decrypt = function(key, data)
{ Crypt.decrypt = function(key, data) {
key = sjcl.codec.hex.toBits(key); key = sjcl.codec.hex.toBits(key);
var encryptedBits = sjcl.codec.base64.toBits(data); var encryptedBits = sjcl.codec.base64.toBits(data);
var version = sjcl.bitArray.extract(encryptedBits, 0, 8); var version = sjcl.bitArray.extract(encryptedBits, 0, 8);
if (version !== 0) { if (version !== 0) {
throw new Error("Unsupported encryption version: "+version); throw new Error('Unsupported encryption version: '+version);
} }
var encrypted = extend(true, {}, cryptConfig, { var encrypted = extend(true, {}, cryptConfig, {
@@ -231,109 +240,121 @@ Crypt.decrypt = function(key, data)
}); });
return sjcl.decrypt(key, JSON.stringify(encrypted)); return sjcl.decrypt(key, JSON.stringify(encrypted));
} };
/** /**
* Validate a ripple address * Validate a ripple address
*
* @param {string} address * @param {string} address
*/ */
Crypt.isValidAddress = function (address) {
return UInt160.is_valid(address);
}
Crypt.isValidAddress = function(address) {
return UInt160.is_valid(address);
};
/** /**
* Validate a ripple address * Validate a ripple address
*
* @param {integer} nWords - number of words * @param {integer} nWords - number of words
*/ */
Crypt.createSecret = function (nWords) {
return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords));
}
Crypt.createSecret = function(nWords) {
return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords));
};
/** /**
* Create a new master key * Create a new master key
*/ */
Crypt.createMaster = function () {
return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4)));
}
Crypt.createMaster = function() {
return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4)));
};
/** /**
* Create a ripple address from a master key * Create a ripple address from a master key
*
* @param {string} masterkey * @param {string} masterkey
*/ */
Crypt.getAddress = function (masterkey) {
return new Crypt.RippleAddress(masterkey).getAddress();
}
Crypt.getAddress = function(masterkey) {
return new Crypt.RippleAddress(masterkey).getAddress();
};
/** /**
* Hash data * Hash data
*
* @param {string} data * @param {string} data
*/ */
Crypt.hashSha512 = function (data) {
return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data));
}
Crypt.hashSha512 = function(data) {
return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data));
};
/** /**
* Sign a data string with a secret key * Sign a data string with a secret key
*
* @param {string} secret * @param {string} secret
* @param {string} data * @param {string} data
*/ */
Crypt.signString = function (secret, data) {
Crypt.signString = function(secret, data) {
var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512); var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(hmac.mac(data)); return sjcl.codec.hex.fromBits(hmac.mac(data));
} };
/** /**
* Create an an accout recovery key * Create an an accout recovery key
*
* @param {string} secret * @param {string} secret
*/ */
Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) { Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) {
var seed = ripple.Seed.from_json(secret).to_bits(); var seed = ripple.Seed.from_json(secret).to_bits();
var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512); var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512);
var key = hmac.mac("ripple/hmac/recovery_encryption_key/v1"); var key = hmac.mac('ripple/hmac/recovery_encryption_key/v1');
key = sjcl.bitArray.bitSlice(key, 0, 256); key = sjcl.bitArray.bitSlice(key, 0, 256);
return sjcl.codec.hex.fromBits(key); return sjcl.codec.hex.fromBits(key);
} };
/** /**
* Convert base64 encoded data into base64url encoded data. * Convert base64 encoded data into base64url encoded data.
*
* @param {String} base64 Data * @param {String} base64 Data
*/ */
Crypt.base64ToBase64Url = function (encodedData) {
Crypt.base64ToBase64Url = function(encodedData) {
return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, ''); return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, '');
}; };
/** /**
* Convert base64url encoded data into base64 encoded data. * Convert base64url encoded data into base64 encoded data.
*
* @param {String} base64 Data * @param {String} base64 Data
*/ */
Crypt.base64UrlToBase64 = function (encodedData) {
Crypt.base64UrlToBase64 = function(encodedData) {
encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/'); encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/');
while (encodedData.length % 4) { while (encodedData.length % 4) {
encodedData += '='; encodedData += '=';
} }
return encodedData; return encodedData;
}; };
/** /**
* Create a string from request parameters that * Create a string from request parameters that
* will be used to sign a request * will be used to sign a request
*
* @param {Object} config - request params * @param {Object} config - request params
* @param {Object} parsed - parsed url * @param {Object} parsed - parsed url
* @param {Object} date * @param {Object} date
* @param {Object} mechanism - type of signing * @param {Object} mechanism - type of signing
*/ */
Crypt.getStringToSign = function (config, parsed, date, mechanism) {
Crypt.getStringToSign = function(config, parsed, date, mechanism) {
// XXX This method doesn't handle signing GET requests correctly. The data // XXX This method doesn't handle signing GET requests correctly. The data
// field will be merged into the search string, not the request body. // field will be merged into the search string, not the request body.
// Sort the properties of the JSON object into canonical form // Sort the properties of the JSON object into canonical form
var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data)); var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data));
@@ -360,16 +381,17 @@ Crypt.getStringToSign = function (config, parsed, date, mechanism) {
date, date,
Crypt.hashSha512(canonicalRequest).toLowerCase() Crypt.hashSha512(canonicalRequest).toLowerCase()
].join('\n'); ].join('\n');
} };
/** /**
* HMAC signed request * HMAC signed request
*
* @param {Object} config * @param {Object} config
* @param {Object} auth_secret * @param {Object} auth_secret
* @param {Object} blob_id * @param {Object} blob_id
*/ */
Crypt.signRequestHmac = function (config, auth_secret, blob_id) {
Crypt.signRequestHmac = function(config, auth_secret, blob_id) {
config = extend(true, {}, config); config = extend(true, {}, config);
// Parse URL // Parse URL
@@ -378,24 +400,29 @@ Crypt.signRequestHmac = function (config, auth_secret, blob_id) {
var signatureType = 'RIPPLE1-HMAC-SHA512'; var signatureType = 'RIPPLE1-HMAC-SHA512';
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = Crypt.signString(auth_secret, stringToSign); var signature = Crypt.signString(auth_secret, stringToSign);
config.url += (parsed.search ? "&" : "?") + var query = querystring.stringify({
'signature='+Crypt.base64ToBase64Url(signature)+ signature: Crypt.base64ToBase64Url(signature),
'&signature_date='+date+ signature_date: date,
'&signature_blob_id='+blob_id+ signature_blob_id: blob_id,
'&signature_type='+signatureType signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config; return config;
}; };
/** /**
* Asymmetric signed request * Asymmetric signed request
*
* @param {Object} config * @param {Object} config
* @param {Object} secretKey * @param {Object} secretKey
* @param {Object} account * @param {Object} account
* @param {Object} blob_id * @param {Object} blob_id
*/ */
Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) {
Crypt.signRequestAsymmetric = function(config, secretKey, account, blob_id) {
config = extend(true, {}, config); config = extend(true, {}, config);
// Parse URL // Parse URL
@@ -404,77 +431,82 @@ Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) {
var signatureType = 'RIPPLE1-ECDSA-SHA512'; var signatureType = 'RIPPLE1-ECDSA-SHA512';
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = message.signMessage(stringToSign, secretKey); var signature = message.signMessage(stringToSign, secretKey);
config.url += (parsed.search ? "&" : "?") + var query = querystring.stringify({
'signature='+Crypt.base64ToBase64Url(signature)+ signature: Crypt.base64ToBase64Url(signature),
'&signature_date='+date+ signature_date: date,
'&signature_blob_id='+blob_id+ signature_blob_id: blob_id,
'&signature_account='+account+ signature_account: account,
'&signature_type='+signatureType; signature_type: signatureType
})
config.url += (parsed.search ? '&' : '?') + query;
return config; return config;
}; };
//prepare for signing //prepare for signing
function copyObjectWithSortedKeys(object) { function copyObjectWithSortedKeys(object) {
if (isPlainObject(object)) { if (isPlainObject(object)) {
var newObj = {}; var newObj = {};
var keysSorted = Object.keys(object).sort(); var keysSorted = Object.keys(object).sort();
var key; var key;
for (var i in keysSorted) { for (var i in keysSorted) {
key = keysSorted[i]; key = keysSorted[i];
if (Object.prototype.hasOwnProperty.call(object, key)) { if (Object.prototype.hasOwnProperty.call(object, key)) {
newObj[key] = copyObjectWithSortedKeys(object[key]); newObj[key] = copyObjectWithSortedKeys(object[key]);
} }
} }
return newObj; return newObj;
} else if (Array.isArray(object)) { } else if (Array.isArray(object)) {
return object.map(copyObjectWithSortedKeys); return object.map(copyObjectWithSortedKeys);
} else { } else {
return object; return object;
} }
} };
//from npm extend //from npm extend
function isPlainObject(obj) { function isPlainObject(obj) {
var hasOwn = Object.prototype.hasOwnProperty; var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString; var toString = Object.prototype.toString;
if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) {
return false; return false;
}
var has_own_constructor = hasOwn.call(obj, 'constructor'); var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object // Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method) if (obj.constructor && !has_own_constructor && !has_is_property_of_method) {
return false; return false;
}
// Own properties are enumerated firstly, so to speed up, // Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own. // if last one is own, then all properties are own.
var key; var key;
for ( key in obj ) {} for ( key in obj ) {}
return key === undefined || hasOwn.call( obj, key ); return key === void(0) || hasOwn.call( obj, key );
}; };
var dateAsIso8601 = (function() {
var dateAsIso8601 = (function () {
function pad(n) { function pad(n) {
return (n < 0 || n > 9 ? "" : "0") + n; return (n < 0 || n > 9 ? '' : '0') + n;
} };
return function dateAsIso8601() { return function dateAsIso8601() {
var date = new Date(); var date = new Date();
return date.getUTCFullYear() + "-" return date.getUTCFullYear() + '-'
+ pad(date.getUTCMonth() + 1) + "-" + pad(date.getUTCMonth() + 1) + '-'
+ pad(date.getUTCDate()) + "T" + pad(date.getUTCDate()) + 'T'
+ pad(date.getUTCHours()) + ":" + pad(date.getUTCHours()) + ':'
+ pad(date.getUTCMinutes()) + ":" + pad(date.getUTCMinutes()) + ':'
+ pad(date.getUTCSeconds()) + ".000Z"; + pad(date.getUTCSeconds()) + '.000Z';
}; };
})(); })();
exports.Crypt = Crypt;
module.exports = Crypt;

View File

@@ -1,21 +1,23 @@
var request = require('superagent'); var request = require('superagent');
function RippleTxt() { function RippleTxt() {
this.txts = {}; this.txts = { };
}; };
/** /**
* Gets the ripple.txt file for the given domain * Gets the ripple.txt file for the given domain
*
* @param {string} domain - Domain to retrieve file from * @param {string} domain - Domain to retrieve file from
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
RippleTxt.prototype.get = function (domain, fn) { RippleTxt.prototype.get = function (domain, fn) {
var self = this; var self = this;
if (self.txts[domain]) return fn(null, self.txts[domain]); if (self.txts[domain]) {
return fn(null, self.txts[domain]);
}
var urls = [ var urls = [
'https://ripple.'+domain+'/ripple.txt', 'https://ripple.'+domain+'/ripple.txt',
'https://www.'+domain+'/ripple.txt', 'https://www.'+domain+'/ripple.txt',
@@ -23,40 +25,47 @@ RippleTxt.prototype.get = function (domain, fn) {
'http://ripple.'+domain+'/ripple.txt', 'http://ripple.'+domain+'/ripple.txt',
'http://www.'+domain+'/ripple.txt', 'http://www.'+domain+'/ripple.txt',
'http://'+domain+'/ripple.txt' 'http://'+domain+'/ripple.txt'
].reverse(); ];
next(); ;(function nextUrl() {
function next () { var url = urls.shift();
if (!urls.length) return fn(new Error("No ripple.txt found"));
var url = urls.pop(); if (!url) {
return fn(new Error('No ripple.txt found'));
}
request.get(url, function(err, resp) { request.get(url, function(err, resp) {
if (err || !resp.text) return next(); if (err || !resp.text) {
return nextUrl();
var sections = self.parse(resp.text); }
self.txts[domain] = sections;
fn(null, sections);
});
}
}
var sections = self.parse(resp.text);
self.txts[domain] = sections;
fn(null, sections);
});
})();
};
/** /**
* Parse a ripple.txt file * Parse a ripple.txt file
*
* @param {string} txt - Unparsed ripple.txt data * @param {string} txt - Unparsed ripple.txt data
*/ */
RippleTxt.prototype.parse = function (txt) {
RippleTxt.prototype.parse = function (txt) {
txt = txt.replace('\r\n', '\n'); var txt = txt.replace(/\r?\n/g, '\n').split('\n')
txt = txt.replace('\r', '\n'); var currentSection = '';
txt = txt.split('\n'); var sections = { };
var currentSection = "", sections = {};
for (var i = 0, l = txt.length; i < l; i++) { for (var i = 0, l = txt.length; i < l; i++) {
var line = txt[i]; var line = txt[i];
if (!line.length || line[0] === '#') { if (!line.length || line[0] === '#') {
continue; continue;
} else if (line[0] === '[' && line[line.length-1] === ']') { }
if (line[0] === '[' && line[line.length-1] === ']') {
currentSection = line.slice(1, line.length-1); currentSection = line.slice(1, line.length-1);
sections[currentSection] = []; sections[currentSection] = [];
} else { } else {
@@ -68,6 +77,6 @@ RippleTxt.prototype.parse = function (txt) {
} }
return sections; return sections;
} };
module.exports = RippleTxt; exports.RippleTxt = RippleTxt;

View File

@@ -1,229 +1,271 @@
var AuthInfo = require('./authinfo'); var async = require('async');
var blobClient = require('./blob'); var blobClient = require('./blob');
var crypt = require('./crypt'); var AuthInfo = require('./authinfo').AuthInfo;
var crypt = require('./crypt').Crypt;
function VaultClient(opts) { function VaultClient(opts) {
if (!opts) opts = {}; if (!opts) {
else if (typeof opts === "string") opts = {domain:opts}; opts = {};
}
this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo; if (typeof opts === 'string') {
this.infos = {}; opts = { domain: opts };
}
this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo();
this.infos = { };
}; };
/**
* Reduce username to standardized form.
* Strips whitespace at beginning and end.
* @param {string} username - Username to normalize
*/
VaultClient.prototype.normalizeUsername = function (username) {
username = ""+username;
username = username.trim();
return username;
};
/**
* Reduce password to standardized form.
* Strips whitespace at beginning and end.
* @param {string} password - password to normalize
*/
VaultClient.prototype.normalizePassword = function (password) {
password = ""+password;
password = password.trim();
return password;
};
/** /**
* Get a ripple name from a given account address, if it has one * Get a ripple name from a given account address, if it has one
* @param {string} address - Account address to query * @param {string} address - Account address to query
* @param {string} url - Url of blob vault * @param {string} url - Url of blob vault
*/ */
VaultClient.prototype.getRippleName = function(address, url, fn) {
VaultClient.prototype.getRippleName = function(address, url, callback) {
//use the url from previously retrieved authInfo, if necessary //use the url from previously retrieved authInfo, if necessary
if (!url) return fn(new Error("Blob vault URL is required")); if (!url) {
blobClient.getRippleName(url, address, fn); callback(new Error('Blob vault URL is required'));
} else {
blobClient.getRippleName(url, address, callback);
}
}; };
/** /**
* Authenticate and retrieve a decrypted blob using a ripple name and password * Authenticate and retrieve a decrypted blob using a ripple name and password
*
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.login = function(username, password, fn) {
VaultClient.prototype.login = function(username, password, callback) {
var self = this; var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){
if (err) return fn(err);
if (authInfo.version !== 3) {
return fn(new Error("This wallet is incompatible with this version of the vault-client."));
}
if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet."));
}
if (!authInfo.exists) { function getAuthInfo(callback) {
return fn(new Error("User does not exist.")); self.authInfo.get(self.domain, username, function(err, authInfo) {
} if (err) {
return callback(err);
}
if ("string" !== typeof authInfo.blobvault) { if (authInfo.version !== 3) {
return fn(new Error("No blobvault specified in the authinfo.")); return callback(new Error('This wallet is incompatible with this version of the vault-client.'));
} }
if (!authInfo.pakdf) {
return callback(new Error('No settings for PAKDF in auth packet.'));
}
if (!authInfo.exists) {
return callback(new Error('User does not exist.'));
}
if (typeof authInfo.blobvault !== 'string') {
return callback(new Error('No blobvault specified in the authinfo.'));
}
callback(null, authInfo);
});
};
function deriveLoginKeys(authInfo, callback) {
//derive login keys //derive login keys
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys){ crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys) {
if (err) return fn(err); if (err) {
callback(err);
blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function (err, blob) { } else {
if (err) return fn(err); callback(null, authInfo, keys);
}
self.infos[keys.id] = authInfo; //save for relogin });
};
fn (null, {
blob : blob, function getBlob(authInfo, keys, callback) {
username : authInfo.username, blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) {
verified : authInfo.emailVerified if (err) {
}); return callback(err);
}
//save for relogin
self.infos[keys.id] = authInfo;
callback(null, {
blob: blob,
username: authInfo.username,
verified: authInfo.emailVerified
}); });
}); });
}); };
};
var steps = [
getAuthInfo,
deriveLoginKeys,
getBlob
];
async.waterfall(steps, callback);
};
/** /**
* Retreive and decrypt blob using a blob url, id and crypt derived previously. * Retreive and decrypt blob using a blob url, id and crypt derived previously.
*
* @param {string} url - Blob vault url * @param {string} url - Blob vault url
* @param {string} id - Blob id from previously retreived blob * @param {string} id - Blob id from previously retreived blob
* @param {string} key - Blob decryption key * @param {string} key - Blob decryption key
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.relogin = function(url, id, key, fn) {
VaultClient.prototype.relogin = function(url, id, key, callback) {
//use the url from previously retrieved authInfo, if necessary //use the url from previously retrieved authInfo, if necessary
if (!url && this.infos[id]) url = this.infos[id].blobvault; if (!url && this.infos[id]) {
url = this.infos[id].blobvault;
if (!url) return fn(new Error("Blob vault URL is required")); }
blobClient.get(url, id, key, function (err, blob) { if (!url) {
if (err) return fn(err); return callback(new Error('Blob vault URL is required'));
}
fn (null, {
blob : blob, blobClient.get(url, id, key, function(err, blob) {
}); if (err) {
callback(err);
} else {
callback (null, { blob: blob });
}
}); });
}; };
/** /**
* Decrypt the secret key using a username and password * Decrypt the secret key using a username and password
*
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {string} encryptSecret * @param {string} encryptSecret
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) {
VaultClient.prototype.unlock = function(username, password, encryptSecret, callback) {
var self = this; var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){ function deriveUnlockKey(authInfo, callback) {
if (err) return fn(err);
//derive unlock key //derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){ crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) {
if (err) return fn(err); if (err) {
return callback(err);
fn(null, { }
keys : keys,
secret : crypt.decrypt(keys.unlock, encryptSecret) callback(null, {
}); keys: keys,
}); secret: crypt.decrypt(keys.unlock, encryptSecret)
});
});
};
self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) {
callback(err);
} else {
deriveUnlockKey(authInfo, callback);
}
}); });
}; };
/** /**
* Retrieve the decrypted blob and secret key in one step using * Retrieve the decrypted blob and secret key in one step using
* the username and password * the username and password
*
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.loginAndUnlock = function(username, password, fn) {
VaultClient.prototype.loginAndUnlock = function(username, password, callback) {
var self = this; var self = this;
this.login(username, password, function(err, resp){ function deriveUnlockKey(authInfo, blob, callback) {
if (err) return fn(err);
if (!resp.blob || !resp.blob.encrypted_secret)
return fn(new Error("Unable to retrieve blob and secret."));
if (!resp.blob.id || !resp.blob.key)
return fn(new Error("Unable to retrieve keys."));
//get authInfo via id - would have been saved from login
var authInfo = self.infos[resp.blob.id];
if (!authInfo) return fn(new Error("Unable to find authInfo"));
//derive unlock key //derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){ crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) {
if (err) return fn(err); if (err) {
return callback(err);
fn(null, { }
blob : resp.blob,
unlock : keys.unlock, callback(null, {
secret : crypt.decrypt(keys.unlock, resp.blob.encrypted_secret), blob: blob,
username : authInfo.username, unlock: keys.unlock,
verified : authInfo.emailVerified secret: crypt.decrypt(keys.unlock, blob.encrypted_secret),
username: authInfo.username,
verified: authInfo.emailVerified
}); });
}); });
};
this.login(username, password, function(err, resp) {
if (err) {
return callback(err);
}
if (!resp.blob || !resp.blob.encrypted_secret) {
return callback(new Error('Unable to retrieve blob and secret.'));
}
if (!resp.blob.id || !resp.blob.key) {
return callback(new Error('Unable to retrieve keys.'));
}
//get authInfo via id - would have been saved from login
var authInfo = self.infos[resp.blob.id];
if (!authInfo) {
return callback(new Error('Unable to find authInfo'));
}
deriveUnlockKey(authInfo, resp.blob, callback);
}); });
}; };
/** /**
* Check blobvault for existance of username * Check blobvault for existance of username
*
* @param {string} username * @param {string} username
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.exists = function (username, fn) {
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo){ VaultClient.prototype.exists = function(username, callback) {
if (err) return fn(err); this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
return fn(null, !!authInfo.exists); if (err) {
callback(err);
} else {
callback(null, !!authInfo.exists);
}
}); });
} };
/**
/*
* Verify an email address for an existing user * Verify an email address for an existing user
*
* @param {string} username * @param {string} username
* @param {string} token - Verification token * @param {string} token - Verification token
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.verify = function (username, token, fn) {
this.authInfo.get(this.domain, username.toLowerCase(), function (err, authInfo) {
if (err) return fn(err);
if ("string" !== typeof authInfo.blobvault) { VaultClient.prototype.verify = function(username, token, callback) {
return fn(new Error("No blobvault specified in the authinfo.")); var self = this;
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) {
return callback(err);
} }
blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, fn); if (typeof authInfo.blobvault !== 'string') {
}); return callback(new Error('No blobvault specified in the authinfo.'));
} }
blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, callback);
});
};
/* /**
* Register a new user and save to the blob vault * Register a new user and save to the blob vault
* *
* @param {object} options * @param {object} options
* @param {string} options.username * @param {string} options.username
* @param {string} options.password * @param {string} options.password
@@ -233,53 +275,77 @@ VaultClient.prototype.verify = function (username, token, fn) {
* @param {object} options.oldUserBlob //optional * @param {object} options.oldUserBlob //optional
* @param {function} fn * @param {function} fn
*/ */
VaultClient.prototype.register = function (options, fn) {
var self = this,
username = this.normalizeUsername(options.username),
password = this.normalizePassword(options.password);
self.authInfo.get(self.domain, username, function(err, authInfo){ VaultClient.prototype.register = function(options, fn) {
var self = this;
if (err) return fn(err); var username = String(options.username).trim();
var password = String(options.password).trim();
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo.")); function getAuthInfo(callback) {
} self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) {
return callback(err);
if (!authInfo.pakdf) { }
return fn(new Error("No settings for PAKDF in auth packet."));
} if (typeof authInfo.blobvault !== 'string') {
return callback(new Error('No blobvault specified in the authinfo.'));
//derive login keys }
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, loginKeys){
if (err) return fn(err); if (!authInfo.pakdf) {
return callback(new Error('No settings for PAKDF in auth packet.'));
//derive unlock key }
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, unlockKeys){
if (err) return fn(err); callback(null, authInfo);
var params = {
'url' : authInfo.blobvault,
'id' : loginKeys.id,
'crypt' : loginKeys.crypt,
'unlock' : unlockKeys.unlock,
'username' : username,
'email' : options.email,
'masterkey' : options.masterkey || crypt.createMaster(),
'activateLink' : options.activateLink,
'oldUserBlob' : options.oldUserBlob
}
blobClient.create(params, function(err, blob){
if (err) return fn(err);
fn(null, blob, loginKeys, authInfo.username);
});
});
}); });
}); };
function deriveKeys(authInfo, callback) {
// derive unlock and login keys
var keys = { };
function deriveKey(keyType, callback) {
crypt.derive(authInfo.pakdf, keyType, username.toLowerCase(), password, function(err, key) {
if (err) {
callback(err);
} else {
keys[keyType] = key;
callback();
}
});
};
async.eachSeries([ 'login', 'unlock' ], deriveKey, function(err) {
if (err) {
callback(err);
} else {
callback(null, authInfo, keys);
}
});
};
function create(authInfo, keys) {
var params = {
url: authInfo.blobvault,
id: keys.loginKeys.id,
crypt: keys.loginKeys.crypt,
unlock: keys.unlockKeys.unlock,
username: username,
email: options.email,
masterkey: options.masterkey || crypt.createMaster(),
activateLink: options.activateLink,
oldUserBlob: options.oldUserBlob
};
blobClient.create(params, function(err, blob) {
if (err) {
callback(err);
} else {
callback(null, blob, loginKeys, authInfo.username);
}
});
};
async.waterfall([ getAuthInfo, deriveKeys ], create);
}; };
exports.VaultClient = VaultClient;
module.exports = VaultClient;

View File

@@ -1,7 +1,7 @@
var assert = require('assert'), var assert = require('assert'),
RippleTxt = require('../src/js/ripple/rippletxt'), RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt,
AuthInfo = require('../src/js/ripple/authinfo'), AuthInfo = require('../src/js/ripple/authinfo').AuthInfo,
VaultClient = require('../src/js/ripple/vaultclient'), VaultClient = require('../src/js/ripple/vaultclient').VaultClient,
Blob = require('../src/js/ripple/blob').Blob, Blob = require('../src/js/ripple/blob').Blob,
UInt256 = require('../src/js/ripple/uint256').UInt256; UInt256 = require('../src/js/ripple/uint256').UInt256;
@@ -22,6 +22,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed c
describe('Ripple Txt', function() { describe('Ripple Txt', function() {
it('should get the context of a ripple.txt file from a given domain', function(done){ it('should get the context of a ripple.txt file from a given domain', function(done){
this.timeout(10000);
var rt = new RippleTxt(); var rt = new RippleTxt();
rt.get(exampleData.domain, function(err, resp){ rt.get(exampleData.domain, function(err, resp){
assert.ifError(err); assert.ifError(err);
@@ -36,6 +37,7 @@ describe('AuthInfo', function() {
var auth = new AuthInfo(); var auth = new AuthInfo();
it ('should', function(done){ it ('should', function(done){
this.timeout(10000);
auth.get(exampleData.domain, exampleData.user, function(err, resp){ auth.get(exampleData.domain, exampleData.user, function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'object'); assert.equal(typeof resp, 'object');