diff options
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js')
-rw-r--r-- | toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js | 589 |
1 files changed, 589 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..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); +}); |