diff options
Diffstat (limited to 'toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js')
-rw-r--r-- | toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js | 335 |
1 files changed, 335 insertions, 0 deletions
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..dd7c292fa4 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js @@ -0,0 +1,335 @@ +/* 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. + */ + +var TEST_CASES = [ + { + name: "CanvasRenderingContext2D.getImageData() with a offscreen canvas", + extractCanvasData: async _ => { + 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 imageData = context.getImageData(0, 0, 100, 100); + + 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: "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 = "#EE2222"; + context.fillRect(0, 0, 100, 100); + context.fillStyle = "#2222EE"; + context.fillRect(20, 20, 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(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", + extractCanvasData: async _ => { + 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 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(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", + extractCanvasData: async _ => { + 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 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(name, data1, data2) { + let diffCnt = countDifferencesInArrayBuffers(data1, data2); + info(`For ${name} there are ${diffCnt} bits are different.`); + return diffCnt > 0; + }, + }, +]; + +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} in the worker`); + + // Clear telemetry before starting test. + Services.fog.testResetFOG(); + + let data = await await runFunctionInWorker( + tab.linkedBrowser, + test.extractCanvasData + ); + + let result = test.isDataRandomized(test.name, data[0], test.originalData); + is( + result, + enabled, + `The image data for for '${test.name}' is ${ + enabled ? "randomized" : "the same" + }.` + ); + + ok( + !test.isDataRandomized(test.name, data[0], data[1]), + `The data for '${test.name}' 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( + test.name, + privateData[0], + test.originalData, + true + ); + is( + result, + enabled, + `The private image data for '${test.name}' is ${ + enabled ? "randomized" : "the same" + }.` + ); + + ok( + !test.isDataRandomized(test.name, privateData[0], privateData[1]), + `"The data for '${test.name}' of first and second access should be the same.` + ); + + // 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 for '${ + test.name + }' between the normal window and the private window are ${ + enabled ? "different" : "the same" + }.` + ); + } + + // 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 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); +}); |