diff --git a/.jshint b/.jshint new file mode 100644 index 000000000..219ea40f2 --- /dev/null +++ b/.jshint @@ -0,0 +1,15 @@ +{ + "camelcase": true, + "curly": true, + "eqeqeq": true, + "freeze": true, + "indent": 2, + "newcap": true, + "quotmark": "single", + "maxdepth": 3, + "maxstatements": 15, + "maxlen": 80, + "eqnull": true, + "funcscope": true, + "node": true +} diff --git a/js/controllers/home.js b/js/controllers/home.js index 2fffeb661..46ac43ac0 100644 --- a/js/controllers/home.js +++ b/js/controllers/home.js @@ -3,15 +3,7 @@ angular.module('copayApp.controllers').controller('HomeController', function($scope, $rootScope, $location, notification, controllerUtils, pluginManager) { controllerUtils.redirIfLogged(); - $scope.retreiving = true; - copay.Identity.anyProfile({ - pluginManager: pluginManager, - }, function(any) { - $scope.retreiving = false; - if (!any) - $location.path('/createProfile'); - }); - + $scope.retreiving = false; $scope.openProfile = function(form) { if (form && form.$invalid) { diff --git a/js/models/Identity.js b/js/models/Identity.js index c7912e35f..68ad4ebe5 100644 --- a/js/models/Identity.js +++ b/js/models/Identity.js @@ -4,6 +4,9 @@ var preconditions = require('preconditions').singleton(); var _ = require('underscore'); var log = require('../log'); +var querystring = require('querystring'); +var request = require('request'); +var cryptoUtil = require('../util/crypto'); var version = require('../../version').version; var TxProposals = require('./TxProposals'); var PublicKeyRing = require('./PublicKeyRing'); @@ -27,6 +30,8 @@ var Storage = module.exports.Storage = require('./Storage'); function Identity(password, opts) { preconditions.checkArgument(opts); + opts = _.extend({}, opts); + this.request = opts.request || request; this.storage = Identity._getStorage(opts, password); this.networkOpts = { 'livenet': opts.network.livenet, @@ -37,6 +42,7 @@ function Identity(password, opts) { 'testnet': opts.network.testnet, }; + this.insightSaveOpts = opts.insightSave || {}; this.walletDefaults = opts.walletDefaults || {}; this.version = opts.version || version; @@ -50,8 +56,6 @@ Identity._createProfile = function(email, password, storage, cb) { Profile.create(email, password, storage, cb); }; - - Identity._newStorage = function(opts) { return new Storage(opts); }; @@ -82,8 +86,6 @@ Identity._newAsync = function(opts) { return new Async(opts); }; - - Identity._getStorage = function(opts, password) { var storageOpts = {}; @@ -150,10 +152,16 @@ Identity.create = function(email, password, opts, cb) { requiredCopayers: 1, totalCopayers: 1, password: password, - name: 'general', + name: 'general' }); iden.createWallet(wopts, function(err, w) { - return cb(null, iden, w); + if (err) { + return cb(err); + } + iden.registerOnInsight(iden.insightSaveOpts, function(error) { + // Ignore error + return cb(null, iden, w); + }); }); }); }; @@ -186,7 +194,12 @@ Identity.open = function(email, password, opts, cb) { var iden = new Identity(password, opts); Identity._openProfile(email, password, iden.storage, function(err, profile) { - if (err) return cb(err); + if (err) { + if (err.message && err.message.indexOf('PNOTFOUND') !== -1) { + return Identity.readFromInsight(email, password, opts, cb); + } + return cb(err); + } iden.profile = profile; var wids = _.pluck(iden.listWallets(), 'id'); @@ -335,15 +348,14 @@ Identity.prototype.closeWallet = function(wid, cb) { }); }; - - -/** - * @desc Return a base64 encrypted version of the wallet - * @return {string} base64 encoded string - */ -Identity.import = function(str, password, opts, cb) { +Identity.importFromJson = function(str, password, opts, cb) { preconditions.checkArgument(str); - var json = JSON.parse(str); + var json; + try { + json = JSON.parse(str); + } catch (e) { + return cb('Unable to retrieve json from string', str); + } if (!_.isNumber(json.iterations)) return cb('BADSTR: Missing iterations'); @@ -351,25 +363,34 @@ Identity.import = function(str, password, opts, cb) { if (!json.profile) return cb('BADSTR: Missing profile'); - var iden = new Identity(password, opts); iden.profile = Profile.import(json.profile, password, iden.storage); json.wallets = json.wallets || {}; + var walletInfoBackup = iden.profile.walletInfos; + iden.profile.walletInfos = {}; - var l = json.wallets.length, + var l = _.size(json.wallets), i = 0; if (!l) return cb(null, iden); - _.each(this.wallets, function(wstr) { - iden.importWallet(wstr, password, skipFields, function(err, w) { + _.each(json.wallets, function(wstr) { + iden.importWallet(wstr, password, opts.skipFields, function(err, w) { if (err) return cb(err); log.debug('Wallet ' + w.getId() + ' imported'); - if (++i == l) - iden.store(cb); + if (++i == l) { + iden.profile.walletInfos = walletInfoBackup; + iden.store(opts, function(err) { + if (err) { + return cb(err); + } else { + return cb(null, iden, iden.openWallets[0]); + } + }); + } }) }); }; @@ -378,7 +399,7 @@ Identity.import = function(str, password, opts, cb) { * @desc Return JSON with base64 encoded strings for wallets and profile, and iteration count * @return {string} Stringify JSON */ -Identity.prototype.export = function() { +Identity.prototype.exportAsJson = function() { var ret = {}; ret.iterations = this.storage.iterations; ret.profile = this.profile.export(); @@ -470,8 +491,59 @@ Identity.prototype.createWallet = function(opts, cb) { this.addWallet(w, function(err) { if (err) return cb(err); self.openWallets.push(w); - w.netStart(); - return cb(err, w); + self.triggerInsightSave(self.insightSaveOpts, function(error) { + // Ignore error + w.netStart(); + return cb(null, w); + }); + }); +}; + +Identity.readFromInsight = function(email, password, opts, callback) { + var key = cryptoUtil.kdf(password, email); + var secret = cryptoUtil.kdf(key, password); + var useRequest = opts.request || request; + var encodedEmail = encodeURIComponent(email); + var retrieveUrl = opts.retrieveUrl || 'http://localhost:3001/api/email/retrieve/' + encodedEmail; + useRequest.get(retrieveUrl + '?' + querystring.encode({secret: secret}), + function(err, response, body) { + if (err) { + return callback('Connection error'); + } + if (response.statusCode !== 200) { + return callback('Connection error'); + } + var decryptedJson = cryptoUtil.decrypt(key, body); + if (!decryptedJson) { + return callback('Internal Error'); + } + return Identity.importFromJson(decryptedJson, password, opts, callback); + } + ); +}; + +Identity.prototype.triggerInsightSave = Identity.prototype.registerOnInsight = function(opts, callback) { + var password = this.profile.password; + var key = cryptoUtil.kdf(password, this.profile.email); + var secret = cryptoUtil.kdf(key, password); + var exportData = this.exportAsJson + var record = cryptoUtil.encrypt(key, this.exportAsJson()); + var registerUrl = opts.registerUrl || 'http://localhost:3001/api/email/register'; + this.request.post({ + url: registerUrl, + body: querystring.encode({ + email: this.profile.email, + secret: secret, + record: record + }) + }, function(err, response, body) { + if (err) { + return callback('Connection error'); + } + if (response.statusCode !== 200) { + return callback('Unable to store data on insight'); + } + return callback(); }); }; @@ -486,11 +558,8 @@ Identity.prototype.addWallet = function(wallet, cb) { self.profile.addWallet(wallet.getId(), { name: wallet.name }, function(err) { - if (err) return cb(err); - wallet.store(function(err) { - return cb(err); - }); + cb(); }); }; diff --git a/js/models/Profile.js b/js/models/Profile.js index c050cdedc..babab434c 100644 --- a/js/models/Profile.js +++ b/js/models/Profile.js @@ -10,6 +10,7 @@ function Profile(info, storage) { preconditions.checkArgument(storage); preconditions.checkArgument(storage.setPassword, 'bad storage'); + this.password = info.password; this.hash = info.hash; this.email = info.email; this.extra = info.extra || {}; @@ -19,8 +20,8 @@ function Profile(info, storage) { this.storage = storage; }; -Profile.hash = function(email, password) { - return bitcore.util.sha256ripe160(email + password).toString('hex'); +Profile.hash = function(email) { + return bitcore.util.sha256ripe160(email).toString('hex'); }; Profile.key = function(hash) { @@ -36,6 +37,7 @@ Profile.create = function(email, password, storage, cb) { var p = new Profile({ email: email, + password: password, hash: Profile.hash(email, password), }, storage); p.store({}, function(err) { @@ -67,7 +69,7 @@ Profile.open = function(email, password, storage, cb) { }; Profile.prototype.toObj = function() { - return _.clone(_.pick(this, 'hash', 'email', 'extra', 'walletInfos')); + return _.clone(_.pick(this, 'password', 'hash', 'email', 'extra', 'walletInfos')); }; diff --git a/js/services/pluginManager.js b/js/services/pluginManager.js index 1b85d15a6..2a86a03c9 100644 --- a/js/services/pluginManager.js +++ b/js/services/pluginManager.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.services').factory('pluginManager', function(angularLoad){ +angular.module('copayApp.services').factory('pluginManager', function(angularLoad) { var pm = new copay.PluginManager(config); var scripts = pm.scripts; diff --git a/js/util/crypto.js b/js/util/crypto.js new file mode 100644 index 000000000..dd5cf8aff --- /dev/null +++ b/js/util/crypto.js @@ -0,0 +1,44 @@ +/** + * Small module for some helpers that wrap CryptoJS with some good practices. + */ +var sjcl = require('sjcl'); +var log = require('../log.js'); +var _ = require('underscore'); + +var SALT = 'copay random string NWRlNmExMTE4NzIzYzYyYWMwODU1MTdkN'; +var SEPARATOR = '&'; +var defaultOptions = { + adata: '', + cipher: 'aes', + ks: 128, + iter: 2000, + mode: 'ccm', + ts: 64 +}; + +module.exports = { + + kdf: function(value1, value2) { + return sjcl.codec.base64.fromBits(sjcl.misc.pbkdf2(value1 + value2, SALT)); + }, + + /** + * Encrypts symmetrically using a passphrase + */ + encrypt: function(key, message) { + return sjcl.encrypt(key, message); + }, + + /** + * Decrypts symmetrically using a passphrase + */ + decrypt: function(key, cypher) { + var output = {}; + try { + return sjcl.decrypt(key, cypher); + } catch (e) { + log.error('Decryption failed due to error: ' + e.message); + return null; + } + } +}; diff --git a/package.json b/package.json index ba0e576e3..81a0d9141 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "inherits": "^2.0.1", "optimist": "^0.6.1", "preconditions": "^1.0.7", + "querystring": "^0.2.0", "request": "^2.40.0", "underscore": "^1.7.0" }, diff --git a/plugins/LocalStorage.js b/plugins/LocalStorage.js index edad56006..d0fcebe84 100644 --- a/plugins/LocalStorage.js +++ b/plugins/LocalStorage.js @@ -7,7 +7,6 @@ function LocalStorage() { LocalStorage.prototype.init = function() { }; - LocalStorage.prototype.getItem = function(k,cb) { return cb(localStorage.getItem(k)); }; @@ -37,5 +36,4 @@ LocalStorage.prototype.allKeys = function(cb) { return cb(ret); }; - module.exports = LocalStorage; diff --git a/util/build.js b/util/build.js index 9732e1a92..267396631 100644 --- a/util/build.js +++ b/util/build.js @@ -46,6 +46,7 @@ var createBundle = function(opts) { expose: 'request' }); b.require('underscore'); + b.require('querystring'); b.require('assert'); b.require('preconditions'); @@ -86,6 +87,9 @@ var createBundle = function(opts) { b.require('./js/models/PluginManager', { expose: '../js/models/PluginManager' }); + b.require('./js/util/crypto', { + expose: '../util/crypto' + }); if (!opts.disablePlugins) { b.require('./plugins/GoogleDrive', {