/* 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 https://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: isDataRandomizedFuzzy, }, { 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: isDataRandomizedFuzzy, }, { 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: isDataRandomizedNotEqual, }, { 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: isDataRandomizedNotEqual, }, { 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: isDataRandomizedNotEqual, }, { 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: isDataRandomizedGreaterThanZero, }, { 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: isDataRandomizedGreaterThanZero, }, { 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: isDataRandomizedGreaterThanZero, }, { 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: isDataRandomizedGreaterThanZero, }, { 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: isDataRandomizedGreaterThanZero, }, { 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: isDataRandomizedGreaterThanZero, }, { 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: isDataRandomizedFuzzy, }, ]; 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.canvasNoiseCalculateTime2.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_enabled_with_siphash() { await SpecialPowers.pushPrefEnv({ set: [ ["privacy.resistFingerprinting.randomization.canvas.use_siphash", true], ], }); await runTest(true); await SpecialPowers.popPrefEnv(); }); add_task(async function run_tests_with_randomization_disabled() { await runTest(false); });