summaryrefslogtreecommitdiffstats
path: root/toolkit/components/resistfingerprinting/tests/browser
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/components/resistfingerprinting/tests/browser
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests/browser')
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser.ini10
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js589
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js323
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js458
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/empty.html8
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/head.js52
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/testPage.html9
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/worker.js7
8 files changed, 1456 insertions, 0 deletions
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.ini b/toolkit/components/resistfingerprinting/tests/browser/browser.ini
new file mode 100644
index 0000000000..3a3aa2ccc6
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files =
+ empty.html
+ head.js
+ testPage.html
+ worker.js
+
+[browser_canvas_randomization.js]
+[browser_canvas_randomization_worker.js]
+[browser_fingerprinting_randomization_key.js]
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
new file mode 100644
index 0000000000..0935beecba
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
@@ -0,0 +1,589 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 1816189 - Testing canvas randomization on canvas data extraction.
+ *
+ * In the test, we create canvas elements and offscreen canvas and test if
+ * the extracted canvas data is altered because of the canvas randomization.
+ */
+
+const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+var TEST_CASES = [
+ {
+ name: "CanvasRenderingContext2D.getImageData().",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], _ => {
+ const canvas = content.document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ });
+ },
+ isDataRandomized(data1, data2, isCompareOriginal) {
+ let diffCnt = compareUint8Arrays(data1, data2);
+ info(`There are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ ok(diffCnt <= expected, "The number of noise bits is expected.");
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a 2d context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], _ => {
+ const canvas = content.document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a webgl context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], _ => {
+ const canvas = content.document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ // Draw a blue rectangle
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 150, 150, 150);
+ context.clearColor(1, 0, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(150, 150, 300, 150);
+ context.clearColor(0, 1, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(0, 0, 150, 150);
+ context.clearColor(0, 0, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a bitmaprenderer context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ const canvas = content.document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ const bitmapCanvas = content.document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ content.document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await content.createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ const dataURL = bitmapCanvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = bitmapCanvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a 2d context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ const canvas = content.document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ let data = await new content.Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new content.Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a webgl context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ const canvas = content.document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ // Draw a blue rectangle
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 150, 150, 150);
+ context.clearColor(1, 0, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(150, 150, 300, 150);
+ context.clearColor(0, 1, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(0, 0, 150, 150);
+ context.clearColor(0, 0, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ let data = await new content.Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // We don't get the consistent blob data on second access with webgl
+ // context regardless of the canvas randomization. So, we report the
+ // same data here to not fail the test. Ideally, we should look into
+ // why this happens, but it's not caused by canvas randomization.
+
+ return [data, data];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a bitmaprenderer context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ const canvas = content.document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ content.document.body.appendChild(canvas);
+
+ const bitmapCanvas = content.document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ content.document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await content.createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let data = await new content.Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new content.Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a 2d context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ let offscreenCanvas = new content.OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ let blob = await offscreenCanvas.convertToBlob();
+
+ let data = await new content.Promise(resolve => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+
+ let dataSecond = await new content.Promise(resolve => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ let offscreenCanvas = new content.OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ // Draw a blue rectangle
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 150, 150, 150);
+ context.clearColor(1, 0, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(150, 150, 300, 150);
+ context.clearColor(0, 1, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(0, 0, 150, 150);
+ context.clearColor(0, 0, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ let blob = await offscreenCanvas.convertToBlob();
+
+ let data = await new content.Promise(resolve => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+
+ let dataSecond = await new content.Promise(resolve => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ let offscreenCanvas = new content.OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ const bitmapCanvas = new content.OffscreenCanvas(100, 100);
+
+ let bitmap = await content.createImageBitmap(offscreenCanvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let blob = await bitmapCanvas.convertToBlob();
+
+ let data = await new content.Promise(resolve => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ // Access the data again.
+ let blobSecond = await bitmapCanvas.convertToBlob();
+
+ let dataSecond = await new content.Promise(resolve => {
+ let fileReader = new content.FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ });
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ extractCanvasData(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ let offscreenCanvas = new content.OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ });
+ },
+ isDataRandomized(data1, data2, isCompareOriginal) {
+ let diffCnt = compareUint8Arrays(data1, data2);
+ info(`There are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ ok(diffCnt <= expected, "The number of noise bits is expected.");
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+];
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ["privacy.fingerprintingProtection.overrides", RFPOverrides],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a private window.
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open tabs in the normal and private window.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ const privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ emptyPage
+ );
+
+ for (let test of TEST_CASES) {
+ info(`Testing ${test.name}`);
+ let data = await test.extractCanvasData(tab.linkedBrowser);
+ let result = test.isDataRandomized(data[0], test.originalData);
+
+ is(
+ result,
+ enabled,
+ `The image data is ${enabled ? "randomized" : "the same"}.`
+ );
+
+ ok(
+ !test.isDataRandomized(data[0], data[1]),
+ "The data of first and second access should be the same."
+ );
+
+ let privateData = await test.extractCanvasData(privateTab.linkedBrowser);
+
+ // Check if we add noise to canvas data in private windows.
+ result = test.isDataRandomized(privateData[0], test.originalData, true);
+ is(
+ result,
+ enabled,
+ `The private image data is ${enabled ? "randomized" : "the same"}.`
+ );
+
+ ok(
+ !test.isDataRandomized(privateData[0], privateData[1]),
+ "The data of first and second access should be the same for private windows."
+ );
+
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ }.`
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", false],
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ // Extract the original canvas data without random noise.
+ for (let test of TEST_CASES) {
+ let data = await test.extractCanvasData(tab.linkedBrowser);
+ test.originalData = data[0];
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function run_tests_with_randomization_enabled() {
+ await runTest(true);
+});
+
+add_task(async function run_tests_with_randomization_disabled() {
+ await runTest(false);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js
new file mode 100644
index 0000000000..2f27925c71
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js
@@ -0,0 +1,323 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+/**
+ * Bug 1816189 - Testing canvas randomization on canvas data extraction in
+ * workers.
+ *
+ * In the test, we create offscreen canvas in workers and test if the extracted
+ * canvas data is altered because of the canvas randomization.
+ */
+
+/**
+ *
+ * Spawns a worker in the given browser and sends a callback function to it.
+ * The result of the callback function is returned as a Promise.
+ *
+ * @param {object} browser - The browser context to spawn the worker in.
+ * @param {Function} fn - The callback function to send to the worker.
+ * @returns {Promise} A Promise that resolves to the result of the callback function.
+ */
+function runFunctionInWorker(browser, fn) {
+ return SpecialPowers.spawn(browser, [fn.toString()], async callback => {
+ // Create a worker.
+ let worker = new content.Worker("worker.js");
+
+ // Send the callback to the worker.
+ return new content.Promise(resolve => {
+ worker.onmessage = e => {
+ resolve(e.data.result);
+ };
+
+ worker.postMessage({
+ callback,
+ });
+ });
+ });
+}
+
+var TEST_CASES = [
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(data1, data2, isCompareOriginal) {
+ let diffCnt = compareUint8Arrays(data1, data2);
+ info(`There are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ ok(diffCnt <= expected, "The number of noise bits is expected.");
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a 2d context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ let blob = await offscreenCanvas.convertToBlob();
+
+ let data = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ let blobSecond = await offscreenCanvas.convertToBlob();
+
+ let dataSecond = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ // Draw a blue rectangle
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 150, 150, 150);
+ context.clearColor(1, 0, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(150, 150, 300, 150);
+ context.clearColor(0, 1, 0, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(0, 0, 150, 150);
+ context.clearColor(0, 0, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ let blob = await offscreenCanvas.convertToBlob();
+
+ let data = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ let blobSecond = await offscreenCanvas.convertToBlob();
+
+ let dataSecond = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context",
+ extractCanvasData: async _ => {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 100, 100);
+
+ const bitmapCanvas = new OffscreenCanvas(100, 100);
+
+ let bitmap = await createImageBitmap(offscreenCanvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let blob = await bitmapCanvas.convertToBlob();
+
+ let data = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+
+ let blobSecond = await bitmapCanvas.convertToBlob();
+
+ let dataSecond = await new Promise(resolve => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blobSecond);
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(data1, data2) {
+ return compareArrayBuffer(data1, data2);
+ },
+ },
+];
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", true],
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ["privacy.fingerprintingProtection.overrides", RFPOverrides],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a private window.
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open tabs in the normal and private window.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ const privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ emptyPage
+ );
+
+ for (let test of TEST_CASES) {
+ info(`Testing ${test.name} in the worker`);
+ let data = await await runFunctionInWorker(
+ tab.linkedBrowser,
+ test.extractCanvasData
+ );
+
+ let result = test.isDataRandomized(data[0], test.originalData);
+ is(
+ result,
+ enabled,
+ `The image data is ${enabled ? "randomized" : "the same"}.`
+ );
+
+ ok(
+ !test.isDataRandomized(data[0], data[1]),
+ "The data of first and second access should be the same."
+ );
+
+ let privateData = await await runFunctionInWorker(
+ privateTab.linkedBrowser,
+ test.extractCanvasData
+ );
+
+ // Check if we add noise to canvas data in private windows.
+ result = test.isDataRandomized(privateData[0], test.originalData, true);
+ is(
+ result,
+ enabled,
+ `The private image data is ${enabled ? "randomized" : "the same"}.`
+ );
+
+ ok(
+ !test.isDataRandomized(privateData[0], privateData[1]),
+ "The data of first and second access should be the same."
+ );
+
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ }.`
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", false],
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a tab for extracting the canvas data in the worker.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ // Extract the original canvas data without random noise.
+ for (let test of TEST_CASES) {
+ let data = await runFunctionInWorker(
+ tab.linkedBrowser,
+ test.extractCanvasData
+ );
+ test.originalData = data[0];
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function run_tests_with_randomization_enabled() {
+ await runTest(true);
+});
+
+add_task(async function run_tests_with_randomization_disabled() {
+ await runTest(false);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js
new file mode 100644
index 0000000000..5213ea277d
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js
@@ -0,0 +1,458 @@
+const TEST_DOMAIN = "https://example.com";
+const TEST_DOMAIN_ANOTHER = "https://example.org";
+const TEST_DOMAIN_THIRD = "https://example.net";
+
+const TEST_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_DOMAIN
+ ) + "testPage.html";
+const TEST_DOMAIN_ANOTHER_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_DOMAIN_ANOTHER
+ ) + "testPage.html";
+const TEST_DOMAIN_THIRD_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_DOMAIN_THIRD
+ ) + "testPage.html";
+
+/**
+ * A helper function to get the random key in a hex string format and test if
+ * the random key works properly.
+ *
+ * @param {Browser} browser The browser element of the testing tab.
+ * @param {string} firstPartyDomain The first-party domain loaded on the tab
+ * @param {string} thirdPartyDomain The third-party domain to test
+ * @returns {string} The random key hex string
+ */
+async function getRandomKeyHexFromBrowser(
+ browser,
+ firstPartyDomain,
+ thirdPartyDomain
+) {
+ // Get the key from the cookieJarSettings of the browser element.
+ let key = browser.cookieJarSettings.fingerprintingRandomizationKey;
+ let keyHex = key.map(bytes => bytes.toString(16).padStart(2, "0")).join("");
+
+ // Get the key from the cookieJarSettings of the top-level document.
+ let keyTop = await SpecialPowers.spawn(browser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ let keyTopHex = keyTop
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+
+ is(
+ keyTopHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the top-level document."
+ );
+
+ // Get the key from the cookieJarSettings of an about:blank iframe.
+ let keyAboutBlank = await SpecialPowers.spawn(browser, [], async _ => {
+ let ifr = content.document.createElement("iframe");
+
+ let loaded = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = "about:blank";
+
+ await loaded;
+
+ return SpecialPowers.spawn(ifr, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ });
+
+ let keyAboutBlankHex = keyAboutBlank
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyAboutBlankHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the about:blank iframe document."
+ );
+
+ // Get the key from the cookieJarSettings of the javascript URL iframe
+ // document.
+ let keyJavascriptURL = await SpecialPowers.spawn(browser, [], async _ => {
+ let ifr = content.document.getElementById("testFrame");
+
+ return ifr.contentDocument.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+
+ let keyJavascriptURLHex = keyJavascriptURL
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyJavascriptURLHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the javascript URL iframe document."
+ );
+
+ // Get the key from the cookieJarSettings of an first-party iframe.
+ let keyFirstPartyFrame = await SpecialPowers.spawn(
+ browser,
+ [firstPartyDomain],
+ async domain => {
+ let ifr = content.document.createElement("iframe");
+
+ let loaded = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = domain;
+
+ await loaded;
+
+ return SpecialPowers.spawn(ifr, [], _ => {
+ return content.document.cookieJarSettings
+ .fingerprintingRandomizationKey;
+ });
+ }
+ );
+
+ let keyFirstPartyFrameHex = keyFirstPartyFrame
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyFirstPartyFrameHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the first-party iframe document."
+ );
+
+ // Get the key from the cookieJarSettings of an third-party iframe
+ let keyThirdPartyFrame = await SpecialPowers.spawn(
+ browser,
+ [thirdPartyDomain],
+ async domain => {
+ let ifr = content.document.createElement("iframe");
+
+ let loaded = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = domain;
+
+ await loaded;
+
+ return SpecialPowers.spawn(ifr, [], _ => {
+ return content.document.cookieJarSettings
+ .fingerprintingRandomizationKey;
+ });
+ }
+ );
+
+ let keyThirdPartyFrameHex = keyThirdPartyFrame
+ .map(bytes => bytes.toString(16).padStart(2, "0"))
+ .join("");
+ is(
+ keyThirdPartyFrameHex,
+ keyHex,
+ "The fingerprinting random key should match between the browser element and the third-party iframe document."
+ );
+
+ return keyHex;
+}
+
+// Test accessing the fingerprinting randomization key will throw if
+// fingerprinting randomization is disabled.
+add_task(async function test_randomization_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.resistFingerprinting.randomization.enabled", false]],
+ });
+
+ // Ensure accessing the fingerprinting randomization key of the browser
+ // element will throw if fingerprinting randomization is disabled.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ try {
+ let key =
+ tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey;
+ ok(
+ false,
+ `Accessing the fingerprinting randomization key should throw when randomization is disabled. ${key}`
+ );
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when randomization is disabled."
+ );
+ }
+
+ // Ensure accessing the fingerprinting randomization key of the top-level
+ // document will throw if fingerprinting randomization is disabled.
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when randomization is disabled."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test accessing the fingerprinting randomization key will throw if
+// fingerprinting resistance is disabled.
+add_task(async function test_randomization_disabled_with_rfp_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", true],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Ensure accessing the fingerprinting randomization key of the browser
+ // element will throw if fingerprinting randomization is disabled.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ try {
+ let key =
+ tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey;
+ ok(
+ false,
+ `Accessing the fingerprinting randomization key should throw when fingerprinting resistance is disabled. ${key}`
+ );
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is disabled."
+ );
+ }
+
+ // Ensure accessing the fingerprinting randomization key of the top-level
+ // document will throw if fingerprinting randomization is disabled.
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is disabled."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test the fingerprinting randomization key generation.
+add_task(async function test_generate_randomization_key() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", true],
+ ["privacy.resistFingerprinting", true],
+ ],
+ });
+
+ for (let testPrivateWin of [true, false]) {
+ let win = window;
+
+ if (testPrivateWin) {
+ win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ }
+
+ let tabOne = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_PAGE
+ );
+ let keyHexOne;
+
+ try {
+ keyHexOne = await getRandomKeyHexFromBrowser(
+ tabOne.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+ ok(true, `The fingerprinting random key: ${keyHexOne}`);
+ } catch (e) {
+ ok(
+ false,
+ "Shouldn't fail when getting the random key from the cookieJarSettings"
+ );
+ }
+
+ // Open the test domain again and check if the key remains the same.
+ let tabTwo = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_PAGE
+ );
+ try {
+ let keyHexTwo = await getRandomKeyHexFromBrowser(
+ tabTwo.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+ is(
+ keyHexTwo,
+ keyHexOne,
+ `The key should remain the same after reopening the tab.`
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Shouldn't fail when getting the random key from the cookieJarSettings"
+ );
+ }
+
+ // Open a tab with a different domain to see if the key changes.
+ let tabAnother = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE
+ );
+ try {
+ let keyHexAnother = await getRandomKeyHexFromBrowser(
+ tabAnother.linkedBrowser,
+ TEST_DOMAIN_ANOTHER_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+ isnot(
+ keyHexAnother,
+ keyHexOne,
+ `The key should be different when loading a different domain`
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Shouldn't fail when getting the random key from the cookieJarSettings"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tabOne);
+ BrowserTestUtils.removeTab(tabTwo);
+ BrowserTestUtils.removeTab(tabAnother);
+ if (testPrivateWin) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ }
+});
+
+// Test the fingerprinting randomization key will change after private session
+// ends.
+add_task(async function test_reset_key_after_pbm_session_ends() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", true],
+ ["privacy.resistFingerprinting", true],
+ ],
+ });
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open a tab in the private window.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ TEST_PAGE
+ );
+
+ let keyHex = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Close the window and open another private window.
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open a tab again in the new private window.
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ TEST_PAGE
+ );
+
+ let keyHexNew = await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ // Ensure the keys are different.
+ isnot(keyHexNew, keyHex, "Ensure the new key is different from the old one.");
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+// Test accessing the fingerprinting randomization key will throw in normal
+// windows if we exempt fingerprinting protection in normal windows.
+add_task(async function test_randomization_with_exempted_normal_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting.randomization.enabled", true],
+ ["privacy.resistFingerprinting", true],
+ ["privacy.resistFingerprinting.testGranularityMask", 2],
+ ],
+ });
+
+ // Ensure accessing the fingerprinting randomization key of the browser
+ // element will throw if fingerprinting randomization is exempted from normal
+ // windows.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ try {
+ let key =
+ tab.linkedBrowser.cookieJarSettings.fingerprintingRandomizationKey;
+ ok(
+ false,
+ `Accessing the fingerprinting randomization key should throw when fingerprinting resistance is exempted in normal windows. ${key}`
+ );
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is exempted in normal windows."
+ );
+ }
+
+ // Ensure accessing the fingerprinting randomization key of the top-level
+ // document will throw if fingerprinting randomization is exempted from normal
+ // windows.
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.cookieJarSettings.fingerprintingRandomizationKey;
+ });
+ } catch (e) {
+ ok(
+ true,
+ "It should throw when getting the key when fingerprinting resistance is exempted in normal windows."
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Open a private window and check the key can be accessed there.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open a tab in the private window.
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ TEST_PAGE
+ );
+
+ // Access the key, this shouldn't throw an error.
+ await getRandomKeyHexFromBrowser(
+ tab.linkedBrowser,
+ TEST_PAGE,
+ TEST_DOMAIN_THIRD_PAGE
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/toolkit/components/resistfingerprinting/tests/browser/empty.html b/toolkit/components/resistfingerprinting/tests/browser/empty.html
new file mode 100644
index 0000000000..4e59eed479
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>An empty page for testing</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/head.js b/toolkit/components/resistfingerprinting/tests/browser/head.js
new file mode 100644
index 0000000000..a5a0b7ef55
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/head.js
@@ -0,0 +1,52 @@
+// Number of bits in the canvas that we randomize when canvas randomization is
+// enabled.
+const NUM_RANDOMIZED_CANVAS_BITS = 256;
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function compareUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+/**
+ * Compares two ArrayBuffer objects to see if they are different.
+ *
+ * @param {ArrayBuffer} buffer1 - The first buffer to compare.
+ * @param {ArrayBuffer} buffer2 - The second buffer to compare.
+ * @returns {boolean} True if the buffers are different, false if they are the
+ * same.
+ */
+function compareArrayBuffer(buffer1, buffer2) {
+ // compare the byte lengths of the two buffers
+ if (buffer1.byteLength !== buffer2.byteLength) {
+ return true;
+ }
+
+ // create typed arrays to represent the two buffers
+ const view1 = new DataView(buffer1);
+ const view2 = new DataView(buffer2);
+
+ // compare the byte values of the two typed arrays
+ for (let i = 0; i < buffer1.byteLength; i++) {
+ if (view1.getUint8(i) !== view2.getUint8(i)) {
+ return true;
+ }
+ }
+
+ // the two buffers are the same
+ return false;
+}
diff --git a/toolkit/components/resistfingerprinting/tests/browser/testPage.html b/toolkit/components/resistfingerprinting/tests/browser/testPage.html
new file mode 100644
index 0000000000..07a17d3d71
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/testPage.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>A page with a javascript URL iframe</title>
+</head>
+<body>
+ <iframe id="testFrame" src="javascript:void(0)"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/resistfingerprinting/tests/browser/worker.js b/toolkit/components/resistfingerprinting/tests/browser/worker.js
new file mode 100644
index 0000000000..894b208549
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/worker.js
@@ -0,0 +1,7 @@
+onmessage = e => {
+ let runnableStr = `(() => {return (${e.data.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ runnable.call(this).then(result => {
+ postMessage({ result });
+ });
+};