From 7e4ae26b8fdee4136464bb3abd2585d6cfca2e53 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Mon, 19 May 2014 15:25:53 -0700 Subject: [PATCH 01/10] [CHORE] include superagent, vault-test --- package.json | 3 ++- test/vault-test.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 test/vault-test.js diff --git a/package.json b/package.json index c9ff74a4..edfab422 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "async": "~0.8.0", "ws": "~0.4.31", "extend": "~1.2.1", - "lru-cache": "~2.5.0" + "lru-cache": "~2.5.0", + "superagent": "^0.18.0" }, "devDependencies": { "mocha": "~1.14.0", diff --git a/test/vault-test.js b/test/vault-test.js new file mode 100644 index 00000000..06c09ef9 --- /dev/null +++ b/test/vault-test.js @@ -0,0 +1 @@ +var assert = require('assert'); \ No newline at end of file From 7fe530e82c0ae1dc352eb3e54d9e87f45f893bb4 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Mon, 19 May 2014 16:00:53 -0700 Subject: [PATCH 02/10] [FEATURE] ripple.txt client --- src/js/ripple/index.js | 1 + src/js/ripple/rippletxt.js | 72 ++++++++++++++++++++++++++++++++++++++ test/vault-test.js | 19 +++++++++- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/js/ripple/rippletxt.js diff --git a/src/js/ripple/index.js b/src/js/ripple/index.js index f09d74c0..7dfc7b80 100644 --- a/src/js/ripple/index.js +++ b/src/js/ripple/index.js @@ -12,6 +12,7 @@ exports.Meta = require('./meta').Meta; exports.SerializedObject = require('./serializedobject').SerializedObject; exports.RippleError = require('./rippleerror').RippleError; exports.Message = require('./message'); +exports.Rippletxt = require('./rippletxt'); exports.binformat = require('./binformat'); exports.utils = require('./utils'); exports.Server = require('./server').Server; diff --git a/src/js/ripple/rippletxt.js b/src/js/ripple/rippletxt.js new file mode 100644 index 00000000..46788bbd --- /dev/null +++ b/src/js/ripple/rippletxt.js @@ -0,0 +1,72 @@ +var request = require('superagent'); + + +function RippleTxt(opts) { + this.txts = {}; +}; + + +/* + * Gets the ripple.txt file for the given domain + * + */ +RippleTxt.prototype.get = function (domain, fn) { + var self = this; + + if (self.txts[domain]) return fn(null, self.txts[domain]); + + var urls = [ + 'https://ripple.'+domain+'/ripple.txt', + 'https://www.'+domain+'/ripple.txt', + 'https://'+domain+'/ripple.txt', + '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(); + + 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); + }); + } +} + + +/* + * parse a ripple.txt file + * + */ +RippleTxt.prototype.parse = function (txt) { + + txt = txt.replace('\r\n', '\n'); + txt = txt.replace('\r', '\n'); + txt = txt.split('\n'); + + 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] === ']') { + currentSection = line.slice(1, line.length-1); + sections[currentSection] = []; + } else { + line = line.replace(/^\s+|\s+$/g, ''); + if (sections[currentSection]) { + sections[currentSection].push(line); + } + } + } + + return sections; +} + +module.exports = RippleTxt; \ No newline at end of file diff --git a/test/vault-test.js b/test/vault-test.js index 06c09ef9..9e23bf3a 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -1 +1,18 @@ -var assert = require('assert'); \ No newline at end of file +var assert = require('assert'); +var RippleTxt = require('../src/js/ripple/rippletxt'); + + +describe('Vault Client', function() { + + describe('Ripple Txt', function() { + + it('should get the context of a ripple.txt file from a given domain', function(done){ + var rt = new RippleTxt(); + rt.get("ripple.com", function(err, resp){ + assert.ifError(err); + assert.equal(typeof resp, 'object'); + done(); + }); + }); + }); +}); From 7ea1ba168da14cabd2d3ad9c7f7f2b83ee7527ef Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Mon, 19 May 2014 16:38:51 -0700 Subject: [PATCH 03/10] [FEATURE] authinfo module --- src/js/ripple/authinfo.js | 32 ++++++++++++++++++++++++++++++++ test/vault-test.js | 14 ++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/js/ripple/authinfo.js diff --git a/src/js/ripple/authinfo.js b/src/js/ripple/authinfo.js new file mode 100644 index 00000000..61c689bf --- /dev/null +++ b/src/js/ripple/authinfo.js @@ -0,0 +1,32 @@ +var RippleTxt = require('./rippletxt'); +var request = require('superagent'); + +function AuthInfo () { + this.rippleTxt = new RippleTxt; +} + +//Can I cache the auth info for later use? +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) return fn(new Error("Authentication info server unreachable")); + fn(null, resp.body); + }); + } +} + +module.exports = AuthInfo; \ No newline at end of file diff --git a/test/vault-test.js b/test/vault-test.js index 9e23bf3a..f05aed40 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -1,6 +1,8 @@ var assert = require('assert'); var RippleTxt = require('../src/js/ripple/rippletxt'); +var AuthInfo = require('../src/js/ripple/authinfo'); +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed certs describe('Vault Client', function() { @@ -15,4 +17,16 @@ describe('Vault Client', function() { }); }); }); + + + describe('AuthInfo', function() { + var auth = new AuthInfo(); + it ('should', function(done){ + auth.get("staging.ripple.com", "testUser", function(err, resp){ + assert.ifError(err); + assert.equal(typeof resp, 'object'); + done(); + }); + }); + }); }); From 1693a57845623f6edec6c7b2c750ee3532b37a21 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Mon, 19 May 2014 16:50:02 -0700 Subject: [PATCH 04/10] [CHORE] update comments --- src/js/ripple/authinfo.js | 7 ++++++- src/js/ripple/rippletxt.js | 13 +++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/js/ripple/authinfo.js b/src/js/ripple/authinfo.js index 61c689bf..423eeb06 100644 --- a/src/js/ripple/authinfo.js +++ b/src/js/ripple/authinfo.js @@ -5,7 +5,12 @@ function AuthInfo () { this.rippleTxt = new RippleTxt; } -//Can I cache the auth info for later use? +/** + * 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; diff --git a/src/js/ripple/rippletxt.js b/src/js/ripple/rippletxt.js index 46788bbd..c8b4af15 100644 --- a/src/js/ripple/rippletxt.js +++ b/src/js/ripple/rippletxt.js @@ -1,14 +1,15 @@ var request = require('superagent'); -function RippleTxt(opts) { +function RippleTxt() { 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; @@ -40,9 +41,9 @@ RippleTxt.prototype.get = function (domain, fn) { } -/* - * parse a ripple.txt file - * +/** + * Parse a ripple.txt file + * @param {string} txt - Unparsed ripple.txt data */ RippleTxt.prototype.parse = function (txt) { From b26129db72fbeaa8b43c9ed670d4fb15f1c890a5 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Tue, 20 May 2014 13:01:51 -0700 Subject: [PATCH 05/10] [FEATURE] vault, blob, and crypt module --- src/js/ripple/authinfo.js | 3 +- src/js/ripple/blob.js | 560 +++++++++++++++++++++++++++++++++++ src/js/ripple/crypt.js | 356 ++++++++++++++++++++++ src/js/ripple/index.js | 2 +- src/js/ripple/vaultclient.js | 269 +++++++++++++++++ test/vault-test.js | 239 ++++++++++++++- 6 files changed, 1411 insertions(+), 18 deletions(-) create mode 100644 src/js/ripple/blob.js create mode 100644 src/js/ripple/crypt.js create mode 100644 src/js/ripple/vaultclient.js diff --git a/src/js/ripple/authinfo.js b/src/js/ripple/authinfo.js index 423eeb06..785663ff 100644 --- a/src/js/ripple/authinfo.js +++ b/src/js/ripple/authinfo.js @@ -22,13 +22,12 @@ AuthInfo.prototype.get = function (domain, username, fn) { 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) return fn(new Error("Authentication info server unreachable")); + if (err || resp.error) return fn(new Error("Authentication info server unreachable")); fn(null, resp.body); }); } diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js new file mode 100644 index 00000000..a4cc7b1c --- /dev/null +++ b/src/js/ripple/blob.js @@ -0,0 +1,560 @@ +var crypt = require('./crypt'), + message = require('./message'), + request = require('superagent'), + extend = require("extend"); + +//Blob object class +var BlobObj = function (url, id, key) { + this.url = url; + this.id = id; + this.key = key; + this.data = {}; +}; + +// Blob operations +// Do NOT change the mapping of existing ops +BlobObj.ops = { + // Special + "noop" : 0, + + // Simple ops + "set" : 16, + "unset" : 17, + "extend" : 18, + + // Meta ops + "push" : 32, + "pop" : 33, + "shift" : 34, + "unshift" : 35, + "filter" : 36 +}; + + +BlobObj.opsReverseMap = []; +for (var name in BlobObj.ops) { + BlobObj.opsReverseMap[BlobObj.ops[name]] = name; +} + + +/* + * Init - + * initialize a new blob object + * + */ +BlobObj.prototype.init = function (fn) { + var self = this, 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")); + } + + //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(); + } + + fn(null, self);//return with newly decrypted blob + + }).timeout(8000); +} + + +/* + * Consolidate - + * Consolidate patches as a new revision + * + */ +BlobObj.prototype.consolidate = function (fn) { + + // Callback is optional + if ("function" !== typeof fn) 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 + }, + }; + + 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")); + }); +}; + + +/* + * ApplyEncryptedPatch - + * save changes from a downloaded patch to the blob + * + */ +BlobObj.prototype.applyEncryptedPatch = function (patch) +{ + try { + var params = JSON.parse(crypt.decrypt(this.key, patch)); + var op = params.shift(); + var path = params.shift(); + + this.applyUpdate(op, path, params); + this.revision++; + + return true; + + } catch (err) { + console.log("client: blob: failed to apply patch:", err.toString()); + console.log(err.stack); + return false; + } +} + + +//decrypt secret with unlock key +BlobObj.prototype.decryptSecret = function (secretUnlockKey) { + return crypt.decrypt(secretUnlockKey, this.data.encrypted_secret); +}; + + +//encrypt secret with unlock key +BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) { + return crypt.encrypt(secretUnlockKey, secret); +}; + + +//decrypt blob with crypt key +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); + return false; + } +}; + + +//encrypt blob data with crypt key +BlobObj.prototype.encrypt = function() +{ + +// Filter Angular metadata before encryption +// if ('object' === typeof this.data && +// 'object' === typeof this.data.contacts) +// this.data.contacts = angular.fromJson(angular.toJson(this.data.contacts)); + + return crypt.encrypt(this.key, JSON.stringify(this.data)); +}; + +BlobObj.prototype.decryptBlobCrypt = function (secret) { + var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); + return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); +}; + +BlobObj.prototype.encryptBlobCrypt = function (secret, blobDecryptKey) { + var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); + return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey); +}; + + + +/**** Blob updating ****/ + + +//set blob element +BlobObj.prototype.set = function (pointer, value, fn) { + this.applyUpdate('set', pointer, [value]); + this.postUpdate('set', pointer, [value], fn); +}; + + +//get remove blob element +BlobObj.prototype.unset = function (pointer, fn) { + this.applyUpdate('unset', pointer, []); + this.postUpdate('unset', pointer, [], fn); +}; + + +//extend blob element +BlobObj.prototype.extend = function (pointer, value, fn) { + this.applyUpdate('extend', pointer, [value]); + this.postUpdate('extend', pointer, [value], fn); +}; + + +//Prepend an entry to an array. +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. + * + * This method will find any entries from the array stored under `pointer` and + * apply the `subcommands` to each of them. + * + * The subcommands can be any commands with the pointer parameter left out. + */ +BlobObj.prototype.filter = function (pointer, field, value, subcommands, callback) { + var params = Array.prototype.slice.apply(arguments); + if ("function" === typeof params[params.length-1]) { + 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 new update to the blob data +BlobObj.prototype.applyUpdate = function (op, path, params) { + + // Exchange from numeric op code to string + if ("number" === typeof 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"); + } + + // Separate each step in the "pointer" + var pointer = path.split("/"); + + var first = pointer.shift(); + 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) { + var _this = this; + var part = _this.unescapeToken(pointer.shift()); + + if (Array.isArray(context)) { + 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 '-'"); + } + } else if ("object" !== typeof context) { + return null; + } else if (!context.hasOwnProperty(part)) { + // Some opcodes create the path as they're going along + if (op === "set") { + context[part] = {}; + } else if (op === "unshift") { + context[part] = []; + } else { + return null; + } + } + + if (pointer.length !== 0) { + 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)); + + 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.unescapeToken = function(str) { + return str.replace(/~./g, function(m) { + switch (m) { + case "~0": + return "~"; + case "~1": + return "/"; + } + throw("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) { + 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"); + } + + 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)) + } + }; + + + 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); + }); +}; + + + +/***** helper functions *****/ + + +function normalizeSubcommands(subcommands, compress) { + // Normalize parameter structure + if ("number" === typeof subcommands[0] || + "string" === 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])) { + // Case 2: Single subcommand as array + // (nothing to do) + } else if (Array.isArray(subcommands[0])) { + // Case 3: Multiple subcommands as array of arrays + subcommands = subcommands[0]; + } + + // Normalize op name and convert strings to numeric codes + subcommands = subcommands.map(function (subcommand) { + if ("string" === typeof subcommand[0]) { + subcommand[0] = BlobObj.ops[subcommand[0]]; + } + if ("number" !== typeof subcommand[0]) { + throw new Error("Invalid op in subcommand"); + } + if ("string" !== typeof subcommand[1]) { + throw new Error("Invalid path in subcommand"); + } + return subcommand; + }); + + if (compress) { + // Convert to the minimal possible format + if (subcommands.length === 1) { + return subcommands[0]; + } else { + return [subcommands]; + } + } else { + return subcommands; + } +} + + +/***** blob client methods ****/ + + +//blob object class +module.exports.Blob = BlobObj + +//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")); + 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")); + }); +} + +//retrive a blob with url, id and key +module.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) { + 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")); + }); +} + + +/** + * Create a blob object + * + * @param {object} options + * @param {string} options.url + * @param {string} options.id + * @param {string} options.crypt + * @param {string} options.unlock + * @param {string} options.username + * @param {string} options.masterkey + * @param {object} options.oldUserBlob + * @param {function} fn + */ +module.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.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 + } + }; + + + 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 diff --git a/src/js/ripple/crypt.js b/src/js/ripple/crypt.js new file mode 100644 index 00000000..e747b822 --- /dev/null +++ b/src/js/ripple/crypt.js @@ -0,0 +1,356 @@ +var sjcl = require('./utils').sjcl, + request = require('superagent'), + extend = require("extend"), + parser = require("url"); + +var cryptConfig = { + cipher : "aes", + mode : "ccm", + ts : 64, // tag length + ks : 256, // key size + iter : 1000 // iterations (key derivation) +}; + +var Crypt = {}; + +// Full domain hash based on SHA512 +function fdh(data, bytelen) +{ + var bitlen = bytelen << 3; + + 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); + counter++; + } + + // Truncate to desired length + 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). +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). + */ +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)); + + 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); + + 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) + break; + } + + var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus), + iSignreq = iSecret.mulmod(iBlind, iModulus), + 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); + }); + + fn (null, result); + }); +} + + +Crypt.encrypt = function(key, data) +{ + key = sjcl.codec.hex.toBits(key); + + var opts = extend(true, {}, cryptConfig); + + var encryptedObj = JSON.parse(sjcl.encrypt(key, data, opts)); + var version = [sjcl.bitArray.partial(8, 0)]; + var initVector = sjcl.codec.base64.toBits(encryptedObj.iv); + var ciphertext = sjcl.codec.base64.toBits(encryptedObj.ct); + + var encryptedBits = sjcl.bitArray.concat(version, initVector); + encryptedBits = sjcl.bitArray.concat(encryptedBits, ciphertext); + + return sjcl.codec.base64.fromBits(encryptedBits); +} + + +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); + } + + var encrypted = extend(true, {}, cryptConfig, { + iv: sjcl.codec.base64.fromBits(sjcl.bitArray.bitSlice(encryptedBits, 8, 8+128)), + ct: sjcl.codec.base64.fromBits(sjcl.bitArray.bitSlice(encryptedBits, 8+128)) + }); + + return sjcl.decrypt(key, JSON.stringify(encrypted)); +} + +Crypt.isValidAddress = function (address) { + return ripple.UInt160.is_valid(address); +} + +Crypt.createSecret = function (words) { + return sjcl.codec.hex.fromBits(sjcl.random.randomWords(words)); +} + +Crypt.createMaster = function () { + return ripple.Base.encode_base_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4))); +} + +/* +Crypt.getAddress = function (masterkey) { + return new RippleAddress(masterkey).getAddress(); +} +*/ + +Crypt.hashSha512 = function (data) { + return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data)); +} + +Crypt.signature = 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)); +} + +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"); + 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) { + return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, ''); +}; + +/** + * Convert base64url encoded data into base64 encoded data. + * + * @param {String} base64 Data + */ +Crypt.base64UrlToBase64 = function (encodedData) { + encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/'); + while (encodedData.length % 4) { + encodedData += '='; + } + return encodedData; +}; + + +//methods for signed requests +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)); + + // Canonical request using Amazon's v4 signature format + // See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + var canonicalRequest = [ + config.method || 'GET', + parsed.pathname || '', + parsed.search || '', + // XXX Headers signing not supported + '', + '', + Crypt.hashSha512(canonicalData).toLowerCase() + ].join('\n'); + + // String to sign inspired by Amazon's v4 signature format + // See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html + // + // We don't have a credential scope, so we skip it. + // + // But that modifies the format, so the format ID is RIPPLE1, instead of AWS4. + return stringToSign = [ + mechanism, + date, + Crypt.hashSha512(canonicalRequest).toLowerCase() + ].join('\n'); +} + +Crypt.signRequestHmac = function (config, auth_secret, blob_id) { + config = extend(true, {}, config); + + // Parse URL + var parsed = parser.parse(config.url); + + var date = dateAsIso8601(); + var signatureType = 'RIPPLE1-HMAC-SHA512'; + + var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); + var signature = Crypt.signature(auth_secret, stringToSign); + + config.url += (parsed.search ? "&" : "?") + + 'signature='+Crypt.base64ToBase64Url(signature)+ + '&signature_date='+date+ + '&signature_blob_id='+blob_id+ + '&signature_type='+signatureType + + return config; +}; + + +Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) { + config = extend(true, {}, config); + + // Parse URL + var parsed = parser.parse(config.url); + + var date = dateAsIso8601(); + 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; + + return config; +}; + + +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) + 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) + 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 ); +}; + + +var dateAsIso8601 = (function () { + function pad(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"; + }; +})(); + + +module.exports = Crypt; \ No newline at end of file diff --git a/src/js/ripple/index.js b/src/js/ripple/index.js index 7dfc7b80..1bb0585e 100644 --- a/src/js/ripple/index.js +++ b/src/js/ripple/index.js @@ -12,7 +12,7 @@ exports.Meta = require('./meta').Meta; exports.SerializedObject = require('./serializedobject').SerializedObject; exports.RippleError = require('./rippleerror').RippleError; exports.Message = require('./message'); -exports.Rippletxt = require('./rippletxt'); +exports.VaultClient = require('./vaultclient'); exports.binformat = require('./binformat'); exports.utils = require('./utils'); exports.Server = require('./server').Server; diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js new file mode 100644 index 00000000..9e598f39 --- /dev/null +++ b/src/js/ripple/vaultclient.js @@ -0,0 +1,269 @@ +var AuthInfo = require('./authinfo'); +var blobClient = require('./blob'); +var crypt = require('./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 = {}; +}; + + +/* + * normalizeUsername - + * Reduce username to standardized form. + * Strips whitespace at beginning and end. + */ +VaultClient.prototype.normalizeUsername = function (username) { + username = ""+username; + username = username.trim(); + return username; +}; + + +/* + * normalizePassword - + * Reduce password to standardized form. + * Strips whitespace at beginning and end. + */ +VaultClient.prototype.normalizePassword = function (password) { + password = ""+password; + password = password.trim(); + return password; +}; + +VaultClient.prototype.getRippleName = function(address, url, fn) { + + //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.getRippleName(url, address, fn); +}; + +/* + * Login - + * authenticate and retrieve a decrypted blob using a ripple name and password + * + */ +VaultClient.prototype.login = function(username, password, fn) { + 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.")); + } + + if ("string" !== typeof authInfo.blobvault) { + return fn(new Error("No blobvault specified in the authinfo.")); + } + + + //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 + }); + }); + }); + }); +}; + + +/* + * Relogin - + * retreive and decrypt blob using a blob url, id and crypt derived previously. + * + */ +VaultClient.prototype.relogin = function(url, id, cryptKey, fn) { + + //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, cryptKey, function (err, blob) { + if (err) return fn(err); + + fn (null, { + blob : blob, + }); + }); +}; + + +/* + * Unlock - + * decrypt the secret key using a username and password + */ +VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) { + var self = this; + + self.authInfo.get(self.domain, username, function(err, authInfo){ + if (err) return fn(err); + + //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) + }); + }); + }); +}; + + +/* + * LoginAndUnlock + * retrieve the decrypted blob and secret key in one step using + * the username and password + * + */ +VaultClient.prototype.loginAndUnlock = function(username, password, fn) { + 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")); + + //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 + }); + }); + }); +}; + + +/* + * Exists - + * check blobvault for existance of username + * + */ +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); + }); +} + + +/* + * Verify - + * verify an email address for an existing user + * + */ +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.")); + } + + blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, fn); + }); +} + +/* + * Register - + * register a new user and save to the blob vault + * + * @param {object} options + * @param {string} options.username + * @param {string} options.password + * @param {string} options.masterkey //optional, will create if absent + * @param {string} options.email + * @param {string} options.activateLink + * @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); + }); + }); + }); + }); +}; + +module.exports = VaultClient; \ No newline at end of file diff --git a/test/vault-test.js b/test/vault-test.js index f05aed40..45119573 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -1,32 +1,241 @@ -var assert = require('assert'); -var RippleTxt = require('../src/js/ripple/rippletxt'); -var AuthInfo = require('../src/js/ripple/authinfo'); +var assert = require('assert'), + RippleTxt = require('../src/js/ripple/rippletxt'), + AuthInfo = require('../src/js/ripple/authinfo'), + VaultClient = require('../src/js/ripple/vaultclient'), + Blob = require('../src/js/ripple/blob').Blob, + UInt256 = require('../src/js/ripple/uint256').UInt256; + +var exampleData = { + id : "57d6ed12d3b98ca91b61afac2fb30212f642daabefd9c7cda623f145f384830c", + crypt : "1733480ceea2970e5f979c8d8e508d79e446b42d54f593e640814ea91deb53ef", + unlock : "452b02b80469a6a2ad692264c04d2a3794ea0ab11d8c902ef774190294db2ce2", + blobURL : "https://id.staging.ripple.com", + username : "testUser", + password : "pass word", + domain : "staging.ripple.com", + encrypted_secret : "QUh5dnBqR0pTTVpjcjVoY0FhN1cxcEdTdW1XS1hLS2VzNlpQT2ZvQkFJWmg1UHRYS1RobUhKTkZUcWNyNlZEVlZYZDNhS1l0" +}; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed certs -describe('Vault Client', function() { - describe('Ripple Txt', function() { - it('should get the context of a ripple.txt file from a given domain', function(done){ - var rt = new RippleTxt(); - rt.get("ripple.com", function(err, resp){ +describe('Ripple Txt', function() { + it('should get the context of a ripple.txt file from a given domain', function(done){ + var rt = new RippleTxt(); + rt.get(exampleData.domain, function(err, resp){ + assert.ifError(err); + assert.equal(typeof resp, 'object'); + done(); + }); + }); +}); + + +describe('AuthInfo', function() { + var auth = new AuthInfo(); + + it ('should', function(done){ + auth.get(exampleData.domain, exampleData.user, function(err, resp){ + assert.ifError(err); + assert.equal(typeof resp, 'object'); + done(); + }); + }); +}); + +describe('VaultClient', function() { + var client = new VaultClient(exampleData.domain); + + describe('initialization', function() { + it('should be initialized with a domain', function() { + var client = new VaultClient({ domain: exampleData.domain }); + assert.strictEqual(client.domain, exampleData.domain); + }); + + it('should default to ripple.com without a domain', function() { + var client = new VaultClient(); + assert.strictEqual(client.domain, 'ripple.com'); + }); + }); + + describe('#exists', function() { + it('should determine if a username exists on the domain', function(done) { + this.timeout(10000); + client.exists(exampleData.username, function(err, resp) { + + assert.ifError(err); + assert.equal(typeof resp, 'boolean'); + done(); + }); + }); + }); + + describe('#login', function() { + it('with username and password should retrive the blob, crypt key, and id', function(done) { + this.timeout(10000); + client.login(exampleData.username, exampleData.password, function(err, resp) { + assert.ifError(err); assert.equal(typeof resp, 'object'); + assert(resp.blob instanceof Blob); + assert.equal(typeof resp.blob.id, 'string'); + assert(UInt256.from_json(resp.blob.id).is_valid(), true); + assert.equal(typeof resp.blob.key, 'string'); + assert(UInt256.from_json(resp.blob.key).is_valid(), true); + assert.equal(typeof resp.username, 'string'); + assert.equal(typeof resp.verified, 'boolean'); done(); }); }); }); - describe('AuthInfo', function() { - var auth = new AuthInfo(); - it ('should', function(done){ - auth.get("staging.ripple.com", "testUser", function(err, resp){ + describe('#relogin', function() { + it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) { + this.timeout(10000); + client.relogin(exampleData.blobURL, exampleData.id, exampleData.crypt, function(err, resp) { + assert.ifError(err); assert.equal(typeof resp, 'object'); + assert(resp.blob instanceof Blob); done(); - }); - }); + }); + }); }); -}); + + describe('#unlock', function() { + it('should access the wallet secret using encryption secret, username and password', function(done) { + this.timeout(10000); + client.unlock(exampleData.username, exampleData.password, exampleData.encrypted_secret, function(err, resp) { + + assert.ifError(err); + assert.equal(typeof resp, 'object'); + assert.equal(typeof resp.keys, 'object'); + assert.equal(typeof resp.keys.unlock, 'string'); + assert(UInt256.from_json(resp.keys.unlock).is_valid(), true); + done(); + }); + }); + }); + + describe('#loginAndUnlock', function() { + it('should get the decrypted blob and decrypted secret given name and password', function(done) { + this.timeout(10000); + client.loginAndUnlock(exampleData.username, exampleData.password, function(err, resp) { + + assert.ifError(err); + assert.equal(typeof resp, 'object'); + assert(resp.blob instanceof Blob); + assert.equal(typeof resp.blob.id, 'string'); + assert(UInt256.from_json(resp.blob.id).is_valid(), true); + assert.equal(typeof resp.blob.key, 'string'); + assert(UInt256.from_json(resp.blob.key).is_valid(), true); + assert.equal(typeof resp.unlock, 'string'); + assert(UInt256.from_json(resp.unlock).is_valid(), true); + assert.equal(typeof resp.secret, 'string'); + assert.equal(typeof resp.username, 'string'); + assert.equal(typeof resp.verified, 'boolean'); + done(); + }); + }); + }); +}); + + +describe('Blob', function() { + var vaultClient; + + vaultClient = new VaultClient({ domain: exampleData.domain }); + vaultClient.login(exampleData.username, exampleData.password, function(err,resp){ + assert.ifError(err); + var blob = resp.blob; + + + describe('#set', function() { + it('should set a new property in the blob', function(done) { + this.timeout(10000) + + blob.extend("/testObject", { + foo : [], + }, function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#extend', function() { + it('should extend an object in the blob', function(done) { + this.timeout(10000) + + blob.extend("/testObject", { + foobar : "baz", + }, function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#unset', function() { + it('should remove a property from the blob', function(done) { + this.timeout(10000) + + blob.unset("/testObject", function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#unshift', function() { + it('should prepend an item to an array in the blob', function(done) { + this.timeout(10000) + + blob.unshift("/testArray", { + name : "bob", + address : "1234" + }, function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#filter', function() { + it('should find a specific entity in an array and apply subcommands to it', function(done) { + this.timeout(10000) + + blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:"Alice"}, function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + + describe('#consolidate', function() { + it('should consolidate and save changes to the blob', function(done) { + this.timeout(10000) + + blob.unset('/testArray', function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + + blob.consolidate(function(err, resp){ + assert.ifError(err); + assert.equal(resp.result, 'success'); + done(); + }); + }); + }); + }); + }); +}); + From 5ba7c31e4c47232e973723dbc8f52fe72588ab77 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Tue, 20 May 2014 13:40:05 -0700 Subject: [PATCH 06/10] [DOC] vault client comments --- src/js/ripple/vaultclient.js | 80 +++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js index 9e598f39..a02ed7be 100644 --- a/src/js/ripple/vaultclient.js +++ b/src/js/ripple/vaultclient.js @@ -2,6 +2,7 @@ var AuthInfo = require('./authinfo'); var blobClient = require('./blob'); var crypt = require('./crypt'); + function VaultClient(opts) { if (!opts) opts = {}; else if (typeof opts === "string") opts = {domain:opts}; @@ -12,10 +13,10 @@ function VaultClient(opts) { }; -/* - * normalizeUsername - +/** * Reduce username to standardized form. * Strips whitespace at beginning and end. + * @param {string} username - Username to normalize */ VaultClient.prototype.normalizeUsername = function (username) { username = ""+username; @@ -24,10 +25,10 @@ VaultClient.prototype.normalizeUsername = function (username) { }; -/* - * normalizePassword - +/** * Reduce password to standardized form. * Strips whitespace at beginning and end. + * @param {string} password - password to normalize */ VaultClient.prototype.normalizePassword = function (password) { password = ""+password; @@ -35,18 +36,25 @@ VaultClient.prototype.normalizePassword = function (password) { 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) { //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.getRippleName(url, address, fn); }; -/* - * Login - - * 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} password + * @param {function} fn - Callback function */ VaultClient.prototype.login = function(username, password, fn) { var self = this; @@ -91,19 +99,21 @@ VaultClient.prototype.login = function(username, password, fn) { }; -/* - * Relogin - - * 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} id - Blob id from previously retreived blob + * @param {string} key - Blob decryption key + * @param {function} fn - Callback function */ -VaultClient.prototype.relogin = function(url, id, cryptKey, fn) { +VaultClient.prototype.relogin = function(url, id, key, fn) { //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, cryptKey, function (err, blob) { + blobClient.get(url, id, key, function (err, blob) { if (err) return fn(err); fn (null, { @@ -113,9 +123,12 @@ VaultClient.prototype.relogin = function(url, id, cryptKey, fn) { }; -/* - * Unlock - - * decrypt the secret key using a username and password +/** + * 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) { var self = this; @@ -136,11 +149,12 @@ VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) { }; -/* - * LoginAndUnlock - * 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 - * + * @param {string} username + * @param {string} password + * @param {function} fn - Callback function */ VaultClient.prototype.loginAndUnlock = function(username, password, fn) { var self = this; @@ -174,10 +188,10 @@ VaultClient.prototype.loginAndUnlock = function(username, password, fn) { }; -/* - * Exists - - * check blobvault for existance of username - * +/** + * 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){ @@ -188,9 +202,10 @@ VaultClient.prototype.exists = function (username, fn) { /* - * Verify - - * verify an email address for an existing user - * + * Verify an email address for an existing user + * @param {string} username + * @param {string} token - Verification token + * @param {function} fn - Callback function */ VaultClient.prototype.verify = function (username, token, fn) { @@ -205,9 +220,9 @@ VaultClient.prototype.verify = function (username, token, fn) { }); } + /* - * Register - - * register a new user and save to the blob vault + * Register a new user and save to the blob vault * * @param {object} options * @param {string} options.username @@ -266,4 +281,5 @@ VaultClient.prototype.register = function (options, fn) { }); }; -module.exports = VaultClient; \ No newline at end of file + +module.exports = VaultClient; From beffd0864aa59a1fe3807eea06ed84089e7faa77 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Tue, 20 May 2014 15:10:16 -0700 Subject: [PATCH 07/10] [FEATURE] register blob --- src/js/ripple/blob.js | 3 ++- src/js/ripple/crypt.js | 61 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index a4cc7b1c..56778cee 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -1,5 +1,4 @@ var crypt = require('./crypt'), - message = require('./message'), request = require('superagent'), extend = require("extend"); @@ -522,6 +521,8 @@ module.exports.create = function (options, fn) created : (new Date()).toJSON() }; + console.log(options.masterkey, crypt.getAddress(options.masterkey)); + blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey); // Migration diff --git a/src/js/ripple/crypt.js b/src/js/ripple/crypt.js index e747b822..ba046ab4 100644 --- a/src/js/ripple/crypt.js +++ b/src/js/ripple/crypt.js @@ -1,4 +1,6 @@ var sjcl = require('./utils').sjcl, + base = require('./base').Base, + message = require('./message'), request = require('superagent'), extend = require("extend"), parser = require("url"); @@ -43,7 +45,8 @@ 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 ******/ @@ -121,6 +124,54 @@ Crypt.derive = function (opts, purpose, username, secret, fn) { } +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) { + this.seed = base.decode_check(33, seed); + + if (!this.seed) { + throw "Invalid seed." + } + + 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++; + } while (!sjcl.ecc.curves.c256.r.greaterEquals(private_gen)); + + public_gen = sjcl.ecc.curves.c256.G.mult(private_gen); + + var sec; + i = 0; + do { + sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i))); + i++; + } while (!sjcl.ecc.curves.c256.r.greaterEquals(sec)); + + var pubKey = sjcl.ecc.curves.c256.G.mult(sec).toJac().add(public_gen).toAffine(); + + return base.encode_check(0, sjcl.codec.bytes.fromBits(SHA256_RIPEMD160(sjcl.codec.bytes.toBits(pubKey.toBytesCompressed())))); + }; + }; +})(); + Crypt.encrypt = function(key, data) { key = sjcl.codec.hex.toBits(key); @@ -167,14 +218,14 @@ Crypt.createSecret = function (words) { } Crypt.createMaster = function () { - return ripple.Base.encode_base_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4))); + return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4))); } -/* + Crypt.getAddress = function (masterkey) { - return new RippleAddress(masterkey).getAddress(); + return new Crypt.RippleAddress(masterkey).getAddress(); } -*/ + Crypt.hashSha512 = function (data) { return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data)); From 55184162d161e4ec2c72784d167c10354cc4c777 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Tue, 20 May 2014 15:40:27 -0700 Subject: [PATCH 08/10] [DOC] crypt comments --- src/js/ripple/crypt.js | 115 +++++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 21 deletions(-) diff --git a/src/js/ripple/crypt.js b/src/js/ripple/crypt.js index ba046ab4..04d3f2d2 100644 --- a/src/js/ripple/crypt.js +++ b/src/js/ripple/crypt.js @@ -1,5 +1,6 @@ var sjcl = require('./utils').sjcl, base = require('./base').Base, + UInt160 = require('./uint160').UInt160, message = require('./message'), request = require('superagent'), extend = require("extend"), @@ -15,7 +16,9 @@ var cryptConfig = { var Crypt = {}; -// Full domain hash based on SHA512 +/** + * Full domain hash based on SHA512 + */ function fdh(data, bytelen) { var bitlen = bytelen << 3; @@ -38,9 +41,12 @@ function fdh(data, bytelen) return output; } - -// This is a function to derive different hashes from the same key. Each hash -// is derived as HMAC-SHA512HALF(key, token). +/** + * 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)); @@ -57,6 +63,11 @@ function keyHash(key, token) { * 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} secret - Also known as passphrase/password + * @param {function} fn */ Crypt.derive = function (opts, purpose, username, secret, fn) { @@ -123,7 +134,10 @@ Crypt.derive = function (opts, purpose, username, secret, fn) { }); } - +/** + * Imported from ripple-client + * + */ Crypt.RippleAddress = (function () { function append_int(a, i) { return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff) @@ -172,6 +186,11 @@ Crypt.RippleAddress = (function () { }; })(); +/** + * Encrypt data + * @params {string} key + * @params {string} data + */ Crypt.encrypt = function(key, data) { key = sjcl.codec.hex.toBits(key); @@ -190,6 +209,11 @@ Crypt.encrypt = function(key, data) } +/** + * Decrypt data + * @params {string} key + * @params {string} data + */ Crypt.decrypt = function(key, data) { key = sjcl.codec.hex.toBits(key); @@ -209,33 +233,66 @@ Crypt.decrypt = function(key, data) return sjcl.decrypt(key, JSON.stringify(encrypted)); } + +/** + * Validate a ripple address + * @param {string} address + */ Crypt.isValidAddress = function (address) { - return ripple.UInt160.is_valid(address); + return UInt160.is_valid(address); } -Crypt.createSecret = function (words) { - return sjcl.codec.hex.fromBits(sjcl.random.randomWords(words)); + +/** + * Validate a ripple address + * @param {integer} nWords - number of words + */ +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))); } +/** + * Create a ripple address from a master key + * @param {string} masterkey + */ 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.signature = function (secret, data) { + +/** + * Sign a data string with a secret key + * @param {string} secret + * @param {string} 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); @@ -246,7 +303,6 @@ Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) { /** * Convert base64 encoded data into base64url encoded data. - * * @param {String} base64 Data */ Crypt.base64ToBase64Url = function (encodedData) { @@ -255,7 +311,6 @@ Crypt.base64ToBase64Url = function (encodedData) { /** * Convert base64url encoded data into base64 encoded data. - * * @param {String} base64 Data */ Crypt.base64UrlToBase64 = function (encodedData) { @@ -267,7 +322,14 @@ Crypt.base64UrlToBase64 = function (encodedData) { }; -//methods for signed requests +/** + * 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) { // XXX This method doesn't handle signing GET requests correctly. The data // field will be merged into the search string, not the request body. @@ -300,17 +362,22 @@ Crypt.getStringToSign = function (config, parsed, date, mechanism) { ].join('\n'); } + +/** + * HMAC signed request + * @param {Object} config + * @param {Object} auth_secret + * @param {Object} blob_id + */ Crypt.signRequestHmac = function (config, auth_secret, blob_id) { config = extend(true, {}, config); // Parse URL - var parsed = parser.parse(config.url); - - var date = dateAsIso8601(); + var parsed = parser.parse(config.url); + var date = dateAsIso8601(); var signatureType = 'RIPPLE1-HMAC-SHA512'; - - var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); - var signature = Crypt.signature(auth_secret, stringToSign); + var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); + var signature = Crypt.signString(auth_secret, stringToSign); config.url += (parsed.search ? "&" : "?") + 'signature='+Crypt.base64ToBase64Url(signature)+ @@ -321,13 +388,18 @@ Crypt.signRequestHmac = function (config, auth_secret, blob_id) { 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) { config = extend(true, {}, config); // Parse URL - var parsed = parser.parse(config.url); - + var parsed = parser.parse(config.url); var date = dateAsIso8601(); var signatureType = 'RIPPLE1-ECDSA-SHA512'; var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); @@ -344,6 +416,7 @@ Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) { }; +//prepare for signing function copyObjectWithSortedKeys(object) { if (isPlainObject(object)) { var newObj = {}; From 396d7b07d87a5cc00fe81b94b9a1bbee9dc21075 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Tue, 20 May 2014 15:56:07 -0700 Subject: [PATCH 09/10] [DOC] blob comments --- src/js/ripple/blob.js | 106 ++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index 56778cee..b2b87c68 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -37,9 +37,8 @@ for (var name in BlobObj.ops) { /* - * Init - - * initialize a new blob object - * + * Initialize a new blob object + * @param {function} fn - Callback function */ BlobObj.prototype.init = function (fn) { var self = this, url; @@ -78,7 +77,7 @@ BlobObj.prototype.init = function (fn) { /* * Consolidate - * Consolidate patches as a new revision - * + * @param {function} fn - Callback function */ BlobObj.prototype.consolidate = function (fn) { @@ -116,7 +115,7 @@ BlobObj.prototype.consolidate = function (fn) { /* * ApplyEncryptedPatch - * save changes from a downloaded patch to the blob - * + * @param {string} patch - encrypted patch string */ BlobObj.prototype.applyEncryptedPatch = function (patch) { @@ -138,19 +137,27 @@ BlobObj.prototype.applyEncryptedPatch = function (patch) } -//decrypt secret with unlock key -BlobObj.prototype.decryptSecret = function (secretUnlockKey) { - return crypt.decrypt(secretUnlockKey, this.data.encrypted_secret); -}; - - -//encrypt secret with unlock key +/** + * Encrypt secret with unlock key + * @param {string} secretUnlockkey + */ BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) { return crypt.encrypt(secretUnlockKey, secret); }; + +/** + * Decrypt secret with unlock key + * @param {string} 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) { try { @@ -164,7 +171,9 @@ BlobObj.prototype.decrypt = function (data) { }; -//encrypt blob data with crypt key +/** + * Encrypt blob with crypt key + */ BlobObj.prototype.encrypt = function() { @@ -176,43 +185,62 @@ BlobObj.prototype.encrypt = function() return crypt.encrypt(this.key, JSON.stringify(this.data)); }; -BlobObj.prototype.decryptBlobCrypt = function (secret) { - var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); - return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); -}; +/** + * Encrypt recovery key + * @param {string} secret + * @param {string} 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) { + var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); + return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); +}; -/**** Blob updating ****/ -//set blob element +/**** Blob updating functions ****/ + + +/** + * Set blob element + */ BlobObj.prototype.set = function (pointer, value, fn) { this.applyUpdate('set', pointer, [value]); this.postUpdate('set', pointer, [value], fn); }; -//get remove blob element +/** + * Remove blob element + */ BlobObj.prototype.unset = function (pointer, fn) { this.applyUpdate('unset', pointer, []); this.postUpdate('unset', pointer, [], fn); }; -//extend blob element +/** + * Extend blob object + */ BlobObj.prototype.extend = function (pointer, value, fn) { this.applyUpdate('extend', pointer, [value]); this.postUpdate('extend', pointer, [value], fn); }; -//Prepend an entry to an array. +/** + * Prepend blob array + */ BlobObj.prototype.unshift = function (pointer, value, fn) { this.applyUpdate('unshift', pointer, [value]); this.postUpdate('unshift', pointer, [value], fn); @@ -242,7 +270,9 @@ BlobObj.prototype.filter = function (pointer, field, value, subcommands, callbac }; -//apply new update to the blob data +/** + * Apply udpdate to the blob + */ BlobObj.prototype.applyUpdate = function (op, path, params) { // Exchange from numeric op code to string @@ -363,7 +393,9 @@ BlobObj.prototype.unescapeToken = function(str) { }; -//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(){}; @@ -460,10 +492,14 @@ function normalizeSubcommands(subcommands, compress) { /***** blob client methods ****/ -//blob object class +/** + * Blob object class + */ module.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")); @@ -475,15 +511,19 @@ module.exports.getRippleName = function (url, address, fn) { }); } -//retrive a blob with url, id and key -module.exports.get = function (url, id, crypt, fn) { +/* + * Retrive a blob with url, id and key + */ +module.exports.get = function (url, id, crypt, fn) { var blob = new BlobObj(url, id, crypt); blob.init(fn); } -//verify email address +/* + * Verify email address + */ module.exports.verify = function (url, username, token, fn) { url += '/v1/user/' + username + '/verify/' + token; request.get(url, function(err, resp){ @@ -521,8 +561,6 @@ module.exports.create = function (options, fn) created : (new Date()).toJSON() }; - console.log(options.masterkey, crypt.getAddress(options.masterkey)); - blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey); // Migration @@ -546,11 +584,9 @@ module.exports.create = function (options, fn) 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) { From 81e0e2672dc852b0bdc80c08aaf4c52a718fdc42 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Tue, 20 May 2014 16:47:15 -0700 Subject: [PATCH 10/10] [CHORE] new example data --- test/vault-test.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/vault-test.js b/test/vault-test.js index 45119573..e2c97f03 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -6,21 +6,20 @@ var assert = require('assert'), UInt256 = require('../src/js/ripple/uint256').UInt256; var exampleData = { - id : "57d6ed12d3b98ca91b61afac2fb30212f642daabefd9c7cda623f145f384830c", - crypt : "1733480ceea2970e5f979c8d8e508d79e446b42d54f593e640814ea91deb53ef", - unlock : "452b02b80469a6a2ad692264c04d2a3794ea0ab11d8c902ef774190294db2ce2", + id : "ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f", + crypt : "f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82", + unlock : "3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4", blobURL : "https://id.staging.ripple.com", - username : "testUser", + username : "exampleUser", password : "pass word", domain : "staging.ripple.com", - encrypted_secret : "QUh5dnBqR0pTTVpjcjVoY0FhN1cxcEdTdW1XS1hLS2VzNlpQT2ZvQkFJWmg1UHRYS1RobUhKTkZUcWNyNlZEVlZYZDNhS1l0" + encrypted_secret : "APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm" }; process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed certs - describe('Ripple Txt', function() { it('should get the context of a ripple.txt file from a given domain', function(done){ var rt = new RippleTxt();