diff --git a/src/js/ripple/authinfo.js b/src/js/ripple/authinfo.js index 785663ff..15bd642a 100644 --- a/src/js/ripple/authinfo.js +++ b/src/js/ripple/authinfo.js @@ -1,36 +1,45 @@ -var RippleTxt = require('./rippletxt'); var request = require('superagent'); +var RippleTxt = require('./rippletxt').RippleTxt; -function AuthInfo () { +function AuthInfo() { this.rippleTxt = new RippleTxt; -} +}; /** * Get auth info for a given username + * * @param {string} domain - Domain which hosts the user's info * @param {string} username - Username who's info we are retreiving * @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; \ No newline at end of file +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; diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index b2b87c68..10392cc3 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -1,124 +1,136 @@ -var crypt = require('./crypt'), - request = require('superagent'), - extend = require("extend"); +var request = require('superagent'); +var extend = require('extend'); +var crypt = require('./crypt').Crypt; //Blob object class -var BlobObj = function (url, id, key) { +function BlobObj(url, id, key) { this.url = url; this.id = id; this.key = key; - this.data = {}; + this.data = { }; }; // Blob operations // Do NOT change the mapping of existing ops BlobObj.ops = { // Special - "noop" : 0, + noop: 0, // Simple ops - "set" : 16, - "unset" : 17, - "extend" : 18, + set: 16, + unset: 17, + extend: 18, // Meta ops - "push" : 32, - "pop" : 33, - "shift" : 34, - "unshift" : 35, - "filter" : 36 + push: 32, + pop: 33, + shift: 34, + unshift: 35, + filter: 36 }; -BlobObj.opsReverseMap = []; -for (var name in BlobObj.ops) { +BlobObj.opsReverseMap = [ ]; + +for (var name in BlobObj.ops) { BlobObj.opsReverseMap[BlobObj.ops[name]] = name; } - -/* +/** * Initialize a new blob object + * * @param {function} fn - Callback function */ -BlobObj.prototype.init = function (fn) { + +BlobObj.prototype.init = function(fn) { 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; - - request.get(url, function(err, resp){ - 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; - - if (!self.decrypt(resp.body.blob)) { - return fn(new Error("Error while decrypting blob")); + request.get(url, function(err, resp) { + 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; + + 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 patches as a new revision + * * @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 config = { - method : 'POST', - url : this.url + '/v1/blob/consolidate', - dataType : 'json', - data : { - blob_id : this.id, - data : encrypted, - revision : this.revision + method: 'POST', + url: this.url + '/v1/blob/consolidate', + dataType: 'json', + data: { + blob_id: this.id, + data: encrypted, + revision: this.revision }, }; - + var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); - + request.post(signed.url) .send(signed.data) .end(function(err, resp) { - - // XXX Add better error information to exception - if (err) return fn(new Error("Failed to consolidate blob - XHR error")); - else if (resp.body && resp.body.result === 'success') return fn(null, resp.body); - else return fn(new Error("Failed to consolidate blob")); + // XXX Add better error information to exception + if (err) { + fn(new Error('Failed to consolidate blob - XHR error')); + } else if (resp.body && resp.body.result === 'success') { + fn(null, resp.body); + } else { + fn(new Error('Failed to consolidate blob')); + } }); }; - - -/* + +/** * ApplyEncryptedPatch - * save changes from a downloaded patch to the blob + * * @param {string} patch - encrypted patch string */ -BlobObj.prototype.applyEncryptedPatch = function (patch) -{ + +BlobObj.prototype.applyEncryptedPatch = function(patch) { try { var params = JSON.parse(crypt.decrypt(this.key, patch)); var op = params.shift(); @@ -128,55 +140,55 @@ BlobObj.prototype.applyEncryptedPatch = function (patch) this.revision++; return true; - } catch (err) { - console.log("client: blob: failed to apply patch:", err.toString()); - console.log(err.stack); + //console.log('client: blob: failed to apply patch:', err.toString()); + //console.log(err.stack); return false; } -} - +}; /** * Encrypt secret with unlock key + * * @param {string} secretUnlockkey */ -BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) { + +BlobObj.prototype.encryptSecret = function(secretUnlockKey, secret) { return crypt.encrypt(secretUnlockKey, secret); }; /** * Decrypt secret with unlock key + * * @param {string} secretUnlockkey */ -BlobObj.prototype.decryptSecret = function (secretUnlockKey) { + +BlobObj.prototype.decryptSecret = function(secretUnlockKey) { return crypt.decrypt(secretUnlockKey, this.encrypted_secret); }; - /** - * Decrypt blob with crypt key + * Decrypt blob with crypt key + * * @param {string} data - encrypted blob data */ -BlobObj.prototype.decrypt = function (data) { - + +BlobObj.prototype.decrypt = function(data) { try { this.data = JSON.parse(crypt.decrypt(this.key, data)); return this; } catch (e) { - console.log("client: blob: decryption failed", e.toString()); - console.log(e.stack); + //console.log('client: blob: decryption failed', e.toString()); + //console.log(e.stack); 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 // if ('object' === typeof this.data && // 'object' === typeof this.data.contacts) @@ -185,68 +197,67 @@ BlobObj.prototype.encrypt = function() return crypt.encrypt(this.key, JSON.stringify(this.data)); }; - /** - * Encrypt recovery key + * Encrypt recovery key + * * @param {string} secret * @param {string} blobDecryptKey */ -BlobObj.prototype.encryptBlobCrypt = function (secret, blobDecryptKey) { + +BlobObj.prototype.encryptBlobCrypt = function(secret, blobDecryptKey) { var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey); }; /** * Decrypt recovery key + * * @param {string} secret */ -BlobObj.prototype.decryptBlobCrypt = function (secret) { + +BlobObj.prototype.decryptBlobCrypt = function(secret) { var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); }; - - - /**** Blob updating functions ****/ - /** * Set blob element */ -BlobObj.prototype.set = function (pointer, value, fn) { + +BlobObj.prototype.set = function(pointer, value, fn) { this.applyUpdate('set', pointer, [value]); this.postUpdate('set', pointer, [value], fn); }; - /** * Remove blob element */ -BlobObj.prototype.unset = function (pointer, fn) { + +BlobObj.prototype.unset = function(pointer, fn) { this.applyUpdate('unset', pointer, []); this.postUpdate('unset', pointer, [], fn); }; - /** * Extend blob object */ -BlobObj.prototype.extend = function (pointer, value, fn) { + +BlobObj.prototype.extend = function(pointer, value, fn) { this.applyUpdate('extend', pointer, [value]); this.postUpdate('extend', pointer, [value], fn); }; - /** * Prepend blob array */ -BlobObj.prototype.unshift = function (pointer, value, fn) { + +BlobObj.prototype.unshift = function(pointer, value, fn) { this.applyUpdate('unshift', pointer, [value]); this.postUpdate('unshift', pointer, [value], fn); }; - /** * 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. */ -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); - if ("function" === typeof params[params.length-1]) { + if (typeof params[params.length - 1] === 'function') { callback = params.pop(); } + params.shift(); // Normalize subcommands to minimize the patch size params = params.slice(0, 2).concat(normalizeSubcommands(params.slice(2), true)); - + this.applyUpdate('filter', pointer, params); this.postUpdate('filter', pointer, params, callback); }; - /** * 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 - if ("number" === typeof op) { + if (typeof op === 'number') { 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" - var pointer = path.split("/"); - + // Separate each step in the 'pointer' + var pointer = path.split('/'); 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); }; - //for applyUpdate function -BlobObj.prototype._traverse = function (context, pointer, - originalPointer, op, params) { +BlobObj.prototype._traverse = function(context, pointer, originalPointer, op, params) { var _this = this; var part = _this.unescapeToken(pointer.shift()); @@ -305,16 +315,15 @@ BlobObj.prototype._traverse = function (context, pointer, if (part === '-') { part = context.length; } else if (part % 1 !== 0 && part >= 0) { - throw new Error("Invalid pointer, array element segments must be " + - "a positive integer, zero or '-'"); + throw new Error('Invalid pointer, array element segments must be a positive integer, zero or '-''); } - } else if ("object" !== typeof context) { + } else if (typeof context !== 'object') { return null; } else if (!context.hasOwnProperty(part)) { // Some opcodes create the path as they're going along - if (op === "set") { + if (op === 'set') { context[part] = {}; - } else if (op === "unshift") { + } else if (op === 'unshift') { context[part] = []; } else { return null; @@ -322,139 +331,133 @@ BlobObj.prototype._traverse = function (context, pointer, } if (pointer.length !== 0) { - return this._traverse(context[part], pointer, - originalPointer, op, params); + return this._traverse(context[part], pointer, originalPointer, op, params); } switch (op) { - case "set": - context[part] = params[0]; - break; - case "unset": - if (Array.isArray(context)) { - context.splice(part, 1); - } else { - delete context[part]; - } - break; - case "extend": - if ("object" !== typeof context[part]) { - throw new Error("Tried to extend a non-object"); - } - extend(true, context[part], params[0]); - break; - case "unshift": - if ("undefined" === typeof context[part]) { - context[part] = []; - } else if (!Array.isArray(context[part])) { - throw new Error("Operator 'unshift' must be applied to an array."); - } - context[part].unshift(params[0]); - break; - case "filter": - if (Array.isArray(context[part])) { - context[part].forEach(function (element, i) { - if ("object" === typeof element && - element.hasOwnProperty(params[0]) && - element[params[0]] === params[1]) { - var subpointer = originalPointer+"/"+i; - var subcommands = normalizeSubcommands(params.slice(2)); + case 'set': + context[part] = params[0]; + break; + case 'unset': + if (Array.isArray(context)) { + context.splice(part, 1); + } else { + delete context[part]; + } + break; + case 'extend': + if (typeof context[part] !== 'object') { + throw new Error('Tried to extend a non-object'); + } + extend(true, context[part], params[0]); + break; + case 'unshift': + if (typeof context[part] === 'undefined') { + context[part] = [ ]; + } else if (!Array.isArray(context[part])) { + throw new Error('Operator "unshift" must be applied to an array.'); + } + context[part].unshift(params[0]); + break; + case 'filter': + if (Array.isArray(context[part])) { + context[part].forEach(function(element, i) { + if (typeof element === 'object' && element.hasOwnProperty(params[0]) && element[params[0]] === params[1]) { + var subpointer = originalPointer + '/' + i; + var subcommands = normalizeSubcommands(params.slice(2)); - subcommands.forEach(function (subcommand) { - var op = subcommand[0]; - var pointer = subpointer+subcommand[1]; - _this.applyUpdate(op, pointer, subcommand.slice(2)); - }); - } - }); - } - break; - default: - throw new Error("Unsupported op "+op); + subcommands.forEach(function(subcommand) { + var op = subcommand[0]; + var pointer = subpointer + subcommand[1]; + _this.applyUpdate(op, pointer, subcommand.slice(2)); + }); + } + }); + } + break; + default: + throw new Error('Unsupported op '+op); } }; - -BlobObj.prototype.escapeToken = function (token) { - return token.replace(/[~\/]/g, function (key) { return key === "~" ? "~0" : "~1"; }); +BlobObj.prototype.escapeToken = function(token) { + return token.replace(/[~\/]/g, function(key) { + return key === '~' ? '~0' : '~1'; + }); }; - BlobObj.prototype.unescapeToken = function(str) { return str.replace(/~./g, function(m) { switch (m) { - case "~0": - return "~"; - case "~1": - return "/"; + case '~0': + return '~'; + case '~1': + return '/'; } - throw("Invalid tilde escape: " + m); + throw new Error('Invalid tilde escape: ' + m); }); }; - /** * 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]; } - if ("number" !== typeof op) { - 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"); + + if (typeof op !== 'number') { + throw new Error('Blob update op code must be a number or a valid op id string'); } - 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(op); var config = { - method : 'POST', - url : this.url + '/v1/blob/patch', - dataType : 'json', - data : { - blob_id : this.id, - patch : crypt.encrypt(this.key, JSON.stringify(params)) + method: 'POST', + url: this.url + '/v1/blob/patch', + dataType: 'json', + data: { + blob_id: this.id, + patch: crypt.encrypt(this.key, JSON.stringify(params)) } }; - var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); request.post(signed.url) - .send(signed.data) - .end(function(err, resp) { - if (err) - return fn(new Error("Patch could not be saved - XHR error")); - else if (!resp.body || resp.body.result !== 'success') - return fn(new Error("Patch could not be saved - bad result")); - - return fn(null, resp.body); + .send(signed.data) + .end(function(err, resp) { + if (err) { + fn(new Error('Patch could not be saved - XHR error')); + } else if (!resp.body || resp.body.result !== 'success') { + fn(new Error('Patch could not be saved - bad result')); + } else { + fn(null, resp.body); + } }); }; - - /***** helper functions *****/ - function normalizeSubcommands(subcommands, compress) { // Normalize parameter structure - if ("number" === typeof subcommands[0] || - "string" === typeof subcommands[0]) { + if (/(number|string)/.test(typeof subcommands[0])) { // Case 1: Single subcommand inline subcommands = [subcommands]; - } else if (subcommands.length === 1 && - Array.isArray(subcommands[0]) && - ("number" === typeof subcommands[0][0] || - "string" === typeof subcommands[0][0])) { + } else if (subcommands.length === 1 && Array.isArray(subcommands[0]) && /(number|string)/.test(subcommands[0][0])) { // Case 2: Single subcommand as array // (nothing to do) } else if (Array.isArray(subcommands[0])) { @@ -463,16 +466,19 @@ function normalizeSubcommands(subcommands, compress) { } // Normalize op name and convert strings to numeric codes - subcommands = subcommands.map(function (subcommand) { - if ("string" === typeof subcommand[0]) { + subcommands = subcommands.map(function(subcommand) { + if (typeof subcommand[0] === 'string') { 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; }); @@ -486,53 +492,63 @@ function normalizeSubcommands(subcommands, compress) { } else { return subcommands; } -} - +}; /***** blob client methods ****/ - /** * 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) { - - if (!crypt.isValidAddress(address)) return fn (new Error("Invalid ripple address")); + +exports.getRippleName = function(url, address, fn) { + if (!crypt.isValidAddress(address)) { + return fn (new Error('Invalid ripple address')); + } + request.get(url + '/v1/user/' + address, function(err, resp){ - if (err) return fn(new Error("Unable to access vault sever")); - else if (resp.body && resp.body.username) return fn(null, resp.body.username); - else if (resp.body && resp.body.exists === false) return fn (new Error("No ripple name for this address")); - else return fn(new Error("Unable to determine if ripple name exists")); - }); -} + if (err) { + fn(new Error('Unable to access vault sever')); + } else if (resp.body && resp.body.username) { + 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); blob.init(fn); -} +}; - -/* +/** * Verify email address */ -module.exports.verify = function (url, username, token, fn) { + +exports.verify = function(url, username, token, fn) { url += '/v1/user/' + username + '/verify/' + token; request.get(url, function(err, resp){ - if (err) return fn(err); - else if (resp.body && resp.body.result === 'success') return fn(null, data); - else return fn(new Error("Failed to verify the account")); - }); -} - + if (err) { + fn(err); + } else if (resp.body && resp.body.result === 'success') { + fn(null, data); + } else { + fn(new Error('Failed to verify the account')); + } + }); +}; /** * Create a blob object @@ -547,51 +563,55 @@ module.exports.verify = function (url, username, token, fn) { * @param {object} options.oldUserBlob * @param {function} fn */ -module.exports.create = function (options, fn) -{ - - var blob = new BlobObj(options.url, options.id, options.crypt); - + +exports.create = function(options, fn) { + var blob = new BlobObj(options.url, options.id, options.crypt); + blob.revision = 0; - blob.data = { - auth_secret : crypt.createSecret(8), - account_id : crypt.getAddress(options.masterkey), - email : options.email, - contacts : [], - created : (new Date()).toJSON() + + blob.data = { + auth_secret: crypt.createSecret(8), + account_id: crypt.getAddress(options.masterkey), + email: options.email, + contacts: [], + created: (new Date()).toJSON() }; - + blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey); // Migration if (options.oldUserBlob) { blob.data.contacts = options.oldUserBlob.data.contacts; } - + //post to the blob vault to create var config = { - method : "POST", - url : options.url + '/v1/user', - data : { - blob_id : options.id, - username : options.username, - address : blob.data.account_id, - auth_secret : blob.data.auth_secret, - data : blob.encrypt(), - email : options.email, - hostlink : options.activateLink, - encrypted_blobdecrypt_key : blob.encryptBlobCrypt(options.masterkey, options.crypt), - encrypted_secret : blob.encrypted_secret + method: 'POST', + url: options.url + '/v1/user', + data: { + blob_id: options.id, + username: options.username, + address: blob.data.account_id, + auth_secret: blob.data.auth_secret, + data: blob.encrypt(), + email: options.email, + hostlink: options.activateLink, + encrypted_blobdecrypt_key: blob.encryptBlobCrypt(options.masterkey, options.crypt), + encrypted_secret: blob.encrypted_secret } }; - + var signed = crypt.signRequestAsymmetric(config, options.masterkey, blob.data.account_id, options.id); - + request.post(signed) .send(signed.data) .end(function(err, resp) { - if (err) return fn(err); - else if (resp.body && resp.body.result === 'success') return fn(null, blob,resp.body); - else return fn(new Error("Could not create blob")); - }); -} \ No newline at end of file + if (err) { + fn(err); + } else if (resp.body && resp.body.result === 'success') { + fn(null, blob,resp.body); + } else { + fn(new Error('Could not create blob')); + } + }); +}; diff --git a/src/js/ripple/crypt.js b/src/js/ripple/crypt.js index 04d3f2d2..f62f7ed0 100644 --- a/src/js/ripple/crypt.js +++ b/src/js/ripple/crypt.js @@ -1,34 +1,36 @@ -var sjcl = require('./utils').sjcl, - base = require('./base').Base, - UInt160 = require('./uint160').UInt160, - message = require('./message'), - request = require('superagent'), - extend = require("extend"), - parser = require("url"); - +var parser = require('url'); +var querystring = require('querystring'); +var extend = require('extend'); +var request = require('superagent'); +var sjcl = require('./utils').sjcl; +var base = require('./base').Base; +var UInt160 = require('./uint160').UInt160; +var message = require('./message'); + var cryptConfig = { - cipher : "aes", - mode : "ccm", - ts : 64, // tag length - ks : 256, // key size - iter : 1000 // iterations (key derivation) + cipher: 'aes', + mode: 'ccm', + ts: 64, // tag length + ks: 256, // key size + iter: 1000 // iterations (key derivation) }; -var Crypt = {}; +var Crypt = { }; /** * Full domain hash based on SHA512 - */ -function fdh(data, bytelen) -{ + */ + +function fdh(data, bytelen) { var bitlen = bytelen << 3; - if (typeof data === "string") { + if (typeof data === 'string') { data = sjcl.codec.utf8String.toBits(data); } // Add hashing rounds until we exceed desired length in bits var counter = 0, output = []; + while (sjcl.bitArray.bitLength(output) < bitlen) { var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data)); output = sjcl.bitArray.concat(output, hash); @@ -39,132 +41,137 @@ function fdh(data, bytelen) output = sjcl.bitArray.clamp(output, bitlen); return output; -} +}; /** * This is a function to derive different hashes from the same key. * Each hash is derived as HMAC-SHA512HALF(key, token). + * * @param {string} key * @param {string} hash - */ + */ + function keyHash(key, token) { var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512); return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256)); -} - - +}; /****** exposed functions ******/ - /** * KEY DERIVATION FUNCTION * * This service takes care of the key derivation, i.e. converting low-entropy * secret into higher entropy secret via either computationally expensive * processes or peer-assisted key derivation (PAKDF). + * * @param {object} opts * @param {string} purpose - Key type/purpose - * @param {string} username + * @param {string} username * @param {string} secret - Also known as passphrase/password * @param {function} fn */ -Crypt.derive = function (opts, purpose, username, secret, fn) { +Crypt.derive = function(opts, purpose, username, secret, fn) { var tokens; - if (purpose=='login') tokens = ['id', 'crypt']; - else tokens = ['unlock']; - var iExponent = new sjcl.bn(String(opts.exponent)), - iModulus = new sjcl.bn(String(opts.modulus)), - iAlpha = new sjcl.bn(String(opts.alpha)); + if (purpose === 'login') { + tokens = ['id', 'crypt']; + } else { + tokens = ['unlock']; + } - var publicInfo = "PAKDF_1_0_0:"+opts.host.length+":"+opts.host+ - ":"+username.length+":"+username+ - ":"+purpose.length+":"+purpose+ - ":", - publicSize = Math.ceil(Math.min((7+iModulus.bitLength()) >>> 3, 256)/8), - publicHash = fdh(publicInfo, publicSize), - publicHex = sjcl.codec.hex.fromBits(publicHash), - iPublic = new sjcl.bn(String(publicHex)).setBitM(0), - secretInfo = publicInfo+":"+secret.length+":"+secret+":", - secretSize = (7+iModulus.bitLength()) >>> 3, - secretHash = fdh(secretInfo, secretSize), - secretHex = sjcl.codec.hex.fromBits(secretHash), - iSecret = new sjcl.bn(String(secretHex)).mod(iModulus); + var iExponent = new sjcl.bn(String(opts.exponent)); + var iModulus = new sjcl.bn(String(opts.modulus)); + var iAlpha = new sjcl.bn(String(opts.alpha)); + + var publicInfo = [ 'PAKDF_1_0_0', opts.host.length, opts.host, username.length, username, purpose.length, purpose ].join(':') + ':'; + var publicSize = Math.ceil(Math.min((7 + iModulus.bitLength()) >>> 3, 256) / 8); + var publicHash = fdh(publicInfo, publicSize); + var publicHex = sjcl.codec.hex.fromBits(publicHash); + var iPublic = new sjcl.bn(String(publicHex)).setBitM(0); + var secretInfo = [ publicInfo, secret.length, secret ].join(':') + ':'; + var secretSize = (7 + iModulus.bitLength()) >>> 3; + var secretHash = fdh(secretInfo, secretSize); + var secretHex = sjcl.codec.hex.fromBits(secretHash); + var iSecret = new sjcl.bn(String(secretHex)).mod(iModulus); if (iSecret.jacobi(iModulus) !== 1) { iSecret = iSecret.mul(iAlpha).mod(iModulus); } var iRandom; + for (;;) { iRandom = sjcl.bn.random(iModulus, 0); - if (iRandom.jacobi(iModulus) === 1) + if (iRandom.jacobi(iModulus) === 1) { break; + } } - var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus), - iSignreq = iSecret.mulmod(iBlind, iModulus), - signreq = sjcl.codec.hex.fromBits(iSignreq.toBits()); - + var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus); + var iSignreq = iSecret.mulmod(iBlind, iModulus); + var signreq = sjcl.codec.hex.fromBits(iSignreq.toBits()); + request.post(opts.url) - .send({ - info : publicInfo, - signreq : signreq - }).end(function(err, resp) { - - if (err || !resp) return fn(new Error("Could not query PAKDF server "+opts.host)); - - var data = resp.body || resp.text ? JSON.parse(resp.text) : {}; - - if (!data.result=='success') return fn(new Error("Could not query PAKDF server "+opts.host)); - - var iSignres = new sjcl.bn(String(data.signres)); - iRandomInv = iRandom.inverseMod(iModulus), - iSigned = iSignres.mulmod(iRandomInv, iModulus), - key = iSigned.toBits(), - result = {}; - - tokens.forEach(function (token) { - result[token] = keyHash(key, token); + .send({ info: publicInfo, signreq: signreq }) + .end(function(err, resp) { + if (err || !resp) { + return fn(new Error('Could not query PAKDF server ' + opts.host)); + } + + var data = resp.body || resp.text ? JSON.parse(resp.text) : {}; + + if (data.result !== 'success') { + return fn(new Error('Could not query PAKDF server '+opts.host)); + } + + var iSignres = new sjcl.bn(String(data.signres)); + var iRandomInv = iRandom.inverseMod(iModulus); + var iSigned = iSignres.mulmod(iRandomInv, iModulus); + var key = iSigned.toBits(); + var result = { }; + + tokens.forEach(function(token) { + result[token] = keyHash(key, token); + }); + + fn(null, result); }); - - fn (null, result); - }); -} +}; /** * Imported from ripple-client - * */ -Crypt.RippleAddress = (function () { + +Crypt.RippleAddress = (function() { function append_int(a, i) { return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff) - } + }; function firstHalfOfSHA512(bytes) { return sjcl.bitArray.bitSlice( sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)), 0, 256 ); - } + }; function SHA256_RIPEMD160(bits) { return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); - } + }; - return function (seed) { + return function(seed) { this.seed = base.decode_check(33, seed); if (!this.seed) { - throw "Invalid seed." + throw new Error('Invalid seed.'); } - this.getAddress = function (seq) { + this.getAddress = function(seq) { seq = seq || 0; var private_gen, public_gen, i = 0; + do { private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i))); i++; @@ -174,6 +181,7 @@ Crypt.RippleAddress = (function () { var sec; i = 0; + do { sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i))); i++; @@ -188,11 +196,12 @@ Crypt.RippleAddress = (function () { /** * 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); var opts = extend(true, {}, cryptConfig); @@ -206,23 +215,23 @@ Crypt.encrypt = function(key, data) encryptedBits = sjcl.bitArray.concat(encryptedBits, ciphertext); return sjcl.codec.base64.fromBits(encryptedBits); -} - +}; /** * 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); var encryptedBits = sjcl.codec.base64.toBits(data); var version = sjcl.bitArray.extract(encryptedBits, 0, 8); if (version !== 0) { - throw new Error("Unsupported encryption version: "+version); + throw new Error('Unsupported encryption version: '+version); } var encrypted = extend(true, {}, cryptConfig, { @@ -231,109 +240,121 @@ Crypt.decrypt = function(key, data) }); return sjcl.decrypt(key, JSON.stringify(encrypted)); -} - +}; /** * Validate a ripple 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 + * * @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 */ -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 + * * @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 + * * @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 + * * @param {string} secret * @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); return sjcl.codec.hex.fromBits(hmac.mac(data)); -} - +}; /** * Create an an accout recovery key + * * @param {string} secret */ + Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) { var seed = ripple.Seed.from_json(secret).to_bits(); 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); return sjcl.codec.hex.fromBits(key); -} +}; /** * Convert base64 encoded data into base64url encoded data. + * * @param {String} base64 Data */ -Crypt.base64ToBase64Url = function (encodedData) { + +Crypt.base64ToBase64Url = function(encodedData) { return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, ''); }; /** * Convert base64url encoded data into base64 encoded data. + * * @param {String} base64 Data */ -Crypt.base64UrlToBase64 = function (encodedData) { + +Crypt.base64UrlToBase64 = function(encodedData) { encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/'); + while (encodedData.length % 4) { encodedData += '='; } + return encodedData; }; - /** * Create a string from request parameters that * will be used to sign a request + * * @param {Object} config - request params * @param {Object} parsed - parsed url * @param {Object} date * @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 // field will be merged into the search string, not the request body. - // Sort the properties of the JSON object into canonical form var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data)); @@ -360,16 +381,17 @@ Crypt.getStringToSign = function (config, parsed, date, mechanism) { date, Crypt.hashSha512(canonicalRequest).toLowerCase() ].join('\n'); -} - +}; /** * HMAC signed request + * * @param {Object} config * @param {Object} auth_secret * @param {Object} blob_id */ -Crypt.signRequestHmac = function (config, auth_secret, blob_id) { + +Crypt.signRequestHmac = function(config, auth_secret, blob_id) { config = extend(true, {}, config); // Parse URL @@ -378,24 +400,29 @@ Crypt.signRequestHmac = function (config, auth_secret, blob_id) { var signatureType = 'RIPPLE1-HMAC-SHA512'; var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var signature = Crypt.signString(auth_secret, stringToSign); - - config.url += (parsed.search ? "&" : "?") + - 'signature='+Crypt.base64ToBase64Url(signature)+ - '&signature_date='+date+ - '&signature_blob_id='+blob_id+ - '&signature_type='+signatureType + + var query = querystring.stringify({ + signature: Crypt.base64ToBase64Url(signature), + signature_date: date, + signature_blob_id: blob_id, + signature_type: signatureType + }); + + config.url += (parsed.search ? '&' : '?') + query; return config; -}; - +}; + /** * Asymmetric signed request + * * @param {Object} config * @param {Object} secretKey * @param {Object} account * @param {Object} blob_id */ -Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) { + +Crypt.signRequestAsymmetric = function(config, secretKey, account, blob_id) { config = extend(true, {}, config); // Parse URL @@ -404,77 +431,82 @@ Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) { var signatureType = 'RIPPLE1-ECDSA-SHA512'; var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var signature = message.signMessage(stringToSign, secretKey); - - config.url += (parsed.search ? "&" : "?") + - 'signature='+Crypt.base64ToBase64Url(signature)+ - '&signature_date='+date+ - '&signature_blob_id='+blob_id+ - '&signature_account='+account+ - '&signature_type='+signatureType; + + var query = querystring.stringify({ + signature: Crypt.base64ToBase64Url(signature), + signature_date: date, + signature_blob_id: blob_id, + signature_account: account, + signature_type: signatureType + }) + + config.url += (parsed.search ? '&' : '?') + query; return config; }; - //prepare for signing function copyObjectWithSortedKeys(object) { if (isPlainObject(object)) { var newObj = {}; var keysSorted = Object.keys(object).sort(); var key; + for (var i in keysSorted) { key = keysSorted[i]; if (Object.prototype.hasOwnProperty.call(object, key)) { newObj[key] = copyObjectWithSortedKeys(object[key]); } } + return newObj; } else if (Array.isArray(object)) { return object.map(copyObjectWithSortedKeys); } else { return object; } -} - +}; //from npm extend function isPlainObject(obj) { var hasOwn = Object.prototype.hasOwnProperty; 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; + } var has_own_constructor = hasOwn.call(obj, 'constructor'); var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // 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; + } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; + 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) { - return (n < 0 || n > 9 ? "" : "0") + n; - } + return (n < 0 || n > 9 ? '' : '0') + n; + }; return function dateAsIso8601() { var date = new Date(); - return date.getUTCFullYear() + "-" - + pad(date.getUTCMonth() + 1) + "-" - + pad(date.getUTCDate()) + "T" - + pad(date.getUTCHours()) + ":" - + pad(date.getUTCMinutes()) + ":" - + pad(date.getUTCSeconds()) + ".000Z"; + return date.getUTCFullYear() + '-' + + pad(date.getUTCMonth() + 1) + '-' + + pad(date.getUTCDate()) + 'T' + + pad(date.getUTCHours()) + ':' + + pad(date.getUTCMinutes()) + ':' + + pad(date.getUTCSeconds()) + '.000Z'; }; })(); - -module.exports = Crypt; \ No newline at end of file +exports.Crypt = Crypt; diff --git a/src/js/ripple/rippletxt.js b/src/js/ripple/rippletxt.js index c8b4af15..9f408785 100644 --- a/src/js/ripple/rippletxt.js +++ b/src/js/ripple/rippletxt.js @@ -1,21 +1,23 @@ var request = require('superagent'); - function RippleTxt() { - this.txts = {}; + this.txts = { }; }; - /** * Gets the ripple.txt file for the given domain + * * @param {string} domain - Domain to retrieve file from * @param {function} fn - Callback function */ + RippleTxt.prototype.get = function (domain, fn) { 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 = [ 'https://ripple.'+domain+'/ripple.txt', 'https://www.'+domain+'/ripple.txt', @@ -23,40 +25,47 @@ RippleTxt.prototype.get = function (domain, fn) { 'http://ripple.'+domain+'/ripple.txt', 'http://www.'+domain+'/ripple.txt', 'http://'+domain+'/ripple.txt' - ].reverse(); - - next(); - function next () { - if (!urls.length) return fn(new Error("No ripple.txt found")); - var url = urls.pop(); + ]; + + ;(function nextUrl() { + var url = urls.shift(); + + if (!url) { + return fn(new Error('No ripple.txt found')); + } request.get(url, function(err, resp) { - if (err || !resp.text) return next(); - - var sections = self.parse(resp.text); - self.txts[domain] = sections; - fn(null, sections); - }); - } -} + if (err || !resp.text) { + return nextUrl(); + } + var sections = self.parse(resp.text); + self.txts[domain] = sections; + + fn(null, sections); + }); + })(); +}; /** * Parse a ripple.txt file + * * @param {string} txt - Unparsed ripple.txt data - */ -RippleTxt.prototype.parse = function (txt) { - - txt = txt.replace('\r\n', '\n'); - txt = txt.replace('\r', '\n'); - txt = txt.split('\n'); + */ + +RippleTxt.prototype.parse = function (txt) { + var txt = txt.replace(/\r?\n/g, '\n').split('\n') + var currentSection = ''; + var sections = { }; - var currentSection = "", sections = {}; for (var i = 0, l = txt.length; i < l; i++) { var line = txt[i]; + if (!line.length || line[0] === '#') { continue; - } else if (line[0] === '[' && line[line.length-1] === ']') { + } + + if (line[0] === '[' && line[line.length-1] === ']') { currentSection = line.slice(1, line.length-1); sections[currentSection] = []; } else { @@ -68,6 +77,6 @@ RippleTxt.prototype.parse = function (txt) { } return sections; -} +}; -module.exports = RippleTxt; \ No newline at end of file +exports.RippleTxt = RippleTxt; diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js index a02ed7be..6a5e4755 100644 --- a/src/js/ripple/vaultclient.js +++ b/src/js/ripple/vaultclient.js @@ -1,229 +1,271 @@ -var AuthInfo = require('./authinfo'); +var async = require('async'); var blobClient = require('./blob'); -var crypt = require('./crypt'); - +var AuthInfo = require('./authinfo').AuthInfo; +var crypt = require('./crypt').Crypt; function VaultClient(opts) { - if (!opts) opts = {}; - else if (typeof opts === "string") opts = {domain:opts}; - - this.domain = opts.domain || 'ripple.com'; - this.authInfo = new AuthInfo; - this.infos = {}; + if (!opts) { + opts = {}; + } + + if (typeof opts === 'string') { + 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 * @param {string} address - Account address to query * @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 - if (!url) return fn(new Error("Blob vault URL is required")); - blobClient.getRippleName(url, address, fn); + if (!url) { + 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 + * * @param {string} username * @param {string} password * @param {function} fn - Callback function */ -VaultClient.prototype.login = function(username, password, fn) { + +VaultClient.prototype.login = function(username, password, callback) { 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) { - return fn(new Error("User does not exist.")); - } + function getAuthInfo(callback) { + self.authInfo.get(self.domain, username, function(err, authInfo) { + if (err) { + return callback(err); + } - if ("string" !== typeof authInfo.blobvault) { - return fn(new Error("No blobvault specified in the authinfo.")); - } - - + if (authInfo.version !== 3) { + 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 - crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys){ - if (err) return fn(err); - - blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function (err, blob) { - if (err) return fn(err); - - self.infos[keys.id] = authInfo; //save for relogin - - fn (null, { - blob : blob, - username : authInfo.username, - verified : authInfo.emailVerified - }); + crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys) { + if (err) { + callback(err); + } else { + callback(null, authInfo, keys); + } + }); + }; + + function getBlob(authInfo, keys, callback) { + blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) { + 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. + * * @param {string} url - Blob vault url * @param {string} id - Blob id from previously retreived blob * @param {string} key - Blob decryption key * @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 - 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 (err) return fn(err); - - fn (null, { - blob : blob, - }); + if (!url && this.infos[id]) { + url = this.infos[id].blobvault; + } + + if (!url) { + return callback(new Error('Blob vault URL is required')); + } + + 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 + * * @param {string} username * @param {string} password * @param {string} encryptSecret * @param {function} fn - Callback function */ -VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) { + +VaultClient.prototype.unlock = function(username, password, encryptSecret, callback) { var self = this; - - self.authInfo.get(self.domain, username, function(err, authInfo){ - if (err) return fn(err); - + + function deriveUnlockKey(authInfo, callback) { //derive unlock key - crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){ - if (err) return fn(err); - - fn(null, { - keys : keys, - secret : crypt.decrypt(keys.unlock, encryptSecret) - }); - }); + crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) { + if (err) { + return callback(err); + } + + 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 * the username and password + * * @param {string} username * @param {string} password * @param {function} fn - Callback function */ -VaultClient.prototype.loginAndUnlock = function(username, password, fn) { + +VaultClient.prototype.loginAndUnlock = function(username, password, callback) { var self = this; - - this.login(username, password, function(err, resp){ - 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")); - + + function deriveUnlockKey(authInfo, blob, callback) { //derive unlock key - crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){ - if (err) return fn(err); - - fn(null, { - blob : resp.blob, - unlock : keys.unlock, - secret : crypt.decrypt(keys.unlock, resp.blob.encrypted_secret), - username : authInfo.username, - verified : authInfo.emailVerified + crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) { + if (err) { + return callback(err); + } + + callback(null, { + blob: blob, + unlock: keys.unlock, + 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 + * * @param {string} username * @param {function} fn - Callback function */ -VaultClient.prototype.exists = function (username, fn) { - this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo){ - if (err) return fn(err); - return fn(null, !!authInfo.exists); + +VaultClient.prototype.exists = function(username, callback) { + this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { + if (err) { + callback(err); + } else { + callback(null, !!authInfo.exists); + } }); -} +}; - -/* +/** * Verify an email address for an existing user + * * @param {string} username * @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) { - return fn(new Error("No blobvault specified in the authinfo.")); +VaultClient.prototype.verify = function(username, token, callback) { + 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 - * + * * @param {object} options * @param {string} options.username * @param {string} options.password @@ -233,53 +275,77 @@ VaultClient.prototype.verify = function (username, token, fn) { * @param {object} options.oldUserBlob //optional * @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){ - - if (err) return fn(err); - - if ("string" !== typeof authInfo.blobvault) { - return fn(new Error("No blobvault specified in the authinfo.")); - } - - - if (!authInfo.pakdf) { - return fn(new Error("No settings for PAKDF in auth packet.")); - } - - //derive login keys - crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, loginKeys){ - if (err) return fn(err); - - //derive unlock key - crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, unlockKeys){ - if (err) return fn(err); - - 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); - }); - }); +VaultClient.prototype.register = function(options, fn) { + var self = this; + var username = String(options.username).trim(); + var password = String(options.password).trim(); + + function getAuthInfo(callback) { + self.authInfo.get(self.domain, username, function(err, authInfo) { + if (err) { + return callback(err); + } + + if (typeof authInfo.blobvault !== 'string') { + return callback(new Error('No blobvault specified in the authinfo.')); + } + + if (!authInfo.pakdf) { + return callback(new Error('No settings for PAKDF in auth packet.')); + } + + callback(null, authInfo); }); - }); + }; + + 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); }; - -module.exports = VaultClient; +exports.VaultClient = VaultClient; diff --git a/test/vault-test.js b/test/vault-test.js index e2c97f03..77f49bca 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -1,7 +1,7 @@ var assert = require('assert'), - RippleTxt = require('../src/js/ripple/rippletxt'), - AuthInfo = require('../src/js/ripple/authinfo'), - VaultClient = require('../src/js/ripple/vaultclient'), + RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt, + AuthInfo = require('../src/js/ripple/authinfo').AuthInfo, + VaultClient = require('../src/js/ripple/vaultclient').VaultClient, Blob = require('../src/js/ripple/blob').Blob, 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() { it('should get the context of a ripple.txt file from a given domain', function(done){ + this.timeout(10000); var rt = new RippleTxt(); rt.get(exampleData.domain, function(err, resp){ assert.ifError(err); @@ -36,6 +37,7 @@ describe('AuthInfo', function() { var auth = new AuthInfo(); it ('should', function(done){ + this.timeout(10000); auth.get(exampleData.domain, exampleData.user, function(err, resp){ assert.ifError(err); assert.equal(typeof resp, 'object');