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) {