From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../FingerprintingWebCompatService.sys.mjs | 242 +++ .../resistfingerprinting/KeyCodeConsensus_En_US.h | 173 ++ .../resistfingerprinting/RFPHelper.sys.mjs | 630 ++++++ .../resistfingerprinting/RFPTargetIPCUtils.h | 23 + .../components/resistfingerprinting/RFPTargets.inc | 122 ++ .../resistfingerprinting/RelativeTimeline.cpp | 33 + .../resistfingerprinting/RelativeTimeline.h | 25 + .../resistfingerprinting/components.conf | 37 + .../resistfingerprinting/docs/implementation.rst | 78 + .../components/resistfingerprinting/docs/index.rst | 8 + .../components/resistfingerprinting/metrics.yaml | 27 + toolkit/components/resistfingerprinting/moz.build | 43 + .../nsIFingerprintingWebCompatService.idl | 46 + .../resistfingerprinting/nsIRFPService.idl | 80 + .../resistfingerprinting/nsRFPService.cpp | 2137 ++++++++++++++++++++ .../components/resistfingerprinting/nsRFPService.h | 467 +++++ .../tests/browser/browser.toml | 33 + .../browser_canvas_fingerprinter_telemetry.js | 111 + .../tests/browser/browser_canvas_randomization.js | 658 ++++++ .../browser/browser_canvas_randomization_worker.js | 335 +++ .../browser_fingerprintingRemoteOverrides.js | 406 ++++ .../browser/browser_fingerprintingWebCompat.js | 559 +++++ .../browser_fingerprinting_randomization_key.js | 490 +++++ .../browser_font_fingerprinter_telemetry.js | 97 + .../browser_fpiServiceWorkers_fingerprinting.js | 85 + .../browser/browser_rfp_canvasplaceholder_pdfjs.js | 52 + ...owser_serviceWorker_fingerprinting_webcompat.js | 172 ++ .../tests/browser/canvas-fingerprinter.html | 22 + .../resistfingerprinting/tests/browser/empty.html | 8 + .../tests/browser/file_pdf.pdf | 12 + .../tests/browser/font-fingerprinter.html | 102 + .../resistfingerprinting/tests/browser/head.js | 227 +++ .../tests/browser/scriptExecPage.html | 37 + .../tests/browser/serviceWorker.js | 15 + .../tests/browser/testHelpers.js | 30 + .../tests/browser/testPage.html | 9 + .../resistfingerprinting/tests/browser/worker.js | 7 + .../resistfingerprinting/tests/chrome/chrome.toml | 4 + .../tests/chrome/test_spoof_english.html | 117 ++ .../resistfingerprinting/tests/gtest/moz.build | 5 + .../tests/gtest/test_reduceprecision.cpp | 566 ++++++ .../resistfingerprinting/tests/moz.build | 9 + 42 files changed, 8339 insertions(+) create mode 100644 toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs create mode 100644 toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h create mode 100644 toolkit/components/resistfingerprinting/RFPHelper.sys.mjs create mode 100644 toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h create mode 100644 toolkit/components/resistfingerprinting/RFPTargets.inc create mode 100644 toolkit/components/resistfingerprinting/RelativeTimeline.cpp create mode 100644 toolkit/components/resistfingerprinting/RelativeTimeline.h create mode 100644 toolkit/components/resistfingerprinting/components.conf create mode 100644 toolkit/components/resistfingerprinting/docs/implementation.rst create mode 100644 toolkit/components/resistfingerprinting/docs/index.rst create mode 100644 toolkit/components/resistfingerprinting/metrics.yaml create mode 100644 toolkit/components/resistfingerprinting/moz.build create mode 100644 toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl create mode 100644 toolkit/components/resistfingerprinting/nsIRFPService.idl create mode 100644 toolkit/components/resistfingerprinting/nsRFPService.cpp create mode 100644 toolkit/components/resistfingerprinting/nsRFPService.h create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser.toml create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/empty.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf create mode 100644 toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/head.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/testHelpers.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/testPage.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/worker.js create mode 100644 toolkit/components/resistfingerprinting/tests/chrome/chrome.toml create mode 100644 toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html create mode 100644 toolkit/components/resistfingerprinting/tests/gtest/moz.build create mode 100644 toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp create mode 100644 toolkit/components/resistfingerprinting/tests/moz.build (limited to 'toolkit/components/resistfingerprinting') 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}(? { + 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 + : BitFlagsEnumSerializer {}; + +} // 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 ? +// (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 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 + +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 `_ 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()
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*)
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*)
ETPSaysShouldNotResistFingerprinting Check
CookieJarSettingsSaysShouldResistFingerprinting Check"] + click SRFP_channel href "https://searchfox.org/mozilla-central/search?q=symbol:_ZN14nsContentUtils26ShouldResistFingerprintingEP10nsIChannelN7mozilla9RFPTargetE&redirect=false" + + SRFP_uri["ShouldResistFingerprinting_dangerous(nsIURI*, OriginAttributes)
PBM Check
Scheme (inc WebExtension) Check
About Page Check
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*)
System Principal Check
PBM Check
Scheme Check
About Page Check
Web Extension Principal Check
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 `_). 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 `_. 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 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 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 sRFPService; +static bool sInitialized = false; + +// Actually enabled fingerprinting protections. +static Atomic sEnabledFingerprintingProtections; + +/* static */ +already_AddRefed 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 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& 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 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 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(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 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 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 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* + 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(); + } + + 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 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> nsRFPService::GenerateKey(nsIChannel* aChannel) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aChannel); + +#ifdef DEBUG + // Ensure we only compute random key for top-level loads. + { + nsCOMPtr loadInfo = aChannel->LoadInfo(); + MOZ_ASSERT(loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT); + } +#endif + + nsCOMPtr 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 service = GetOrCreate(); + + nsCOMPtr 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(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(topLevelSite.get()), + topLevelSite.Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + Maybe> 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 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 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 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 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 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& aKey) { + MOZ_ASSERT(XRE_IsParentProcess()); + NS_ENSURE_ARG_POINTER(aChannel); + + Maybe> 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& aCanvasKey) { + NS_ENSURE_ARG_POINTER(aCookieJarSettings); + + nsTArray 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(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 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(canvasKey.Elements()), + *reinterpret_cast(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(canvasKey.Elements() + 16), + *reinterpret_cast(canvasKey.Elements() + 24)); + + // Ensure at least 20 random changes may occur. + uint8_t numNoises = std::clamp(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 ""; +} + +static void MaybeCurrentCaller(nsACString& aFilename, uint32_t& aLineNum, + uint32_t& aColumnNum) { + aFilename.AssignLiteral(""); + + 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& 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 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()) + : "")); + } + + 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& 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 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>& 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 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 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 nsRFPService::GetOverriddenFingerprintingSettingsForChannel( + nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr uri; + Unused << aChannel->GetURI(getter_AddRefs(uri)); + + if (uri->SchemeIs("about") && !NS_IsContentAccessibleAboutURI(uri)) { + return Nothing(); + } + + nsCOMPtr loadInfo = aChannel->LoadInfo(); + MOZ_ASSERT(loadInfo); + + RefPtr 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 topWGP = + bc->Top()->Canonical()->GetCurrentWindowGlobal(); + + if (NS_WARN_IF(!topWGP)) { + return Nothing(); + } + + nsCOMPtr 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 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 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 nsRFPService::GetOverriddenFingerprintingSettingsForURI( + nsIURI* aFirstPartyURI, nsIURI* aThirdPartyURI) { + MOZ_ASSERT(aFirstPartyURI); + MOZ_ASSERT(XRE_IsParentProcess()); + + RefPtr 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 result = service->mFingerprintingOverrides.MaybeGet("*"_ns); + + RefPtr 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 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 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 +#include +#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(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 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& 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> 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 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 GetOverriddenFingerprintingSettingsForURI( + nsIURI* aFirstPartyURI, nsIURI* aThirdPartyURI); + + // -------------------------------------------------------------------------- + + static void MaybeReportCanvasFingerprinter(nsTArray& 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& 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 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* + 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& 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 mBrowsingSessionKeys; + + nsCOMPtr mWebCompatService; + nsTHashMap 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 @@ + + + + A page containing a canvas fingerprinter + + + + + + + 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 @@ + + + + An empty page for testing + + + + 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<>endobj 2 0 obj<>endobj 3 0 obj<>endobj +xref +0 4 +0000000000 65535 f +0000000010 00000 n +0000000053 00000 n +0000000102 00000 n +trailer<> +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 @@ + + + + A page containing a font fingerprinter + + + + + 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 @@ + + + A content page for executing script! + + + +

Here the content!

+ + + 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 @@ + + + + A page with a javascript URL iframe + + + + + 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 @@ + + + + + + Test for Bug 1486258 + + + + + +Mozilla Bug 1486258 +

+ +
+
+ + + + + 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 + +#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 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 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"] -- cgit v1.2.3