зеркало из https://github.com/mozilla/gecko-dev.git
489 строки
14 KiB
JavaScript
489 строки
14 KiB
JavaScript
/* 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"];
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|
|
|
Cu.import("resource://services-sync/constants.js");
|
|
Cu.import("resource://services-sync/keys.js");
|
|
Cu.import("resource://services-common/log4moz.js");
|
|
Cu.import("resource://services-sync/util.js");
|
|
|
|
/**
|
|
* 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 = Log4Moz.repository.getLogger("Sync.Identity");
|
|
this._log.Level = Log4Moz.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,
|
|
|
|
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.basicPassword = null;
|
|
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 each (let login in 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 each (let login in 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(Utils.exceptionStr(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;
|
|
},
|
|
|
|
/**
|
|
* 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 each (let login in 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 each (let login in 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;
|
|
},
|
|
|
|
/**
|
|
* 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 each (let login in 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);
|
|
},
|
|
|
|
/**
|
|
* Deletes Sync credentials from the password manager.
|
|
*/
|
|
deleteSyncCredentials: function deleteSyncCredentials() {
|
|
let logins = Services.logins.findLogins({}, PWDMGR_HOST, "", "");
|
|
for each (let login in 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));
|
|
}
|
|
};
|