identity.js 18 KB

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