summaryrefslogtreecommitdiffstats
path: root/toolkit/components/resistfingerprinting/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests')
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser.ini10
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js589
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js323
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js458
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/empty.html8
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/head.js52
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/testPage.html9
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/worker.js7
-rw-r--r--toolkit/components/resistfingerprinting/tests/chrome.ini4
-rw-r--r--toolkit/components/resistfingerprinting/tests/moz.build13
-rw-r--r--toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp565
-rw-r--r--toolkit/components/resistfingerprinting/tests/test_spoof_english.html119
12 files changed, 2157 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 });
+ });
+};
diff --git a/toolkit/components/resistfingerprinting/tests/chrome.ini b/toolkit/components/resistfingerprinting/tests/chrome.ini
new file mode 100644
index 0000000000..ff7c487ab4
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_spoof_english.html]
diff --git a/toolkit/components/resistfingerprinting/tests/moz.build b/toolkit/components/resistfingerprinting/tests/moz.build
new file mode 100644
index 0000000000..f5c1582193
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/moz.build
@@ -0,0 +1,13 @@
+UNIFIED_SOURCES += [
+ "test_reduceprecision.cpp",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "chrome.ini",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser/browser.ini",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
new file mode 100644
index 0000000000..4a2ec72b66
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
@@ -0,0 +1,565 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <math.h>
+
+#include "gtest/gtest.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsRFPService.h"
+
+using namespace mozilla;
+
+// clang-format off
+/*
+ Hello! Are you looking at this file because you got an error you don't understand?
+ Perhaps something that looks like the following?
+
+ toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp:15: Failure
+ Expected: reduced1
+ Which is: 2064.83
+ To be equal to: reduced2
+ Which is: 2064.83
+
+ "Gosh," you might say, "They sure look equal to me. What the heck is going on here?"
+
+ The answer lies beyond what you can see, in that which you cannot see. One must
+ journey into the depths, the hidden, that which the world fights its hardest to
+ conceal from us.
+
+ Specially: you need to look at more decimal places. Run the test with:
+ MOZ_LOG="nsResistFingerprinting:5"
+
+ And look for two successive lines similar to the below (the format will certainly
+ be different by the time you read this comment):
+ V/nsResistFingerprinting Given: 2064.83384599999999, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241692.00000000000000, Got: 2064.83383999999978
+ V/nsResistFingerprinting Given: 2064.83383999999978, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241691.00000000000000, Got: 2064.83381999999983
+
+ Look at the last two values:
+ Got: 2064.83383999999978
+ Got: 2064.83381999999983
+
+ They're supposed to be equal. They're not. But they both round to 2064.83.
+*/
+// clang-format on
+
+bool setupJitter(bool enabled) {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ bool jitterEnabled = false;
+ if (prefs) {
+ prefs->GetBoolPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ &jitterEnabled);
+ prefs->SetBoolPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.jitter", enabled);
+ }
+
+ return jitterEnabled;
+}
+
+void cleanupJitter(bool jitterWasEnabled) {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ prefs->SetBoolPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ jitterWasEnabled);
+ }
+}
+
+void process(double clock, nsRFPService::TimeScale clockUnits,
+ double precision) {
+ double reduced1 = nsRFPService::ReduceTimePrecisionImpl(
+ clock, clockUnits, precision, -1, TimerPrecisionType::Normal);
+ double reduced2 = nsRFPService::ReduceTimePrecisionImpl(
+ reduced1, clockUnits, precision, -1, TimerPrecisionType::Normal);
+ ASSERT_EQ(reduced1, reduced2);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Assumptions)
+{
+ ASSERT_EQ(FLT_RADIX, 2);
+ ASSERT_EQ(DBL_MANT_DIG, 53);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Reciprocal)
+{
+ bool jitterEnabled = setupJitter(false);
+ // This one has a rounding error in the Reciprocal case:
+ process(2064.8338460, nsRFPService::TimeScale::MicroSeconds, 20);
+ // These are just big values
+ process(1516305819, nsRFPService::TimeScale::MicroSeconds, 20);
+ process(69053.12, nsRFPService::TimeScale::MicroSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_KnownGood)
+{
+ bool jitterEnabled = setupJitter(false);
+ process(2064.8338460, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(69027.62, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(69053.12, nsRFPService::TimeScale::MilliSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_KnownBad)
+{
+ bool jitterEnabled = setupJitter(false);
+ process(1054.842405, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(273.53038600000002, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(628.66686500000003, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(521.28919100000007, nsRFPService::TimeScale::MilliSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Edge)
+{
+ bool jitterEnabled = setupJitter(false);
+ process(2611.14, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2611.16, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2612.16, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2601.64, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2595.16, nsRFPService::TimeScale::MilliSeconds, 20);
+ process(2578.66, nsRFPService::TimeScale::MilliSeconds, 20);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 2611.12);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations_HighRes)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 2611.12);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations_RFP)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 2611.12);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations_DangerouslyNone)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.14);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.145);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.141);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.15999);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.15);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 2611.13);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254740990.0);
+ // 9007199254740995 is approximated to 9007199254740996
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254740996);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254741000.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743564
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 9007199254743564.0);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_HighRes)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254740980.0);
+ // 9007199254740995 is approximated to 9007199254740980
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254740980);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254741000.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743560
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::UnconditionalAKAHighRes);
+ ASSERT_EQ(result, 9007199254743560.0);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_RFP)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254740990.0);
+ // 9007199254740995 is approximated to 9007199254740980
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254740995);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254741000.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743560
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::RFP);
+ ASSERT_EQ(result, 9007199254743565.0);
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting,
+ ReducePrecision_ExpectedLossOfPrecision_DangerouslyNone)
+{
+ bool jitterEnabled = setupJitter(false);
+ double result;
+ // We lose integer precision at 9007199254740992 - let's confirm that.
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254740992.0);
+ // 9007199254740995 is approximated to 9007199254740980
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254740995);
+ // 9007199254740999 is approximated as 9007199254741000
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254740999.0);
+ // 9007199254743568 can be represented exactly, but will be clamped to
+ // 9007199254743560
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1,
+ TimerPrecisionType::DangerouslyNone);
+ ASSERT_EQ(result, 9007199254743568.0);
+ cleanupJitter(jitterEnabled);
+}
+
+// Use an ugly but simple hack to turn an integer-based rand()
+// function to a double-based one.
+#define RAND_DOUBLE (rand() * (rand() / (double)rand()))
+
+// If you're doing logging, you really don't want to run this test.
+#define RUN_AGGRESSIVE false
+
+TEST(ResistFingerprinting, ReducePrecision_Aggressive)
+{
+ if (!RUN_AGGRESSIVE) {
+ return;
+ }
+
+ bool jitterEnabled = setupJitter(false);
+
+ for (int i = 0; i < 10000; i++) {
+ // Test three different time magnitudes, with decimals.
+ // Note that we need separate variables for the different units, as scaling
+ // them after calculating them will erase effects of approximation.
+ // A magnitude in the seconds since epoch range.
+ double time1_s = fmod(RAND_DOUBLE, 1516305819.0);
+ double time1_ms = fmod(RAND_DOUBLE, 1516305819000.0);
+ double time1_us = fmod(RAND_DOUBLE, 1516305819000000.0);
+ // A magnitude in the 'couple of minutes worth of milliseconds' range.
+ double time2_s = fmod(RAND_DOUBLE, (60.0 * 60 * 5));
+ double time2_ms = fmod(RAND_DOUBLE, (1000.0 * 60 * 60 * 5));
+ double time2_us = fmod(RAND_DOUBLE, (1000000.0 * 60 * 60 * 5));
+ // A magnitude in the small range
+ double time3_s = fmod(RAND_DOUBLE, 10);
+ double time3_ms = fmod(RAND_DOUBLE, 10000);
+ double time3_us = fmod(RAND_DOUBLE, 10000000);
+
+ // Test two precision magnitudes, no decimals.
+ // A magnitude in the high milliseconds.
+ double precision1 = rand() % 250000;
+ // a magnitude in the low microseconds.
+ double precision2 = rand() % 200;
+
+ process(time1_s, nsRFPService::TimeScale::Seconds, precision1);
+ process(time1_s, nsRFPService::TimeScale::Seconds, precision2);
+ process(time2_s, nsRFPService::TimeScale::Seconds, precision1);
+ process(time2_s, nsRFPService::TimeScale::Seconds, precision2);
+ process(time3_s, nsRFPService::TimeScale::Seconds, precision1);
+ process(time3_s, nsRFPService::TimeScale::Seconds, precision2);
+
+ process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+ process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+ process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+ process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+ process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+ process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+
+ process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+ process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+ process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+ process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+ process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+ process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+ }
+ cleanupJitter(jitterEnabled);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_JitterTestVectors)
+{
+ bool jitterEnabled = setupJitter(true);
+
+ // clang-format off
+ /*
+ * Here's our test vector. We set the secret to the 16 byte value
+ * 0x0001020304050607 0x1011121314151617
+ *
+ * We then use a stand alone XOrShift128+ generator:
+ *
+ * The midpoints are:
+ * 0 = 328
+ * 500 = 25
+ * 1000 = 73
+ * 1500 = 89
+ * 2000 = 73
+ * 2500 = 153
+ * 3000 = 9
+ * 3500 = 89
+ * 4000 = 73
+ * 4500 = 205
+ * 5000 = 253
+ * 5500 = 141
+ */
+ // clang-format on
+
+ // Set the secret
+ long long throwAway;
+ uint8_t hardcodedSecret[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x10, 0x11, 0x12, 0x13,
+ 0x14, 0x15, 0x16, 0x17};
+
+ nsRFPService::RandomMidpoint(0, 500, -1, &throwAway, hardcodedSecret);
+
+ // Run the test vectors
+ double result;
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 1, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 0);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 184, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 0);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 185, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 329, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 499, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 500, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 520, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 524, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 525, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 1000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 930, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 1000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 1072, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 1000);
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4000, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4050, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4072, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4073, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4340, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4499, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4500, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4536, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4704, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 4500);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4705, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 5000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 4726, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 5000);
+ result = nsRFPService::ReduceTimePrecisionImpl(
+ 5106, nsRFPService::TimeScale::MicroSeconds, 500, 4000,
+ TimerPrecisionType::Normal);
+ ASSERT_EQ(result, 5000);
+
+ cleanupJitter(jitterEnabled);
+}
diff --git a/toolkit/components/resistfingerprinting/tests/test_spoof_english.html b/toolkit/components/resistfingerprinting/tests/test_spoof_english.html
new file mode 100644
index 0000000000..1ebc6277e9
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/test_spoof_english.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1486258
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1486258</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1486258">Mozilla Bug 1486258</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script type="text/javascript">
+/* global SpecialPowers */
+
+const { BrowserTestUtils } = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
+;
+
+const gOrigAvailableLocales = Services.locale.availableLocales;
+const gOrigRequestedLocales = Services.locale.requestedLocales;
+
+const kDoNotSpoof = 1;
+const kSpoofEnglish = 2;
+
+async function runTest(locale) {
+ return BrowserTestUtils.withNewTab("http://example.com/", browser => {
+ return SpecialPowers.spawn(browser, [locale], _locale => {
+ return {
+ locale: new Intl.PluralRules(_locale).resolvedOptions().locale,
+ weekday: new Date(0).toLocaleString(_locale, {weekday: "long"}),
+ currency: Number(1000).toLocaleString(_locale, {currency: "USD"}),
+ };
+ });
+ });
+}
+
+async function runSpoofTest(spoof) {
+ ok(spoof == kDoNotSpoof ||
+ spoof == kSpoofEnglish, "check supported parameter");
+
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["privacy.spoof_english", spoof],
+ ],
+ });
+
+ is(SpecialPowers.getBoolPref("javascript.use_us_english_locale", false),
+ spoof == kSpoofEnglish,
+ "use_us_english_locale");
+
+ let results = await runTest(undefined);
+
+ await SpecialPowers.popPrefEnv();
+
+ return results;
+}
+
+async function setupLocale(locale) {
+ Services.locale.availableLocales = [locale];
+ Services.locale.requestedLocales = [locale];
+}
+
+async function clearLocale() {
+ Services.locale.availableLocales = gOrigAvailableLocales;
+ Services.locale.requestedLocales = gOrigRequestedLocales;
+}
+
+(async function() {
+ SimpleTest.waitForExplicitFinish();
+
+ // 1. preliminary for spoof test
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["privacy.resistFingerprinting", true],
+ ],
+ });
+
+ // 2. log English/German results
+ let enResults = await runTest("en-US");
+ is(enResults.locale, "en-US", "default locale");
+
+ let deResults = await runTest("de");
+ is(deResults.locale, "de", "default locale");
+
+ // 3. set system locale to German
+ await setupLocale("de");
+
+ // 4. log non-spoofed results
+ let nonSpoofedResults = await runSpoofTest(kDoNotSpoof);
+
+ // 5. log spoofed results
+ let spoofedResults = await runSpoofTest(kSpoofEnglish);
+
+ // 6. compare spoofed/non-spoofed results
+ for (let key in enResults) {
+ isnot(enResults[key], deResults[key], "compare en/de result: " + key);
+ is(nonSpoofedResults[key], deResults[key], "compare non-spoofed result: " + key);
+ is(spoofedResults[key], enResults[key], "compare spoofed result: " + key);
+ }
+
+ // 7. restore default locale
+ await clearLocale();
+
+ SimpleTest.finish();
+})();
+
+</script>
+
+</body>
+</html>