Password sync for 0.3 (bug #468697)
authorAnant Narayanan <anant@kix.in>
Tue, 03 Mar 2009 00:42:57 +0100
changeset 45309 deadcea07a9ca774a7626092cd2c1eb8e0a93628
parent 45307 2d2fe67a444270186589b535fce06e14ff38febc
child 45310 9473b6c8985d907911467eba54963cb8e85b42a4
push idunknown
push userunknown
push dateunknown
bugs468697
Password sync for 0.3 (bug #468697)
services/sync/modules/engines/passwords.js
services/sync/modules/type_records/passwords.js
services/sync/modules/util.js
services/sync/services-sync.js
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -14,16 +14,17 @@
  * The Original Code is Bookmarks Sync.
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2008
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Justin Dolske <dolske@mozilla.com>
+ *  Anant Narayanan <anant@kix.in>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -32,225 +33,229 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ['PasswordEngine'];
 
 const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
 
+Cu.import("resource://weave/log4moz.js");
 Cu.import("resource://weave/util.js");
-Cu.import("resource://weave/async.js");
 Cu.import("resource://weave/engines.js");
-Cu.import("resource://weave/syncCores.js");
 Cu.import("resource://weave/stores.js");
 Cu.import("resource://weave/trackers.js");
+Cu.import("resource://weave/async.js");
+Cu.import("resource://weave/ext/Observers.js");
+Cu.import("resource://weave/type_records/passwords.js");
 
 Function.prototype.async = Async.sugar;
 
 function PasswordEngine() {
   this._init();
 }
 PasswordEngine.prototype = {
-  __proto__: new SyncEngine(),
-
-  get name() { return "passwords"; },
-  get displayName() { return "Saved Passwords"; },
-  get logName() { return "PasswordEngine"; },
-  get serverPrefix() { return "user-data/passwords/"; },
-
-  __store: null,
-  get _store() {
-    if (!this.__store)
-      this.__store = new PasswordStore();
-    return this.__store;
+  __proto__: SyncEngine.prototype,
+  name: "passwords",
+  displayName: "Passwords",
+  logName: "Passwords",
+  _storeObj: PasswordStore,
+  _trackerObj: PasswordTracker,
+  _recordObj: LoginRec,
+  
+  /* We override syncStartup & syncFinish to populate/reset our local cache
+     of loginInfo items. We can remove this when the interface to query
+     LoginInfo items by GUID is ready
+  */
+  _syncStartup: function PasswordStore__syncStartup() {
+    let self = yield;
+    this._store._cacheLogins();
+    yield SyncEngine.prototype._syncStartup.async(this, self.cb);
   },
-
-  __core: null,
-  get _core() {
-    if (!this.__core)
-      this.__core = new PasswordSyncCore(this._store);
-    return this.__core;
-  },
-
-  __tracker: null,
-  get _tracker() {
-    if (!this.__tracker)
-      this.__tracker = new PasswordTracker();
-    return this.__tracker;
+  
+  _syncFinish: function PasswordStore__syncFinish() {
+    let self = yield;
+    this._store._clearLoginCache();
+    yield SyncEngine.prototype._syncFinish.async(this, self.cb);
   }
 };
 
-function PasswordSyncCore(store) {
-  this._store = store;
-  this._init();
-}
-PasswordSyncCore.prototype = {
-  _logName: "PasswordSync",
-  _store: null,
-
-  _commandLike: function PSC_commandLike(a, b) {
-    // Not used.
-    return false;
-  }
-};
-PasswordSyncCore.prototype.__proto__ = new SyncCore();
-
 function PasswordStore() {
   this._init();
 }
 PasswordStore.prototype = {
+  __proto__: Store.prototype,
   _logName: "PasswordStore",
-  _lookup: null,
 
-  __loginManager: null,
   get _loginManager() {
-    if (!this.__loginManager)
-      this.__loginManager = Utils.getLoginManager();
-    return this.__loginManager;
-  },
-
-  __nsLoginInfo: null,
-  get _nsLoginInfo() {
-    if (!this.__nsLoginInfo)
-      this.__nsLoginInfo = Utils.makeNewLoginInfo();
-    return this.__nsLoginInfo;
+    let loginManager = Utils.getLoginManager();
+    this.__defineGetter__("_loginManager", function() loginManager);
+    return loginManager;
   },
 
-  /*
-   * _hashLoginInfo
-   *
-   * nsILoginInfo objects don't have a unique GUID, so we need to generate one
-   * on the fly. This is done by taking a hash of every field in the object.
-   * Note that the resulting GUID could potentiually reveal passwords via
-   * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by
-   * anyone, so this should generally be safe.
-   */
-   _hashLoginInfo: function PasswordStore__hashLoginInfo(aLogin) {
-    var loginKey = aLogin.hostname      + ":" +
-                   aLogin.formSubmitURL + ":" +
-                   aLogin.httpRealm     + ":" +
-                   aLogin.username      + ":" +
-                   aLogin.password      + ":" +
-                   aLogin.usernameField + ":" +
-                   aLogin.passwordField;
+  get _loginItems() {
+    let loginItems = {};
+    let logins = this._loginManager.getAllLogins({});
+    for (let i = 0; i < logins.length; i++) {
+      let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
+      loginItems[metaInfo.guid] = logins[i];
+    }
+    
+    this.__defineGetter__("_loginItems", function() loginItems);
+    return loginItems;
+  },
+  
+  _nsLoginInfo: null,
+  _init: function PasswordStore_init() {
+    Store.prototype._init.call(this);
+    this._nsLoginInfo = new Components.Constructor(
+      "@mozilla.org/login-manager/loginInfo;1",
+      Ci.nsILoginInfo,
+      "init"
+    );
+  },
+  
+  _cacheLogins: function PasswordStore__cacheLogins() {
+    /* Force the getter to populate the property
+       Also, this way, we don't fail if the store is created twice?
+     */
+    return this._loginItems;
+  },
+  
+  _clearLoginCache: function PasswordStore__clearLoginCache() {
+    this.__loginItems = null;
+  },
+  
+  _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) {
+    return new this._nsLoginInfo(record.hostname,
+                                  record.formSubmitURL,
+                                  record.httpRealm,
+                                  record.username,
+                                  record.password,
+                                  record.usernameField,
+                                  record.passwordField);
+  },
 
-    return Utils.sha1(loginKey);
+  getAllIDs: function PasswordStore__getAllIDs() {
+    let items = {};
+    let logins = this._loginManager.getAllLogins({});
+    
+    for (let i = 0; i < logins.length; i++) {
+      let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
+      items[metaInfo.guid] = logins[i].hostname;
+    }
+    
+    return items;
   },
 
-  _createCommand: function PasswordStore__createCommand(command) {
-    this._log.info("PasswordStore got createCommand: " + command );
+  changeItemID: function PasswordStore__changeItemID(oldID, newID) {
+    if (!(oldID in this._loginItems)) {
+      this._log.warn("Can't change GUID " + oldID + " to " +
+                     newID + ": Item does not exist");
+      return;
+    }
+    let info = this._loginItems[oldID];
+    
+    if (newID in this._loginItems) {
+      this._log.warn("Can't change GUID " + oldID + " to " +
+                     newID + ": new ID already in use");
+      return;
+    }
+    
+    this._log.debug("Changing GUID " + oldID + " to " + newID);
 
-    var login = new this._nsLoginInfo(command.data.hostname,
-                                      command.data.formSubmitURL,
-                                      command.data.httpRealm,
-                                      command.data.username,
-                                      command.data.password,
-                                      command.data.usernameField,
-                                      command.data.passwordField);
+    let prop = Cc["@mozilla.org/hash-property-bag;1"].
+               createInstance(Ci.nsIWritablePropertyBag2);
+    prop.setPropertyAsAUTF8String("guid", newID);
 
-    this._loginManager.addLogin(login);
+    this._loginManager.modifyLogin(info, prop);
+  },
+
+  itemExists: function PasswordStore__itemExists(id) {
+    return ((id in this._loginItems) == true);
   },
 
-  _removeCommand: function PasswordStore__removeCommand(command) {
-    this._log.info("PasswordStore got removeCommand: " + command );
-
-    if (command.GUID in this._lookup) {
-      var data  = this._lookup[command.GUID];
-      var login = new this._nsLoginInfo(data.hostname,
-                                        data.formSubmitURL,
-                                        data.httpRealm,
-                                        data.username,
-                                        data.password,
-                                        data.usernameField,
-                                        data.passwordField);
-      this._loginManager.removeLogin(login);
+  createRecord: function PasswordStore__createRecord(guid, cryptoMetaURL) {
+    let record = new LoginRec();
+    record.id = guid;
+    if (guid in this._loginItems) {
+      let login = this._loginItems[guid];
+      record.encryption = cryptoMetaURL;
+      record.hostname = login.hostname;
+      record.formSubmitURL = login.formSubmitURL;
+      record.httpRealm = login.httpRealm;
+      record.username = login.username;
+      record.password = login.password;
+      record.usernameField = login.usernameField;
+      record.passwordField = login.passwordField;
     } else {
-      this._log.warn("Invalid GUID for remove, ignoring request!");
+      /* Deleted item */
+      record.cleartext = null;
     }
+    return record;
   },
 
-  _editCommand: function PasswordStore__editCommand(command) {
-    this._log.info("PasswordStore got editCommand: " + command );
-    throw "Password syncs are expected to only be create/remove!";
+  create: function PasswordStore__create(record) {
+    this._loginManager.addLogin(this._nsLoginInfoFromRecord(record));
   },
 
-  wrap: function PasswordStore_wrap() {
-    /* Return contents of this store, as JSON. */
-    var items = {};
-    var logins = this._loginManager.getAllLogins({});
-
-    for (var i = 0; i < logins.length; i++) {
-      var login = logins[i];
-
-      var key = this._hashLoginInfo(login);
+  remove: function PasswordStore__remove(record) {
+    if (record.id in this._loginItems) {
+      this._loginManager.removeLogin(this._loginItems[record.id]);
+      return;
+    }
+    
+    this._log.debug("Asked to remove record that doesn't exist, ignoring!");
+  },
 
-      items[key] = { hostname      : login.hostname,
-                     formSubmitURL : login.formSubmitURL,
-                     httpRealm     : login.httpRealm,
-                     username      : login.username,
-                     password      : login.password,
-                     usernameField : login.usernameField,
-                     passwordField : login.passwordField };
+  update: function PasswordStore__update(record) {
+    if (!(record.id in this._loginItems)) {
+      this._log.debug("Skipping update for unknown item: " + record.id);
+      return;
     }
+    let login = this._loginItems[record.id];
+    this._log.trace("Updating " + record.id + " (" + itemId + ")");
 
-    this._lookup = items;
-    return items;
+    let newinfo = this._nsLoginInfoFromRecord(record);
+    this._loginManager.modifyLogin(login, newinfo);
   },
 
   wipe: function PasswordStore_wipe() {
     this._loginManager.removeAllLogins();
-  },
-
-  _resetGUIDs: function PasswordStore__resetGUIDs() {
-    let self = yield;
-    // Not needed.
   }
 };
-PasswordStore.prototype.__proto__ = new Store();
 
 function PasswordTracker() {
   this._init();
 }
 PasswordTracker.prototype = {
+  __proto__: Tracker.prototype,
   _logName: "PasswordTracker",
 
-  __loginManager : null,
-  get _loginManager() {
-    if (!this.__loginManager)
-      this.__loginManager = Cc["@mozilla.org/login-manager;1"].
-                            getService(Ci.nsILoginManager);
-    return this.__loginManager;
+  _init: function PasswordTracker_init() {
+    Tracker.prototype._init.call(this);
+    Observers.add("passwordmgr-storage-changed", this);
   },
 
-  /* We use nsILoginManager's countLogins() method, as it is
-   * the only method that doesn't require the user to enter
-   * a master password, but still gives us an idea of how much
-   * info has changed.
-   *
-   * FIXME: This does not track edits of passwords, so we
-   * artificially add 30 to every score. We should move to
-   * using the LoginManager shim at some point.
-   *
-   * Each addition/removal is worth 15 points.
-   */
-  _loginCount: 0,
-  get score() {
-    var count = this._loginManager.countLogins("", "", "");
-    var score = (Math.abs(this._loginCount - count) * 15) + 30;
+  /* A single add, remove or change is 15 points, all items removed is 50 */
+  observe: function PasswordTracker_observe(aSubject, aTopic, aData) {
+    if (this.ignoreAll)
+      return;
+
+    this._log.debug("Received notification " + aData);
 
-    if (score >= 100)
-      return 100;
-    else
-      return score;
-  },
-
-  resetScore: function PasswordTracker_resetScore() {
-    this._loginCount = this._loginManager.countLogins("", "", "");
-  },
-
-  _init: function PasswordTracker__init() {
-    this._log = Log4Moz.Service.getLogger("Service."  + this._logName);
-    this._loginCount = this._loginManager.countLogins("", "", "");
+    switch (aData) {
+    case 'addLogin':
+    case 'modifyLogin':
+    case 'removeLogin':
+      let metaInfo = aSubject.QueryInterface(Ci.nsILoginMetaInfo);
+      this._score += 15;
+      this.addChangedID(metaInfo.guid);
+      break;
+    case 'removeAllLogins':
+      this._score += 50;
+      break;
+    }
   }
 };
-PasswordTracker.prototype.__proto__ = new Tracker();
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/type_records/passwords.js
@@ -0,0 +1,100 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Weave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Anant Narayanan <anant@kix.in>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const EXPORTED_SYMBOLS = ['LoginRec'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://weave/log4moz.js");
+Cu.import("resource://weave/util.js");
+Cu.import("resource://weave/async.js");
+Cu.import("resource://weave/base_records/wbo.js");
+Cu.import("resource://weave/base_records/crypto.js");
+Cu.import("resource://weave/base_records/keys.js");
+
+Function.prototype.async = Async.sugar;
+
+function LoginRec(uri) {
+  this._LoginRec_init(uri);
+}
+LoginRec.prototype = {
+  __proto__: CryptoWrapper.prototype,
+  _logName: "Record.Login",
+
+  _LoginRec_init: function LoginItem_init(uri) {
+    this._CryptoWrap_init(uri);
+    this.cleartext = {
+    };
+  },
+
+  get hostname() this.cleartext.hostname,
+  set hostname(value) {
+      this.cleartext.hostname = value;
+  },
+
+  get formSubmitURL() this.cleartext.formSubmitURL,
+  set formSubmitURL(value) {
+    this.cleartext.formSubmitURL = value;
+  },
+
+  get httpRealm() this.cleartext.httpRealm,
+  set httpRealm(value) {
+    this.cleartext.httpRealm = value;
+  },
+  
+  get username() this.cleartext.username,
+  set username(value) {
+    this.cleartext.username = value;
+  },
+  
+  get password() this.cleartext.password,
+  set password(value) {
+    this.cleartext.password = value;
+  },
+  
+  get usernameField() this.cleartext.usernameField,
+  set usernameField(value) {
+    this.cleartext.usernameField = value;
+  },
+  
+  get passwordField() this.cleartext.passwordField,
+  set passwordField(value) {
+    this.cleartext.passwordField = value;
+  }
+};
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -88,25 +88,17 @@ let Utils = {
       file.create(file.NORMAL_FILE_TYPE, PERMS_FILE);
     return file;
   },
 
   getLoginManager: function getLoginManager() {
     return Cc["@mozilla.org/login-manager;1"].
            getService(Ci.nsILoginManager);
   },
-
-  makeNewLoginInfo: function getNewLoginInfo() {
-    return new Components.Constructor(
-      "@mozilla.org/login-manager/loginInfo;1",
-      Ci.nsILoginInfo,
-      "init"
-    );
-  },
-
+  
   findPassword: function findPassword(realm, username) {
     // fixme: make a request and get the realm ?
     let password;
     let lm = Cc["@mozilla.org/login-manager;1"]
              .getService(Ci.nsILoginManager);
     let logins = lm.findLogins({}, 'chrome://weave', null, realm);
 
     for (let i = 0; i < logins.length; i++) {
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -11,17 +11,17 @@ pref("extensions.weave.autoconnect", tru
 pref("extensions.weave.enabled", true);
 pref("extensions.weave.schedule", 1);
 
 pref("extensions.weave.syncOnQuit.enabled", true);
 
 pref("extensions.weave.engine.bookmarks", true);
 pref("extensions.weave.engine.history", true);
 pref("extensions.weave.engine.cookies", false);
-pref("extensions.weave.engine.passwords", false);
+pref("extensions.weave.engine.passwords", true);
 pref("extensions.weave.engine.forms", false);
 pref("extensions.weave.engine.tabs", true);
 pref("extensions.weave.engine.input", false);
 
 pref("extensions.weave.log.appender.console", "Warn");
 pref("extensions.weave.log.appender.dump", "Error");
 pref("extensions.weave.log.appender.briefLog", "Info");
 pref("extensions.weave.log.appender.debugLog", "Trace");