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