UNPKG

ripple-vault-client

Version:
621 lines (508 loc) 15.4 kB
var async = require('async'); var blobClient = require('./blob').BlobClient; var Blob = require('./blob').Blob; var AuthInfo = require('./authinfo').AuthInfo; var RippleTxt = require('./rippletxt').RippleTxt; var crypt = require('./crypt').Crypt; function VaultClient(opts) { var self = this; if (!opts) { opts = { }; } if (typeof opts === 'string') { opts = { domain: opts }; } this.domain = opts.domain || 'ripple.com'; this.infos = { }; }; /** * getAuthInfo * gets auth info for a username. returns authinfo * even if user does not exists (with exist set to false) * @param {string} username * @param {function} callback */ VaultClient.prototype.getAuthInfo = function (username, callback) { AuthInfo.get(this.domain, 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 (typeof authInfo.blobvault !== 'string') { return callback(new Error('No blobvault specified in the authinfo.')); } callback(null, authInfo); }); }; /** * _deriveLoginKeys * method designed for asnyc waterfall */ VaultClient.prototype._deriveLoginKeys = function (authInfo, password, callback) { var normalizedUsername = authInfo.username.toLowerCase().replace(/-/g, ''); //derive login keys crypt.derive(authInfo.pakdf, 'login', normalizedUsername, password, function(err, keys) { if (err) { callback(err); } else { callback(null, authInfo, password, keys); } }); }; /** * _deriveUnlockKey * method designed for asnyc waterfall */ VaultClient.prototype._deriveUnlockKey = function (authInfo, password, keys, callback) { var normalizedUsername = authInfo.username.toLowerCase().replace(/-/g, ''); //derive unlock key crypt.derive(authInfo.pakdf, 'unlock', normalizedUsername, password, function(err, unlock) { if (err) { console.log('error', 'derive:', err); return callback(err); } if (!keys) { keys = { }; } keys.unlock = unlock.unlock; callback(null, authInfo, keys); }); }; /** * 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, callback) { //use the url from previously retrieved authInfo, if necessary if (!url) { callback(new Error('Blob vault URL is required')); } else { blobClient.getRippleName(url, address, callback); } }; /** * Check blobvault for existance of username * * @param {string} username * @param {function} fn - Callback function */ VaultClient.prototype.exists = function(username, callback) { AuthInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { if (err) { callback(err); } else { callback(null, !!authInfo.exists); } }); }; /** * Check blobvault for existance of address * * @param {string} address * @param {function} fn - Callback function */ VaultClient.prototype.addressExists = function(address, callback) { AuthInfo.getAddress(this.domain, address, function(err, authInfo) { if (err) { callback(err); } else { callback(null, !!authInfo.addressExists); } }); }; /** * 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, device_id, callback) { var self = this; var steps = [ getAuthInfo, self._deriveLoginKeys, getBlob ]; async.waterfall(steps, callback); function getAuthInfo(callback) { self.getAuthInfo(username, function(err, authInfo){ if (authInfo && !authInfo.exists) { return callback(new Error('User does not exist.')); } return callback (err, authInfo, password); }); } function getBlob(authInfo, password, keys, callback) { 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); } //save for relogin self.infos[keys.id] = authInfo; //migrate missing fields if (blob.missing_fields) { if (blob.missing_fields.encrypted_blobdecrypt_key) { console.log('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, verified : authInfo.emailVerified, //DEPRECIATE emailVerified : authInfo.emailVerified, profileVerified : authInfo.profile_verified, identityVerified : authInfo.identity_verified }); }); }; 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 console.log('error:', 'decrypt:', error); } options = { username : params.username, blob : params.blob, masterkey : secret, keys : keys }; blobClient.updateKeys(options, function(err, resp){ if (err) { console.log('error:', 'updateKeys:', err); } }); } }; /** * Retreive and decrypt blob using a blob url, id and crypt derived previously. * * @param {string} url - Blob vault url * @param {string} id - Blob id from previously retreived blob * @param {string} key - Blob decryption key * @param {function} fn - Callback function */ VaultClient.prototype.relogin = function(url, id, key, device_id, callback) { //use the url from previously retrieved authInfo, if necessary if (!url && this.infos[id]) { url = this.infos[id].blobvault; } if (!url) { return callback(new Error('Blob vault URL is required')); } var options = { url : url, blob_id : id, key : key, device_id : device_id }; blobClient.get(options, function(err, blob) { if (err) { callback(err); } else { callback (null, { blob: blob }); } }); }; /** * Decrypt the secret key using a username and password * * @param {string} username * @param {string} password * @param {string} encryptSecret * @param {function} fn - Callback function */ VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) { var self = this; var steps = [ getAuthInfo, self._deriveUnlockKey, unlockSecret ]; async.waterfall(steps, fn); function getAuthInfo(callback) { self.getAuthInfo(username, function(err, authInfo){ if (authInfo && !authInfo.exists) { return callback(new Error('User does not exist.')); } return callback (err, authInfo, password, {}); }); } function unlockSecret (authinfo, keys, callback) { var secret; try { secret = crypt.decrypt(keys.unlock, encryptSecret); } catch (error) { return callback(error); } callback(null, { keys : keys, secret : secret }); } }; /** * 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, device_id, fn) { var self = this; var steps = [ login, deriveUnlockKey, unlockSecret ]; async.waterfall(steps, fn); function login (callback) { self.login(username, password, device_id, function(err, resp) { if (err) { return callback(err); } if (!resp.blob || !resp.blob.encrypted_secret) { return callback(new Error('Unable to retrieve blob and secret.')); } if (!resp.blob.id || !resp.blob.key) { return callback(new Error('Unable to retrieve keys.')); } //get authInfo via id - would have been saved from login var authInfo = self.infos[resp.blob.id]; if (!authInfo) { return callback(new Error('Unable to find authInfo')); } callback(null, authInfo, password, resp.blob); }); }; function deriveUnlockKey (authInfo, password, blob, callback) { self._deriveUnlockKey(authInfo, password, null, function(err, authInfo, keys){ callback(err, keys.unlock, authInfo, blob); }); }; function unlockSecret (unlock, authInfo, blob, callback) { var secret; try { secret = crypt.decrypt(unlock, blob.encrypted_secret); } catch (error) { return callback(error); } callback(null, { blob : blob, unlock : unlock, secret : secret, username : authInfo.username, verified : authInfo.emailVerified, //DEPRECIATE emailVerified : authInfo.emailVerified, profileVerified : authInfo.profile_verified, identityVerified : authInfo.identity_verified }); }; }; /** * 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, callback) { var self = this; self.getAuthInfo(username, function (err, authInfo){ if (err) { return callback(err); } blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, callback); }); }; /* * changePassword * @param {object} options * @param {string} options.username * @param {string} options.password * @param {string} options.masterkey * @param {object} options.blob */ VaultClient.prototype.changePassword = function (options, fn) { var self = this; var password = String(options.password).trim(); var steps = [ getAuthInfo, self._deriveLoginKeys, self._deriveUnlockKey, changePassword ]; async.waterfall(steps, fn); function getAuthInfo(callback) { self.getAuthInfo(options.username, function(err, authInfo) { return callback (err, authInfo, password); }); }; function changePassword (authInfo, keys, callback) { options.keys = keys; blobClient.updateKeys(options, callback); }; }; /** * rename * rename a ripple account * @param {object} options * @param {string} options.username * @param {string} options.new_username * @param {string} options.password * @param {string} options.masterkey * @param {object} options.blob * @param {function} fn */ VaultClient.prototype.rename = function (options, fn) { var self = this; var new_username = String(options.new_username).trim(); var password = String(options.password).trim(); var steps = [ getAuthInfo, self._deriveLoginKeys, self._deriveUnlockKey, renameBlob ]; async.waterfall(steps, fn); function getAuthInfo(callback) { self.getAuthInfo(new_username, function(err, authInfo){ if (authInfo && authInfo.exists) { return callback(new Error('username already taken.')); } else { authInfo.username = new_username; } return callback (err, authInfo, password); }); }; function renameBlob (authInfo, keys, callback) { options.keys = keys; blobClient.rename(options, callback); }; }; /** * 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; 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, self._deriveLoginKeys, self._deriveUnlockKey, create ]; async.waterfall(steps, fn); function getAuthInfo(callback) { self.getAuthInfo(username, function(err, authInfo){ return callback (err, authInfo, password); }); }; function create(authInfo, keys, callback) { var params = { url : authInfo.blobvault, id : keys.id, crypt : keys.crypt, unlock : keys.unlock, username : username, email : options.email, masterkey : options.masterkey || crypt.createMaster(), activateLink : options.activateLink, oldUserBlob : options.oldUserBlob, domain : options.domain }; blobClient.create(params, function(err, blob) { if (err) { callback(err); } else { callback(null, { blob : blob, username : username }); } }); }; }; /** * validateUsername * check username for validity */ 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; }; /** * 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.recoverBlob = blobClient.recoverBlob; VaultClient.prototype.deleteBlob = blobClient.deleteBlob; VaultClient.prototype.requestToken = blobClient.requestToken; VaultClient.prototype.verifyToken = blobClient.verifyToken; VaultClient.prototype.getAttestation = blobClient.getAttestation; VaultClient.prototype.updateAttestation = blobClient.updateAttestation; VaultClient.prototype.getAttestationSummary = blobClient.getAttestationSummary; //export by name exports.VaultClient = VaultClient; exports.AuthInfo = AuthInfo; exports.RippleTxt = RippleTxt; exports.Blob = Blob;