|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["IdentityManager"]; |
|
8 |
|
9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
10 |
|
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"); |
|
16 |
|
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 } |
|
23 |
|
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")]; |
|
64 |
|
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, |
|
75 |
|
76 _basicPassword: null, |
|
77 _basicPasswordAllowLookup: true, |
|
78 _basicPasswordUpdated: false, |
|
79 |
|
80 _syncKey: null, |
|
81 _syncKeyAllowLookup: true, |
|
82 _syncKeySet: false, |
|
83 |
|
84 _syncKeyBundle: null, |
|
85 |
|
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 }, |
|
95 |
|
96 finalize: function() { |
|
97 // Nothing to do for this identity provider. |
|
98 return Promise.resolve(); |
|
99 }, |
|
100 |
|
101 /** |
|
102 * Called whenever Service.logout() is called. |
|
103 */ |
|
104 logout: function() { |
|
105 // nothing to do for this identity provider. |
|
106 }, |
|
107 |
|
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 }, |
|
116 |
|
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 }, |
|
124 |
|
125 get account() { |
|
126 return Svc.Prefs.get("account", this.username); |
|
127 }, |
|
128 |
|
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 } |
|
148 |
|
149 this.username = this.usernameFromAccount(value); |
|
150 }, |
|
151 |
|
152 get username() { |
|
153 return Svc.Prefs.get("username", null); |
|
154 }, |
|
155 |
|
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(); |
|
164 |
|
165 if (value == this.username) { |
|
166 return; |
|
167 } |
|
168 |
|
169 Svc.Prefs.set("username", value); |
|
170 } else { |
|
171 Svc.Prefs.reset("username"); |
|
172 } |
|
173 |
|
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 }, |
|
179 |
|
180 /** |
|
181 * Resets/Drops all credentials we hold for the current user. |
|
182 */ |
|
183 resetCredentials: function() { |
|
184 this.basicPassword = null; |
|
185 this.resetSyncKey(); |
|
186 }, |
|
187 |
|
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 }, |
|
195 |
|
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 } |
|
208 |
|
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 } |
|
215 |
|
216 this._basicPasswordAllowLookup = false; |
|
217 } |
|
218 |
|
219 return this._basicPassword; |
|
220 }, |
|
221 |
|
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 } |
|
236 |
|
237 let username = this.username; |
|
238 if (!username) { |
|
239 throw new Error("basicPassword cannot be set before username."); |
|
240 } |
|
241 |
|
242 this._log.info("Basic password being updated."); |
|
243 this._basicPassword = Utils.encodeUTF8(value); |
|
244 this._basicPasswordUpdated = true; |
|
245 }, |
|
246 |
|
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 } |
|
262 |
|
263 for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) { |
|
264 if (login.username.toLowerCase() == username) { |
|
265 this._syncKey = login.password; |
|
266 } |
|
267 } |
|
268 |
|
269 this._syncKeyAllowLookup = false; |
|
270 } |
|
271 |
|
272 return this._syncKey; |
|
273 }, |
|
274 |
|
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 } |
|
299 |
|
300 if (!this.username) { |
|
301 throw new Error("syncKey cannot be set before username."); |
|
302 } |
|
303 |
|
304 this._log.info("Sync Key being updated."); |
|
305 this._syncKey = value; |
|
306 |
|
307 // Clear any cached Sync Key Bundle and regenerate it. |
|
308 this._syncKeyBundle = null; |
|
309 let bundle = this.syncKeyBundle; |
|
310 |
|
311 this._syncKeyUpdated = true; |
|
312 }, |
|
313 |
|
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 } |
|
329 |
|
330 if (!this.syncKey) { |
|
331 this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " + |
|
332 "set!"); |
|
333 return null; |
|
334 } |
|
335 |
|
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 } |
|
344 |
|
345 return this._syncKeyBundle; |
|
346 }, |
|
347 |
|
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 } |
|
358 |
|
359 if (Utils.mpLocked()) { |
|
360 return STATUS_OK; |
|
361 } |
|
362 |
|
363 if (!this.basicPassword) { |
|
364 return LOGIN_FAILED_NO_PASSWORD; |
|
365 } |
|
366 |
|
367 if (!this.syncKey) { |
|
368 return LOGIN_FAILED_NO_PASSPHRASE; |
|
369 } |
|
370 |
|
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 } |
|
376 |
|
377 return STATUS_OK; |
|
378 }, |
|
379 |
|
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 } |
|
399 |
|
400 this._basicPasswordUpdated = false; |
|
401 } |
|
402 |
|
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 } |
|
411 |
|
412 this._syncKeyUpdated = false; |
|
413 } |
|
414 |
|
415 }, |
|
416 |
|
417 /** |
|
418 * Deletes the Sync Key from the system. |
|
419 */ |
|
420 deleteSyncKey: function deleteSyncKey() { |
|
421 this.syncKey = null; |
|
422 this.persistCredentials(); |
|
423 }, |
|
424 |
|
425 hasBasicCredentials: function hasBasicCredentials() { |
|
426 // Because JavaScript. |
|
427 return this.username && this.basicPassword && true; |
|
428 }, |
|
429 |
|
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 }, |
|
436 |
|
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 } |
|
453 |
|
454 if (exists) { |
|
455 return; |
|
456 } |
|
457 |
|
458 this._log.debug("Updating saved password for " + username + " in " + |
|
459 realm); |
|
460 |
|
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 }, |
|
467 |
|
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 } |
|
478 |
|
479 // Wait until after store is updated in case it fails. |
|
480 this._basicPassword = null; |
|
481 this._basicPasswordAllowLookup = true; |
|
482 this._basicPasswordUpdated = false; |
|
483 |
|
484 this._syncKey = null; |
|
485 // this._syncKeyBundle is nullified as part of _syncKey setter. |
|
486 this._syncKeyAllowLookup = true; |
|
487 this._syncKeyUpdated = false; |
|
488 }, |
|
489 |
|
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 } |
|
496 |
|
497 return value ? value.toLowerCase() : value; |
|
498 }, |
|
499 |
|
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 } |
|
507 |
|
508 return null; |
|
509 }, |
|
510 |
|
511 /** |
|
512 * Helper method to return an authenticator for basic Resource requests. |
|
513 */ |
|
514 getBasicResourceAuthenticator: |
|
515 function getBasicResourceAuthenticator(username, password) { |
|
516 |
|
517 return function basicAuthenticator(resource) { |
|
518 let value = "Basic " + btoa(username + ":" + password); |
|
519 return {headers: {authorization: value}}; |
|
520 }; |
|
521 }, |
|
522 |
|
523 _onResourceRequestBasic: function _onResourceRequestBasic(resource) { |
|
524 let value = "Basic " + btoa(this.username + ":" + this.basicPassword); |
|
525 return {headers: {authorization: value}}; |
|
526 }, |
|
527 |
|
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); |
|
533 |
|
534 return {headers: {authorization: result.header}}; |
|
535 }, |
|
536 |
|
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 } |
|
544 |
|
545 return null; |
|
546 }, |
|
547 |
|
548 onRESTRequestBasic: function onRESTRequestBasic(request) { |
|
549 let up = this.username + ":" + this.basicPassword; |
|
550 request.setHeader("authorization", "Basic " + btoa(up)); |
|
551 }, |
|
552 |
|
553 createClusterManager: function(service) { |
|
554 Cu.import("resource://services-sync/stages/cluster.js"); |
|
555 return new ClusterManager(service); |
|
556 }, |
|
557 |
|
558 offerSyncOptions: function () { |
|
559 // Do nothing for Sync 1.1. |
|
560 return {accepted: true}; |
|
561 }, |
|
562 }; |