summaryrefslogtreecommitdiffstats
path: root/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js')
-rw-r--r--toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js658
1 files changed, 658 insertions, 0 deletions
diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
new file mode 100644
index 0000000000..8b4be78d53
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js
@@ -0,0 +1,658 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 1816189 - Testing canvas randomization on canvas data extraction.
+ *
+ * In the test, we create canvas elements and offscreen canvas and test if
+ * the extracted canvas data is altered because of the canvas randomization.
+ */
+
+const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+var TEST_CASES = [
+ {
+ name: "CanvasRenderingContext2D.getImageData() but constant color",
+ shouldBeRandomized: false,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData().",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a 2d context",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a webgl context",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ const canvas = document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const dataURL = canvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = canvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toDataURL() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const bitmapCanvas = document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ const dataURL = bitmapCanvas.toDataURL();
+
+ // Access the data again.
+ const dataURLSecond = bitmapCanvas.toDataURL();
+
+ return [dataURL, dataURLSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ return data1 !== data2;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a 2d context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ let data = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a webgl context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+
+ const context = canvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ let data = await new Promise(resolve => {
+ canvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // We don't get the consistent blob data on second access with webgl
+ // context regardless of the canvas randomization. So, we report the
+ // same data here to not fail the test. Ideally, we should look into
+ // why this happens, but it's not caused by canvas randomization.
+
+ return [data, data];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "HTMLCanvasElement.toBlob() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const bitmapCanvas = document.createElement("canvas");
+ bitmapCanvas.width = 100;
+ bitmapCanvas.heigh = 100;
+ document.body.appendChild(bitmapCanvas);
+
+ let bitmap = await createImageBitmap(canvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let data = await new Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ // Access the data again.
+ let dataSecond = await new Promise(resolve => {
+ bitmapCanvas.toBlob(blob => {
+ let fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a 2d context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ let blob = await offscreenCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a webgl context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("webgl");
+
+ context.enable(context.SCISSOR_TEST);
+ context.scissor(0, 0, 100, 100);
+ context.clearColor(1, 0.2, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(15, 15, 30, 15);
+ context.clearColor(0.2, 1, 0.2, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+ context.scissor(50, 50, 15, 15);
+ context.clearColor(0.2, 0.2, 1, 1);
+ context.clear(context.COLOR_BUFFER_BIT);
+
+ let blob = await offscreenCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await offscreenCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "OffscreenCanvas.convertToBlob() with a bitmaprenderer context",
+ shouldBeRandomized: true,
+ async extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const bitmapCanvas = new OffscreenCanvas(100, 100);
+
+ let bitmap = await createImageBitmap(offscreenCanvas);
+ const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
+ bitmapContext.transferFromImageBitmap(bitmap);
+
+ let blob = await bitmapCanvas.convertToBlob();
+ let data = await blob.arrayBuffer();
+
+ // Access the data again.
+ let blobSecond = await bitmapCanvas.convertToBlob();
+ let dataSecond = await blobSecond.arrayBuffer();
+
+ return [data, dataSecond];
+ },
+ isDataRandomized(name, data1, data2) {
+ let diffCnt = countDifferencesInArrayBuffers(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+ return diffCnt > 0;
+ },
+ },
+ {
+ name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas",
+ shouldBeRandomized: true,
+ extractCanvasData() {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ // Access the data again.
+ const imageDataSecond = context.getImageData(0, 0, 100, 100);
+
+ return [imageData.data, imageDataSecond.data];
+ },
+ isDataRandomized(name, data1, data2, isCompareOriginal) {
+ let diffCnt = countDifferencesInUint8Arrays(data1, data2);
+ info(`For ${name} there are ${diffCnt} bits are different.`);
+
+ // The Canvas randomization adds at most 512 bits noise to the image data.
+ // We compare the image data arrays to see if they are different and the
+ // difference is within the range.
+
+ // If we are compare two randomized arrays, the difference can be doubled.
+ let expected = isCompareOriginal
+ ? NUM_RANDOMIZED_CANVAS_BITS
+ : NUM_RANDOMIZED_CANVAS_BITS * 2;
+
+ // The number of difference bits should never bigger than the expected
+ // number. It could be zero if the randomization is disabled.
+ Assert.lessOrEqual(
+ diffCnt,
+ expected,
+ "The number of noise bits is expected."
+ );
+
+ return diffCnt <= expected && diffCnt > 0;
+ },
+ },
+];
+
+function runExtractCanvasData(test, tab) {
+ let code = test.extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async code => {
+ let result = await content.eval(`({${code}}).extractCanvasData()`);
+ return result;
+ });
+}
+
+async function runTest(enabled) {
+ // Enable/Disable CanvasRandomization by the RFP target overrides.
+ let RFPOverrides = enabled ? "+CanvasRandomization" : "-CanvasRandomization";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", true],
+ ["privacy.fingerprintingProtection.pbmode", true],
+ ["privacy.fingerprintingProtection.overrides", RFPOverrides],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a private window.
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ // Open tabs in the normal and private window.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ const privateTab = await BrowserTestUtils.openNewForegroundTab(
+ privateWindow.gBrowser,
+ emptyPage
+ );
+
+ for (let test of TEST_CASES) {
+ info(`Testing ${test.name}`);
+
+ // Clear telemetry before starting test.
+ Services.fog.testResetFOG();
+
+ let data = await runExtractCanvasData(test, tab);
+ let result = test.isDataRandomized(test.name, data[0], test.originalData);
+
+ if (test.shouldBeRandomized) {
+ is(
+ result,
+ enabled,
+ `The image data is ${enabled ? "randomized" : "the same"} for ${
+ test.name
+ }.`
+ );
+ } else {
+ is(
+ result,
+ false,
+ `The image data for ${test.name} should never be randomized.`
+ );
+ }
+
+ ok(
+ !test.isDataRandomized(test.name, data[0], data[1]),
+ `The data of first and second access should be the same for ${test.name}.`
+ );
+
+ let privateData = await runExtractCanvasData(test, privateTab);
+
+ // Check if we add noise to canvas data in private windows.
+ result = test.isDataRandomized(
+ test.name,
+ privateData[0],
+ test.originalData,
+ true
+ );
+ if (test.shouldBeRandomized) {
+ is(
+ result,
+ enabled,
+ `The private image data is ${enabled ? "randomized" : "the same"} for ${
+ test.name
+ }.`
+ );
+ } else {
+ is(
+ result,
+ false,
+ `The image data for ${test.name} should never be randomized.`
+ );
+ }
+
+ ok(
+ !test.isDataRandomized(test.name, privateData[0], privateData[1]),
+ "The data of first and second access should be the same for private windows."
+ );
+
+ if (test.shouldBeRandomized) {
+ // Make sure the noises are different between normal window and private
+ // windows.
+ result = test.isDataRandomized(test.name, privateData[0], data[0]);
+ is(
+ result,
+ enabled,
+ `The image data between the normal window and the private window are ${
+ enabled ? "different" : "the same"
+ } for ${test.name}.`
+ );
+
+ // Verify the telemetry is recorded if canvas randomization is enabled.
+ if (enabled) {
+ await Services.fog.testFlushAllChildren();
+
+ Assert.greater(
+ Glean.fingerprintingProtection.canvasNoiseCalculateTime.testGetValue()
+ .sum,
+ 0,
+ "The telemetry of canvas randomization is recorded."
+ );
+ }
+ }
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(privateTab);
+ await BrowserTestUtils.closeWindow(privateWindow);
+}
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ // Extract the original canvas data without random noise.
+ for (let test of TEST_CASES) {
+ let data = await runExtractCanvasData(test, tab);
+ test.originalData = data[0];
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function run_tests_with_randomization_enabled() {
+ await runTest(true);
+});
+
+add_task(async function run_tests_with_randomization_disabled() {
+ await runTest(false);
+});