Merge pull request #102 from shekenahglory/develop

Save encrypted blob decrypt key at login if missing
This commit is contained in:
wltsmrz
2014-06-18 21:35:16 -07:00
5 changed files with 163 additions and 45 deletions

View File

@@ -2,15 +2,13 @@ var async = require('async');
var superagent = require('superagent');
var RippleTxt = require('./rippletxt').RippleTxt;
function AuthInfo() {
this.rippleTxt = new RippleTxt();
var AuthInfo = { };
AuthInfo._getRippleTxt = function(domain, callback) {
RippleTxt.get(domain, callback);
};
AuthInfo.prototype._getRippleTxt = function(domain, callback) {
this.rippleTxt.get(domain, callback);
};
AuthInfo.prototype._getUser = function(url, callback) {
AuthInfo._getUser = function(url, callback) {
superagent.get(url, callback);
};
@@ -23,7 +21,7 @@ AuthInfo.prototype._getUser = function(url, callback) {
* @param {function} fn - Callback function
*/
AuthInfo.prototype.get = function(domain, username, callback) {
AuthInfo.get = function(domain, username, callback) {
var self = this;
username = username.toLowerCase();

View File

@@ -102,10 +102,11 @@ BlobObj.prototype.init = function(fn) {
if (err || !resp.body || resp.body.result !== 'success') {
return fn(new Error('Could not retrieve blob'));
}
self.revision = resp.body.revision;
self.revision = resp.body.revision;
self.encrypted_secret = resp.body.encrypted_secret;
self.missing_fields = resp.body.missing_fields;
if (!self.decrypt(resp.body.blob)) {
return fn(new Error('Error while decrypting blob'));
}
@@ -902,7 +903,11 @@ BlobClient.recoverBlob = function (opts, fn) {
if (err) {
fn(err);
} else if (resp.body && resp.body.result === 'success') {
handleRecovery(resp);
if (!resp.body.encrypted_blobdecrypt_key) {
fn(new Error('Missing encrypted blob decrypt key.'));
} else {
handleRecovery(resp);
}
} else if (resp.body && resp.body.result === 'error') {
fn(new Error(resp.body.message));
} else {

View File

@@ -1,7 +1,8 @@
var superagent = require('superagent');
var request = require('superagent');
var Currency = require('./currency').Currency;
function RippleTxt() {
this.txts = { };
var RippleTxt = {
txts : { }
};
RippleTxt.urlTemplates = [
@@ -13,22 +14,13 @@ RippleTxt.urlTemplates = [
'http://ripple.{{domain}}/ripple.txt'
];
RippleTxt.request = function() {
return request;
};
RippleTxt.prototype.request = function(url, callback) {
return superagent.get(url, callback);
};
/**
* 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) {
RippleTxt.get = function(domain, fn) {
var self = this;
if (self.txts[domain]) {
@@ -44,7 +36,7 @@ RippleTxt.prototype.get = function(domain, fn) {
url = url.replace('{{domain}}', domain);
self.request(url, function(err, resp) {
request.get(url, function(err, resp) {
if (err || !resp.text) {
return nextUrl(++i);
}
@@ -59,11 +51,10 @@ RippleTxt.prototype.get = function(domain, fn) {
/**
* Parse a ripple.txt file
*
* @param {string} txt - Unparsed ripple.txt data
*/
RippleTxt.prototype.parse = function(txt) {
RippleTxt.parse = function(txt) {
var currentSection = '';
var sections = { };
@@ -90,4 +81,53 @@ RippleTxt.prototype.parse = function(txt) {
return sections;
};
/**
* extractDomain
* attempt to extract the domain from a given url
* returns the url if unsuccessful
* @param {Object} url
*/
RippleTxt.extractDomain = function (url) {
match = /[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?([^.\?][^\?.]+?)?$/.exec(url);
return match && match[0] ? match[0] : url;
};
/**
* getCurrencies
* returns domain, issuer account and currency object
* for each currency found in the domain's ripple.txt file
* @param {Object} domain
* @param {Object} fn
*/
RippleTxt.getCurrencies = function(domain, fn) {
domain = RippleTxt.extractDomain(domain);
this.get(domain, function(err, txt) {
if (err) {
return fn(err);
}
if (err || !txt.currencies || !txt.accounts) {
return fn(null, []);
}
//NOTE: this won't be accurate if there are
//multiple issuer accounts with different
//currencies associated with each.
var issuer = txt.accounts[0];
var currencies = [];
txt.currencies.forEach(function(currency) {
currencies.push({
issuer : issuer,
currency : Currency.from_json(currency),
domain : domain
});
});
fn(null, currencies);
});
};
exports.RippleTxt = RippleTxt;

View File

@@ -2,7 +2,7 @@ var async = require('async');
var blobClient = require('./blob').BlobClient;
var AuthInfo = require('./authinfo').AuthInfo;
var crypt = require('./crypt').Crypt;
var log = require('./log').sub('vault');
function VaultClient(opts) {
var self = this;
@@ -16,7 +16,6 @@ function VaultClient(opts) {
}
this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo();
this.infos = { };
};
@@ -29,7 +28,7 @@ function VaultClient(opts) {
*/
VaultClient.prototype.getAuthInfo = function (username, callback) {
this.authInfo.get(this.domain, username, function(err, authInfo) {
AuthInfo.get(this.domain, username, function(err, authInfo) {
if (err) {
return callback(err);
}
@@ -78,7 +77,7 @@ VaultClient.prototype._deriveUnlockKey = function (authInfo, password, keys, cal
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', authInfo.username.toLowerCase(), password, function(err, unlock) {
if (err) {
console.log('Error',err);
log.error('derive:', err);
return callback(err);
}
@@ -145,6 +144,16 @@ VaultClient.prototype.login = function(username, password, callback) {
//save for relogin
self.infos[keys.id] = authInfo;
//migrate missing fields
if (blob.missing_fields) {
if (blob.missing_fields.encrypted_blobdecrypt_key) {
log.info('migration: saving encrypted blob decrypt key');
authInfo.blob = blob;
//get the key to unlock the secret, then update the blob keys
self._deriveUnlockKey(authInfo, password, keys, updateKeys);
}
}
callback(null, {
blob : blob,
username : authInfo.username,
@@ -152,6 +161,32 @@ VaultClient.prototype.login = function(username, password, callback) {
});
});
};
function updateKeys (err, params, keys) {
if (err || !keys.unlock) {
return; //unable to unlock
}
var secret;
try {
secret = crypt.decrypt(keys.unlock, params.blob.encrypted_secret);
} catch (error) {
return log.error('decrypt:', error);
}
options = {
username : params.username,
blob : params.blob,
masterkey : secret,
keys : keys
};
blobClient.updateKeys(options, function(err, resp){
if (err) {
log.error('updateKeys:', err);
}
});
}
};
/**
@@ -307,7 +342,7 @@ VaultClient.prototype.loginAndUnlock = function(username, password, fn) {
*/
VaultClient.prototype.exists = function(username, callback) {
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
AuthInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) {
callback(err);
} else {
@@ -462,6 +497,11 @@ VaultClient.prototype.register = function(options, fn) {
var self = this;
var username = String(options.username).trim();
var password = String(options.password).trim();
var result = self.validateUsername(username);
if (!result.valid) {
return fn(new Error('invalid username.'));
}
var steps = [
getAuthInfo,
@@ -473,8 +513,7 @@ VaultClient.prototype.register = function(options, fn) {
async.waterfall(steps, fn);
function getAuthInfo(callback) {
self.getAuthInfo(username, function(err, authInfo){
self.getAuthInfo(username, function(err, authInfo){
return callback (err, authInfo, password);
});
};
@@ -485,7 +524,7 @@ VaultClient.prototype.register = function(options, fn) {
id : keys.id,
crypt : keys.crypt,
unlock : keys.unlock,
username : authInfo.username,
username : username,
email : options.email,
masterkey : options.masterkey || crypt.createMaster(),
activateLink : options.activateLink,
@@ -499,11 +538,37 @@ VaultClient.prototype.register = function(options, fn) {
} else {
callback(null, {
blob : blob,
username : authInfo.username
username : username
});
}
});
};
};
VaultClient.prototype.validateUsername = function (username) {
username = String(username).trim();
var result = {
valid : false,
reason : ''
};
if (username.length < 2) {
result.reason = 'tooshort';
} else if (username.length > 20) {
result.reason = 'toolong';
} else if (!/^[a-zA-Z0-9\-]+$/.exec(username)) {
result.reason = 'charset';
} else if (/^-/.exec(username)) {
result.reason = 'starthyphen';
} else if (/-$/.exec(username)) {
result.reason = 'endhyphen';
} else if (/--/.exec(username)) {
result.reason = 'multhyphen';
} else {
result.valid = true;
}
return result;
};
exports.VaultClient = VaultClient;

View File

@@ -5,7 +5,6 @@ var VaultClient = require('../src/js/ripple/vaultclient').VaultClient;
var Blob = require('../src/js/ripple/blob').Blob;
var UInt256 = require('../src/js/ripple/uint256').UInt256;
var sjcl = require('../build/sjcl');
var random = require('crypto').randomBytes(256);
var nock = require('nock');
var online = process.argv.indexOf('--online-blobvault') !== -1 ? true : false;
@@ -113,7 +112,9 @@ var blob = new Blob();
//must be set for self signed certs
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
sjcl.random.addEntropy(random.toString()); //add entropy to seed the generator
while(!sjcl.random.isReady()) {
sjcl.random.addEntropy(require('crypto').randomBytes(128).toString()); //add entropy to seed the generator
}
var mockRippleTxt;
var mockAuthSign;
@@ -203,21 +204,30 @@ if (!online) {
describe('Ripple Txt', function () {
it('should get the content of a ripple.txt file from a given domain', function(done) {
var rt = new RippleTxt();
rt.get(exampleData.domain, function(err, resp) {
RippleTxt.get(exampleData.domain, function(err, resp) {
assert.ifError(err);
assert.strictEqual(typeof resp, 'object');
done();
});
});
it('should get currencies from a ripple.txt file for a given domain', function(done) {
RippleTxt.getCurrencies(exampleData.domain, function(err, currencies) {
assert.ifError(err);
assert(Array.isArray(currencies));
done();
});
});
it('should get the domain from a given url', function() {
var domain = RippleTxt.extractDomain("http://www.example.com");
assert.strictEqual(typeof domain, 'string');
});
});
describe('AuthInfo', function() {
it('should get auth info', function(done) {
var auth = new AuthInfo();
auth.get(exampleData.domain, exampleData.username, function(err, resp) {
AuthInfo.get(exampleData.domain, exampleData.username, function(err, resp) {
assert.ifError(err);
Object.keys(authInfoRes.body).forEach(function(prop) {
assert(resp.hasOwnProperty(prop));