573 lines
17 KiB
JavaScript
573 lines
17 KiB
JavaScript
/* 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);
|
|
});
|