Commit 48bf9658 authored by ma1's avatar ma1 Committed by Beatriz Rizental
Browse files

BB 42019: Empty browser's clipboard on browser shutdown

parent 4216d2bb
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -115,6 +115,9 @@ pref("browser.pagethumbnails.capturing_disabled", true);
pref("browser.backup.enabled", false);
pref("browser.backup.scheduled.enabled", false);

// Empty clipboard content from private windows on exit (tor-browser#42154)
pref("browser.privatebrowsing.preserveClipboard", false);

// tor-browser#42611: Do not include the URL of the image, when copying it.
// Also, do not save clipboard in history/cloud.
pref("clipboard.imageAsFile.enabled", false);
+162 −0
Original line number Diff line number Diff line
@@ -173,6 +173,166 @@ let gThisInstanceIsLaunchOnLogin = false;
// a taskbar tab shortcut will contain the "taskbar-tab" flag.
let gThisInstanceIsTaskbarTab = false;

// Empty clipboard content from private windows on exit
// (tor-browser#42154)
const ClipboardPrivacy = {
  _lastClipboardHash: null,
  _globalActivation: false,
  _isPrivateClipboard: false,
  _hasher: null,
  _shuttingDown: false,

  _createTransferable() {
    const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
      Ci.nsITransferable
    );
    trans.init(null);
    return trans;
  },
  _computeClipboardHash() {
    const flavors = ["text/x-moz-url", "text/plain"];
    if (
      !Services.clipboard.hasDataMatchingFlavors(
        flavors,
        Ci.nsIClipboard.kGlobalClipboard
      )
    ) {
      return null;
    }
    const trans = this._createTransferable();
    flavors.forEach(trans.addDataFlavor);
    try {
      Services.clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
      const clipboardContent = {};
      trans.getAnyTransferData({}, clipboardContent);
      const { data } = clipboardContent.value.QueryInterface(
        Ci.nsISupportsString
      );
      const bytes = new TextEncoder().encode(data);
      const hasher = (this._hasher ||= Cc[
        "@mozilla.org/security/hash;1"
      ].createInstance(Ci.nsICryptoHash));
      hasher.init(hasher.SHA256);
      hasher.update(bytes, bytes.length);
      return hasher.finish(true);
    } catch (e) {}
    return null;
  },

  startup() {
    this._lastClipboardHash = this._computeClipboardHash();

    // Here we track changes in active window / application,
    // by filtering focus events and window closures.
    const handleActivation = (win, activation) => {
      if (activation) {
        if (!this._globalActivation) {
          // focus changed within this window, bail out.
          return;
        }
        this._globalActivation = false;
      } else if (!Services.focus.activeWindow) {
        // focus is leaving this window:
        // let's track whether it remains within the browser.
        lazy.setTimeout(() => {
          this._globalActivation = !Services.focus.activeWindow;
        }, 100);
      }

      const checkClipboardContent = () => {
        const clipboardHash = this._computeClipboardHash();
        if (clipboardHash !== this._lastClipboardHash) {
          this._isPrivateClipboard =
            !activation &&
            (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
              lazy.PrivateBrowsingUtils.isWindowPrivate(win));
          this._lastClipboardHash = clipboardHash;
          lazy.log.debug(
            `Clipboard changed: private ${this._isPrivateClipboard}, hash ${clipboardHash}.`
          );
        }
      };

      if (win.closed) {
        checkClipboardContent();
      } else {
        // defer clipboard access on DOM events to work-around tor-browser#42306
        lazy.setTimeout(checkClipboardContent, 0);
      }
    };
    const focusListener = e =>
      e.isTrusted && handleActivation(e.currentTarget, e.type === "focusin");
    const initWindow = win => {
      for (const e of ["focusin", "focusout"]) {
        win.addEventListener(e, focusListener);
      }
    };
    for (const w of Services.ww.getWindowEnumerator()) {
      initWindow(w);
    }
    Services.ww.registerNotification((win, event) => {
      switch (event) {
        case "domwindowopened":
          initWindow(win);
          break;
        case "domwindowclosed":
          handleActivation(win, false);
          if (
            this._isPrivateClipboard &&
            lazy.PrivateBrowsingUtils.isWindowPrivate(win) &&
            (this._shuttingDown ||
              !Array.from(Services.ww.getWindowEnumerator()).find(
                w =>
                  lazy.PrivateBrowsingUtils.isWindowPrivate(w) &&
                  // We need to filter out the HIDDEN WebExtensions window,
                  // which might be private as well but is not UI-relevant.
                  !w.location.href.startsWith("chrome://extensions/")
              ))
          ) {
            // no more private windows, empty private content if needed
            this.emptyPrivate();
          }
      }
    });

    lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
      "ClipboardPrivacy: removing private data",
      () => {
        this._shuttingDown = true;
        this.emptyPrivate();
      }
    );
  },
  emptyPrivate() {
    if (
      this._isPrivateClipboard &&
      !Services.prefs.getBoolPref(
        "browser.privatebrowsing.preserveClipboard",
        false
      ) &&
      this._lastClipboardHash === this._computeClipboardHash()
    ) {
      // nsIClipboard.emptyClipboard() does nothing in Wayland:
      // we'll set an empty string as a work-around.
      const trans = this._createTransferable();
      const flavor = "text/plain";
      trans.addDataFlavor(flavor);
      const emptyString = Cc["@mozilla.org/supports-string;1"].createInstance(
        Ci.nsISupportsString
      );
      emptyString.data = "";
      trans.setTransferData(flavor, emptyString);
      const { clipboard } = Services,
        { kGlobalClipboard } = clipboard;
      clipboard.setData(trans, null, kGlobalClipboard);
      clipboard.emptyClipboard(kGlobalClipboard);
      this._lastClipboardHash = null;
      this._isPrivateClipboard = false;
      lazy.log.info("Private clipboard emptied.");
    }
  },
};

/**
 * Fission-compatible JSProcess implementations.
 * Each actor options object takes the form of a ProcessActorOptions dictionary.
@@ -1863,6 +2023,8 @@ BrowserGlue.prototype = {

    lazy.DoHController.init();

    ClipboardPrivacy.startup();

    this._firstWindowTelemetry(aWindow);
    this._firstWindowLoaded();