summaryrefslogtreecommitdiffstats
path: root/toolkit/components/resistfingerprinting
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/resistfingerprinting
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/resistfingerprinting')
-rw-r--r--toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs242
-rw-r--r--toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h173
-rw-r--r--toolkit/components/resistfingerprinting/RFPHelper.sys.mjs630
-rw-r--r--toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h23
-rw-r--r--toolkit/components/resistfingerprinting/RFPTargets.inc122
-rw-r--r--toolkit/components/resistfingerprinting/RelativeTimeline.cpp33
-rw-r--r--toolkit/components/resistfingerprinting/RelativeTimeline.h25
-rw-r--r--toolkit/components/resistfingerprinting/components.conf37
-rw-r--r--toolkit/components/resistfingerprinting/docs/implementation.rst78
-rw-r--r--toolkit/components/resistfingerprinting/docs/index.rst8
-rw-r--r--toolkit/components/resistfingerprinting/metrics.yaml27
-rw-r--r--toolkit/components/resistfingerprinting/moz.build43
-rw-r--r--toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl46
-rw-r--r--toolkit/components/resistfingerprinting/nsIRFPService.idl80
-rw-r--r--toolkit/components/resistfingerprinting/nsRFPService.cpp2137
-rw-r--r--toolkit/components/resistfingerprinting/nsRFPService.h467
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser.toml33
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js111
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js658
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js335
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js406
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js559
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js490
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js97
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js85
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js52
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js172
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html22
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/empty.html8
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf12
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html102
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/head.js227
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html37
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js15
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/testHelpers.js30
-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/chrome.toml4
-rw-r--r--toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html117
-rw-r--r--toolkit/components/resistfingerprinting/tests/gtest/moz.build5
-rw-r--r--toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp566
-rw-r--r--toolkit/components/resistfingerprinting/tests/moz.build9
42 files changed, 8339 insertions, 0 deletions
diff --git a/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs b/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs
new file mode 100644
index 0000000000..b6d5bed59c
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs
@@ -0,0 +1,242 @@
+// -*- 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+});
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
+ return console.createInstance({
+ prefix: "FingerprintingWebCompatService",
+ maxLogLevelPref:
+ "privacy.fingerprintingProtection.WebCompatService.logLevel",
+ });
+});
+
+const SCHEMA = `{
+ "type": "object",
+ "title": "Fingerprinting Overrides",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "firstPartyDomain",
+ "overrides"
+ ],
+ "properties": {
+ "overrides": {
+ "type": "string",
+ "pattern": "^[+-][A-Za-z]+(?:,[+-][A-Za-z]+)*$",
+ "description": "The fingerprinting overrides. See https://searchfox.org/mozilla-central/source/toolkit/components/resistfingerprinting/RFPTargets.inc for details."
+ },
+ "firstPartyDomain": {
+ "type": "string",
+ "pattern": "^(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$",
+ "description": "The first-party domain associated with the override. Use '*' to match all domains. Only legit domains allowed."
+ },
+ "thirdPartyDomain": {
+ "type": "string",
+ "pattern": "^(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$",
+ "description": "The third-party domain associated with the override. Use '*' to match all domains. Only legit domains allowed. Leave this field empty if the override is only for the first-party context."
+ }
+ }
+}`;
+
+const COLLECTION_NAME = "fingerprinting-protection-overrides";
+const PREF_GRANULAR_OVERRIDES =
+ "privacy.fingerprintingProtection.granularOverrides";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "granularOverridesPref",
+ PREF_GRANULAR_OVERRIDES
+);
+
+/**
+ * The object represents a fingerprinting override.
+ */
+export class FingerprintingOverride {
+ classID = Components.ID("{07f45442-1806-44be-9230-12eb79de9bac}");
+ QueryInterface = ChromeUtils.generateQI(["nsIFingerprintingOverride"]);
+
+ constructor(firstPartyDomain, thirdPartyDomain, overrides) {
+ this.firstPartyDomain = firstPartyDomain;
+ this.thirdPartyDomain = thirdPartyDomain;
+ this.overrides = overrides;
+ }
+}
+
+/**
+ * The singleton service that is responsible for the WebCompat of the
+ * fingerprinting protection. It gets fingerprinting overrides from remote
+ * settings and the local test pref.
+ */
+export class FingerprintingWebCompatService {
+ classId = Components.ID("{e7b1da06-2594-4670-aea4-131070baca4c}");
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIFingerprintingWebCompatService",
+ ]);
+ #initialized = false;
+ #remoteOverrides;
+ #granularOverrides;
+ #rs;
+ #validator;
+
+ #isParentProcess;
+
+ constructor() {
+ this.#isParentProcess =
+ Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+
+ this.#remoteOverrides = new Set();
+ this.#granularOverrides = new Set();
+
+ this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
+ this.#validator = new lazy.JsonSchema.Validator(SCHEMA);
+ }
+
+ async init() {
+ lazy.logConsole.debug("init");
+ // We can only access remote settings from the parent process. So, never
+ // init it in the content process.
+ if (!this.#isParentProcess) {
+ throw new Error(
+ "Shouldn't init FingerprintingWebCompatService in content processes."
+ );
+ }
+
+ // Return if we have initiated.
+ if (this.#initialized) {
+ return;
+ }
+ this.#initialized = true;
+
+ // Register listener to import overrides when the overrides pref changes.
+ Services.prefs.addObserver(PREF_GRANULAR_OVERRIDES, this);
+
+ // Register the sync event for the remote settings updates.
+ this.#rs.on("sync", event => {
+ let {
+ data: { current },
+ } = event;
+ this.#onRemoteUpdate(current);
+
+ this.#populateOverrides();
+ });
+
+ // Get the remote overrides from the remote settings.
+ await this.#importRemoteSettingsOverrides();
+
+ // Get the granular overrides from the pref.
+ this.#importPrefOverrides();
+
+ // Populate the overrides to the nsRFPService.
+ this.#populateOverrides();
+
+ lazy.logConsole.debug("Init completes");
+ }
+
+ // Import fingerprinting overrides from the local granular pref.
+ #importPrefOverrides() {
+ lazy.logConsole.debug("importLocalGranularOverrides");
+
+ // Clear overrides before we update.
+ this.#granularOverrides.clear();
+
+ let overrides;
+ try {
+ overrides = JSON.parse(lazy.granularOverridesPref || "[]");
+ } catch (error) {
+ lazy.logConsole.error(
+ `Failed to parse granular override JSON string: Not a valid JSON.`,
+ error
+ );
+ return;
+ }
+
+ // Ensure we have an array we can iterate over and not an object.
+ if (!Array.isArray(overrides)) {
+ lazy.logConsole.error(
+ "Failed to parse granular overrides JSON String: Not an array."
+ );
+ return;
+ }
+
+ for (let override of overrides) {
+ // Validate the override.
+ let { valid, errors } = this.#validator.validate(override);
+
+ if (!valid) {
+ lazy.logConsole.debug("Override validation error", override, errors);
+ continue;
+ }
+
+ this.#granularOverrides.add(
+ this.#createFingerprintingOverrideFrom(override)
+ );
+ }
+ }
+
+ // Import fingerprinting overrides from the remote settings.
+ async #importRemoteSettingsOverrides() {
+ lazy.logConsole.debug("importRemoteSettingsOverrides");
+
+ let entries;
+ try {
+ entries = await this.#rs.get();
+ } catch (e) {}
+
+ this.#onRemoteUpdate(entries || []);
+ }
+
+ #onRemoteUpdate(entries) {
+ lazy.logConsole.debug("onUpdateEntries", { entries });
+ // Clear all overrides before we update the overrides.
+ this.#remoteOverrides.clear();
+
+ for (let entry of entries) {
+ this.#remoteOverrides.add(this.#createFingerprintingOverrideFrom(entry));
+ }
+ }
+
+ #createFingerprintingOverrideFrom(entry) {
+ return new FingerprintingOverride(
+ entry.firstPartyDomain,
+ entry.thirdPartyDomain,
+ entry.overrides
+ );
+ }
+
+ #populateOverrides() {
+ lazy.logConsole.debug("populateOverrides");
+
+ // Create the array that contains all overrides. We explicitly concat the
+ // overrides from testing pref after the ones from remote settings to ensure
+ // that the testing pref will take precedence.
+ let overrides = Array.from(this.#remoteOverrides).concat(
+ Array.from(this.#granularOverrides)
+ );
+
+ // Set the remote override to the RFP service.
+ Services.rfp.setFingerprintingOverrides(Array.from(overrides));
+ }
+
+ observe(subject, topic, prefName) {
+ if (prefName != PREF_GRANULAR_OVERRIDES) {
+ return;
+ }
+
+ this.#importPrefOverrides();
+ this.#populateOverrides();
+ }
+
+ shutdown() {
+ lazy.logConsole.debug("shutdown");
+
+ Services.prefs.removeObserver(PREF_GRANULAR_OVERRIDES, this);
+ }
+}
diff --git a/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h b/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h
new file mode 100644
index 0000000000..c8fafcf22e
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h
@@ -0,0 +1,173 @@
+/* -*- 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, MetaLeft, 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..a085aa492a
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
@@ -0,0 +1,630 @@
+// -*- 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
+ // 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");
+ 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.
+ const l10n = new Localization(
+ ["toolkit/global/resistFingerPrinting.ftl"],
+ true
+ );
+ const message = l10n.formatValueSync("privacy-spoof-english");
+ const flags = Services.prompt.STD_YES_NO_BUTTONS;
+ const 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/RFPTargetIPCUtils.h b/toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h
new file mode 100644
index 0000000000..198332e3f6
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 __RFPTargetIPCUtils_h__
+#define __RFPTargetIPCUtils_h__
+
+#include "ipc/EnumSerializer.h"
+
+#include "nsRFPService.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::RFPTarget>
+ : BitFlagsEnumSerializer<mozilla::RFPTarget,
+ mozilla::RFPTarget::AllTargets> {};
+
+} // namespace IPC
+
+#endif // __RFPTargetIPCUtils_h__
diff --git a/toolkit/components/resistfingerprinting/RFPTargets.inc b/toolkit/components/resistfingerprinting/RFPTargets.inc
new file mode 100644
index 0000000000..d2327ffbfe
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/RFPTargets.inc
@@ -0,0 +1,122 @@
+/* -*- 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, 1llu << 0)
+ITEM_VALUE(PointerEvents, 1llu << 1)
+ITEM_VALUE(KeyboardEvents, 1llu << 2)
+ITEM_VALUE(ScreenOrientation, 1llu << 3)
+// SpeechSynthesis part of the Web Speech API
+ITEM_VALUE(SpeechSynthesis, 1llu << 4)
+// `prefers-color-scheme` CSS media feature
+ITEM_VALUE(CSSPrefersColorScheme, 1llu << 5)
+// `prefers-reduced-motion` CSS media feature
+ITEM_VALUE(CSSPrefersReducedMotion, 1llu << 6)
+// `prefers-contrast` CSS media feature
+ITEM_VALUE(CSSPrefersContrast, 1llu << 7)
+// Add random noises to image data extracted from canvas.
+ITEM_VALUE(CanvasRandomization, 1llu << 8)
+// Canvas targets: For unusual combinations of these, see comments
+// in IsImageExtractionAllowed
+ITEM_VALUE(CanvasImageExtractionPrompt, 1llu << 9)
+ITEM_VALUE(CanvasExtractionFromThirdPartiesIsBlocked, 1llu << 10)
+ITEM_VALUE(CanvasExtractionBeforeUserInputIsBlocked, 1llu << 11)
+
+ITEM_VALUE(JSLocale, 1llu << 12)
+
+// Various "client identification" values of the navigator object
+ITEM_VALUE(NavigatorAppVersion, 1llu << 13)
+ITEM_VALUE(NavigatorBuildID, 1llu << 14)
+ITEM_VALUE(NavigatorHWConcurrency, 1llu << 15)
+ITEM_VALUE(NavigatorOscpu, 1llu << 16)
+ITEM_VALUE(NavigatorPlatform, 1llu << 17)
+ITEM_VALUE(NavigatorUserAgent, 1llu << 18)
+// Audio/VideoStreamTrack labels
+ITEM_VALUE(StreamTrackLabel, 1llu << 19)
+ITEM_VALUE(StreamVideoFacingMode, 1llu << 20)
+ITEM_VALUE(JSDateTimeUTC, 1llu << 21)
+ITEM_VALUE(JSMathFdlibm, 1llu << 22)
+ITEM_VALUE(Gamepad, 1llu << 23)
+ITEM_VALUE(HttpUserAgent, 1llu << 24)
+ITEM_VALUE(WindowOuterSize, 1llu << 25)
+ITEM_VALUE(WindowScreenXY, 1llu << 26)
+ITEM_VALUE(WindowInnerScreenXY, 1llu << 27)
+ITEM_VALUE(ScreenPixelDepth, 1llu << 28)
+ITEM_VALUE(ScreenRect, 1llu << 29)
+ITEM_VALUE(ScreenAvailRect, 1llu << 30)
+// HTMLVideoElement
+// mozParsedFrames, mozDecodedFrames, mozPresentedFrames, mozPaintedFrames
+ITEM_VALUE(VideoElementMozFrames, 1llu << 31)
+// mozFrameDelay
+ITEM_VALUE(VideoElementMozFrameDelay, 1llu << 32)
+// getVideoPlaybackQuality()
+ITEM_VALUE(VideoElementPlaybackQuality, 1llu << 33)
+// See also Reduce Timer Precision (RTP) Caller Type
+ITEM_VALUE(ReduceTimerPrecision, 1llu << 34)
+// Hide keyboard and pointer WidgetEvents
+ITEM_VALUE(WidgetEvents, 1llu << 35)
+ITEM_VALUE(MediaDevices, 1llu << 36)
+ITEM_VALUE(MediaCapabilities, 1llu << 37)
+ITEM_VALUE(AudioSampleRate, 1llu << 38)
+ITEM_VALUE(NavigatorConnection, 1llu << 39)
+ITEM_VALUE(WindowDevicePixelRatio, 1llu << 40)
+ITEM_VALUE(MouseEventScreenPoint, 1llu << 41)
+// Visibility level of font families available to CSS font-matching
+ITEM_VALUE(FontVisibilityBaseSystem, 1llu << 42)
+ITEM_VALUE(FontVisibilityLangPack, 1llu << 43)
+ITEM_VALUE(DeviceSensors, 1llu << 44)
+ITEM_VALUE(FrameRate, 1llu << 45)
+ITEM_VALUE(RoundWindowSize, 1llu << 46)
+ITEM_VALUE(UseStandinsForNativeColors, 1llu << 47)
+ITEM_VALUE(AudioContext, 1llu << 48)
+ITEM_VALUE(MediaError, 1llu << 49)
+ITEM_VALUE(DOMStyleOsxFontSmoothing, 1llu << 50)
+// `device-height`/`device-width` CSS media features
+ITEM_VALUE(CSSDeviceSize, 1llu << 51)
+// `color`/`color-gamut` CSS media features
+ITEM_VALUE(CSSColorInfo, 1llu << 52)
+// `resolution` CSS media feature
+ITEM_VALUE(CSSResolution, 1llu << 53)
+// `prefers-reduced-transparency` CSS media feature
+ITEM_VALUE(CSSPrefersReducedTransparency, 1llu << 54)
+// `inverted-colors` CSS media feature
+ITEM_VALUE(CSSInvertedColors, 1llu << 55)
+// `video-dynamic-range` CSS media feature
+ITEM_VALUE(CSSVideoDynamicRange, 1llu << 56)
+ITEM_VALUE(CSSPointerCapabilities, 1llu << 57)
+// WebGL
+ITEM_VALUE(WebGLRenderCapability, 1llu << 58)
+ITEM_VALUE(WebGLRenderInfo, 1llu << 59)
+ITEM_VALUE(SiteSpecificZoom, 1llu << 60)
+// Are font visibility restrictions applied when resolving a CSS <generic-family>?
+// (This may block the fonts selected in Preferences from actually being used.)
+ITEM_VALUE(FontVisibilityRestrictGenerics, 1llu << 61)
+
+// !!! Don't forget to update kDefaultFingerprintingProtections 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)
+
+/*
+ * Some users desperately want the entire ResistFingerprinting experience, except
+ * one particular behavior (usually TimeZone or ColorScheme.) This value enables
+ * them to specify an override list that will include or exclude everything,
+ * allowing them to opt-in or opt-out of new RFPTargets we add in Firefox or to
+ * the default set enabled. It should come first, otherwise behavior is undefined.
+ * Examples:
+ * +AllTargets,-CSSPrefersColorScheme
+ * -AllTargets,+Gamepad
+ */
+ITEM_VALUE(AllTargets, 0xFFFFFFFF'FFFFFFFF)
diff --git a/toolkit/components/resistfingerprinting/RelativeTimeline.cpp b/toolkit/components/resistfingerprinting/RelativeTimeline.cpp
new file mode 100644
index 0000000000..dd2d304adb
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/RelativeTimeline.cpp
@@ -0,0 +1,33 @@
+/* -*- 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;
+ }
+
+ rv = randomGenerator->GenerateRandomBytesInto(mRandomTimelineSeed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mRandomTimelineSeed = rand();
+ return mRandomTimelineSeed;
+ }
+ }
+ 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/components.conf b/toolkit/components/resistfingerprinting/components.conf
new file mode 100644
index 0000000000..aae29becf4
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/components.conf
@@ -0,0 +1,37 @@
+# -*- 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/.
+
+Classes = [
+ {
+ 'name': 'RFPService',
+ 'cid': '{20093b2e-d5d5-4ce0-8355-96b8d2dc7ff5}',
+ 'interfaces': ['nsIRFPService'],
+ 'contract_ids': ['@mozilla.org/rfp-service;1'],
+ 'type': 'mozilla::nsRFPService',
+ 'headers': ['/toolkit/components/resistfingerprinting/nsRFPService.h'],
+ 'singleton': True,
+ 'constructor': 'mozilla::nsRFPService::GetOrCreate',
+ 'js_name': 'rfp',
+ 'categories': {
+ 'profile-after-change': 'nsRFPService',
+ },
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{07f45442-1806-44be-9230-12eb79de9bac}',
+ 'contract_ids': ['@mozilla.org/fingerprinting-override;1'],
+ 'esModule': 'resource://gre/modules/FingerprintingWebCompatService.sys.mjs',
+ 'constructor': 'FingerprintingOverride',
+ },
+ {
+ 'cid': '{e7b1da06-2594-4670-aea4-131070baca4c}',
+ 'contract_ids': ['@mozilla.org/fingerprinting-webcompat-service;1'],
+ 'singleton': True,
+ 'esModule': 'resource://gre/modules/FingerprintingWebCompatService.sys.mjs',
+ 'constructor': 'FingerprintingWebCompatService',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
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/metrics.yaml b/toolkit/components/resistfingerprinting/metrics.yaml
new file mode 100644
index 0000000000..916c8c7871
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/metrics.yaml
@@ -0,0 +1,27 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: Privacy: Anti-Tracking'
+
+fingerprinting.protection:
+ canvas_noise_calculate_time:
+ type: timing_distribution
+ time_unit: millisecond
+ description: >
+ Counts how long to generate canvas random noises.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838856
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838856#c1
+ notification_emails:
+ - tihuang@mozilla.com
+ - tom@mozilla.com
+ - tschuster@mozilla.com
+ expires: never
+ telemetry_mirror: FINGERPRINTING_PROTECTION_CANVAS_NOISE_CALCULATE_TIME_MS
diff --git a/toolkit/components/resistfingerprinting/moz.build b/toolkit/components/resistfingerprinting/moz.build
new file mode 100644
index 0000000000..6ac25d3b76
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/moz.build
@@ -0,0 +1,43 @@
+# -*- 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",
+ "RFPTargetIPCUtils.h",
+]
+
+EXTRA_JS_MODULES += [
+ "FingerprintingWebCompatService.sys.mjs",
+ "RFPHelper.sys.mjs",
+]
+
+XPIDL_MODULE = "toolkit_resistfingerprinting"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPIDL_SOURCES += [
+ "nsIFingerprintingWebCompatService.idl",
+ "nsIRFPService.idl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl b/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl
new file mode 100644
index 0000000000..01b1379e44
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl
@@ -0,0 +1,46 @@
+/* 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 "nsISupports.idl"
+
+/**
+ * The object to represent a fingerprinting override entry.
+ */
+[scriptable, uuid(07f45442-1806-44be-9230-12eb79de9bac)]
+interface nsIFingerprintingOverride : nsISupports
+{
+ /**
+ * The scope where the override needs to apply.
+ */
+ readonly attribute ACString firstPartyDomain;
+ readonly attribute ACString thirdPartyDomain;
+
+ /**
+ * The fingerprinting overrides definition. This use the same format as the
+ * fingerprinting override pref "privacy.fingerprintingProtection.overrides".
+ */
+ readonly attribute ACString overrides;
+};
+
+/**
+ * A service that monitors updates to the overrides of fingerprinting protection
+ * from remote settings and the local pref.
+ */
+[scriptable, uuid(e7b1da06-2594-4670-aea4-131070baca4c)]
+interface nsIFingerprintingWebCompatService : nsISupports
+{
+ /**
+ * Init the service.
+ */
+ void init();
+
+ /**
+ * Shutdown the service.
+ */
+ void shutdown();
+};
+
+%{C++
+#define NS_FINGERPRINTINGWEBCOMPATSERVICE_CONTRACTID "@mozilla.org/fingerprinting-webcompat-service;1"
+%}
diff --git a/toolkit/components/resistfingerprinting/nsIRFPService.idl b/toolkit/components/resistfingerprinting/nsIRFPService.idl
new file mode 100644
index 0000000000..b81868d202
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/nsIRFPService.idl
@@ -0,0 +1,80 @@
+/* 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 "nsISupports.idl"
+#include "nsIFingerprintingWebCompatService.idl"
+
+interface nsIPrincipal;
+interface nsIChannel;
+
+/**
+ * The singleton serivce which handles fingerprinting protection stuffs.
+ */
+[scriptable, builtinclass, uuid(20093b2e-d5d5-4ce0-8355-96b8d2dc7ff5)]
+interface nsIRFPService : nsISupports
+{
+ /**
+ * Set the fingerprinting overrides.
+ *
+ * @param aOverrides
+ * An array of fingerprinting overrides.
+ */
+ void setFingerprintingOverrides(in Array<nsIFingerprintingOverride> aOverrides);
+
+ /**
+ * Get the fingerprinting overrides of the given scope. This is for testing
+ * purpose.
+ *
+ * @param aScope
+ * The scope of the fingerprinting override.
+ * @return The RFPTarget flags for the fingerprinting overrides of the given
+ * scope. 0 if the overridden scope doesn't exist.
+ *
+ */
+ uint64_t getFingerprintingOverrides(in ACString aDomainKey);
+
+ /**
+ * Clean all overrides. This is for testing purpose.
+ */
+ void cleanAllOverrides();
+
+ /**
+ * The bitfield of the default enabled RFP Targets.
+ */
+ readonly attribute uint64_t enabledFingerprintingProtections;
+
+ /**
+ * Clean all fingerprinting randomization keys.
+ */
+ void cleanAllRandomKeys();
+
+ /**
+ * Clean the fingerprinting randomization key by the given principal.
+ */
+ void cleanRandomKeyByPrincipal(in nsIPrincipal aPrincipal);
+
+ /**
+ * Clean the fingerprinting randomization key by the given domain.
+ */
+ void cleanRandomKeyByDomain(in ACString aDomain);
+
+ /**
+ * Clean the fingerprinting randomization key by the given host and
+ * OriginAttributesPattern.
+ */
+ void cleanRandomKeyByHost(in ACString aHost,
+ in AString aPattern);
+
+ /**
+ * Clean the fingerprinting randomization key by the given
+ * OriginAttributesPattern.
+ */
+ void cleanRandomKeyByOriginAttributesPattern(in AString aPattern);
+
+ /**
+ * Test function for generating and return the fingerprinting randomization
+ * key for the given channel.
+ */
+ Array<uint8_t> testGenerateRandomKey(in nsIChannel aChannel);
+};
diff --git a/toolkit/components/resistfingerprinting/nsRFPService.cpp b/toolkit/components/resistfingerprinting/nsRFPService.cpp
new file mode 100644
index 0000000000..8579fe2a3b
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -0,0 +1,2137 @@
+/* -*- 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/ContentBlockingNotifier.h"
+#include "mozilla/glean/GleanMetrics.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/Sprintf.h"
+#include "mozilla/StaticPrefs_javascript.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CanvasRenderingContextHelper.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/fallible.h"
+#include "mozilla/XorShift128PlusRNG.h"
+
+#include "nsAboutProtocolUtils.h"
+#include "nsBaseHashtable.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsCoord.h"
+#include "nsTHashMap.h"
+#include "nsDebug.h"
+#include "nsEffectiveTLDService.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 "nsILoadInfo.h"
+#include "nsIObserverService.h"
+#include "nsIRandomGenerator.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsIUserIdleService.h"
+#include "nsIWebProgressListener.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");
+
+static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection");
+
+#define RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF \
+ "privacy.fingerprintingProtection.overrides"
+#define RFP_TIMER_UNCONDITIONAL_VALUE 20
+#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
+
+#define FP_OVERRIDES_DOMAIN_KEY_DELIMITER ','
+
+// Fingerprinting protections that are enabled by default. This can be
+// overridden using the privacy.fingerprintingProtection.overrides pref.
+#if defined(MOZ_WIDGET_ANDROID)
+const RFPTarget kDefaultFingerprintingProtections =
+ RFPTarget::CanvasRandomization;
+#else
+const RFPTarget kDefaultFingerprintingProtections =
+ RFPTarget::CanvasRandomization | RFPTarget::FontVisibilityLangPack;
+#endif
+
+static constexpr uint32_t kSuspiciousFingerprintingActivityThreshold = 1;
+
+// ============================================================================
+// ============================================================================
+// ============================================================================
+// Structural Stuff & Pref Observing
+
+NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver, nsIRFPService)
+
+static StaticRefPtr<nsRFPService> sRFPService;
+static bool sInitialized = false;
+
+// Actually enabled fingerprinting protections.
+static Atomic<RFPTarget> sEnabledFingerprintingProtections;
+
+/* static */
+already_AddRefed<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 do_AddRef(sRFPService);
+}
+
+static const char* gCallbackPrefs[] = {
+ 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);
+ }
+
+ Preferences::RegisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs,
+ this);
+
+ JS::SetReduceMicrosecondTimePrecisionCallback(
+ nsRFPService::ReduceTimePrecisionAsUSecsWrapper);
+
+ // Called from here to get the initial list of enabled fingerprinting
+ // protections.
+ UpdateFPPOverrideList();
+
+ return rv;
+}
+
+/* static */
+bool nsRFPService::IsRFPPrefEnabled(bool aIsPrivateMode) {
+ if (StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() ||
+ (aIsPrivateMode &&
+ StaticPrefs::privacy_resistFingerprinting_pbmode_DoNotUseDirectly())) {
+ return true;
+ }
+ return false;
+}
+
+/* static */
+bool nsRFPService::IsRFPEnabledFor(
+ bool aIsPrivateMode, RFPTarget aTarget,
+ const Maybe<RFPTarget>& aOverriddenFingerprintingSettings) {
+ MOZ_ASSERT(aTarget != RFPTarget::AllTargets);
+
+ if (StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() ||
+ (aIsPrivateMode &&
+ StaticPrefs::privacy_resistFingerprinting_pbmode_DoNotUseDirectly())) {
+ if (aTarget == RFPTarget::JSLocale) {
+ return StaticPrefs::privacy_spoof_english() == 2;
+ }
+ return true;
+ }
+
+ if (StaticPrefs::privacy_fingerprintingProtection_DoNotUseDirectly() ||
+ (aIsPrivateMode &&
+ StaticPrefs::
+ privacy_fingerprintingProtection_pbmode_DoNotUseDirectly())) {
+ if (aTarget == RFPTarget::IsAlwaysEnabledForPrecompute) {
+ return true;
+ }
+
+ if (aOverriddenFingerprintingSettings) {
+ return bool(aOverriddenFingerprintingSettings.ref() & aTarget);
+ }
+
+ return bool(sEnabledFingerprintingProtections & aTarget);
+ }
+
+ return false;
+}
+
+void nsRFPService::UpdateFPPOverrideList() {
+ nsAutoString targetOverrides;
+ nsresult rv = Preferences::GetString(
+ RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF, targetOverrides);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning,
+ ("Could not get fingerprinting override pref value"));
+ return;
+ }
+
+ RFPTarget enabled = CreateOverridesFromText(
+ targetOverrides, kDefaultFingerprintingProtections);
+
+ sEnabledFingerprintingProtections = 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);
+ }
+ }
+
+ if (mWebCompatService) {
+ mWebCompatService->Shutdown();
+ }
+
+ 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();
+ }
+}
+
+NS_IMETHODIMP
+nsRFPService::Observe(nsISupports* aObject, const char* aTopic,
+ const char16_t* aMessage) {
+ if (strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, aTopic) == 0) {
+ StartShutdown();
+ }
+
+ 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.
+ OriginAttributesPattern pattern;
+ pattern.mPrivateBrowsingId.Construct(1);
+ ClearBrowsingSessionKey(pattern);
+ }
+
+ if (!strcmp(OBSERVER_TOPIC_IDLE_DAILY, aTopic)) {
+ if (StaticPrefs::
+ privacy_resistFingerprinting_randomization_daily_reset_enabled()) {
+ OriginAttributesPattern pattern;
+ pattern.mPrivateBrowsingId.Construct(
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID);
+ ClearBrowsingSessionKey(pattern);
+ }
+
+ if (StaticPrefs::
+ privacy_resistFingerprinting_randomization_daily_reset_private_enabled()) {
+ OriginAttributesPattern pattern;
+ pattern.mPrivateBrowsingId.Construct(1);
+ ClearBrowsingSessionKey(pattern);
+ }
+ }
+
+ if (nsCRT::strcmp(aTopic, "profile-after-change") == 0 &&
+ XRE_IsParentProcess()) {
+ // Get the singleton of the remote override service if we are in the parent
+ // process.
+ nsresult rv;
+ mWebCompatService =
+ do_GetService(NS_FINGERPRINTINGWEBCOMPATSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mWebCompatService->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ 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!
+ free(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, JS::RTPCallerTypeToken aCallerType, JSContext* aCx) {
+ MOZ_ASSERT(aCx);
+
+#ifdef DEBUG
+ nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx);
+ MOZ_ASSERT(global->GetRTPCallerType() == RTPCallerTypeFromToken(aCallerType));
+#endif
+
+ RTPCallerType callerType = RTPCallerTypeFromToken(aCallerType);
+ 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 */
+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);
+
+ // "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:" MOZILLA_UAVERSION ") Gecko/");
+
+#if defined(ANDROID)
+ userAgent.AppendLiteral(MOZILLA_UAVERSION);
+#else
+ userAgent.AppendLiteral(LEGACY_UA_GECKO_TRAIL);
+#endif
+
+ userAgent.AppendLiteral(" Firefox/" MOZILLA_UAVERSION);
+
+ MOZ_ASSERT(userAgent.Length() <= preallocatedLength);
+}
+
+/* static */
+nsCString nsRFPService::GetSpoofedJSLocale() { return "en-US"_ns; }
+
+// ============================================================================
+// ============================================================================
+// ============================================================================
+// 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) {
+ nsAtom* lang = aDoc->GetContentLanguage();
+
+ // If the content-langauge is not given, we try to get langauge from the
+ // HTML lang attribute.
+ if (!lang) {
+ if (dom::Element* elm = aDoc->GetHtmlElement()) {
+ lang = elm->GetLang();
+ }
+ }
+
+ // If two or more languages are given, per HTML5 spec, we should consider
+ // it as 'unknown'. So we use the default one.
+ if (lang) {
+ nsDependentAtomString langStr(lang);
+ if (!langStr.Contains(char16_t(','))) {
+ langStr.StripWhitespace();
+ GetKeyboardLangAndRegion(langStr, 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::GetBrowsingSessionKey(
+ const OriginAttributes& aOriginAttributes, nsID& aBrowsingSessionKey) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsAutoCString oaSuffix;
+ aOriginAttributes.CreateSuffix(oaSuffix);
+
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Info,
+ ("Get the browsing session key for the originAttributes: %s\n",
+ oaSuffix.get()));
+
+ // If any fingerprinting randomization protection is enabled, we generate the
+ // browsing session key.
+ // Note that there is only canvas randomization protection currently.
+ if (!nsContentUtils::ShouldResistFingerprinting(
+ "Checking the target activation globally without local context",
+ RFPTarget::CanvasRandomization)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ Maybe<nsID> sessionKey = mBrowsingSessionKeys.MaybeGet(oaSuffix);
+
+ // The key has been generated, bail out earlier.
+ if (sessionKey) {
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Info,
+ ("The browsing session key exists: %s\n",
+ sessionKey.ref().ToString().get()));
+ aBrowsingSessionKey = sessionKey.ref();
+ return NS_OK;
+ }
+
+ nsID& newKey =
+ mBrowsingSessionKeys.InsertOrUpdate(oaSuffix, nsID::GenerateUUID());
+
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug,
+ ("Generated browsing session key: %s\n", newKey.ToString().get()));
+ aBrowsingSessionKey = newKey;
+
+ return NS_OK;
+}
+
+void nsRFPService::ClearBrowsingSessionKey(
+ const OriginAttributesPattern& aPattern) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ for (auto iter = mBrowsingSessionKeys.Iter(); !iter.Done(); iter.Next()) {
+ nsAutoCString key(iter.Key());
+ OriginAttributes attrs;
+ Unused << attrs.PopulateFromSuffix(key);
+
+ // Remove the entry if the origin attributes pattern matches
+ if (aPattern.Matches(attrs)) {
+ iter.Remove();
+ }
+ }
+}
+
+void nsRFPService::ClearBrowsingSessionKey(
+ const OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsAutoCString key;
+ aOriginAttributes.CreateSuffix(key);
+
+ mBrowsingSessionKeys.Remove(key);
+}
+
+// static
+Maybe<nsTArray<uint8_t>> nsRFPService::GenerateKey(nsIChannel* aChannel) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(aChannel);
+
+#ifdef DEBUG
+ // Ensure we only compute random key for top-level loads.
+ {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ MOZ_ASSERT(loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_DOCUMENT);
+ }
+#endif
+
+ nsCOMPtr<nsIURI> topLevelURI;
+ Unused << aChannel->GetURI(getter_AddRefs(topLevelURI));
+
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug,
+ ("Generating the randomization key for top-level URI: %s\n",
+ topLevelURI->GetSpecOrDefault().get()));
+
+ RefPtr<nsRFPService> service = GetOrCreate();
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+
+ // Set the partitionKey using the top level URI to ensure that the key is
+ // specific to the top level site.
+ attrs.SetPartitionKey(topLevelURI);
+
+ nsAutoCString oaSuffix;
+ attrs.CreateSuffix(oaSuffix);
+
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug,
+ ("Get the key using OriginAttributes: %s\n", oaSuffix.get()));
+
+ nsID sessionKey = {};
+ if (NS_FAILED(service->GetBrowsingSessionKey(attrs, sessionKey))) {
+ return Nothing();
+ }
+
+ // Return nothing if fingerprinting randomization is disabled for the given
+ // channel.
+ //
+ // Note that canvas randomization is the only fingerprinting randomization
+ // protection currently.
+ if (!nsContentUtils::ShouldResistFingerprinting(
+ aChannel, RFPTarget::CanvasRandomization)) {
+ return Nothing();
+ }
+ auto sessionKeyStr = sessionKey.ToString();
+
+ // 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();
+ }
+
+ // Using the OriginAttributes to get the top level site. The site is composed
+ // of scheme, host, and port.
+ 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;
+}
+
+NS_IMETHODIMP
+nsRFPService::CleanAllRandomKeys() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ mBrowsingSessionKeys.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::CleanRandomKeyByPrincipal(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE);
+
+ OriginAttributes attrs = aPrincipal->OriginAttributesRef();
+ nsCOMPtr<nsIURI> uri = aPrincipal->GetURI();
+ attrs.SetPartitionKey(uri);
+
+ ClearBrowsingSessionKey(attrs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::CleanRandomKeyByDomain(const nsACString& aDomain) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ // Get http URI from the domain.
+ nsCOMPtr<nsIURI> httpURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(httpURI), "http://"_ns + aDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use the originAttributes to get the partitionKey.
+ OriginAttributes attrs;
+ attrs.SetPartitionKey(httpURI);
+
+ // Create a originAttributesPattern and set the http partitionKey to the
+ // pattern.
+ OriginAttributesPattern pattern;
+ pattern.mPartitionKey.Reset();
+ pattern.mPartitionKey.Construct(attrs.mPartitionKey);
+
+ ClearBrowsingSessionKey(pattern);
+
+ // Get https URI from the domain.
+ nsCOMPtr<nsIURI> httpsURI;
+ rv = NS_NewURI(getter_AddRefs(httpsURI), "https://"_ns + aDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use the originAttributes to get the partitionKey and set to the pattern.
+ attrs.SetPartitionKey(httpsURI);
+ pattern.mPartitionKey.Reset();
+ pattern.mPartitionKey.Construct(attrs.mPartitionKey);
+
+ ClearBrowsingSessionKey(pattern);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::CleanRandomKeyByHost(const nsACString& aHost,
+ const nsAString& aPattern) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Get http URI from the host.
+ nsCOMPtr<nsIURI> httpURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(httpURI), "http://"_ns + aHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use the originAttributes to get the partitionKey.
+ OriginAttributes attrs;
+ attrs.SetPartitionKey(httpURI);
+
+ // Set the partitionKey to the pattern.
+ pattern.mPartitionKey.Reset();
+ pattern.mPartitionKey.Construct(attrs.mPartitionKey);
+
+ ClearBrowsingSessionKey(pattern);
+
+ // Get https URI from the host.
+ nsCOMPtr<nsIURI> httpsURI;
+ rv = NS_NewURI(getter_AddRefs(httpsURI), "https://"_ns + aHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use the originAttributes to get the partitionKey and set to the pattern.
+ attrs.SetPartitionKey(httpsURI);
+ pattern.mPartitionKey.Reset();
+ pattern.mPartitionKey.Construct(attrs.mPartitionKey);
+
+ ClearBrowsingSessionKey(pattern);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::CleanRandomKeyByOriginAttributesPattern(
+ const nsAString& aPattern) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ClearBrowsingSessionKey(pattern);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::TestGenerateRandomKey(nsIChannel* aChannel,
+ nsTArray<uint8_t>& aKey) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ NS_ENSURE_ARG_POINTER(aChannel);
+
+ Maybe<nsTArray<uint8_t>> key = GenerateKey(aChannel);
+
+ if (!key) {
+ return NS_OK;
+ }
+
+ aKey = key.ref().Clone();
+ return NS_OK;
+}
+
+// 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 aWidth,
+ uint32_t aHeight, uint32_t aSize,
+ gfx::SurfaceFormat aSurfaceFormat) {
+ NS_ENSURE_ARG_POINTER(aData);
+
+ if (!aCookieJarSettings) {
+ return NS_OK;
+ }
+
+ if (aSize <= 4) {
+ return NS_OK;
+ }
+
+ // Don't randomize if all pixels are uniform.
+ static constexpr size_t bytesPerPixel = 4;
+ MOZ_ASSERT(aSize == aWidth * aHeight * bytesPerPixel,
+ "Pixels must be tightly-packed");
+ const bool allPixelsMatch = [&]() {
+ auto itr = RangedPtr<const uint8_t>(aData, aSize);
+ const auto itrEnd = itr + aSize;
+ for (; itr != itrEnd; itr += bytesPerPixel) {
+ if (memcmp(itr.get(), aData, bytesPerPixel) != 0) {
+ return false;
+ }
+ }
+ return true;
+ }();
+ if (allPixelsMatch) {
+ return NS_OK;
+ }
+
+ auto timerId =
+ glean::fingerprinting_protection::canvas_noise_calculate_time.Start();
+
+ nsTArray<uint8_t> canvasKey;
+ nsresult rv = GenerateCanvasKeyFromImageData(aCookieJarSettings, aData, aSize,
+ canvasKey);
+ if (NS_FAILED(rv)) {
+ glean::fingerprinting_protection::canvas_noise_calculate_time.Cancel(
+ std::move(timerId));
+ return 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 20 random changes may occur.
+ uint8_t numNoises = std::clamp<uint8_t>(rnd3, 20, 255);
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wunreachable-code"
+#endif
+ if (false) {
+ // For debugging purposes you can dump the image with this code
+ // then convert it with the image-magick command
+ // convert -size WxH -depth 8 rgba:$i $i.png
+ // Depending on surface format, the alpha and color channels might be mixed
+ // up...
+ static int calls = 0;
+ char filename[256];
+ SprintfLiteral(filename, "rendered_image_%dx%d_%d_pre", aWidth, aHeight,
+ calls);
+ FILE* outputFile = fopen(filename, "wb"); // "wb" for binary write mode
+ fwrite(aData, 1, aSize, outputFile);
+ fclose(outputFile);
+ calls++;
+ }
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+ while (numNoises--) {
+ // 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();
+
+ // 50% chance to XOR a 0x2 or 0x1 into the existing byte
+ aData[idx] = aData[idx] ^ (0x2 >> (bit & 0x1));
+ }
+
+ glean::fingerprinting_protection::canvas_noise_calculate_time
+ .StopAndAccumulate(std::move(timerId));
+
+ return NS_OK;
+}
+
+static const char* CanvasFingerprinterToString(
+ ContentBlockingNotifier::CanvasFingerprinter aFingerprinter) {
+ switch (aFingerprinter) {
+ case ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS:
+ return "FingerprintJS";
+ case ContentBlockingNotifier::CanvasFingerprinter::eAkamai:
+ return "Akamai";
+ case ContentBlockingNotifier::CanvasFingerprinter::eVariant1:
+ return "Variant1";
+ case ContentBlockingNotifier::CanvasFingerprinter::eVariant2:
+ return "Variant2";
+ case ContentBlockingNotifier::CanvasFingerprinter::eVariant3:
+ return "Variant3";
+ case ContentBlockingNotifier::CanvasFingerprinter::eVariant4:
+ return "Variant4";
+ case ContentBlockingNotifier::CanvasFingerprinter::eMaybe:
+ return "Maybe";
+ }
+ return "<error>";
+}
+
+static void MaybeCurrentCaller(nsACString& aFilename, uint32_t& aLineNum,
+ uint32_t& aColumnNum) {
+ aFilename.AssignLiteral("<unknown>");
+
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (!cx) {
+ return;
+ }
+
+ JS::AutoFilename scriptFilename;
+ JS::ColumnNumberOneOrigin columnNum;
+ if (JS::DescribeScriptedCaller(cx, &scriptFilename, &aLineNum, &columnNum)) {
+ if (const char* file = scriptFilename.get()) {
+ aFilename = nsDependentCString(file);
+ }
+ }
+ aColumnNum = columnNum.oneOriginValue();
+}
+
+/* static */ void nsRFPService::MaybeReportCanvasFingerprinter(
+ nsTArray<CanvasUsage>& aUses, nsIChannel* aChannel,
+ nsACString& aOriginNoSuffix) {
+ if (!aChannel) {
+ return;
+ }
+
+ uint32_t extractedWebGL = 0;
+ bool seenExtractedWebGL_300x150 = false;
+
+ uint32_t extracted2D = 0;
+ bool seenExtracted2D_16x16 = false;
+ bool seenExtracted2D_122x110 = false;
+ bool seenExtracted2D_240x60 = false;
+ bool seenExtracted2D_280x60 = false;
+ bool seenExtracted2D_860x6 = false;
+ CanvasFeatureUsage featureUsage = CanvasFeatureUsage::None;
+
+ uint32_t extractedOther = 0;
+
+ for (const auto& usage : aUses) {
+ int32_t width = usage.mSize.width;
+ int32_t height = usage.mSize.height;
+
+ if (width > 2000 || height > 1000) {
+ // Canvases used for fingerprinting are usually relatively small.
+ continue;
+ }
+
+ if (usage.mType == dom::CanvasContextType::Canvas2D) {
+ featureUsage |= usage.mFeatureUsage;
+ extracted2D++;
+ if (width == 16 && height == 16) {
+ seenExtracted2D_16x16 = true;
+ } else if (width == 240 && height == 60) {
+ seenExtracted2D_240x60 = true;
+ } else if (width == 122 && height == 110) {
+ seenExtracted2D_122x110 = true;
+ } else if (width == 280 && height == 60) {
+ seenExtracted2D_280x60 = true;
+ } else if (width == 860 && height == 6) {
+ seenExtracted2D_860x6 = true;
+ }
+ } else if (usage.mType == dom::CanvasContextType::WebGL1) {
+ extractedWebGL++;
+ if (width == 300 && height == 150) {
+ seenExtractedWebGL_300x150 = true;
+ }
+ } else {
+ extractedOther++;
+ }
+ }
+
+ Maybe<ContentBlockingNotifier::CanvasFingerprinter> fingerprinter;
+ if (seenExtractedWebGL_300x150 && seenExtracted2D_240x60 &&
+ seenExtracted2D_122x110) {
+ fingerprinter =
+ Some(ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS);
+ } else if (seenExtractedWebGL_300x150 && seenExtracted2D_280x60 &&
+ seenExtracted2D_16x16) {
+ fingerprinter = Some(ContentBlockingNotifier::CanvasFingerprinter::eAkamai);
+ } else if (seenExtractedWebGL_300x150 && extracted2D > 0 &&
+ (featureUsage & CanvasFeatureUsage::SetFont)) {
+ fingerprinter =
+ Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant1);
+ } else if (extractedWebGL > 0 && extracted2D > 1 && seenExtracted2D_860x6) {
+ fingerprinter =
+ Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant2);
+ } else if (extractedOther > 0 && (extractedWebGL > 0 || extracted2D > 0)) {
+ fingerprinter =
+ Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant3);
+ } else if (extracted2D > 0 && (featureUsage & CanvasFeatureUsage::SetFont) &&
+ (featureUsage &
+ (CanvasFeatureUsage::FillRect | CanvasFeatureUsage::LineTo |
+ CanvasFeatureUsage::Stroke))) {
+ fingerprinter =
+ Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant4);
+ } else if (extractedOther + extractedWebGL + extracted2D > 1) {
+ // This I added primarily to not miss anything, but it can cause false
+ // positives.
+ fingerprinter = Some(ContentBlockingNotifier::CanvasFingerprinter::eMaybe);
+ }
+
+ bool knownFingerprintText =
+ bool(featureUsage & CanvasFeatureUsage::KnownFingerprintText);
+ if (!knownFingerprintText && fingerprinter.isNothing()) {
+ return;
+ }
+
+ if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Info)) {
+ nsAutoCString filename;
+ uint32_t lineNum = 0;
+ uint32_t columnNum = 0;
+ MaybeCurrentCaller(filename, lineNum, columnNum);
+
+ nsAutoCString origin(aOriginNoSuffix);
+ MOZ_LOG(
+ gFingerprinterDetection, LogLevel::Info,
+ ("Detected a potential canvas fingerprinter on %s in script %s:%d:%d "
+ "(KnownFingerprintText: %s, CanvasFingerprinter: %s)",
+ origin.get(), filename.get(), lineNum, columnNum,
+ knownFingerprintText ? "true" : "false",
+ fingerprinter.isSome()
+ ? CanvasFingerprinterToString(fingerprinter.value())
+ : "<none>"));
+ }
+
+ ContentBlockingNotifier::OnEvent(
+ aChannel, false,
+ nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING,
+ aOriginNoSuffix, Nothing(), fingerprinter,
+ Some(featureUsage & CanvasFeatureUsage::KnownFingerprintText));
+}
+
+/* static */ void nsRFPService::MaybeReportFontFingerprinter(
+ nsIChannel* aChannel, nsACString& aOriginNoSuffix) {
+ if (!aChannel) {
+ return;
+ }
+
+ if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Info)) {
+ nsAutoCString filename;
+ uint32_t lineNum = 0;
+ uint32_t columnNum = 0;
+ MaybeCurrentCaller(filename, lineNum, columnNum);
+
+ nsAutoCString origin(aOriginNoSuffix);
+ MOZ_LOG(gFingerprinterDetection, LogLevel::Info,
+ ("Detected a potential font fingerprinter on %s in script %s:%d:%d",
+ origin.get(), filename.get(), lineNum, columnNum));
+ }
+
+ ContentBlockingNotifier::OnEvent(
+ aChannel, false,
+ nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING,
+ aOriginNoSuffix);
+}
+
+/* static */
+bool nsRFPService::CheckSuspiciousFingerprintingActivity(
+ nsTArray<ContentBlockingLog::LogEntry>& aLogs) {
+ if (aLogs.Length() == 0) {
+ return false;
+ }
+
+ uint32_t cnt = 0;
+ // We use these two booleans to prevent counting duplicated fingerprinting
+ // events.
+ bool foundCanvas = false;
+ bool foundFont = false;
+
+ // Iterate through the logs to see if there are suspicious fingerprinting
+ // activities.
+ for (auto& log : aLogs) {
+ // If it's a known canvas fingerprinter, we can directly return true from
+ // here.
+ if (log.mCanvasFingerprinter &&
+ (log.mCanvasFingerprinter.ref() ==
+ ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS ||
+ log.mCanvasFingerprinter.ref() ==
+ ContentBlockingNotifier::CanvasFingerprinter::eAkamai)) {
+ return true;
+ } else if (!foundCanvas && log.mType ==
+ nsIWebProgressListener::
+ STATE_ALLOWED_CANVAS_FINGERPRINTING) {
+ cnt++;
+ foundCanvas = true;
+ } else if (!foundFont &&
+ log.mType ==
+ nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) {
+ cnt++;
+ foundFont = true;
+ }
+ }
+
+ // If the number of suspicious fingerprinting activity exceeds the threshold,
+ // we return true to indicates there is a suspicious fingerprinting activity.
+ return cnt > kSuspiciousFingerprintingActivityThreshold;
+}
+
+/* static */
+nsresult nsRFPService::CreateOverrideDomainKey(
+ nsIFingerprintingOverride* aOverride, nsACString& aDomainKey) {
+ MOZ_ASSERT(aOverride);
+
+ aDomainKey.Truncate();
+
+ nsAutoCString firstPartyDomain;
+ nsresult rv = aOverride->GetFirstPartyDomain(firstPartyDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The first party domain shouldn't be empty. And it shouldn't contain a comma
+ // because we use a comma as a delimiter.
+ if (firstPartyDomain.IsEmpty() ||
+ firstPartyDomain.Contains(FP_OVERRIDES_DOMAIN_KEY_DELIMITER)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString thirdPartyDomain;
+ rv = aOverride->GetThirdPartyDomain(thirdPartyDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We don't accept both domains are wildcards.
+ if (firstPartyDomain.EqualsLiteral("*") &&
+ thirdPartyDomain.EqualsLiteral("*")) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (thirdPartyDomain.IsEmpty()) {
+ aDomainKey.Assign(firstPartyDomain);
+ } else {
+ // Ensure the third-party domain doesn't contain a delimiter.
+ if (thirdPartyDomain.Contains(FP_OVERRIDES_DOMAIN_KEY_DELIMITER)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aDomainKey.Assign(firstPartyDomain);
+ aDomainKey.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER);
+ aDomainKey.Append(thirdPartyDomain);
+ }
+
+ return NS_OK;
+}
+
+/* static */
+RFPTarget nsRFPService::CreateOverridesFromText(const nsString& aOverridesText,
+ RFPTarget aBaseOverrides) {
+ RFPTarget result = aBaseOverrides;
+
+ for (const nsAString& each : aOverridesText.Split(',')) {
+ Maybe<RFPTarget> mappedValue =
+ nsRFPService::TextToRFPTarget(Substring(each, 1, each.Length() - 1));
+ if (mappedValue.isSome()) {
+ RFPTarget target = mappedValue.value();
+ if (target == RFPTarget::IsAlwaysEnabledForPrecompute) {
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning,
+ ("RFPTarget::%s is not a valid value",
+ NS_ConvertUTF16toUTF8(each).get()));
+ } else if (each[0] == '+') {
+ result |= target;
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning,
+ ("Mapped value %s (0x%" PRIx64
+ "), to an addition, now we have 0x%" PRIx64,
+ NS_ConvertUTF16toUTF8(each).get(), uint64_t(target),
+ uint64_t(result)));
+ } else if (each[0] == '-') {
+ result &= ~target;
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning,
+ ("Mapped value %s (0x%" PRIx64
+ ") to a subtraction, now we have 0x%" PRIx64,
+ NS_ConvertUTF16toUTF8(each).get(), uint64_t(target),
+ uint64_t(result)));
+ } else {
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning,
+ ("Mapped value %s (0x%" PRIx64
+ ") to an RFPTarget Enum, but the first "
+ "character wasn't + or -",
+ NS_ConvertUTF16toUTF8(each).get(), uint64_t(target)));
+ }
+ } else {
+ MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning,
+ ("Could not map the value %s to an RFPTarget Enum",
+ NS_ConvertUTF16toUTF8(each).get()));
+ }
+ }
+
+ return result;
+}
+
+NS_IMETHODIMP
+nsRFPService::SetFingerprintingOverrides(
+ const nsTArray<RefPtr<nsIFingerprintingOverride>>& aOverrides) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Clear all overrides before importing.
+ mFingerprintingOverrides.Clear();
+
+ for (const auto& fpOverride : aOverrides) {
+ nsAutoCString domainKey;
+
+ nsresult rv = nsRFPService::CreateOverrideDomainKey(fpOverride, domainKey);
+ // Skip the current overrides if we fail to create the domain key.
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsAutoCString overridesText;
+ rv = fpOverride->GetOverrides(overridesText);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RFPTarget targets = nsRFPService::CreateOverridesFromText(
+ NS_ConvertUTF8toUTF16(overridesText),
+ mFingerprintingOverrides.Contains(domainKey)
+ ? mFingerprintingOverrides.Get(domainKey)
+ : sEnabledFingerprintingProtections);
+
+ // The newly added one will replace the existing one for the given domain
+ // key.
+ mFingerprintingOverrides.InsertOrUpdate(domainKey, targets);
+ }
+
+ if (Preferences::GetBool(
+ "privacy.fingerprintingProtection.remoteOverrides.testing", false)) {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE);
+
+ obs->NotifyObservers(nullptr, "fpp-test:set-overrides-finishes", nullptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::GetEnabledFingerprintingProtections(uint64_t* aProtections) {
+ RFPTarget enabled = sEnabledFingerprintingProtections;
+
+ *aProtections = uint64_t(enabled);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::GetFingerprintingOverrides(const nsACString& aDomainKey,
+ uint64_t* aOverrides) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ Maybe<RFPTarget> overrides = mFingerprintingOverrides.MaybeGet(aDomainKey);
+
+ if (!overrides) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aOverrides = uint64_t(overrides.ref());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsRFPService::CleanAllOverrides() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ mFingerprintingOverrides.Clear();
+ return NS_OK;
+}
+
+/* static */
+Maybe<RFPTarget> nsRFPService::GetOverriddenFingerprintingSettingsForChannel(
+ nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsIURI> uri;
+ Unused << aChannel->GetURI(getter_AddRefs(uri));
+
+ if (uri->SchemeIs("about") && !NS_IsContentAccessibleAboutURI(uri)) {
+ return Nothing();
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ MOZ_ASSERT(loadInfo);
+
+ RefPtr<dom::BrowsingContext> bc;
+ loadInfo->GetTargetBrowsingContext(getter_AddRefs(bc));
+ if (!bc || !bc->IsContent()) {
+ return Nothing();
+ }
+
+ // The channel is for the first-party load.
+ if (!loadInfo->GetIsThirdPartyContextToTopWindow()) {
+ return GetOverriddenFingerprintingSettingsForURI(uri, nullptr);
+ }
+
+ // The channel is for the third-party load. We get the first-party URI from
+ // the top-level window global parent.
+ RefPtr<dom::WindowGlobalParent> topWGP =
+ bc->Top()->Canonical()->GetCurrentWindowGlobal();
+
+ if (NS_WARN_IF(!topWGP)) {
+ return Nothing();
+ }
+
+ nsCOMPtr<nsIPrincipal> topPrincipal = topWGP->DocumentPrincipal();
+ if (NS_WARN_IF(!topPrincipal)) {
+ return Nothing();
+ }
+
+ // Only apply the override if the top is content. In testing, the top level
+ // document could be a null principal. We don't need to apply override in this
+ // case.
+ if (!topPrincipal->GetIsContentPrincipal()) {
+ return Nothing();
+ }
+
+ nsCOMPtr<nsIURI> topURI = topWGP->GetDocumentURI();
+ if (NS_WARN_IF(!topURI)) {
+ return Nothing();
+ }
+
+ // The top-level page could be navigated to an error page. We cannot get
+ // the correct override in this case. So, we return nothing from here.
+ if (nsContentUtils::IsErrorPage(topURI)) {
+ return Nothing();
+ }
+
+#ifdef DEBUG
+ // Verify if the top URI matches the partitionKey of the channel.
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+
+ nsAutoString partitionKey;
+ cookieJarSettings->GetPartitionKey(partitionKey);
+
+ OriginAttributes attrs;
+ attrs.SetPartitionKey(topURI);
+
+ // The partitionKey of the channel could haven't been set here if the loading
+ // channel is top-level.
+ MOZ_ASSERT_IF(!partitionKey.IsEmpty(),
+ attrs.mPartitionKey.Equals(partitionKey));
+#endif
+
+ return GetOverriddenFingerprintingSettingsForURI(topURI, uri);
+}
+
+/* static */
+Maybe<RFPTarget> nsRFPService::GetOverriddenFingerprintingSettingsForURI(
+ nsIURI* aFirstPartyURI, nsIURI* aThirdPartyURI) {
+ MOZ_ASSERT(aFirstPartyURI);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ RefPtr<nsRFPService> service = GetOrCreate();
+ if (NS_WARN_IF(!service)) {
+ return Nothing();
+ }
+
+ // The fingerprinting overrides with a specific scope will replace the
+ // overrides with a more general scope. For example, the {first-party domain}
+ // will take over {first-party domain, *} because the latter one has a smaller
+ // scope.
+
+ // First, we get the overrides that applies to every context.
+ Maybe<RFPTarget> result = service->mFingerprintingOverrides.MaybeGet("*"_ns);
+
+ RefPtr<nsEffectiveTLDService> eTLDService =
+ nsEffectiveTLDService::GetInstance();
+ if (NS_WARN_IF(!eTLDService)) {
+ return Nothing();
+ }
+
+ nsAutoCString firstPartyDomain;
+ nsresult rv = eTLDService->GetBaseDomain(aFirstPartyURI, 0, firstPartyDomain);
+ if (NS_FAILED(rv)) {
+ return Nothing();
+ }
+
+ // The check is for a first-party load. A first-party load can be a
+ // top-level load or a first-party subresource/iframe load. The first-party
+ // load can match the following two scopes.
+ // 1. {first-party domain, *}: Every context that is under the given
+ // first-party domain, including itself.
+ // 2. {first-party domain}: First-party contexts that load the given
+ // first-party domain.
+ if (!aThirdPartyURI) {
+ // Test the {first-party domain, *} scope.
+ nsAutoCString key;
+ key.Assign(firstPartyDomain);
+ key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER);
+ key.Append("*");
+
+ Maybe<RFPTarget> fpOverrides =
+ service->mFingerprintingOverrides.MaybeGet(key);
+ if (fpOverrides) {
+ result = fpOverrides;
+ }
+
+ // Test the {first-party domain} scope.
+ fpOverrides = service->mFingerprintingOverrides.MaybeGet(firstPartyDomain);
+ if (fpOverrides) {
+ result = fpOverrides;
+ }
+
+ return result;
+ }
+
+ // The check is for a third-party load. The third-party load can match the
+ // following three scopes.
+ // 1. {first-party domain, *}: Every context that is under the given
+ // first-party domain.
+ // 2. {*, third-party domain}: Every third-party context that loads the
+ // given third-party domain.
+ // 3. {first-party domain, third-party domain}: The third-party context that
+ // is under the given first-party domain.
+
+ nsAutoCString thirdPartyDomain;
+ rv = eTLDService->GetBaseDomain(aThirdPartyURI, 0, thirdPartyDomain);
+ if (NS_FAILED(rv)) {
+ return Nothing();
+ }
+
+ // Test {first-party domain, *} scope.
+ nsAutoCString key;
+ key.Assign(firstPartyDomain);
+ key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER);
+ key.Append("*");
+ Maybe<RFPTarget> fpOverrides =
+ service->mFingerprintingOverrides.MaybeGet(key);
+ if (fpOverrides) {
+ result = fpOverrides;
+ }
+
+ // Test {*, third-party domain} scope.
+ key.Assign("*");
+ key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER);
+ key.Append(thirdPartyDomain);
+ fpOverrides = service->mFingerprintingOverrides.MaybeGet(key);
+ if (fpOverrides) {
+ result = fpOverrides;
+ }
+
+ // Test {first-party domain, third-party domain} scope.
+ key.Assign(firstPartyDomain);
+ key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER);
+ key.Append(thirdPartyDomain);
+ fpOverrides = service->mFingerprintingOverrides.MaybeGet(key);
+ if (fpOverrides) {
+ result = fpOverrides;
+ }
+
+ return result;
+}
diff --git a/toolkit/components/resistfingerprinting/nsRFPService.h b/toolkit/components/resistfingerprinting/nsRFPService.h
new file mode 100644
index 0000000000..9e0ae9b6af
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -0,0 +1,467 @@
+/* -*- 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 <tuple>
+#include "ErrorList.h"
+#include "PLDHashTable.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/ContentBlockingLog.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/TypedEnumBits.h"
+#include "js/RealmOptions.h"
+#include "nsHashtablesFwd.h"
+#include "nsICookieJarSettings.h"
+#include "nsIFingerprintingWebCompatService.h"
+#include "nsIObserver.h"
+#include "nsISupports.h"
+#include "nsIRFPService.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 armv81"
+# define SPOOFED_PLATFORM "Linux armv81"
+#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 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;
+
+class nsIChannel;
+
+namespace mozilla {
+class WidgetKeyboardEvent;
+class OriginAttributes;
+class OriginAttributesPattern;
+namespace dom {
+class Document;
+enum class CanvasContextType : uint8_t;
+} // namespace dom
+
+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)
+};
+
+inline JS::RTPCallerTypeToken RTPCallerTypeToToken(RTPCallerType aType) {
+ return JS::RTPCallerTypeToken{uint8_t(aType)};
+}
+
+inline RTPCallerType RTPCallerTypeFromToken(JS::RTPCallerTypeToken aToken) {
+ MOZ_RELEASE_ASSERT(
+ aToken.value == uint8_t(RTPCallerType::Normal) ||
+ aToken.value == uint8_t(RTPCallerType::SystemPrincipal) ||
+ aToken.value == uint8_t(RTPCallerType::ResistFingerprinting) ||
+ aToken.value == uint8_t(RTPCallerType::CrossOriginIsolated));
+ return static_cast<RTPCallerType>(aToken.value);
+}
+
+enum TimerPrecisionType {
+ DangerouslyNone = 1,
+ UnconditionalAKAHighRes = 2,
+ Normal = 3,
+ RFP = 4,
+};
+
+// ============================================================================
+
+enum class CanvasFeatureUsage : uint8_t {
+ None = 0,
+ KnownFingerprintText = 1 << 0,
+ SetFont = 1 << 1,
+ FillRect = 1 << 2,
+ LineTo = 1 << 3,
+ Stroke = 1 << 4
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(CanvasFeatureUsage);
+
+class CanvasUsage {
+ public:
+ nsIntSize mSize;
+ dom::CanvasContextType mType;
+ CanvasFeatureUsage mFeatureUsage;
+
+ CanvasUsage(nsIntSize aSize, dom::CanvasContextType aType,
+ CanvasFeatureUsage aFeatureUsage)
+ : mSize(aSize), mType(aType), mFeatureUsage(aFeatureUsage) {}
+};
+
+// ============================================================================
+
+// NOLINTNEXTLINE(bugprone-macro-parentheses)
+#define ITEM_VALUE(name, val) name = val,
+
+// The definition for fingerprinting protections. Each enum represents one
+// fingerprinting protection that targets one specific WebAPI our fingerprinting
+// surface. The enums can be found in RFPTargets.inc.
+enum class RFPTarget : uint64_t {
+#include "RFPTargets.inc"
+};
+
+#undef ITEM_VALUE
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RFPTarget);
+
+// ============================================================================
+
+class nsRFPService final : public nsIObserver, public nsIRFPService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIRFPSERVICE
+
+ static already_AddRefed<nsRFPService> GetOrCreate();
+
+ // _Rarely_ you will need to know if RFP is enabled, or if FPP is enabled.
+ // 98% of the time you should use nsContentUtils::ShouldResistFingerprinting
+ // as the difference will not matter to you.
+ static bool IsRFPPrefEnabled(bool aIsPrivateMode);
+
+ static bool IsRFPEnabledFor(
+ bool aIsPrivateMode, RFPTarget aTarget,
+ const Maybe<RFPTarget>& aOverriddenFingerprintingSettings);
+
+ // --------------------------------------------------------------------------
+ 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);
+ // 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 generates the locale string (e.g. "en-US") that should be
+ // spoofed by the JavaScript engine.
+ static nsCString GetSpoofedJSLocale();
+
+ // --------------------------------------------------------------------------
+
+ /**
+ * 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(nsIChannel* aChannel);
+
+ // 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 aWidth,
+ uint32_t aHeight, uint32_t aSize,
+ mozilla::gfx::SurfaceFormat aSurfaceFormat);
+
+ // --------------------------------------------------------------------------
+
+ // The method for getting the granular fingerprinting protection override of
+ // the given channel. Due to WebCompat reason, there can be a granular
+ // overrides to replace default enabled RFPTargets for the context of the
+ // channel. The method will return Nothing() to indicate using the default
+ // RFPTargets
+ static Maybe<RFPTarget> GetOverriddenFingerprintingSettingsForChannel(
+ nsIChannel* aChannel);
+
+ // The method for getting the granular fingerprinting protection override of
+ // the given first-party and third-party URIs. It will return the granular
+ // overrides if there is one defined for the context of the first-party URI
+ // and third-party URI. Otherwise, it will return Nothing() to indicate using
+ // the default RFPTargets.
+ static Maybe<RFPTarget> GetOverriddenFingerprintingSettingsForURI(
+ nsIURI* aFirstPartyURI, nsIURI* aThirdPartyURI);
+
+ // --------------------------------------------------------------------------
+
+ static void MaybeReportCanvasFingerprinter(nsTArray<CanvasUsage>& aUses,
+ nsIChannel* aChannel,
+ nsACString& aOriginNoSuffix);
+
+ static void MaybeReportFontFingerprinter(nsIChannel* aChannel,
+ nsACString& aOriginNoSuffix);
+
+ // --------------------------------------------------------------------------
+
+ // A helper function to check if there is a suspicious fingerprinting
+ // activity from given content blocking origin logs. It returns true if we
+ // detect suspicious fingerprinting activities.
+ static bool CheckSuspiciousFingerprintingActivity(
+ nsTArray<ContentBlockingLog::LogEntry>& aLogs);
+
+ private:
+ nsresult Init();
+
+ nsRFPService() = default;
+
+ ~nsRFPService() = default;
+
+ 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;
+
+ // --------------------------------------------------------------------------
+
+ // Used by the JS Engine
+ static double ReduceTimePrecisionAsUSecsWrapper(
+ double aTime, JS::RTPCallerTypeToken aCallerType, JSContext* aCx);
+
+ 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 GetBrowsingSessionKey(const OriginAttributes& aOriginAttributes,
+ nsID& aBrowsingSessionKey);
+ void ClearBrowsingSessionKey(const OriginAttributesPattern& aPattern);
+ void ClearBrowsingSessionKey(const OriginAttributes& aOriginAttributes);
+
+ // 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.
+ nsTHashMap<nsCStringHashKey, nsID> mBrowsingSessionKeys;
+
+ nsCOMPtr<nsIFingerprintingWebCompatService> mWebCompatService;
+ nsTHashMap<nsCStringHashKey, RFPTarget> mFingerprintingOverrides;
+
+ // A helper function to create the domain key for the fingerprinting
+ // overrides. The key can be in the following five formats.
+ // 1. {first-party domain}: The override only apply to the first-party domain.
+ // 2. {first-party domain, *}: The overrides apply to every contexts under the
+ // top-level domain, including itself.
+ // 3. {*, third-party domain}: The overrides apply to the third-party domain
+ // under any top-level domain.
+ // 4. {first-party domain, third-party domain}: the overrides apply to the
+ // specific third-party domain under the given first-party domain.
+ // 5. {*}: A global overrides that will apply to every context.
+ static nsresult CreateOverrideDomainKey(nsIFingerprintingOverride* aOverride,
+ nsACString& aDomainKey);
+
+ // A helper function to create the RFPTarget bitfield based on the given
+ // overrides text and the based overrides bitfield. The function will parse
+ // the text and update the based overrides bitfield accordingly. Then, it will
+ // return the updated bitfield.
+ static RFPTarget CreateOverridesFromText(
+ const nsString& aOverridesText, RFPTarget aBaseOverrides = RFPTarget(0));
+};
+
+} // namespace mozilla
+
+#endif /* __nsRFPService_h__ */
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.toml b/toolkit/components/resistfingerprinting/tests/browser/browser.toml
new file mode 100644
index 0000000000..213d6d4287
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files = [
+ "canvas-fingerprinter.html",
+ "empty.html",
+ "font-fingerprinter.html",
+ "head.js",
+ "scriptExecPage.html",
+ "serviceWorker.js",
+ "testHelpers.js",
+ "testPage.html",
+ "worker.js",
+]
+
+["browser_canvas_fingerprinter_telemetry.js"]
+
+["browser_canvas_randomization.js"]
+
+["browser_canvas_randomization_worker.js"]
+
+["browser_fingerprintingRemoteOverrides.js"]
+
+["browser_fingerprintingWebCompat.js"]
+
+["browser_fingerprinting_randomization_key.js"]
+
+["browser_font_fingerprinter_telemetry.js"]
+
+["browser_fpiServiceWorkers_fingerprinting.js"]
+
+["browser_rfp_canvasplaceholder_pdfjs.js"]
+support-files = ["file_pdf.pdf"]
+
+["browser_serviceWorker_fingerprinting_webcompat.js"]
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js
new file mode 100644
index 0000000000..d2a33a4347
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js
@@ -0,0 +1,111 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_PAGE_NORMAL = TEST_PATH + "empty.html";
+const TEST_PAGE_FINGERPRINTER = TEST_PATH + "canvas-fingerprinter.html";
+
+const TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB = "CANVAS_FINGERPRINTING_PER_TAB";
+
+const KEY_UNKNOWN = "unknown";
+const KEY_KNOWN_TEXT = "known_text";
+
+async function clearTelemetry() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry
+ .getKeyedHistogramById(TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB)
+ .clear();
+}
+
+async function getKeyedHistogram(histogram_id, key, bucket, checkCntFn) {
+ let histogram;
+
+ // Wait until the telemetry probe appears.
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ histogram = histograms[histogram_id];
+
+ let checkRes = false;
+
+ if (histogram && histogram[key]) {
+ checkRes = checkCntFn ? checkCntFn(histogram[key].values[bucket]) : true;
+ }
+
+ return checkRes;
+ });
+
+ return histogram[key].values[bucket] || 0;
+}
+
+async function checkKeyedHistogram(histogram_id, key, bucket, expectedCnt) {
+ let cnt = await getKeyedHistogram(histogram_id, key, bucket, cnt => {
+ if (cnt === undefined) {
+ cnt = 0;
+ }
+
+ return cnt == expectedCnt;
+ });
+
+ is(cnt, expectedCnt, "There should be expected count in keyed telemetry.");
+}
+
+add_setup(async function () {
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // First, we open a page without any canvas fingerprinters
+ await BrowserTestUtils.withNewTab(TEST_PAGE_NORMAL, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // Check that the telemetry has been record properly for normal page. The
+ // telemetry should show there was no known fingerprinting attempt.
+ await checkKeyedHistogram(
+ TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB,
+ KEY_UNKNOWN,
+ 0,
+ 1
+ );
+
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // Now open a page with a canvas fingerprinter
+ await BrowserTestUtils.withNewTab(TEST_PAGE_FINGERPRINTER, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // The telemetry should show a a known fingerprinting text and a known
+ // canvas fingerprinter.
+ await checkKeyedHistogram(
+ TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB,
+ KEY_KNOWN_TEXT,
+ 6, // CanvasFingerprinter::eVariant4
+ 1
+ );
+
+ await clearTelemetry();
+});
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..8b4be78d53
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
@@ -0,0 +1,658 @@
+/* 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() but constant color",
+ shouldBeRandomized: false,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ 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(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} 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.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData().",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ 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(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} 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.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a 2d context",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a webgl context",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const bitmapCanvas = document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await 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(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a 2d context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ let data = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a webgl context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ let data = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new 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(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const bitmapCanvas = document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let data = await new Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a 2d context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ let blob = await offscreenCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ let blob = await offscreenCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 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 blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await bitmapCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 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(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} 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.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+];
+
+function runExtractCanvasData(test, tab) {
+ let code = test.extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async code => {
+ let result = await content.eval(`({${code}}).extractCanvasData()`);
+ return result;
+ });
+}
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["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}`);
+
+ // Clear telemetry before starting test.
+ Services.fog.testResetFOG();
+
+ let data = await runExtractCanvasData(test, tab);
+ let result = test.isDataRandomized(test.name, data[0], test.originalData);
+
+ if (test.shouldBeRandomized) {
+ is(
+ result,
+ enabled,
+ `The image data is ${enabled ? "randomized" : "the same"} for ${
+ test.name
+ }.`
+ );
+ } else {
+ is(
+ result,
+ false,
+ `The image data for ${test.name} should never be randomized.`
+ );
+ }
+
+ ok(
+ !test.isDataRandomized(test.name, data[0], data[1]),
+ `The data of first and second access should be the same for ${test.name}.`
+ );
+
+ let privateData = await runExtractCanvasData(test, privateTab);
+
+ // Check if we add noise to canvas data in private windows.
+ result = test.isDataRandomized(
+ test.name,
+ privateData[0],
+ test.originalData,
+ true
+ );
+ if (test.shouldBeRandomized) {
+ is(
+ result,
+ enabled,
+ `The private image data is ${enabled ? "randomized" : "the same"} for ${
+ test.name
+ }.`
+ );
+ } else {
+ is(
+ result,
+ false,
+ `The image data for ${test.name} should never be randomized.`
+ );
+ }
+
+ ok(
+ !test.isDataRandomized(test.name, privateData[0], privateData[1]),
+ "The data of first and second access should be the same for private windows."
+ );
+
+ if (test.shouldBeRandomized) {
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(test.name, privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ } for ${test.name}.`
+ );
+
+ // Verify the telemetry is recorded if canvas randomization is enabled.
+ if (enabled) {
+ await Services.fog.testFlushAllChildren();
+
+ Assert.greater(
+ Glean.fingerprintingProtection.canvasNoiseCalculateTime.testGetValue()
+ .sum,
+ 0,
+ "The telemetry of canvas randomization is recorded."
+ );
+ }
+ }
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["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 runExtractCanvasData(test, tab);
+ 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..dd7c292fa4
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js
@@ -0,0 +1,335 @@
+/* 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.
+ */
+
+var TEST_CASES = [
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} 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.
+ Assert.lessOrEqual(
+ 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 = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 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(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 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(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ 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 = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 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(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+];
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["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`);
+
+ // Clear telemetry before starting test.
+ Services.fog.testResetFOG();
+
+ let data = await await runFunctionInWorker(
+ tab.linkedBrowser,
+ test.extractCanvasData
+ );
+
+ let result = test.isDataRandomized(test.name, data[0], test.originalData);
+ is(
+ result,
+ enabled,
+ `The image data for for '${test.name}' is ${
+ enabled ? "randomized" : "the same"
+ }.`
+ );
+
+ ok(
+ !test.isDataRandomized(test.name, data[0], data[1]),
+ `The data for '${test.name}' 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(
+ test.name,
+ privateData[0],
+ test.originalData,
+ true
+ );
+ is(
+ result,
+ enabled,
+ `The private image data for '${test.name}' is ${
+ enabled ? "randomized" : "the same"
+ }.`
+ );
+
+ ok(
+ !test.isDataRandomized(test.name, privateData[0], privateData[1]),
+ `"The data for '${test.name}' of first and second access should be the same.`
+ );
+
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(test.name, privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data for '${
+ test.name
+ }' between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ }.`
+ );
+ }
+
+ // Verify the telemetry is recorded if canvas randomization is enabled.
+ if (enabled) {
+ await Services.fog.testFlushAllChildren();
+
+ Assert.greater(
+ Glean.fingerprintingProtection.canvasNoiseCalculateTime.testGetValue()
+ .sum,
+ 0,
+ "The telemetry of canvas randomization is recorded."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["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_fingerprintingRemoteOverrides.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js
new file mode 100644
index 0000000000..29c7c9170b
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js
@@ -0,0 +1,406 @@
+/* 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/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const COLLECTION_NAME = "fingerprinting-protection-overrides";
+
+// The javascript bitwise operator only support 32bits. So, we only test
+// RFPTargets that is under low 32 bits.
+const TARGET_DEFAULT = extractLow32Bits(
+ Services.rfp.enabledFingerprintingProtections
+);
+const TARGET_PointerEvents = 0x00000002;
+const TARGET_CanvasRandomization = 0x000000100;
+const TARGET_WindowOuterSize = 0x002000000;
+const TARGET_Gamepad = 0x00800000;
+
+// A helper function to filter high 32 bits.
+function extractLow32Bits(value) {
+ return value & 0xffffffff;
+}
+
+const TEST_CASES = [
+ // Test simple addition.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides: TARGET_DEFAULT | TARGET_WindowOuterSize,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test simple subtraction
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-WindowOuterSize",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides: TARGET_DEFAULT & ~TARGET_WindowOuterSize,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test simple subtraction for default targets.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-CanvasRandomization",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides: TARGET_DEFAULT & ~TARGET_CanvasRandomization,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test multiple targets
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test multiple entries
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "example.org",
+ },
+ {
+ id: "2",
+ last_modified: 1000000000000001,
+ overrides: "+Gamepad",
+ firstPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ {
+ domain: "example.com",
+ overrides: TARGET_DEFAULT | TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an entry with both first-party and third-party domains.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "example.org",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org,example.com",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an entry with the first-party set to a wildcard.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "*",
+ },
+ ],
+ expects: [
+ {
+ domain: "*",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test a entry with the first-party set to a wildcard and the third-party set
+ // to a domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "*,example.com",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an entry with all targets disabled using '-AllTargets' but only enable one target.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-AllTargets,+WindowOuterSize",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "*,example.com",
+ overrides: TARGET_WindowOuterSize,
+ },
+ ],
+ },
+ // Test an entry with all targets disabled using '-AllTargets' but enable some targets.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-AllTargets,+WindowOuterSize,+Gamepad",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "*,example.com",
+ overrides: TARGET_WindowOuterSize | TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an invalid entry with only third party domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.com",
+ noEntry: true,
+ },
+ ],
+ },
+ // Test an invalid entry with both wildcards.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "*",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ noEntry: true,
+ },
+ ],
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.fingerprintingProtection.remoteOverrides.testing", true]],
+ });
+
+ registerCleanupFunction(() => {
+ Services.rfp.cleanAllOverrides();
+ });
+});
+
+add_task(async function test_remote_settings() {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ for (let test of TEST_CASES) {
+ info(`Testing with entry ${JSON.stringify(test.entires)}`);
+
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by a remote settings sync.
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: test.entires,
+ },
+ });
+ await promise;
+
+ ok(true, "Got overrides update");
+
+ for (let expect of test.expects) {
+ // Get the addition and subtraction flags for the domain.
+ try {
+ let overrides = extractLow32Bits(
+ Services.rfp.getFingerprintingOverrides(expect.domain)
+ );
+
+ // Verify if the flags are matching to expected values.
+ is(overrides, expect.overrides, "The override value is correct.");
+ } catch (e) {
+ ok(expect.noEntry, "The override entry doesn't exist.");
+ }
+ }
+ }
+
+ db.clear();
+});
+
+add_task(async function test_pref() {
+ for (let test of TEST_CASES) {
+ info(`Testing with entry ${JSON.stringify(test.entires)}`);
+
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by setting the pref.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify(test.entires),
+ ],
+ ],
+ });
+
+ await promise;
+ ok(true, "Got overrides update");
+
+ for (let expect of test.expects) {
+ try {
+ // Get the addition and subtraction flags for the domain.
+ let overrides = extractLow32Bits(
+ Services.rfp.getFingerprintingOverrides(expect.domain)
+ );
+
+ // Verify if the flags are matching to expected values.
+ is(overrides, expect.overrides, "The override value is correct.");
+ } catch (e) {
+ ok(expect.noEntry, "The override entry doesn't exist.");
+ }
+ }
+ }
+});
+
+// Verify if the pref overrides the remote settings.
+add_task(async function test_pref_override_remote_settings() {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ // Trigger a remote settings sync.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ },
+ });
+ await promise;
+
+ // Then, setting the pref.
+ promise = promiseObserver("fpp-test:set-overrides-finishes");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify([
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+PointerEvents,-WindowOuterSize,-Gamepad",
+ firstPartyDomain: "example.org",
+ },
+ ]),
+ ],
+ ],
+ });
+ await promise;
+
+ // Get the addition and subtraction flags for the domain.
+ let overrides = extractLow32Bits(
+ Services.rfp.getFingerprintingOverrides("example.org")
+ );
+
+ // Verify if the flags are matching to the pref settings.
+ is(
+ overrides,
+ (TARGET_DEFAULT | TARGET_PointerEvents) &
+ ~TARGET_Gamepad &
+ ~TARGET_WindowOuterSize,
+ "The override addition value is correct."
+ );
+
+ db.clear();
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js
new file mode 100644
index 0000000000..1a882fc63d
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js
@@ -0,0 +1,559 @@
+/* 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/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const COLLECTION_NAME = "fingerprinting-protection-overrides";
+
+const TOP_DOMAIN = "https://example.com";
+const THIRD_PARTY_DOMAIN = "https://example.org";
+
+const SPOOFED_HW_CONCURRENCY = 2;
+const DEFAULT_HW_CONCURRENCY = navigator.hardwareConcurrency;
+
+const TEST_TOP_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TOP_DOMAIN
+ ) + "empty.html";
+const TEST_THIRD_PARTY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ THIRD_PARTY_DOMAIN
+ ) + "empty.html";
+
+const TEST_CASES = [
+ // Test the wildcard entry.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "*",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that only applies to the first-party context.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that applies to every context under the first-party domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "*",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that applies to the specific third-party domain under the first-party domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that applies to the every third-party domain under any first-party domains.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test a entry that doesn't apply to the current contexts. The first-party
+ // domain is different.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.net",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test a entry that doesn't apply to the current contexts. The first-party
+ // domain is different.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "*",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test a entry that doesn't apply to a the current contexts. Both first-party
+ // and third-party domains are different.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test multiple entries that enable HW concurrency in the first-party context
+ // and WindowOuter in the third-party context.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ },
+ {
+ id: "2",
+ last_modified: 1000000000000001,
+ overrides: "+NavigatorHWConcurrency",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: true,
+ },
+ },
+ },
+];
+
+async function openAndSetupTestPage() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ // Create a first-party and a third-party iframe in the tab.
+ let { firstPartyBC, thirdPartyBC } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_TOP_PAGE, TEST_THIRD_PARTY_PAGE],
+ async (firstPartySrc, thirdPartySrc) => {
+ let firstPartyFrame = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ firstPartyFrame.onload = resolve;
+ });
+ content.document.body.appendChild(firstPartyFrame);
+ firstPartyFrame.src = firstPartySrc;
+ await loading;
+
+ let thirdPartyFrame = content.document.createElement("iframe");
+ loading = new content.Promise(resolve => {
+ thirdPartyFrame.onload = resolve;
+ });
+ content.document.body.appendChild(thirdPartyFrame);
+ thirdPartyFrame.src = thirdPartySrc;
+ await loading;
+
+ return {
+ firstPartyBC: firstPartyFrame.browsingContext,
+ thirdPartyBC: thirdPartyFrame.browsingContext,
+ };
+ }
+ );
+
+ return { tab, firstPartyBC, thirdPartyBC };
+}
+
+async function openAndSetupTestPageForPopup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ // Create a third-party iframe and a pop-up from this iframe.
+ let { thirdPartyFrameBC, popupBC } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_THIRD_PARTY_PAGE],
+ async thirdPartySrc => {
+ let thirdPartyFrame = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ thirdPartyFrame.onload = resolve;
+ });
+ content.document.body.appendChild(thirdPartyFrame);
+ thirdPartyFrame.src = thirdPartySrc;
+ await loading;
+
+ let popupBC = await SpecialPowers.spawn(
+ thirdPartyFrame,
+ [thirdPartySrc],
+ async src => {
+ let win;
+ let loading = new content.Promise(resolve => {
+ win = content.open(src);
+ win.onload = resolve;
+ });
+ await loading;
+
+ return win.browsingContext;
+ }
+ );
+
+ return {
+ thirdPartyFrameBC: thirdPartyFrame.browsingContext,
+ popupBC,
+ };
+ }
+ );
+
+ return { tab, thirdPartyFrameBC, popupBC };
+}
+
+async function verifyResultInTab(tab, firstPartyBC, thirdPartyBC, expected) {
+ let testWindowOuter = enabled => {
+ if (enabled) {
+ ok(
+ content.wrappedJSObject.outerHeight ==
+ content.wrappedJSObject.innerHeight &&
+ content.wrappedJSObject.outerWidth ==
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is enabled for WindowOuterSize."
+ );
+ } else {
+ ok(
+ content.wrappedJSObject.outerHeight !=
+ content.wrappedJSObject.innerHeight ||
+ content.wrappedJSObject.outerWidth !=
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is not enabled for WindowOuterSize."
+ );
+ }
+ };
+
+ let testHWConcurrency = expected => {
+ is(
+ content.wrappedJSObject.navigator.hardwareConcurrency,
+ expected,
+ "The hardware concurrency is expected."
+ );
+ };
+
+ info(
+ `Verify top-level context with fingerprinting protection is ${
+ expected.top ? "enabled" : "not enabled"
+ }.`
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expected.windowOuter.top],
+ testWindowOuter
+ );
+ let expectHWConcurrencyTop = expected.hwConcurrency.top
+ ? SPOOFED_HW_CONCURRENCY
+ : DEFAULT_HW_CONCURRENCY;
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expectHWConcurrencyTop],
+ testHWConcurrency
+ );
+
+ let workerTopResult = await runFunctionInWorker(
+ tab.linkedBrowser,
+ async _ => {
+ return navigator.hardwareConcurrency;
+ }
+ );
+ is(
+ workerTopResult,
+ expectHWConcurrencyTop,
+ "The top worker reports the expected HW concurrency."
+ );
+
+ info(
+ `Verify first-party context with fingerprinting protection is ${
+ expected.firstParty ? "enabled" : "not enabled"
+ }.`
+ );
+ await SpecialPowers.spawn(
+ firstPartyBC,
+ [expected.windowOuter.firstParty],
+ testWindowOuter
+ );
+ let expectHWConcurrencyFirstParty = expected.hwConcurrency.firstParty
+ ? SPOOFED_HW_CONCURRENCY
+ : DEFAULT_HW_CONCURRENCY;
+ await SpecialPowers.spawn(
+ firstPartyBC,
+ [expectHWConcurrencyFirstParty],
+ testHWConcurrency
+ );
+
+ let workerFirstPartyResult = await runFunctionInWorker(
+ firstPartyBC,
+ async _ => {
+ return navigator.hardwareConcurrency;
+ }
+ );
+ is(
+ workerFirstPartyResult,
+ expectHWConcurrencyFirstParty,
+ "The first-party worker reports the expected HW concurrency."
+ );
+
+ info(
+ `Verify third-party context with fingerprinting protection is ${
+ expected.thirdParty ? "enabled" : "not enabled"
+ }.`
+ );
+ await SpecialPowers.spawn(
+ thirdPartyBC,
+ [expected.windowOuter.thirdParty],
+ testWindowOuter
+ );
+ let expectHWConcurrencyThirdParty = expected.hwConcurrency.thirdParty
+ ? SPOOFED_HW_CONCURRENCY
+ : DEFAULT_HW_CONCURRENCY;
+ await SpecialPowers.spawn(
+ thirdPartyBC,
+ [expectHWConcurrencyThirdParty],
+ testHWConcurrency
+ );
+
+ let workerThirdPartyResult = await runFunctionInWorker(
+ thirdPartyBC,
+ async _ => {
+ return navigator.hardwareConcurrency;
+ }
+ );
+ is(
+ workerThirdPartyResult,
+ expectHWConcurrencyThirdParty,
+ "The third-party worker reports the expected HW concurrency."
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection.remoteOverrides.testing", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ],
+ });
+
+ registerCleanupFunction(() => {
+ Services.rfp.cleanAllOverrides();
+ });
+});
+
+add_task(async function () {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ for (let test of TEST_CASES) {
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by a remote settings sync.
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: test.entires,
+ },
+ });
+ await promise;
+
+ ok(true, "Got overrides update");
+
+ let { tab, firstPartyBC, thirdPartyBC } = await openAndSetupTestPage();
+
+ await verifyResultInTab(tab, firstPartyBC, thirdPartyBC, test.expects);
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ db.clear();
+});
+
+// A test to verify that a pop-up inherits overrides from the third-party iframe
+// that opens it.
+add_task(async function test_popup_inheritance() {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by a remote settings sync.
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ },
+ });
+ await promise;
+
+ let { tab, thirdPartyFrameBC, popupBC } =
+ await openAndSetupTestPageForPopup();
+
+ // Ensure the third-party iframe has the correct overrides.
+ await SpecialPowers.spawn(thirdPartyFrameBC, [], _ => {
+ ok(
+ content.wrappedJSObject.outerHeight ==
+ content.wrappedJSObject.innerHeight &&
+ content.wrappedJSObject.outerWidth ==
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is enabled for third-party iframe."
+ );
+ });
+
+ // Verify the popup inherits overrides from the opener.
+ await SpecialPowers.spawn(popupBC, [], _ => {
+ ok(
+ content.wrappedJSObject.outerHeight ==
+ content.wrappedJSObject.innerHeight &&
+ content.wrappedJSObject.outerWidth ==
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is enabled for the pop-up."
+ );
+
+ content.close();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ db.clear();
+});
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..f2d037491d
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js
@@ -0,0 +1,490 @@
+let { ForgetAboutSite } = ChromeUtils.importESModule(
+ "resource://gre/modules/ForgetAboutSite.sys.mjs"
+);
+
+requestLongerTimeout(2);
+
+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 resistance is disabled.
+add_task(async function test_randomization_disabled_with_rfp_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["privacy.resistFingerprinting.pbmode", false],
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", 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", 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", 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);
+
+ let promisePBExit = TestUtils.topicObserved("last-pb-context-exited");
+ await BrowserTestUtils.closeWindow(privateWin);
+ await promisePBExit;
+
+ 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", false],
+ ["privacy.resistFingerprinting.pbmode", true],
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ],
+ });
+
+ // 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);
+});
+
+// Test that the random key gets reset when the site data gets cleared.
+add_task(async function test_reset_random_key_when_clear_site_data() {
+ // Enable fingerprinting randomization key generation.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+
+ // Open a tab and get randomization key from the test domain.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ let keyHex = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Open another tab and get randomization key from another domain.
+ let anotherTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE
+ );
+
+ let keyHexAnother = await getRandomKeyHexFromBrowser(
+ anotherTab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(anotherTab);
+
+ // Call ForgetAboutSite for the test domain.
+ await ForgetAboutSite.removeDataFromDomain("example.com");
+
+ // Open the tab for the test domain again and verify the key is reset.
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ let keyHexNew = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Open the tab for another domain again and verify the key is intact.
+ anotherTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE
+ );
+ let keyHexAnotherNew = await getRandomKeyHexFromBrowser(
+ anotherTab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Ensure the keys are different for the test domain.
+ isnot(keyHexNew, keyHex, "Ensure the new key is different from the old one.");
+
+ // Ensure the key for another domain isn't changed.
+ is(
+ keyHexAnother,
+ keyHexAnotherNew,
+ "Ensure the key of another domain isn't reset."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(anotherTab);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js
new file mode 100644
index 0000000000..2231197eed
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js
@@ -0,0 +1,97 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_PAGE_NORMAL = TEST_PATH + "empty.html";
+const TEST_PAGE_FINGERPRINTER = TEST_PATH + "font-fingerprinter.html";
+
+const TELEMETRY_FONT_FINGERPRINTING_PER_TAB = "FONT_FINGERPRINTING_PER_TAB";
+
+async function clearTelemetry() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry
+ .getHistogramById(TELEMETRY_FONT_FINGERPRINTING_PER_TAB)
+ .clear();
+}
+
+async function getHistogram(histogram_id, bucket, checkCntFn) {
+ let histogram;
+
+ // Wait until the telemetry probe appears.
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ histogram = histograms[histogram_id];
+
+ let checkRes = false;
+
+ if (histogram) {
+ checkRes = checkCntFn ? checkCntFn(histogram.values[bucket]) : true;
+ }
+
+ return checkRes;
+ });
+
+ return histogram.values[bucket] || 0;
+}
+
+async function checkHistogram(histogram_id, bucket, expectedCnt) {
+ let cnt = await getHistogram(histogram_id, bucket, cnt => {
+ if (cnt === undefined) {
+ cnt = 0;
+ }
+
+ return cnt == expectedCnt;
+ });
+
+ is(cnt, expectedCnt, "There should be expected count in telemetry.");
+}
+
+add_setup(async function () {
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // First, we open a page without any canvas fingerprinters
+ await BrowserTestUtils.withNewTab(TEST_PAGE_NORMAL, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // Check that the telemetry has been record properly for normal page. The
+ // telemetry should show there was no known fingerprinting attempt.
+ await checkHistogram(TELEMETRY_FONT_FINGERPRINTING_PER_TAB, 0 /* false */, 1);
+
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // Now open a page with a canvas fingerprinter
+ await BrowserTestUtils.withNewTab(TEST_PAGE_FINGERPRINTER, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // The telemetry should show one font fingerprinting attempt.
+ await checkHistogram(TELEMETRY_FONT_FINGERPRINTING_PER_TAB, 1 /* true */, 1);
+
+ await clearTelemetry();
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js
new file mode 100644
index 0000000000..64279ae442
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js
@@ -0,0 +1,85 @@
+/* import-globals-from testHelpers.js */
+
+// This test ensures that a service worker for an exempted domain is exempted when it is
+// in the first party context, and not exempted when it is in a third party context.
+
+runTestInFirstAndThirdPartyContexts(
+ "ServiceWorkers - Check that RFP correctly is exempted and not exempted when FPI is enabled",
+ async win => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set externally and then captured because this function gets stringified and
+ // then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.firstParty.isolate", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the first-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ console.info(
+ "First Party, got: " +
+ res.value +
+ " Expected: " +
+ DEFAULT_HARDWARE_CONCURRENCY
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "As a first party, HW Concurrency should not be spoofed"
+ );
+ },
+ async win => {
+ let SPOOFED_HW_CONCURRENCY = 2;
+
+ // Register service worker for the third-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ console.info(
+ "Third Party, got: " + res.value + " Expected: " + SPOOFED_HW_CONCURRENCY
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "As a third party, HW Concurrency should be spoofed"
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.firstparty.isolate", true],
+ ["privacy.resistFingerprinting", true],
+ ["privacy.resistFingerprinting.exemptedDomains", "example.com"],
+ ]
+);
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js b/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js
new file mode 100644
index 0000000000..d80abfdc81
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js
@@ -0,0 +1,52 @@
+const PDF_FILE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_pdf.pdf";
+
+add_task(async function canvas_placeholder_pdfjs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PDF_FILE);
+
+ const data = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ function extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 10;
+ canvas.height = 10;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#332211";
+ context.fillRect(0, 0, 10, 10);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ return context.getImageData(0, 0, 10, 10).data;
+ }
+
+ return content.eval(`(${extractCanvasData})()`);
+ });
+
+ is(data.length, 10 * 10 * 4, "correct canvas data size");
+
+ let failure = false;
+ for (var i = 0; i < 10 * 10 * 4; i += 4) {
+ if (
+ data[i] != 0x33 ||
+ data[i + 1] != 0x22 ||
+ data[i + 2] != 0x11 ||
+ data[i + 3] != 0xff
+ ) {
+ ok(false, `incorrect data ${data.slice(i, i + 4)} @ ${i}..${i + 3}`);
+ failure = true;
+ break;
+ }
+ }
+ ok(!failure, "canvas is correct");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js
new file mode 100644
index 0000000000..a9bab38e61
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from testHelpers.js */
+
+runTestInFirstAndThirdPartyContexts(
+ "ServiceWorkers - Ensure the fingerprinting WebCompat overrides the fingerprinting protection in third-party context.",
+ async win => {
+ let SPOOFED_HW_CONCURRENCY = 2;
+
+ // Register service worker for the first-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "HW Concurrency should be spoofed in the first-party context"
+ );
+ },
+
+ async win => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set externally and then captured because this function gets stringified and
+ // then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.fingerprintingProtection", false]],
+ });
+
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the third-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the third-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "HW Concurrency should not be spoofed in the third-party context"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"],
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify([
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-NavigatorHWConcurrency",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "example.com",
+ },
+ ]),
+ ],
+ ]
+);
+
+runTestInFirstAndThirdPartyContexts(
+ "ServiceWorkers - Ensure the fingerprinting WebCompat overrides the fingerprinting protection in third-party context with FPI enabled.",
+ async win => {
+ let SPOOFED_HW_CONCURRENCY = 2;
+
+ // Register service worker for the first-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "HW Concurrency should be spoofed in the first-party context"
+ );
+ },
+
+ async win => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set enternally and then captured because this function gets stringified and
+ // then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.fingerprintingProtection", false]],
+ });
+
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the third-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the third-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "HW Concurrency should not be spoofed in the third-party context"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.firstparty.isolate", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"],
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify([
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-NavigatorHWConcurrency",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "example.com",
+ },
+ ]),
+ ],
+ ]
+);
diff --git a/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html b/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html
new file mode 100644
index 0000000000..d48d289529
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<head>
+ <title>A page containing a canvas fingerprinter</title>
+</head>
+<body>
+ <canvas width=200 height=200>
+ </canvas>
+ <script>
+ var canvas = document.querySelector("canvas");
+ var context = canvas.getContext("2d");
+
+ context.fillStyle = "rgb(100, 210, 0)";
+ context.fillRect(100, 10, 50, 50);
+ context.fillStyle = "#f65";
+ context.font = "16pt Arial";
+ context.fillText("<@nv45. F1n63r,Pr1n71n6!", 20, 40);
+
+ var data = canvas.toDataURL();
+ </script>
+</body>
+</html>
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/file_pdf.pdf b/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf
new file mode 100644
index 0000000000..593558f9a4
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf
@@ -0,0 +1,12 @@
+%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000102 00000 n
+trailer<</Size 4/Root 1 0 R>>
+startxref
+149
+%EOF \ No newline at end of file
diff --git a/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html b/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html
new file mode 100644
index 0000000000..63e542d67b
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<html>
+<head>
+ <title>A page containing a font fingerprinter</title>
+</head>
+<body>
+<script>
+/*
+* Based on https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js
+* (Archived: https://web.archive.org/web/20150706050408/https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js#L233)
+*
+* Fingerprintjs2 0.1.4 - Modern & flexible browser fingerprint library v2
+* https://github.com/Valve/fingerprintjs2
+* Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com)
+* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+*/
+// kudos to http://www.lalit.org/lab/javascript-css-font-detect/
+function getFonts() {
+ // a font will be compared against all the three default fonts.
+ // and if it doesn't match all 3 then that font is not available.
+ var baseFonts = ["monospace", "sans-serif", "serif"];
+
+ //we use m or w because these two characters take up the maximum width.
+ // And we use a LLi so that the same matching fonts can get separated
+ var testString = "mmmmmmmmmmlli";
+
+ //we test using 72px font size, we may use any size. I guess larger the better.
+ var testSize = "72px";
+
+ var h = document.getElementsByTagName("body")[0];
+
+ // create a SPAN in the document to get the width of the text we use to test
+ var s = document.createElement("span");
+ s.style.fontSize = testSize;
+ s.innerHTML = testString;
+ var defaultWidth = {};
+ var defaultHeight = {};
+ for (var index in baseFonts) {
+ //get the default width for the three base fonts
+ s.style.fontFamily = baseFonts[index];
+ h.appendChild(s);
+ defaultWidth[baseFonts[index]] = s.offsetWidth; //width for the default font
+ defaultHeight[baseFonts[index]] = s.offsetHeight; //height for the defualt font
+ h.removeChild(s);
+ }
+ var detect = function (font) {
+ var detected = false;
+ for (var index in baseFonts) {
+ s.style.fontFamily = font + "," + baseFonts[index]; // name of the font along with the base font for fallback.
+ h.appendChild(s);
+ var matched = (s.offsetWidth !== defaultWidth[baseFonts[index]] || s.offsetHeight !== defaultHeight[baseFonts[index]]);
+ h.removeChild(s);
+ detected = detected || matched;
+ }
+ return detected;
+ };
+ var fontList = [
+ "Abadi MT Condensed Light", "Academy Engraved LET", "ADOBE CASLON PRO", "Adobe Garamond", "ADOBE GARAMOND PRO", "Agency FB", "Aharoni", "Albertus Extra Bold", "Albertus Medium", "Algerian", "Amazone BT", "American Typewriter",
+ "American Typewriter Condensed", "AmerType Md BT", "Andale Mono", "Andalus", "Angsana New", "AngsanaUPC", "Antique Olive", "Aparajita", "Apple Chancery", "Apple Color Emoji", "Apple SD Gothic Neo", "Arabic Typesetting", "ARCHER", "Arial", "Arial Black", "Arial Hebrew",
+ "Arial MT", "Arial Narrow", "Arial Rounded MT Bold", "Arial Unicode MS", "ARNO PRO", "Arrus BT", "Aurora Cn BT", "AvantGarde Bk BT", "AvantGarde Md BT", "AVENIR", "Ayuthaya", "Bandy", "Bangla Sangam MN", "Bank Gothic", "BankGothic Md BT", "Baskerville",
+ "Baskerville Old Face", "Batang", "BatangChe", "Bauer Bodoni", "Bauhaus 93", "Bazooka", "Bell MT", "Bembo", "Benguiat Bk BT", "Berlin Sans FB", "Berlin Sans FB Demi", "Bernard MT Condensed", "BernhardFashion BT", "BernhardMod BT", "Big Caslon", "BinnerD",
+ "Bitstream Vera Sans Mono", "Blackadder ITC", "BlairMdITC TT", "Bodoni 72", "Bodoni 72 Oldstyle", "Bodoni 72 Smallcaps", "Bodoni MT", "Bodoni MT Black", "Bodoni MT Condensed", "Bodoni MT Poster Compressed", "Book Antiqua", "Bookman Old Style",
+ "Bookshelf Symbol 7", "Boulder", "Bradley Hand", "Bradley Hand ITC", "Bremen Bd BT", "Britannic Bold", "Broadway", "Browallia New", "BrowalliaUPC", "Brush Script MT", "Calibri", "Californian FB", "Calisto MT", "Calligrapher", "Cambria", "Cambria Math", "Candara",
+ "CaslonOpnface BT", "Castellar", "Centaur", "Century", "Century Gothic", "Century Schoolbook", "Cezanne", "CG Omega", "CG Times", "Chalkboard", "Chalkboard SE", "Chalkduster", "Charlesworth", "Charter Bd BT", "Charter BT", "Chaucer",
+ "ChelthmITC Bk BT", "Chiller", "Clarendon", "Clarendon Condensed", "CloisterBlack BT", "Cochin", "Colonna MT", "Comic Sans", "Comic Sans MS", "Consolas", "Constantia", "Cooper Black", "Copperplate", "Copperplate Gothic", "Copperplate Gothic Bold",
+ "Copperplate Gothic Light", "CopperplGoth Bd BT", "Corbel", "Cordia New", "CordiaUPC", "Cornerstone", "Coronet", "Courier", "Courier New", "Cuckoo", "Curlz MT", "DaunPenh", "Dauphin", "David", "DB LCD Temp", "DELICIOUS", "Denmark", "Devanagari Sangam MN",
+ "DFKai-SB", "Didot", "DilleniaUPC", "DIN", "DokChampa", "Dotum", "DotumChe", "Ebrima", "Edwardian Script ITC", "Elephant", "English 111 Vivace BT", "Engravers MT", "EngraversGothic BT", "Eras Bold ITC", "Eras Demi ITC", "Eras Light ITC", "Eras Medium ITC",
+ "Estrangelo Edessa", "EucrosiaUPC", "Euphemia", "Euphemia UCAS", "EUROSTILE", "Exotc350 Bd BT", "FangSong", "Felix Titling", "Fixedsys", "FONTIN", "Footlight MT Light", "Forte", "Franklin Gothic", "Franklin Gothic Book", "Franklin Gothic Demi",
+ "Franklin Gothic Demi Cond", "Franklin Gothic Heavy", "Franklin Gothic Medium", "Franklin Gothic Medium Cond", "FrankRuehl", "Fransiscan", "Freefrm721 Blk BT", "FreesiaUPC", "Freestyle Script", "French Script MT", "FrnkGothITC Bk BT", "Fruitger", "FRUTIGER",
+ "Futura", "Futura Bk BT", "Futura Lt BT", "Futura Md BT", "Futura ZBlk BT", "FuturaBlack BT", "Gabriola", "Galliard BT", "Garamond", "Gautami", "Geeza Pro", "Geneva", "Geometr231 BT", "Geometr231 Hv BT", "Geometr231 Lt BT", "Georgia", "GeoSlab 703 Lt BT",
+ "GeoSlab 703 XBd BT", "Gigi", "Gill Sans", "Gill Sans MT", "Gill Sans MT Condensed", "Gill Sans MT Ext Condensed Bold", "Gill Sans Ultra Bold", "Gill Sans Ultra Bold Condensed", "Gisha", "Gloucester MT Extra Condensed", "GOTHAM", "GOTHAM BOLD",
+ "Goudy Old Style", "Goudy Stout", "GoudyHandtooled BT", "GoudyOLSt BT", "Gujarati Sangam MN", "Gulim", "GulimChe", "Gungsuh", "GungsuhChe", "Gurmukhi MN", "Haettenschweiler", "Harlow Solid Italic", "Harrington", "Heather", "Heiti SC", "Heiti TC", "HELV", "Helvetica",
+ "Helvetica Neue", "Herald", "High Tower Text", "Hiragino Kaku Gothic ProN", "Hiragino Mincho ProN", "Hoefler Text", "Humanst 521 Cn BT", "Humanst521 BT", "Humanst521 Lt BT", "Impact", "Imprint MT Shadow", "Incised901 Bd BT", "Incised901 BT",
+ "Incised901 Lt BT", "INCONSOLATA", "Informal Roman", "Informal011 BT", "INTERSTATE", "IrisUPC", "Iskoola Pota", "JasmineUPC", "Jazz LET", "Jenson", "Jester", "Jokerman", "Juice ITC", "Kabel Bk BT", "Kabel Ult BT", "Kailasa", "KaiTi", "Kalinga", "Kannada Sangam MN",
+ "Kartika", "Kaufmann Bd BT", "Kaufmann BT", "Khmer UI", "KodchiangUPC", "Kokila", "Korinna BT", "Kristen ITC", "Krungthep", "Kunstler Script", "Lao UI", "Latha", "Leelawadee", "Letter Gothic", "Levenim MT", "LilyUPC", "Lithograph", "Lithograph Light", "Long Island",
+ "Lucida Bright", "Lucida Calligraphy", "Lucida Console", "Lucida Fax", "LUCIDA GRANDE", "Lucida Handwriting", "Lucida Sans", "Lucida Sans Typewriter", "Lucida Sans Unicode", "Lydian BT", "Magneto", "Maiandra GD", "Malayalam Sangam MN", "Malgun Gothic",
+ "Mangal", "Marigold", "Marion", "Marker Felt", "Market", "Marlett", "Matisse ITC", "Matura MT Script Capitals", "Meiryo", "Meiryo UI", "Microsoft Himalaya", "Microsoft JhengHei", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Sans Serif", "Microsoft Tai Le",
+ "Microsoft Uighur", "Microsoft YaHei", "Microsoft Yi Baiti", "MingLiU", "MingLiU_HKSCS", "MingLiU_HKSCS-ExtB", "MingLiU-ExtB", "Minion", "Minion Pro", "Miriam", "Miriam Fixed", "Mistral", "Modern", "Modern No. 20", "Mona Lisa Solid ITC TT", "Monaco", "Mongolian Baiti",
+ "MONO", "Monotype Corsiva", "MoolBoran", "Mrs Eaves", "MS Gothic", "MS LineDraw", "MS Mincho", "MS Outlook", "MS PGothic", "MS PMincho", "MS Reference Sans Serif", "MS Reference Specialty", "MS Sans Serif", "MS Serif", "MS UI Gothic", "MT Extra", "MUSEO", "MV Boli", "MYRIAD",
+ "MYRIAD PRO", "Nadeem", "Narkisim", "NEVIS", "News Gothic", "News GothicMT", "NewsGoth BT", "Niagara Engraved", "Niagara Solid", "Noteworthy", "NSimSun", "Nyala", "OCR A Extended", "Old Century", "Old English Text MT", "Onyx", "Onyx BT", "OPTIMA", "Oriya Sangam MN",
+ "OSAKA", "OzHandicraft BT", "Palace Script MT", "Palatino", "Palatino Linotype", "Papyrus", "Parchment", "Party LET", "Pegasus", "Perpetua", "Perpetua Titling MT", "PetitaBold", "Pickwick", "Plantagenet Cherokee", "Playbill", "PMingLiU", "PMingLiU-ExtB",
+ "Poor Richard", "Poster", "PosterBodoni BT", "PRINCETOWN LET", "Pristina", "PTBarnum BT", "Pythagoras", "Raavi", "Rage Italic", "Ravie", "Ribbon131 Bd BT", "Rockwell", "Rockwell Condensed", "Rockwell Extra Bold", "Rod", "Roman", "Sakkal Majalla",
+ "Santa Fe LET", "Savoye LET", "Sceptre", "Script", "Script MT Bold", "SCRIPTINA", "Segoe Print", "Segoe Script", "Segoe UI", "Segoe UI Light", "Segoe UI Semibold", "Segoe UI Symbol", "Serifa", "Serifa BT", "Serifa Th BT", "ShelleyVolante BT", "Sherwood",
+ "Shonar Bangla", "Showcard Gothic", "Shruti", "Signboard", "SILKSCREEN", "SimHei", "Simplified Arabic", "Simplified Arabic Fixed", "SimSun", "SimSun-ExtB", "Sinhala Sangam MN", "Sketch Rockwell", "Skia", "Small Fonts", "Snap ITC", "Snell Roundhand", "Socket",
+ "Souvenir Lt BT", "Staccato222 BT", "Steamer", "Stencil", "Storybook", "Styllo", "Subway", "Swis721 BlkEx BT", "Swiss911 XCm BT", "Sylfaen", "Synchro LET", "System", "Tahoma", "Tamil Sangam MN", "Technical", "Teletype", "Telugu Sangam MN", "Tempus Sans ITC",
+ "Terminal", "Thonburi", "Times", "Times New Roman", "Times New Roman PS", "Traditional Arabic", "Trajan", "TRAJAN PRO", "Trebuchet MS", "Tristan", "Tubular", "Tunga", "Tw Cen MT", "Tw Cen MT Condensed", "Tw Cen MT Condensed Extra Bold",
+ "TypoUpright BT", "Unicorn", "Univers", "Univers CE 55 Medium", "Univers Condensed", "Utsaah", "Vagabond", "Vani", "Verdana", "Vijaya", "Viner Hand ITC", "VisualUI", "Vivaldi", "Vladimir Script", "Vrinda", "Westminster", "WHITNEY", "Wide Latin", "Wingdings",
+ "Wingdings 2", "Wingdings 3", "ZapfEllipt BT", "ZapfHumnst BT", "ZapfHumnst Dm BT", "Zapfino", "Zurich BlkEx BT", "Zurich Ex BT", "ZWAdobeF"];
+ var available = [];
+ for (var i = 0, l = fontList.length; i < l; i++) {
+ if(detect(fontList[i])) {
+ available.push(fontList[i]);
+ }
+ }
+ return available;
+}
+
+let fonts = getFonts();
+console.log("detected fonts:", fonts);
+</script>
+</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..9d8ec19956
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/head.js
@@ -0,0 +1,227 @@
+// 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 countDifferencesInUint8Arrays(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 countDifferencesInArrayBuffers(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);
+
+ let differences = 0;
+ // compare the byte values of the two typed arrays
+ for (let i = 0; i < buffer1.byteLength; i++) {
+ if (view1.getUint8(i) !== view2.getUint8(i)) {
+ differences += 1;
+ }
+ }
+
+ // the two buffers are the same
+ return differences;
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ };
+ Services.obs.addObserver(obs, topic);
+ });
+}
+
+/**
+ *
+ * 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,
+ });
+ });
+ });
+}
+
+const TEST_FIRST_PARTY_CONTEXT_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.net"
+ ) + "testPage.html";
+const TEST_THIRD_PARTY_CONTEXT_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "scriptExecPage.html";
+
+/**
+ * Executes provided callbacks in both first and third party contexts.
+ *
+ * @param {string} name - Name of the test.
+ * @param {Function} firstPartyCallback - The callback to be executed in the first-party context.
+ * @param {Function} thirdPartyCallback - The callback to be executed in the third-party context.
+ * @param {Function} [cleanupFunction] - A cleanup function to be called after the tests.
+ * @param {Array} [extraPrefs] - Optional. An array of preferences to be set before running the test.
+ */
+function runTestInFirstAndThirdPartyContexts(
+ name,
+ firstPartyCallback,
+ thirdPartyCallback,
+ cleanupFunction,
+ extraPrefs
+) {
+ add_task(async _ => {
+ info("Starting test `" + name + "' in first and third party contexts");
+
+ await SpecialPowers.flushPrefEnv();
+
+ if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) {
+ await SpecialPowers.pushPrefEnv({ set: extraPrefs });
+ }
+
+ info("Creating a new tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_FIRST_PARTY_CONTEXT_PAGE
+ );
+
+ info("Creating a 3rd party content and a 1st party popup");
+ let firstPartyPopupPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_THIRD_PARTY_CONTEXT_PAGE,
+ true
+ );
+
+ let thirdPartyBC = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_THIRD_PARTY_CONTEXT_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ content.open(url);
+
+ return ifr.browsingContext;
+ }
+ );
+ let firstPartyTab = await firstPartyPopupPromise;
+ let firstPartyBrowser = firstPartyTab.linkedBrowser;
+
+ info("Sending code to the 3rd party content and the 1st party popup");
+ let runningCallbackTask = async obj => {
+ return new content.Promise(resolve => {
+ content.postMessage({ callback: obj.callback }, "*");
+
+ content.addEventListener("message", function msg(event) {
+ // This is the event from above postMessage.
+ if (event.data.callback) {
+ return;
+ }
+
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ });
+ };
+
+ let firstPartyTask = SpecialPowers.spawn(
+ firstPartyBrowser,
+ [
+ {
+ callback: firstPartyCallback.toString(),
+ },
+ ],
+ runningCallbackTask
+ );
+
+ let thirdPartyTask = SpecialPowers.spawn(
+ thirdPartyBC,
+ [
+ {
+ callback: thirdPartyCallback.toString(),
+ },
+ ],
+ runningCallbackTask
+ );
+
+ await Promise.all([firstPartyTask, thirdPartyTask]);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(firstPartyTab);
+ });
+
+ add_task(async _ => {
+ info("Cleaning up.");
+ if (cleanupFunction) {
+ await cleanupFunction();
+ }
+ });
+}
diff --git a/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html b/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html
new file mode 100644
index 0000000000..324cc2f261
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html
@@ -0,0 +1,37 @@
+<html>
+<head>
+ <title>A content page for executing script!</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js"></script>
+</head>
+<body>
+<h1>Here the content!</h1>
+<script>
+
+function info(msg) {
+ window.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ window.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = async function(e) {
+ if (!e.data.callback) {
+ return;
+ }
+ let data = e.data.callback;
+ let runnableStr = `(() => {return (${data});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+
+ await runnable.call(this, this, window);
+
+ window.postMessage({ type: "finish" }, "*");
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js b/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js
new file mode 100644
index 0000000000..e9ee00cbf8
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js
@@ -0,0 +1,15 @@
+self.addEventListener("message", async e => {
+ let res = {};
+
+ switch (e.data.type) {
+ case "GetHWConcurrency":
+ res.result = "OK";
+ res.value = navigator.hardwareConcurrency;
+ break;
+
+ default:
+ res.result = "ERROR";
+ }
+
+ e.source.postMessage(res);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js b/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js
new file mode 100644
index 0000000000..3feb5dfc11
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js
@@ -0,0 +1,30 @@
+async function registerServiceWorker(win, url) {
+ let reg = await win.navigator.serviceWorker.register(url);
+ if (reg.installing.state !== "activated") {
+ await new Promise(resolve => {
+ let w = reg.installing;
+ w.addEventListener("statechange", function onStateChange() {
+ if (w.state === "activated") {
+ w.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ }
+
+ return reg.active;
+}
+
+function sendAndWaitWorkerMessage(target, worker, message) {
+ return new Promise(resolve => {
+ worker.addEventListener(
+ "message",
+ msg => {
+ resolve(msg.data);
+ },
+ { once: true }
+ );
+
+ target.postMessage(message);
+ });
+}
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/chrome.toml b/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..6ccea5d1e0
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+
+["test_spoof_english.html"]
diff --git a/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html b/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html
new file mode 100644
index 0000000000..750743ba01
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html
@@ -0,0 +1,117 @@
+<!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, content */
+
+const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+);
+
+const gOrigAvailableLocales = Services.locale.availableLocales;
+const gOrigRequestedLocales = Services.locale.requestedLocales;
+
+const kDoNotSpoof = 1;
+const kSpoofEnglish = 2;
+
+async function runTest(locale) {
+ return BrowserTestUtils.withNewTab("https://example.com/", browser => {
+ return SpecialPowers.spawn(browser, [locale], _locale => {
+ let locale = JSON.stringify(_locale);
+ return content.eval(`({
+ 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],
+ ],
+ });
+
+ 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-DE");
+ is(deResults.locale, "de-DE", "default locale");
+
+ // 3. set system locale to German
+ await setupLocale("de-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>
diff --git a/toolkit/components/resistfingerprinting/tests/gtest/moz.build b/toolkit/components/resistfingerprinting/tests/gtest/moz.build
new file mode 100644
index 0000000000..dcc6a7e12e
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/gtest/moz.build
@@ -0,0 +1,5 @@
+UNIFIED_SOURCES += [
+ "test_reduceprecision.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp
new file mode 100644
index 0000000000..5d8b598ff0
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp
@@ -0,0 +1,566 @@
+/* -*- 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.
+
+ Specifically: 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 probably
+ 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.
+ Good luck and godspeed.
+*/
+// 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/moz.build b/toolkit/components/resistfingerprinting/tests/moz.build
new file mode 100644
index 0000000000..3a6ca329ff
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/moz.build
@@ -0,0 +1,9 @@
+MOCHITEST_CHROME_MANIFESTS += [
+ "chrome/chrome.toml",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser/browser.toml",
+]
+
+TEST_DIRS += ["gtest"]