diff --git a/background/background.html b/background/background.html
index 523dcf1..194b47c 100644
--- a/background/background.html
+++ b/background/background.html
@@ -1,4 +1,6 @@
+
+
@@ -22,6 +24,8 @@
+
+
diff --git a/background/firebase_sync.js b/background/firebase_sync.js
new file mode 100644
index 0000000..4c79a17
--- /dev/null
+++ b/background/firebase_sync.js
@@ -0,0 +1,78 @@
+var FirebaseSync = function() {
+
+ var dataRef = new Firebase('https://gombot.firebaseIO.com');
+
+ var authClient = new FirebaseAuthClient(dataRef, authClientCallback);
+
+ var usersRef = dataRef.child('users');
+
+ var currentUser;
+
+ chrome.webRequest.onBeforeSendHeaders.addListener(
+ function(details) {
+ // Remove Origin header if it exists
+ for (var i = 0; i < details.requestHeaders.length; ++i) {
+ if (details.requestHeaders[i].name === 'Origin') {
+ details.requestHeaders.splice(i, 1);
+ break;
+ }
+ }
+ // Remove Referer header if it exists
+ for (var i = 0; i < details.requestHeaders.length; ++i) {
+ if (details.requestHeaders[i].name === 'Referer') {
+ details.requestHeaders.splice(i, 1);
+ }
+ }
+ // Add a Referer header for gombot.org
+ details.requestHeaders.push({ name: 'Referer', value: 'https://gombot.org/'});
+ return {requestHeaders: details.requestHeaders};
+ },
+ { urls: ["https://auth.firebase.com/*"] },
+ ["blocking", "requestHeaders"]
+ );
+
+
+ function authClientCallback(error, user) {
+ if (error) {
+ // an error occurred while attempting login
+ console.log(error);
+ } else if (user) {
+ currentUser = user;
+ usersRef.child(user.id).on('value', getUserData);
+ // user authenticated with Firebase
+ console.log('User ID: ' + user.id + ', Provider: ' + user.provider);
+ } else {
+ if (currentUser) usersRef.child(currentUser.id).off('value', getUserData);
+ currentUser = null;
+ // user is logged out
+ }
+ }
+
+ function create(email, password, options) {
+ authClient.createUser(email, password, function(error, user) {
+ if (!error) {
+ console.log('User Id: ' + user.id + ', Email: ' + user.email);
+ } else {
+ console.log("FirebaseSync.create error:", error);
+ }
+ });
+ }
+
+ function login(email, password, options) {
+ authClient.login('password', {
+ email: email,
+ password: password,
+ rememberMe: true
+ });
+ }
+
+ function getUserData(data) {
+ console.log("User data:", data.val());
+ }
+
+ return {
+ create: create,
+ login: login,
+ getUserData: getUserData
+ };
+};
\ No newline at end of file
diff --git a/background/gombot.js b/background/gombot.js
index 197cc1d..c339631 100644
--- a/background/gombot.js
+++ b/background/gombot.js
@@ -45,9 +45,10 @@ var _Gombot = function(importedModules, Gombot) {
Gombot.TldService = getModule("TldService")(getModule("Tld"), getModule("Uri"));
Gombot.SiteConfigs = getModule("SiteConfigs");
Gombot.Realms = getModule("Realms")(Gombot, Gombot.SiteConfigs, getModule("Uri"));
- Gombot.Storage = getModule("Storage")(Backbone, _, Gombot.LocalStorage); // defined by backbone.localStorage.js
- Gombot.GombotClient = getModule("GombotClient");
- Gombot.Sync = getModule("GombotSync")(Gombot, Backbone, _);
+ Gombot.Storage = getModule("Storage")(Backbone, _, Gombot.LocalStorage); // local sync; defined by backbone.localStorage.js
+ //Gombot.GombotClient = getModule("GombotClient");
+ //Gombot.Sync = getModule("GombotSync")(Gombot, Backbone, _); // original sync using our api
+ //Gombot.FirebaseSync = getModule("FirebaseSync")(Gombot); // sync using firebase
Gombot.LoginCredential = getModule("LoginCredential")(Gombot, Backbone, _);
Gombot.LoginCredentialCollection = getModule("LoginCredentialCollection")(Backbone, _, Gombot.LoginCredential); // LoginCredential need to be initialized
Gombot.CapturedCredentialStorage = getModule("CapturedCredentialStorage")(Gombot, getModule("Uri"));
@@ -55,8 +56,12 @@ var _Gombot = function(importedModules, Gombot) {
Gombot.AccountManager = getModule("AccountManager")(Gombot, _);
Gombot.CommandHandler = getModule("CommandHandler")(Gombot, Gombot.Messaging, _);
Gombot.Pages = getModule("Pages")(Gombot);
- Gombot.InfobarManager = getModule("InfobarManager");
- Gombot.Infobars = getModule("Infobars")(Gombot);
+ Gombot.Crypto = getModule("GombotCrypto");
+ Gombot.User = getModule("User")(Backbone, _, Gombot);
+ if (typeof chrome !== "undefined") {
+ Gombot.InfobarManager = getModule("InfobarManager");
+ Gombot.Infobars = getModule("Infobars")(Gombot);
+ }
var currentUser = null;
Gombot.getCurrentUser = function() {
@@ -73,19 +78,24 @@ var _Gombot = function(importedModules, Gombot) {
currentUser.destroy({ localOnly: true, success: function() { currentUser = null; callback(); }});
};
- new Gombot.Storage("users", function(store) {
- Gombot.User = getModule("User")(Backbone, _, Gombot, store);
- Gombot.UserCollection = getModule("UserCollection")(Backbone, _, Gombot, store);
- checkFirstRun();
- });
-
- function checkFirstRun() {
- Gombot.LocalStorage.getItem("firstRun", function(firstRun) {
- initGombot(firstRun);
+ Gombot.init = function(options) {
+ options = options || {};
+ options.storeName = options.storeName || "users";
+ options.callback = options.callback || checkFirstRun;
+ new Gombot.Storage(options.storeName, function(store) {
+ Gombot.SyncAdapter = getModule("SyncAdapter")(Gombot, Gombot.Crypto, store, _);
+ Gombot.UserCollection = getModule("UserCollection")(Backbone, _, Gombot, store);
+ options.callback();
});
}
- function initGombot(firstRun) {
+ function checkFirstRun() {
+ Gombot.LocalStorage.getItem("firstRun", function(firstRun) {
+ fetchUsers(firstRun);
+ });
+ }
+
+ function fetchUsers(firstRun) {
Gombot.users = new Gombot.UserCollection();
Gombot.users.fetch({
success: function() {
@@ -102,6 +112,7 @@ var _Gombot = function(importedModules, Gombot) {
if (typeof module !== "undefined" && module.exports) {
module.exports = _Gombot; // export namespace constructor, for Firefox
-} else { // otherwise, just create the global Gombot namespace
+} else { // otherwise, just create the global Gombot namespace and init
var Gombot = _Gombot({});
+ Gombot.init();
}
diff --git a/background/main.js b/background/main.js
index fd9ab94..fd14c7b 100644
--- a/background/main.js
+++ b/background/main.js
@@ -47,33 +47,9 @@ windows.on('open', function(window) {
addToolbarButton();
});
-/** Load all Gombot modules **/
-
-var gombotModules = {
- Backbone: require("./lib/backbone"),
- _ : require("./lib/underscore"),
- Messaging: require("./messaging"),
- LocalStorage: require("./local_storage"),
- Tld: require("./lib/tld.js"),
- Uri: require("./lib/jsuri"),
- TldService: require("./tld_service"),
- SiteConfigs: require("./site_configs"),
- Realms: require("./realms"),
- Storage: require("./storage"),
- GombotClient: require("./client/client"),
- GombotSync: require("./gombot_sync"),
- LoginCredential: require("./models/login_credential"),
- LoginCredentialCollection: require("./collections/login_credential_collection"),
- CapturedCredentialStorage: require("./captured_credential_storage"),
- Linker: require("./linker"),
- CommandHandler: require("./command_handler"),
- User: require("./models/user"),
- UserCollection: require("./collections/user_collection"),
- AccountManager: require("./account_manager"),
- Pages: require("./pages")
-};
-
-var Gombot = require("./gombot")(gombotModules);
+var GombotModules = require("./modules");
+var Gombot = require("./gombot")(GombotModules);
+Gombot.init();
/** Tpp panel stuff **/
@@ -109,3 +85,5 @@ pageMod.PageMod({
Gombot.Messaging.registerPageModWorker(worker);
}
});
+
+exports.gombot = Gombot;
diff --git a/background/models/user.js b/background/models/user.js
index 59e81f3..ad09564 100644
--- a/background/models/user.js
+++ b/background/models/user.js
@@ -1,12 +1,9 @@
-var User = function(Backbone, _, Gombot, LocalStorage) {
+var User = function(Backbone, _, Gombot) {
const USER_DATA_VERSIONS = [
"identity.mozilla.com/gombot/v1/userData"
];
- var GombotSync = Gombot.Sync,
- LoginCredentialCollection = Gombot.LoginCredentialCollection;
-
// attributes should be something like:
// {
// "version": "identity.mozilla.com/gombot/v1/userData",
@@ -38,8 +35,6 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
disabledSites: {}
},
- localStorage: LocalStorage,
-
initialize: function() {
Backbone.Model.prototype.initialize.apply(this, arguments);
this.addSyncListener(this.get("logins"));
@@ -64,6 +59,7 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
},
isAuthenticated: function() {
+ return false;
return this.client && ((this.client.isAuthenticated && this.client.isAuthenticated()) || (this.client.keys && this.client.user));
},
@@ -71,60 +67,61 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
// call model.toJSON({ encrypted: true, ciphertext: })
// Other toJSON() creates a standard plaintext representation of a User object
toJSON: function(args) {
- var result;
- args = args || {};
- if (args.ciphertext) {
- result = { ciphertext: args.ciphertext, updated: this.updated, id: this.id, email: this.get("email"), version: this.get("version") };
- if (this.isAuthenticated()) _.extend(result, { client: this.client.toJSON() });
- return result;
- }
- else {
- result = Backbone.Model.prototype.toJSON.apply(this, arguments);
- return _.extend(result, { logins: this.get("logins").toJSON() });
+ var result = Backbone.Model.prototype.toJSON.apply(this, arguments);
+ return _.extend(result, { logins: this.get("logins").toJSON() });
+ },
+
+ // Returns an object containing key/values of data that will be
+ // stored in plaintext with an encrypted copy of this model's data.
+ // The metadata should not contain any information that is intended
+ // to be stored encrypted at rest.
+ getMetadata: function() {
+ return {
+ id: this.id,
+ email: this.get("email"),
+ version: this.get("version"),
+ updated: this.updated
}
},
parse: function(resp) {
- if (resp.ciphertext) this.ciphertext = resp.ciphertext;
if (resp.updated) this.updated = resp.updated;
- if (resp.client) this.client = resp.client;
- delete resp.ciphertext;
delete resp.updated;
- delete resp.client;
return resp;
},
sync: function(method, model, options) {
- var self = this;
- var success = function(resp) {
- var s = options.success;
- options.success = function(model, resp, options) {
- console.log("User.sync finished method="+method+" resp="+JSON.stringify(resp)+" model="+JSON.stringify(model));
- // resp.data is returned by GombotSync calls with plaintext user data
- if (s) s(model, resp.data || {}, options);
- }
- if (resp.updated) self.updated = resp.updated;
- // ciphertext in resp indicates we need to write it out to local storage
- if (resp.ciphertext) {
- if (method === "read") {
- self.save(resp.data, _.extend(options, { localOnly: true, ciphertext: resp.ciphertext }));
- } else {
- console.log("localSync method="+method);
- Backbone.localSync(method, model, _.extend(options, { ciphertext: resp.ciphertext }));
- }
- } else if (options.success) {
- options.success(model, resp, options);
- }
- };
- var error = function(args) {
- if (options.error) options.error(args);
- };
- var o = _.clone(options);
- if (options.localOnly) {
- Backbone.localSync(method, model, options);
- } else {
- GombotSync.sync(method, model, _.extend(o,{ success: success, error: error }));
- }
+ Gombot.SyncAdapter.sync(method, model, options);
+ // var self = this;
+ // var success = function(resp) {
+ // var s = options.success;
+ // options.success = function(model, resp, options) {
+ // console.log("User.sync finished method="+method+" resp="+JSON.stringify(resp)+" model="+JSON.stringify(model));
+ // // resp.data is returned by GombotSync calls with plaintext user data
+ // if (s) s(model, resp.data || {}, options);
+ // }
+ // if (resp.updated) self.updated = resp.updated;
+ // // ciphertext in resp indicates we need to write it out to local storage
+ // if (resp.ciphertext) {
+ // if (method === "read") {
+ // self.save(resp.data, _.extend(options, { localOnly: true, ciphertext: resp.ciphertext }));
+ // } else {
+ // console.log("localSync method="+method);
+ // Backbone.localSync(method, model, _.extend(options, { ciphertext: resp.ciphertext }));
+ // }
+ // } else if (options.success) {
+ // options.success(model, resp, options);
+ // }
+ // };
+ // var error = function(args) {
+ // if (options.error) options.error(args);
+ // };
+ // var o = _.clone(options);
+ // if (options.localOnly) {
+ // Backbone.localSync(method, model, options);
+ // } else {
+ // GombotSync.sync(method, model, _.extend(o,{ success: success, error: error }));
+ // }
},
set: function(key, val, options) {
@@ -137,9 +134,9 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
} else {
(attributes = {})[key] = val;
}
- if (attributes.logins !== undefined && !(attributes.logins instanceof LoginCredentialCollection)) {
+ if (attributes.logins !== undefined && !(attributes.logins instanceof Gombot.LoginCredentialCollection)) {
logins = attributes.logins;
- attributes.logins = this.get("logins") || new LoginCredentialCollection();
+ attributes.logins = this.get("logins") || new Gombot.LoginCredentialCollection();
}
result = Backbone.Model.prototype.set.call(this, attributes, options);
if (result && logins) {
diff --git a/background/modules.js b/background/modules.js
new file mode 100644
index 0000000..92edfca
--- /dev/null
+++ b/background/modules.js
@@ -0,0 +1,29 @@
+/** Load all Gombot modules **/
+
+var GombotModules = {
+ Backbone: require("./lib/backbone"),
+ _ : require("./lib/underscore"),
+ Messaging: require("./messaging"),
+ LocalStorage: require("./local_storage"),
+ Tld: require("./lib/tld.js"),
+ Uri: require("./lib/jsuri"),
+ TldService: require("./tld_service"),
+ SiteConfigs: require("./site_configs"),
+ Realms: require("./realms"),
+ Storage: require("./storage"),
+ //GombotClient: require("./client/client"),
+ //GombotSync: require("./gombot_sync"),
+ LoginCredential: require("./models/login_credential"),
+ LoginCredentialCollection: require("./collections/login_credential_collection"),
+ CapturedCredentialStorage: require("./captured_credential_storage"),
+ Linker: require("./linker"),
+ CommandHandler: require("./command_handler"),
+ User: require("./models/user"),
+ UserCollection: require("./collections/user_collection"),
+ AccountManager: require("./account_manager"),
+ Pages: require("./pages"),
+ GombotCrypto: require("./client/crypto"),
+ SyncAdapter: require("./sync_adapter")
+};
+
+module.exports = GombotModules;
\ No newline at end of file
diff --git a/background/sync_adapter.js b/background/sync_adapter.js
new file mode 100644
index 0000000..eecbc60
--- /dev/null
+++ b/background/sync_adapter.js
@@ -0,0 +1,121 @@
+var SyncAdapter = function(Gombot, GombotCrypto, SyncStrategy, _) {
+
+ // TODO: seed from an actual entropy source
+ GombotCrypto.seed("oiqwjeciouqh3c89cnkjasdcnasjf84u9jcuqwiench734fhujhwuqhf73f73fhsdjfhasdf734fhdkcnuf"+(new Date().toString()) ,function(err) {
+ if (err) console.log("GombotCrypto.seed error:", err);
+ });
+
+ function maybeHandleError(handler, err) {
+ if (err) {
+ console.log("SyncMediator error", err);
+ if (handler) handler(err);
+ return true;
+ }
+ else return false;
+ }
+
+ function encryptModel(model, keys, options) {
+ GombotCrypto.encrypt(keys, JSON.stringify(model), function(err, ciphertext) {
+ if (err) return maybeHandleError(options.error, err);
+ options.success(ciphertext);
+ });
+ }
+
+ function decryptModelData(ciphertext, keys, options) {
+ GombotCrypto.decrypt(keys, ciphertext, function(err, json) {
+ var modelData;
+ try {
+ if (!err) {
+ modelData = JSON.parse(json);
+ }
+ } catch (e) {
+ err = new Error("Could not parse decrypted JSON:", json);
+ }
+ if (err) return maybeHandleError(options.error, err);
+ options.success(modelData);
+ });
+ }
+
+ function createCryptoProxyForModel(model, keys) {
+ var clone = _.clone(model);
+ return _.extend(clone, {
+ toJSON: function(options) {
+ // missing options means synchronous response to underyling object
+ if (!options || !options.success) return model.toJSON();
+ var o = _.clone(options);
+ encryptModel(model, keys, _.extend(o, { success: function(ciphertext) {
+ options.success(_.extend(model.getMetadata(), {
+ ciphertext: ciphertext // encrypted plaintext model
+ }));
+ }}));
+ },
+ parse: function(resp, options) {
+ var o = _.clone(options),
+ ciphertext = resp.ciphertext;
+ decryptModelData(ciphertext, keys, _.extend(o, { success: function(modelData) {
+ delete resp.ciphertext;
+ options.success(_.extend(resp, modelData));
+ }}));
+ }
+ });
+ }
+
+ function getCryptoProxyForModel(model, options) {
+ if (model.cryptoProxy) return options.success(model.cryptoProxy);
+ var o = _.clone(options);
+ deriveKeysForModel(model, _.extend(o, { success: function(keys) {
+ model.cryptoProxy = createCryptoProxyForModel(model, keys);
+ options.success(model.cryptoProxy);
+ }}));
+ }
+
+ var kdf = GombotCrypto.derive;
+ // Special kdf derivation function we'll pass to GombotClient to handle FX slowness bug
+ if (typeof require !== "undefined") {
+ kdf = function (args, callback) {
+ console.log("in derive")
+ require("gombot-crypto-jetpack").kdf(args.email, args.password).then(function(keys) {
+ callback(null, keys);
+ });
+ }
+ }
+
+ // options.password must be present
+ function deriveKeysForModel(model, options) {
+ kdf({
+ email: model.get("email"),
+ password: options.password
+ }, function(err, keys) {
+ if (err) return maybeHandleError(options.error, err);
+ options.success(keys);
+ });
+ }
+
+ function setSyncStrategy(strategy) {
+ SyncStrategy = strategy;
+ }
+
+ function sync(method, model, options) {
+ if (!(model instanceof Gombot.User)) {
+ if (options.error) options.error("sync only supports syncing instances of Gombot.User");
+ return false;
+ }
+ var o = _.clone(options);
+ getCryptoProxyForModel(model, _.extend(o, { success: function(cryptoProxyForModel) {
+ var o = _.clone(options);
+ // translate model proxy back to original model
+ SyncStrategy.sync(method, cryptoProxyForModel, _.extend(o, { success: function(modelProxy, resp, modifiedOptions) {
+ if (options.success) options.success(model, resp, options);
+ }}));
+ }}));
+ }
+
+ return {
+ sync: sync,
+ setSyncStrategy: setSyncStrategy
+ };
+};
+
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = SyncAdapter;
+}
diff --git a/lib/backbone.localStorage.js b/lib/backbone.localStorage.js
index 93d9816..7da7aa2 100644
--- a/lib/backbone.localStorage.js
+++ b/lib/backbone.localStorage.js
@@ -3,7 +3,7 @@
*
* https://github.com/jeromegn/Backbone.localStorage
*/
-var Storage = function(Backbone, _, LocalStorage) {
+var Storage = function(Backbone, _, LocalStorage, store) {
return (function (root, factory) {
// if (typeof define === "function" && define.amd) {
// // AMD. Register as an anonymous module.
@@ -62,9 +62,12 @@ _.extend(Backbone.LocalStorage.prototype, {
var cb = _.after(2, function() {
callback(model.toJSON());
});
- this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model.toJSON({ ciphertext: options.ciphertext })), cb);
- this.records.push(model.id.toString());
- this.save(cb);
+ var o = _.clone(options);
+ model.toJSON(_.extend(o, { success: (function(jsonObj) {
+ this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(jsonObj), cb);
+ this.records.push(model.id.toString());
+ this.save(cb);
+ }).bind(this)}));
return;
},
@@ -116,6 +119,12 @@ _.extend(Backbone.LocalStorage.prototype, {
// fix for "illegal access" error on Android when JSON.parse is passed null
jsonData: function (data) {
return data && JSON.parse(data);
+ },
+
+ sync: function(method, model, options) {
+ var o = _.clone(options);
+ o.store = this;
+ Backbone.LocalStorage.sync(method, model, o);
}
});
@@ -124,7 +133,7 @@ _.extend(Backbone.LocalStorage.prototype, {
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, options) {
- var store = model.localStorage || model.collection.localStorage;
+ var store = options.store || model.localStorage || model.collection.localStorage;
var syncDfd = (typeof $ !== "undefined") && $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
@@ -149,6 +158,8 @@ Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, option
if (options && options.complete) options.complete(resp);
};
+ console.log("Backbone.LocalStorage.sync", method, model, options);
+
switch (method) {
case "read": model.id != undefined ? store.find(model, callback, options) : store.findAll(callback, options); break;
case "create": store.create(model, callback, options); break;
diff --git a/manifest.json b/manifest.json
index 9c8c085..025076f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -8,12 +8,14 @@
"notifications",
"storage",
"http://*/",
- "https://*/"
+ "https://*/",
+ "webRequest",
+ "webRequestBlocking"
],
"icons": {
"128": "images/gombot-icon-128.png"
},
-
+ "content_security_policy": "script-src 'self' https://cdn.firebase.com https://auth.firebase.com https://*.firebaseio.com; object-src 'self'",
"background": {
"page": "background/background.html"
},
diff --git a/test/test-local-sync.js b/test/test-local-sync.js
new file mode 100644
index 0000000..e4e1e09
--- /dev/null
+++ b/test/test-local-sync.js
@@ -0,0 +1,22 @@
+exports.testCreate = function(test) {
+ var GombotModules = require("./modules");
+ var Gombot = require("./gombot")(GombotModules);
+
+ Gombot.init({ storeName: "testUsers", callback: function() {
+ var email = "test+"+Math.floor((1+Math.random())*10000)+"@test.com";
+ var u = new Gombot.User({ email: email });
+ u.save(null, { success: function() {
+ console.log("user saved", u);
+ test.pass();
+ test.done();
+ },
+ error: function(err) {
+ console.log("error:", err);
+ test.fail();
+ test.done();
+ },
+ password: "foobar"
+ });
+ }});
+ test.waitUntilDone();
+}
\ No newline at end of file