summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/FxAccountsStorage.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/FxAccountsStorage.jsm')
-rw-r--r--services/fxaccounts/FxAccountsStorage.jsm606
1 files changed, 0 insertions, 606 deletions
diff --git a/services/fxaccounts/FxAccountsStorage.jsm b/services/fxaccounts/FxAccountsStorage.jsm
deleted file mode 100644
index 4362cdf5b..000000000
--- a/services/fxaccounts/FxAccountsStorage.jsm
+++ /dev/null
@@ -1,606 +0,0 @@
-/* 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 = [
- "FxAccountsStorageManagerCanStoreField",
- "FxAccountsStorageManager",
-];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/FxAccountsCommon.js");
-Cu.import("resource://gre/modules/osfile.jsm");
-Cu.import("resource://services-common/utils.js");
-
-var haveLoginManager = true;
-
-// A helper function so code can check what fields are able to be stored by
-// the storage manager without having a reference to a manager instance.
-function FxAccountsStorageManagerCanStoreField(fieldName) {
- return FXA_PWDMGR_MEMORY_FIELDS.has(fieldName) ||
- FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName) ||
- FXA_PWDMGR_SECURE_FIELDS.has(fieldName);
-}
-
-// The storage manager object.
-this.FxAccountsStorageManager = function(options = {}) {
- this.options = {
- filename: options.filename || DEFAULT_STORAGE_FILENAME,
- baseDir: options.baseDir || OS.Constants.Path.profileDir,
- }
- this.plainStorage = new JSONStorage(this.options);
- // On b2g we have no loginManager for secure storage, and tests may want
- // to pretend secure storage isn't available.
- let useSecure = 'useSecure' in options ? options.useSecure : haveLoginManager;
- if (useSecure) {
- this.secureStorage = new LoginManagerStorage();
- } else {
- this.secureStorage = null;
- }
- this._clearCachedData();
- // See .initialize() below - this protects against it not being called.
- this._promiseInitialized = Promise.reject("initialize not called");
- // A promise to avoid storage races - see _queueStorageOperation
- this._promiseStorageComplete = Promise.resolve();
-}
-
-this.FxAccountsStorageManager.prototype = {
- _initialized: false,
- _needToReadSecure: true,
-
- // An initialization routine that *looks* synchronous to the callers, but
- // is actually async as everything else waits for it to complete.
- initialize(accountData) {
- if (this._initialized) {
- throw new Error("already initialized");
- }
- this._initialized = true;
- // If we just throw away our pre-rejected promise it is reported as an
- // unhandled exception when it is GCd - so add an empty .catch handler here
- // to prevent this.
- this._promiseInitialized.catch(() => {});
- this._promiseInitialized = this._initialize(accountData);
- },
-
- _initialize: Task.async(function* (accountData) {
- log.trace("initializing new storage manager");
- try {
- if (accountData) {
- // If accountData is passed we don't need to read any storage.
- this._needToReadSecure = false;
- // split it into the 2 parts, write it and we are done.
- for (let [name, val] of Object.entries(accountData)) {
- if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
- this.cachedPlain[name] = val;
- } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
- this.cachedSecure[name] = val;
- } else {
- // Hopefully it's an "in memory" field. If it's not we log a warning
- // but still treat it as such (so it will still be available in this
- // session but isn't persisted anywhere.)
- if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
- log.warn("Unknown FxA field name in user data, treating as in-memory", name);
- }
- this.cachedMemory[name] = val;
- }
- }
- // write it out and we are done.
- yield this._write();
- return;
- }
- // So we were initialized without account data - that means we need to
- // read the state from storage. We try and read plain storage first and
- // only attempt to read secure storage if the plain storage had a user.
- this._needToReadSecure = yield this._readPlainStorage();
- if (this._needToReadSecure && this.secureStorage) {
- yield this._doReadAndUpdateSecure();
- }
- } finally {
- log.trace("initializing of new storage manager done");
- }
- }),
-
- finalize() {
- // We can't throw this instance away while it is still writing or we may
- // end up racing with the newly created one.
- log.trace("StorageManager finalizing");
- return this._promiseInitialized.then(() => {
- return this._promiseStorageComplete;
- }).then(() => {
- this._promiseStorageComplete = null;
- this._promiseInitialized = null;
- this._clearCachedData();
- log.trace("StorageManager finalized");
- })
- },
-
- // We want to make sure we don't end up doing multiple storage requests
- // concurrently - which has a small window for reads if the master-password
- // is locked at initialization time and becomes unlocked later, and always
- // has an opportunity for updates.
- // We also want to make sure we finished writing when finalizing, so we
- // can't accidentally end up with the previous user's write finishing after
- // a signOut attempts to clear it.
- // So all such operations "queue" themselves via this.
- _queueStorageOperation(func) {
- // |result| is the promise we return - it has no .catch handler, so callers
- // of the storage operation still see failure as a normal rejection.
- let result = this._promiseStorageComplete.then(func);
- // But the promise we assign to _promiseStorageComplete *does* have a catch
- // handler so that rejections in one storage operation does not prevent
- // future operations from starting (ie, _promiseStorageComplete must never
- // be in a rejected state)
- this._promiseStorageComplete = result.catch(err => {
- log.error("${func} failed: ${err}", {func, err});
- });
- return result;
- },
-
- // Get the account data by combining the plain and secure storage.
- // If fieldNames is specified, it may be a string or an array of strings,
- // and only those fields are returned. If not specified the entire account
- // data is returned except for "in memory" fields. Note that not specifying
- // field names will soon be deprecated/removed - we want all callers to
- // specify the fields they care about.
- getAccountData: Task.async(function* (fieldNames = null) {
- yield this._promiseInitialized;
- // We know we are initialized - this means our .cachedPlain is accurate
- // and doesn't need to be read (it was read if necessary by initialize).
- // So if there's no uid, there's no user signed in.
- if (!('uid' in this.cachedPlain)) {
- return null;
- }
- let result = {};
- if (fieldNames === null) {
- // The "old" deprecated way of fetching a logged in user.
- for (let [name, value] of Object.entries(this.cachedPlain)) {
- result[name] = value;
- }
- // But the secure data may not have been read, so try that now.
- yield this._maybeReadAndUpdateSecure();
- // .cachedSecure now has as much as it possibly can (which is possibly
- // nothing if (a) secure storage remains locked and (b) we've never updated
- // a field to be stored in secure storage.)
- for (let [name, value] of Object.entries(this.cachedSecure)) {
- result[name] = value;
- }
- // Note we don't return cachedMemory fields here - they must be explicitly
- // requested.
- return result;
- }
- // The new explicit way of getting attributes.
- if (!Array.isArray(fieldNames)) {
- fieldNames = [fieldNames];
- }
- let checkedSecure = false;
- for (let fieldName of fieldNames) {
- if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) {
- if (this.cachedMemory[fieldName] !== undefined) {
- result[fieldName] = this.cachedMemory[fieldName];
- }
- } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
- if (this.cachedPlain[fieldName] !== undefined) {
- result[fieldName] = this.cachedPlain[fieldName];
- }
- } else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) {
- // We may not have read secure storage yet.
- if (!checkedSecure) {
- yield this._maybeReadAndUpdateSecure();
- checkedSecure = true;
- }
- if (this.cachedSecure[fieldName] !== undefined) {
- result[fieldName] = this.cachedSecure[fieldName];
- }
- } else {
- throw new Error("unexpected field '" + name + "'");
- }
- }
- return result;
- }),
-
- // Update just the specified fields. This DOES NOT allow you to change to
- // a different user, nor to set the user as signed-out.
- updateAccountData: Task.async(function* (newFields) {
- yield this._promiseInitialized;
- if (!('uid' in this.cachedPlain)) {
- // If this storage instance shows no logged in user, then you can't
- // update fields.
- throw new Error("No user is logged in");
- }
- if (!newFields || 'uid' in newFields || 'email' in newFields) {
- // Once we support
- // user changing email address this may need to change, but it's not
- // clear how we would be told of such a change anyway...
- throw new Error("Can't change uid or email address");
- }
- log.debug("_updateAccountData with items", Object.keys(newFields));
- // work out what bucket.
- for (let [name, value] of Object.entries(newFields)) {
- if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
- if (value == null) {
- delete this.cachedMemory[name];
- } else {
- this.cachedMemory[name] = value;
- }
- } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
- if (value == null) {
- delete this.cachedPlain[name];
- } else {
- this.cachedPlain[name] = value;
- }
- } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
- // don't do the "delete on null" thing here - we need to keep it until
- // we have managed to read so we can nuke it on write.
- this.cachedSecure[name] = value;
- } else {
- // Throwing seems reasonable here as some client code has explicitly
- // specified the field name, so it's either confused or needs to update
- // how this field is to be treated.
- throw new Error("unexpected field '" + name + "'");
- }
- }
- // If we haven't yet read the secure data, do so now, else we may write
- // out partial data.
- yield this._maybeReadAndUpdateSecure();
- // Now save it - but don't wait on the _write promise - it's queued up as
- // a storage operation, so .finalize() will wait for completion, but no need
- // for us to.
- this._write();
- }),
-
- _clearCachedData() {
- this.cachedMemory = {};
- this.cachedPlain = {};
- // If we don't have secure storage available we have cachedPlain and
- // cachedSecure be the same object.
- this.cachedSecure = this.secureStorage == null ? this.cachedPlain : {};
- },
-
- /* Reads the plain storage and caches the read values in this.cachedPlain.
- Only ever called once and unlike the "secure" storage, is expected to never
- fail (ie, plain storage is considered always available, whereas secure
- storage may be unavailable if it is locked).
-
- Returns a promise that resolves with true if valid account data was found,
- false otherwise.
-
- Note: _readPlainStorage is only called during initialize, so isn't
- protected via _queueStorageOperation() nor _promiseInitialized.
- */
- _readPlainStorage: Task.async(function* () {
- let got;
- try {
- got = yield this.plainStorage.get();
- } catch(err) {
- // File hasn't been created yet. That will be done
- // when write is called.
- if (!(err instanceof OS.File.Error) || !err.becauseNoSuchFile) {
- log.error("Failed to read plain storage", err);
- }
- // either way, we return null.
- got = null;
- }
- if (!got || !got.accountData || !got.accountData.uid ||
- got.version != DATA_FORMAT_VERSION) {
- return false;
- }
- // We need to update our .cachedPlain, but can't just assign to it as
- // it may need to be the exact same object as .cachedSecure
- // As a sanity check, .cachedPlain must be empty (as we are called by init)
- // XXX - this would be a good use-case for a RuntimeAssert or similar, as
- // being added in bug 1080457.
- if (Object.keys(this.cachedPlain).length != 0) {
- throw new Error("should be impossible to have cached data already.")
- }
- for (let [name, value] of Object.entries(got.accountData)) {
- this.cachedPlain[name] = value;
- }
- return true;
- }),
-
- /* If we haven't managed to read the secure storage, try now, so
- we can merge our cached data with the data that's already been set.
- */
- _maybeReadAndUpdateSecure: Task.async(function* () {
- if (this.secureStorage == null || !this._needToReadSecure) {
- return;
- }
- return this._queueStorageOperation(() => {
- if (this._needToReadSecure) { // we might have read it by now!
- return this._doReadAndUpdateSecure();
- }
- });
- }),
-
- /* Unconditionally read the secure storage and merge our cached data (ie, data
- which has already been set while the secure storage was locked) with
- the read data
- */
- _doReadAndUpdateSecure: Task.async(function* () {
- let { uid, email } = this.cachedPlain;
- try {
- log.debug("reading secure storage with existing", Object.keys(this.cachedSecure));
- // If we already have anything in .cachedSecure it means something has
- // updated cachedSecure before we've read it. That means that after we do
- // manage to read we must write back the merged data.
- let needWrite = Object.keys(this.cachedSecure).length != 0;
- let readSecure = yield this.secureStorage.get(uid, email);
- // and update our cached data with it - anything already in .cachedSecure
- // wins (including the fact it may be null or undefined, the latter
- // which means it will be removed from storage.
- if (readSecure && readSecure.version != DATA_FORMAT_VERSION) {
- log.warn("got secure data but the data format version doesn't match");
- readSecure = null;
- }
- if (readSecure && readSecure.accountData) {
- log.debug("secure read fetched items", Object.keys(readSecure.accountData));
- for (let [name, value] of Object.entries(readSecure.accountData)) {
- if (!(name in this.cachedSecure)) {
- this.cachedSecure[name] = value;
- }
- }
- if (needWrite) {
- log.debug("successfully read secure data; writing updated data back")
- yield this._doWriteSecure();
- }
- }
- this._needToReadSecure = false;
- } catch (ex) {
- if (ex instanceof this.secureStorage.STORAGE_LOCKED) {
- log.debug("setAccountData: secure storage is locked trying to read");
- } else {
- log.error("failed to read secure storage", ex);
- throw ex;
- }
- }
- }),
-
- _write() {
- // We don't want multiple writes happening concurrently, and we also need to
- // know when an "old" storage manager is done (this.finalize() waits for this)
- return this._queueStorageOperation(() => this.__write());
- },
-
- __write: Task.async(function* () {
- // Write everything back - later we could track what's actually dirty,
- // but for now we write it all.
- log.debug("writing plain storage", Object.keys(this.cachedPlain));
- let toWritePlain = {
- version: DATA_FORMAT_VERSION,
- accountData: this.cachedPlain,
- }
- yield this.plainStorage.set(toWritePlain);
-
- // If we have no secure storage manager we are done.
- if (this.secureStorage == null) {
- return;
- }
- // and only attempt to write to secure storage if we've managed to read it,
- // otherwise we might clobber data that's already there.
- if (!this._needToReadSecure) {
- yield this._doWriteSecure();
- }
- }),
-
- /* Do the actual write of secure data. Caller is expected to check if we actually
- need to write and to ensure we are in a queued storage operation.
- */
- _doWriteSecure: Task.async(function* () {
- // We need to remove null items here.
- for (let [name, value] of Object.entries(this.cachedSecure)) {
- if (value == null) {
- delete this.cachedSecure[name];
- }
- }
- log.debug("writing secure storage", Object.keys(this.cachedSecure));
- let toWriteSecure = {
- version: DATA_FORMAT_VERSION,
- accountData: this.cachedSecure,
- }
- try {
- yield this.secureStorage.set(this.cachedPlain.uid, toWriteSecure);
- } catch (ex) {
- if (!(ex instanceof this.secureStorage.STORAGE_LOCKED)) {
- throw ex;
- }
- // This shouldn't be possible as once it is unlocked it can't be
- // re-locked, and we can only be here if we've previously managed to
- // read.
- log.error("setAccountData: secure storage is locked trying to write");
- }
- }),
-
- // Delete the data for an account - ie, called on "sign out".
- deleteAccountData() {
- return this._queueStorageOperation(() => this._deleteAccountData());
- },
-
- _deleteAccountData: Task.async(function* () {
- log.debug("removing account data");
- yield this._promiseInitialized;
- yield this.plainStorage.set(null);
- if (this.secureStorage) {
- yield this.secureStorage.set(null);
- }
- this._clearCachedData();
- log.debug("account data reset");
- }),
-}
-
-/**
- * JSONStorage constructor that creates instances that may set/get
- * to a specified file, in a directory that will be created if it
- * doesn't exist.
- *
- * @param options {
- * filename: of the file to write to
- * baseDir: directory where the file resides
- * }
- * @return instance
- */
-function JSONStorage(options) {
- this.baseDir = options.baseDir;
- this.path = OS.Path.join(options.baseDir, options.filename);
-};
-
-JSONStorage.prototype = {
- set: function(contents) {
- log.trace("starting write of json user data", contents ? Object.keys(contents.accountData) : "null");
- let start = Date.now();
- return OS.File.makeDir(this.baseDir, {ignoreExisting: true})
- .then(CommonUtils.writeJSON.bind(null, contents, this.path))
- .then(result => {
- log.trace("finished write of json user data - took", Date.now()-start);
- return result;
- });
- },
-
- get: function() {
- log.trace("starting fetch of json user data");
- let start = Date.now();
- return CommonUtils.readJSON(this.path).then(result => {
- log.trace("finished fetch of json user data - took", Date.now()-start);
- return result;
- });
- },
-};
-
-function StorageLockedError() {
-}
-/**
- * LoginManagerStorage constructor that creates instances that set/get
- * data stored securely in the nsILoginManager.
- *
- * @return instance
- */
-
-function LoginManagerStorage() {
-}
-
-LoginManagerStorage.prototype = {
- STORAGE_LOCKED: StorageLockedError,
- // The fields in the credentials JSON object that are stored in plain-text
- // in the profile directory. All other fields are stored in the login manager,
- // and thus are only available when the master-password is unlocked.
-
- // a hook point for testing.
- get _isLoggedIn() {
- return Services.logins.isLoggedIn;
- },
-
- // Clear any data from the login manager. Returns true if the login manager
- // was unlocked (even if no existing logins existed) or false if it was
- // locked (meaning we don't even know if it existed or not.)
- _clearLoginMgrData: Task.async(function* () {
- try { // Services.logins might be third-party and broken...
- yield Services.logins.initializationPromise;
- if (!this._isLoggedIn) {
- return false;
- }
- let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
- for (let login of logins) {
- Services.logins.removeLogin(login);
- }
- return true;
- } catch (ex) {
- log.error("Failed to clear login data: ${}", ex);
- return false;
- }
- }),
-
- set: Task.async(function* (uid, contents) {
- if (!contents) {
- // Nuke it from the login manager.
- let cleared = yield this._clearLoginMgrData();
- if (!cleared) {
- // just log a message - we verify that the uid matches when
- // we reload it, so having a stale entry doesn't really hurt.
- log.info("not removing credentials from login manager - not logged in");
- }
- log.trace("storage set finished clearing account data");
- return;
- }
-
- // We are saving actual data.
- log.trace("starting write of user data to the login manager");
- try { // Services.logins might be third-party and broken...
- // and the stuff into the login manager.
- yield Services.logins.initializationPromise;
- // If MP is locked we silently fail - the user may need to re-auth
- // next startup.
- if (!this._isLoggedIn) {
- log.info("not saving credentials to login manager - not logged in");
- throw new this.STORAGE_LOCKED();
- }
- // write the data to the login manager.
- let loginInfo = new Components.Constructor(
- "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
- let login = new loginInfo(FXA_PWDMGR_HOST,
- null, // aFormSubmitURL,
- FXA_PWDMGR_REALM, // aHttpRealm,
- uid, // aUsername
- JSON.stringify(contents), // aPassword
- "", // aUsernameField
- "");// aPasswordField
-
- let existingLogins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null,
- FXA_PWDMGR_REALM);
- if (existingLogins.length) {
- Services.logins.modifyLogin(existingLogins[0], login);
- } else {
- Services.logins.addLogin(login);
- }
- log.trace("finished write of user data to the login manager");
- } catch (ex) {
- if (ex instanceof this.STORAGE_LOCKED) {
- throw ex;
- }
- // just log and consume the error here - it may be a 3rd party login
- // manager replacement that's simply broken.
- log.error("Failed to save data to the login manager", ex);
- }
- }),
-
- get: Task.async(function* (uid, email) {
- log.trace("starting fetch of user data from the login manager");
-
- try { // Services.logins might be third-party and broken...
- // read the data from the login manager and merge it for return.
- yield Services.logins.initializationPromise;
-
- if (!this._isLoggedIn) {
- log.info("returning partial account data as the login manager is locked.");
- throw new this.STORAGE_LOCKED();
- }
-
- let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
- if (logins.length == 0) {
- // This could happen if the MP was locked when we wrote the data.
- log.info("Can't find any credentials in the login manager");
- return null;
- }
- let login = logins[0];
- // Support either the uid or the email as the username - as of bug 1183951
- // we store the uid, but we support having either for b/w compat.
- if (login.username == uid || login.username == email) {
- return JSON.parse(login.password);
- }
- log.info("username in the login manager doesn't match - ignoring it");
- yield this._clearLoginMgrData();
- } catch (ex) {
- if (ex instanceof this.STORAGE_LOCKED) {
- throw ex;
- }
- // just log and consume the error here - it may be a 3rd party login
- // manager replacement that's simply broken.
- log.error("Failed to get data from the login manager", ex);
- }
- return null;
- }),
-}
-