diff options
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests/browser')
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 }); + }); +}; |