From 7be13bebfcc533fe7a3312e5629320bb469acd4c Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Mon, 7 Jul 2014 10:21:04 -0700 Subject: [PATCH] [FEATURE] vault client: function for 2FA settings --- src/js/ripple/blob.js | 132 +++++++++++++++++++++++++++++------ src/js/ripple/vaultclient.js | 114 ++++++++++++------------------ test/vault-test.js | 9 +-- 3 files changed, 161 insertions(+), 94 deletions(-) diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index 656819f9..361c3554 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -7,12 +7,15 @@ var log = require('./log').sub('blob'); var BlobClient = {}; //Blob object class -function BlobObj(url, id, key) { - this.url = url; - this.id = id; - this.key = key; - this.identity = new Identity(this); - this.data = { }; +function BlobObj(options) { + if (!options) options = { }; + + this.device_id = options.device_id; + this.url = options.url; + this.id = options.blob_id; + this.key = options.key; + this.identity = new Identity(this); + this.data = { }; }; // Blob operations @@ -96,11 +99,16 @@ BlobObj.prototype.init = function(fn) { self.url = 'http://' + url; } - url = self.url + '/v1/blob/' + self.id; - + url = self.url + '/v1/blob/' + self.id; + url += '?' + request.get(url, function(err, resp) { - if (err || !resp.body || resp.body.result !== 'success') { + if (err) { + return fn(new Error(err.message || 'Could not retrieve blob')); + } else if (!resp.body) { return fn(new Error('Could not retrieve blob')); + } else if (resp.body.result !== 'success') { + return fn(new Error('Incorrect username or password')); } self.revision = resp.body.revision; @@ -513,6 +521,50 @@ BlobObj.prototype.postUpdate = function(op, pointer, params, fn) { }); }; +/** + * set2FA + * modify 2 factor auth settings + * @params {object} options + * @params {string} + * @params {boolean} options.remember_me //remember for 30 days + * @params {boolean} options.enabled + * @params {string} options.phone + * @params {string} options.country_code + * @params {string} options.via //sms, etc + */ + +BlobObj.prototype.set2FA = function(options, fn) { + + var config = { + method : 'POST', + url : this.url + '/v1/blob/' + this.id + '/2FA', + data : { + remember_me : options.remember_me, + enabled : options.enabled, + phone : options.phone, + country_code : options.country_code, + via : options.via + } + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signAsymmetric(options.masterkey, this.data.account_id, this.id); + + request.post(signed.url) + .send(signed.data) + .end(function(err, resp) { + if (err) { + fn(err); + } else if (resp.body && resp.body.result === 'success') { + fn(null, resp.body); + } else if (resp.body && resp.body.result === 'error') { + fn(new Error(resp.body.message)); + } else { + fn(new Error('Unable to update settings.')); + } + }); +}; + /***** helper functions *****/ function normalizeSubcommands(subcommands, compress) { @@ -796,7 +848,7 @@ exports.Blob = BlobObj; * Get ripple name for a given address */ -exports.getRippleName = function(url, address, fn) { +BlobClient.getRippleName = function(url, address, fn) { if (!crypt.isValidAddress(address)) { return fn (new Error('Invalid ripple address')); } @@ -817,10 +869,15 @@ exports.getRippleName = function(url, address, fn) { /** * Retrive a blob with url, id and key + * @params {object} options + * @params {string} options.url + * @params {string} options.blob_id + * @params {string} options.key + * @params {string} options.device_id //optional */ -BlobClient.get = function (url, id, crypt, fn) { - var blob = new BlobObj(url, id, crypt); +BlobClient.get = function (options, fn) { + var blob = new BlobObj(options); blob.init(fn); }; @@ -842,8 +899,15 @@ BlobClient.verify = function(url, username, token, fn) { }; /** - * ResendEmail - * resend verification email + * resendEmail + * send a new verification email + * @param {object} opts + * @param {string} opts.id + * @param {string} opts.username + * @param {string} opts.account_id + * @param {string} opts.email + * @param {string} opts.activateLink + * @param {function} fn - Callback */ BlobClient.resendEmail = function (opts, fn) { @@ -916,9 +980,14 @@ BlobClient.recoverBlob = function (opts, fn) { }); function handleRecovery (resp) { - //decrypt crypt key - var crypt = decryptBlobCrypt(opts.masterkey, resp.body.encrypted_blobdecrypt_key); - var blob = new BlobObj(opts.url, resp.body.blob_id, crypt); + + var params = { + url : opts.url, + blob_id : resp.body.blob_id, + key : decryptBlobCrypt(opts.masterkey, resp.body.encrypted_blobdecrypt_key) + } + + var blob = new BlobObj(params); blob.revision = resp.body.revision; blob.encrypted_secret = resp.body.encrypted_secret; @@ -946,8 +1015,18 @@ BlobClient.recoverBlob = function (opts, fn) { /** * updateProfile - * update information stored outside the blob - */ + * update information stored outside the blob - HMAC signed + * @param {object} + * @param {string} opts.url + * @param {string} opts.username + * @param {string} opts.auth_secret + * @param {srring} opts.blob_id + * @param {object} opts.profile + * @param {string} opts.profile.phone - optional + * @param {string} opts.profile.country - optional + * @param {string} opts.profile.region - optional + * @param {string} opts.profile.city - optional + */ BlobClient.updateProfile = function (opts, fn) { var config = { @@ -1088,7 +1167,12 @@ BlobClient.rename = function (opts, fn) { */ BlobClient.create = function(options, fn) { - var blob = new BlobObj(options.url, options.id, options.crypt); + var params = { + url : options.url, + blob_id : options.id, + key : options.crypt + } + var blob = new BlobObj(params); blob.revision = 0; @@ -1144,7 +1228,13 @@ BlobClient.create = function(options, fn) { }; /** - * deleteBlob + * deleteBlob + * @param {object} options + * @param {string} options.url + * @param {string} options.username + * @param {string} options.blob_id + * @param {string} options.account_id + * @param {string} options.masterkey */ BlobClient.deleteBlob = function(options, fn) { diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js index 2c29f2fd..02600c03 100644 --- a/src/js/ripple/vaultclient.js +++ b/src/js/ripple/vaultclient.js @@ -133,7 +133,7 @@ VaultClient.prototype.exists = function(username, callback) { * @param {function} fn - Callback function */ -VaultClient.prototype.login = function(username, password, callback) { +VaultClient.prototype.login = function(username, password, device_id, callback) { var self = this; var steps = [ @@ -156,7 +156,14 @@ VaultClient.prototype.login = function(username, password, callback) { } function getBlob(authInfo, password, keys, callback) { - blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) { + var options = { + url : authInfo.blobvault, + blob_id : keys.id, + key : keys.crypt, + device_id : device_id + }; + + blobClient.get(options, function(err, blob) { if (err) { return callback(err); } @@ -218,7 +225,7 @@ VaultClient.prototype.login = function(username, password, callback) { * @param {function} fn - Callback function */ -VaultClient.prototype.relogin = function(url, id, key, callback) { +VaultClient.prototype.relogin = function(url, id, key, device_id, callback) { //use the url from previously retrieved authInfo, if necessary if (!url && this.infos[id]) { url = this.infos[id].blobvault; @@ -228,7 +235,14 @@ VaultClient.prototype.relogin = function(url, id, key, callback) { return callback(new Error('Blob vault URL is required')); } - blobClient.get(url, id, key, function(err, blob) { + var options = { + url : url, + blob_id : id, + key : key, + device_id : device_id + }; + + blobClient.get(options, function(err, blob) { if (err) { callback(err); } else { @@ -293,7 +307,7 @@ VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) { * @param {function} fn - Callback function */ -VaultClient.prototype.loginAndUnlock = function(username, password, fn) { +VaultClient.prototype.loginAndUnlock = function(username, password, device_id, fn) { var self = this; var steps = [ @@ -305,7 +319,7 @@ VaultClient.prototype.loginAndUnlock = function(username, password, fn) { async.waterfall(steps, fn); function login (callback) { - self.login(username, password, function(err, resp) { + self.login(username, password, device_id, function(err, resp) { if (err) { return callback(err); @@ -374,69 +388,6 @@ VaultClient.prototype.verify = function(username, token, callback) { }); }; -/** - * resendEmail - * send a new verification email - * @param {object} options - * @param {string} options.id - * @param {string} options.username - * @param {string} options.account_id - * @param {string} options.email - * @param {string} options.activateLink - * @param {function} fn - Callback - */ - -VaultClient.prototype.resendEmail = function (options, fn) { - blobClient.resendEmail(options, fn); -}; - -/** - * deleteBlob - * @param {object} options - * @param {string} options.url - * @param {string} options.username - * @param {string} options.blob_id - * @param {string} options.account_id - * @param {string} options.masterkey - */ - -VaultClient.prototype.deleteBlob = function (options, fn) { - blobClient.deleteBlob(options, fn); -}; - -/** - * updateProfile - * update information stored outside the blob - * @param {object} - * @param {string} options.url - * @param {string} options.username - * @param {string} options.auth_secret - * @param {srring} options.blob_id - * @param {object} options.profile - * @param {string} options.profile.phone - optional - * @param {string} options.profile.country - optional - * @param {string} options.profile.region - optional - * @param {string} options.profile.city - optional - */ - -VaultClient.prototype.updateProfile = function (options, fn) { - blobClient.updateProfile(options, fn); -}; - -/** - * recoverBlob - * recover blob with account secret - * @param {object} options - * @param {string} options.url - * @param {string} options.username - * @param {string} options.masterkey - * @param {function} - */ - -VaultClient.prototype.recoverBlob = function (options, fn) { - blobClient.recoverBlob(options, fn); -}; - /* * changePassword * @param {object} options @@ -581,6 +532,11 @@ VaultClient.prototype.register = function(options, fn) { }; }; +/** + * validateUsername + * check username for validity + */ + VaultClient.prototype.validateUsername = function (username) { username = String(username).trim(); var result = { @@ -607,4 +563,24 @@ VaultClient.prototype.validateUsername = function (username) { return result; }; +/** + * generateDeviceID + * create a new random device ID for 2FA + */ +VaultClient.prototype.generateDeviceID = function () { + return crypt.createSecret(4); +}; + +/*** pass thru some blob client function ***/ + +VaultClient.prototype.resendEmail = blobClient.resendEmail; + +VaultClient.prototype.updateProfile = blobClient.updateProfile; + +VaultClient.prototype.recoverBlob = blobClient.recoverBlob; + +VaultClient.prototype.deleteBlob = blobClient.deleteBlob; + + +//export by name exports.VaultClient = VaultClient; diff --git a/test/vault-test.js b/test/vault-test.js index 7d664702..c0bd25c8 100644 --- a/test/vault-test.js +++ b/test/vault-test.js @@ -19,6 +19,7 @@ var exampleData = { masterkey : 'ssize4HrSYZShMWBtK6BhALGEk8VH', email_token : '77825040-9096-4695-9cbc-76720f6a8649', activateLink : 'https://staging.ripple.com/client/#/register/activate/', + device_id : "", blob: { url: 'https://id.staging.ripple.com', id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f', @@ -289,7 +290,7 @@ describe('VaultClient', function () { 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) { + client.login(exampleData.username, exampleData.password, exampleData.device_id, function(err, resp) { if (online) { assert.ifError(err); assert.strictEqual(typeof resp, 'object'); @@ -313,7 +314,7 @@ describe('VaultClient', function () { 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.blob.url, exampleData.id, exampleData.crypt, function(err, resp) { + client.relogin(exampleData.blob.url, exampleData.id, exampleData.crypt, exampleData.device_id, function(err, resp) { assert.ifError(err); assert.strictEqual(typeof resp, 'object'); assert(resp.blob instanceof Blob); @@ -345,7 +346,7 @@ describe('VaultClient', function () { 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) { + client.loginAndUnlock(exampleData.username, exampleData.password, exampleData.device_id, function(err, resp) { if (online) { assert.ifError(err); assert.strictEqual(typeof resp, 'object'); @@ -454,7 +455,7 @@ describe('Blob', function () { if (online) { this.timeout(10000); - client.login(exampleData.username, exampleData.password, function(err, res) { + client.login(exampleData.username, exampleData.password, exampleData.device_id, function(err, res) { resp = res; blob = res.blob; done();