diff --git a/app-template/config-template.xml b/app-template/config-template.xml index 8031c8110..39b67d212 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -72,6 +72,8 @@ + + diff --git a/src/js/models/profile.js b/src/js/models/profile.js index 74b0c33b9..7690d1c2d 100644 --- a/src/js/models/profile.js +++ b/src/js/models/profile.js @@ -9,12 +9,12 @@ function Profile() { this.version = '1.0.0'; }; -Profile.create = function(opts) { - opts = opts || {}; +Profile.create = function(appVersion) { var x = new Profile(); + x.appVersion = appVersion; x.createdOn = Date.now(); - x.credentials = opts.credentials || []; + x.credentials = []; x.disclaimerAccepted = true; x.checked = {}; return x; @@ -23,6 +23,7 @@ Profile.create = function(opts) { Profile.fromObj = function(obj) { var x = new Profile(); + x.appVersion = obj.appVersion; x.createdOn = obj.createdOn; x.credentials = obj.credentials; x.disclaimerAccepted = obj.disclaimerAccepted; @@ -62,6 +63,39 @@ Profile.prototype.isDeviceChecked = function(ua) { return this.checkedUA == ua; }; +/** + * + * @param {Profile} other + */ +Profile.prototype.merge = function(other) { + + var newCredentials = []; + var otherCredentialsLength = other.credentials.length; + var thisProfile = this; + + other.credentials.forEach(function(otherCredential) { + var credentialExists = false; + thisProfile.credentials.forEach(function(thisCredential) { + if (otherCredential.walletId === thisCredential.walletId) { + credentialExists = true; + } + }); + if (!credentialExists) { + newCredentials.push(otherCredential); + } + }); + + Array.prototype.push.apply(this.credentials, newCredentials); +}; + +/** + * It's a simple operation, but it means that all the profile logic stays + * in this file. + * @param {string} appVersion - ie "4.11.0" + */ +Profile.prototype.setAppVersion = function(appVersion) { + this.appVersion = appVersion; +} Profile.prototype.setChecked = function(ua, walletId) { if (this.checkedUA != ua) { diff --git a/src/js/services/desktopSecureStorageService.js b/src/js/services/desktopSecureStorageService.js new file mode 100644 index 000000000..6e148da2c --- /dev/null +++ b/src/js/services/desktopSecureStorageService.js @@ -0,0 +1,6 @@ +'use strict'; + +angular.module('copayApp.services').factory('desktopSecureStorageService', function($log) { + // Placeholder + return {}; +}); \ No newline at end of file diff --git a/src/js/services/localStorage.js b/src/js/services/localStorage.js index c772b7eef..ba0db231b 100644 --- a/src/js/services/localStorage.js +++ b/src/js/services/localStorage.js @@ -20,8 +20,7 @@ angular.module('copayApp.services') if (isChromeApp || isNW) { chrome.storage.local.get(k, function(data) { - //TODO check for errors - return cb(null, data[k]); + return cb(chrome.runtime.lastError, data[k]); }); } else { return cb(null, ls.getItem(k)); @@ -56,16 +55,24 @@ angular.module('copayApp.services') obj[k] = v; - chrome.storage.local.set(obj, cb); + chrome.storage.local.set(obj, function(){ + cb(chrome.runtime.lastError); + }); } else { - ls.setItem(k, v); + try { + ls.setItem(k, v); + } catch (e) { + return cb(e); + } return cb(); } }; root.remove = function(k, cb) { if (isChromeApp || isNW) { - chrome.storage.local.remove(k, cb); + chrome.storage.local.remove(k, function(){ + cb(chrome.runtime.lastError); + }); } else { ls.removeItem(k); return cb(); diff --git a/src/js/services/mobileSecureStorageService.js b/src/js/services/mobileSecureStorageService.js new file mode 100644 index 000000000..f9994fdf8 --- /dev/null +++ b/src/js/services/mobileSecureStorageService.js @@ -0,0 +1,88 @@ +'use strict'; + +angular.module('copayApp.services').factory('mobileSecureStorageService', function($log, appConfigService, platformInfo) { + var root = {}; + + var isReady = false; + var initialisationFailed = false; + var pending = []; + + var storage = null; + + if (platformInfo.isCordova) { + storage = new cordova.plugins.SecureStorage( + function () { + isReady = true; + for (var i = 0; i < pending.length; i++) { + pending[i](); + } + pending = []; + }, + function (error) { + initialisationFailed = true; + }, + appConfigService.packageNameId); + } + + root.get = function(key, cb) { + + if (!platformInfo.isMobile) { + cb(new Error('mobileSecureStorageService is only available on mobile.')); + return; + } + + if (!isReady) { + if (initialisationFailed) { + cb(new Error('mobileSecureStorageService initialisation failed.')); + } else { + pending.push(function(){ root.get(key, cb); }); + } + return; + } + + storage.get( + function (value) { + cb(null, value); + }, + function (error) { + $log.debug('mss get failed. ' + error); + if (error.message === 'Failure in SecureStorage.get() - The specified item could not be found in the keychain' || // iOS + error.message === 'Key [_SS_profile] not found.') { // Android + // The callback expects no error, but also no value, if it cannot be found. + cb(null, null); + } else { + cb(new Error(error)); + } + }, + key); + }; + + root.set = function(key, value, cb) { + + if (!platformInfo.isMobile) { + cb(new Error('mobileSecureStorageService is only available on mobile.')); + } + + if (!isReady) { + if (initialisationFailed) { + cb(new Error('mobileSecureStorageService initialisation failed.')); + } else { + pending.push(function(){ root.set(key, value, cb); }); + } + return; + } + + storage.set( + function (value) { + cb(); + }, + function (error) { + cb(new Error(error)); + }, + key, value); + + }; + + return root; +}); + diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index dac88169f..25f2a6852 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -706,7 +706,7 @@ angular.module('copayApp.services') configService.get(function(err) { if (err) $log.debug(err); - var p = Profile.create(); + var p = Profile.create(appConfigService.version); storageService.storeNewProfile(p, function(err) { if (err) return cb(err); root.bindProfile(p, function(err) { diff --git a/src/js/services/secureStorageService.js b/src/js/services/secureStorageService.js new file mode 100644 index 000000000..c066109c2 --- /dev/null +++ b/src/js/services/secureStorageService.js @@ -0,0 +1,32 @@ +'use strict'; + +angular.module('copayApp.services').factory('secureStorageService', function(desktopSecureStorageService, localStorageService, $log, mobileSecureStorageService, platformInfo) { + var root = {}; + + // To make wrong code look wrong + function alteredKeyIndicatingDesireForSecureStorage(key) { + return key + ":desiredSecure"; + } + + root.get = function(k, cb) { + if (platformInfo.isMobile) { + mobileSecureStorageService.get(k, cb); + } else if (platformInfo.isNW) { + desktopSecureStorageService.get(k, cb); + } else { // Browser + localStorageService.get(alteredKeyIndicatingDesireForSecureStorage(k), cb); + } + } + + root.set = function(k, v, cb) { + if (platformInfo.isMobile) { + mobileSecureStorageService.set(k, v, cb); + } else if (platformInfo.isNW) { + desktopSecureStorageService.set(k, v, cb); + } else { // Browser + localStorageService.set(alteredKeyIndicatingDesireForSecureStorage(k), v, cb); + } + } + + return root; +}); \ No newline at end of file diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index 3d1ecfeef..2dc3d7511 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -1,6 +1,6 @@ 'use strict'; angular.module('copayApp.services') - .factory('storageService', function(logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, platformInfo, $timeout) { + .factory('storageService', function(appConfigService, logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, platformInfo, secureStorageService, $timeout) { var root = {}; var storage; @@ -116,34 +116,107 @@ angular.module('copayApp.services') }; root.storeNewProfile = function(profile, cb) { - storage.create('profile', profile.toObj(), cb); + secureStorageService.set('profile', profile.toObj(), cb); }; root.storeProfile = function(profile, cb) { - storage.set('profile', profile.toObj(), cb); + secureStorageService.set('profile', profile.toObj(), cb); }; - root.getProfile = function(cb) { - storage.get('profile', function(err, str) { - if (err || !str) - return cb(err); + /** + * @callback getProfileCallback + * @param {Error} error - falsy if profile not found. + * @param {Profile} profile - falsy if error or profile not found. + */ - decryptOnMobile(str, function(err, str) { - if (err) return cb(err); - var p, err; - try { - p = Profile.fromString(str); - } catch (e) { - $log.debug('Could not read profile:', e); - err = new Error('Could not read profile:' + p); + /** + * + * @param {Profile} oldProfile + * @param {Profile} secureProfile - may be falsy if no secure profile found. + * @param {getProfileCallback} cb + */ + function _migrateProfiles(oldProfile, secureProfile, cb) { + var newProfile; + + if (secureProfile) { + secureProfile.merge(oldProfile); + newProfile = secureProfile; + } else { + newProfile = oldProfile; + newProfile.setAppVersion(appConfigService.version); + } + + root.storeNewProfile(newProfile, function(storeErr) { + if (storeErr) { + cb(storeErr, null); + return; + } + + storage.remove('profile', function(removeErr){ + if (removeErr) { + cb(removeErr, null); + return; } - return cb(err, p); + + cb(null, newProfile); }); + }); }; - root.deleteProfile = function(cb) { - storage.remove('profile', cb); + /** + * + * @param {getProfileCallback} cb + */ + root.getProfile = function(cb) { + secureStorageService.get('profile', function(secureErr, secureStr) { + var secureProfile; + var oldProfile; + + if (secureErr) { + return cb(secureErr, null); + } + + if (secureStr) { + try { + secureProfile = Profile.fromString(secureStr); + $log.debug('profile: ' + JSON.stringify(secureProfile)); + } catch (e) { + $log.error(e); + return cb(e, null); + } + } + + storage.get('profile', function(getErr, getStr) { + if (getErr) { + return cb(getErr); + } + + if (!getStr) { + if (secureProfile) { + return cb(null, secureProfile); + } else { + // No profiles found. No errors either. + return cb(null, null); + } + } + + decryptOnMobile(getStr, function(err, str) { + if (err) return cb(err); + var p, err; + try { + oldProfile = Profile.fromString(str); + } catch (e) { + $log.debug('Could not read profile:', e); + err = new Error('Could not read profile.'); + return(err, null); + } + + _migrateProfiles(oldProfile, secureProfile, cb); + + }); + }); + }); }; root.setFeedbackInfo = function(feedbackValues, cb) {