summaryrefslogtreecommitdiffstats
path: root/toolkit/components/resistfingerprinting
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/resistfingerprinting
parentInitial commit. (diff)
downloadfirefox-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')
-rw-r--r--toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h174
-rw-r--r--toolkit/components/resistfingerprinting/RFPHelper.sys.mjs642
-rw-r--r--toolkit/components/resistfingerprinting/RFPTargets.inc58
-rw-r--r--toolkit/components/resistfingerprinting/RelativeTimeline.cpp39
-rw-r--r--toolkit/components/resistfingerprinting/RelativeTimeline.h25
-rw-r--r--toolkit/components/resistfingerprinting/docs/implementation.rst78
-rw-r--r--toolkit/components/resistfingerprinting/docs/index.rst8
-rw-r--r--toolkit/components/resistfingerprinting/moz.build28
-rw-r--r--toolkit/components/resistfingerprinting/nsRFPService.cpp1461
-rw-r--r--toolkit/components/resistfingerprinting/nsRFPService.h349
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser.ini10
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js589
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js323
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js458
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/empty.html8
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/head.js52
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/testPage.html9
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/worker.js7
-rw-r--r--toolkit/components/resistfingerprinting/tests/chrome.ini4
-rw-r--r--toolkit/components/resistfingerprinting/tests/moz.build13
-rw-r--r--toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp565
-rw-r--r--toolkit/components/resistfingerprinting/tests/test_spoof_english.html119
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>