From 182e1863f433f624bac070b95f96bbe3cfcfd720 Mon Sep 17 00:00:00 2001 From: Matthew Fettig Date: Wed, 11 Jun 2014 09:26:03 -0700 Subject: [PATCH] [FEATURE] recover blob and change password --- src/js/ripple/blob.js | 158 +++++++++++++++++++++++++++++++-- src/js/ripple/signedrequest.js | 29 ++++++ src/js/ripple/vaultclient.js | 129 ++++++++++++++++++++++++--- 3 files changed, 299 insertions(+), 17 deletions(-) diff --git a/src/js/ripple/blob.js b/src/js/ripple/blob.js index df2b9eef..bf86877e 100644 --- a/src/js/ripple/blob.js +++ b/src/js/ripple/blob.js @@ -1,8 +1,8 @@ var crypt = require('./crypt').Crypt; var SignedRequest = require('./signedrequest').SignedRequest; -var request = require('superagent'); -var extend = require("extend"); -var async = require("async"); +var request = require('superagent'); +var extend = require("extend"); +var async = require("async"); var BlobClient = {}; @@ -261,11 +261,12 @@ BlobObj.prototype.encryptBlobCrypt = function(secret, blobDecryptKey) { * Decrypt recovery key * * @param {string} secret + * @param {string} encryptedKey */ -BlobObj.prototype.decryptBlobCrypt = function(secret) { +function decryptBlobCrypt (secret, encryptedKey) { var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); - return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); + return crypt.decrypt(recoveryEncryptionKey, encryptedKey); }; /**** Blob updating functions ****/ @@ -843,6 +844,7 @@ BlobClient.verify = function(url, username, token, fn) { * ResendEmail * resend verification email */ + BlobClient.resendEmail = function (opts, fn) { var config = { method : 'POST', @@ -875,6 +877,145 @@ BlobClient.resendEmail = function (opts, fn) { }); }; +/** + * RecoverBlob + * recover a blob using the account secret + * @param {object} opts + * @param {string} opts.url + * @param {string} opts.username + * @param {string} opts.masterkey + * @param {function} fn + */ + +BlobClient.recoverBlob = function (opts, fn) { + var username = String(opts.username).trim(); + var config = { + method : 'GET', + url : opts.url + '/v1/user/recov/' + username, + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signAsymmetricRecovery(opts.masterkey, username); + + request.get(signed.url) + .end(function(err, resp) { + if (err) { + fn(err); + } else if (resp.body && resp.body.result === 'success') { + handleRecovery(resp); + } else if (resp.body && resp.body.result === 'error') { + fn(new Error(resp.body.message)); + } else { + fn(new Error('Could not recover blob')); + } + }); + + 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); + + blob.revision = resp.body.revision; + blob.encrypted_secret = resp.body.encrypted_secret; + + if (!blob.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 && blob.applyEncryptedPatch(patch); + }); + + if (successful) { + blob.consolidate(); + } + } + + //return with newly decrypted blob + fn(null, blob); + }; +}; + +/** + * updateKeys + * @param {object} opts + * @param {string} opts.username + * @param {string} opts.password + * @param {string} opts.masterkey + * @param {object} opts.pakdf + */ + +BlobObj.prototype.updateKeys = function (opts, fn) { + var self = this; + var username = String(opts.username).trim(); + var password = String(opts.password).trim(); + + function deriveKeys(callback) { + // derive unlock and login keys + var keys = { }; + + function deriveKey(keyType, callback) { + crypt.derive(opts.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, keys); + } + }); + }; + + function updateBlob (keys, callback) { + var old_id = self.id; + + self.id = keys.login.id; + self.key = keys.login.crypt; + self.encrypted_secret = self.encryptSecret(keys.unlock.unlock, opts.masterkey); + + //post to the blob vault to create + var config = { + method : 'POST', + url : self.url + '/v1/user/' + username, + data : { + blob_id : self.id, + data : self.encrypt(), + revision : self.revision, + encrypted_blobdecrypt_key : self.encryptBlobCrypt(opts.masterkey, self.key), + encrypted_secret : self.encrypted_secret + } + }; + + var signedRequest = new SignedRequest(config); + var signed = signedRequest.signAsymmetric(opts.masterkey, self.data.account_id, old_id); + + request.post(signed.url) + .send(signed.data) + .end(function(err, resp) { + if (err) { + fn(new Error('Updated blob could not be saved - XHR error')); + } else if (!resp.body || resp.body.result !== 'success') { + fn(new Error('Updated blob could not be saved - bad result')); + } else { + fn(null, resp.body); + } + }); + }; + + async.waterfall([ deriveKeys, updateBlob ], fn); +}; + /** * Rename a ripple account * @@ -894,6 +1035,7 @@ BlobClient.rename = function (opts, fn) { username: opts.new_username, data: opts.blob.encrypt(), encrypted_secret: opts.blob.encryptedSecret, + encrypted_blobdecrypt_key: opts.blob.encrypted_blobdecrypt_key, revision: opts.blob.revision } }; @@ -978,7 +1120,7 @@ BlobClient.create = function(options, fn) { if (err) { fn(err); } else if (resp.body && resp.body.result === 'success') { - fn(null, blob,resp.body); + fn(null, blob, resp.body); } else if (resp.body && resp.body.result === 'error') { fn(new Error(resp.body.message)); } else { @@ -988,3 +1130,7 @@ BlobClient.create = function(options, fn) { }; exports.BlobClient = BlobClient; + + + + diff --git a/src/js/ripple/signedrequest.js b/src/js/ripple/signedrequest.js index 1280a66d..26798867 100644 --- a/src/js/ripple/signedrequest.js +++ b/src/js/ripple/signedrequest.js @@ -8,6 +8,7 @@ var SignedRequest = function (config) { // XXX Constructor should be generalized and constructing from an Angular.js // $http config should be a SignedRequest.from... utility method. this.config = extend(true, {}, config); + if (!this.config.data) this.config.data = {}; }; @@ -150,6 +151,34 @@ SignedRequest.prototype.signAsymmetric = function (secretKey, account, blob_id) return config; }; +/** + * Asymmetric signed request for vault recovery + * @param {Object} config + * @param {Object} secretKey + * @param {Object} username + */ +SignedRequest.prototype.signAsymmetricRecovery = function (secretKey, username) { + var config = extend(true, {}, this.config); + + // Parse URL + var parsed = parser.parse(config.url); + var date = dateAsIso8601(); + var signatureType = 'RIPPLE1-ECDSA-SHA512'; + var stringToSign = this.getStringToSign(parsed, date, signatureType); + var signature = Message.signMessage(stringToSign, secretKey); + + var query = querystring.stringify({ + signature: Crypt.base64ToBase64Url(signature), + signature_date: date, + signature_username: username, + signature_type: signatureType + }); + + config.url += (parsed.search ? '&' : '?') + query; + + return config; +}; + var dateAsIso8601 = (function () { function pad(n) { return (n < 0 || n > 9 ? "" : "0") + n; diff --git a/src/js/ripple/vaultclient.js b/src/js/ripple/vaultclient.js index 3e13612a..4429009b 100644 --- a/src/js/ripple/vaultclient.js +++ b/src/js/ripple/vaultclient.js @@ -90,9 +90,9 @@ VaultClient.prototype.login = function(username, password, callback) { self.infos[keys.id] = authInfo; callback(null, { - blob: blob, - username: authInfo.username, - verified: authInfo.emailVerified + blob : blob, + username : authInfo.username, + verified : authInfo.emailVerified }); }); }; @@ -203,11 +203,11 @@ VaultClient.prototype.loginAndUnlock = function(username, password, callback) { } callback(null, { - blob : blob, - unlock : keys.unlock, - secret : secret, - username : authInfo.username, - verified : authInfo.emailVerified + blob : blob, + unlock : keys.unlock, + secret : secret, + username : authInfo.username, + verified : authInfo.emailVerified }); }); }; @@ -231,7 +231,7 @@ VaultClient.prototype.loginAndUnlock = function(username, password, callback) { if (!authInfo) { return callback(new Error('Unable to find authInfo')); } - + deriveUnlockKey(authInfo, resp.blob, callback); }); }; @@ -281,10 +281,117 @@ VaultClient.prototype.verify = function(username, token, callback) { * resendEmail * send a new verification email * @param {object} options + * @param {function} fn - Callback + */ +VaultClient.prototype.resendEmail = function (options, fn) { + blobClient.resendEmail(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); +}; + +VaultClient.prototype.updateBlobKeys = function (options, fn) { + var username = String(options.username).trim(); + + this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { + if (err) { + return callback(err); + } + options.pakdf = authInfo.pakdf; + options.blob.updateKeys(options, fn); + }); +} + +/** + * rename + * rename a ripple account + * @param {object} options * @param {function} callback */ -VaultClient.prototype.resendEmail = function (options, callback) { - blobClient.resendEmail(options, callback); +VaultClient.prototype.rename = function (options, callback) { + var self = this; + var new_username = options.new_username; + var password = options.password; + + // TODO duplicate function + function getAuthInfo(callback) { + self.authInfo.get(self.domain, new_username, function(err, authInfo) { + if (err) { + return callback(err); + } + + 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); + }); + } + + // TODO duplicate function + function deriveLoginKeys(authInfo, callback) { + //derive login keys + crypt.derive(authInfo.pakdf, 'login', new_username.toLowerCase(), password, function(err, keys) { + if (err) { + callback(err); + } else { + callback(null, authInfo, keys); + } + }); + } + + // TODO duplicate function + function deriveUnlockKey(authInfo, callback) { + //derive unlock key + crypt.derive(authInfo.pakdf, 'unlock', new_username.toLowerCase(), password, function(err, keys) { + if (err) { + console.log('Error',err); + return callback(err); + } + + callback(null, keys.unlock); + }); + } + + getAuthInfo(function(err, authInfo){ + deriveLoginKeys(authInfo, function(err, authInfo, loginKeys){ + deriveUnlockKey(authInfo, function(err, unlockKeys){ + if (err) { + console.log('Error', err); + return; + } + + options.crypt = loginKeys.crypt; + options.new_blob_id = loginKeys.id; + options.unlock = unlockKeys; + + blobClient.rename(options, callback); + }) + }) + }); }; /**