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 };