tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

newidentity.js (19673B)


      1 "use strict";
      2 
      3 // Use a lazy getter because NewIdentityButton is declared more than once
      4 // otherwise.
      5 ChromeUtils.defineLazyGetter(this, "NewIdentityButton", () => {
      6  // Logger adapted from CustomizableUI.jsm
      7  const logger = (() => {
      8    const consoleOptions = {
      9      maxLogLevelPref: "browser.new_identity.log_level",
     10      prefix: "NewIdentity",
     11    };
     12    return console.createInstance(consoleOptions);
     13  })();
     14 
     15  const topics = Object.freeze({
     16    newIdentityRequested: "new-identity-requested",
     17  });
     18 
     19  /**
     20   * This class contains the actual implementation of the various step involved
     21   * when running new identity.
     22   */
     23  class NewIdentityImpl {
     24    async run() {
     25      this.disableAllJS();
     26      await this.clearState();
     27      await this.openNewWindow();
     28      this.closeOldWindow();
     29      this.broadcast();
     30    }
     31 
     32    // Disable JS (as a defense-in-depth measure)
     33 
     34    disableAllJS() {
     35      logger.info("Disabling JavaScript");
     36      const enumerator = Services.wm.getEnumerator("navigator:browser");
     37      while (enumerator.hasMoreElements()) {
     38        const win = enumerator.getNext();
     39        this.disableWindowJS(win);
     40      }
     41    }
     42 
     43    disableWindowJS(win) {
     44      const browsers = win.gBrowser?.browsers || [];
     45      for (const browser of browsers) {
     46        if (!browser) {
     47          continue;
     48        }
     49        this.disableBrowserJS(browser);
     50        try {
     51          browser.webNavigation?.stop(browser.webNavigation.STOP_ALL);
     52        } catch (e) {
     53          logger.warn("Could not stop navigation", e, browser.currentURI);
     54        }
     55      }
     56    }
     57 
     58    disableBrowserJS(browser) {
     59      if (!browser) {
     60        return;
     61      }
     62      // Does the following still apply?
     63      // Solution from: https://bugzilla.mozilla.org/show_bug.cgi?id=409737
     64      // XXX: This kills the entire window. We need to redirect
     65      // focus and inform the user via a lightbox.
     66      const eventSuppressor = browser.contentWindow?.windowUtils;
     67      if (browser.browsingContext) {
     68        browser.browsingContext.allowJavascript = false;
     69      }
     70      try {
     71        // My estimation is that this does not get the inner iframe windows,
     72        // but that does not matter, because iframes should be destroyed
     73        // on the next load.
     74        // Should we log when browser.contentWindow is null?
     75        if (browser.contentWindow) {
     76          browser.contentWindow.name = null;
     77          browser.contentWindow.window.name = null;
     78        }
     79      } catch (e) {
     80        logger.warn("Failed to reset window.name", e);
     81      }
     82      eventSuppressor?.suppressEventHandling(true);
     83    }
     84 
     85    // Clear state
     86 
     87    async clearState() {
     88      logger.info("Clearing the state");
     89      this.closeTabs();
     90      this.clearSearchBar();
     91      this.clearPrivateSessionHistory();
     92      this.clearHTTPAuths();
     93      this.clearCryptoTokens();
     94      this.clearOCSPCache();
     95      this.clearSecuritySettings();
     96      this.clearImageCaches();
     97      this.clearStorage();
     98      this.clearPreferencesAndPermissions();
     99      await this.clearData();
    100      await this.reloadAddons();
    101      this.clearConnections();
    102      this.clearPrivateSession();
    103    }
    104 
    105    clearSiteSpecificZoom() {
    106      Services.prefs.setBoolPref(
    107        "browser.zoom.siteSpecific",
    108        !Services.prefs.getBoolPref("browser.zoom.siteSpecific")
    109      );
    110      Services.prefs.setBoolPref(
    111        "browser.zoom.siteSpecific",
    112        !Services.prefs.getBoolPref("browser.zoom.siteSpecific")
    113      );
    114    }
    115 
    116    closeTabs() {
    117      if (
    118        !Services.prefs.getBoolPref("browser.new_identity.close_newnym", true)
    119      ) {
    120        logger.info("Not closing tabs");
    121        return;
    122      }
    123      // TODO: muck around with browser.tabs.warnOnClose.. maybe..
    124      logger.info("Closing tabs...");
    125      const enumerator = Services.wm.getEnumerator("navigator:browser");
    126      const windowsToClose = [];
    127      while (enumerator.hasMoreElements()) {
    128        const win = enumerator.getNext();
    129        const browser = win.gBrowser;
    130        if (!browser) {
    131          logger.warn("No browser for possible window to close");
    132          continue;
    133        }
    134        const tabsToRemove = [];
    135        for (const b of browser.browsers) {
    136          const tab = browser.getTabForBrowser(b);
    137          if (tab) {
    138            tabsToRemove.push(tab);
    139          } else {
    140            logger.warn("Browser has a null tab", b);
    141          }
    142        }
    143        if (win == window) {
    144          browser.addWebTab("about:blank");
    145        } else {
    146          // It is a bad idea to alter the window list while iterating
    147          // over it, so add this window to an array and close it later.
    148          windowsToClose.push(win);
    149        }
    150        // Close each tab except the new blank one that we created.
    151        tabsToRemove.forEach(aTab => browser.removeTab(aTab));
    152      }
    153      // Close all XUL windows except this one.
    154      logger.info("Closing windows...");
    155      windowsToClose.forEach(aWin => aWin.close());
    156      logger.info("Closed all tabs");
    157 
    158      // This clears the undo tab history.
    159      const tabs = Services.prefs.getIntPref(
    160        "browser.sessionstore.max_tabs_undo"
    161      );
    162      Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0);
    163      Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", tabs);
    164    }
    165 
    166    clearSearchBar() {
    167      logger.info("Clearing searchbox");
    168      // Bug #10800: Trying to clear search/find can cause exceptions
    169      // in unknown cases. Just log for now.
    170      try {
    171        const searchBar = window.document.getElementById("searchbar");
    172        if (searchBar) {
    173          searchBar.textbox.reset();
    174        }
    175      } catch (e) {
    176        logger.error("Exception on clearing search box", e);
    177      }
    178      try {
    179        if (gFindBarInitialized) {
    180          const findbox = gFindBar.getElement("findbar-textbox");
    181          findbox.reset();
    182          gFindBar.close();
    183        }
    184      } catch (e) {
    185        logger.error("Exception on clearing find bar", e);
    186      }
    187    }
    188 
    189    clearPrivateSessionHistory() {
    190      logger.info("Emitting Private Browsing Session clear event");
    191      Services.obs.notifyObservers(null, "browser:purge-session-history");
    192    }
    193 
    194    clearHTTPAuths() {
    195      if (
    196        !Services.prefs.getBoolPref(
    197          "browser.new_identity.clear_http_auth",
    198          true
    199        )
    200      ) {
    201        logger.info("Skipping HTTP Auths, because disabled");
    202        return;
    203      }
    204      logger.info("Clearing HTTP Auths");
    205      const auth = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
    206        Ci.nsIHttpAuthManager
    207      );
    208      auth.clearAll();
    209    }
    210 
    211    clearCryptoTokens() {
    212      logger.info("Clearing Crypto Tokens");
    213      // Clear all crypto auth tokens. This includes calls to PK11_LogoutAll(),
    214      // nsNSSComponent::LogoutAuthenticatedPK11() and clearing the SSL session
    215      // cache.
    216      const sdr = Cc["@mozilla.org/security/sdr;1"].getService(
    217        Ci.nsISecretDecoderRing
    218      );
    219      sdr.logoutAndTeardown();
    220    }
    221 
    222    clearOCSPCache() {
    223      // nsNSSComponent::Observe() watches security.OCSP.enabled, which calls
    224      // setValidationOptions(), which in turn calls setNonPkixOcspEnabled() which,
    225      // if security.OCSP.enabled is set to 0, calls CERT_DisableOCSPChecking(),
    226      // which calls CERT_ClearOCSPCache().
    227      // See: https://mxr.mozilla.org/comm-esr24/source/mozilla/security/manager/ssl/src/nsNSSComponent.cpp
    228      const ocsp = Services.prefs.getIntPref("security.OCSP.enabled");
    229      Services.prefs.setIntPref("security.OCSP.enabled", 0);
    230      Services.prefs.setIntPref("security.OCSP.enabled", ocsp);
    231    }
    232 
    233    clearSecuritySettings() {
    234      // Clear site security settings
    235      const sss = Cc["@mozilla.org/ssservice;1"].getService(
    236        Ci.nsISiteSecurityService
    237      );
    238      sss.clearAll();
    239    }
    240 
    241    clearImageCaches() {
    242      logger.info("Clearing Image Cache");
    243      // In Firefox 18 and newer, there are two image caches: one that is used
    244      // for regular browsing, and one that is used for private browsing.
    245      this.clearImageCacheRB();
    246      this.clearImageCachePB();
    247    }
    248 
    249    clearImageCacheRB() {
    250      try {
    251        const imgTools = Cc["@mozilla.org/image/tools;1"].getService(
    252          Ci.imgITools
    253        );
    254        const imgCache = imgTools.getImgCacheForDocument(null);
    255        // Evict all but chrome cache
    256        imgCache.clearCache(false);
    257      } catch (e) {
    258        // FIXME: This can happen in some rare cases involving XULish image data
    259        // in combination with our image cache isolation patch. Sure isn't
    260        // a good thing, but it's not really a super-cookie vector either.
    261        // We should fix it eventually.
    262        logger.error("Exception on image cache clearing", e);
    263      }
    264    }
    265 
    266    clearImageCachePB() {
    267      const imgTools = Cc["@mozilla.org/image/tools;1"].getService(
    268        Ci.imgITools
    269      );
    270      try {
    271        // Try to clear the private browsing cache. To do so, we must locate a
    272        // content document that is contained within a private browsing window.
    273        let didClearPBCache = false;
    274        const enumerator = Services.wm.getEnumerator("navigator:browser");
    275        while (!didClearPBCache && enumerator.hasMoreElements()) {
    276          const win = enumerator.getNext();
    277          let browserDoc = win.document.documentElement;
    278          if (!browserDoc.hasAttribute("privatebrowsingmode")) {
    279            continue;
    280          }
    281          const tabbrowser = win.gBrowser;
    282          if (!tabbrowser) {
    283            continue;
    284          }
    285          for (const browser of tabbrowser.browsers) {
    286            const doc = browser.contentDocument;
    287            if (doc) {
    288              const imgCache = imgTools.getImgCacheForDocument(doc);
    289              // Evict all but chrome cache
    290              imgCache.clearCache(false);
    291              didClearPBCache = true;
    292              break;
    293            }
    294          }
    295        }
    296      } catch (e) {
    297        logger.error("Exception on private browsing image cache clearing", e);
    298      }
    299    }
    300 
    301    clearStorage() {
    302      logger.info("Clearing Disk and Memory Caches");
    303      try {
    304        Services.cache2.clear();
    305      } catch (e) {
    306        logger.error("Exception on cache clearing", e);
    307      }
    308 
    309      logger.info("Clearing Cookies and DOM Storage");
    310      Services.cookies.removeAll();
    311    }
    312 
    313    clearPreferencesAndPermissions() {
    314      logger.info("Clearing Content Preferences");
    315      ChromeUtils.defineESModuleGetters(this, {
    316        PrivateBrowsingUtils:
    317          "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
    318      });
    319      const pbCtxt = PrivateBrowsingUtils.privacyContextFromWindow(window);
    320      const cps = Cc["@mozilla.org/content-pref/service;1"].getService(
    321        Ci.nsIContentPrefService2
    322      );
    323      cps.removeAllDomains(pbCtxt);
    324      this.clearSiteSpecificZoom();
    325 
    326      logger.info("Clearing permissions");
    327      try {
    328        Services.perms.removeAll();
    329      } catch (e) {
    330        // Actually, this catch does not appear to be needed. Leaving it in for
    331        // safety though.
    332        logger.error("Cannot clear permissions", e);
    333      }
    334 
    335      logger.info("Syncing prefs");
    336      // Force prefs to be synced to disk
    337      Services.prefs.savePrefFile(null);
    338    }
    339 
    340    async clearData() {
    341      logger.info("Calling the clearDataService");
    342      const flags =
    343        Services.clearData.CLEAR_ALL ^ Services.clearData.CLEAR_PASSWORDS;
    344      return new Promise(resolve => {
    345        Services.clearData.deleteData(flags, {
    346          onDataDeleted(code) {
    347            if (code !== Cr.NS_OK) {
    348              logger.error(`Error while calling the clearDataService: ${code}`);
    349            }
    350            // We always resolve, because we do not want to interrupt the new
    351            // identity procedure.
    352            resolve();
    353          },
    354        });
    355      });
    356    }
    357 
    358    clearConnections() {
    359      logger.info("Closing open connections");
    360      // Clear keep-alive
    361      Services.obs.notifyObservers(this, "net:prune-all-connections");
    362    }
    363 
    364    clearPrivateSession() {
    365      logger.info("Ending any remaining private browsing sessions.");
    366      Services.obs.notifyObservers(null, "last-pb-context-exited");
    367    }
    368 
    369    async reloadAddons() {
    370      logger.info("Reloading add-ons to clear their temporary state.");
    371      // Reload all active extensions except search engines, which would throw.
    372      const addons = await AddonManager.getAddonsByTypes(["extension"]);
    373      const isSearchEngine = async addon =>
    374        (await (await fetch(addon.getResourceURI("manifest.json").spec)).json())
    375          ?.chrome_settings_overrides?.search_provider;
    376      const reloadIfNeeded = async addon =>
    377        addon.isActive && !(await isSearchEngine(addon)) && addon.reload();
    378      await Promise.all(addons.map(addon => reloadIfNeeded(addon)));
    379    }
    380 
    381    // Broadcast as a hook to clear other data
    382 
    383    broadcast() {
    384      logger.info("Broadcasting the new identity");
    385      Services.obs.notifyObservers({}, topics.newIdentityRequested);
    386    }
    387 
    388    // Window management
    389 
    390    openNewWindow() {
    391      logger.info("Opening a new window");
    392      return new Promise(resolve => {
    393        // Open a new window forcing the about:privatebrowsing page (tor-browser#41765)
    394        // unless user explicitly overrides this policy (tor-browser #42236)
    395        const trustedHomePref = "browser.startup.homepage.new_identity";
    396        const homeURL = HomePage.get();
    397        const defaultHomeURL = HomePage.getDefault();
    398        const isTrustedHome =
    399          homeURL === defaultHomeURL ||
    400          homeURL === "chrome://browser/content/blanktab.html" || // about:blank
    401          homeURL === Services.prefs.getStringPref(trustedHomePref, "");
    402        const isCustomHome =
    403          Services.prefs.getIntPref("browser.startup.page") === 1;
    404        const win = OpenBrowserWindow({
    405          private: isCustomHome && isTrustedHome ? "private" : "no-home",
    406        });
    407        // This mechanism to know when the new window is ready is used by
    408        // OpenBrowserWindow itself (see its definition in browser.js).
    409        win.addEventListener(
    410          "MozAfterPaint",
    411          () => {
    412            resolve();
    413            if (isTrustedHome || !isCustomHome) {
    414              return;
    415            }
    416            const tbl = win.TabsProgressListener;
    417            const { onLocationChange } = tbl;
    418            tbl.onLocationChange = (...args) => {
    419              tbl.onLocationChange = onLocationChange;
    420              tbl.onLocationChange(...args);
    421              const url = URL.parse(homeURL);
    422              if (!url) {
    423                // malformed URL, bail out
    424                return;
    425              }
    426 
    427              let displayAddress = url.hostname;
    428              if (!displayAddress) {
    429                // no host, use full address and truncate if too long
    430                const MAX_LEN = 32;
    431                displayAddress = url.href;
    432                if (displayAddress.length > MAX_LEN) {
    433                  displayAddress = `${displayAddress.substring(0, MAX_LEN)}…`;
    434                }
    435              }
    436              const callback = () => {
    437                Services.prefs.setStringPref(trustedHomePref, homeURL);
    438                win.BrowserHome();
    439              };
    440              const notificationBox = win.gBrowser.getNotificationBox();
    441              notificationBox.appendNotification(
    442                "new-identity-safe-home",
    443                {
    444                  label: {
    445                    "l10n-id": "new-identity-blocked-home-notification",
    446                    "l10n-args": { url: displayAddress },
    447                  },
    448                  priority: notificationBox.PRIORITY_INFO_MEDIUM,
    449                },
    450                [
    451                  {
    452                    "l10n-id": "new-identity-blocked-home-ignore-button",
    453                    callback,
    454                  },
    455                ]
    456              );
    457            };
    458          },
    459          { once: true }
    460        );
    461      });
    462    }
    463 
    464    closeOldWindow() {
    465      logger.info("Closing the old window");
    466 
    467      // Run garbage collection and cycle collection after window is gone.
    468      // This ensures that blob URIs are forgotten.
    469      window.addEventListener("unload", function () {
    470        logger.debug("Initiating New Identity GC pass");
    471        // Clear out potential pending sInterSliceGCTimer:
    472        window.windowUtils.runNextCollectorTimer();
    473        // Clear out potential pending sICCTimer:
    474        window.windowUtils.runNextCollectorTimer();
    475        // Schedule a garbage collection in 4000-1000ms...
    476        window.windowUtils.garbageCollect();
    477        // To ensure the GC runs immediately instead of 4-10s from now, we need
    478        // to poke it at least 11 times.
    479        // We need 5 pokes for GC, 1 poke for the interSliceGC, and 5 pokes for
    480        // CC.
    481        // See nsJSContext::RunNextCollectorTimer() in
    482        // https://mxr.mozilla.org/mozilla-central/source/dom/base/nsJSEnvironment.cpp#1970.
    483        // XXX: We might want to make our own method for immediate full GC...
    484        for (let poke = 0; poke < 11; poke++) {
    485          window.windowUtils.runNextCollectorTimer();
    486        }
    487        // And now, since the GC probably actually ran *after* the CC last time,
    488        // run the whole thing again.
    489        window.windowUtils.garbageCollect();
    490        for (let poke = 0; poke < 11; poke++) {
    491          window.windowUtils.runNextCollectorTimer();
    492        }
    493        logger.debug("Completed New Identity GC pass");
    494      });
    495 
    496      // Close the current window for added safety
    497      window.close();
    498    }
    499  }
    500 
    501  let newIdentityInProgress = false;
    502  return {
    503    async onCommand() {
    504      try {
    505        // Ignore if there's a New Identity in progress to avoid race
    506        // conditions leading to failures (see bug 11783 for an example).
    507        if (newIdentityInProgress) {
    508          return;
    509        }
    510        newIdentityInProgress = true;
    511 
    512        const prefConfirm = "browser.new_identity.confirm_newnym";
    513        const shouldConfirm = Services.prefs.getBoolPref(prefConfirm, true);
    514        if (shouldConfirm) {
    515          const [titleString, bodyString, checkboxString, restartString] =
    516            await document.l10n.formatValues([
    517              { id: "new-identity-dialog-title" },
    518              { id: "new-identity-dialog-description" },
    519              { id: "restart-warning-dialog-do-not-warn-checkbox" },
    520              { id: "restart-warning-dialog-restart-button" },
    521            ]);
    522          const flags =
    523            Services.prompt.BUTTON_POS_0 *
    524              Services.prompt.BUTTON_TITLE_IS_STRING +
    525            Services.prompt.BUTTON_POS_0_DEFAULT +
    526            Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE +
    527            Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
    528          const propBag = await Services.prompt.asyncConfirmEx(
    529            window.browsingContext,
    530            Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
    531            titleString,
    532            bodyString,
    533            flags,
    534            restartString,
    535            null,
    536            null,
    537            checkboxString,
    538            false
    539          );
    540          if (propBag.get("buttonNumClicked") !== 0) {
    541            return;
    542          }
    543          if (propBag.get("checked")) {
    544            Services.prefs.setBoolPref(prefConfirm, false);
    545          }
    546        }
    547 
    548        const impl = new NewIdentityImpl();
    549        await impl.run();
    550      } catch (e) {
    551        // If something went wrong make sure we have the New Identity button
    552        // enabled (again).
    553        logger.error("Unexpected error", e);
    554        window.alert("New Identity unexpected error: " + e);
    555      } finally {
    556        newIdentityInProgress = false;
    557      }
    558    },
    559  };
    560 });