michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["IdentityManager"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: michael@0: // Lazy import to prevent unnecessary load on startup. michael@0: for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) { michael@0: XPCOMUtils.defineLazyModuleGetter(this, symbol, michael@0: "resource://services-sync/keys.js", michael@0: symbol); michael@0: } michael@0: michael@0: /** michael@0: * Manages "legacy" identity and authentication for Sync. michael@0: * See browserid_identity for the Firefox Accounts based identity manager. michael@0: * michael@0: * The following entities are managed: michael@0: * michael@0: * account - The main Sync/services account. This is typically an email michael@0: * address. michael@0: * username - A normalized version of your account. This is what's michael@0: * transmitted to the server. michael@0: * basic password - UTF-8 password used for authenticating when using HTTP michael@0: * basic authentication. michael@0: * sync key - The main encryption key used by Sync. michael@0: * sync key bundle - A representation of your sync key. michael@0: * michael@0: * When changes are made to entities that are stored in the password manager michael@0: * (basic password, sync key), those changes are merely staged. To commit them michael@0: * to the password manager, you'll need to call persistCredentials(). michael@0: * michael@0: * This type also manages authenticating Sync's network requests. Sync's michael@0: * network code calls into getRESTRequestAuthenticator and michael@0: * getResourceAuthenticator (depending on the network layer being used). Each michael@0: * returns a function which can be used to add authentication information to an michael@0: * outgoing request. michael@0: * michael@0: * In theory, this type supports arbitrary identity and authentication michael@0: * mechanisms. You can add support for them by monkeypatching the global michael@0: * instance of this type. Specifically, you'll need to redefine the michael@0: * aforementioned network code functions to do whatever your authentication michael@0: * mechanism needs them to do. In addition, you may wish to install custom michael@0: * functions to support your API. Although, that is certainly not required. michael@0: * If you do monkeypatch, please be advised that Sync expects the core michael@0: * attributes to have values. You will need to carry at least account and michael@0: * username forward. If you do not wish to support one of the built-in michael@0: * authentication mechanisms, you'll probably want to redefine currentAuthState michael@0: * and any other function that involves the built-in functionality. michael@0: */ michael@0: this.IdentityManager = function IdentityManager() { michael@0: this._log = Log.repository.getLogger("Sync.Identity"); michael@0: this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")]; michael@0: michael@0: this._basicPassword = null; michael@0: this._basicPasswordAllowLookup = true; michael@0: this._basicPasswordUpdated = false; michael@0: this._syncKey = null; michael@0: this._syncKeyAllowLookup = true; michael@0: this._syncKeySet = false; michael@0: this._syncKeyBundle = null; michael@0: } michael@0: IdentityManager.prototype = { michael@0: _log: null, michael@0: michael@0: _basicPassword: null, michael@0: _basicPasswordAllowLookup: true, michael@0: _basicPasswordUpdated: false, michael@0: michael@0: _syncKey: null, michael@0: _syncKeyAllowLookup: true, michael@0: _syncKeySet: false, michael@0: michael@0: _syncKeyBundle: null, michael@0: michael@0: /** michael@0: * Initialize the identity provider. Returns a promise that is resolved michael@0: * when initialization is complete and the provider can be queried for michael@0: * its state michael@0: */ michael@0: initialize: function() { michael@0: // Nothing to do for this identity provider. michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: finalize: function() { michael@0: // Nothing to do for this identity provider. michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Called whenever Service.logout() is called. michael@0: */ michael@0: logout: function() { michael@0: // nothing to do for this identity provider. michael@0: }, michael@0: michael@0: /** michael@0: * Ensure the user is logged in. Returns a promise that resolves when michael@0: * the user is logged in, or is rejected if the login attempt has failed. michael@0: */ michael@0: ensureLoggedIn: function() { michael@0: // nothing to do for this identity provider michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Indicates if the identity manager is still initializing michael@0: */ michael@0: get readyToAuthenticate() { michael@0: // We initialize in a fully sync manner, so we are always finished. michael@0: return true; michael@0: }, michael@0: michael@0: get account() { michael@0: return Svc.Prefs.get("account", this.username); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the active account name. michael@0: * michael@0: * This should almost always be called in favor of setting username, as michael@0: * username is derived from account. michael@0: * michael@0: * Changing the account name has the side-effect of wiping out stored michael@0: * credentials. Keep in mind that persistCredentials() will need to be called michael@0: * to flush the changes to disk. michael@0: * michael@0: * Set this value to null to clear out identity information. michael@0: */ michael@0: set account(value) { michael@0: if (value) { michael@0: value = value.toLowerCase(); michael@0: Svc.Prefs.set("account", value); michael@0: } else { michael@0: Svc.Prefs.reset("account"); michael@0: } michael@0: michael@0: this.username = this.usernameFromAccount(value); michael@0: }, michael@0: michael@0: get username() { michael@0: return Svc.Prefs.get("username", null); michael@0: }, michael@0: michael@0: /** michael@0: * Set the username value. michael@0: * michael@0: * Changing the username has the side-effect of wiping credentials. michael@0: */ michael@0: set username(value) { michael@0: if (value) { michael@0: value = value.toLowerCase(); michael@0: michael@0: if (value == this.username) { michael@0: return; michael@0: } michael@0: michael@0: Svc.Prefs.set("username", value); michael@0: } else { michael@0: Svc.Prefs.reset("username"); michael@0: } michael@0: michael@0: // If we change the username, we interpret this as a major change event michael@0: // and wipe out the credentials. michael@0: this._log.info("Username changed. Removing stored credentials."); michael@0: this.resetCredentials(); michael@0: }, michael@0: michael@0: /** michael@0: * Resets/Drops all credentials we hold for the current user. michael@0: */ michael@0: resetCredentials: function() { michael@0: this.basicPassword = null; michael@0: this.resetSyncKey(); michael@0: }, michael@0: michael@0: /** michael@0: * Resets/Drops the sync key we hold for the current user. michael@0: */ michael@0: resetSyncKey: function() { michael@0: this.syncKey = null; michael@0: // syncKeyBundle cleared as a result of setting syncKey. michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the HTTP Basic auth password. michael@0: * michael@0: * Returns a string if set or null if it is not set. michael@0: */ michael@0: get basicPassword() { michael@0: if (this._basicPasswordAllowLookup) { michael@0: // We need a username to find the credentials. michael@0: let username = this.username; michael@0: if (!username) { michael@0: return null; michael@0: } michael@0: michael@0: for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) { michael@0: if (login.username.toLowerCase() == username) { michael@0: // It should already be UTF-8 encoded, but we don't take any chances. michael@0: this._basicPassword = Utils.encodeUTF8(login.password); michael@0: } michael@0: } michael@0: michael@0: this._basicPasswordAllowLookup = false; michael@0: } michael@0: michael@0: return this._basicPassword; michael@0: }, michael@0: michael@0: /** michael@0: * Set the HTTP basic password to use. michael@0: * michael@0: * Changes will not persist unless persistSyncCredentials() is called. michael@0: */ michael@0: set basicPassword(value) { michael@0: // Wiping out value. michael@0: if (!value) { michael@0: this._log.info("Basic password has no value. Removing."); michael@0: this._basicPassword = null; michael@0: this._basicPasswordUpdated = true; michael@0: this._basicPasswordAllowLookup = false; michael@0: return; michael@0: } michael@0: michael@0: let username = this.username; michael@0: if (!username) { michael@0: throw new Error("basicPassword cannot be set before username."); michael@0: } michael@0: michael@0: this._log.info("Basic password being updated."); michael@0: this._basicPassword = Utils.encodeUTF8(value); michael@0: this._basicPasswordUpdated = true; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain the Sync Key. michael@0: * michael@0: * This returns a 26 character "friendly" Base32 encoded string on success or michael@0: * null if no Sync Key could be found. michael@0: * michael@0: * If the Sync Key hasn't been set in this session, this will look in the michael@0: * password manager for the sync key. michael@0: */ michael@0: get syncKey() { michael@0: if (this._syncKeyAllowLookup) { michael@0: let username = this.username; michael@0: if (!username) { michael@0: return null; michael@0: } michael@0: michael@0: for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) { michael@0: if (login.username.toLowerCase() == username) { michael@0: this._syncKey = login.password; michael@0: } michael@0: } michael@0: michael@0: this._syncKeyAllowLookup = false; michael@0: } michael@0: michael@0: return this._syncKey; michael@0: }, michael@0: michael@0: /** michael@0: * Set the active Sync Key. michael@0: * michael@0: * If being set to null, the Sync Key and its derived SyncKeyBundle are michael@0: * removed. However, the Sync Key won't be deleted from the password manager michael@0: * until persistSyncCredentials() is called. michael@0: * michael@0: * If a value is provided, it should be a 26 or 32 character "friendly" michael@0: * Base32 string for which Utils.isPassphrase() returns true. michael@0: * michael@0: * A side-effect of setting the Sync Key is that a SyncKeyBundle is michael@0: * generated. For historical reasons, this will silently error out if the michael@0: * value is not a proper Sync Key (!Utils.isPassphrase()). This should be michael@0: * fixed in the future (once service.js is more sane) to throw if the passed michael@0: * value is not valid. michael@0: */ michael@0: set syncKey(value) { michael@0: if (!value) { michael@0: this._log.info("Sync Key has no value. Deleting."); michael@0: this._syncKey = null; michael@0: this._syncKeyBundle = null; michael@0: this._syncKeyUpdated = true; michael@0: return; michael@0: } michael@0: michael@0: if (!this.username) { michael@0: throw new Error("syncKey cannot be set before username."); michael@0: } michael@0: michael@0: this._log.info("Sync Key being updated."); michael@0: this._syncKey = value; michael@0: michael@0: // Clear any cached Sync Key Bundle and regenerate it. michael@0: this._syncKeyBundle = null; michael@0: let bundle = this.syncKeyBundle; michael@0: michael@0: this._syncKeyUpdated = true; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain the active SyncKeyBundle. michael@0: * michael@0: * This returns a SyncKeyBundle representing a key pair derived from the michael@0: * Sync Key on success. If no Sync Key is present or if the Sync Key is not michael@0: * valid, this returns null. michael@0: * michael@0: * The SyncKeyBundle should be treated as immutable. michael@0: */ michael@0: get syncKeyBundle() { michael@0: // We can't obtain a bundle without a username set. michael@0: if (!this.username) { michael@0: this._log.warn("Attempted to obtain Sync Key Bundle with no username set!"); michael@0: return null; michael@0: } michael@0: michael@0: if (!this.syncKey) { michael@0: this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " + michael@0: "set!"); michael@0: return null; michael@0: } michael@0: michael@0: if (!this._syncKeyBundle) { michael@0: try { michael@0: this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey); michael@0: } catch (ex) { michael@0: this._log.warn(Utils.exceptionStr(ex)); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: return this._syncKeyBundle; michael@0: }, michael@0: michael@0: /** michael@0: * The current state of the auth credentials. michael@0: * michael@0: * This essentially validates that enough credentials are available to use michael@0: * Sync. michael@0: */ michael@0: get currentAuthState() { michael@0: if (!this.username) { michael@0: return LOGIN_FAILED_NO_USERNAME; michael@0: } michael@0: michael@0: if (Utils.mpLocked()) { michael@0: return STATUS_OK; michael@0: } michael@0: michael@0: if (!this.basicPassword) { michael@0: return LOGIN_FAILED_NO_PASSWORD; michael@0: } michael@0: michael@0: if (!this.syncKey) { michael@0: return LOGIN_FAILED_NO_PASSPHRASE; michael@0: } michael@0: michael@0: // If we have a Sync Key but no bundle, bundle creation failed, which michael@0: // implies a bad Sync Key. michael@0: if (!this.syncKeyBundle) { michael@0: return LOGIN_FAILED_INVALID_PASSPHRASE; michael@0: } michael@0: michael@0: return STATUS_OK; michael@0: }, michael@0: michael@0: /** michael@0: * Persist credentials to password store. michael@0: * michael@0: * When credentials are updated, they are changed in memory only. This will michael@0: * need to be called to save them to the underlying password store. michael@0: * michael@0: * If the password store is locked (e.g. if the master password hasn't been michael@0: * entered), this could throw an exception. michael@0: */ michael@0: persistCredentials: function persistCredentials(force) { michael@0: if (this._basicPasswordUpdated || force) { michael@0: if (this._basicPassword) { michael@0: this._setLogin(PWDMGR_PASSWORD_REALM, this.username, michael@0: this._basicPassword); michael@0: } else { michael@0: for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) { michael@0: Services.logins.removeLogin(login); michael@0: } michael@0: } michael@0: michael@0: this._basicPasswordUpdated = false; michael@0: } michael@0: michael@0: if (this._syncKeyUpdated || force) { michael@0: if (this._syncKey) { michael@0: this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey); michael@0: } else { michael@0: for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) { michael@0: Services.logins.removeLogin(login); michael@0: } michael@0: } michael@0: michael@0: this._syncKeyUpdated = false; michael@0: } michael@0: michael@0: }, michael@0: michael@0: /** michael@0: * Deletes the Sync Key from the system. michael@0: */ michael@0: deleteSyncKey: function deleteSyncKey() { michael@0: this.syncKey = null; michael@0: this.persistCredentials(); michael@0: }, michael@0: michael@0: hasBasicCredentials: function hasBasicCredentials() { michael@0: // Because JavaScript. michael@0: return this.username && this.basicPassword && true; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the array of basic logins from nsiPasswordManager. michael@0: */ michael@0: _getLogins: function _getLogins(realm) { michael@0: return Services.logins.findLogins({}, PWDMGR_HOST, null, realm); michael@0: }, michael@0: michael@0: /** michael@0: * Set a login in the password manager. michael@0: * michael@0: * This has the side-effect of deleting any other logins for the specified michael@0: * realm. michael@0: */ michael@0: _setLogin: function _setLogin(realm, username, password) { michael@0: let exists = false; michael@0: for each (let login in this._getLogins(realm)) { michael@0: if (login.username == username && login.password == password) { michael@0: exists = true; michael@0: } else { michael@0: this._log.debug("Pruning old login for " + username + " from " + realm); michael@0: Services.logins.removeLogin(login); michael@0: } michael@0: } michael@0: michael@0: if (exists) { michael@0: return; michael@0: } michael@0: michael@0: this._log.debug("Updating saved password for " + username + " in " + michael@0: realm); michael@0: michael@0: let loginInfo = new Components.Constructor( michael@0: "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); michael@0: let login = new loginInfo(PWDMGR_HOST, null, realm, username, michael@0: password, "", ""); michael@0: Services.logins.addLogin(login); michael@0: }, michael@0: michael@0: /** michael@0: * Deletes Sync credentials from the password manager. michael@0: */ michael@0: deleteSyncCredentials: function deleteSyncCredentials() { michael@0: for (let host of Utils.getSyncCredentialsHosts()) { michael@0: let logins = Services.logins.findLogins({}, host, "", ""); michael@0: for each (let login in logins) { michael@0: Services.logins.removeLogin(login); michael@0: } michael@0: } michael@0: michael@0: // Wait until after store is updated in case it fails. michael@0: this._basicPassword = null; michael@0: this._basicPasswordAllowLookup = true; michael@0: this._basicPasswordUpdated = false; michael@0: michael@0: this._syncKey = null; michael@0: // this._syncKeyBundle is nullified as part of _syncKey setter. michael@0: this._syncKeyAllowLookup = true; michael@0: this._syncKeyUpdated = false; michael@0: }, michael@0: michael@0: usernameFromAccount: function usernameFromAccount(value) { michael@0: // If we encounter characters not allowed by the API (as found for michael@0: // instance in an email address), hash the value. michael@0: if (value && value.match(/[^A-Z0-9._-]/i)) { michael@0: return Utils.sha1Base32(value.toLowerCase()).toLowerCase(); michael@0: } michael@0: michael@0: return value ? value.toLowerCase() : value; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a function to be used for adding auth to Resource HTTP requests. michael@0: */ michael@0: getResourceAuthenticator: function getResourceAuthenticator() { michael@0: if (this.hasBasicCredentials()) { michael@0: return this._onResourceRequestBasic.bind(this); michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Helper method to return an authenticator for basic Resource requests. michael@0: */ michael@0: getBasicResourceAuthenticator: michael@0: function getBasicResourceAuthenticator(username, password) { michael@0: michael@0: return function basicAuthenticator(resource) { michael@0: let value = "Basic " + btoa(username + ":" + password); michael@0: return {headers: {authorization: value}}; michael@0: }; michael@0: }, michael@0: michael@0: _onResourceRequestBasic: function _onResourceRequestBasic(resource) { michael@0: let value = "Basic " + btoa(this.username + ":" + this.basicPassword); michael@0: return {headers: {authorization: value}}; michael@0: }, michael@0: michael@0: _onResourceRequestMAC: function _onResourceRequestMAC(resource, method) { michael@0: // TODO Get identifier and key from somewhere. michael@0: let identifier; michael@0: let key; michael@0: let result = Utils.computeHTTPMACSHA1(identifier, key, method, resource.uri); michael@0: michael@0: return {headers: {authorization: result.header}}; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a function to be used for adding auth to RESTRequest instances. michael@0: */ michael@0: getRESTRequestAuthenticator: function getRESTRequestAuthenticator() { michael@0: if (this.hasBasicCredentials()) { michael@0: return this.onRESTRequestBasic.bind(this); michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: onRESTRequestBasic: function onRESTRequestBasic(request) { michael@0: let up = this.username + ":" + this.basicPassword; michael@0: request.setHeader("authorization", "Basic " + btoa(up)); michael@0: }, michael@0: michael@0: createClusterManager: function(service) { michael@0: Cu.import("resource://services-sync/stages/cluster.js"); michael@0: return new ClusterManager(service); michael@0: }, michael@0: michael@0: offerSyncOptions: function () { michael@0: // Do nothing for Sync 1.1. michael@0: return {accepted: true}; michael@0: }, michael@0: };