Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["IdentityManager"];
9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/Promise.jsm");
13 Cu.import("resource://services-sync/constants.js");
14 Cu.import("resource://gre/modules/Log.jsm");
15 Cu.import("resource://services-sync/util.js");
17 // Lazy import to prevent unnecessary load on startup.
18 for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
19 XPCOMUtils.defineLazyModuleGetter(this, symbol,
20 "resource://services-sync/keys.js",
21 symbol);
22 }
24 /**
25 * Manages "legacy" identity and authentication for Sync.
26 * See browserid_identity for the Firefox Accounts based identity manager.
27 *
28 * The following entities are managed:
29 *
30 * account - The main Sync/services account. This is typically an email
31 * address.
32 * username - A normalized version of your account. This is what's
33 * transmitted to the server.
34 * basic password - UTF-8 password used for authenticating when using HTTP
35 * basic authentication.
36 * sync key - The main encryption key used by Sync.
37 * sync key bundle - A representation of your sync key.
38 *
39 * When changes are made to entities that are stored in the password manager
40 * (basic password, sync key), those changes are merely staged. To commit them
41 * to the password manager, you'll need to call persistCredentials().
42 *
43 * This type also manages authenticating Sync's network requests. Sync's
44 * network code calls into getRESTRequestAuthenticator and
45 * getResourceAuthenticator (depending on the network layer being used). Each
46 * returns a function which can be used to add authentication information to an
47 * outgoing request.
48 *
49 * In theory, this type supports arbitrary identity and authentication
50 * mechanisms. You can add support for them by monkeypatching the global
51 * instance of this type. Specifically, you'll need to redefine the
52 * aforementioned network code functions to do whatever your authentication
53 * mechanism needs them to do. In addition, you may wish to install custom
54 * functions to support your API. Although, that is certainly not required.
55 * If you do monkeypatch, please be advised that Sync expects the core
56 * attributes to have values. You will need to carry at least account and
57 * username forward. If you do not wish to support one of the built-in
58 * authentication mechanisms, you'll probably want to redefine currentAuthState
59 * and any other function that involves the built-in functionality.
60 */
61 this.IdentityManager = function IdentityManager() {
62 this._log = Log.repository.getLogger("Sync.Identity");
63 this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")];
65 this._basicPassword = null;
66 this._basicPasswordAllowLookup = true;
67 this._basicPasswordUpdated = false;
68 this._syncKey = null;
69 this._syncKeyAllowLookup = true;
70 this._syncKeySet = false;
71 this._syncKeyBundle = null;
72 }
73 IdentityManager.prototype = {
74 _log: null,
76 _basicPassword: null,
77 _basicPasswordAllowLookup: true,
78 _basicPasswordUpdated: false,
80 _syncKey: null,
81 _syncKeyAllowLookup: true,
82 _syncKeySet: false,
84 _syncKeyBundle: null,
86 /**
87 * Initialize the identity provider. Returns a promise that is resolved
88 * when initialization is complete and the provider can be queried for
89 * its state
90 */
91 initialize: function() {
92 // Nothing to do for this identity provider.
93 return Promise.resolve();
94 },
96 finalize: function() {
97 // Nothing to do for this identity provider.
98 return Promise.resolve();
99 },
101 /**
102 * Called whenever Service.logout() is called.
103 */
104 logout: function() {
105 // nothing to do for this identity provider.
106 },
108 /**
109 * Ensure the user is logged in. Returns a promise that resolves when
110 * the user is logged in, or is rejected if the login attempt has failed.
111 */
112 ensureLoggedIn: function() {
113 // nothing to do for this identity provider
114 return Promise.resolve();
115 },
117 /**
118 * Indicates if the identity manager is still initializing
119 */
120 get readyToAuthenticate() {
121 // We initialize in a fully sync manner, so we are always finished.
122 return true;
123 },
125 get account() {
126 return Svc.Prefs.get("account", this.username);
127 },
129 /**
130 * Sets the active account name.
131 *
132 * This should almost always be called in favor of setting username, as
133 * username is derived from account.
134 *
135 * Changing the account name has the side-effect of wiping out stored
136 * credentials. Keep in mind that persistCredentials() will need to be called
137 * to flush the changes to disk.
138 *
139 * Set this value to null to clear out identity information.
140 */
141 set account(value) {
142 if (value) {
143 value = value.toLowerCase();
144 Svc.Prefs.set("account", value);
145 } else {
146 Svc.Prefs.reset("account");
147 }
149 this.username = this.usernameFromAccount(value);
150 },
152 get username() {
153 return Svc.Prefs.get("username", null);
154 },
156 /**
157 * Set the username value.
158 *
159 * Changing the username has the side-effect of wiping credentials.
160 */
161 set username(value) {
162 if (value) {
163 value = value.toLowerCase();
165 if (value == this.username) {
166 return;
167 }
169 Svc.Prefs.set("username", value);
170 } else {
171 Svc.Prefs.reset("username");
172 }
174 // If we change the username, we interpret this as a major change event
175 // and wipe out the credentials.
176 this._log.info("Username changed. Removing stored credentials.");
177 this.resetCredentials();
178 },
180 /**
181 * Resets/Drops all credentials we hold for the current user.
182 */
183 resetCredentials: function() {
184 this.basicPassword = null;
185 this.resetSyncKey();
186 },
188 /**
189 * Resets/Drops the sync key we hold for the current user.
190 */
191 resetSyncKey: function() {
192 this.syncKey = null;
193 // syncKeyBundle cleared as a result of setting syncKey.
194 },
196 /**
197 * Obtains the HTTP Basic auth password.
198 *
199 * Returns a string if set or null if it is not set.
200 */
201 get basicPassword() {
202 if (this._basicPasswordAllowLookup) {
203 // We need a username to find the credentials.
204 let username = this.username;
205 if (!username) {
206 return null;
207 }
209 for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
210 if (login.username.toLowerCase() == username) {
211 // It should already be UTF-8 encoded, but we don't take any chances.
212 this._basicPassword = Utils.encodeUTF8(login.password);
213 }
214 }
216 this._basicPasswordAllowLookup = false;
217 }
219 return this._basicPassword;
220 },
222 /**
223 * Set the HTTP basic password to use.
224 *
225 * Changes will not persist unless persistSyncCredentials() is called.
226 */
227 set basicPassword(value) {
228 // Wiping out value.
229 if (!value) {
230 this._log.info("Basic password has no value. Removing.");
231 this._basicPassword = null;
232 this._basicPasswordUpdated = true;
233 this._basicPasswordAllowLookup = false;
234 return;
235 }
237 let username = this.username;
238 if (!username) {
239 throw new Error("basicPassword cannot be set before username.");
240 }
242 this._log.info("Basic password being updated.");
243 this._basicPassword = Utils.encodeUTF8(value);
244 this._basicPasswordUpdated = true;
245 },
247 /**
248 * Obtain the Sync Key.
249 *
250 * This returns a 26 character "friendly" Base32 encoded string on success or
251 * null if no Sync Key could be found.
252 *
253 * If the Sync Key hasn't been set in this session, this will look in the
254 * password manager for the sync key.
255 */
256 get syncKey() {
257 if (this._syncKeyAllowLookup) {
258 let username = this.username;
259 if (!username) {
260 return null;
261 }
263 for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
264 if (login.username.toLowerCase() == username) {
265 this._syncKey = login.password;
266 }
267 }
269 this._syncKeyAllowLookup = false;
270 }
272 return this._syncKey;
273 },
275 /**
276 * Set the active Sync Key.
277 *
278 * If being set to null, the Sync Key and its derived SyncKeyBundle are
279 * removed. However, the Sync Key won't be deleted from the password manager
280 * until persistSyncCredentials() is called.
281 *
282 * If a value is provided, it should be a 26 or 32 character "friendly"
283 * Base32 string for which Utils.isPassphrase() returns true.
284 *
285 * A side-effect of setting the Sync Key is that a SyncKeyBundle is
286 * generated. For historical reasons, this will silently error out if the
287 * value is not a proper Sync Key (!Utils.isPassphrase()). This should be
288 * fixed in the future (once service.js is more sane) to throw if the passed
289 * value is not valid.
290 */
291 set syncKey(value) {
292 if (!value) {
293 this._log.info("Sync Key has no value. Deleting.");
294 this._syncKey = null;
295 this._syncKeyBundle = null;
296 this._syncKeyUpdated = true;
297 return;
298 }
300 if (!this.username) {
301 throw new Error("syncKey cannot be set before username.");
302 }
304 this._log.info("Sync Key being updated.");
305 this._syncKey = value;
307 // Clear any cached Sync Key Bundle and regenerate it.
308 this._syncKeyBundle = null;
309 let bundle = this.syncKeyBundle;
311 this._syncKeyUpdated = true;
312 },
314 /**
315 * Obtain the active SyncKeyBundle.
316 *
317 * This returns a SyncKeyBundle representing a key pair derived from the
318 * Sync Key on success. If no Sync Key is present or if the Sync Key is not
319 * valid, this returns null.
320 *
321 * The SyncKeyBundle should be treated as immutable.
322 */
323 get syncKeyBundle() {
324 // We can't obtain a bundle without a username set.
325 if (!this.username) {
326 this._log.warn("Attempted to obtain Sync Key Bundle with no username set!");
327 return null;
328 }
330 if (!this.syncKey) {
331 this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " +
332 "set!");
333 return null;
334 }
336 if (!this._syncKeyBundle) {
337 try {
338 this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey);
339 } catch (ex) {
340 this._log.warn(Utils.exceptionStr(ex));
341 return null;
342 }
343 }
345 return this._syncKeyBundle;
346 },
348 /**
349 * The current state of the auth credentials.
350 *
351 * This essentially validates that enough credentials are available to use
352 * Sync.
353 */
354 get currentAuthState() {
355 if (!this.username) {
356 return LOGIN_FAILED_NO_USERNAME;
357 }
359 if (Utils.mpLocked()) {
360 return STATUS_OK;
361 }
363 if (!this.basicPassword) {
364 return LOGIN_FAILED_NO_PASSWORD;
365 }
367 if (!this.syncKey) {
368 return LOGIN_FAILED_NO_PASSPHRASE;
369 }
371 // If we have a Sync Key but no bundle, bundle creation failed, which
372 // implies a bad Sync Key.
373 if (!this.syncKeyBundle) {
374 return LOGIN_FAILED_INVALID_PASSPHRASE;
375 }
377 return STATUS_OK;
378 },
380 /**
381 * Persist credentials to password store.
382 *
383 * When credentials are updated, they are changed in memory only. This will
384 * need to be called to save them to the underlying password store.
385 *
386 * If the password store is locked (e.g. if the master password hasn't been
387 * entered), this could throw an exception.
388 */
389 persistCredentials: function persistCredentials(force) {
390 if (this._basicPasswordUpdated || force) {
391 if (this._basicPassword) {
392 this._setLogin(PWDMGR_PASSWORD_REALM, this.username,
393 this._basicPassword);
394 } else {
395 for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
396 Services.logins.removeLogin(login);
397 }
398 }
400 this._basicPasswordUpdated = false;
401 }
403 if (this._syncKeyUpdated || force) {
404 if (this._syncKey) {
405 this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey);
406 } else {
407 for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
408 Services.logins.removeLogin(login);
409 }
410 }
412 this._syncKeyUpdated = false;
413 }
415 },
417 /**
418 * Deletes the Sync Key from the system.
419 */
420 deleteSyncKey: function deleteSyncKey() {
421 this.syncKey = null;
422 this.persistCredentials();
423 },
425 hasBasicCredentials: function hasBasicCredentials() {
426 // Because JavaScript.
427 return this.username && this.basicPassword && true;
428 },
430 /**
431 * Obtains the array of basic logins from nsiPasswordManager.
432 */
433 _getLogins: function _getLogins(realm) {
434 return Services.logins.findLogins({}, PWDMGR_HOST, null, realm);
435 },
437 /**
438 * Set a login in the password manager.
439 *
440 * This has the side-effect of deleting any other logins for the specified
441 * realm.
442 */
443 _setLogin: function _setLogin(realm, username, password) {
444 let exists = false;
445 for each (let login in this._getLogins(realm)) {
446 if (login.username == username && login.password == password) {
447 exists = true;
448 } else {
449 this._log.debug("Pruning old login for " + username + " from " + realm);
450 Services.logins.removeLogin(login);
451 }
452 }
454 if (exists) {
455 return;
456 }
458 this._log.debug("Updating saved password for " + username + " in " +
459 realm);
461 let loginInfo = new Components.Constructor(
462 "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
463 let login = new loginInfo(PWDMGR_HOST, null, realm, username,
464 password, "", "");
465 Services.logins.addLogin(login);
466 },
468 /**
469 * Deletes Sync credentials from the password manager.
470 */
471 deleteSyncCredentials: function deleteSyncCredentials() {
472 for (let host of Utils.getSyncCredentialsHosts()) {
473 let logins = Services.logins.findLogins({}, host, "", "");
474 for each (let login in logins) {
475 Services.logins.removeLogin(login);
476 }
477 }
479 // Wait until after store is updated in case it fails.
480 this._basicPassword = null;
481 this._basicPasswordAllowLookup = true;
482 this._basicPasswordUpdated = false;
484 this._syncKey = null;
485 // this._syncKeyBundle is nullified as part of _syncKey setter.
486 this._syncKeyAllowLookup = true;
487 this._syncKeyUpdated = false;
488 },
490 usernameFromAccount: function usernameFromAccount(value) {
491 // If we encounter characters not allowed by the API (as found for
492 // instance in an email address), hash the value.
493 if (value && value.match(/[^A-Z0-9._-]/i)) {
494 return Utils.sha1Base32(value.toLowerCase()).toLowerCase();
495 }
497 return value ? value.toLowerCase() : value;
498 },
500 /**
501 * Obtain a function to be used for adding auth to Resource HTTP requests.
502 */
503 getResourceAuthenticator: function getResourceAuthenticator() {
504 if (this.hasBasicCredentials()) {
505 return this._onResourceRequestBasic.bind(this);
506 }
508 return null;
509 },
511 /**
512 * Helper method to return an authenticator for basic Resource requests.
513 */
514 getBasicResourceAuthenticator:
515 function getBasicResourceAuthenticator(username, password) {
517 return function basicAuthenticator(resource) {
518 let value = "Basic " + btoa(username + ":" + password);
519 return {headers: {authorization: value}};
520 };
521 },
523 _onResourceRequestBasic: function _onResourceRequestBasic(resource) {
524 let value = "Basic " + btoa(this.username + ":" + this.basicPassword);
525 return {headers: {authorization: value}};
526 },
528 _onResourceRequestMAC: function _onResourceRequestMAC(resource, method) {
529 // TODO Get identifier and key from somewhere.
530 let identifier;
531 let key;
532 let result = Utils.computeHTTPMACSHA1(identifier, key, method, resource.uri);
534 return {headers: {authorization: result.header}};
535 },
537 /**
538 * Obtain a function to be used for adding auth to RESTRequest instances.
539 */
540 getRESTRequestAuthenticator: function getRESTRequestAuthenticator() {
541 if (this.hasBasicCredentials()) {
542 return this.onRESTRequestBasic.bind(this);
543 }
545 return null;
546 },
548 onRESTRequestBasic: function onRESTRequestBasic(request) {
549 let up = this.username + ":" + this.basicPassword;
550 request.setHeader("authorization", "Basic " + btoa(up));
551 },
553 createClusterManager: function(service) {
554 Cu.import("resource://services-sync/stages/cluster.js");
555 return new ClusterManager(service);
556 },
558 offerSyncOptions: function () {
559 // Do nothing for Sync 1.1.
560 return {accepted: true};
561 },
562 };