summaryrefslogtreecommitdiffstats
path: root/toolkit/components/resistfingerprinting/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/resistfingerprinting/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests')
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser.toml33
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js111
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js658
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js335
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js406
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js559
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js490
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js97
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js85
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js52
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js172
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html22
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/empty.html8
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf12
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html102
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/head.js227
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html37
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js15
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/testHelpers.js30
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/testPage.html9
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/worker.js7
-rw-r--r--toolkit/components/resistfingerprinting/tests/chrome/chrome.toml4
-rw-r--r--toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html117
-rw-r--r--toolkit/components/resistfingerprinting/tests/gtest/moz.build5
-rw-r--r--toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp566
-rw-r--r--toolkit/components/resistfingerprinting/tests/moz.build9
26 files changed, 4168 insertions, 0 deletions
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.toml b/toolkit/components/resistfingerprinting/tests/browser/browser.toml
new file mode 100644
index 0000000000..213d6d4287
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files = [
+ "canvas-fingerprinter.html",
+ "empty.html",
+ "font-fingerprinter.html",
+ "head.js",
+ "scriptExecPage.html",
+ "serviceWorker.js",
+ "testHelpers.js",
+ "testPage.html",
+ "worker.js",
+]
+
+["browser_canvas_fingerprinter_telemetry.js"]
+
+["browser_canvas_randomization.js"]
+
+["browser_canvas_randomization_worker.js"]
+
+["browser_fingerprintingRemoteOverrides.js"]
+
+["browser_fingerprintingWebCompat.js"]
+
+["browser_fingerprinting_randomization_key.js"]
+
+["browser_font_fingerprinter_telemetry.js"]
+
+["browser_fpiServiceWorkers_fingerprinting.js"]
+
+["browser_rfp_canvasplaceholder_pdfjs.js"]
+support-files = ["file_pdf.pdf"]
+
+["browser_serviceWorker_fingerprinting_webcompat.js"]
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js
new file mode 100644
index 0000000000..d2a33a4347
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js
@@ -0,0 +1,111 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_PAGE_NORMAL = TEST_PATH + "empty.html";
+const TEST_PAGE_FINGERPRINTER = TEST_PATH + "canvas-fingerprinter.html";
+
+const TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB = "CANVAS_FINGERPRINTING_PER_TAB";
+
+const KEY_UNKNOWN = "unknown";
+const KEY_KNOWN_TEXT = "known_text";
+
+async function clearTelemetry() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry
+ .getKeyedHistogramById(TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB)
+ .clear();
+}
+
+async function getKeyedHistogram(histogram_id, key, bucket, checkCntFn) {
+ let histogram;
+
+ // Wait until the telemetry probe appears.
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ histogram = histograms[histogram_id];
+
+ let checkRes = false;
+
+ if (histogram && histogram[key]) {
+ checkRes = checkCntFn ? checkCntFn(histogram[key].values[bucket]) : true;
+ }
+
+ return checkRes;
+ });
+
+ return histogram[key].values[bucket] || 0;
+}
+
+async function checkKeyedHistogram(histogram_id, key, bucket, expectedCnt) {
+ let cnt = await getKeyedHistogram(histogram_id, key, bucket, cnt => {
+ if (cnt === undefined) {
+ cnt = 0;
+ }
+
+ return cnt == expectedCnt;
+ });
+
+ is(cnt, expectedCnt, "There should be expected count in keyed telemetry.");
+}
+
+add_setup(async function () {
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // First, we open a page without any canvas fingerprinters
+ await BrowserTestUtils.withNewTab(TEST_PAGE_NORMAL, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // Check that the telemetry has been record properly for normal page. The
+ // telemetry should show there was no known fingerprinting attempt.
+ await checkKeyedHistogram(
+ TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB,
+ KEY_UNKNOWN,
+ 0,
+ 1
+ );
+
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // Now open a page with a canvas fingerprinter
+ await BrowserTestUtils.withNewTab(TEST_PAGE_FINGERPRINTER, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // The telemetry should show a a known fingerprinting text and a known
+ // canvas fingerprinter.
+ await checkKeyedHistogram(
+ TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB,
+ KEY_KNOWN_TEXT,
+ 6, // CanvasFingerprinter::eVariant4
+ 1
+ );
+
+ await clearTelemetry();
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
new file mode 100644
index 0000000000..8b4be78d53
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
@@ -0,0 +1,658 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 1816189 - Testing canvas randomization on canvas data extraction.
+ *
+ * In the test, we create canvas elements and offscreen canvas and test if
+ * the extracted canvas data is altered because of the canvas randomization.
+ */
+
+const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+var TEST_CASES = [
+ {
+ name: "CanvasRenderingContext2D.getImageData() but constant color",
+ shouldBeRandomized: false,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData().",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a 2d context",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a webgl context",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const bitmapCanvas = document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ const dataURL = bitmapCanvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = bitmapCanvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a 2d context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ let data = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a webgl context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ let data = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // We don't get the consistent blob data on second access with webgl
+ // context regardless of the canvas randomization. So, we report the
+ // same data here to not fail the test. Ideally, we should look into
+ // why this happens, but it's not caused by canvas randomization.
+
+ return [data, data];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const bitmapCanvas = document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let data = await new Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a 2d context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ let blob = await offscreenCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ let blob = await offscreenCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const bitmapCanvas = new OffscreenCanvas(100, 100);
+
+ let bitmap = await createImageBitmap(offscreenCanvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let blob = await bitmapCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await bitmapCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+];
+
+function runExtractCanvasData(test, tab) {
+ let code = test.extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async code => {
+ let result = await content.eval(`({${code}}).extractCanvasData()`);
+ return result;
+ });
+}
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ["privacy.fingerprintingProtection.overrides", RFPOverrides],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a private window.
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open tabs in the normal and private window.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ const privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ emptyPage
+ );
+
+ for (let test of TEST_CASES) {
+ info(`Testing ${test.name}`);
+
+ // Clear telemetry before starting test.
+ Services.fog.testResetFOG();
+
+ let data = await runExtractCanvasData(test, tab);
+ let result = test.isDataRandomized(test.name, data[0], test.originalData);
+
+ if (test.shouldBeRandomized) {
+ is(
+ result,
+ enabled,
+ `The image data is ${enabled ? "randomized" : "the same"} for ${
+ test.name
+ }.`
+ );
+ } else {
+ is(
+ result,
+ false,
+ `The image data for ${test.name} should never be randomized.`
+ );
+ }
+
+ ok(
+ !test.isDataRandomized(test.name, data[0], data[1]),
+ `The data of first and second access should be the same for ${test.name}.`
+ );
+
+ let privateData = await runExtractCanvasData(test, privateTab);
+
+ // Check if we add noise to canvas data in private windows.
+ result = test.isDataRandomized(
+ test.name,
+ privateData[0],
+ test.originalData,
+ true
+ );
+ if (test.shouldBeRandomized) {
+ is(
+ result,
+ enabled,
+ `The private image data is ${enabled ? "randomized" : "the same"} for ${
+ test.name
+ }.`
+ );
+ } else {
+ is(
+ result,
+ false,
+ `The image data for ${test.name} should never be randomized.`
+ );
+ }
+
+ ok(
+ !test.isDataRandomized(test.name, privateData[0], privateData[1]),
+ "The data of first and second access should be the same for private windows."
+ );
+
+ if (test.shouldBeRandomized) {
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(test.name, privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ } for ${test.name}.`
+ );
+
+ // Verify the telemetry is recorded if canvas randomization is enabled.
+ if (enabled) {
+ await Services.fog.testFlushAllChildren();
+
+ Assert.greater(
+ Glean.fingerprintingProtection.canvasNoiseCalculateTime.testGetValue()
+ .sum,
+ 0,
+ "The telemetry of canvas randomization is recorded."
+ );
+ }
+ }
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ // Extract the original canvas data without random noise.
+ for (let test of TEST_CASES) {
+ let data = await runExtractCanvasData(test, tab);
+ test.originalData = data[0];
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function run_tests_with_randomization_enabled() {
+ await runTest(true);
+});
+
+add_task(async function run_tests_with_randomization_disabled() {
+ await runTest(false);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js
new file mode 100644
index 0000000000..dd7c292fa4
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js
@@ -0,0 +1,335 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+/**
+ * Bug 1816189 - Testing canvas randomization on canvas data extraction in
+ * workers.
+ *
+ * In the test, we create offscreen canvas in workers and test if the extracted
+ * canvas data is altered because of the canvas randomization.
+ */
+
+var TEST_CASES = [
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a 2d context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ let blob = await offscreenCanvas.convertToBlob();
+
+ let data = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ let blobSecond = await offscreenCanvas.convertToBlob();
+
+ let dataSecond = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ let blob = await offscreenCanvas.convertToBlob();
+
+ let data = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ let blobSecond = await offscreenCanvas.convertToBlob();
+
+ let dataSecond = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const bitmapCanvas = new OffscreenCanvas(100, 100);
+
+ let bitmap = await createImageBitmap(offscreenCanvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let blob = await bitmapCanvas.convertToBlob();
+
+ let data = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ let blobSecond = await bitmapCanvas.convertToBlob();
+
+ let dataSecond = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+];
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ["privacy.fingerprintingProtection.overrides", RFPOverrides],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a private window.
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open tabs in the normal and private window.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ const privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ emptyPage
+ );
+
+ for (let test of TEST_CASES) {
+ info(`Testing ${test.name} in the worker`);
+
+ // Clear telemetry before starting test.
+ Services.fog.testResetFOG();
+
+ let data = await await runFunctionInWorker(
+ tab.linkedBrowser,
+ test.extractCanvasData
+ );
+
+ let result = test.isDataRandomized(test.name, data[0], test.originalData);
+ is(
+ result,
+ enabled,
+ `The image data for for '${test.name}' is ${
+ enabled ? "randomized" : "the same"
+ }.`
+ );
+
+ ok(
+ !test.isDataRandomized(test.name, data[0], data[1]),
+ `The data for '${test.name}' of first and second access should be the same.`
+ );
+
+ let privateData = await await runFunctionInWorker(
+ privateTab.linkedBrowser,
+ test.extractCanvasData
+ );
+
+ // Check if we add noise to canvas data in private windows.
+ result = test.isDataRandomized(
+ test.name,
+ privateData[0],
+ test.originalData,
+ true
+ );
+ is(
+ result,
+ enabled,
+ `The private image data for '${test.name}' is ${
+ enabled ? "randomized" : "the same"
+ }.`
+ );
+
+ ok(
+ !test.isDataRandomized(test.name, privateData[0], privateData[1]),
+ `"The data for '${test.name}' of first and second access should be the same.`
+ );
+
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(test.name, privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data for '${
+ test.name
+ }' between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ }.`
+ );
+ }
+
+ // Verify the telemetry is recorded if canvas randomization is enabled.
+ if (enabled) {
+ await Services.fog.testFlushAllChildren();
+
+ Assert.greater(
+ Glean.fingerprintingProtection.canvasNoiseCalculateTime.testGetValue()
+ .sum,
+ 0,
+ "The telemetry of canvas randomization is recorded."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a tab for extracting the canvas data in the worker.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ // Extract the original canvas data without random noise.
+ for (let test of TEST_CASES) {
+ let data = await runFunctionInWorker(
+ tab.linkedBrowser,
+ test.extractCanvasData
+ );
+ test.originalData = data[0];
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function run_tests_with_randomization_enabled() {
+ await runTest(true);
+});
+
+add_task(async function run_tests_with_randomization_disabled() {
+ await runTest(false);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js
new file mode 100644
index 0000000000..29c7c9170b
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js
@@ -0,0 +1,406 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const COLLECTION_NAME = "fingerprinting-protection-overrides";
+
+// The javascript bitwise operator only support 32bits. So, we only test
+// RFPTargets that is under low 32 bits.
+const TARGET_DEFAULT = extractLow32Bits(
+ Services.rfp.enabledFingerprintingProtections
+);
+const TARGET_PointerEvents = 0x00000002;
+const TARGET_CanvasRandomization = 0x000000100;
+const TARGET_WindowOuterSize = 0x002000000;
+const TARGET_Gamepad = 0x00800000;
+
+// A helper function to filter high 32 bits.
+function extractLow32Bits(value) {
+ return value & 0xffffffff;
+}
+
+const TEST_CASES = [
+ // Test simple addition.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides: TARGET_DEFAULT | TARGET_WindowOuterSize,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test simple subtraction
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-WindowOuterSize",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides: TARGET_DEFAULT & ~TARGET_WindowOuterSize,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test simple subtraction for default targets.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-CanvasRandomization",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides: TARGET_DEFAULT & ~TARGET_CanvasRandomization,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test multiple targets
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ { domain: "example.com", noEntry: true },
+ ],
+ },
+ // Test multiple entries
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "example.org",
+ },
+ {
+ id: "2",
+ last_modified: 1000000000000001,
+ overrides: "+Gamepad",
+ firstPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ {
+ domain: "example.com",
+ overrides: TARGET_DEFAULT | TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an entry with both first-party and third-party domains.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "example.org",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org,example.com",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an entry with the first-party set to a wildcard.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "*",
+ },
+ ],
+ expects: [
+ {
+ domain: "*",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test a entry with the first-party set to a wildcard and the third-party set
+ // to a domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "*,example.com",
+ overrides:
+ (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) &
+ ~TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an entry with all targets disabled using '-AllTargets' but only enable one target.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-AllTargets,+WindowOuterSize",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "*,example.com",
+ overrides: TARGET_WindowOuterSize,
+ },
+ ],
+ },
+ // Test an entry with all targets disabled using '-AllTargets' but enable some targets.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-AllTargets,+WindowOuterSize,+Gamepad",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "*,example.com",
+ overrides: TARGET_WindowOuterSize | TARGET_Gamepad,
+ },
+ ],
+ },
+ // Test an invalid entry with only third party domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.com",
+ noEntry: true,
+ },
+ ],
+ },
+ // Test an invalid entry with both wildcards.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize,+PointerEvents,-Gamepad",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "*",
+ },
+ ],
+ expects: [
+ {
+ domain: "example.org",
+ noEntry: true,
+ },
+ ],
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.fingerprintingProtection.remoteOverrides.testing", true]],
+ });
+
+ registerCleanupFunction(() => {
+ Services.rfp.cleanAllOverrides();
+ });
+});
+
+add_task(async function test_remote_settings() {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ for (let test of TEST_CASES) {
+ info(`Testing with entry ${JSON.stringify(test.entires)}`);
+
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by a remote settings sync.
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: test.entires,
+ },
+ });
+ await promise;
+
+ ok(true, "Got overrides update");
+
+ for (let expect of test.expects) {
+ // Get the addition and subtraction flags for the domain.
+ try {
+ let overrides = extractLow32Bits(
+ Services.rfp.getFingerprintingOverrides(expect.domain)
+ );
+
+ // Verify if the flags are matching to expected values.
+ is(overrides, expect.overrides, "The override value is correct.");
+ } catch (e) {
+ ok(expect.noEntry, "The override entry doesn't exist.");
+ }
+ }
+ }
+
+ db.clear();
+});
+
+add_task(async function test_pref() {
+ for (let test of TEST_CASES) {
+ info(`Testing with entry ${JSON.stringify(test.entires)}`);
+
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by setting the pref.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify(test.entires),
+ ],
+ ],
+ });
+
+ await promise;
+ ok(true, "Got overrides update");
+
+ for (let expect of test.expects) {
+ try {
+ // Get the addition and subtraction flags for the domain.
+ let overrides = extractLow32Bits(
+ Services.rfp.getFingerprintingOverrides(expect.domain)
+ );
+
+ // Verify if the flags are matching to expected values.
+ is(overrides, expect.overrides, "The override value is correct.");
+ } catch (e) {
+ ok(expect.noEntry, "The override entry doesn't exist.");
+ }
+ }
+ }
+});
+
+// Verify if the pref overrides the remote settings.
+add_task(async function test_pref_override_remote_settings() {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ // Trigger a remote settings sync.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.org",
+ },
+ ],
+ },
+ });
+ await promise;
+
+ // Then, setting the pref.
+ promise = promiseObserver("fpp-test:set-overrides-finishes");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify([
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+PointerEvents,-WindowOuterSize,-Gamepad",
+ firstPartyDomain: "example.org",
+ },
+ ]),
+ ],
+ ],
+ });
+ await promise;
+
+ // Get the addition and subtraction flags for the domain.
+ let overrides = extractLow32Bits(
+ Services.rfp.getFingerprintingOverrides("example.org")
+ );
+
+ // Verify if the flags are matching to the pref settings.
+ is(
+ overrides,
+ (TARGET_DEFAULT | TARGET_PointerEvents) &
+ ~TARGET_Gamepad &
+ ~TARGET_WindowOuterSize,
+ "The override addition value is correct."
+ );
+
+ db.clear();
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js
new file mode 100644
index 0000000000..1a882fc63d
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js
@@ -0,0 +1,559 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const COLLECTION_NAME = "fingerprinting-protection-overrides";
+
+const TOP_DOMAIN = "https://example.com";
+const THIRD_PARTY_DOMAIN = "https://example.org";
+
+const SPOOFED_HW_CONCURRENCY = 2;
+const DEFAULT_HW_CONCURRENCY = navigator.hardwareConcurrency;
+
+const TEST_TOP_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TOP_DOMAIN
+ ) + "empty.html";
+const TEST_THIRD_PARTY_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ THIRD_PARTY_DOMAIN
+ ) + "empty.html";
+
+const TEST_CASES = [
+ // Test the wildcard entry.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "*",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that only applies to the first-party context.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that applies to every context under the first-party domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "*",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that applies to the specific third-party domain under the first-party domain.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test the entry that applies to the every third-party domain under any first-party domains.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "*",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: true,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test a entry that doesn't apply to the current contexts. The first-party
+ // domain is different.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.net",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test a entry that doesn't apply to the current contexts. The first-party
+ // domain is different.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "*",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test a entry that doesn't apply to a the current contexts. Both first-party
+ // and third-party domains are different.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "example.com",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: false,
+ },
+ },
+ },
+ // Test multiple entries that enable HW concurrency in the first-party context
+ // and WindowOuter in the third-party context.
+ {
+ entires: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ },
+ {
+ id: "2",
+ last_modified: 1000000000000001,
+ overrides: "+NavigatorHWConcurrency",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ expects: {
+ windowOuter: {
+ top: true,
+ firstParty: true,
+ thirdParty: false,
+ },
+ hwConcurrency: {
+ top: false,
+ firstParty: false,
+ thirdParty: true,
+ },
+ },
+ },
+];
+
+async function openAndSetupTestPage() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ // Create a first-party and a third-party iframe in the tab.
+ let { firstPartyBC, thirdPartyBC } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_TOP_PAGE, TEST_THIRD_PARTY_PAGE],
+ async (firstPartySrc, thirdPartySrc) => {
+ let firstPartyFrame = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ firstPartyFrame.onload = resolve;
+ });
+ content.document.body.appendChild(firstPartyFrame);
+ firstPartyFrame.src = firstPartySrc;
+ await loading;
+
+ let thirdPartyFrame = content.document.createElement("iframe");
+ loading = new content.Promise(resolve => {
+ thirdPartyFrame.onload = resolve;
+ });
+ content.document.body.appendChild(thirdPartyFrame);
+ thirdPartyFrame.src = thirdPartySrc;
+ await loading;
+
+ return {
+ firstPartyBC: firstPartyFrame.browsingContext,
+ thirdPartyBC: thirdPartyFrame.browsingContext,
+ };
+ }
+ );
+
+ return { tab, firstPartyBC, thirdPartyBC };
+}
+
+async function openAndSetupTestPageForPopup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ // Create a third-party iframe and a pop-up from this iframe.
+ let { thirdPartyFrameBC, popupBC } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_THIRD_PARTY_PAGE],
+ async thirdPartySrc => {
+ let thirdPartyFrame = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ thirdPartyFrame.onload = resolve;
+ });
+ content.document.body.appendChild(thirdPartyFrame);
+ thirdPartyFrame.src = thirdPartySrc;
+ await loading;
+
+ let popupBC = await SpecialPowers.spawn(
+ thirdPartyFrame,
+ [thirdPartySrc],
+ async src => {
+ let win;
+ let loading = new content.Promise(resolve => {
+ win = content.open(src);
+ win.onload = resolve;
+ });
+ await loading;
+
+ return win.browsingContext;
+ }
+ );
+
+ return {
+ thirdPartyFrameBC: thirdPartyFrame.browsingContext,
+ popupBC,
+ };
+ }
+ );
+
+ return { tab, thirdPartyFrameBC, popupBC };
+}
+
+async function verifyResultInTab(tab, firstPartyBC, thirdPartyBC, expected) {
+ let testWindowOuter = enabled => {
+ if (enabled) {
+ ok(
+ content.wrappedJSObject.outerHeight ==
+ content.wrappedJSObject.innerHeight &&
+ content.wrappedJSObject.outerWidth ==
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is enabled for WindowOuterSize."
+ );
+ } else {
+ ok(
+ content.wrappedJSObject.outerHeight !=
+ content.wrappedJSObject.innerHeight ||
+ content.wrappedJSObject.outerWidth !=
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is not enabled for WindowOuterSize."
+ );
+ }
+ };
+
+ let testHWConcurrency = expected => {
+ is(
+ content.wrappedJSObject.navigator.hardwareConcurrency,
+ expected,
+ "The hardware concurrency is expected."
+ );
+ };
+
+ info(
+ `Verify top-level context with fingerprinting protection is ${
+ expected.top ? "enabled" : "not enabled"
+ }.`
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expected.windowOuter.top],
+ testWindowOuter
+ );
+ let expectHWConcurrencyTop = expected.hwConcurrency.top
+ ? SPOOFED_HW_CONCURRENCY
+ : DEFAULT_HW_CONCURRENCY;
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expectHWConcurrencyTop],
+ testHWConcurrency
+ );
+
+ let workerTopResult = await runFunctionInWorker(
+ tab.linkedBrowser,
+ async _ => {
+ return navigator.hardwareConcurrency;
+ }
+ );
+ is(
+ workerTopResult,
+ expectHWConcurrencyTop,
+ "The top worker reports the expected HW concurrency."
+ );
+
+ info(
+ `Verify first-party context with fingerprinting protection is ${
+ expected.firstParty ? "enabled" : "not enabled"
+ }.`
+ );
+ await SpecialPowers.spawn(
+ firstPartyBC,
+ [expected.windowOuter.firstParty],
+ testWindowOuter
+ );
+ let expectHWConcurrencyFirstParty = expected.hwConcurrency.firstParty
+ ? SPOOFED_HW_CONCURRENCY
+ : DEFAULT_HW_CONCURRENCY;
+ await SpecialPowers.spawn(
+ firstPartyBC,
+ [expectHWConcurrencyFirstParty],
+ testHWConcurrency
+ );
+
+ let workerFirstPartyResult = await runFunctionInWorker(
+ firstPartyBC,
+ async _ => {
+ return navigator.hardwareConcurrency;
+ }
+ );
+ is(
+ workerFirstPartyResult,
+ expectHWConcurrencyFirstParty,
+ "The first-party worker reports the expected HW concurrency."
+ );
+
+ info(
+ `Verify third-party context with fingerprinting protection is ${
+ expected.thirdParty ? "enabled" : "not enabled"
+ }.`
+ );
+ await SpecialPowers.spawn(
+ thirdPartyBC,
+ [expected.windowOuter.thirdParty],
+ testWindowOuter
+ );
+ let expectHWConcurrencyThirdParty = expected.hwConcurrency.thirdParty
+ ? SPOOFED_HW_CONCURRENCY
+ : DEFAULT_HW_CONCURRENCY;
+ await SpecialPowers.spawn(
+ thirdPartyBC,
+ [expectHWConcurrencyThirdParty],
+ testHWConcurrency
+ );
+
+ let workerThirdPartyResult = await runFunctionInWorker(
+ thirdPartyBC,
+ async _ => {
+ return navigator.hardwareConcurrency;
+ }
+ );
+ is(
+ workerThirdPartyResult,
+ expectHWConcurrencyThirdParty,
+ "The third-party worker reports the expected HW concurrency."
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection.remoteOverrides.testing", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ],
+ });
+
+ registerCleanupFunction(() => {
+ Services.rfp.cleanAllOverrides();
+ });
+});
+
+add_task(async function () {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ for (let test of TEST_CASES) {
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by a remote settings sync.
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: test.entires,
+ },
+ });
+ await promise;
+
+ ok(true, "Got overrides update");
+
+ let { tab, firstPartyBC, thirdPartyBC } = await openAndSetupTestPage();
+
+ await verifyResultInTab(tab, firstPartyBC, thirdPartyBC, test.expects);
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ db.clear();
+});
+
+// A test to verify that a pop-up inherits overrides from the third-party iframe
+// that opens it.
+add_task(async function test_popup_inheritance() {
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ // Create a promise for waiting the overrides get updated.
+ let promise = promiseObserver("fpp-test:set-overrides-finishes");
+
+ // Trigger the fingerprinting overrides update by a remote settings sync.
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "+WindowOuterSize",
+ firstPartyDomain: "example.com",
+ thirdPartyDomain: "example.org",
+ },
+ ],
+ },
+ });
+ await promise;
+
+ let { tab, thirdPartyFrameBC, popupBC } =
+ await openAndSetupTestPageForPopup();
+
+ // Ensure the third-party iframe has the correct overrides.
+ await SpecialPowers.spawn(thirdPartyFrameBC, [], _ => {
+ ok(
+ content.wrappedJSObject.outerHeight ==
+ content.wrappedJSObject.innerHeight &&
+ content.wrappedJSObject.outerWidth ==
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is enabled for third-party iframe."
+ );
+ });
+
+ // Verify the popup inherits overrides from the opener.
+ await SpecialPowers.spawn(popupBC, [], _ => {
+ ok(
+ content.wrappedJSObject.outerHeight ==
+ content.wrappedJSObject.innerHeight &&
+ content.wrappedJSObject.outerWidth ==
+ content.wrappedJSObject.innerWidth,
+ "Fingerprinting target WindowOuterSize is enabled for the pop-up."
+ );
+
+ content.close();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ db.clear();
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js
new file mode 100644
index 0000000000..f2d037491d
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js
@@ -0,0 +1,490 @@
+let { ForgetAboutSite } = ChromeUtils.importESModule(
+ "resource://gre/modules/ForgetAboutSite.sys.mjs"
+);
+
+requestLongerTimeout(2);
+
+const TEST_DOMAIN = "https://example.com";
+const TEST_DOMAIN_ANOTHER = "https://example.org";
+const TEST_DOMAIN_THIRD = "https://example.net";
+
+const TEST_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_DOMAIN
+ ) + "testPage.html";
+const TEST_DOMAIN_ANOTHER_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_DOMAIN_ANOTHER
+ ) + "testPage.html";
+const TEST_DOMAIN_THIRD_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_DOMAIN_THIRD
+ ) + "testPage.html";
+
+/**
+ * A helper function to get the random key in a hex string format and test if
+ * the random key works properly.
+ *
+ * @param {Browser} browser The browser element of the testing tab.
+ * @param {string} firstPartyDomain The first-party domain loaded on the tab
+ * @param {string} thirdPartyDomain The third-party domain to test
+ * @returns {string} The random key hex string
+ */
+async function getRandomKeyHexFromBrowser(
+ browser,
+ firstPartyDomain,
+ thirdPartyDomain
+) {
+ // Get the key from the cookieJarSettings of the browser element.
+ let key = browser.cookieJarSettings.fingerprintingRandomizationKey;
+ let keyHex = key.map(bytes => bytes.toString(16).padStart(2, "0")).join("");
+
+ // Get the key from the cookieJarSettings of the top-level document.
+ let keyTop = await SpecialPowers.spawn(browser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ let keyTopHex = keyTop
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+
+ is(
+ keyTopHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the top-level document."
+ );
+
+ // Get the key from the cookieJarSettings of an about:blank iframe.
+ let keyAboutBlank = await SpecialPowers.spawn(browser, [], async _ => {
+ let ifr = content.document.createElement("iframe");
+
+ let loaded = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = "about:blank";
+
+ await loaded;
+
+ return SpecialPowers.spawn(ifr, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ });
+
+ let keyAboutBlankHex = keyAboutBlank
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyAboutBlankHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the about:blank iframe document."
+ );
+
+ // Get the key from the cookieJarSettings of the javascript URL iframe
+ // document.
+ let keyJavascriptURL = await SpecialPowers.spawn(browser, [], async _ => {
+ let ifr = content.document.getElementById("testFrame");
+
+ return ifr.contentDocument.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+
+ let keyJavascriptURLHex = keyJavascriptURL
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyJavascriptURLHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the javascript URL iframe document."
+ );
+
+ // Get the key from the cookieJarSettings of an first-party iframe.
+ let keyFirstPartyFrame = await SpecialPowers.spawn(
+ browser,
+ [firstPartyDomain],
+ async domain => {
+ let ifr = content.document.createElement("iframe");
+
+ let loaded = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = domain;
+
+ await loaded;
+
+ return SpecialPowers.spawn(ifr, [], _ => {
+ return content.document.cookieJarSettings
+ .fingerprintingRandomizationKey;
+ });
+ }
+ );
+
+ let keyFirstPartyFrameHex = keyFirstPartyFrame
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyFirstPartyFrameHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the first-party iframe document."
+ );
+
+ // Get the key from the cookieJarSettings of an third-party iframe
+ let keyThirdPartyFrame = await SpecialPowers.spawn(
+ browser,
+ [thirdPartyDomain],
+ async domain => {
+ let ifr = content.document.createElement("iframe");
+
+ let loaded = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = domain;
+
+ await loaded;
+
+ return SpecialPowers.spawn(ifr, [], _ => {
+ return content.document.cookieJarSettings
+ .fingerprintingRandomizationKey;
+ });
+ }
+ );
+
+ let keyThirdPartyFrameHex = keyThirdPartyFrame
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyThirdPartyFrameHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the third-party iframe document."
+ );
+
+ return keyHex;
+}
+
+// Test accessing the fingerprinting randomization key will throw if
+// fingerprinting resistance is disabled.
+add_task(async function test_randomization_disabled_with_rfp_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["privacy.resistFingerprinting.pbmode", false],
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ],
+ });
+
+ // Ensure accessing the fingerprinting randomization key of the browser
+ // element will throw if fingerprinting randomization is disabled.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ try {
+ let key =
+ tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey;
+ ok(
+ false,
+ `Accessing the fingerprinting randomization key should throw when fingerprinting resistance is disabled. ${key}`
+ );
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is disabled."
+ );
+ }
+
+ // Ensure accessing the fingerprinting randomization key of the top-level
+ // document will throw if fingerprinting randomization is disabled.
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is disabled."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test the fingerprinting randomization key generation.
+add_task(async function test_generate_randomization_key() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+
+ for (let testPrivateWin of [true, false]) {
+ let win = window;
+
+ if (testPrivateWin) {
+ win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ }
+
+ let tabOne = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_PAGE
+ );
+ let keyHexOne;
+
+ try {
+ keyHexOne = await getRandomKeyHexFromBrowser(
+ tabOne.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+ ok(true, `The fingerprinting random key: ${keyHexOne}`);
+ } catch (e) {
+ ok(
+ false,
+ "Shouldn't fail when getting the random key from the cookieJarSettings"
+ );
+ }
+
+ // Open the test domain again and check if the key remains the same.
+ let tabTwo = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_PAGE
+ );
+ try {
+ let keyHexTwo = await getRandomKeyHexFromBrowser(
+ tabTwo.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+ is(
+ keyHexTwo,
+ keyHexOne,
+ `The key should remain the same after reopening the tab.`
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Shouldn't fail when getting the random key from the cookieJarSettings"
+ );
+ }
+
+ // Open a tab with a different domain to see if the key changes.
+ let tabAnother = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE
+ );
+ try {
+ let keyHexAnother = await getRandomKeyHexFromBrowser(
+ tabAnother.linkedBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+ isnot(
+ keyHexAnother,
+ keyHexOne,
+ `The key should be different when loading a different domain`
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Shouldn't fail when getting the random key from the cookieJarSettings"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tabOne);
+ BrowserTestUtils.removeTab(tabTwo);
+ BrowserTestUtils.removeTab(tabAnother);
+ if (testPrivateWin) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ }
+});
+
+// Test the fingerprinting randomization key will change after private session
+// ends.
+add_task(async function test_reset_key_after_pbm_session_ends() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open a tab in the private window.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ TEST_PAGE
+ );
+
+ let keyHex = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Close the window and open another private window.
+ BrowserTestUtils.removeTab(tab);
+
+ let promisePBExit = TestUtils.topicObserved("last-pb-context-exited");
+ await BrowserTestUtils.closeWindow(privateWin);
+ await promisePBExit;
+
+ privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open a tab again in the new private window.
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ TEST_PAGE
+ );
+
+ let keyHexNew = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Ensure the keys are different.
+ isnot(keyHexNew, keyHex, "Ensure the new key is different from the old one.");
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+// Test accessing the fingerprinting randomization key will throw in normal
+// windows if we exempt fingerprinting protection in normal windows.
+add_task(async function test_randomization_with_exempted_normal_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["privacy.resistFingerprinting.pbmode", true],
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ],
+ });
+
+ // Ensure accessing the fingerprinting randomization key of the browser
+ // element will throw if fingerprinting randomization is exempted from normal
+ // windows.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ try {
+ let key =
+ tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey;
+ ok(
+ false,
+ `Accessing the fingerprinting randomization key should throw when fingerprinting resistance is exempted in normal windows. ${key}`
+ );
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is exempted in normal windows."
+ );
+ }
+
+ // Ensure accessing the fingerprinting randomization key of the top-level
+ // document will throw if fingerprinting randomization is exempted from normal
+ // windows.
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is exempted in normal windows."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Open a private window and check the key can be accessed there.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open a tab in the private window.
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ TEST_PAGE
+ );
+
+ // Access the key, this shouldn't throw an error.
+ await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+// Test that the random key gets reset when the site data gets cleared.
+add_task(async function test_reset_random_key_when_clear_site_data() {
+ // Enable fingerprinting randomization key generation.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+
+ // Open a tab and get randomization key from the test domain.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ let keyHex = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Open another tab and get randomization key from another domain.
+ let anotherTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE
+ );
+
+ let keyHexAnother = await getRandomKeyHexFromBrowser(
+ anotherTab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(anotherTab);
+
+ // Call ForgetAboutSite for the test domain.
+ await ForgetAboutSite.removeDataFromDomain("example.com");
+
+ // Open the tab for the test domain again and verify the key is reset.
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ let keyHexNew = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Open the tab for another domain again and verify the key is intact.
+ anotherTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE
+ );
+ let keyHexAnotherNew = await getRandomKeyHexFromBrowser(
+ anotherTab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Ensure the keys are different for the test domain.
+ isnot(keyHexNew, keyHex, "Ensure the new key is different from the old one.");
+
+ // Ensure the key for another domain isn't changed.
+ is(
+ keyHexAnother,
+ keyHexAnotherNew,
+ "Ensure the key of another domain isn't reset."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(anotherTab);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js
new file mode 100644
index 0000000000..2231197eed
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js
@@ -0,0 +1,97 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_PAGE_NORMAL = TEST_PATH + "empty.html";
+const TEST_PAGE_FINGERPRINTER = TEST_PATH + "font-fingerprinter.html";
+
+const TELEMETRY_FONT_FINGERPRINTING_PER_TAB = "FONT_FINGERPRINTING_PER_TAB";
+
+async function clearTelemetry() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry
+ .getHistogramById(TELEMETRY_FONT_FINGERPRINTING_PER_TAB)
+ .clear();
+}
+
+async function getHistogram(histogram_id, bucket, checkCntFn) {
+ let histogram;
+
+ // Wait until the telemetry probe appears.
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ histogram = histograms[histogram_id];
+
+ let checkRes = false;
+
+ if (histogram) {
+ checkRes = checkCntFn ? checkCntFn(histogram.values[bucket]) : true;
+ }
+
+ return checkRes;
+ });
+
+ return histogram.values[bucket] || 0;
+}
+
+async function checkHistogram(histogram_id, bucket, expectedCnt) {
+ let cnt = await getHistogram(histogram_id, bucket, cnt => {
+ if (cnt === undefined) {
+ cnt = 0;
+ }
+
+ return cnt == expectedCnt;
+ });
+
+ is(cnt, expectedCnt, "There should be expected count in telemetry.");
+}
+
+add_setup(async function () {
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // First, we open a page without any canvas fingerprinters
+ await BrowserTestUtils.withNewTab(TEST_PAGE_NORMAL, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // Check that the telemetry has been record properly for normal page. The
+ // telemetry should show there was no known fingerprinting attempt.
+ await checkHistogram(TELEMETRY_FONT_FINGERPRINTING_PER_TAB, 0 /* false */, 1);
+
+ await clearTelemetry();
+});
+
+add_task(async function test_canvas_fingerprinting_telemetry() {
+ let promiseWindowDestroyed = BrowserUtils.promiseObserved(
+ "window-global-destroyed"
+ );
+
+ // Now open a page with a canvas fingerprinter
+ await BrowserTestUtils.withNewTab(TEST_PAGE_FINGERPRINTER, async _ => {});
+
+ // Make sure the tab was closed properly before checking Telemetry.
+ await promiseWindowDestroyed;
+
+ // The telemetry should show one font fingerprinting attempt.
+ await checkHistogram(TELEMETRY_FONT_FINGERPRINTING_PER_TAB, 1 /* true */, 1);
+
+ await clearTelemetry();
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js
new file mode 100644
index 0000000000..64279ae442
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js
@@ -0,0 +1,85 @@
+/* import-globals-from testHelpers.js */
+
+// This test ensures that a service worker for an exempted domain is exempted when it is
+// in the first party context, and not exempted when it is in a third party context.
+
+runTestInFirstAndThirdPartyContexts(
+ "ServiceWorkers - Check that RFP correctly is exempted and not exempted when FPI is enabled",
+ async win => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set externally and then captured because this function gets stringified and
+ // then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.firstParty.isolate", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the first-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ console.info(
+ "First Party, got: " +
+ res.value +
+ " Expected: " +
+ DEFAULT_HARDWARE_CONCURRENCY
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "As a first party, HW Concurrency should not be spoofed"
+ );
+ },
+ async win => {
+ let SPOOFED_HW_CONCURRENCY = 2;
+
+ // Register service worker for the third-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ console.info(
+ "Third Party, got: " + res.value + " Expected: " + SPOOFED_HW_CONCURRENCY
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "As a third party, HW Concurrency should be spoofed"
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.firstparty.isolate", true],
+ ["privacy.resistFingerprinting", true],
+ ["privacy.resistFingerprinting.exemptedDomains", "example.com"],
+ ]
+);
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js b/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js
new file mode 100644
index 0000000000..d80abfdc81
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js
@@ -0,0 +1,52 @@
+const PDF_FILE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "file_pdf.pdf";
+
+add_task(async function canvas_placeholder_pdfjs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting", true]],
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PDF_FILE);
+
+ const data = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ function extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 10;
+ canvas.height = 10;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#332211";
+ context.fillRect(0, 0, 10, 10);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ return context.getImageData(0, 0, 10, 10).data;
+ }
+
+ return content.eval(`(${extractCanvasData})()`);
+ });
+
+ is(data.length, 10 * 10 * 4, "correct canvas data size");
+
+ let failure = false;
+ for (var i = 0; i < 10 * 10 * 4; i += 4) {
+ if (
+ data[i] != 0x33 ||
+ data[i + 1] != 0x22 ||
+ data[i + 2] != 0x11 ||
+ data[i + 3] != 0xff
+ ) {
+ ok(false, `incorrect data ${data.slice(i, i + 4)} @ ${i}..${i + 3}`);
+ failure = true;
+ break;
+ }
+ }
+ ok(!failure, "canvas is correct");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js
new file mode 100644
index 0000000000..a9bab38e61
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from testHelpers.js */
+
+runTestInFirstAndThirdPartyContexts(
+ "ServiceWorkers - Ensure the fingerprinting WebCompat overrides the fingerprinting protection in third-party context.",
+ async win => {
+ let SPOOFED_HW_CONCURRENCY = 2;
+
+ // Register service worker for the first-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "HW Concurrency should be spoofed in the first-party context"
+ );
+ },
+
+ async win => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set externally and then captured because this function gets stringified and
+ // then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.fingerprintingProtection", false]],
+ });
+
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the third-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the third-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "HW Concurrency should not be spoofed in the third-party context"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"],
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify([
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-NavigatorHWConcurrency",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "example.com",
+ },
+ ]),
+ ],
+ ]
+);
+
+runTestInFirstAndThirdPartyContexts(
+ "ServiceWorkers - Ensure the fingerprinting WebCompat overrides the fingerprinting protection in third-party context with FPI enabled.",
+ async win => {
+ let SPOOFED_HW_CONCURRENCY = 2;
+
+ // Register service worker for the first-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "HW Concurrency should be spoofed in the first-party context"
+ );
+ },
+
+ async win => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set enternally and then captured because this function gets stringified and
+ // then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.fingerprintingProtection", false]],
+ });
+
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the third-party window.
+ if (!win.sw) {
+ win.sw = await registerServiceWorker(win, "serviceWorker.js");
+ }
+
+ // Check navigator HW concurrency from the third-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win.sw,
+ win.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "HW Concurrency should not be spoofed in the third-party context"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.firstparty.isolate", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"],
+ [
+ "privacy.fingerprintingProtection.granularOverrides",
+ JSON.stringify([
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ overrides: "-NavigatorHWConcurrency",
+ firstPartyDomain: "example.net",
+ thirdPartyDomain: "example.com",
+ },
+ ]),
+ ],
+ ]
+);
diff --git a/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html b/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html
new file mode 100644
index 0000000000..d48d289529
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<head>
+ <title>A page containing a canvas fingerprinter</title>
+</head>
+<body>
+ <canvas width=200 height=200>
+ </canvas>
+ <script>
+ var canvas = document.querySelector("canvas");
+ var context = canvas.getContext("2d");
+
+ context.fillStyle = "rgb(100, 210, 0)";
+ context.fillRect(100, 10, 50, 50);
+ context.fillStyle = "#f65";
+ context.font = "16pt Arial";
+ context.fillText("<@nv45. F1n63r,Pr1n71n6!", 20, 40);
+
+ var data = canvas.toDataURL();
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/empty.html b/toolkit/components/resistfingerprinting/tests/browser/empty.html
new file mode 100644
index 0000000000..4e59eed479
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>An empty page for testing</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf b/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf
new file mode 100644
index 0000000000..593558f9a4
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf
@@ -0,0 +1,12 @@
+%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000102 00000 n
+trailer<</Size 4/Root 1 0 R>>
+startxref
+149
+%EOF \ No newline at end of file
diff --git a/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html b/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html
new file mode 100644
index 0000000000..63e542d67b
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<html>
+<head>
+ <title>A page containing a font fingerprinter</title>
+</head>
+<body>
+<script>
+/*
+* Based on https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js
+* (Archived: https://web.archive.org/web/20150706050408/https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js#L233)
+*
+* Fingerprintjs2 0.1.4 - Modern & flexible browser fingerprint library v2
+* https://github.com/Valve/fingerprintjs2
+* Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com)
+* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+*/
+// kudos to http://www.lalit.org/lab/javascript-css-font-detect/
+function getFonts() {
+ // a font will be compared against all the three default fonts.
+ // and if it doesn't match all 3 then that font is not available.
+ var baseFonts = ["monospace", "sans-serif", "serif"];
+
+ //we use m or w because these two characters take up the maximum width.
+ // And we use a LLi so that the same matching fonts can get separated
+ var testString = "mmmmmmmmmmlli";
+
+ //we test using 72px font size, we may use any size. I guess larger the better.
+ var testSize = "72px";
+
+ var h = document.getElementsByTagName("body")[0];
+
+ // create a SPAN in the document to get the width of the text we use to test
+ var s = document.createElement("span");
+ s.style.fontSize = testSize;
+ s.innerHTML = testString;
+ var defaultWidth = {};
+ var defaultHeight = {};
+ for (var index in baseFonts) {
+ //get the default width for the three base fonts
+ s.style.fontFamily = baseFonts[index];
+ h.appendChild(s);
+ defaultWidth[baseFonts[index]] = s.offsetWidth; //width for the default font
+ defaultHeight[baseFonts[index]] = s.offsetHeight; //height for the defualt font
+ h.removeChild(s);
+ }
+ var detect = function (font) {
+ var detected = false;
+ for (var index in baseFonts) {
+ s.style.fontFamily = font + "," + baseFonts[index]; // name of the font along with the base font for fallback.
+ h.appendChild(s);
+ var matched = (s.offsetWidth !== defaultWidth[baseFonts[index]] || s.offsetHeight !== defaultHeight[baseFonts[index]]);
+ h.removeChild(s);
+ detected = detected || matched;
+ }
+ return detected;
+ };
+ var fontList = [
+ "Abadi MT Condensed Light", "Academy Engraved LET", "ADOBE CASLON PRO", "Adobe Garamond", "ADOBE GARAMOND PRO", "Agency FB", "Aharoni", "Albertus Extra Bold", "Albertus Medium", "Algerian", "Amazone BT", "American Typewriter",
+ "American Typewriter Condensed", "AmerType Md BT", "Andale Mono", "Andalus", "Angsana New", "AngsanaUPC", "Antique Olive", "Aparajita", "Apple Chancery", "Apple Color Emoji", "Apple SD Gothic Neo", "Arabic Typesetting", "ARCHER", "Arial", "Arial Black", "Arial Hebrew",
+ "Arial MT", "Arial Narrow", "Arial Rounded MT Bold", "Arial Unicode MS", "ARNO PRO", "Arrus BT", "Aurora Cn BT", "AvantGarde Bk BT", "AvantGarde Md BT", "AVENIR", "Ayuthaya", "Bandy", "Bangla Sangam MN", "Bank Gothic", "BankGothic Md BT", "Baskerville",
+ "Baskerville Old Face", "Batang", "BatangChe", "Bauer Bodoni", "Bauhaus 93", "Bazooka", "Bell MT", "Bembo", "Benguiat Bk BT", "Berlin Sans FB", "Berlin Sans FB Demi", "Bernard MT Condensed", "BernhardFashion BT", "BernhardMod BT", "Big Caslon", "BinnerD",
+ "Bitstream Vera Sans Mono", "Blackadder ITC", "BlairMdITC TT", "Bodoni 72", "Bodoni 72 Oldstyle", "Bodoni 72 Smallcaps", "Bodoni MT", "Bodoni MT Black", "Bodoni MT Condensed", "Bodoni MT Poster Compressed", "Book Antiqua", "Bookman Old Style",
+ "Bookshelf Symbol 7", "Boulder", "Bradley Hand", "Bradley Hand ITC", "Bremen Bd BT", "Britannic Bold", "Broadway", "Browallia New", "BrowalliaUPC", "Brush Script MT", "Calibri", "Californian FB", "Calisto MT", "Calligrapher", "Cambria", "Cambria Math", "Candara",
+ "CaslonOpnface BT", "Castellar", "Centaur", "Century", "Century Gothic", "Century Schoolbook", "Cezanne", "CG Omega", "CG Times", "Chalkboard", "Chalkboard SE", "Chalkduster", "Charlesworth", "Charter Bd BT", "Charter BT", "Chaucer",
+ "ChelthmITC Bk BT", "Chiller", "Clarendon", "Clarendon Condensed", "CloisterBlack BT", "Cochin", "Colonna MT", "Comic Sans", "Comic Sans MS", "Consolas", "Constantia", "Cooper Black", "Copperplate", "Copperplate Gothic", "Copperplate Gothic Bold",
+ "Copperplate Gothic Light", "CopperplGoth Bd BT", "Corbel", "Cordia New", "CordiaUPC", "Cornerstone", "Coronet", "Courier", "Courier New", "Cuckoo", "Curlz MT", "DaunPenh", "Dauphin", "David", "DB LCD Temp", "DELICIOUS", "Denmark", "Devanagari Sangam MN",
+ "DFKai-SB", "Didot", "DilleniaUPC", "DIN", "DokChampa", "Dotum", "DotumChe", "Ebrima", "Edwardian Script ITC", "Elephant", "English 111 Vivace BT", "Engravers MT", "EngraversGothic BT", "Eras Bold ITC", "Eras Demi ITC", "Eras Light ITC", "Eras Medium ITC",
+ "Estrangelo Edessa", "EucrosiaUPC", "Euphemia", "Euphemia UCAS", "EUROSTILE", "Exotc350 Bd BT", "FangSong", "Felix Titling", "Fixedsys", "FONTIN", "Footlight MT Light", "Forte", "Franklin Gothic", "Franklin Gothic Book", "Franklin Gothic Demi",
+ "Franklin Gothic Demi Cond", "Franklin Gothic Heavy", "Franklin Gothic Medium", "Franklin Gothic Medium Cond", "FrankRuehl", "Fransiscan", "Freefrm721 Blk BT", "FreesiaUPC", "Freestyle Script", "French Script MT", "FrnkGothITC Bk BT", "Fruitger", "FRUTIGER",
+ "Futura", "Futura Bk BT", "Futura Lt BT", "Futura Md BT", "Futura ZBlk BT", "FuturaBlack BT", "Gabriola", "Galliard BT", "Garamond", "Gautami", "Geeza Pro", "Geneva", "Geometr231 BT", "Geometr231 Hv BT", "Geometr231 Lt BT", "Georgia", "GeoSlab 703 Lt BT",
+ "GeoSlab 703 XBd BT", "Gigi", "Gill Sans", "Gill Sans MT", "Gill Sans MT Condensed", "Gill Sans MT Ext Condensed Bold", "Gill Sans Ultra Bold", "Gill Sans Ultra Bold Condensed", "Gisha", "Gloucester MT Extra Condensed", "GOTHAM", "GOTHAM BOLD",
+ "Goudy Old Style", "Goudy Stout", "GoudyHandtooled BT", "GoudyOLSt BT", "Gujarati Sangam MN", "Gulim", "GulimChe", "Gungsuh", "GungsuhChe", "Gurmukhi MN", "Haettenschweiler", "Harlow Solid Italic", "Harrington", "Heather", "Heiti SC", "Heiti TC", "HELV", "Helvetica",
+ "Helvetica Neue", "Herald", "High Tower Text", "Hiragino Kaku Gothic ProN", "Hiragino Mincho ProN", "Hoefler Text", "Humanst 521 Cn BT", "Humanst521 BT", "Humanst521 Lt BT", "Impact", "Imprint MT Shadow", "Incised901 Bd BT", "Incised901 BT",
+ "Incised901 Lt BT", "INCONSOLATA", "Informal Roman", "Informal011 BT", "INTERSTATE", "IrisUPC", "Iskoola Pota", "JasmineUPC", "Jazz LET", "Jenson", "Jester", "Jokerman", "Juice ITC", "Kabel Bk BT", "Kabel Ult BT", "Kailasa", "KaiTi", "Kalinga", "Kannada Sangam MN",
+ "Kartika", "Kaufmann Bd BT", "Kaufmann BT", "Khmer UI", "KodchiangUPC", "Kokila", "Korinna BT", "Kristen ITC", "Krungthep", "Kunstler Script", "Lao UI", "Latha", "Leelawadee", "Letter Gothic", "Levenim MT", "LilyUPC", "Lithograph", "Lithograph Light", "Long Island",
+ "Lucida Bright", "Lucida Calligraphy", "Lucida Console", "Lucida Fax", "LUCIDA GRANDE", "Lucida Handwriting", "Lucida Sans", "Lucida Sans Typewriter", "Lucida Sans Unicode", "Lydian BT", "Magneto", "Maiandra GD", "Malayalam Sangam MN", "Malgun Gothic",
+ "Mangal", "Marigold", "Marion", "Marker Felt", "Market", "Marlett", "Matisse ITC", "Matura MT Script Capitals", "Meiryo", "Meiryo UI", "Microsoft Himalaya", "Microsoft JhengHei", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Sans Serif", "Microsoft Tai Le",
+ "Microsoft Uighur", "Microsoft YaHei", "Microsoft Yi Baiti", "MingLiU", "MingLiU_HKSCS", "MingLiU_HKSCS-ExtB", "MingLiU-ExtB", "Minion", "Minion Pro", "Miriam", "Miriam Fixed", "Mistral", "Modern", "Modern No. 20", "Mona Lisa Solid ITC TT", "Monaco", "Mongolian Baiti",
+ "MONO", "Monotype Corsiva", "MoolBoran", "Mrs Eaves", "MS Gothic", "MS LineDraw", "MS Mincho", "MS Outlook", "MS PGothic", "MS PMincho", "MS Reference Sans Serif", "MS Reference Specialty", "MS Sans Serif", "MS Serif", "MS UI Gothic", "MT Extra", "MUSEO", "MV Boli", "MYRIAD",
+ "MYRIAD PRO", "Nadeem", "Narkisim", "NEVIS", "News Gothic", "News GothicMT", "NewsGoth BT", "Niagara Engraved", "Niagara Solid", "Noteworthy", "NSimSun", "Nyala", "OCR A Extended", "Old Century", "Old English Text MT", "Onyx", "Onyx BT", "OPTIMA", "Oriya Sangam MN",
+ "OSAKA", "OzHandicraft BT", "Palace Script MT", "Palatino", "Palatino Linotype", "Papyrus", "Parchment", "Party LET", "Pegasus", "Perpetua", "Perpetua Titling MT", "PetitaBold", "Pickwick", "Plantagenet Cherokee", "Playbill", "PMingLiU", "PMingLiU-ExtB",
+ "Poor Richard", "Poster", "PosterBodoni BT", "PRINCETOWN LET", "Pristina", "PTBarnum BT", "Pythagoras", "Raavi", "Rage Italic", "Ravie", "Ribbon131 Bd BT", "Rockwell", "Rockwell Condensed", "Rockwell Extra Bold", "Rod", "Roman", "Sakkal Majalla",
+ "Santa Fe LET", "Savoye LET", "Sceptre", "Script", "Script MT Bold", "SCRIPTINA", "Segoe Print", "Segoe Script", "Segoe UI", "Segoe UI Light", "Segoe UI Semibold", "Segoe UI Symbol", "Serifa", "Serifa BT", "Serifa Th BT", "ShelleyVolante BT", "Sherwood",
+ "Shonar Bangla", "Showcard Gothic", "Shruti", "Signboard", "SILKSCREEN", "SimHei", "Simplified Arabic", "Simplified Arabic Fixed", "SimSun", "SimSun-ExtB", "Sinhala Sangam MN", "Sketch Rockwell", "Skia", "Small Fonts", "Snap ITC", "Snell Roundhand", "Socket",
+ "Souvenir Lt BT", "Staccato222 BT", "Steamer", "Stencil", "Storybook", "Styllo", "Subway", "Swis721 BlkEx BT", "Swiss911 XCm BT", "Sylfaen", "Synchro LET", "System", "Tahoma", "Tamil Sangam MN", "Technical", "Teletype", "Telugu Sangam MN", "Tempus Sans ITC",
+ "Terminal", "Thonburi", "Times", "Times New Roman", "Times New Roman PS", "Traditional Arabic", "Trajan", "TRAJAN PRO", "Trebuchet MS", "Tristan", "Tubular", "Tunga", "Tw Cen MT", "Tw Cen MT Condensed", "Tw Cen MT Condensed Extra Bold",
+ "TypoUpright BT", "Unicorn", "Univers", "Univers CE 55 Medium", "Univers Condensed", "Utsaah", "Vagabond", "Vani", "Verdana", "Vijaya", "Viner Hand ITC", "VisualUI", "Vivaldi", "Vladimir Script", "Vrinda", "Westminster", "WHITNEY", "Wide Latin", "Wingdings",
+ "Wingdings 2", "Wingdings 3", "ZapfEllipt BT", "ZapfHumnst BT", "ZapfHumnst Dm BT", "Zapfino", "Zurich BlkEx BT", "Zurich Ex BT", "ZWAdobeF"];
+ var available = [];
+ for (var i = 0, l = fontList.length; i < l; i++) {
+ if(detect(fontList[i])) {
+ available.push(fontList[i]);
+ }
+ }
+ return available;
+}
+
+let fonts = getFonts();
+console.log("detected fonts:", fonts);
+</script>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/head.js b/toolkit/components/resistfingerprinting/tests/browser/head.js
new file mode 100644
index 0000000000..9d8ec19956
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/head.js
@@ -0,0 +1,227 @@
+// Number of bits in the canvas that we randomize when canvas randomization is
+// enabled.
+const NUM_RANDOMIZED_CANVAS_BITS = 256;
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+/**
+ * Compares two ArrayBuffer objects to see if they are different.
+ *
+ * @param {ArrayBuffer} buffer1 - The first buffer to compare.
+ * @param {ArrayBuffer} buffer2 - The second buffer to compare.
+ * @returns {boolean} True if the buffers are different, false if they are the
+ * same.
+ */
+function countDifferencesInArrayBuffers(buffer1, buffer2) {
+ // compare the byte lengths of the two buffers
+ if (buffer1.byteLength !== buffer2.byteLength) {
+ return true;
+ }
+
+ // create typed arrays to represent the two buffers
+ const view1 = new DataView(buffer1);
+ const view2 = new DataView(buffer2);
+
+ let differences = 0;
+ // compare the byte values of the two typed arrays
+ for (let i = 0; i < buffer1.byteLength; i++) {
+ if (view1.getUint8(i) !== view2.getUint8(i)) {
+ differences += 1;
+ }
+ }
+
+ // the two buffers are the same
+ return differences;
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ };
+ Services.obs.addObserver(obs, topic);
+ });
+}
+
+/**
+ *
+ * Spawns a worker in the given browser and sends a callback function to it.
+ * The result of the callback function is returned as a Promise.
+ *
+ * @param {object} browser - The browser context to spawn the worker in.
+ * @param {Function} fn - The callback function to send to the worker.
+ * @returns {Promise} A Promise that resolves to the result of the callback function.
+ */
+function runFunctionInWorker(browser, fn) {
+ return SpecialPowers.spawn(browser, [fn.toString()], async callback => {
+ // Create a worker.
+ let worker = new content.Worker("worker.js");
+
+ // Send the callback to the worker.
+ return new content.Promise(resolve => {
+ worker.onmessage = e => {
+ resolve(e.data.result);
+ };
+
+ worker.postMessage({
+ callback,
+ });
+ });
+ });
+}
+
+const TEST_FIRST_PARTY_CONTEXT_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.net"
+ ) + "testPage.html";
+const TEST_THIRD_PARTY_CONTEXT_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "scriptExecPage.html";
+
+/**
+ * Executes provided callbacks in both first and third party contexts.
+ *
+ * @param {string} name - Name of the test.
+ * @param {Function} firstPartyCallback - The callback to be executed in the first-party context.
+ * @param {Function} thirdPartyCallback - The callback to be executed in the third-party context.
+ * @param {Function} [cleanupFunction] - A cleanup function to be called after the tests.
+ * @param {Array} [extraPrefs] - Optional. An array of preferences to be set before running the test.
+ */
+function runTestInFirstAndThirdPartyContexts(
+ name,
+ firstPartyCallback,
+ thirdPartyCallback,
+ cleanupFunction,
+ extraPrefs
+) {
+ add_task(async _ => {
+ info("Starting test `" + name + "' in first and third party contexts");
+
+ await SpecialPowers.flushPrefEnv();
+
+ if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) {
+ await SpecialPowers.pushPrefEnv({ set: extraPrefs });
+ }
+
+ info("Creating a new tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_FIRST_PARTY_CONTEXT_PAGE
+ );
+
+ info("Creating a 3rd party content and a 1st party popup");
+ let firstPartyPopupPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_THIRD_PARTY_CONTEXT_PAGE,
+ true
+ );
+
+ let thirdPartyBC = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_THIRD_PARTY_CONTEXT_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ content.open(url);
+
+ return ifr.browsingContext;
+ }
+ );
+ let firstPartyTab = await firstPartyPopupPromise;
+ let firstPartyBrowser = firstPartyTab.linkedBrowser;
+
+ info("Sending code to the 3rd party content and the 1st party popup");
+ let runningCallbackTask = async obj => {
+ return new content.Promise(resolve => {
+ content.postMessage({ callback: obj.callback }, "*");
+
+ content.addEventListener("message", function msg(event) {
+ // This is the event from above postMessage.
+ if (event.data.callback) {
+ return;
+ }
+
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ });
+ };
+
+ let firstPartyTask = SpecialPowers.spawn(
+ firstPartyBrowser,
+ [
+ {
+ callback: firstPartyCallback.toString(),
+ },
+ ],
+ runningCallbackTask
+ );
+
+ let thirdPartyTask = SpecialPowers.spawn(
+ thirdPartyBC,
+ [
+ {
+ callback: thirdPartyCallback.toString(),
+ },
+ ],
+ runningCallbackTask
+ );
+
+ await Promise.all([firstPartyTask, thirdPartyTask]);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(firstPartyTab);
+ });
+
+ add_task(async _ => {
+ info("Cleaning up.");
+ if (cleanupFunction) {
+ await cleanupFunction();
+ }
+ });
+}
diff --git a/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html b/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html
new file mode 100644
index 0000000000..324cc2f261
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html
@@ -0,0 +1,37 @@
+<html>
+<head>
+ <title>A content page for executing script!</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js"></script>
+</head>
+<body>
+<h1>Here the content!</h1>
+<script>
+
+function info(msg) {
+ window.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ window.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = async function(e) {
+ if (!e.data.callback) {
+ return;
+ }
+ let data = e.data.callback;
+ let runnableStr = `(() => {return (${data});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+
+ await runnable.call(this, this, window);
+
+ window.postMessage({ type: "finish" }, "*");
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js b/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js
new file mode 100644
index 0000000000..e9ee00cbf8
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js
@@ -0,0 +1,15 @@
+self.addEventListener("message", async e => {
+ let res = {};
+
+ switch (e.data.type) {
+ case "GetHWConcurrency":
+ res.result = "OK";
+ res.value = navigator.hardwareConcurrency;
+ break;
+
+ default:
+ res.result = "ERROR";
+ }
+
+ e.source.postMessage(res);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js b/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js
new file mode 100644
index 0000000000..3feb5dfc11
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js
@@ -0,0 +1,30 @@
+async function registerServiceWorker(win, url) {
+ let reg = await win.navigator.serviceWorker.register(url);
+ if (reg.installing.state !== "activated") {
+ await new Promise(resolve => {
+ let w = reg.installing;
+ w.addEventListener("statechange", function onStateChange() {
+ if (w.state === "activated") {
+ w.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ }
+
+ return reg.active;
+}
+
+function sendAndWaitWorkerMessage(target, worker, message) {
+ return new Promise(resolve => {
+ worker.addEventListener(
+ "message",
+ msg => {
+ resolve(msg.data);
+ },
+ { once: true }
+ );
+
+ target.postMessage(message);
+ });
+}
diff --git a/toolkit/components/resistfingerprinting/tests/browser/testPage.html b/toolkit/components/resistfingerprinting/tests/browser/testPage.html
new file mode 100644
index 0000000000..07a17d3d71
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/testPage.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>A page with a javascript URL iframe</title>
+</head>
+<body>
+ <iframe id="testFrame" src="javascript:void(0)"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/worker.js b/toolkit/components/resistfingerprinting/tests/browser/worker.js
new file mode 100644
index 0000000000..894b208549
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/worker.js
@@ -0,0 +1,7 @@
+onmessage = e => {
+ let runnableStr = `(() => {return (${e.data.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ runnable.call(this).then(result => {
+ postMessage({ result });
+ });
+};
diff --git a/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml b/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..6ccea5d1e0
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+
+["test_spoof_english.html"]
diff --git a/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html b/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html
new file mode 100644
index 0000000000..750743ba01
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1486258
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1486258</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1486258">Mozilla Bug 1486258</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script type="text/javascript">
+/* global SpecialPowers, content */
+
+const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+);
+
+const gOrigAvailableLocales = Services.locale.availableLocales;
+const gOrigRequestedLocales = Services.locale.requestedLocales;
+
+const kDoNotSpoof = 1;
+const kSpoofEnglish = 2;
+
+async function runTest(locale) {
+ return BrowserTestUtils.withNewTab("https://example.com/", browser => {
+ return SpecialPowers.spawn(browser, [locale], _locale => {
+ let locale = JSON.stringify(_locale);
+ return content.eval(`({
+ locale: new Intl.PluralRules(${locale}).resolvedOptions().locale,
+ weekday: new Date(0).toLocaleString(${locale}, {weekday: "long"}),
+ currency: Number(1000).toLocaleString(${locale}, {currency: "USD"}),
+ })`);
+ });
+ });
+}
+
+async function runSpoofTest(spoof) {
+ ok(spoof == kDoNotSpoof ||
+ spoof == kSpoofEnglish, "check supported parameter");
+
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["privacy.spoof_english", spoof],
+ ],
+ });
+
+ let results = await runTest(undefined);
+
+ await SpecialPowers.popPrefEnv();
+
+ return results;
+}
+
+async function setupLocale(locale) {
+ Services.locale.availableLocales = [locale];
+ Services.locale.requestedLocales = [locale];
+}
+
+async function clearLocale() {
+ Services.locale.availableLocales = gOrigAvailableLocales;
+ Services.locale.requestedLocales = gOrigRequestedLocales;
+}
+
+(async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ // 1. preliminary for spoof test
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["privacy.resistFingerprinting", true],
+ ],
+ });
+
+ // 2. log English/German results
+ let enResults = await runTest("en-US");
+ is(enResults.locale, "en-US", "default locale");
+
+ let deResults = await runTest("de-DE");
+ is(deResults.locale, "de-DE", "default locale");
+
+ // 3. set system locale to German
+ await setupLocale("de-DE");
+
+ // 4. log non-spoofed results
+ let nonSpoofedResults = await runSpoofTest(kDoNotSpoof);
+
+ // 5. log spoofed results
+ let spoofedResults = await runSpoofTest(kSpoofEnglish);
+
+ // 6. compare spoofed/non-spoofed results
+ for (let key in enResults) {
+ isnot(enResults[key], deResults[key], "compare en/de result: " + key);
+ is(nonSpoofedResults[key], deResults[key], "compare non-spoofed result: " + key);
+ is(spoofedResults[key], enResults[key], "compare spoofed result: " + key);
+ }
+
+ // 7. restore default locale
+ await clearLocale();
+
+ SimpleTest.finish();
+})();
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/gtest/moz.build b/toolkit/components/resistfingerprinting/tests/gtest/moz.build
new file mode 100644
index 0000000000..dcc6a7e12e
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/gtest/moz.build
@@ -0,0 +1,5 @@
+UNIFIED_SOURCES += [
+ "test_reduceprecision.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp
new file mode 100644
index 0000000000..5d8b598ff0
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp
@@ -0,0 +1,566 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <math.h>
+
+#include "gtest/gtest.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsRFPService.h"
+
+using namespace mozilla;
+
+// clang-format off
+/*
+ Hello! Are you looking at this file because you got an error you don't understand?
+ Perhaps something that looks like the following?
+
+ toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp:15: Failure
+ Expected: reduced1
+ Which is: 2064.83
+ To be equal to: reduced2
+ Which is: 2064.83
+
+ "Gosh," you might say, "They sure look equal to me. What the heck is going on here?"
+
+ The answer lies beyond what you can see, in that which you cannot see. One must
+ journey into the depths, the hidden, that which the world fights its hardest to
+ conceal from us.
+
+ Specifically: you need to look at more decimal places. Run the test with:
+ MOZ_LOG="nsResistFingerprinting:5"
+
+ And look for two successive lines similar to the below (the format will probably
+ be different by the time you read this comment):
+ V/nsResistFingerprinting Given: 2064.83384599999999, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241692.00000000000000, Got: 2064.83383999999978
+ V/nsResistFingerprinting Given: 2064.83383999999978, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241691.00000000000000, Got: 2064.83381999999983
+
+ Look at the last two values:
+ Got: 2064.83383999999978
+ Got: 2064.83381999999983
+
+ They're supposed to be equal. They're not. But they both round to 2064.83.
+ Good luck and godspeed.
+*/
+// clang-format on
+
+bool setupJitter(bool enabled) {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ bool jitterEnabled = false;
+ if (prefs) {
+ prefs->GetBoolPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ &jitterEnabled);
+ prefs->SetBoolPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.jitter", enabled);
+ }
+
+ return jitterEnabled;
+}
+
+void cleanupJitter(bool jitterWasEnabled) {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ prefs->SetBoolPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ jitterWasEnabled);
+ }
+}
+
+void process(double clock, nsRFPService::TimeScale clockUnits,
+ double precision) {
+ double reduced1 = nsRFPService::ReduceTimePrecisionImpl(
+ clock, clockUnits, precision, -1, TimerPrecisionType::Normal);
+ double reduced2 = nsRFPService::ReduceTimePrecisionImpl(
+ reduced1, clockUnits, precision, -1, TimerPrecisionType::Normal);
+ ASSERT_EQ(reduced1, reduced2);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Assumptions)
+{
+ ASSERT_EQ(FLT_RADIX, 2);
+ ASSERT_EQ(DBL_MANT_DIG, 53);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Reciprocal)
+{
+ bool jitterEnabled = setupJitter(false);
+ // This one has a rounding error in the Reciprocal case:
+ process(2064.8338460, nsRFPService::TimeScale::MicroSeconds, 20);
+ // These are just big values
+ process(1516305819, nsRFPService::TimeScale::MicroSeconds, 20);
+ process(69053.12, nsRFPService::TimeScale::MicroSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_KnownGood)
+{
+ bool jitterEnabled = setupJitter(false);
+ process(2064.8338460, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(69027.62, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(69053.12, nsRFPService::TimeScale::MilliSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_KnownBad)
+{
+ bool jitterEnabled = setupJitter(false);
+ process(1054.842405, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(273.53038600000002, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(628.66686500000003, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(521.28919100000007, nsRFPService::TimeScale::MilliSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Edge)
+{
+ bool jitterEnabled = setupJitter(false);
+ process(2611.14, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2611.16, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2612.16, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2601.64, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2595.16, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2578.66, nsRFPService::TimeScale::MilliSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.12);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations_HighRes)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.12);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations_RFP)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.12);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations_DangerouslyNone)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.145);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.141);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.15999);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.15);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.13);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254740990.0);
+ // 9007199254740995 is approximated to 9007199254740996
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254740996);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254741000.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743564
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254743564.0);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_HighRes)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254740980.0);
+ // 9007199254740995 is approximated to 9007199254740980
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254740980);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254741000.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743560
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254743560.0);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_RFP)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254740990.0);
+ // 9007199254740995 is approximated to 9007199254740980
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254740995);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254741000.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743560
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254743565.0);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting,
+ ReducePrecision_ExpectedLossOfPrecision_DangerouslyNone)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254740992.0);
+ // 9007199254740995 is approximated to 9007199254740980
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254740995);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254740999.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743560
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254743568.0);
+ cleanupJitter(jitterEnabled);
+}
+
+// Use an ugly but simple hack to turn an integer-based rand()
+// function to a double-based one.
+#define RAND_DOUBLE (rand() * (rand() / (double)rand()))
+
+// If you're doing logging, you really don't want to run this test.
+#define RUN_AGGRESSIVE false
+
+TEST(ResistFingerprinting, ReducePrecision_Aggressive)
+{
+ if (!RUN_AGGRESSIVE) {
+ return;
+ }
+
+ bool jitterEnabled = setupJitter(false);
+
+ for (int i = 0; i < 10000; i++) {
+ // Test three different time magnitudes, with decimals.
+ // Note that we need separate variables for the different units, as scaling
+ // them after calculating them will erase effects of approximation.
+ // A magnitude in the seconds since epoch range.
+ double time1_s = fmod(RAND_DOUBLE, 1516305819.0);
+ double time1_ms = fmod(RAND_DOUBLE, 1516305819000.0);
+ double time1_us = fmod(RAND_DOUBLE, 1516305819000000.0);
+ // A magnitude in the 'couple of minutes worth of milliseconds' range.
+ double time2_s = fmod(RAND_DOUBLE, (60.0 * 60 * 5));
+ double time2_ms = fmod(RAND_DOUBLE, (1000.0 * 60 * 60 * 5));
+ double time2_us = fmod(RAND_DOUBLE, (1000000.0 * 60 * 60 * 5));
+ // A magnitude in the small range
+ double time3_s = fmod(RAND_DOUBLE, 10);
+ double time3_ms = fmod(RAND_DOUBLE, 10000);
+ double time3_us = fmod(RAND_DOUBLE, 10000000);
+
+ // Test two precision magnitudes, no decimals.
+ // A magnitude in the high milliseconds.
+ double precision1 = rand() % 250000;
+ // a magnitude in the low microseconds.
+ double precision2 = rand() % 200;
+
+ process(time1_s, nsRFPService::TimeScale::Seconds, precision1);
+ process(time1_s, nsRFPService::TimeScale::Seconds, precision2);
+ process(time2_s, nsRFPService::TimeScale::Seconds, precision1);
+ process(time2_s, nsRFPService::TimeScale::Seconds, precision2);
+ process(time3_s, nsRFPService::TimeScale::Seconds, precision1);
+ process(time3_s, nsRFPService::TimeScale::Seconds, precision2);
+
+ process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+ process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+ process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+ process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+ process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+ process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+
+ process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+ process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+ process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+ process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+ process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+ process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+ }
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_JitterTestVectors)
+{
+ bool jitterEnabled = setupJitter(true);
+
+ // clang-format off
+ /*
+ * Here's our test vector. We set the secret to the 16 byte value
+ * 0x0001020304050607 0x1011121314151617
+ *
+ * We then use a stand alone XOrShift128+ generator:
+ *
+ * The midpoints are:
+ * 0 = 328
+ * 500 = 25
+ * 1000 = 73
+ * 1500 = 89
+ * 2000 = 73
+ * 2500 = 153
+ * 3000 = 9
+ * 3500 = 89
+ * 4000 = 73
+ * 4500 = 205
+ * 5000 = 253
+ * 5500 = 141
+ */
+ // clang-format on
+
+ // Set the secret
+ long long throwAway;
+ uint8_t hardcodedSecret[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x10, 0x11, 0x12, 0x13,
+ 0x14, 0x15, 0x16, 0x17};
+
+ nsRFPService::RandomMidpoint(0, 500, -1, &throwAway, hardcodedSecret);
+
+ // Run the test vectors
+ double result;
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 1, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 0);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 184, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 0);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 185, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 329, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 499, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 500, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 520, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 524, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 525, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 1000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 930, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 1000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 1072, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 1000);
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4000, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4050, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4072, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4073, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4340, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4499, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4500, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4536, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4704, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4705, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 5000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4726, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 5000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 5106, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 5000);
+
+ cleanupJitter(jitterEnabled);
+}
diff --git a/toolkit/components/resistfingerprinting/tests/moz.build b/toolkit/components/resistfingerprinting/tests/moz.build
new file mode 100644
index 0000000000..3a6ca329ff
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/moz.build
@@ -0,0 +1,9 @@
+MOCHITEST_CHROME_MANIFESTS += [
+ "chrome/chrome.toml",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser/browser.toml",
+]
+
+TEST_DIRS += ["gtest"]