tor-browser

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

ClipboardPrivacy.sys.mjs (5877B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
      9  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     10  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     11 });
     12 
     13 /**
     14 * Empty clipboard content from private windows on exit.
     15 *
     16 * See tor-browser#42154.
     17 */
     18 export const ClipboardPrivacy = {
     19  _lastClipboardHash: null,
     20  _globalActivation: false,
     21  _isPrivateClipboard: false,
     22  _hasher: null,
     23  _shuttingDown: false,
     24  _log: null,
     25 
     26  _createTransferable() {
     27    const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
     28      Ci.nsITransferable
     29    );
     30    trans.init(null);
     31    return trans;
     32  },
     33  _computeClipboardHash() {
     34    const flavors = ["text/x-moz-url", "text/plain"];
     35    if (
     36      !Services.clipboard.hasDataMatchingFlavors(
     37        flavors,
     38        Ci.nsIClipboard.kGlobalClipboard
     39      )
     40    ) {
     41      return null;
     42    }
     43    const trans = this._createTransferable();
     44    flavors.forEach(trans.addDataFlavor);
     45    try {
     46      Services.clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
     47      const clipboardContent = {};
     48      trans.getAnyTransferData({}, clipboardContent);
     49      const { data } = clipboardContent.value.QueryInterface(
     50        Ci.nsISupportsString
     51      );
     52      const bytes = new TextEncoder().encode(data);
     53      const hasher = (this._hasher ||= Cc[
     54        "@mozilla.org/security/hash;1"
     55      ].createInstance(Ci.nsICryptoHash));
     56      hasher.init(hasher.SHA256);
     57      hasher.update(bytes, bytes.length);
     58      return hasher.finish(true);
     59    } catch (e) {}
     60    return null;
     61  },
     62 
     63  init() {
     64    this._log = console.createInstance({
     65      prefix: "ClipboardPrivacy",
     66    });
     67    this._lastClipboardHash = this._computeClipboardHash();
     68 
     69    // Here we track changes in active window / application,
     70    // by filtering focus events and window closures.
     71    const handleActivation = (win, activation) => {
     72      if (activation) {
     73        if (!this._globalActivation) {
     74          // focus changed within this window, bail out.
     75          return;
     76        }
     77        this._globalActivation = false;
     78      } else if (!Services.focus.activeWindow) {
     79        // focus is leaving this window:
     80        // let's track whether it remains within the browser.
     81        lazy.setTimeout(() => {
     82          this._globalActivation = !Services.focus.activeWindow;
     83        }, 100);
     84      }
     85 
     86      const checkClipboardContent = () => {
     87        const clipboardHash = this._computeClipboardHash();
     88        if (clipboardHash !== this._lastClipboardHash) {
     89          this._isPrivateClipboard =
     90            !activation &&
     91            (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
     92              lazy.PrivateBrowsingUtils.isWindowPrivate(win));
     93          this._lastClipboardHash = clipboardHash;
     94          this._log.debug(
     95            `Clipboard changed: private ${this._isPrivateClipboard}, hash ${clipboardHash}.`
     96          );
     97        }
     98      };
     99 
    100      if (win.closed) {
    101        checkClipboardContent();
    102      } else {
    103        // defer clipboard access on DOM events to work-around tor-browser#42306
    104        lazy.setTimeout(checkClipboardContent, 0);
    105      }
    106    };
    107    const focusListener = e =>
    108      e.isTrusted && handleActivation(e.currentTarget, e.type === "focusin");
    109    const initWindow = win => {
    110      for (const e of ["focusin", "focusout"]) {
    111        win.addEventListener(e, focusListener);
    112      }
    113    };
    114    for (const w of Services.ww.getWindowEnumerator()) {
    115      initWindow(w);
    116    }
    117    Services.ww.registerNotification((win, event) => {
    118      switch (event) {
    119        case "domwindowopened":
    120          initWindow(win);
    121          break;
    122        case "domwindowclosed":
    123          handleActivation(win, false);
    124          if (
    125            this._isPrivateClipboard &&
    126            lazy.PrivateBrowsingUtils.isWindowPrivate(win) &&
    127            (this._shuttingDown ||
    128              !Array.from(Services.ww.getWindowEnumerator()).find(
    129                w =>
    130                  lazy.PrivateBrowsingUtils.isWindowPrivate(w) &&
    131                  // We need to filter out the HIDDEN WebExtensions window,
    132                  // which might be private as well but is not UI-relevant.
    133                  !w.location.href.startsWith("chrome://extensions/")
    134              ))
    135          ) {
    136            // no more private windows, empty private content if needed
    137            this.emptyPrivate();
    138          }
    139      }
    140    });
    141 
    142    lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
    143      "ClipboardPrivacy: removing private data",
    144      () => {
    145        this._shuttingDown = true;
    146        this.emptyPrivate();
    147      }
    148    );
    149  },
    150  emptyPrivate() {
    151    if (
    152      this._isPrivateClipboard &&
    153      !Services.prefs.getBoolPref(
    154        "browser.privatebrowsing.preserveClipboard",
    155        false
    156      ) &&
    157      this._lastClipboardHash === this._computeClipboardHash()
    158    ) {
    159      // nsIClipboard.emptyClipboard() does nothing in Wayland:
    160      // we'll set an empty string as a work-around.
    161      const trans = this._createTransferable();
    162      const flavor = "text/plain";
    163      trans.addDataFlavor(flavor);
    164      const emptyString = Cc["@mozilla.org/supports-string;1"].createInstance(
    165        Ci.nsISupportsString
    166      );
    167      emptyString.data = "";
    168      trans.setTransferData(flavor, emptyString);
    169      const { clipboard } = Services,
    170        { kGlobalClipboard } = clipboard;
    171      clipboard.setData(trans, null, kGlobalClipboard);
    172      clipboard.emptyClipboard(kGlobalClipboard);
    173      this._lastClipboardHash = null;
    174      this._isPrivateClipboard = false;
    175      this._log.info("Private clipboard emptied.");
    176    }
    177  },
    178 };