diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/resistfingerprinting | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/resistfingerprinting')
22 files changed, 5019 insertions, 0 deletions
diff --git a/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h b/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h new file mode 100644 index 0000000000..a7956dbe1d --- /dev/null +++ b/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/** + * This file contains the spoofed keycodes of en-US for fingerprinting + * resistance. When privacy.resistFingerprinting is active, we spoof the user's + * keyboard layout according to the language of the document. + * + * Use CONTROL to define the control key. + * CONTROL(keyNameIndex, codeNameIndex, keyCode) + * @param keyNameIndex The keyNameIndex of this control key. + * See KeyNameList.h for details. + * @param codeNameIndex The codeNameIndex of this contorl key. + * See PhysicalKeyCodeNameList.h for details. + * @param keyCode The keyCode of this control key. + * See KeyEvent.webidl for details. + * + * Use KEY to define the key with its modifier states. The key will be spoofed + * with given modifier states. + * KEY(keyString, codeNameIndex, keyCode, modifiers) + * @param keyString The key string of this key. + * @param codeNameIndex The codeNameIndex of this key. + * See PhysicalKeyCodeNameList.h for details. + * @param keyCode The keyCode of this key. + * See KeyEvent.webidl for details. + * @param modifiers The spoofing modifier states for this key. + * See BasicEvents.h for details. + */ + +/** + * Spoofed keycodes for English content (US English keyboard layout). + */ + +// clang-format off +CONTROL(Alt, AltLeft, dom::KeyboardEvent_Binding::DOM_VK_ALT) +CONTROL(ArrowDown, ArrowDown, dom::KeyboardEvent_Binding::DOM_VK_DOWN) +CONTROL(ArrowLeft, ArrowLeft, dom::KeyboardEvent_Binding::DOM_VK_LEFT) +CONTROL(ArrowRight, ArrowRight, dom::KeyboardEvent_Binding::DOM_VK_RIGHT) +CONTROL(ArrowUp, ArrowUp, dom::KeyboardEvent_Binding::DOM_VK_UP) +CONTROL(Backspace, Backspace, dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) +CONTROL(CapsLock, CapsLock, dom::KeyboardEvent_Binding::DOM_VK_CAPS_LOCK) +// Leaving "ContextMenu" key unimplemented; not every english keyboard has +// this. For example, MACOS doesn't have this. +CONTROL(Control, ControlLeft, dom::KeyboardEvent_Binding::DOM_VK_CONTROL) +CONTROL(Delete, Delete, dom::KeyboardEvent_Binding::DOM_VK_DELETE) +CONTROL(End, End, dom::KeyboardEvent_Binding::DOM_VK_END) +CONTROL(Enter, Enter, dom::KeyboardEvent_Binding::DOM_VK_RETURN) +CONTROL(Escape, Escape, dom::KeyboardEvent_Binding::DOM_VK_ESCAPE) +// Leaving "Help" key unimplemented; it only appears in some keyboard in Linux. +CONTROL(Home, Home, dom::KeyboardEvent_Binding::DOM_VK_HOME) +CONTROL(Insert, Insert, dom::KeyboardEvent_Binding::DOM_VK_INSERT) +CONTROL(Meta, OSLeft, dom::KeyboardEvent_Binding::DOM_VK_WIN) +CONTROL(OS, OSLeft, dom::KeyboardEvent_Binding::DOM_VK_WIN) +CONTROL(PageDown, PageDown, dom::KeyboardEvent_Binding::DOM_VK_PAGE_DOWN) +CONTROL(PageUp, PageUp, dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP) +// Leaving "Pause", "PrintScreen" and "ScrollLock" keys unimplemented; they are +// non-MACOS only. +CONTROL(Shift, ShiftLeft, dom::KeyboardEvent_Binding::DOM_VK_SHIFT) +CONTROL(Tab, Tab, dom::KeyboardEvent_Binding::DOM_VK_TAB) +CONTROL(F1, F1, dom::KeyboardEvent_Binding::DOM_VK_F1) +CONTROL(F2, F2, dom::KeyboardEvent_Binding::DOM_VK_F2) +CONTROL(F3, F3, dom::KeyboardEvent_Binding::DOM_VK_F3) +CONTROL(F4, F4, dom::KeyboardEvent_Binding::DOM_VK_F4) +CONTROL(F5, F5, dom::KeyboardEvent_Binding::DOM_VK_F5) +CONTROL(F6, F6, dom::KeyboardEvent_Binding::DOM_VK_F6) +CONTROL(F7, F7, dom::KeyboardEvent_Binding::DOM_VK_F7) +CONTROL(F8, F8, dom::KeyboardEvent_Binding::DOM_VK_F8) +CONTROL(F9, F9, dom::KeyboardEvent_Binding::DOM_VK_F9) +CONTROL(F10, F10, dom::KeyboardEvent_Binding::DOM_VK_F10) +CONTROL(F11, F11, dom::KeyboardEvent_Binding::DOM_VK_F11) +CONTROL(F12, F12, dom::KeyboardEvent_Binding::DOM_VK_F12) +// Leaving "F13" to "F35" key unimplemented; they are some how platform +// dependent. "F13" to "F19" are on MAC's full keyboard but may not exist on +// usual keyboard. "F20" to "F24" are only available on Windows and Linux. +// "F25" to "F35" are Linux only. Leaving "Clear" key unimplemented; it's +// inconsistent between platforms. +KEY(" ", Space, dom::KeyboardEvent_Binding::DOM_VK_SPACE, MODIFIER_NONE) +KEY(",", Comma, dom::KeyboardEvent_Binding::DOM_VK_COMMA, MODIFIER_NONE) +KEY("<", Comma, dom::KeyboardEvent_Binding::DOM_VK_COMMA, MODIFIER_SHIFT) +KEY(".", Period, dom::KeyboardEvent_Binding::DOM_VK_PERIOD, MODIFIER_NONE) +KEY(">", Period, dom::KeyboardEvent_Binding::DOM_VK_PERIOD, MODIFIER_SHIFT) +KEY("/", Slash, dom::KeyboardEvent_Binding::DOM_VK_SLASH, MODIFIER_NONE) +KEY("?", Slash, dom::KeyboardEvent_Binding::DOM_VK_SLASH, MODIFIER_SHIFT) +KEY(";", Semicolon, dom::KeyboardEvent_Binding::DOM_VK_SEMICOLON, MODIFIER_NONE) +KEY(":", Semicolon, dom::KeyboardEvent_Binding::DOM_VK_SEMICOLON, MODIFIER_SHIFT) +KEY("'", Quote, dom::KeyboardEvent_Binding::DOM_VK_QUOTE, MODIFIER_NONE) +KEY("\"", Quote, dom::KeyboardEvent_Binding::DOM_VK_QUOTE, MODIFIER_SHIFT) +KEY("[", BracketLeft, dom::KeyboardEvent_Binding::DOM_VK_OPEN_BRACKET, MODIFIER_NONE) +KEY("{", BracketLeft, dom::KeyboardEvent_Binding::DOM_VK_OPEN_BRACKET, MODIFIER_SHIFT) +KEY("]", BracketRight, dom::KeyboardEvent_Binding::DOM_VK_CLOSE_BRACKET, MODIFIER_NONE) +KEY("}", BracketRight, dom::KeyboardEvent_Binding::DOM_VK_CLOSE_BRACKET, MODIFIER_SHIFT) +KEY("`", Backquote, dom::KeyboardEvent_Binding::DOM_VK_BACK_QUOTE, MODIFIER_NONE) +KEY("~", Backquote, dom::KeyboardEvent_Binding::DOM_VK_BACK_QUOTE, MODIFIER_SHIFT) +KEY("\\", Backslash, dom::KeyboardEvent_Binding::DOM_VK_BACK_SLASH, MODIFIER_NONE) +KEY("|", Backslash, dom::KeyboardEvent_Binding::DOM_VK_BACK_SLASH, MODIFIER_SHIFT) +KEY("-", Minus, dom::KeyboardEvent_Binding::DOM_VK_HYPHEN_MINUS, MODIFIER_NONE) +KEY("_", Minus, dom::KeyboardEvent_Binding::DOM_VK_HYPHEN_MINUS, MODIFIER_SHIFT) +KEY("=", Equal, dom::KeyboardEvent_Binding::DOM_VK_EQUALS, MODIFIER_NONE) +KEY("+", Equal, dom::KeyboardEvent_Binding::DOM_VK_EQUALS, MODIFIER_SHIFT) +KEY("A", KeyA, dom::KeyboardEvent_Binding::DOM_VK_A, MODIFIER_SHIFT) +KEY("B", KeyB, dom::KeyboardEvent_Binding::DOM_VK_B, MODIFIER_SHIFT) +KEY("C", KeyC, dom::KeyboardEvent_Binding::DOM_VK_C, MODIFIER_SHIFT) +KEY("D", KeyD, dom::KeyboardEvent_Binding::DOM_VK_D, MODIFIER_SHIFT) +KEY("E", KeyE, dom::KeyboardEvent_Binding::DOM_VK_E, MODIFIER_SHIFT) +KEY("F", KeyF, dom::KeyboardEvent_Binding::DOM_VK_F, MODIFIER_SHIFT) +KEY("G", KeyG, dom::KeyboardEvent_Binding::DOM_VK_G, MODIFIER_SHIFT) +KEY("H", KeyH, dom::KeyboardEvent_Binding::DOM_VK_H, MODIFIER_SHIFT) +KEY("I", KeyI, dom::KeyboardEvent_Binding::DOM_VK_I, MODIFIER_SHIFT) +KEY("J", KeyJ, dom::KeyboardEvent_Binding::DOM_VK_J, MODIFIER_SHIFT) +KEY("K", KeyK, dom::KeyboardEvent_Binding::DOM_VK_K, MODIFIER_SHIFT) +KEY("L", KeyL, dom::KeyboardEvent_Binding::DOM_VK_L, MODIFIER_SHIFT) +KEY("M", KeyM, dom::KeyboardEvent_Binding::DOM_VK_M, MODIFIER_SHIFT) +KEY("N", KeyN, dom::KeyboardEvent_Binding::DOM_VK_N, MODIFIER_SHIFT) +KEY("O", KeyO, dom::KeyboardEvent_Binding::DOM_VK_O, MODIFIER_SHIFT) +KEY("P", KeyP, dom::KeyboardEvent_Binding::DOM_VK_P, MODIFIER_SHIFT) +KEY("Q", KeyQ, dom::KeyboardEvent_Binding::DOM_VK_Q, MODIFIER_SHIFT) +KEY("R", KeyR, dom::KeyboardEvent_Binding::DOM_VK_R, MODIFIER_SHIFT) +KEY("S", KeyS, dom::KeyboardEvent_Binding::DOM_VK_S, MODIFIER_SHIFT) +KEY("T", KeyT, dom::KeyboardEvent_Binding::DOM_VK_T, MODIFIER_SHIFT) +KEY("U", KeyU, dom::KeyboardEvent_Binding::DOM_VK_U, MODIFIER_SHIFT) +KEY("V", KeyV, dom::KeyboardEvent_Binding::DOM_VK_V, MODIFIER_SHIFT) +KEY("W", KeyW, dom::KeyboardEvent_Binding::DOM_VK_W, MODIFIER_SHIFT) +KEY("X", KeyX, dom::KeyboardEvent_Binding::DOM_VK_X, MODIFIER_SHIFT) +KEY("Y", KeyY, dom::KeyboardEvent_Binding::DOM_VK_Y, MODIFIER_SHIFT) +KEY("Z", KeyZ, dom::KeyboardEvent_Binding::DOM_VK_Z, MODIFIER_SHIFT) +KEY("a", KeyA, dom::KeyboardEvent_Binding::DOM_VK_A, MODIFIER_NONE) +KEY("b", KeyB, dom::KeyboardEvent_Binding::DOM_VK_B, MODIFIER_NONE) +KEY("c", KeyC, dom::KeyboardEvent_Binding::DOM_VK_C, MODIFIER_NONE) +KEY("d", KeyD, dom::KeyboardEvent_Binding::DOM_VK_D, MODIFIER_NONE) +KEY("e", KeyE, dom::KeyboardEvent_Binding::DOM_VK_E, MODIFIER_NONE) +KEY("f", KeyF, dom::KeyboardEvent_Binding::DOM_VK_F, MODIFIER_NONE) +KEY("g", KeyG, dom::KeyboardEvent_Binding::DOM_VK_G, MODIFIER_NONE) +KEY("h", KeyH, dom::KeyboardEvent_Binding::DOM_VK_H, MODIFIER_NONE) +KEY("i", KeyI, dom::KeyboardEvent_Binding::DOM_VK_I, MODIFIER_NONE) +KEY("j", KeyJ, dom::KeyboardEvent_Binding::DOM_VK_J, MODIFIER_NONE) +KEY("k", KeyK, dom::KeyboardEvent_Binding::DOM_VK_K, MODIFIER_NONE) +KEY("l", KeyL, dom::KeyboardEvent_Binding::DOM_VK_L, MODIFIER_NONE) +KEY("m", KeyM, dom::KeyboardEvent_Binding::DOM_VK_M, MODIFIER_NONE) +KEY("n", KeyN, dom::KeyboardEvent_Binding::DOM_VK_N, MODIFIER_NONE) +KEY("o", KeyO, dom::KeyboardEvent_Binding::DOM_VK_O, MODIFIER_NONE) +KEY("p", KeyP, dom::KeyboardEvent_Binding::DOM_VK_P, MODIFIER_NONE) +KEY("q", KeyQ, dom::KeyboardEvent_Binding::DOM_VK_Q, MODIFIER_NONE) +KEY("r", KeyR, dom::KeyboardEvent_Binding::DOM_VK_R, MODIFIER_NONE) +KEY("s", KeyS, dom::KeyboardEvent_Binding::DOM_VK_S, MODIFIER_NONE) +KEY("t", KeyT, dom::KeyboardEvent_Binding::DOM_VK_T, MODIFIER_NONE) +KEY("u", KeyU, dom::KeyboardEvent_Binding::DOM_VK_U, MODIFIER_NONE) +KEY("v", KeyV, dom::KeyboardEvent_Binding::DOM_VK_V, MODIFIER_NONE) +KEY("w", KeyW, dom::KeyboardEvent_Binding::DOM_VK_W, MODIFIER_NONE) +KEY("x", KeyX, dom::KeyboardEvent_Binding::DOM_VK_X, MODIFIER_NONE) +KEY("y", KeyY, dom::KeyboardEvent_Binding::DOM_VK_Y, MODIFIER_NONE) +KEY("z", KeyZ, dom::KeyboardEvent_Binding::DOM_VK_Z, MODIFIER_NONE) +KEY("0", Digit0, dom::KeyboardEvent_Binding::DOM_VK_0, MODIFIER_NONE) +KEY("1", Digit1, dom::KeyboardEvent_Binding::DOM_VK_1, MODIFIER_NONE) +KEY("2", Digit2, dom::KeyboardEvent_Binding::DOM_VK_2, MODIFIER_NONE) +KEY("3", Digit3, dom::KeyboardEvent_Binding::DOM_VK_3, MODIFIER_NONE) +KEY("4", Digit4, dom::KeyboardEvent_Binding::DOM_VK_4, MODIFIER_NONE) +KEY("5", Digit5, dom::KeyboardEvent_Binding::DOM_VK_5, MODIFIER_NONE) +KEY("6", Digit6, dom::KeyboardEvent_Binding::DOM_VK_6, MODIFIER_NONE) +KEY("7", Digit7, dom::KeyboardEvent_Binding::DOM_VK_7, MODIFIER_NONE) +KEY("8", Digit8, dom::KeyboardEvent_Binding::DOM_VK_8, MODIFIER_NONE) +KEY("9", Digit9, dom::KeyboardEvent_Binding::DOM_VK_9, MODIFIER_NONE) +KEY(")", Digit0, dom::KeyboardEvent_Binding::DOM_VK_0, MODIFIER_SHIFT) +KEY("!", Digit1, dom::KeyboardEvent_Binding::DOM_VK_1, MODIFIER_SHIFT) +KEY("@", Digit2, dom::KeyboardEvent_Binding::DOM_VK_2, MODIFIER_SHIFT) +KEY("#", Digit3, dom::KeyboardEvent_Binding::DOM_VK_3, MODIFIER_SHIFT) +KEY("$", Digit4, dom::KeyboardEvent_Binding::DOM_VK_4, MODIFIER_SHIFT) +KEY("%", Digit5, dom::KeyboardEvent_Binding::DOM_VK_5, MODIFIER_SHIFT) +KEY("^", Digit6, dom::KeyboardEvent_Binding::DOM_VK_6, MODIFIER_SHIFT) +KEY("&", Digit7, dom::KeyboardEvent_Binding::DOM_VK_7, MODIFIER_SHIFT) +KEY("*", Digit8, dom::KeyboardEvent_Binding::DOM_VK_8, MODIFIER_SHIFT) +KEY("(", Digit9, dom::KeyboardEvent_Binding::DOM_VK_9, MODIFIER_SHIFT) +// clang-format on diff --git a/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs b/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs new file mode 100644 index 0000000000..d6d7a6eefe --- /dev/null +++ b/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs @@ -0,0 +1,642 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const kPrefResistFingerprinting = "privacy.resistFingerprinting"; +const kPrefSpoofEnglish = "privacy.spoof_english"; +const kTopicHttpOnModifyRequest = "http-on-modify-request"; + +const kPrefLetterboxing = "privacy.resistFingerprinting.letterboxing"; +const kPrefLetterboxingDimensions = + "privacy.resistFingerprinting.letterboxing.dimensions"; +const kPrefLetterboxingTesting = + "privacy.resistFingerprinting.letterboxing.testing"; +const kTopicDOMWindowOpened = "domwindowopened"; + +var logConsole; +function log(msg) { + if (!logConsole) { + logConsole = console.createInstance({ + prefix: "RFPHelper.jsm", + maxLogLevelPref: "privacy.resistFingerprinting.jsmloglevel", + }); + } + + logConsole.log(msg); +} + +class _RFPHelper { + // ============================================================================ + // Shared Setup + // ============================================================================ + constructor() { + this._initialized = false; + } + + init() { + if (this._initialized) { + return; + } + this._initialized = true; + + // Add unconditional observers + Services.prefs.addObserver(kPrefResistFingerprinting, this); + Services.prefs.addObserver(kPrefLetterboxing, this); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_letterboxingDimensions", + kPrefLetterboxingDimensions, + "", + null, + this._parseLetterboxingDimensions + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_isLetterboxingTesting", + kPrefLetterboxingTesting, + false + ); + + // Add RFP and Letterboxing observers if prefs are enabled + this._handleResistFingerprintingChanged(); + this._handleLetterboxingPrefChanged(); + } + + uninit() { + if (!this._initialized) { + return; + } + this._initialized = false; + + // Remove unconditional observers + Services.prefs.removeObserver(kPrefResistFingerprinting, this); + Services.prefs.removeObserver(kPrefLetterboxing, this); + // Remove the RFP observers, swallowing exceptions if they weren't present + this._removeRFPObservers(); + } + + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + this._handlePrefChanged(data); + break; + case kTopicHttpOnModifyRequest: + this._handleHttpOnModifyRequest(subject, data); + break; + case kTopicDOMWindowOpened: + // We attach to the newly created window by adding tabsProgressListener + // and event listener on it. We listen for new tabs being added or + // the change of the content principal and apply margins accordingly. + this._handleDOMWindowOpened(subject); + break; + default: + break; + } + } + + handleEvent(aMessage) { + switch (aMessage.type) { + case "TabOpen": { + let tab = aMessage.target; + this._addOrClearContentMargin(tab.linkedBrowser); + break; + } + default: + break; + } + } + + _handlePrefChanged(data) { + switch (data) { + case kPrefResistFingerprinting: + this._handleResistFingerprintingChanged(); + break; + case kPrefSpoofEnglish: + this._handleSpoofEnglishChanged(); + break; + case kPrefLetterboxing: + this._handleLetterboxingPrefChanged(); + break; + default: + break; + } + } + + contentSizeUpdated(win) { + this._updateMarginsForTabsInWindow(win); + } + + // ============================================================================ + // Language Prompt + // ============================================================================ + _addRFPObservers() { + Services.prefs.addObserver(kPrefSpoofEnglish, this); + if (this._shouldPromptForLanguagePref()) { + Services.obs.addObserver(this, kTopicHttpOnModifyRequest); + } + } + + _removeRFPObservers() { + try { + Services.prefs.removeObserver(kPrefSpoofEnglish, this); + } catch (e) { + // do nothing + } + try { + Services.obs.removeObserver(this, kTopicHttpOnModifyRequest); + } catch (e) { + // do nothing + } + } + + _handleResistFingerprintingChanged() { + if (Services.prefs.getBoolPref(kPrefResistFingerprinting)) { + this._addRFPObservers(); + } else { + this._removeRFPObservers(); + } + } + + _handleSpoofEnglishChanged() { + switch (Services.prefs.getIntPref(kPrefSpoofEnglish)) { + case 0: // will prompt + // This should only happen when turning privacy.resistFingerprinting off. + // Works like disabling accept-language spoofing. + // fall through + case 1: // don't spoof + if ( + Services.prefs.prefHasUserValue("javascript.use_us_english_locale") + ) { + Services.prefs.clearUserPref("javascript.use_us_english_locale"); + } + // We don't reset intl.accept_languages. Instead, setting + // privacy.spoof_english to 1 allows user to change preferred language + // settings through Preferences UI. + break; + case 2: // spoof + Services.prefs.setCharPref("intl.accept_languages", "en-US, en"); + Services.prefs.setBoolPref("javascript.use_us_english_locale", true); + break; + default: + break; + } + } + + _shouldPromptForLanguagePref() { + return ( + Services.locale.appLocaleAsBCP47.substr(0, 2) !== "en" && + Services.prefs.getIntPref(kPrefSpoofEnglish) === 0 + ); + } + + _handleHttpOnModifyRequest(subject, data) { + // If we are loading an HTTP page from content, show the + // "request English language web pages?" prompt. + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + + let notificationCallbacks = httpChannel.notificationCallbacks; + if (!notificationCallbacks) { + return; + } + + let loadContext = notificationCallbacks.getInterface(Ci.nsILoadContext); + if (!loadContext || !loadContext.isContent) { + return; + } + + if (!subject.URI.schemeIs("http") && !subject.URI.schemeIs("https")) { + return; + } + // The above QI did not throw, the scheme is http[s], and we know the + // load context is content, so we must have a true HTTP request from content. + // Stop the observer and display the prompt if another window has + // not already done so. + Services.obs.removeObserver(this, kTopicHttpOnModifyRequest); + + if (!this._shouldPromptForLanguagePref()) { + return; + } + + this._promptForLanguagePreference(); + + // The Accept-Language header for this request was set when the + // channel was created. Reset it to match the value that will be + // used for future requests. + let val = this._getCurrentAcceptLanguageValue(subject.URI); + if (val) { + httpChannel.setRequestHeader("Accept-Language", val, false); + } + } + + _promptForLanguagePreference() { + // Display two buttons, both with string titles. + let flags = Services.prompt.STD_YES_NO_BUTTONS; + let brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + let brandShortName = brandBundle.GetStringFromName("brandShortName"); + let navigatorBundle = Services.strings.createBundle( + "chrome://browser/locale/browser.properties" + ); + let message = navigatorBundle.formatStringFromName( + "privacy.spoof_english", + [brandShortName] + ); + let response = Services.prompt.confirmEx( + null, + "", + message, + flags, + null, + null, + null, + null, + { value: false } + ); + + // Update preferences to reflect their response and to prevent the prompt + // from being displayed again. + Services.prefs.setIntPref(kPrefSpoofEnglish, response == 0 ? 2 : 1); + } + + _getCurrentAcceptLanguageValue(uri) { + let channel = Services.io.newChannelFromURI( + uri, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + let httpChannel; + try { + httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + } catch (e) { + return null; + } + return httpChannel.getRequestHeader("Accept-Language"); + } + + // ============================================================================== + // Letterboxing + // ============================================================================ + /** + * We use the TabsProgressListener to catch the change of the content + * principal. We would clear the margins around the content viewport if + * it is the system principal. + */ + onLocationChange(aBrowser) { + this._addOrClearContentMargin(aBrowser); + } + + _handleLetterboxingPrefChanged() { + if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) { + Services.ww.registerNotification(this); + this._registerActor(); + this._attachAllWindows(); + } else { + this._unregisterActor(); + this._detachAllWindows(); + Services.ww.unregisterNotification(this); + } + } + + _registerActor() { + ChromeUtils.registerWindowActor("RFPHelper", { + parent: { + esModuleURI: "resource:///actors/RFPHelperParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/RFPHelperChild.sys.mjs", + events: { + resize: {}, + }, + }, + allFrames: true, + }); + } + + _unregisterActor() { + ChromeUtils.unregisterWindowActor("RFPHelper"); + } + + // The function to parse the dimension set from the pref value. The pref value + // should be formated as 'width1xheight1, width2xheight2, ...'. For + // example, '100x100, 200x200, 400x200 ...'. + _parseLetterboxingDimensions(aPrefValue) { + if (!aPrefValue || !aPrefValue.match(/^(?:\d+x\d+,\s*)*(?:\d+x\d+)$/)) { + if (aPrefValue) { + console.error( + `Invalid pref value for ${kPrefLetterboxingDimensions}: ${aPrefValue}` + ); + } + return []; + } + + return aPrefValue.split(",").map(item => { + let sizes = item.split("x").map(size => parseInt(size, 10)); + + return { + width: sizes[0], + height: sizes[1], + }; + }); + } + + _addOrClearContentMargin(aBrowser) { + let tab = aBrowser.getTabBrowser().getTabForBrowser(aBrowser); + + // We won't do anything for lazy browsers. + if (!aBrowser.isConnected) { + return; + } + + // We should apply no margin around an empty tab or a tab with system + // principal. + if (tab.isEmpty || aBrowser.contentPrincipal.isSystemPrincipal) { + this._clearContentViewMargin(aBrowser); + } else { + this._roundContentView(aBrowser); + } + } + + /** + * Given a width or height, returns the appropriate margin to apply. + */ + steppedRange(aDimension) { + let stepping; + if (aDimension <= 50) { + return 0; + } else if (aDimension <= 500) { + stepping = 50; + } else if (aDimension <= 1600) { + stepping = 100; + } else { + stepping = 200; + } + + return (aDimension % stepping) / 2; + } + + /** + * The function will round the given browser by adding margins around the + * content viewport. + */ + async _roundContentView(aBrowser) { + let logId = Math.random(); + log("_roundContentView[" + logId + "]"); + let win = aBrowser.ownerGlobal; + let browserContainer = aBrowser + .getTabBrowser() + .getBrowserContainer(aBrowser); + + let { contentWidth, contentHeight, containerWidth, containerHeight } = + await win.promiseDocumentFlushed(() => { + let contentWidth = aBrowser.clientWidth; + let contentHeight = aBrowser.clientHeight; + let containerWidth = browserContainer.clientWidth; + let containerHeight = browserContainer.clientHeight; + + // If the findbar or devtools are out, we need to subtract their height (plus 1 + // for the separator) from the container height, because we need to adjust our + // letterboxing to account for it; however it is not included in that dimension + // (but rather is subtracted from the content height.) + let findBar = win.gFindBarInitialized ? win.gFindBar : undefined; + let findBarOffset = + findBar && !findBar.hidden ? findBar.clientHeight + 1 : 0; + let devtools = browserContainer.getElementsByClassName( + "devtools-toolbox-bottom-iframe" + ); + let devtoolsOffset = devtools.length ? devtools[0].clientHeight : 0; + + return { + contentWidth, + contentHeight, + containerWidth, + containerHeight: containerHeight - findBarOffset - devtoolsOffset, + }; + }); + + log( + "_roundContentView[" + + logId + + "] contentWidth=" + + contentWidth + + " contentHeight=" + + contentHeight + + " containerWidth=" + + containerWidth + + " containerHeight=" + + containerHeight + + " " + ); + + let calcMargins = (aWidth, aHeight) => { + let result; + log( + "_roundContentView[" + + logId + + "] calcMargins(" + + aWidth + + ", " + + aHeight + + ")" + ); + // If the set is empty, we will round the content with the default + // stepping size. + if (!this._letterboxingDimensions.length) { + result = { + width: this.steppedRange(aWidth), + height: this.steppedRange(aHeight), + }; + log( + "_roundContentView[" + + logId + + "] calcMargins(" + + aWidth + + ", " + + aHeight + + ") = " + + result.width + + " x " + + result.height + ); + return result; + } + + let matchingArea = aWidth * aHeight; + let minWaste = Number.MAX_SAFE_INTEGER; + let targetDimensions = undefined; + + // Find the desired dimensions which waste the least content area. + for (let dim of this._letterboxingDimensions) { + // We don't need to consider the dimensions which cannot fit into the + // real content size. + if (dim.width > aWidth || dim.height > aHeight) { + continue; + } + + let waste = matchingArea - dim.width * dim.height; + + if (waste >= 0 && waste < minWaste) { + targetDimensions = dim; + minWaste = waste; + } + } + + // If we cannot find any dimensions match to the real content window, this + // means the content area is smaller the smallest size in the set. In this + // case, we won't apply any margins. + if (!targetDimensions) { + result = { + width: 0, + height: 0, + }; + } else { + result = { + width: (aWidth - targetDimensions.width) / 2, + height: (aHeight - targetDimensions.height) / 2, + }; + } + + log( + "_roundContentView[" + + logId + + "] calcMargins(" + + aWidth + + ", " + + aHeight + + ") = " + + result.width + + " x " + + result.height + ); + return result; + }; + + // Calculating the margins around the browser element in order to round the + // content viewport. We will use a 200x100 stepping if the dimension set + // is not given. + let margins = calcMargins(containerWidth, containerHeight); + + // If the size of the content is already quantized, we do nothing. + if (aBrowser.style.margin == `${margins.height}px ${margins.width}px`) { + log("_roundContentView[" + logId + "] is_rounded == true"); + if (this._isLetterboxingTesting) { + log( + "_roundContentView[" + + logId + + "] is_rounded == true test:letterboxing:update-margin-finish" + ); + Services.obs.notifyObservers( + null, + "test:letterboxing:update-margin-finish" + ); + } + return; + } + + win.requestAnimationFrame(() => { + log( + "_roundContentView[" + + logId + + "] setting margins to " + + margins.width + + " x " + + margins.height + ); + // One cannot (easily) control the color of a margin unfortunately. + // An initial attempt to use a border instead of a margin resulted + // in offset event dispatching; so for now we use a colorless margin. + aBrowser.style.margin = `${margins.height}px ${margins.width}px`; + }); + } + + _clearContentViewMargin(aBrowser) { + aBrowser.ownerGlobal.requestAnimationFrame(() => { + aBrowser.style.margin = ""; + }); + } + + _updateMarginsForTabsInWindow(aWindow) { + let tabBrowser = aWindow.gBrowser; + + for (let tab of tabBrowser.tabs) { + let browser = tab.linkedBrowser; + this._addOrClearContentMargin(browser); + } + } + + _attachWindow(aWindow) { + aWindow.gBrowser.addTabsProgressListener(this); + aWindow.addEventListener("TabOpen", this); + + // Rounding the content viewport. + this._updateMarginsForTabsInWindow(aWindow); + } + + _attachAllWindows() { + let windowList = Services.wm.getEnumerator("navigator:browser"); + + while (windowList.hasMoreElements()) { + let win = windowList.getNext(); + + if (win.closed || !win.gBrowser) { + continue; + } + + this._attachWindow(win); + } + } + + _detachWindow(aWindow) { + let tabBrowser = aWindow.gBrowser; + tabBrowser.removeTabsProgressListener(this); + aWindow.removeEventListener("TabOpen", this); + + // Clear all margins and tooltip for all browsers. + for (let tab of tabBrowser.tabs) { + let browser = tab.linkedBrowser; + this._clearContentViewMargin(browser); + } + } + + _detachAllWindows() { + let windowList = Services.wm.getEnumerator("navigator:browser"); + + while (windowList.hasMoreElements()) { + let win = windowList.getNext(); + + if (win.closed || !win.gBrowser) { + continue; + } + + this._detachWindow(win); + } + } + + _handleDOMWindowOpened(win) { + let self = this; + + win.addEventListener( + "load", + () => { + // We attach to the new window when it has been loaded if the new loaded + // window is a browsing window. + if ( + win.document.documentElement.getAttribute("windowtype") !== + "navigator:browser" + ) { + return; + } + self._attachWindow(win); + }, + { once: true } + ); + } +} + +export let RFPHelper = new _RFPHelper(); diff --git a/toolkit/components/resistfingerprinting/RFPTargets.inc b/toolkit/components/resistfingerprinting/RFPTargets.inc new file mode 100644 index 0000000000..a65b52cba8 --- /dev/null +++ b/toolkit/components/resistfingerprinting/RFPTargets.inc @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// Names should not be re-used. + +ITEM_VALUE(TouchEvents, 1u << 0) +ITEM_VALUE(PointerEvents, 1u << 1) +ITEM_VALUE(KeyboardEvents, 1u << 2) +ITEM_VALUE(ScreenOrientation, 1u << 3) +// SpeechSynthesis part of the Web Speech API +ITEM_VALUE(SpeechSynthesis, 1u << 4) +// `prefers-color-scheme` CSS media feature +ITEM_VALUE(CSSPrefersColorScheme, 1u << 5) +// `prefers-reduced-motion` CSS media feature +ITEM_VALUE(CSSPrefersReducedMotion, 1u << 6) +// `prefers-contrast` CSS media feature +ITEM_VALUE(CSSPrefersContrast, 1u << 7) +// Add random noises to image data extracted from canvas. +ITEM_VALUE(CanvasRandomization, 1u << 8) +// Canvas targets: For unusual combinations of these, see comments +// in IsImageExtractionAllowed +ITEM_VALUE(CanvasImageExtractionPrompt, 1u << 9) +ITEM_VALUE(CanvasExtractionFromThirdPartiesIsBlocked, 1u << 10) +ITEM_VALUE(CanvasExtractionBeforeUserInputIsBlocked, 1u << 11) +// Various "client identification" values of the navigator object +ITEM_VALUE(NavigatorAppName, 1u << 12) +ITEM_VALUE(NavigatorAppVersion, 1u << 13) +ITEM_VALUE(NavigatorBuildID, 1u << 14) +ITEM_VALUE(NavigatorHWConcurrency, 1u << 15) +ITEM_VALUE(NavigatorOscpu, 1u << 16) +ITEM_VALUE(NavigatorPlatform, 1u << 17) +ITEM_VALUE(NavigatorUserAgent, 1u << 18) +// Audio/VideoStreamTrack labels +ITEM_VALUE(StreamTrackLabel, 1u << 19) +ITEM_VALUE(StreamVideoFacingMode, 1u << 20) + +// !!! Don't forget to update kDefaultFingerintingProtections in nsRFPService.cpp +// if necessary. + +/* + * In certain cases, we precompute the value of ShouldRFP for e.g. a Document. + * (This saves us more computation and casting later.) This document will still + * need to check whether an individual target is allowed, but the initial + * question of "Does this document have any RFP applied to it ever?" can still + * be precomputed. This enum value will always be included in RFPLite, so when a + * document asks if they might have RFP enabled, it will return true. (Putting + * this value in the overrides pref is undefined behavior and may do anything.) + */ +ITEM_VALUE(IsAlwaysEnabledForPrecompute, 0) + +/* + * This value is the default argument value, to allow all callsites to ShouldRFP + * continue working. We will eventually remove the default argument, and then also + * remove this enum value. + */ +ITEM_VALUE(Unknown, 0xffffffff) diff --git a/toolkit/components/resistfingerprinting/RelativeTimeline.cpp b/toolkit/components/resistfingerprinting/RelativeTimeline.cpp new file mode 100644 index 0000000000..eca7fca975 --- /dev/null +++ b/toolkit/components/resistfingerprinting/RelativeTimeline.cpp @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsCOMPtr.h" +#include "nsIRandomGenerator.h" +#include "nsServiceManagerUtils.h" + +#include "RelativeTimeline.h" + +namespace mozilla { + +int64_t RelativeTimeline::GetRandomTimelineSeed() { + if (mRandomTimelineSeed == 0) { + nsresult rv; + nsCOMPtr<nsIRandomGenerator> randomGenerator = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + mRandomTimelineSeed = rand(); + return mRandomTimelineSeed; + } + + uint8_t* buffer = nullptr; + rv = randomGenerator->GenerateRandomBytes(sizeof(mRandomTimelineSeed), + &buffer); + if (NS_WARN_IF(NS_FAILED(rv))) { + mRandomTimelineSeed = rand(); + return mRandomTimelineSeed; + } + + memcpy(&mRandomTimelineSeed, buffer, sizeof(mRandomTimelineSeed)); + MOZ_ASSERT(buffer); + free(buffer); + } + return mRandomTimelineSeed; +} + +} // namespace mozilla diff --git a/toolkit/components/resistfingerprinting/RelativeTimeline.h b/toolkit/components/resistfingerprinting/RelativeTimeline.h new file mode 100644 index 0000000000..bf23de904d --- /dev/null +++ b/toolkit/components/resistfingerprinting/RelativeTimeline.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef __RelativeTimeline_h__ +#define __RelativeTimeline_h__ + +#include <cstdint> + +namespace mozilla { + +class RelativeTimeline { + public: + RelativeTimeline() : mRandomTimelineSeed(0) {} + + int64_t GetRandomTimelineSeed(); + + private: + uint64_t mRandomTimelineSeed; +}; + +} // namespace mozilla + +#endif /* __RelativeTimeline_h__ */ diff --git a/toolkit/components/resistfingerprinting/docs/implementation.rst b/toolkit/components/resistfingerprinting/docs/implementation.rst new file mode 100644 index 0000000000..9e8a7491d4 --- /dev/null +++ b/toolkit/components/resistfingerprinting/docs/implementation.rst @@ -0,0 +1,78 @@ +========================= +Implementation +========================= + +Checking if an object should resist fingerprinting is ideally done by referencing the `Document's ShouldResistFingerprinting <https://searchfox.org/mozilla-central/search?q=symbol:_ZNK7mozilla3dom8Document26ShouldResistFingerprintingENS_9RFPTargetE&redirect=false>`_ method. This is both fast and correct. In certain other situations, you may need to call some of the ``nsContentUtils::ShouldResistFingerprinting`` functions. When doing so, you should avoid calling either of the functions marked *dangerous*. + +As you can see in the callgraph below, directly calling a *dangerous* function will skip some of the checks that occur further up-stack. + +.. mermaid:: + + graph TD + SRFP["ShouldResistFingerprinting()"] + click SRFP href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEN7mozilla9RFPTargetE&redirect=false" + + SRGP_GO["ShouldResistFingerprinting(nsIGlobalObject* aGlobalObject"] + click SRGP_GO href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEP15nsIGlobalObjectN7mozilla9RFPTargetE&redirect=false" + + GO_SRFP["nsIGlobalObject*::ShouldResistFingerprinting()"] + click GO_SRFP href "https://searchfox.org/mozilla-central/search?q=symbol:_ZNK15nsIGlobalObject26ShouldResistFingerprintingEN7mozilla9RFPTargetE&redirect=false" + + Doc_SRFP["Document::ShouldResistFingerprinting()<br />System Principal Check"] + click Doc_SRFP href "https://searchfox.org/mozilla-central/search?q=symbol:_ZNK7mozilla3dom8Document26ShouldResistFingerprintingENS_9RFPTargetE&redirect=false" + + SRFP_char["ShouldResistFingerprinting(const char*)"] + click SRFP_char href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEPKcN7mozilla9RFPTargetE&redirect=false" + + SRFP_callertype_go["ShouldResistFingerprinting(CallerType, nsIGlobalObject*)<br />System Principal Check"] + click SRFP_callertype_go href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEN7mozilla3dom10CallerTypeEP15nsIGlobalObjectNS0_9RFPTargetE&redirect=false" + + SRFP_docshell["ShouldResistFingerprinting(nsIDocShell*)"] + click SRFP_docshell href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEP11nsIDocShellN7mozilla9RFPTargetE&redirect=false" + + SRFP_channel["ShouldResistFingerprinting(nsIChannel*)<br />ETPSaysShouldNotResistFingerprinting Check<br />CookieJarSettingsSaysShouldResistFingerprinting Check"] + click SRFP_channel href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEP10nsIChannelN7mozilla9RFPTargetE&redirect=false" + + SRFP_uri["ShouldResistFingerprinting_dangerous(nsIURI*, OriginAttributes)<br />PBM Check<br />Scheme (inc WebExtension) Check<br />About Page Check<br />URI & Partition Key Exempt Check"] + click SRFP_uri href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils36ShouldResistFingerprinting_dangerousEP6nsIURIRKN7mozilla16OriginAttributesEPKcNS2_9RFPTargetE&redirect=false" + + SRFP_principal["ShouldResistFingerprinting_dangerous(nsIPrincipal*)<br />System Principal Check<br />PBM Check<br />Scheme Check<br />About Page Check<br />Web Extension Principal Check<br />URI & Partition Key Exempt Check"] + click SRFP_principal href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils36ShouldResistFingerprinting_dangerousEP12nsIPrincipalPKcN7mozilla9RFPTargetE&redirect=false" + + + + + SRFP_principal --> |null| SRFP_char + + SRFP_uri --> |null| SRFP_char + + SRFP_channel -->|null| SRFP_char + SRFP_channel --> |Document Load| SRFP_uri + SRFP_channel --> |Subresource Load| SRFP_principal + + SRFP_docshell -->|null| SRFP_char + SRFP_docshell --> Doc_SRFP + + SRFP_callertype_go --> SRGP_GO + SRFP_char --> SRFP + SRGP_GO -->|null| SRFP_char + SRGP_GO --> GO_SRFP + + GO_SRFP --> |innerWindow, outerWindow| Doc_SRFP + Doc_SRFP --> SRFP_channel + + +Exemptions and Targets +~~~~~~~~~~~~~~~~~~~~~~ + +Fingerprinting Resistance takes into account many things to determine if we should alter behavior: + +* Whether we are the System Principal +* Whether we are a Web Extension +* Whether Fingerprinting Resistance is applied to all browsing modes or only Private Browsing Mode +* Whether the specific site you are visiting has been granted an exemption (taking into account the framing page) +* Whether the specific **activity** is granted an exemption + +All callsites for ``ShouldResistFingerprinting`` take a (currently) optional ``RFPTarget`` value, which defaults to ``Unknown``. While arguments such as ``Document`` or ``nsIChannel`` provide context for the first four exemptions above, the Target provides context for the final one. A Target is a Web API or an activity - such as a Pointer Event, the Screen Orientation, or plugging a gamepad into your computer (and therefore producing a `gamepadconnected event <https://www.w3.org/TR/gamepad/#event-gamepadconnected>`_). Most Targets correlate strongly to a specific Web API, but not all do: for example whether or not to automatically reject Canvas extraction requests from third parties is a separate Target from prompting to reject canvas extraction. + +In some situations we may *not* alter our behavior for a certain activity - this could be based on the fingerprinting resistance mode you are using, or per-site overrides to correct breakage. Targets are defined `RFPTargets.inc <https://searchfox.org/mozilla-central/source/toolkit/components/resistfingerprinting/RFPTargets.inc>`_. diff --git a/toolkit/components/resistfingerprinting/docs/index.rst b/toolkit/components/resistfingerprinting/docs/index.rst new file mode 100644 index 0000000000..57bb24beac --- /dev/null +++ b/toolkit/components/resistfingerprinting/docs/index.rst @@ -0,0 +1,8 @@ +========================= +Fingerprinting Resistance +========================= + +.. toctree:: + :maxdepth: 1 + + implementation diff --git a/toolkit/components/resistfingerprinting/moz.build b/toolkit/components/resistfingerprinting/moz.build new file mode 100644 index 0000000000..550328165a --- /dev/null +++ b/toolkit/components/resistfingerprinting/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") + +TEST_DIRS += ["tests"] + +UNIFIED_SOURCES += [ + "nsRFPService.cpp", + "RelativeTimeline.cpp", +] + +SPHINX_TREES["resistfingerprinting"] = "docs" + +FINAL_LIBRARY = "xul" + +EXPORTS += ["nsRFPService.h", "RFPTargets.inc"] +EXPORTS.mozilla += [ + "RelativeTimeline.h", +] + +EXTRA_JS_MODULES += [ + "RFPHelper.sys.mjs", +] diff --git a/toolkit/components/resistfingerprinting/nsRFPService.cpp b/toolkit/components/resistfingerprinting/nsRFPService.cpp new file mode 100644 index 0000000000..42dca79e25 --- /dev/null +++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp @@ -0,0 +1,1461 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsRFPService.h" + +#include <algorithm> +#include <cfloat> +#include <cinttypes> +#include <cmath> +#include <cstdlib> +#include <cstring> +#include <ctime> +#include <new> +#include <type_traits> +#include <utility> + +#include "MainThreadUtils.h" +#include "ScopedNSSTypes.h" + +#include "mozilla/ArrayIterator.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Casting.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/HelperMacros.h" +#include "mozilla/Likely.h" +#include "mozilla/Logging.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_javascript.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/fallible.h" +#include "mozilla/XorShift128PlusRNG.h" + +#include "nsBaseHashtable.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsCoord.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsJSUtils.h" +#include "nsLiteralString.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsTArray.h" +#include "nsTLiteralString.h" +#include "nsTPromiseFlatString.h" +#include "nsTStringRepr.h" +#include "nsXPCOM.h" + +#include "nsICookieJarSettings.h" +#include "nsICryptoHash.h" +#include "nsIGlobalObject.h" +#include "nsIObserverService.h" +#include "nsIRandomGenerator.h" +#include "nsIUserIdleService.h" +#include "nsIXULAppInfo.h" + +#include "nscore.h" +#include "prenv.h" +#include "prtime.h" +#include "xpcpublic.h" + +#include "js/Date.h" + +using namespace mozilla; + +static mozilla::LazyLogModule gResistFingerprintingLog( + "nsResistFingerprinting"); + +#define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting" +#define RESIST_FINGERPRINTING_PBMODE_PREF "privacy.resistFingerprinting.pbmode" +#define RESIST_FINGERPRINTINGPROTECTION_PREF "privacy.fingerprintingProtection" +#define RESIST_FINGERPRINTINGPROTECTION_PBMODE_PREF \ + "privacy.fingerprintingProtection.pbmode" +#define RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF \ + "privacy.fingerprintingProtection.overrides" +#define RFP_TIMER_UNCONDITIONAL_VALUE 20 +#define PROFILE_INITIALIZED_TOPIC "profile-initial-state" +#define LAST_PB_SESSION_EXITED_TOPIC "last-pb-context-exited" + +static constexpr uint32_t kVideoFramesPerSec = 30; +static constexpr uint32_t kVideoDroppedRatio = 5; + +#define RFP_DEFAULT_SPOOFING_KEYBOARD_LANG KeyboardLang::EN +#define RFP_DEFAULT_SPOOFING_KEYBOARD_REGION KeyboardRegion::US + +// Fingerprinting protections that are enabled by default. This can be +// overridden using the privacy.fingerprintingProtection.overrides pref. +const uint32_t kDefaultFingerintingProtections = + uint32_t(RFPTarget::CanvasRandomization); + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Structural Stuff & Pref Observing + +NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver) + +static StaticRefPtr<nsRFPService> sRFPService; +static bool sInitialized = false; + +// Actually enabled fingerprinting protections. +static Atomic<uint32_t> sEnabledFingerintingProtections; + +/* static */ +nsRFPService* nsRFPService::GetOrCreate() { + if (!sInitialized) { + sRFPService = new nsRFPService(); + nsresult rv = sRFPService->Init(); + + if (NS_FAILED(rv)) { + sRFPService = nullptr; + return nullptr; + } + + ClearOnShutdown(&sRFPService); + sInitialized = true; + } + + return sRFPService; +} + +static const char* gCallbackPrefs[] = { + RESIST_FINGERPRINTING_PREF, + RESIST_FINGERPRINTING_PBMODE_PREF, + RESIST_FINGERPRINTINGPROTECTION_PREF, + RESIST_FINGERPRINTINGPROTECTION_PBMODE_PREF, + RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF, + nullptr, +}; + +nsresult nsRFPService::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE); + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + if (XRE_IsParentProcess()) { + rv = obs->AddObserver(this, LAST_PB_SESSION_EXITED_TOPIC, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obs->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, false); + NS_ENSURE_SUCCESS(rv, rv); + } + +#if defined(XP_WIN) + rv = obs->AddObserver(this, PROFILE_INITIALIZED_TOPIC, false); + NS_ENSURE_SUCCESS(rv, rv); +#endif + + Preferences::RegisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs, + this); + // We backup the original TZ value here. + const char* tzValue = PR_GetEnv("TZ"); + if (tzValue != nullptr) { + mInitialTZValue = nsCString(tzValue); + } + + // Call Update here to cache the values of the prefs and set the timezone. + UpdateRFPPref(); + UpdateFPPOverrideList(); + + return rv; +} + +/* static */ +bool nsRFPService::IsRFPEnabledFor(RFPTarget aTarget) { + if (StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() || + StaticPrefs::privacy_resistFingerprinting_pbmode_DoNotUseDirectly()) { + return true; + } + + if (StaticPrefs::privacy_fingerprintingProtection_DoNotUseDirectly() || + StaticPrefs::privacy_fingerprintingProtection_pbmode_DoNotUseDirectly()) { + if (aTarget == RFPTarget::IsAlwaysEnabledForPrecompute) { + return true; + } + // All not yet explicitly defined targets are disabled by default (no + // fingerprinting protection). + if (aTarget == RFPTarget::Unknown) { + return false; + } + return sEnabledFingerintingProtections & uint32_t(aTarget); + } + + return false; +} + +// This function updates every fingerprinting item necessary except +// timing-related +void nsRFPService::UpdateRFPPref() { + MOZ_ASSERT(NS_IsMainThread()); + + bool resistFingerprinting = nsContentUtils::ShouldResistFingerprinting(); + + JS::SetReduceMicrosecondTimePrecisionCallback( + nsRFPService::ReduceTimePrecisionAsUSecsWrapper); + + // The JavaScript engine can already set the timezone per realm/global, + // but we think there are still other users of libc that rely + // on the TZ environment variable. + if (!StaticPrefs::privacy_resistFingerprinting_testing_setTZtoUTC()) { + return; + } + + if (resistFingerprinting) { + PR_SetEnv("TZ=UTC"); + } else if (sInitialized) { + // We will not touch the TZ value if 'privacy.resistFingerprinting' is false + // during the time of initialization. + if (!mInitialTZValue.IsEmpty()) { + nsAutoCString tzValue = "TZ="_ns + mInitialTZValue; + static char* tz = nullptr; + + // If the tz has been set before, we free it first since it will be + // allocated a new value later. + if (tz != nullptr) { + free(tz); + } + // PR_SetEnv() needs the input string been leaked intentionally, so + // we copy it here. + tz = ToNewCString(tzValue, mozilla::fallible); + if (tz != nullptr) { + PR_SetEnv(tz); + } + } else { +#if defined(XP_WIN) + // For Windows, we reset the TZ to an empty string. This will make Windows + // to use its system timezone. + PR_SetEnv("TZ="); +#else + // For POSIX like system, we reset the TZ to the /etc/localtime, which is + // the system timezone. + PR_SetEnv("TZ=:/etc/localtime"); +#endif + } + } + + // If and only if the time zone was changed above, propagate the change to the + // <time.h> functions and the JS runtime. + if (resistFingerprinting || sInitialized) { + // localtime_r (and other functions) may not call tzset, so do this here + // after changing TZ to ensure all <time.h> functions use the new time zone. +#if defined(XP_WIN) + _tzset(); +#else + tzset(); +#endif + + nsJSUtils::ResetTimeZone(); + } +} + +void nsRFPService::UpdateFPPOverrideList() { + nsAutoString targetOverrides; + nsresult rv = Preferences::GetString( + RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF, targetOverrides); + if (!NS_SUCCEEDED(rv) || targetOverrides.IsEmpty()) { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Could not map any values")); + return; + } + + uint32_t enabled = kDefaultFingerintingProtections; + for (const nsAString& each : targetOverrides.Split(',')) { + Maybe<RFPTarget> mappedValue = + nsRFPService::TextToRFPTarget(Substring(each, 1, each.Length() - 1)); + if (mappedValue.isSome()) { + RFPTarget target = mappedValue.value(); + if (target == RFPTarget::IsAlwaysEnabledForPrecompute || + target == RFPTarget::Unknown) { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("RFPTarget::%s is not a valid value", + NS_ConvertUTF16toUTF8(each).get())); + } else if (each[0] == '+') { + enabled |= uint32_t(target); + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Mapped value %s (0x%08x), to an addition, now we have 0x%08x", + NS_ConvertUTF16toUTF8(each).get(), unsigned(target), enabled)); + } else if (each[0] == '-') { + enabled &= ~uint32_t(target); + MOZ_LOG( + gResistFingerprintingLog, LogLevel::Warning, + ("Mapped value %s (0x%08x) to a subtraction, now we have 0x%08x", + NS_ConvertUTF16toUTF8(each).get(), unsigned(target), enabled)); + } else { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Mapped value %s (0x%08x) to an RFPTarget Enum, but the first " + "character wasn't + or -", + NS_ConvertUTF16toUTF8(each).get(), unsigned(target))); + } + } else { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Could not map the value %s to an RFPTarget Enum", + NS_ConvertUTF16toUTF8(each).get())); + } + } + + sEnabledFingerintingProtections = enabled; +} + +/* static */ +Maybe<RFPTarget> nsRFPService::TextToRFPTarget(const nsAString& aText) { +#define ITEM_VALUE(name, value) \ + if (aText.EqualsLiteral(#name)) { \ + return Some(RFPTarget::name); \ + } + +#include "RFPTargets.inc" +#undef ITEM_VALUE + + return Nothing(); +} + +void nsRFPService::StartShutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + if (XRE_IsParentProcess()) { + obs->RemoveObserver(this, LAST_PB_SESSION_EXITED_TOPIC); + obs->RemoveObserver(this, OBSERVER_TOPIC_IDLE_DAILY); + } + } + + Preferences::UnregisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs, + this); +} + +// static +void nsRFPService::PrefChanged(const char* aPref, void* aSelf) { + static_cast<nsRFPService*>(aSelf)->PrefChanged(aPref); +} + +void nsRFPService::PrefChanged(const char* aPref) { + nsDependentCString pref(aPref); + + if (pref.EqualsLiteral(RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF)) { + UpdateFPPOverrideList(); + } else { + UpdateRFPPref(); + +#if defined(XP_WIN) + if (StaticPrefs::privacy_resistFingerprinting_testing_setTZtoUTC() && + !XRE_IsE10sParentProcess()) { + // Windows does not follow POSIX. Updates to the TZ environment variable + // are not reflected immediately on that platform as they are on UNIX + // systems without this call. + _tzset(); + } +#endif + } +} + +NS_IMETHODIMP +nsRFPService::Observe(nsISupports* aObject, const char* aTopic, + const char16_t* aMessage) { + if (strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, aTopic) == 0) { + StartShutdown(); + } +#if defined(XP_WIN) + else if (!strcmp(PROFILE_INITIALIZED_TOPIC, aTopic)) { + // If we're e10s, then we don't need to run this, since the child process + // will simply inherit the environment variable from the parent process, in + // which case it's unnecessary to call _tzset(). + if (XRE_IsParentProcess() && !XRE_IsE10sParentProcess()) { + // Windows does not follow POSIX. Updates to the TZ environment variable + // are not reflected immediately on that platform as they are on UNIX + // systems without this call. + _tzset(); + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE); + + nsresult rv = obs->RemoveObserver(this, PROFILE_INITIALIZED_TOPIC); + NS_ENSURE_SUCCESS(rv, rv); + } +#endif + + if (strcmp(LAST_PB_SESSION_EXITED_TOPIC, aTopic) == 0) { + // Clear the private session key when the private session ends so that we + // can generate a new key for the new private session. + ClearSessionKey(true); + } + + if (!strcmp(OBSERVER_TOPIC_IDLE_DAILY, aTopic)) { + if (StaticPrefs:: + privacy_resistFingerprinting_randomization_daily_reset_enabled()) { + ClearSessionKey(false); + } + + if (StaticPrefs:: + privacy_resistFingerprinting_randomization_daily_reset_private_enabled()) { + ClearSessionKey(true); + } + } + + return NS_OK; +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Reduce Timer Precision Stuff + +constexpr double RFP_TIME_ATOM_MS = 16.667; // 60Hz, 1000/60 but rounded. +/* +In RFP RAF always runs at 60Hz, so we're ~0.02% off of 1000/60 here. +```js +extra_frames_per_frame = 16.667 / (1000/60) - 1 // 0.00028 +sec_per_extra_frame = 1 / (extra_frames_per_frame * 60) // 833.33 +min_per_extra_frame = sec_per_extra_frame / 60 // 13.89 +``` +We expect an extra frame every ~14 minutes, which is enough to be smooth. +16.67 would be ~1.4 minutes, which is OK, but is more noticable. +Put another way, if this is the only unacceptable hitch you have across 14 +minutes, I'm impressed, and we might revisit this. +*/ + +/* static */ +double nsRFPService::TimerResolution(RTPCallerType aRTPCallerType) { + double prefValue = StaticPrefs:: + privacy_resistFingerprinting_reduceTimerPrecision_microseconds(); + if (aRTPCallerType == RTPCallerType::ResistFingerprinting) { + return std::max(RFP_TIME_ATOM_MS * 1000.0, prefValue); + } + return prefValue; +} + +/** + * The purpose of this function is to deterministicly generate a random midpoint + * between a lower clamped value and an upper clamped value. Assuming a clamping + * resolution of 100, here is an example: + * + * |---------------------------------------|--------------------------| + * lower clamped value (e.g. 300) | upper clamped value (400) + * random midpoint (e.g. 360) + * + * If our actual timestamp (e.g. 325) is below the midpoint, we keep it clamped + * downwards. If it were equal to or above the midpoint (e.g. 365) we would + * round it upwards to the largest clamped value (in this example: 400). + * + * The question is: does time go backwards? + * + * The midpoint is deterministicly random and generated from three components: + * a secret seed, a per-timeline (context) 'mix-in', and a clamped time. + * + * When comparing times across different seed values: time may go backwards. + * For a clamped time of 300, one seed may generate a midpoint of 305 and + * another 395. So comparing an (actual) timestamp of 325 and 351 could see the + * 325 clamped up to 400 and the 351 clamped down to 300. The seed is + * per-process, so this case occurs when one can compare timestamps + * cross-process. This is uncommon (because we don't have site isolation.) The + * circumstances this could occur are BroadcastChannel, Storage Notification, + * and in theory (but not yet implemented) SharedWorker. This should be an + * exhaustive list (at time of comment writing!). + * + * Aside from cross-process communication, derived timestamps across different + * time origins may go backwards. (Specifically, derived means adding two + * timestamps together to get an (approximate) absolute time.) + * Assume a page and a worker. If one calls performance.now() in the page and + * then triggers a call to performance.now() in the worker, the following + * invariant should hold true: + * page.performance.timeOrigin + page.performance.now() < + * worker.performance.timeOrigin + worker.performance.now() + * + * We break this invariant. + * + * The 'Context Mix-in' is a securely generated random seed that is unique for + * each timeline that starts over at zero. It is needed to ensure that the + * sequence of midpoints (as calculated by the secret seed and clamped time) + * does not repeat. In RelativeTimeline.h, we define a 'RelativeTimeline' class + * that can be inherited by any object that has a relative timeline. The most + * obvious examples are Documents and Workers. An attacker could let time go + * forward and observe (roughly) where the random midpoints fall. Then they + * create a new object, time starts back over at zero, and they know + * (approximately) where the random midpoints are. + * + * When the timestamp given is a non-relative timestamp (e.g. it is relative to + * the unix epoch) it is not possible to replay a sequence of random values. + * Thus, providing a zero context pointer is an indicator that the timestamp + * given is absolute and does not need any additional randomness. + * + * @param aClampedTimeUSec [in] The clamped input time in microseconds. + * @param aResolutionUSec [in] The current resolution for clamping in + * microseconds. + * @param aMidpointOut [out] The midpoint, in microseconds, between [0, + * aResolutionUSec]. + * @param aContextMixin [in] An opaque random value for relative + * timestamps. 0 for absolute timestamps + * @param aSecretSeed [in] TESTING ONLY. When provided, the current seed + * will be replaced with this value. + * @return A nsresult indicating success of failure. If the + * function failed, nothing is written to aMidpointOut + */ + +/* static */ +nsresult nsRFPService::RandomMidpoint(long long aClampedTimeUSec, + long long aResolutionUSec, + int64_t aContextMixin, + long long* aMidpointOut, + uint8_t* aSecretSeed /* = nullptr */) { + nsresult rv; + const int kSeedSize = 16; + static Atomic<uint8_t*> sSecretMidpointSeed; + + if (MOZ_UNLIKELY(!aMidpointOut)) { + return NS_ERROR_INVALID_ARG; + } + + /* + * Below, we will use three different values to seed a fairly simple random + * number generator. On the first run we initiate the secret seed, which + * is mixed in with the time epoch and the context mix in to seed the RNG. + * + * This isn't the most secure method of generating a random midpoint but is + * reasonably performant and should be sufficient for our purposes. + */ + + // If we don't have a seed, we need to get one. + if (MOZ_UNLIKELY(!sSecretMidpointSeed)) { + nsCOMPtr<nsIRandomGenerator> randomGenerator = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint8_t* temp = nullptr; + rv = randomGenerator->GenerateRandomBytes(kSeedSize, &temp); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (MOZ_UNLIKELY(!sSecretMidpointSeed.compareExchange(nullptr, temp))) { + // Some other thread initted this first, never mind! + delete[] temp; + } + } + + // sSecretMidpointSeed is now set, and invariant. The contents of the buffer + // it points to is also invariant, _unless_ this function is called with a + // non-null |aSecretSeed|. + uint8_t* seed = sSecretMidpointSeed; + MOZ_RELEASE_ASSERT(seed); + + // If someone has passed in the testing-only parameter, replace our seed with + // it. We do _not_ re-allocate the buffer, since that can lead to UAF below. + // The math could still be racy if the caller supplies a new secret seed while + // some other thread is calling this function, but since this is arcane + // test-only functionality that is used in only one test-case presently, we + // put the burden of using this particular footgun properly on the test code. + if (MOZ_UNLIKELY(aSecretSeed != nullptr)) { + memcpy(seed, aSecretSeed, kSeedSize); + } + + // Seed and create our random number generator. + non_crypto::XorShift128PlusRNG rng(aContextMixin ^ *(uint64_t*)(seed), + aClampedTimeUSec ^ *(uint64_t*)(seed + 8)); + + // Retrieve the output midpoint value. + if (MOZ_UNLIKELY(aResolutionUSec <= 0)) { // ??? Bug 1718066 + return NS_ERROR_FAILURE; + } + *aMidpointOut = rng.next() % aResolutionUSec; + + return NS_OK; +} + +/** + * Given a precision value, this function will reduce a given input time to the + * nearest multiple of that precision. + * + * It will check if it is appropriate to clamp the input time according to the + * values of the given TimerPrecisionType. Note that if one desires a minimum + * precision for Resist Fingerprinting, it is the caller's responsibility to + * provide the correct value. This means you should pass TimerResolution(), + * which enforces a minimum value on the precision based on preferences. + * + * It ensures the given precision value is greater than zero, if it is not it + * returns the input time. + * + * While the correct thing to pass is TimerResolution() we expose it as an + * argument for testing purposes only. + * + * @param aTime [in] The input time to be clamped. + * @param aTimeScale [in] The units the input time is in (Seconds, + * Milliseconds, or Microseconds). + * @param aResolutionUSec [in] The precision (in microseconds) to clamp to. + * @param aContextMixin [in] An opaque random value for relative timestamps. + * 0 for absolute timestamps + * @return If clamping is appropriate, the clamped value of the + * input, otherwise the input. + */ +/* static */ +double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale, + double aResolutionUSec, + int64_t aContextMixin, + TimerPrecisionType aType) { + if (aType == TimerPrecisionType::DangerouslyNone) { + return aTime; + } + + // This boolean will serve as a flag indicating we are clamping the time + // unconditionally. We do this when timer reduction preference is off; but we + // still want to apply 20us clamping to al timestamps to avoid leaking + // nano-second precision. + bool unconditionalClamping = false; + if (aType == UnconditionalAKAHighRes || aResolutionUSec <= 0) { + unconditionalClamping = true; + aResolutionUSec = RFP_TIMER_UNCONDITIONAL_VALUE; // 20 microseconds + aContextMixin = 0; // Just clarifies our logging statement at the end, + // otherwise unused + } + + // Increase the time as needed until it is in microseconds. + // Note that a double can hold up to 2**53 with integer precision. This gives + // us only until June 5, 2255 in time-since-the-epoch with integer precision. + // So we will be losing microseconds precision after that date. + // We think this is okay, and we codify it in some tests. + double timeScaled = aTime * (1000000 / aTimeScale); + // Cut off anything less than a microsecond. + long long timeAsInt = timeScaled; + + // If we have a blank context mixin, this indicates we (should) have an + // absolute timestamp. We check the time, and if it less than a unix timestamp + // about 10 years in the past, we output to the log and, in debug builds, + // assert. This is an error case we want to understand and fix: we must have + // given a relative timestamp with a mixin of 0 which is incorrect. Anyone + // running a debug build _probably_ has an accurate clock, and if they don't, + // they'll hopefully find this message and understand why things are crashing. + const long long kFeb282008 = 1204233985000; + if (aContextMixin == 0 && timeAsInt < kFeb282008 && !unconditionalClamping && + aType != TimerPrecisionType::RFP) { + nsAutoCString type; + TypeToText(aType, type); + MOZ_LOG( + gResistFingerprintingLog, LogLevel::Error, + ("About to assert. aTime=%lli<%lli aContextMixin=%" PRId64 " aType=%s", + timeAsInt, kFeb282008, aContextMixin, type.get())); + MOZ_ASSERT( + false, + "ReduceTimePrecisionImpl was given a relative time " + "with an empty context mix-in (or your clock is 10+ years off.) " + "Run this with MOZ_LOG=nsResistFingerprinting:1 to get more details."); + } + + // Cast the resolution (in microseconds) to an int. + long long resolutionAsInt = aResolutionUSec; + // Perform the clamping. + // We do a cast back to double to perform the division with doubles, then + // floor the result and the rest occurs with integer precision. This is + // because it gives consistency above and below zero. Above zero, performing + // the division in integers truncates decimals, taking the result closer to + // zero (a floor). Below zero, performing the division in integers truncates + // decimals, taking the result closer to zero (a ceil). The impact of this is + // that comparing two clamped values that should be related by a constant + // (e.g. 10s) that are across the zero barrier will no longer work. We need to + // round consistently towards positive infinity or negative infinity (we chose + // negative.) This can't be done with a truncation, it must be done with + // floor. + long long clamped = + floor(double(timeAsInt) / resolutionAsInt) * resolutionAsInt; + + long long midpoint = 0; + long long clampedAndJittered = clamped; + if (!unconditionalClamping && + StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter()) { + if (!NS_FAILED(RandomMidpoint(clamped, resolutionAsInt, aContextMixin, + &midpoint)) && + timeAsInt >= clamped + midpoint) { + clampedAndJittered += resolutionAsInt; + } + } + + // Cast it back to a double and reduce it to the correct units. + double ret = double(clampedAndJittered) / (1000000.0 / double(aTimeScale)); + + MOZ_LOG( + gResistFingerprintingLog, LogLevel::Verbose, + ("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding %s with (%lli, " + "Originally %.*f), " + "Intermediate: (%lli), Clamped: (%lli) Jitter: (%i Context: %" PRId64 + " Midpoint: %lli) " + "Final: (%lli Converted: %.*f)", + DBL_DIG - 1, aTime, DBL_DIG - 1, timeScaled, timeAsInt, + (unconditionalClamping ? "unconditionally" : "normally"), + resolutionAsInt, DBL_DIG - 1, aResolutionUSec, + (long long)floor(double(timeAsInt) / resolutionAsInt), clamped, + StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter(), + aContextMixin, midpoint, clampedAndJittered, DBL_DIG - 1, ret)); + + return ret; +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsUSecs(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType) { + const auto type = GetTimerPrecisionType(aRTPCallerType); + return nsRFPService::ReduceTimePrecisionImpl(aTime, MicroSeconds, + TimerResolution(aRTPCallerType), + aContextMixin, type); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsMSecs(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType) { + const auto type = GetTimerPrecisionType(aRTPCallerType); + return nsRFPService::ReduceTimePrecisionImpl(aTime, MilliSeconds, + TimerResolution(aRTPCallerType), + aContextMixin, type); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly( + double aTime, int64_t aContextMixin, RTPCallerType aRTPCallerType) { + return nsRFPService::ReduceTimePrecisionImpl( + aTime, MilliSeconds, TimerResolution(aRTPCallerType), aContextMixin, + GetTimerPrecisionTypeRFPOnly(aRTPCallerType)); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsSecs(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType) { + const auto type = GetTimerPrecisionType(aRTPCallerType); + return nsRFPService::ReduceTimePrecisionImpl( + aTime, Seconds, TimerResolution(aRTPCallerType), aContextMixin, type); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsSecsRFPOnly( + double aTime, int64_t aContextMixin, RTPCallerType aRTPCallerType) { + return nsRFPService::ReduceTimePrecisionImpl( + aTime, Seconds, TimerResolution(aRTPCallerType), aContextMixin, + GetTimerPrecisionTypeRFPOnly(aRTPCallerType)); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsUSecsWrapper( + double aTime, bool aShouldResistFingerprinting, JSContext* aCx) { + MOZ_ASSERT(aCx); + + nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx); + MOZ_ASSERT(global); + + RTPCallerType callerType; + if (aShouldResistFingerprinting) { + callerType = RTPCallerType::ResistFingerprinting; + } else if (global->CrossOriginIsolated()) { + callerType = RTPCallerType::CrossOriginIsolated; + } else { + callerType = RTPCallerType::Normal; + } + return nsRFPService::ReduceTimePrecisionImpl( + aTime, MicroSeconds, TimerResolution(callerType), + 0, /* For absolute timestamps (all the JS engine does), supply zero + context mixin */ + GetTimerPrecisionType(callerType)); +} + +/* static */ +TimerPrecisionType nsRFPService::GetTimerPrecisionType( + RTPCallerType aRTPCallerType) { + if (aRTPCallerType == RTPCallerType::SystemPrincipal) { + return DangerouslyNone; + } + + if (aRTPCallerType == RTPCallerType::ResistFingerprinting) { + return RFP; + } + + if (StaticPrefs::privacy_reduceTimerPrecision() && + aRTPCallerType == RTPCallerType::CrossOriginIsolated) { + return UnconditionalAKAHighRes; + } + + if (StaticPrefs::privacy_reduceTimerPrecision()) { + return Normal; + } + + if (StaticPrefs::privacy_reduceTimerPrecision_unconditional()) { + return UnconditionalAKAHighRes; + } + + return DangerouslyNone; +} + +/* static */ +TimerPrecisionType nsRFPService::GetTimerPrecisionTypeRFPOnly( + RTPCallerType aRTPCallerType) { + if (aRTPCallerType == RTPCallerType::ResistFingerprinting) { + return RFP; + } + + if (StaticPrefs::privacy_reduceTimerPrecision_unconditional() && + aRTPCallerType != RTPCallerType::SystemPrincipal) { + return UnconditionalAKAHighRes; + } + + return DangerouslyNone; +} + +/* static */ +void nsRFPService::TypeToText(TimerPrecisionType aType, nsACString& aText) { + switch (aType) { + case TimerPrecisionType::DangerouslyNone: + aText.AssignLiteral("DangerouslyNone"); + return; + case TimerPrecisionType::Normal: + aText.AssignLiteral("Normal"); + return; + case TimerPrecisionType::RFP: + aText.AssignLiteral("RFP"); + return; + case TimerPrecisionType::UnconditionalAKAHighRes: + aText.AssignLiteral("UnconditionalAKAHighRes"); + return; + default: + MOZ_ASSERT(false, "Shouldn't go here"); + aText.AssignLiteral("Unknown Enum Value"); + return; + } +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Video Statistics Spoofing + +/* static */ +uint32_t nsRFPService::CalculateTargetVideoResolution(uint32_t aVideoQuality) { + return aVideoQuality * NSToIntCeil(aVideoQuality * 16 / 9.0); +} + +/* static */ +uint32_t nsRFPService::GetSpoofedTotalFrames(double aTime) { + double precision = + TimerResolution(RTPCallerType::ResistFingerprinting) / 1000 / 1000; + double time = floor(aTime / precision) * precision; + + return NSToIntFloor(time * kVideoFramesPerSec); +} + +/* static */ +uint32_t nsRFPService::GetSpoofedDroppedFrames(double aTime, uint32_t aWidth, + uint32_t aHeight) { + uint32_t targetRes = CalculateTargetVideoResolution( + StaticPrefs::privacy_resistFingerprinting_target_video_res()); + + // The video resolution is less than or equal to the target resolution, we + // report a zero dropped rate for this case. + if (targetRes >= aWidth * aHeight) { + return 0; + } + + double precision = + TimerResolution(RTPCallerType::ResistFingerprinting) / 1000 / 1000; + double time = floor(aTime / precision) * precision; + // Bound the dropped ratio from 0 to 100. + uint32_t boundedDroppedRatio = std::min(kVideoDroppedRatio, 100U); + + return NSToIntFloor(time * kVideoFramesPerSec * + (boundedDroppedRatio / 100.0)); +} + +/* static */ +uint32_t nsRFPService::GetSpoofedPresentedFrames(double aTime, uint32_t aWidth, + uint32_t aHeight) { + uint32_t targetRes = CalculateTargetVideoResolution( + StaticPrefs::privacy_resistFingerprinting_target_video_res()); + + // The target resolution is greater than the current resolution. For this + // case, there will be no dropped frames, so we report total frames directly. + if (targetRes >= aWidth * aHeight) { + return GetSpoofedTotalFrames(aTime); + } + + double precision = + TimerResolution(RTPCallerType::ResistFingerprinting) / 1000 / 1000; + double time = floor(aTime / precision) * precision; + // Bound the dropped ratio from 0 to 100. + uint32_t boundedDroppedRatio = std::min(kVideoDroppedRatio, 100U); + + return NSToIntFloor(time * kVideoFramesPerSec * + ((100 - boundedDroppedRatio) / 100.0)); +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// User-Agent/Version Stuff + +static const char* GetSpoofedVersion() { +#ifdef ANDROID + // Return Desktop's ESR version. + // When Android RFP returns an ESR version >= 120, we can remove the "rv:109" + // spoofing in GetSpoofedUserAgent() below and stop #including + // StaticPrefs_network.h. + return "115.0"; +#else + return MOZILLA_UAVERSION; +#endif +} + +/* static */ +void nsRFPService::GetSpoofedUserAgent(nsACString& userAgent, + bool isForHTTPHeader) { + // This function generates the spoofed value of User Agent. + // We spoof the values of the platform and Firefox version, which could be + // used as fingerprinting sources to identify individuals. + // Reference of the format of User Agent: + // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorID/userAgent + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent + + // These magic numbers are the lengths of the UA string literals below. + // Assume three-digit Firefox version numbers so we have room to grow. + size_t preallocatedLength = + 13 + + (isForHTTPHeader ? mozilla::ArrayLength(SPOOFED_HTTP_UA_OS) + : mozilla::ArrayLength(SPOOFED_UA_OS)) - + 1 + 5 + 3 + 10 + mozilla::ArrayLength(LEGACY_UA_GECKO_TRAIL) - 1 + 9 + 3 + + 2; + userAgent.SetCapacity(preallocatedLength); + + const char* spoofedVersion = GetSpoofedVersion(); + + // "Mozilla/5.0 (%s; rv:%d.0) Gecko/%d Firefox/%d.0" + userAgent.AssignLiteral("Mozilla/5.0 ("); + + if (isForHTTPHeader) { + userAgent.AppendLiteral(SPOOFED_HTTP_UA_OS); + } else { + userAgent.AppendLiteral(SPOOFED_UA_OS); + } + + userAgent.AppendLiteral("; rv:"); + + // Desktop Firefox (regular and RFP) won't need to spoof "rv:109" in versions + // >= 120 (bug 1806690), but Android RFP will need to continue spoofing 109 + // as long as Android's GetSpoofedVersion() returns a version < 120 above. + uint32_t forceRV = mozilla::StaticPrefs::network_http_useragent_forceRVOnly(); + if (forceRV) { + userAgent.Append(nsPrintfCString("%u.0", forceRV)); + } else { + userAgent.Append(spoofedVersion); + } + + userAgent.AppendLiteral(") Gecko/"); + +#if defined(ANDROID) + userAgent.Append(spoofedVersion); +#else + userAgent.AppendLiteral(LEGACY_UA_GECKO_TRAIL); +#endif + + userAgent.AppendLiteral(" Firefox/"); + userAgent.Append(spoofedVersion); + + MOZ_ASSERT(userAgent.Length() <= preallocatedLength); +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Keyboard Spoofing Stuff + +nsTHashMap<KeyboardHashKey, const SpoofingKeyboardCode*>* + nsRFPService::sSpoofingKeyboardCodes = nullptr; + +KeyboardHashKey::KeyboardHashKey(const KeyboardLangs aLang, + const KeyboardRegions aRegion, + const KeyNameIndexType aKeyIdx, + const nsAString& aKey) + : mLang(aLang), mRegion(aRegion), mKeyIdx(aKeyIdx), mKey(aKey) {} + +KeyboardHashKey::KeyboardHashKey(KeyTypePointer aOther) + : mLang(aOther->mLang), + mRegion(aOther->mRegion), + mKeyIdx(aOther->mKeyIdx), + mKey(aOther->mKey) {} + +KeyboardHashKey::KeyboardHashKey(KeyboardHashKey&& aOther) noexcept + : PLDHashEntryHdr(std::move(aOther)), + mLang(std::move(aOther.mLang)), + mRegion(std::move(aOther.mRegion)), + mKeyIdx(std::move(aOther.mKeyIdx)), + mKey(std::move(aOther.mKey)) {} + +KeyboardHashKey::~KeyboardHashKey() = default; + +bool KeyboardHashKey::KeyEquals(KeyTypePointer aOther) const { + return mLang == aOther->mLang && mRegion == aOther->mRegion && + mKeyIdx == aOther->mKeyIdx && mKey == aOther->mKey; +} + +KeyboardHashKey::KeyTypePointer KeyboardHashKey::KeyToPointer(KeyType aKey) { + return &aKey; +} + +PLDHashNumber KeyboardHashKey::HashKey(KeyTypePointer aKey) { + PLDHashNumber hash = mozilla::HashString(aKey->mKey); + return mozilla::AddToHash(hash, aKey->mRegion, aKey->mKeyIdx, aKey->mLang); +} + +/* static */ +void nsRFPService::MaybeCreateSpoofingKeyCodes(const KeyboardLangs aLang, + const KeyboardRegions aRegion) { + if (sSpoofingKeyboardCodes == nullptr) { + sSpoofingKeyboardCodes = + new nsTHashMap<KeyboardHashKey, const SpoofingKeyboardCode*>(); + } + + if (KeyboardLang::EN == aLang) { + switch (aRegion) { + case KeyboardRegion::US: + MaybeCreateSpoofingKeyCodesForEnUS(); + break; + } + } +} + +/* static */ +void nsRFPService::MaybeCreateSpoofingKeyCodesForEnUS() { + MOZ_ASSERT(sSpoofingKeyboardCodes); + + static bool sInitialized = false; + const KeyboardLangs lang = KeyboardLang::EN; + const KeyboardRegions reg = KeyboardRegion::US; + + if (sInitialized) { + return; + } + + static const SpoofingKeyboardInfo spoofingKeyboardInfoTable[] = { +#define KEY(key_, _codeNameIdx, _keyCode, _modifier) \ + {NS_LITERAL_STRING_FROM_CSTRING(key_), \ + KEY_NAME_INDEX_USE_STRING, \ + {CODE_NAME_INDEX_##_codeNameIdx, _keyCode, _modifier}}, +#define CONTROL(keyNameIdx_, _codeNameIdx, _keyCode) \ + {u""_ns, \ + KEY_NAME_INDEX_##keyNameIdx_, \ + {CODE_NAME_INDEX_##_codeNameIdx, _keyCode, MODIFIER_NONE}}, +#include "KeyCodeConsensus_En_US.h" +#undef CONTROL +#undef KEY + }; + + for (const auto& keyboardInfo : spoofingKeyboardInfoTable) { + KeyboardHashKey key(lang, reg, keyboardInfo.mKeyIdx, keyboardInfo.mKey); + MOZ_ASSERT(!sSpoofingKeyboardCodes->Contains(key), + "Double-defining key code; fix your KeyCodeConsensus file"); + sSpoofingKeyboardCodes->InsertOrUpdate(key, &keyboardInfo.mSpoofingCode); + } + + sInitialized = true; +} + +/* static */ +void nsRFPService::GetKeyboardLangAndRegion(const nsAString& aLanguage, + KeyboardLangs& aLocale, + KeyboardRegions& aRegion) { + nsAutoString langStr; + nsAutoString regionStr; + uint32_t partNum = 0; + + for (const nsAString& part : aLanguage.Split('-')) { + if (partNum == 0) { + langStr = part; + } else { + regionStr = part; + break; + } + + partNum++; + } + + // We test each language here as well as the region. There are some cases that + // only the language is given, we will use the default region code when this + // happens. The default region should depend on the given language. + if (langStr.EqualsLiteral(RFP_KEYBOARD_LANG_STRING_EN)) { + aLocale = KeyboardLang::EN; + // Give default values first. + aRegion = KeyboardRegion::US; + + if (regionStr.EqualsLiteral(RFP_KEYBOARD_REGION_STRING_US)) { + aRegion = KeyboardRegion::US; + } + } else { + // There is no spoofed keyboard locale for the given language. We use the + // default one in this case. + aLocale = RFP_DEFAULT_SPOOFING_KEYBOARD_LANG; + aRegion = RFP_DEFAULT_SPOOFING_KEYBOARD_REGION; + } +} + +/* static */ +bool nsRFPService::GetSpoofedKeyCodeInfo( + const dom::Document* aDoc, const WidgetKeyboardEvent* aKeyboardEvent, + SpoofingKeyboardCode& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + KeyboardLangs keyboardLang = RFP_DEFAULT_SPOOFING_KEYBOARD_LANG; + KeyboardRegions keyboardRegion = RFP_DEFAULT_SPOOFING_KEYBOARD_REGION; + // If the document is given, we use the content language which is get from the + // document. Otherwise, we use the default one. + if (aDoc != nullptr) { + nsAutoString language; + aDoc->GetContentLanguage(language); + + // If the content-langauge is not given, we try to get langauge from the + // HTML lang attribute. + if (language.IsEmpty()) { + dom::Element* elm = aDoc->GetHtmlElement(); + + if (elm != nullptr) { + elm->GetLang(language); + } + } + + // If two or more languages are given, per HTML5 spec, we should consider + // it as 'unknown'. So we use the default one. + if (!language.IsEmpty() && !language.Contains(char16_t(','))) { + language.StripWhitespace(); + GetKeyboardLangAndRegion(language, keyboardLang, keyboardRegion); + } + } + + MaybeCreateSpoofingKeyCodes(keyboardLang, keyboardRegion); + + KeyNameIndex keyIdx = aKeyboardEvent->mKeyNameIndex; + nsAutoString keyName; + + if (keyIdx == KEY_NAME_INDEX_USE_STRING) { + keyName = aKeyboardEvent->mKeyValue; + } + + KeyboardHashKey key(keyboardLang, keyboardRegion, keyIdx, keyName); + const SpoofingKeyboardCode* keyboardCode = sSpoofingKeyboardCodes->Get(key); + + if (keyboardCode != nullptr) { + aOut = *keyboardCode; + return true; + } + + return false; +} + +/* static */ +bool nsRFPService::GetSpoofedModifierStates( + const dom::Document* aDoc, const WidgetKeyboardEvent* aKeyboardEvent, + const Modifiers aModifier, bool& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + // For modifier or control keys, we don't need to hide its modifier states. + if (aKeyboardEvent->mKeyNameIndex != KEY_NAME_INDEX_USE_STRING) { + return false; + } + + // We will spoof the modifer state for Alt, Shift, and AltGraph. + // We don't spoof the Control key, because it is often used + // for command key combinations in web apps. + if ((aModifier & (MODIFIER_ALT | MODIFIER_SHIFT | MODIFIER_ALTGRAPH)) != 0) { + SpoofingKeyboardCode keyCodeInfo; + + if (GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) { + aOut = ((keyCodeInfo.mModifierStates & aModifier) != 0); + return true; + } + } + + return false; +} + +/* static */ +bool nsRFPService::GetSpoofedCode(const dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + nsAString& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + SpoofingKeyboardCode keyCodeInfo; + + if (!GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) { + return false; + } + + WidgetKeyboardEvent::GetDOMCodeName(keyCodeInfo.mCode, aOut); + + // We need to change the 'Left' with 'Right' if the location indicates + // it's a right key. + if (aKeyboardEvent->mLocation == + dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_RIGHT && + StringEndsWith(aOut, u"Left"_ns)) { + aOut.ReplaceLiteral(aOut.Length() - 4, 4, u"Right"); + } + + return true; +} + +/* static */ +bool nsRFPService::GetSpoofedKeyCode(const dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + SpoofingKeyboardCode keyCodeInfo; + + if (GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) { + aOut = keyCodeInfo.mKeyCode; + return true; + } + + return false; +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Randomization Stuff +nsresult nsRFPService::EnsureSessionKey(bool aIsPrivate) { + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Info, + ("Ensure the session key for %s browsing session\n", + aIsPrivate ? "private" : "normal")); + + if (!StaticPrefs::privacy_resistFingerprinting_randomization_enabled()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Maybe<nsID>& sessionKey = + aIsPrivate ? mPrivateBrowsingSessionKey : mBrowsingSessionKey; + + // The key has been generated, bail out earlier. + if (sessionKey) { + MOZ_LOG( + gResistFingerprintingLog, LogLevel::Info, + ("The %s session key exists: %s\n", aIsPrivate ? "private" : "normal", + sessionKey.ref().ToString().get())); + return NS_OK; + } + + sessionKey.emplace(nsID::GenerateUUID()); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug, + ("Generated %s session key: %s\n", aIsPrivate ? "private" : "normal", + sessionKey.ref().ToString().get())); + + return NS_OK; +} + +void nsRFPService::ClearSessionKey(bool aIsPrivate) { + MOZ_ASSERT(XRE_IsParentProcess()); + + Maybe<nsID>& sessionKey = + aIsPrivate ? mPrivateBrowsingSessionKey : mBrowsingSessionKey; + + sessionKey.reset(); +} + +// static +Maybe<nsTArray<uint8_t>> nsRFPService::GenerateKey(nsIURI* aTopLevelURI, + bool aIsPrivate) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aTopLevelURI); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug, + ("Generating %s randomization key for top-level URI: %s\n", + aIsPrivate ? "private" : "normal", + aTopLevelURI->GetSpecOrDefault().get())); + + RefPtr<nsRFPService> service = GetOrCreate(); + + if (NS_FAILED(service->EnsureSessionKey(aIsPrivate))) { + return Nothing(); + } + + // Return nothing if fingerprinting resistance is disabled or fingerprinting + // resistance is exempted from the normal windows. Note that we still need to + // generate the key for exempted domains because there could be unexempted + // sub-documents that need the key. + if (!nsContentUtils::ShouldResistFingerprinting( + "Coarse Efficiency Check", RFPTarget::CanvasRandomization) || + (!aIsPrivate && + StaticPrefs::privacy_resistFingerprinting_testGranularityMask() & + 0x02 /* NonPBMExemptMask */)) { + return Nothing(); + } + + const nsID& sessionKey = aIsPrivate + ? service->mPrivateBrowsingSessionKey.ref() + : service->mBrowsingSessionKey.ref(); + + auto sessionKeyStr = sessionKey.ToString(); + + // Using the OriginAttributes to get the site from the top-level URI. The site + // is composed of scheme, host, and port. + OriginAttributes attrs; + attrs.SetPartitionKey(aTopLevelURI); + + // Generate the key by using the hMAC. The key is based on the session key and + // the partitionKey, i.e. top-level site. + HMAC hmac; + + nsresult rv = hmac.Begin( + SEC_OID_SHA256, + Span(reinterpret_cast<const uint8_t*>(sessionKeyStr.get()), NSID_LENGTH)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + NS_ConvertUTF16toUTF8 topLevelSite(attrs.mPartitionKey); + rv = hmac.Update(reinterpret_cast<const uint8_t*>(topLevelSite.get()), + topLevelSite.Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + Maybe<nsTArray<uint8_t>> key; + key.emplace(); + + rv = hmac.End(key.ref()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + return key; +} + +// static +nsresult nsRFPService::GenerateCanvasKeyFromImageData( + nsICookieJarSettings* aCookieJarSettings, uint8_t* aImageData, + uint32_t aSize, nsTArray<uint8_t>& aCanvasKey) { + NS_ENSURE_ARG_POINTER(aCookieJarSettings); + + nsTArray<uint8_t> randomKey; + nsresult rv = + aCookieJarSettings->GetFingerprintingRandomizationKey(randomKey); + + // There is no random key for this cookieJarSettings. This means that the + // randomization is disabled. So, we can bail out from here without doing + // anything. + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + // Generate the key for randomizing the canvas data using hMAC. The key is + // based on the random key of the document and the canvas data itself. So, + // different canvas would have different keys. + HMAC hmac; + + rv = hmac.Begin(SEC_OID_SHA256, Span(randomKey)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hmac.Update(aImageData, aSize); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hmac.End(aCanvasKey); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +nsresult nsRFPService::RandomizePixels(nsICookieJarSettings* aCookieJarSettings, + uint8_t* aData, uint32_t aSize, + gfx::SurfaceFormat aSurfaceFormat) { + NS_ENSURE_ARG_POINTER(aData); + + if (!aCookieJarSettings) { + return NS_OK; + } + + if (aSize == 0) { + return NS_OK; + } + + nsTArray<uint8_t> canvasKey; + nsresult rv = GenerateCanvasKeyFromImageData(aCookieJarSettings, aData, aSize, + canvasKey); + NS_ENSURE_SUCCESS(rv, rv); + + // Calculate the number of pixels based on the given data size. One pixel uses + // 4 bytes that contains ARGB information. + uint32_t pixelCnt = aSize / 4; + + // Generate random values that will decide the RGB channel and the pixel + // position that we are going to introduce the noises. The channel and + // position are predictable to ensure we have a consistant result with the + // same canvas in the same browsing session. + + // Seed and create the first random number generator which will be used to + // select RGB channel and the pixel position. The seed is the first half of + // the canvas key. + non_crypto::XorShift128PlusRNG rng1( + *reinterpret_cast<uint64_t*>(canvasKey.Elements()), + *reinterpret_cast<uint64_t*>(canvasKey.Elements() + 8)); + + // Use the last 8 bits as the number of noises. + uint8_t rnd3 = canvasKey.LastElement(); + + // Clear the last 8 bits. + canvasKey.ReplaceElementAt(canvasKey.Length() - 1, 0); + + // Use the remaining 120 bits to seed and create the second random number + // generator. The random number will be used to decided the noise bit that + // will be added to the lowest order bit of the channel of the pixel. + non_crypto::XorShift128PlusRNG rng2( + *reinterpret_cast<uint64_t*>(canvasKey.Elements() + 16), + *reinterpret_cast<uint64_t*>(canvasKey.Elements() + 24)); + + // Ensure at least 16 random changes may occur. + uint8_t numNoises = std::clamp<uint8_t>(rnd3, 15, 255); + + for (uint8_t i = 0; i <= numNoises; i++) { + // Choose which RGB channel to add a noise. The pixel data is in either + // the BGRA or the ARGB format depending on the endianess. To choose the + // color channel we need to add the offset according the endianess. + uint32_t channel; + if (aSurfaceFormat == gfx::SurfaceFormat::B8G8R8A8) { + channel = rng1.next() % 3; + } else if (aSurfaceFormat == gfx::SurfaceFormat::A8R8G8B8) { + channel = rng1.next() % 3 + 1; + } else { + return NS_ERROR_INVALID_ARG; + } + + uint32_t idx = 4 * (rng1.next() % pixelCnt) + channel; + uint8_t bit = rng2.next(); + + aData[idx] = aData[idx] ^ (bit & 0x1); + } + + return NS_OK; +} diff --git a/toolkit/components/resistfingerprinting/nsRFPService.h b/toolkit/components/resistfingerprinting/nsRFPService.h new file mode 100644 index 0000000000..abb84af116 --- /dev/null +++ b/toolkit/components/resistfingerprinting/nsRFPService.h @@ -0,0 +1,349 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef __nsRFPService_h__ +#define __nsRFPService_h__ + +#include <cstdint> +#include "ErrorList.h" +#include "PLDHashTable.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/gfx/Types.h" +#include "nsHashtablesFwd.h" +#include "nsICookieJarSettings.h" +#include "nsIObserver.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +// Defines regarding spoofed values of Navigator object. These spoofed values +// are returned when 'privacy.resistFingerprinting' is true. +// We decided to give different spoofed values according to the platform. The +// reason is that it is easy to detect the real platform. So there is no benefit +// for hiding the platform: it only brings breakages, like keyboard shortcuts +// won't work in macOS if we spoof it as a Windows platform. +#ifdef XP_WIN +# define SPOOFED_UA_OS "Windows NT 10.0; Win64; x64" +# define SPOOFED_APPVERSION "5.0 (Windows)" +# define SPOOFED_OSCPU "Windows NT 10.0; Win64; x64" +# define SPOOFED_PLATFORM "Win32" +#elif defined(XP_MACOSX) +# define SPOOFED_UA_OS "Macintosh; Intel Mac OS X 10.15" +# define SPOOFED_APPVERSION "5.0 (Macintosh)" +# define SPOOFED_OSCPU "Intel Mac OS X 10.15" +# define SPOOFED_PLATFORM "MacIntel" +#elif defined(MOZ_WIDGET_ANDROID) +# define SPOOFED_UA_OS "Android 10; Mobile" +# define SPOOFED_APPVERSION "5.0 (Android 10)" +# define SPOOFED_OSCPU "Linux aarch64" +# define SPOOFED_PLATFORM "Linux aarch64" +#else +// For Linux and other platforms, like BSDs, SunOS and etc, we will use Linux +// platform. +# define SPOOFED_UA_OS "X11; Linux x86_64" +# define SPOOFED_APPVERSION "5.0 (X11)" +# define SPOOFED_OSCPU "Linux x86_64" +# define SPOOFED_PLATFORM "Linux x86_64" +#endif + +#define SPOOFED_APPNAME "Netscape" +#define LEGACY_BUILD_ID "20181001000000" +#define LEGACY_UA_GECKO_TRAIL "20100101" + +#define SPOOFED_POINTER_INTERFACE MouseEvent_Binding::MOZ_SOURCE_MOUSE + +// For the HTTP User-Agent header, we use a simpler set of spoofed values +// that do not reveal the specific desktop platform. +#if defined(MOZ_WIDGET_ANDROID) +# define SPOOFED_HTTP_UA_OS "Android 10; Mobile" +#else +# define SPOOFED_HTTP_UA_OS "Windows NT 10.0" +#endif + +struct JSContext; + +namespace mozilla { +class WidgetKeyboardEvent; +namespace dom { +class Document; +} + +enum KeyboardLang { EN = 0x01 }; + +#define RFP_KEYBOARD_LANG_STRING_EN "en" + +typedef uint8_t KeyboardLangs; + +enum KeyboardRegion { US = 0x01 }; + +#define RFP_KEYBOARD_REGION_STRING_US "US" + +typedef uint8_t KeyboardRegions; + +// This struct has the information about how to spoof the keyboardEvent.code, +// keyboardEvent.keycode and modifier states. +struct SpoofingKeyboardCode { + CodeNameIndex mCode; + uint8_t mKeyCode; + Modifiers mModifierStates; +}; + +struct SpoofingKeyboardInfo { + nsString mKey; + KeyNameIndex mKeyIdx; + SpoofingKeyboardCode mSpoofingCode; +}; + +class KeyboardHashKey : public PLDHashEntryHdr { + public: + typedef const KeyboardHashKey& KeyType; + typedef const KeyboardHashKey* KeyTypePointer; + + KeyboardHashKey(const KeyboardLangs aLang, const KeyboardRegions aRegion, + const KeyNameIndexType aKeyIdx, const nsAString& aKey); + + explicit KeyboardHashKey(KeyTypePointer aOther); + + KeyboardHashKey(KeyboardHashKey&& aOther) noexcept; + + ~KeyboardHashKey(); + + bool KeyEquals(KeyTypePointer aOther) const; + + static KeyTypePointer KeyToPointer(KeyType aKey); + + static PLDHashNumber HashKey(KeyTypePointer aKey); + + enum { ALLOW_MEMMOVE = true }; + + KeyboardLangs mLang; + KeyboardRegions mRegion; + KeyNameIndexType mKeyIdx; + nsString mKey; +}; + +// ============================================================================ + +// Reduce Timer Precision (RTP) Caller Type +enum class RTPCallerType : uint8_t { + Normal = 0, + SystemPrincipal = (1 << 0), + ResistFingerprinting = (1 << 1), + CrossOriginIsolated = (1 << 2) +}; + +enum TimerPrecisionType { + DangerouslyNone = 1, + UnconditionalAKAHighRes = 2, + Normal = 3, + RFP = 4, +}; + +// ============================================================================ + +// NOLINTNEXTLINE(bugprone-macro-parentheses) +#define ITEM_VALUE(name, val) name = val, + +enum class RFPTarget : uint32_t { +#include "RFPTargets.inc" +}; + +#undef ITEM_VALUE + +// ============================================================================ + +class nsRFPService final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static nsRFPService* GetOrCreate(); + + static bool IsRFPEnabledFor(RFPTarget aTarget); + + // -------------------------------------------------------------------------- + static double TimerResolution(RTPCallerType aRTPCallerType); + + enum TimeScale { Seconds = 1, MilliSeconds = 1000, MicroSeconds = 1000000 }; + + // The following Reduce methods can be called off main thread. + static double ReduceTimePrecisionAsUSecs(double aTime, int64_t aContextMixin, + RTPCallerType aRTPCallerType); + static double ReduceTimePrecisionAsMSecs(double aTime, int64_t aContextMixin, + RTPCallerType aRTPCallerType); + static double ReduceTimePrecisionAsMSecsRFPOnly(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType); + static double ReduceTimePrecisionAsSecs(double aTime, int64_t aContextMixin, + RTPCallerType aRTPCallerType); + static double ReduceTimePrecisionAsSecsRFPOnly(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType); + + // Used by the JS Engine, as it doesn't know about the TimerPrecisionType enum + static double ReduceTimePrecisionAsUSecsWrapper( + double aTime, bool aShouldResistFingerprinting, JSContext* aCx); + + // Public only for testing purposes + static double ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale, + double aResolutionUSec, + int64_t aContextMixin, + TimerPrecisionType aType); + static nsresult RandomMidpoint(long long aClampedTimeUSec, + long long aResolutionUSec, + int64_t aContextMixin, long long* aMidpointOut, + uint8_t* aSecretSeed = nullptr); + + // -------------------------------------------------------------------------- + + // This method calculates the video resolution (i.e. height x width) based + // on the video quality (480p, 720p, etc). + static uint32_t CalculateTargetVideoResolution(uint32_t aVideoQuality); + + // Methods for getting spoofed media statistics and the return value will + // depend on the video resolution. + static uint32_t GetSpoofedTotalFrames(double aTime); + static uint32_t GetSpoofedDroppedFrames(double aTime, uint32_t aWidth, + uint32_t aHeight); + static uint32_t GetSpoofedPresentedFrames(double aTime, uint32_t aWidth, + uint32_t aHeight); + + // -------------------------------------------------------------------------- + + // This method generates the spoofed value of User Agent. + static void GetSpoofedUserAgent(nsACString& userAgent, bool isForHTTPHeader); + + // -------------------------------------------------------------------------- + + /** + * This method for getting spoofed modifier states for the given keyboard + * event. + * + * @param aDoc [in] the owner's document for getting content + * language. + * @param aKeyboardEvent [in] the keyboard event that needs to be spoofed. + * @param aModifier [in] the modifier that needs to be spoofed. + * @param aOut [out] the spoofed state for the given modifier. + * @return true if there is a spoofed state for the modifier. + */ + static bool GetSpoofedModifierStates( + const mozilla::dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, const Modifiers aModifier, + bool& aOut); + + /** + * This method for getting spoofed code for the given keyboard event. + * + * @param aDoc [in] the owner's document for getting content + * language. + * @param aKeyboardEvent [in] the keyboard event that needs to be spoofed. + * @param aOut [out] the spoofed code. + * @return true if there is a spoofed code in the fake keyboard + * layout. + */ + static bool GetSpoofedCode(const dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + nsAString& aOut); + + /** + * This method for getting spoofed keyCode for the given keyboard event. + * + * @param aDoc [in] the owner's document for getting content + * language. + * @param aKeyboardEvent [in] the keyboard event that needs to be spoofed. + * @param aOut [out] the spoofed keyCode. + * @return true if there is a spoofed keyCode in the fake + * keyboard layout. + */ + static bool GetSpoofedKeyCode(const mozilla::dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t& aOut); + + // -------------------------------------------------------------------------- + + // The method to generate the key for randomization. It can return nothing if + // the session key is not available due to the randomization is disabled. + static Maybe<nsTArray<uint8_t>> GenerateKey(nsIURI* aTopLevelURI, + bool aIsPrivate); + + // The method to add random noises to the image data based on the random key + // of the given cookieJarSettings. + static nsresult RandomizePixels(nsICookieJarSettings* aCookieJarSettings, + uint8_t* aData, uint32_t aSize, + mozilla::gfx::SurfaceFormat aSurfaceFormat); + + // -------------------------------------------------------------------------- + + private: + nsresult Init(); + + nsRFPService() = default; + + ~nsRFPService() = default; + + nsCString mInitialTZValue; + + void UpdateRFPPref(); + void UpdateFPPOverrideList(); + void StartShutdown(); + + void PrefChanged(const char* aPref); + static void PrefChanged(const char* aPref, void* aSelf); + + static Maybe<RFPTarget> TextToRFPTarget(const nsAString& aText); + + // -------------------------------------------------------------------------- + + static void MaybeCreateSpoofingKeyCodes(const KeyboardLangs aLang, + const KeyboardRegions aRegion); + static void MaybeCreateSpoofingKeyCodesForEnUS(); + + static void GetKeyboardLangAndRegion(const nsAString& aLanguage, + KeyboardLangs& aLocale, + KeyboardRegions& aRegion); + static bool GetSpoofedKeyCodeInfo(const mozilla::dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + SpoofingKeyboardCode& aOut); + + static nsTHashMap<KeyboardHashKey, const SpoofingKeyboardCode*>* + sSpoofingKeyboardCodes; + + // -------------------------------------------------------------------------- + + static TimerPrecisionType GetTimerPrecisionType(RTPCallerType aRTPCallerType); + + static TimerPrecisionType GetTimerPrecisionTypeRFPOnly( + RTPCallerType aRTPCallerType); + + static void TypeToText(TimerPrecisionType aType, nsACString& aText); + + // -------------------------------------------------------------------------- + + // A helper function to generate canvas key from the given image data and + // randomization key. + static nsresult GenerateCanvasKeyFromImageData( + nsICookieJarSettings* aCookieJarSettings, uint8_t* aImageData, + uint32_t aSize, nsTArray<uint8_t>& aCanvasKey); + + // Generate the session key if it hasn't been generated. + nsresult EnsureSessionKey(bool aIsPrivate); + void ClearSessionKey(bool aIsPrivate); + + // The keys that represent the browsing session. The lifetime of the key ties + // to the browsing session. For normal windows, the key is generated when + // loading the first http channel after the browser startup and persists until + // the browser shuts down. For private windows, the key is generated when + // opening a http channel on a private window and reset after all private + // windows close, i.e. private browsing session ends. + // + // The key will be used to generate the randomization noise used to fiddle the + // browser fingerprints. Note that this key lives and can only be accessed in + // the parent process. + Maybe<nsID> mBrowsingSessionKey; + Maybe<nsID> mPrivateBrowsingSessionKey; +}; + +} // namespace mozilla + +#endif /* __nsRFPService_h__ */ diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.ini b/toolkit/components/resistfingerprinting/tests/browser/browser.ini new file mode 100644 index 0000000000..3a3aa2ccc6 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = + empty.html + head.js + testPage.html + worker.js + +[browser_canvas_randomization.js] +[browser_canvas_randomization_worker.js] +[browser_fingerprinting_randomization_key.js] diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js new file mode 100644 index 0000000000..0935beecba --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js @@ -0,0 +1,589 @@ +/* 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/. */ + +/** + * Bug 1816189 - Testing canvas randomization on canvas data extraction. + * + * In the test, we create canvas elements and offscreen canvas and test if + * the extracted canvas data is altered because of the canvas randomization. + */ + +const emptyPage = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "empty.html"; + +var TEST_CASES = [ + { + name: "CanvasRenderingContext2D.getImageData().", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], _ => { + const canvas = content.document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + + const context = canvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + const imageData = context.getImageData(0, 0, 100, 100); + + // Access the data again. + const imageDataSecond = context.getImageData(0, 0, 100, 100); + + return [imageData.data, imageDataSecond.data]; + }); + }, + isDataRandomized(data1, data2, isCompareOriginal) { + let diffCnt = compareUint8Arrays(data1, data2); + info(`There are ${diffCnt} bits are different.`); + + // The Canvas randomization adds at most 512 bits noise to the image data. + // We compare the image data arrays to see if they are different and the + // difference is within the range. + + // If we are compare two randomized arrays, the difference can be doubled. + let expected = isCompareOriginal + ? NUM_RANDOMIZED_CANVAS_BITS + : NUM_RANDOMIZED_CANVAS_BITS * 2; + + // The number of difference bits should never bigger than the expected + // number. It could be zero if the randomization is disabled. + ok(diffCnt <= expected, "The number of noise bits is expected."); + + return diffCnt <= expected && diffCnt > 0; + }, + }, + { + name: "HTMLCanvasElement.toDataURL() with a 2d context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], _ => { + const canvas = content.document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + + const context = canvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + const dataURL = canvas.toDataURL(); + + // Access the data again. + const dataURLSecond = canvas.toDataURL(); + + return [dataURL, dataURLSecond]; + }); + }, + isDataRandomized(data1, data2) { + return data1 !== data2; + }, + }, + { + name: "HTMLCanvasElement.toDataURL() with a webgl context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], _ => { + const canvas = content.document.createElement("canvas"); + + const context = canvas.getContext("webgl"); + + // Draw a blue rectangle + context.enable(context.SCISSOR_TEST); + context.scissor(0, 150, 150, 150); + context.clearColor(1, 0, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(150, 150, 300, 150); + context.clearColor(0, 1, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(0, 0, 150, 150); + context.clearColor(0, 0, 1, 1); + context.clear(context.COLOR_BUFFER_BIT); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + const dataURL = canvas.toDataURL(); + + // Access the data again. + const dataURLSecond = canvas.toDataURL(); + + return [dataURL, dataURLSecond]; + }); + }, + isDataRandomized(data1, data2) { + return data1 !== data2; + }, + }, + { + name: "HTMLCanvasElement.toDataURL() with a bitmaprenderer context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + const canvas = content.document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + + const context = canvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + const bitmapCanvas = content.document.createElement("canvas"); + bitmapCanvas.width = 100; + bitmapCanvas.heigh = 100; + content.document.body.appendChild(bitmapCanvas); + + let bitmap = await content.createImageBitmap(canvas); + const bitmapContext = bitmapCanvas.getContext("bitmaprenderer"); + bitmapContext.transferFromImageBitmap(bitmap); + + const dataURL = bitmapCanvas.toDataURL(); + + // Access the data again. + const dataURLSecond = bitmapCanvas.toDataURL(); + + return [dataURL, dataURLSecond]; + }); + }, + isDataRandomized(data1, data2) { + return data1 !== data2; + }, + }, + { + name: "HTMLCanvasElement.toBlob() with a 2d context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + const canvas = content.document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + + const context = canvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + let data = await new content.Promise(resolve => { + canvas.toBlob(blob => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + }); + + // Access the data again. + let dataSecond = await new content.Promise(resolve => { + canvas.toBlob(blob => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + }); + + return [data, dataSecond]; + }); + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "HTMLCanvasElement.toBlob() with a webgl context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + const canvas = content.document.createElement("canvas"); + + const context = canvas.getContext("webgl"); + + // Draw a blue rectangle + context.enable(context.SCISSOR_TEST); + context.scissor(0, 150, 150, 150); + context.clearColor(1, 0, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(150, 150, 300, 150); + context.clearColor(0, 1, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(0, 0, 150, 150); + context.clearColor(0, 0, 1, 1); + context.clear(context.COLOR_BUFFER_BIT); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + let data = await new content.Promise(resolve => { + canvas.toBlob(blob => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + }); + + // We don't get the consistent blob data on second access with webgl + // context regardless of the canvas randomization. So, we report the + // same data here to not fail the test. Ideally, we should look into + // why this happens, but it's not caused by canvas randomization. + + return [data, data]; + }); + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "HTMLCanvasElement.toBlob() with a bitmaprenderer context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + const canvas = content.document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + + const context = canvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + // Add the canvas element to the document + content.document.body.appendChild(canvas); + + const bitmapCanvas = content.document.createElement("canvas"); + bitmapCanvas.width = 100; + bitmapCanvas.heigh = 100; + content.document.body.appendChild(bitmapCanvas); + + let bitmap = await content.createImageBitmap(canvas); + const bitmapContext = bitmapCanvas.getContext("bitmaprenderer"); + bitmapContext.transferFromImageBitmap(bitmap); + + let data = await new content.Promise(resolve => { + bitmapCanvas.toBlob(blob => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + }); + + // Access the data again. + let dataSecond = await new content.Promise(resolve => { + bitmapCanvas.toBlob(blob => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + }); + + return [data, dataSecond]; + }); + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "OffscreenCanvas.convertToBlob() with a 2d context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + let offscreenCanvas = new content.OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + let blob = await offscreenCanvas.convertToBlob(); + + let data = await new content.Promise(resolve => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + + // Access the data again. + let blobSecond = await offscreenCanvas.convertToBlob(); + + let dataSecond = await new content.Promise(resolve => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blobSecond); + }); + + return [data, dataSecond]; + }); + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "OffscreenCanvas.convertToBlob() with a webgl context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + let offscreenCanvas = new content.OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("webgl"); + + // Draw a blue rectangle + context.enable(context.SCISSOR_TEST); + context.scissor(0, 150, 150, 150); + context.clearColor(1, 0, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(150, 150, 300, 150); + context.clearColor(0, 1, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(0, 0, 150, 150); + context.clearColor(0, 0, 1, 1); + context.clear(context.COLOR_BUFFER_BIT); + + let blob = await offscreenCanvas.convertToBlob(); + + let data = await new content.Promise(resolve => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + + // Access the data again. + let blobSecond = await offscreenCanvas.convertToBlob(); + + let dataSecond = await new content.Promise(resolve => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blobSecond); + }); + + return [data, dataSecond]; + }); + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + let offscreenCanvas = new content.OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + const bitmapCanvas = new content.OffscreenCanvas(100, 100); + + let bitmap = await content.createImageBitmap(offscreenCanvas); + const bitmapContext = bitmapCanvas.getContext("bitmaprenderer"); + bitmapContext.transferFromImageBitmap(bitmap); + + let blob = await bitmapCanvas.convertToBlob(); + + let data = await new content.Promise(resolve => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + + // Access the data again. + let blobSecond = await bitmapCanvas.convertToBlob(); + + let dataSecond = await new content.Promise(resolve => { + let fileReader = new content.FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blobSecond); + }); + + return [data, dataSecond]; + }); + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas", + extractCanvasData(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + let offscreenCanvas = new content.OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + const imageData = context.getImageData(0, 0, 100, 100); + + // Access the data again. + const imageDataSecond = context.getImageData(0, 0, 100, 100); + + return [imageData.data, imageDataSecond.data]; + }); + }, + isDataRandomized(data1, data2, isCompareOriginal) { + let diffCnt = compareUint8Arrays(data1, data2); + info(`There are ${diffCnt} bits are different.`); + + // The Canvas randomization adds at most 512 bits noise to the image data. + // We compare the image data arrays to see if they are different and the + // difference is within the range. + + // If we are compare two randomized arrays, the difference can be doubled. + let expected = isCompareOriginal + ? NUM_RANDOMIZED_CANVAS_BITS + : NUM_RANDOMIZED_CANVAS_BITS * 2; + + // The number of difference bits should never bigger than the expected + // number. It could be zero if the randomization is disabled. + ok(diffCnt <= expected, "The number of noise bits is expected."); + + return diffCnt <= expected && diffCnt > 0; + }, + }, +]; + +async function runTest(enabled) { + // Enable/Disable CanvasRandomization by the RFP target overrides. + let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization"; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", true], + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.pbmode", true], + ["privacy.fingerprintingProtection.overrides", RFPOverrides], + ["privacy.resistFingerprinting", false], + ], + }); + + // Open a private window. + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open tabs in the normal and private window. + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage); + + const privateTab = await BrowserTestUtils.openNewForegroundTab( + privateWindow.gBrowser, + emptyPage + ); + + for (let test of TEST_CASES) { + info(`Testing ${test.name}`); + let data = await test.extractCanvasData(tab.linkedBrowser); + let result = test.isDataRandomized(data[0], test.originalData); + + is( + result, + enabled, + `The image data is ${enabled ? "randomized" : "the same"}.` + ); + + ok( + !test.isDataRandomized(data[0], data[1]), + "The data of first and second access should be the same." + ); + + let privateData = await test.extractCanvasData(privateTab.linkedBrowser); + + // Check if we add noise to canvas data in private windows. + result = test.isDataRandomized(privateData[0], test.originalData, true); + is( + result, + enabled, + `The private image data is ${enabled ? "randomized" : "the same"}.` + ); + + ok( + !test.isDataRandomized(privateData[0], privateData[1]), + "The data of first and second access should be the same for private windows." + ); + + // Make sure the noises are different between normal window and private + // windows. + result = test.isDataRandomized(privateData[0], data[0]); + is( + result, + enabled, + `The image data between the normal window and the private window are ${ + enabled ? "different" : "the same" + }.` + ); + } + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(privateTab); + await BrowserTestUtils.closeWindow(privateWindow); +} + +add_setup(async function () { + // Disable the fingerprinting randomization. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", false], + ["privacy.fingerprintingProtection", false], + ["privacy.fingerprintingProtection.pbmode", false], + ["privacy.resistFingerprinting", false], + ], + }); + + // Open a tab for extracting the canvas data. + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage); + + // Extract the original canvas data without random noise. + for (let test of TEST_CASES) { + let data = await test.extractCanvasData(tab.linkedBrowser); + test.originalData = data[0]; + } + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function run_tests_with_randomization_enabled() { + await runTest(true); +}); + +add_task(async function run_tests_with_randomization_disabled() { + await runTest(false); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js new file mode 100644 index 0000000000..2f27925c71 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js @@ -0,0 +1,323 @@ +/* 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/. */ + +const emptyPage = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "empty.html"; + +/** + * Bug 1816189 - Testing canvas randomization on canvas data extraction in + * workers. + * + * In the test, we create offscreen canvas in workers and test if the extracted + * canvas data is altered because of the canvas randomization. + */ + +/** + * + * Spawns a worker in the given browser and sends a callback function to it. + * The result of the callback function is returned as a Promise. + * + * @param {object} browser - The browser context to spawn the worker in. + * @param {Function} fn - The callback function to send to the worker. + * @returns {Promise} A Promise that resolves to the result of the callback function. + */ +function runFunctionInWorker(browser, fn) { + return SpecialPowers.spawn(browser, [fn.toString()], async callback => { + // Create a worker. + let worker = new content.Worker("worker.js"); + + // Send the callback to the worker. + return new content.Promise(resolve => { + worker.onmessage = e => { + resolve(e.data.result); + }; + + worker.postMessage({ + callback, + }); + }); + }); +} + +var TEST_CASES = [ + { + name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas", + extractCanvasData: async _ => { + let offscreenCanvas = new OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + const imageData = context.getImageData(0, 0, 100, 100); + + const imageDataSecond = context.getImageData(0, 0, 100, 100); + + return [imageData.data, imageDataSecond.data]; + }, + isDataRandomized(data1, data2, isCompareOriginal) { + let diffCnt = compareUint8Arrays(data1, data2); + info(`There are ${diffCnt} bits are different.`); + + // The Canvas randomization adds at most 512 bits noise to the image data. + // We compare the image data arrays to see if they are different and the + // difference is within the range. + + // If we are compare two randomized arrays, the difference can be doubled. + let expected = isCompareOriginal + ? NUM_RANDOMIZED_CANVAS_BITS + : NUM_RANDOMIZED_CANVAS_BITS * 2; + + // The number of difference bits should never bigger than the expected + // number. It could be zero if the randomization is disabled. + ok(diffCnt <= expected, "The number of noise bits is expected."); + + return diffCnt <= expected && diffCnt > 0; + }, + }, + { + name: "OffscreenCanvas.convertToBlob() with a 2d context", + extractCanvasData: async _ => { + let offscreenCanvas = new OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + let blob = await offscreenCanvas.convertToBlob(); + + let data = await new Promise(resolve => { + let fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + + let blobSecond = await offscreenCanvas.convertToBlob(); + + let dataSecond = await new Promise(resolve => { + let fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blobSecond); + }); + + return [data, dataSecond]; + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "OffscreenCanvas.convertToBlob() with a webgl context", + extractCanvasData: async _ => { + let offscreenCanvas = new OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("webgl"); + + // Draw a blue rectangle + context.enable(context.SCISSOR_TEST); + context.scissor(0, 150, 150, 150); + context.clearColor(1, 0, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(150, 150, 300, 150); + context.clearColor(0, 1, 0, 1); + context.clear(context.COLOR_BUFFER_BIT); + context.scissor(0, 0, 150, 150); + context.clearColor(0, 0, 1, 1); + context.clear(context.COLOR_BUFFER_BIT); + + let blob = await offscreenCanvas.convertToBlob(); + + let data = await new Promise(resolve => { + let fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + + let blobSecond = await offscreenCanvas.convertToBlob(); + + let dataSecond = await new Promise(resolve => { + let fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blobSecond); + }); + + return [data, dataSecond]; + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, + { + name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context", + extractCanvasData: async _ => { + let offscreenCanvas = new OffscreenCanvas(100, 100); + + const context = offscreenCanvas.getContext("2d"); + + // Draw a red rectangle + context.fillStyle = "red"; + context.fillRect(0, 0, 100, 100); + + const bitmapCanvas = new OffscreenCanvas(100, 100); + + let bitmap = await createImageBitmap(offscreenCanvas); + const bitmapContext = bitmapCanvas.getContext("bitmaprenderer"); + bitmapContext.transferFromImageBitmap(bitmap); + + let blob = await bitmapCanvas.convertToBlob(); + + let data = await new Promise(resolve => { + let fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blob); + }); + + let blobSecond = await bitmapCanvas.convertToBlob(); + + let dataSecond = await new Promise(resolve => { + let fileReader = new FileReader(); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.readAsArrayBuffer(blobSecond); + }); + + return [data, dataSecond]; + }, + isDataRandomized(data1, data2) { + return compareArrayBuffer(data1, data2); + }, + }, +]; + +async function runTest(enabled) { + // Enable/Disable CanvasRandomization by the RFP target overrides. + let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization"; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", true], + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.pbmode", true], + ["privacy.fingerprintingProtection.overrides", RFPOverrides], + ["privacy.resistFingerprinting", false], + ], + }); + + // Open a private window. + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open tabs in the normal and private window. + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage); + + const privateTab = await BrowserTestUtils.openNewForegroundTab( + privateWindow.gBrowser, + emptyPage + ); + + for (let test of TEST_CASES) { + info(`Testing ${test.name} in the worker`); + let data = await await runFunctionInWorker( + tab.linkedBrowser, + test.extractCanvasData + ); + + let result = test.isDataRandomized(data[0], test.originalData); + is( + result, + enabled, + `The image data is ${enabled ? "randomized" : "the same"}.` + ); + + ok( + !test.isDataRandomized(data[0], data[1]), + "The data of first and second access should be the same." + ); + + let privateData = await await runFunctionInWorker( + privateTab.linkedBrowser, + test.extractCanvasData + ); + + // Check if we add noise to canvas data in private windows. + result = test.isDataRandomized(privateData[0], test.originalData, true); + is( + result, + enabled, + `The private image data is ${enabled ? "randomized" : "the same"}.` + ); + + ok( + !test.isDataRandomized(privateData[0], privateData[1]), + "The data of first and second access should be the same." + ); + + // Make sure the noises are different between normal window and private + // windows. + result = test.isDataRandomized(privateData[0], data[0]); + is( + result, + enabled, + `The image data between the normal window and the private window are ${ + enabled ? "different" : "the same" + }.` + ); + } + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(privateTab); + await BrowserTestUtils.closeWindow(privateWindow); +} + +add_setup(async function () { + // Disable the fingerprinting randomization. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", false], + ["privacy.fingerprintingProtection", false], + ["privacy.fingerprintingProtection.pbmode", false], + ["privacy.resistFingerprinting", false], + ], + }); + + // Open a tab for extracting the canvas data in the worker. + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage); + + // Extract the original canvas data without random noise. + for (let test of TEST_CASES) { + let data = await runFunctionInWorker( + tab.linkedBrowser, + test.extractCanvasData + ); + test.originalData = data[0]; + } + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function run_tests_with_randomization_enabled() { + await runTest(true); +}); + +add_task(async function run_tests_with_randomization_disabled() { + await runTest(false); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js new file mode 100644 index 0000000000..5213ea277d --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js @@ -0,0 +1,458 @@ +const TEST_DOMAIN = "https://example.com"; +const TEST_DOMAIN_ANOTHER = "https://example.org"; +const TEST_DOMAIN_THIRD = "https://example.net"; + +const TEST_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_DOMAIN + ) + "testPage.html"; +const TEST_DOMAIN_ANOTHER_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_DOMAIN_ANOTHER + ) + "testPage.html"; +const TEST_DOMAIN_THIRD_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_DOMAIN_THIRD + ) + "testPage.html"; + +/** + * A helper function to get the random key in a hex string format and test if + * the random key works properly. + * + * @param {Browser} browser The browser element of the testing tab. + * @param {string} firstPartyDomain The first-party domain loaded on the tab + * @param {string} thirdPartyDomain The third-party domain to test + * @returns {string} The random key hex string + */ +async function getRandomKeyHexFromBrowser( + browser, + firstPartyDomain, + thirdPartyDomain +) { + // Get the key from the cookieJarSettings of the browser element. + let key = browser.cookieJarSettings.fingerprintingRandomizationKey; + let keyHex = key.map(bytes => bytes.toString(16).padStart(2, "0")).join(""); + + // Get the key from the cookieJarSettings of the top-level document. + let keyTop = await SpecialPowers.spawn(browser, [], _ => { + return content.document.cookieJarSettings.fingerprintingRandomizationKey; + }); + let keyTopHex = keyTop + .map(bytes => bytes.toString(16).padStart(2, "0")) + .join(""); + + is( + keyTopHex, + keyHex, + "The fingerprinting random key should match between the browser element and the top-level document." + ); + + // Get the key from the cookieJarSettings of an about:blank iframe. + let keyAboutBlank = await SpecialPowers.spawn(browser, [], async _ => { + let ifr = content.document.createElement("iframe"); + + let loaded = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = "about:blank"; + + await loaded; + + return SpecialPowers.spawn(ifr, [], _ => { + return content.document.cookieJarSettings.fingerprintingRandomizationKey; + }); + }); + + let keyAboutBlankHex = keyAboutBlank + .map(bytes => bytes.toString(16).padStart(2, "0")) + .join(""); + is( + keyAboutBlankHex, + keyHex, + "The fingerprinting random key should match between the browser element and the about:blank iframe document." + ); + + // Get the key from the cookieJarSettings of the javascript URL iframe + // document. + let keyJavascriptURL = await SpecialPowers.spawn(browser, [], async _ => { + let ifr = content.document.getElementById("testFrame"); + + return ifr.contentDocument.cookieJarSettings.fingerprintingRandomizationKey; + }); + + let keyJavascriptURLHex = keyJavascriptURL + .map(bytes => bytes.toString(16).padStart(2, "0")) + .join(""); + is( + keyJavascriptURLHex, + keyHex, + "The fingerprinting random key should match between the browser element and the javascript URL iframe document." + ); + + // Get the key from the cookieJarSettings of an first-party iframe. + let keyFirstPartyFrame = await SpecialPowers.spawn( + browser, + [firstPartyDomain], + async domain => { + let ifr = content.document.createElement("iframe"); + + let loaded = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = domain; + + await loaded; + + return SpecialPowers.spawn(ifr, [], _ => { + return content.document.cookieJarSettings + .fingerprintingRandomizationKey; + }); + } + ); + + let keyFirstPartyFrameHex = keyFirstPartyFrame + .map(bytes => bytes.toString(16).padStart(2, "0")) + .join(""); + is( + keyFirstPartyFrameHex, + keyHex, + "The fingerprinting random key should match between the browser element and the first-party iframe document." + ); + + // Get the key from the cookieJarSettings of an third-party iframe + let keyThirdPartyFrame = await SpecialPowers.spawn( + browser, + [thirdPartyDomain], + async domain => { + let ifr = content.document.createElement("iframe"); + + let loaded = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = domain; + + await loaded; + + return SpecialPowers.spawn(ifr, [], _ => { + return content.document.cookieJarSettings + .fingerprintingRandomizationKey; + }); + } + ); + + let keyThirdPartyFrameHex = keyThirdPartyFrame + .map(bytes => bytes.toString(16).padStart(2, "0")) + .join(""); + is( + keyThirdPartyFrameHex, + keyHex, + "The fingerprinting random key should match between the browser element and the third-party iframe document." + ); + + return keyHex; +} + +// Test accessing the fingerprinting randomization key will throw if +// fingerprinting randomization is disabled. +add_task(async function test_randomization_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting.randomization.enabled", false]], + }); + + // Ensure accessing the fingerprinting randomization key of the browser + // element will throw if fingerprinting randomization is disabled. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + try { + let key = + tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey; + ok( + false, + `Accessing the fingerprinting randomization key should throw when randomization is disabled. ${key}` + ); + } catch (e) { + ok( + true, + "It should throw when getting the key when randomization is disabled." + ); + } + + // Ensure accessing the fingerprinting randomization key of the top-level + // document will throw if fingerprinting randomization is disabled. + try { + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + return content.document.cookieJarSettings.fingerprintingRandomizationKey; + }); + } catch (e) { + ok( + true, + "It should throw when getting the key when randomization is disabled." + ); + } + + BrowserTestUtils.removeTab(tab); +}); + +// Test accessing the fingerprinting randomization key will throw if +// fingerprinting resistance is disabled. +add_task(async function test_randomization_disabled_with_rfp_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", true], + ["privacy.resistFingerprinting", false], + ], + }); + + // Ensure accessing the fingerprinting randomization key of the browser + // element will throw if fingerprinting randomization is disabled. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + try { + let key = + tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey; + ok( + false, + `Accessing the fingerprinting randomization key should throw when fingerprinting resistance is disabled. ${key}` + ); + } catch (e) { + ok( + true, + "It should throw when getting the key when fingerprinting resistance is disabled." + ); + } + + // Ensure accessing the fingerprinting randomization key of the top-level + // document will throw if fingerprinting randomization is disabled. + try { + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + return content.document.cookieJarSettings.fingerprintingRandomizationKey; + }); + } catch (e) { + ok( + true, + "It should throw when getting the key when fingerprinting resistance is disabled." + ); + } + + BrowserTestUtils.removeTab(tab); +}); + +// Test the fingerprinting randomization key generation. +add_task(async function test_generate_randomization_key() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", true], + ["privacy.resistFingerprinting", true], + ], + }); + + for (let testPrivateWin of [true, false]) { + let win = window; + + if (testPrivateWin) { + win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + } + + let tabOne = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TEST_PAGE + ); + let keyHexOne; + + try { + keyHexOne = await getRandomKeyHexFromBrowser( + tabOne.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + ok(true, `The fingerprinting random key: ${keyHexOne}`); + } catch (e) { + ok( + false, + "Shouldn't fail when getting the random key from the cookieJarSettings" + ); + } + + // Open the test domain again and check if the key remains the same. + let tabTwo = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TEST_PAGE + ); + try { + let keyHexTwo = await getRandomKeyHexFromBrowser( + tabTwo.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + is( + keyHexTwo, + keyHexOne, + `The key should remain the same after reopening the tab.` + ); + } catch (e) { + ok( + false, + "Shouldn't fail when getting the random key from the cookieJarSettings" + ); + } + + // Open a tab with a different domain to see if the key changes. + let tabAnother = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TEST_DOMAIN_ANOTHER_PAGE + ); + try { + let keyHexAnother = await getRandomKeyHexFromBrowser( + tabAnother.linkedBrowser, + TEST_DOMAIN_ANOTHER_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + isnot( + keyHexAnother, + keyHexOne, + `The key should be different when loading a different domain` + ); + } catch (e) { + ok( + false, + "Shouldn't fail when getting the random key from the cookieJarSettings" + ); + } + + BrowserTestUtils.removeTab(tabOne); + BrowserTestUtils.removeTab(tabTwo); + BrowserTestUtils.removeTab(tabAnother); + if (testPrivateWin) { + await BrowserTestUtils.closeWindow(win); + } + } +}); + +// Test the fingerprinting randomization key will change after private session +// ends. +add_task(async function test_reset_key_after_pbm_session_ends() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", true], + ["privacy.resistFingerprinting", true], + ], + }); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open a tab in the private window. + let tab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + TEST_PAGE + ); + + let keyHex = await getRandomKeyHexFromBrowser( + tab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + // Close the window and open another private window. + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(privateWin); + + privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open a tab again in the new private window. + tab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + TEST_PAGE + ); + + let keyHexNew = await getRandomKeyHexFromBrowser( + tab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + // Ensure the keys are different. + isnot(keyHexNew, keyHex, "Ensure the new key is different from the old one."); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(privateWin); +}); + +// Test accessing the fingerprinting randomization key will throw in normal +// windows if we exempt fingerprinting protection in normal windows. +add_task(async function test_randomization_with_exempted_normal_window() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.randomization.enabled", true], + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.testGranularityMask", 2], + ], + }); + + // Ensure accessing the fingerprinting randomization key of the browser + // element will throw if fingerprinting randomization is exempted from normal + // windows. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + try { + let key = + tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey; + ok( + false, + `Accessing the fingerprinting randomization key should throw when fingerprinting resistance is exempted in normal windows. ${key}` + ); + } catch (e) { + ok( + true, + "It should throw when getting the key when fingerprinting resistance is exempted in normal windows." + ); + } + + // Ensure accessing the fingerprinting randomization key of the top-level + // document will throw if fingerprinting randomization is exempted from normal + // windows. + try { + await SpecialPowers.spawn(tab.linkedBrowser, [], _ => { + return content.document.cookieJarSettings.fingerprintingRandomizationKey; + }); + } catch (e) { + ok( + true, + "It should throw when getting the key when fingerprinting resistance is exempted in normal windows." + ); + } + + BrowserTestUtils.removeTab(tab); + + // Open a private window and check the key can be accessed there. + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open a tab in the private window. + tab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + TEST_PAGE + ); + + // Access the key, this shouldn't throw an error. + await getRandomKeyHexFromBrowser( + tab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/empty.html b/toolkit/components/resistfingerprinting/tests/browser/empty.html new file mode 100644 index 0000000000..4e59eed479 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/empty.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> + <title>An empty page for testing</title> +</head> +<body> +</body> +</html> diff --git a/toolkit/components/resistfingerprinting/tests/browser/head.js b/toolkit/components/resistfingerprinting/tests/browser/head.js new file mode 100644 index 0000000000..a5a0b7ef55 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/head.js @@ -0,0 +1,52 @@ +// Number of bits in the canvas that we randomize when canvas randomization is +// enabled. +const NUM_RANDOMIZED_CANVAS_BITS = 256; + +/** + * Compares two Uint8Arrays and returns the number of bits that are different. + * + * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare. + * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare. + * @returns {number} - The number of bits that are different between the two + * arrays. + */ +function compareUint8Arrays(arr1, arr2) { + let count = 0; + for (let i = 0; i < arr1.length; i++) { + let diff = arr1[i] ^ arr2[i]; + while (diff > 0) { + count += diff & 1; + diff >>= 1; + } + } + return count; +} + +/** + * Compares two ArrayBuffer objects to see if they are different. + * + * @param {ArrayBuffer} buffer1 - The first buffer to compare. + * @param {ArrayBuffer} buffer2 - The second buffer to compare. + * @returns {boolean} True if the buffers are different, false if they are the + * same. + */ +function compareArrayBuffer(buffer1, buffer2) { + // compare the byte lengths of the two buffers + if (buffer1.byteLength !== buffer2.byteLength) { + return true; + } + + // create typed arrays to represent the two buffers + const view1 = new DataView(buffer1); + const view2 = new DataView(buffer2); + + // compare the byte values of the two typed arrays + for (let i = 0; i < buffer1.byteLength; i++) { + if (view1.getUint8(i) !== view2.getUint8(i)) { + return true; + } + } + + // the two buffers are the same + return false; +} diff --git a/toolkit/components/resistfingerprinting/tests/browser/testPage.html b/toolkit/components/resistfingerprinting/tests/browser/testPage.html new file mode 100644 index 0000000000..07a17d3d71 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/testPage.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <title>A page with a javascript URL iframe</title> +</head> +<body> + <iframe id="testFrame" src="javascript:void(0)"></iframe> +</body> +</html> diff --git a/toolkit/components/resistfingerprinting/tests/browser/worker.js b/toolkit/components/resistfingerprinting/tests/browser/worker.js new file mode 100644 index 0000000000..894b208549 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/worker.js @@ -0,0 +1,7 @@ +onmessage = e => { + let runnableStr = `(() => {return (${e.data.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this).then(result => { + postMessage({ result }); + }); +}; diff --git a/toolkit/components/resistfingerprinting/tests/chrome.ini b/toolkit/components/resistfingerprinting/tests/chrome.ini new file mode 100644 index 0000000000..ff7c487ab4 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/chrome.ini @@ -0,0 +1,4 @@ +[DEFAULT] +skip-if = os == 'android' + +[test_spoof_english.html] diff --git a/toolkit/components/resistfingerprinting/tests/moz.build b/toolkit/components/resistfingerprinting/tests/moz.build new file mode 100644 index 0000000000..f5c1582193 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/moz.build @@ -0,0 +1,13 @@ +UNIFIED_SOURCES += [ + "test_reduceprecision.cpp", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "chrome.ini", +] + +BROWSER_CHROME_MANIFESTS += [ + "browser/browser.ini", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp new file mode 100644 index 0000000000..4a2ec72b66 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp @@ -0,0 +1,565 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +#include <math.h> + +#include "gtest/gtest.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsServiceManagerUtils.h" +#include "nsRFPService.h" + +using namespace mozilla; + +// clang-format off +/* + Hello! Are you looking at this file because you got an error you don't understand? + Perhaps something that looks like the following? + + toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp:15: Failure + Expected: reduced1 + Which is: 2064.83 + To be equal to: reduced2 + Which is: 2064.83 + + "Gosh," you might say, "They sure look equal to me. What the heck is going on here?" + + The answer lies beyond what you can see, in that which you cannot see. One must + journey into the depths, the hidden, that which the world fights its hardest to + conceal from us. + + Specially: you need to look at more decimal places. Run the test with: + MOZ_LOG="nsResistFingerprinting:5" + + And look for two successive lines similar to the below (the format will certainly + be different by the time you read this comment): + V/nsResistFingerprinting Given: 2064.83384599999999, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241692.00000000000000, Got: 2064.83383999999978 + V/nsResistFingerprinting Given: 2064.83383999999978, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241691.00000000000000, Got: 2064.83381999999983 + + Look at the last two values: + Got: 2064.83383999999978 + Got: 2064.83381999999983 + + They're supposed to be equal. They're not. But they both round to 2064.83. +*/ +// clang-format on + +bool setupJitter(bool enabled) { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + bool jitterEnabled = false; + if (prefs) { + prefs->GetBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", + &jitterEnabled); + prefs->SetBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", enabled); + } + + return jitterEnabled; +} + +void cleanupJitter(bool jitterWasEnabled) { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + prefs->SetBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", + jitterWasEnabled); + } +} + +void process(double clock, nsRFPService::TimeScale clockUnits, + double precision) { + double reduced1 = nsRFPService::ReduceTimePrecisionImpl( + clock, clockUnits, precision, -1, TimerPrecisionType::Normal); + double reduced2 = nsRFPService::ReduceTimePrecisionImpl( + reduced1, clockUnits, precision, -1, TimerPrecisionType::Normal); + ASSERT_EQ(reduced1, reduced2); +} + +TEST(ResistFingerprinting, ReducePrecision_Assumptions) +{ + ASSERT_EQ(FLT_RADIX, 2); + ASSERT_EQ(DBL_MANT_DIG, 53); +} + +TEST(ResistFingerprinting, ReducePrecision_Reciprocal) +{ + bool jitterEnabled = setupJitter(false); + // This one has a rounding error in the Reciprocal case: + process(2064.8338460, nsRFPService::TimeScale::MicroSeconds, 20); + // These are just big values + process(1516305819, nsRFPService::TimeScale::MicroSeconds, 20); + process(69053.12, nsRFPService::TimeScale::MicroSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_KnownGood) +{ + bool jitterEnabled = setupJitter(false); + process(2064.8338460, nsRFPService::TimeScale::MilliSeconds, 20); + process(69027.62, nsRFPService::TimeScale::MilliSeconds, 20); + process(69053.12, nsRFPService::TimeScale::MilliSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_KnownBad) +{ + bool jitterEnabled = setupJitter(false); + process(1054.842405, nsRFPService::TimeScale::MilliSeconds, 20); + process(273.53038600000002, nsRFPService::TimeScale::MilliSeconds, 20); + process(628.66686500000003, nsRFPService::TimeScale::MilliSeconds, 20); + process(521.28919100000007, nsRFPService::TimeScale::MilliSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Edge) +{ + bool jitterEnabled = setupJitter(false); + process(2611.14, nsRFPService::TimeScale::MilliSeconds, 20); + process(2611.16, nsRFPService::TimeScale::MilliSeconds, 20); + process(2612.16, nsRFPService::TimeScale::MilliSeconds, 20); + process(2601.64, nsRFPService::TimeScale::MilliSeconds, 20); + process(2595.16, nsRFPService::TimeScale::MilliSeconds, 20); + process(2578.66, nsRFPService::TimeScale::MilliSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.12); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations_HighRes) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.12); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations_RFP) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.12); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations_DangerouslyNone) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.145); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.141); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.15999); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.15); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.13); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254740990.0); + // 9007199254740995 is approximated to 9007199254740996 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254740996); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254741000.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743564 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254743564.0); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_HighRes) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254740980.0); + // 9007199254740995 is approximated to 9007199254740980 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254740980); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254741000.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743560 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254743560.0); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_RFP) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254740990.0); + // 9007199254740995 is approximated to 9007199254740980 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254740995); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254741000.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743560 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254743565.0); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, + ReducePrecision_ExpectedLossOfPrecision_DangerouslyNone) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254740992.0); + // 9007199254740995 is approximated to 9007199254740980 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254740995); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254740999.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743560 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254743568.0); + cleanupJitter(jitterEnabled); +} + +// Use an ugly but simple hack to turn an integer-based rand() +// function to a double-based one. +#define RAND_DOUBLE (rand() * (rand() / (double)rand())) + +// If you're doing logging, you really don't want to run this test. +#define RUN_AGGRESSIVE false + +TEST(ResistFingerprinting, ReducePrecision_Aggressive) +{ + if (!RUN_AGGRESSIVE) { + return; + } + + bool jitterEnabled = setupJitter(false); + + for (int i = 0; i < 10000; i++) { + // Test three different time magnitudes, with decimals. + // Note that we need separate variables for the different units, as scaling + // them after calculating them will erase effects of approximation. + // A magnitude in the seconds since epoch range. + double time1_s = fmod(RAND_DOUBLE, 1516305819.0); + double time1_ms = fmod(RAND_DOUBLE, 1516305819000.0); + double time1_us = fmod(RAND_DOUBLE, 1516305819000000.0); + // A magnitude in the 'couple of minutes worth of milliseconds' range. + double time2_s = fmod(RAND_DOUBLE, (60.0 * 60 * 5)); + double time2_ms = fmod(RAND_DOUBLE, (1000.0 * 60 * 60 * 5)); + double time2_us = fmod(RAND_DOUBLE, (1000000.0 * 60 * 60 * 5)); + // A magnitude in the small range + double time3_s = fmod(RAND_DOUBLE, 10); + double time3_ms = fmod(RAND_DOUBLE, 10000); + double time3_us = fmod(RAND_DOUBLE, 10000000); + + // Test two precision magnitudes, no decimals. + // A magnitude in the high milliseconds. + double precision1 = rand() % 250000; + // a magnitude in the low microseconds. + double precision2 = rand() % 200; + + process(time1_s, nsRFPService::TimeScale::Seconds, precision1); + process(time1_s, nsRFPService::TimeScale::Seconds, precision2); + process(time2_s, nsRFPService::TimeScale::Seconds, precision1); + process(time2_s, nsRFPService::TimeScale::Seconds, precision2); + process(time3_s, nsRFPService::TimeScale::Seconds, precision1); + process(time3_s, nsRFPService::TimeScale::Seconds, precision2); + + process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision1); + process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision2); + process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision1); + process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision2); + process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision1); + process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision2); + + process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision1); + process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision2); + process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision1); + process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision2); + process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision1); + process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision2); + } + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_JitterTestVectors) +{ + bool jitterEnabled = setupJitter(true); + + // clang-format off + /* + * Here's our test vector. We set the secret to the 16 byte value + * 0x0001020304050607 0x1011121314151617 + * + * We then use a stand alone XOrShift128+ generator: + * + * The midpoints are: + * 0 = 328 + * 500 = 25 + * 1000 = 73 + * 1500 = 89 + * 2000 = 73 + * 2500 = 153 + * 3000 = 9 + * 3500 = 89 + * 4000 = 73 + * 4500 = 205 + * 5000 = 253 + * 5500 = 141 + */ + // clang-format on + + // Set the secret + long long throwAway; + uint8_t hardcodedSecret[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17}; + + nsRFPService::RandomMidpoint(0, 500, -1, &throwAway, hardcodedSecret); + + // Run the test vectors + double result; + + result = nsRFPService::ReduceTimePrecisionImpl( + 1, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 0); + result = nsRFPService::ReduceTimePrecisionImpl( + 184, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 0); + result = nsRFPService::ReduceTimePrecisionImpl( + 185, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 329, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 499, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + + result = nsRFPService::ReduceTimePrecisionImpl( + 500, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 520, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 524, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 525, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 1000); + result = nsRFPService::ReduceTimePrecisionImpl( + 930, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 1000); + result = nsRFPService::ReduceTimePrecisionImpl( + 1072, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 1000); + + result = nsRFPService::ReduceTimePrecisionImpl( + 4000, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4050, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4072, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4073, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4340, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4499, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + + result = nsRFPService::ReduceTimePrecisionImpl( + 4500, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4536, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4704, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4705, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 5000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4726, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 5000); + result = nsRFPService::ReduceTimePrecisionImpl( + 5106, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 5000); + + cleanupJitter(jitterEnabled); +} diff --git a/toolkit/components/resistfingerprinting/tests/test_spoof_english.html b/toolkit/components/resistfingerprinting/tests/test_spoof_english.html new file mode 100644 index 0000000000..1ebc6277e9 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/test_spoof_english.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1486258 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1486258</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1486258">Mozilla Bug 1486258</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script type="text/javascript"> +/* global SpecialPowers */ + +const { BrowserTestUtils } = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); +; + +const gOrigAvailableLocales = Services.locale.availableLocales; +const gOrigRequestedLocales = Services.locale.requestedLocales; + +const kDoNotSpoof = 1; +const kSpoofEnglish = 2; + +async function runTest(locale) { + return BrowserTestUtils.withNewTab("http://example.com/", browser => { + return SpecialPowers.spawn(browser, [locale], _locale => { + return { + locale: new Intl.PluralRules(_locale).resolvedOptions().locale, + weekday: new Date(0).toLocaleString(_locale, {weekday: "long"}), + currency: Number(1000).toLocaleString(_locale, {currency: "USD"}), + }; + }); + }); +} + +async function runSpoofTest(spoof) { + ok(spoof == kDoNotSpoof || + spoof == kSpoofEnglish, "check supported parameter"); + + await SpecialPowers.pushPrefEnv({ + "set": [ + ["privacy.spoof_english", spoof], + ], + }); + + is(SpecialPowers.getBoolPref("javascript.use_us_english_locale", false), + spoof == kSpoofEnglish, + "use_us_english_locale"); + + let results = await runTest(undefined); + + await SpecialPowers.popPrefEnv(); + + return results; +} + +async function setupLocale(locale) { + Services.locale.availableLocales = [locale]; + Services.locale.requestedLocales = [locale]; +} + +async function clearLocale() { + Services.locale.availableLocales = gOrigAvailableLocales; + Services.locale.requestedLocales = gOrigRequestedLocales; +} + +(async function() { + SimpleTest.waitForExplicitFinish(); + + // 1. preliminary for spoof test + await SpecialPowers.pushPrefEnv({ + "set": [ + ["privacy.resistFingerprinting", true], + ], + }); + + // 2. log English/German results + let enResults = await runTest("en-US"); + is(enResults.locale, "en-US", "default locale"); + + let deResults = await runTest("de"); + is(deResults.locale, "de", "default locale"); + + // 3. set system locale to German + await setupLocale("de"); + + // 4. log non-spoofed results + let nonSpoofedResults = await runSpoofTest(kDoNotSpoof); + + // 5. log spoofed results + let spoofedResults = await runSpoofTest(kSpoofEnglish); + + // 6. compare spoofed/non-spoofed results + for (let key in enResults) { + isnot(enResults[key], deResults[key], "compare en/de result: " + key); + is(nonSpoofedResults[key], deResults[key], "compare non-spoofed result: " + key); + is(spoofedResults[key], enResults[key], "compare spoofed result: " + key); + } + + // 7. restore default locale + await clearLocale(); + + SimpleTest.finish(); +})(); + +</script> + +</body> +</html> |