From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../tests/browser/browser.toml | 33 ++ .../browser_canvas_fingerprinter_telemetry.js | 111 ++++ .../tests/browser/browser_canvas_randomization.js | 658 +++++++++++++++++++++ .../browser/browser_canvas_randomization_worker.js | 335 +++++++++++ .../browser_fingerprintingRemoteOverrides.js | 406 +++++++++++++ .../browser/browser_fingerprintingWebCompat.js | 559 +++++++++++++++++ .../browser_fingerprinting_randomization_key.js | 490 +++++++++++++++ .../browser_font_fingerprinter_telemetry.js | 97 +++ .../browser_fpiServiceWorkers_fingerprinting.js | 85 +++ .../browser/browser_rfp_canvasplaceholder_pdfjs.js | 52 ++ ...owser_serviceWorker_fingerprinting_webcompat.js | 172 ++++++ .../tests/browser/canvas-fingerprinter.html | 22 + .../resistfingerprinting/tests/browser/empty.html | 8 + .../tests/browser/file_pdf.pdf | 12 + .../tests/browser/font-fingerprinter.html | 102 ++++ .../resistfingerprinting/tests/browser/head.js | 227 +++++++ .../tests/browser/scriptExecPage.html | 37 ++ .../tests/browser/serviceWorker.js | 15 + .../tests/browser/testHelpers.js | 30 + .../tests/browser/testPage.html | 9 + .../resistfingerprinting/tests/browser/worker.js | 7 + .../resistfingerprinting/tests/chrome/chrome.toml | 4 + .../tests/chrome/test_spoof_english.html | 117 ++++ .../resistfingerprinting/tests/gtest/moz.build | 5 + .../tests/gtest/test_reduceprecision.cpp | 566 ++++++++++++++++++ .../resistfingerprinting/tests/moz.build | 9 + 26 files changed, 4168 insertions(+) create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser.toml create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/empty.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf create mode 100644 toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/head.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/testHelpers.js create mode 100644 toolkit/components/resistfingerprinting/tests/browser/testPage.html create mode 100644 toolkit/components/resistfingerprinting/tests/browser/worker.js create mode 100644 toolkit/components/resistfingerprinting/tests/chrome/chrome.toml create mode 100644 toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html create mode 100644 toolkit/components/resistfingerprinting/tests/gtest/moz.build create mode 100644 toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp create mode 100644 toolkit/components/resistfingerprinting/tests/moz.build (limited to 'toolkit/components/resistfingerprinting/tests') diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.toml b/toolkit/components/resistfingerprinting/tests/browser/browser.toml new file mode 100644 index 0000000000..213d6d4287 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser.toml @@ -0,0 +1,33 @@ +[DEFAULT] +support-files = [ + "canvas-fingerprinter.html", + "empty.html", + "font-fingerprinter.html", + "head.js", + "scriptExecPage.html", + "serviceWorker.js", + "testHelpers.js", + "testPage.html", + "worker.js", +] + +["browser_canvas_fingerprinter_telemetry.js"] + +["browser_canvas_randomization.js"] + +["browser_canvas_randomization_worker.js"] + +["browser_fingerprintingRemoteOverrides.js"] + +["browser_fingerprintingWebCompat.js"] + +["browser_fingerprinting_randomization_key.js"] + +["browser_font_fingerprinter_telemetry.js"] + +["browser_fpiServiceWorkers_fingerprinting.js"] + +["browser_rfp_canvasplaceholder_pdfjs.js"] +support-files = ["file_pdf.pdf"] + +["browser_serviceWorker_fingerprinting_webcompat.js"] diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js new file mode 100644 index 0000000000..d2a33a4347 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js @@ -0,0 +1,111 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_PAGE_NORMAL = TEST_PATH + "empty.html"; +const TEST_PAGE_FINGERPRINTER = TEST_PATH + "canvas-fingerprinter.html"; + +const TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB = "CANVAS_FINGERPRINTING_PER_TAB"; + +const KEY_UNKNOWN = "unknown"; +const KEY_KNOWN_TEXT = "known_text"; + +async function clearTelemetry() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry + .getKeyedHistogramById(TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB) + .clear(); +} + +async function getKeyedHistogram(histogram_id, key, bucket, checkCntFn) { + let histogram; + + // Wait until the telemetry probe appears. + await TestUtils.waitForCondition(() => { + let histograms = Services.telemetry.getSnapshotForKeyedHistograms( + "main", + false /* clear */ + ).parent; + + histogram = histograms[histogram_id]; + + let checkRes = false; + + if (histogram && histogram[key]) { + checkRes = checkCntFn ? checkCntFn(histogram[key].values[bucket]) : true; + } + + return checkRes; + }); + + return histogram[key].values[bucket] || 0; +} + +async function checkKeyedHistogram(histogram_id, key, bucket, expectedCnt) { + let cnt = await getKeyedHistogram(histogram_id, key, bucket, cnt => { + if (cnt === undefined) { + cnt = 0; + } + + return cnt == expectedCnt; + }); + + is(cnt, expectedCnt, "There should be expected count in keyed telemetry."); +} + +add_setup(async function () { + await clearTelemetry(); +}); + +add_task(async function test_canvas_fingerprinting_telemetry() { + let promiseWindowDestroyed = BrowserUtils.promiseObserved( + "window-global-destroyed" + ); + + // First, we open a page without any canvas fingerprinters + await BrowserTestUtils.withNewTab(TEST_PAGE_NORMAL, async _ => {}); + + // Make sure the tab was closed properly before checking Telemetry. + await promiseWindowDestroyed; + + // Check that the telemetry has been record properly for normal page. The + // telemetry should show there was no known fingerprinting attempt. + await checkKeyedHistogram( + TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB, + KEY_UNKNOWN, + 0, + 1 + ); + + await clearTelemetry(); +}); + +add_task(async function test_canvas_fingerprinting_telemetry() { + let promiseWindowDestroyed = BrowserUtils.promiseObserved( + "window-global-destroyed" + ); + + // Now open a page with a canvas fingerprinter + await BrowserTestUtils.withNewTab(TEST_PAGE_FINGERPRINTER, async _ => {}); + + // Make sure the tab was closed properly before checking Telemetry. + await promiseWindowDestroyed; + + // The telemetry should show a a known fingerprinting text and a known + // canvas fingerprinter. + await checkKeyedHistogram( + TELEMETRY_CANVAS_FINGERPRINTING_PER_TAB, + KEY_KNOWN_TEXT, + 6, // CanvasFingerprinter::eVariant4 + 1 + ); + + await clearTelemetry(); +}); 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..8b4be78d53 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js @@ -0,0 +1,658 @@ +/* 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() 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(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: "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(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: "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(name, data1, data2) { + return data1 !== data2; + }, + }, + { + 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(name, data1, data2) { + return data1 !== data2; + }, + }, + { + 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(name, data1, data2) { + return data1 !== data2; + }, + }, + { + 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(name, data1, data2) { + let diffCnt = countDifferencesInArrayBuffers(data1, data2); + info(`For ${name} there are ${diffCnt} bits are different.`); + return diffCnt > 0; + }, + }, + { + 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(name, data1, data2) { + let diffCnt = countDifferencesInArrayBuffers(data1, data2); + info(`For ${name} there are ${diffCnt} bits are different.`); + return diffCnt > 0; + }, + }, + { + 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(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 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(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", + 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(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", + 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(name, data1, data2) { + let diffCnt = countDifferencesInArrayBuffers(data1, data2); + info(`For ${name} there are ${diffCnt} bits are different.`); + return diffCnt > 0; + }, + }, + { + 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(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; + }, + }, +]; + +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.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. + 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_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..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); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js new file mode 100644 index 0000000000..29c7c9170b --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js @@ -0,0 +1,406 @@ +/* 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/. */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const COLLECTION_NAME = "fingerprinting-protection-overrides"; + +// The javascript bitwise operator only support 32bits. So, we only test +// RFPTargets that is under low 32 bits. +const TARGET_DEFAULT = extractLow32Bits( + Services.rfp.enabledFingerprintingProtections +); +const TARGET_PointerEvents = 0x00000002; +const TARGET_CanvasRandomization = 0x000000100; +const TARGET_WindowOuterSize = 0x002000000; +const TARGET_Gamepad = 0x00800000; + +// A helper function to filter high 32 bits. +function extractLow32Bits(value) { + return value & 0xffffffff; +} + +const TEST_CASES = [ + // Test simple addition. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.org", + }, + ], + expects: [ + { + domain: "example.org", + overrides: TARGET_DEFAULT | TARGET_WindowOuterSize, + }, + { domain: "example.com", noEntry: true }, + ], + }, + // Test simple subtraction + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "-WindowOuterSize", + firstPartyDomain: "example.org", + }, + ], + expects: [ + { + domain: "example.org", + overrides: TARGET_DEFAULT & ~TARGET_WindowOuterSize, + }, + { domain: "example.com", noEntry: true }, + ], + }, + // Test simple subtraction for default targets. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "-CanvasRandomization", + firstPartyDomain: "example.org", + }, + ], + expects: [ + { + domain: "example.org", + overrides: TARGET_DEFAULT & ~TARGET_CanvasRandomization, + }, + { domain: "example.com", noEntry: true }, + ], + }, + // Test multiple targets + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + firstPartyDomain: "example.org", + }, + ], + expects: [ + { + domain: "example.org", + overrides: + (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) & + ~TARGET_Gamepad, + }, + { domain: "example.com", noEntry: true }, + ], + }, + // Test multiple entries + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + firstPartyDomain: "example.org", + }, + { + id: "2", + last_modified: 1000000000000001, + overrides: "+Gamepad", + firstPartyDomain: "example.com", + }, + ], + expects: [ + { + domain: "example.org", + overrides: + (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) & + ~TARGET_Gamepad, + }, + { + domain: "example.com", + overrides: TARGET_DEFAULT | TARGET_Gamepad, + }, + ], + }, + // Test an entry with both first-party and third-party domains. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + firstPartyDomain: "example.org", + thirdPartyDomain: "example.com", + }, + ], + expects: [ + { + domain: "example.org,example.com", + overrides: + (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) & + ~TARGET_Gamepad, + }, + ], + }, + // Test an entry with the first-party set to a wildcard. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + firstPartyDomain: "*", + }, + ], + expects: [ + { + domain: "*", + overrides: + (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) & + ~TARGET_Gamepad, + }, + ], + }, + // Test a entry with the first-party set to a wildcard and the third-party set + // to a domain. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + firstPartyDomain: "*", + thirdPartyDomain: "example.com", + }, + ], + expects: [ + { + domain: "*,example.com", + overrides: + (TARGET_DEFAULT | TARGET_WindowOuterSize | TARGET_PointerEvents) & + ~TARGET_Gamepad, + }, + ], + }, + // Test an entry with all targets disabled using '-AllTargets' but only enable one target. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "-AllTargets,+WindowOuterSize", + firstPartyDomain: "*", + thirdPartyDomain: "example.com", + }, + ], + expects: [ + { + domain: "*,example.com", + overrides: TARGET_WindowOuterSize, + }, + ], + }, + // Test an entry with all targets disabled using '-AllTargets' but enable some targets. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "-AllTargets,+WindowOuterSize,+Gamepad", + firstPartyDomain: "*", + thirdPartyDomain: "example.com", + }, + ], + expects: [ + { + domain: "*,example.com", + overrides: TARGET_WindowOuterSize | TARGET_Gamepad, + }, + ], + }, + // Test an invalid entry with only third party domain. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + thirdPartyDomain: "example.com", + }, + ], + expects: [ + { + domain: "example.com", + noEntry: true, + }, + ], + }, + // Test an invalid entry with both wildcards. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize,+PointerEvents,-Gamepad", + firstPartyDomain: "*", + thirdPartyDomain: "*", + }, + ], + expects: [ + { + domain: "example.org", + noEntry: true, + }, + ], + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.fingerprintingProtection.remoteOverrides.testing", true]], + }); + + registerCleanupFunction(() => { + Services.rfp.cleanAllOverrides(); + }); +}); + +add_task(async function test_remote_settings() { + // Add initial empty record. + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + for (let test of TEST_CASES) { + info(`Testing with entry ${JSON.stringify(test.entires)}`); + + // Create a promise for waiting the overrides get updated. + let promise = promiseObserver("fpp-test:set-overrides-finishes"); + + // Trigger the fingerprinting overrides update by a remote settings sync. + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: test.entires, + }, + }); + await promise; + + ok(true, "Got overrides update"); + + for (let expect of test.expects) { + // Get the addition and subtraction flags for the domain. + try { + let overrides = extractLow32Bits( + Services.rfp.getFingerprintingOverrides(expect.domain) + ); + + // Verify if the flags are matching to expected values. + is(overrides, expect.overrides, "The override value is correct."); + } catch (e) { + ok(expect.noEntry, "The override entry doesn't exist."); + } + } + } + + db.clear(); +}); + +add_task(async function test_pref() { + for (let test of TEST_CASES) { + info(`Testing with entry ${JSON.stringify(test.entires)}`); + + // Create a promise for waiting the overrides get updated. + let promise = promiseObserver("fpp-test:set-overrides-finishes"); + + // Trigger the fingerprinting overrides update by setting the pref. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.fingerprintingProtection.granularOverrides", + JSON.stringify(test.entires), + ], + ], + }); + + await promise; + ok(true, "Got overrides update"); + + for (let expect of test.expects) { + try { + // Get the addition and subtraction flags for the domain. + let overrides = extractLow32Bits( + Services.rfp.getFingerprintingOverrides(expect.domain) + ); + + // Verify if the flags are matching to expected values. + is(overrides, expect.overrides, "The override value is correct."); + } catch (e) { + ok(expect.noEntry, "The override entry doesn't exist."); + } + } + } +}); + +// Verify if the pref overrides the remote settings. +add_task(async function test_pref_override_remote_settings() { + // Add initial empty record. + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + // Trigger a remote settings sync. + let promise = promiseObserver("fpp-test:set-overrides-finishes"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.org", + }, + ], + }, + }); + await promise; + + // Then, setting the pref. + promise = promiseObserver("fpp-test:set-overrides-finishes"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.fingerprintingProtection.granularOverrides", + JSON.stringify([ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+PointerEvents,-WindowOuterSize,-Gamepad", + firstPartyDomain: "example.org", + }, + ]), + ], + ], + }); + await promise; + + // Get the addition and subtraction flags for the domain. + let overrides = extractLow32Bits( + Services.rfp.getFingerprintingOverrides("example.org") + ); + + // Verify if the flags are matching to the pref settings. + is( + overrides, + (TARGET_DEFAULT | TARGET_PointerEvents) & + ~TARGET_Gamepad & + ~TARGET_WindowOuterSize, + "The override addition value is correct." + ); + + db.clear(); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js new file mode 100644 index 0000000000..1a882fc63d --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js @@ -0,0 +1,559 @@ +/* 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/. */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const COLLECTION_NAME = "fingerprinting-protection-overrides"; + +const TOP_DOMAIN = "https://example.com"; +const THIRD_PARTY_DOMAIN = "https://example.org"; + +const SPOOFED_HW_CONCURRENCY = 2; +const DEFAULT_HW_CONCURRENCY = navigator.hardwareConcurrency; + +const TEST_TOP_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TOP_DOMAIN + ) + "empty.html"; +const TEST_THIRD_PARTY_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + THIRD_PARTY_DOMAIN + ) + "empty.html"; + +const TEST_CASES = [ + // Test the wildcard entry. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "*", + }, + ], + expects: { + windowOuter: { + top: true, + firstParty: true, + thirdParty: true, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test the entry that only applies to the first-party context. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.com", + }, + ], + expects: { + windowOuter: { + top: true, + firstParty: true, + thirdParty: false, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test the entry that applies to every context under the first-party domain. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.com", + thirdPartyDomain: "*", + }, + ], + expects: { + windowOuter: { + top: true, + firstParty: true, + thirdParty: true, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test the entry that applies to the specific third-party domain under the first-party domain. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.com", + thirdPartyDomain: "example.org", + }, + ], + expects: { + windowOuter: { + top: false, + firstParty: false, + thirdParty: true, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test the entry that applies to the every third-party domain under any first-party domains. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "*", + thirdPartyDomain: "example.org", + }, + ], + expects: { + windowOuter: { + top: false, + firstParty: false, + thirdParty: true, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test a entry that doesn't apply to the current contexts. The first-party + // domain is different. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.net", + }, + ], + expects: { + windowOuter: { + top: false, + firstParty: false, + thirdParty: false, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test a entry that doesn't apply to the current contexts. The first-party + // domain is different. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.net", + thirdPartyDomain: "*", + }, + ], + expects: { + windowOuter: { + top: false, + firstParty: false, + thirdParty: false, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test a entry that doesn't apply to a the current contexts. Both first-party + // and third-party domains are different. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.net", + thirdPartyDomain: "example.com", + }, + ], + expects: { + windowOuter: { + top: false, + firstParty: false, + thirdParty: false, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: false, + }, + }, + }, + // Test multiple entries that enable HW concurrency in the first-party context + // and WindowOuter in the third-party context. + { + entires: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.com", + }, + { + id: "2", + last_modified: 1000000000000001, + overrides: "+NavigatorHWConcurrency", + firstPartyDomain: "example.com", + thirdPartyDomain: "example.org", + }, + ], + expects: { + windowOuter: { + top: true, + firstParty: true, + thirdParty: false, + }, + hwConcurrency: { + top: false, + firstParty: false, + thirdParty: true, + }, + }, + }, +]; + +async function openAndSetupTestPage() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + // Create a first-party and a third-party iframe in the tab. + let { firstPartyBC, thirdPartyBC } = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_TOP_PAGE, TEST_THIRD_PARTY_PAGE], + async (firstPartySrc, thirdPartySrc) => { + let firstPartyFrame = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + firstPartyFrame.onload = resolve; + }); + content.document.body.appendChild(firstPartyFrame); + firstPartyFrame.src = firstPartySrc; + await loading; + + let thirdPartyFrame = content.document.createElement("iframe"); + loading = new content.Promise(resolve => { + thirdPartyFrame.onload = resolve; + }); + content.document.body.appendChild(thirdPartyFrame); + thirdPartyFrame.src = thirdPartySrc; + await loading; + + return { + firstPartyBC: firstPartyFrame.browsingContext, + thirdPartyBC: thirdPartyFrame.browsingContext, + }; + } + ); + + return { tab, firstPartyBC, thirdPartyBC }; +} + +async function openAndSetupTestPageForPopup() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + // Create a third-party iframe and a pop-up from this iframe. + let { thirdPartyFrameBC, popupBC } = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_THIRD_PARTY_PAGE], + async thirdPartySrc => { + let thirdPartyFrame = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + thirdPartyFrame.onload = resolve; + }); + content.document.body.appendChild(thirdPartyFrame); + thirdPartyFrame.src = thirdPartySrc; + await loading; + + let popupBC = await SpecialPowers.spawn( + thirdPartyFrame, + [thirdPartySrc], + async src => { + let win; + let loading = new content.Promise(resolve => { + win = content.open(src); + win.onload = resolve; + }); + await loading; + + return win.browsingContext; + } + ); + + return { + thirdPartyFrameBC: thirdPartyFrame.browsingContext, + popupBC, + }; + } + ); + + return { tab, thirdPartyFrameBC, popupBC }; +} + +async function verifyResultInTab(tab, firstPartyBC, thirdPartyBC, expected) { + let testWindowOuter = enabled => { + if (enabled) { + ok( + content.wrappedJSObject.outerHeight == + content.wrappedJSObject.innerHeight && + content.wrappedJSObject.outerWidth == + content.wrappedJSObject.innerWidth, + "Fingerprinting target WindowOuterSize is enabled for WindowOuterSize." + ); + } else { + ok( + content.wrappedJSObject.outerHeight != + content.wrappedJSObject.innerHeight || + content.wrappedJSObject.outerWidth != + content.wrappedJSObject.innerWidth, + "Fingerprinting target WindowOuterSize is not enabled for WindowOuterSize." + ); + } + }; + + let testHWConcurrency = expected => { + is( + content.wrappedJSObject.navigator.hardwareConcurrency, + expected, + "The hardware concurrency is expected." + ); + }; + + info( + `Verify top-level context with fingerprinting protection is ${ + expected.top ? "enabled" : "not enabled" + }.` + ); + await SpecialPowers.spawn( + tab.linkedBrowser, + [expected.windowOuter.top], + testWindowOuter + ); + let expectHWConcurrencyTop = expected.hwConcurrency.top + ? SPOOFED_HW_CONCURRENCY + : DEFAULT_HW_CONCURRENCY; + await SpecialPowers.spawn( + tab.linkedBrowser, + [expectHWConcurrencyTop], + testHWConcurrency + ); + + let workerTopResult = await runFunctionInWorker( + tab.linkedBrowser, + async _ => { + return navigator.hardwareConcurrency; + } + ); + is( + workerTopResult, + expectHWConcurrencyTop, + "The top worker reports the expected HW concurrency." + ); + + info( + `Verify first-party context with fingerprinting protection is ${ + expected.firstParty ? "enabled" : "not enabled" + }.` + ); + await SpecialPowers.spawn( + firstPartyBC, + [expected.windowOuter.firstParty], + testWindowOuter + ); + let expectHWConcurrencyFirstParty = expected.hwConcurrency.firstParty + ? SPOOFED_HW_CONCURRENCY + : DEFAULT_HW_CONCURRENCY; + await SpecialPowers.spawn( + firstPartyBC, + [expectHWConcurrencyFirstParty], + testHWConcurrency + ); + + let workerFirstPartyResult = await runFunctionInWorker( + firstPartyBC, + async _ => { + return navigator.hardwareConcurrency; + } + ); + is( + workerFirstPartyResult, + expectHWConcurrencyFirstParty, + "The first-party worker reports the expected HW concurrency." + ); + + info( + `Verify third-party context with fingerprinting protection is ${ + expected.thirdParty ? "enabled" : "not enabled" + }.` + ); + await SpecialPowers.spawn( + thirdPartyBC, + [expected.windowOuter.thirdParty], + testWindowOuter + ); + let expectHWConcurrencyThirdParty = expected.hwConcurrency.thirdParty + ? SPOOFED_HW_CONCURRENCY + : DEFAULT_HW_CONCURRENCY; + await SpecialPowers.spawn( + thirdPartyBC, + [expectHWConcurrencyThirdParty], + testHWConcurrency + ); + + let workerThirdPartyResult = await runFunctionInWorker( + thirdPartyBC, + async _ => { + return navigator.hardwareConcurrency; + } + ); + is( + workerThirdPartyResult, + expectHWConcurrencyThirdParty, + "The third-party worker reports the expected HW concurrency." + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.fingerprintingProtection.remoteOverrides.testing", true], + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.pbmode", true], + ], + }); + + registerCleanupFunction(() => { + Services.rfp.cleanAllOverrides(); + }); +}); + +add_task(async function () { + // Add initial empty record. + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + for (let test of TEST_CASES) { + // Create a promise for waiting the overrides get updated. + let promise = promiseObserver("fpp-test:set-overrides-finishes"); + + // Trigger the fingerprinting overrides update by a remote settings sync. + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: test.entires, + }, + }); + await promise; + + ok(true, "Got overrides update"); + + let { tab, firstPartyBC, thirdPartyBC } = await openAndSetupTestPage(); + + await verifyResultInTab(tab, firstPartyBC, thirdPartyBC, test.expects); + + BrowserTestUtils.removeTab(tab); + } + + db.clear(); +}); + +// A test to verify that a pop-up inherits overrides from the third-party iframe +// that opens it. +add_task(async function test_popup_inheritance() { + // Add initial empty record. + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + // Create a promise for waiting the overrides get updated. + let promise = promiseObserver("fpp-test:set-overrides-finishes"); + + // Trigger the fingerprinting overrides update by a remote settings sync. + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "1", + last_modified: 1000000000000001, + overrides: "+WindowOuterSize", + firstPartyDomain: "example.com", + thirdPartyDomain: "example.org", + }, + ], + }, + }); + await promise; + + let { tab, thirdPartyFrameBC, popupBC } = + await openAndSetupTestPageForPopup(); + + // Ensure the third-party iframe has the correct overrides. + await SpecialPowers.spawn(thirdPartyFrameBC, [], _ => { + ok( + content.wrappedJSObject.outerHeight == + content.wrappedJSObject.innerHeight && + content.wrappedJSObject.outerWidth == + content.wrappedJSObject.innerWidth, + "Fingerprinting target WindowOuterSize is enabled for third-party iframe." + ); + }); + + // Verify the popup inherits overrides from the opener. + await SpecialPowers.spawn(popupBC, [], _ => { + ok( + content.wrappedJSObject.outerHeight == + content.wrappedJSObject.innerHeight && + content.wrappedJSObject.outerWidth == + content.wrappedJSObject.innerWidth, + "Fingerprinting target WindowOuterSize is enabled for the pop-up." + ); + + content.close(); + }); + + BrowserTestUtils.removeTab(tab); + + db.clear(); +}); 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..f2d037491d --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprinting_randomization_key.js @@ -0,0 +1,490 @@ +let { ForgetAboutSite } = ChromeUtils.importESModule( + "resource://gre/modules/ForgetAboutSite.sys.mjs" +); + +requestLongerTimeout(2); + +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 resistance is disabled. +add_task(async function test_randomization_disabled_with_rfp_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", false], + ["privacy.resistFingerprinting.pbmode", false], + ["privacy.fingerprintingProtection", false], + ["privacy.fingerprintingProtection.pbmode", 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", 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", 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); + + let promisePBExit = TestUtils.topicObserved("last-pb-context-exited"); + await BrowserTestUtils.closeWindow(privateWin); + await promisePBExit; + + 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", false], + ["privacy.resistFingerprinting.pbmode", true], + ["privacy.fingerprintingProtection", false], + ["privacy.fingerprintingProtection.pbmode", false], + ], + }); + + // 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); +}); + +// Test that the random key gets reset when the site data gets cleared. +add_task(async function test_reset_random_key_when_clear_site_data() { + // Enable fingerprinting randomization key generation. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + // Open a tab and get randomization key from the test domain. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + let keyHex = await getRandomKeyHexFromBrowser( + tab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + // Open another tab and get randomization key from another domain. + let anotherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_DOMAIN_ANOTHER_PAGE + ); + + let keyHexAnother = await getRandomKeyHexFromBrowser( + anotherTab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(anotherTab); + + // Call ForgetAboutSite for the test domain. + await ForgetAboutSite.removeDataFromDomain("example.com"); + + // Open the tab for the test domain again and verify the key is reset. + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + let keyHexNew = await getRandomKeyHexFromBrowser( + tab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + // Open the tab for another domain again and verify the key is intact. + anotherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_DOMAIN_ANOTHER_PAGE + ); + let keyHexAnotherNew = await getRandomKeyHexFromBrowser( + anotherTab.linkedBrowser, + TEST_PAGE, + TEST_DOMAIN_THIRD_PAGE + ); + + // Ensure the keys are different for the test domain. + isnot(keyHexNew, keyHex, "Ensure the new key is different from the old one."); + + // Ensure the key for another domain isn't changed. + is( + keyHexAnother, + keyHexAnotherNew, + "Ensure the key of another domain isn't reset." + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(anotherTab); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js new file mode 100644 index 0000000000..2231197eed --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js @@ -0,0 +1,97 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_PAGE_NORMAL = TEST_PATH + "empty.html"; +const TEST_PAGE_FINGERPRINTER = TEST_PATH + "font-fingerprinter.html"; + +const TELEMETRY_FONT_FINGERPRINTING_PER_TAB = "FONT_FINGERPRINTING_PER_TAB"; + +async function clearTelemetry() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry + .getHistogramById(TELEMETRY_FONT_FINGERPRINTING_PER_TAB) + .clear(); +} + +async function getHistogram(histogram_id, bucket, checkCntFn) { + let histogram; + + // Wait until the telemetry probe appears. + await TestUtils.waitForCondition(() => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).parent; + + histogram = histograms[histogram_id]; + + let checkRes = false; + + if (histogram) { + checkRes = checkCntFn ? checkCntFn(histogram.values[bucket]) : true; + } + + return checkRes; + }); + + return histogram.values[bucket] || 0; +} + +async function checkHistogram(histogram_id, bucket, expectedCnt) { + let cnt = await getHistogram(histogram_id, bucket, cnt => { + if (cnt === undefined) { + cnt = 0; + } + + return cnt == expectedCnt; + }); + + is(cnt, expectedCnt, "There should be expected count in telemetry."); +} + +add_setup(async function () { + await clearTelemetry(); +}); + +add_task(async function test_canvas_fingerprinting_telemetry() { + let promiseWindowDestroyed = BrowserUtils.promiseObserved( + "window-global-destroyed" + ); + + // First, we open a page without any canvas fingerprinters + await BrowserTestUtils.withNewTab(TEST_PAGE_NORMAL, async _ => {}); + + // Make sure the tab was closed properly before checking Telemetry. + await promiseWindowDestroyed; + + // Check that the telemetry has been record properly for normal page. The + // telemetry should show there was no known fingerprinting attempt. + await checkHistogram(TELEMETRY_FONT_FINGERPRINTING_PER_TAB, 0 /* false */, 1); + + await clearTelemetry(); +}); + +add_task(async function test_canvas_fingerprinting_telemetry() { + let promiseWindowDestroyed = BrowserUtils.promiseObserved( + "window-global-destroyed" + ); + + // Now open a page with a canvas fingerprinter + await BrowserTestUtils.withNewTab(TEST_PAGE_FINGERPRINTER, async _ => {}); + + // Make sure the tab was closed properly before checking Telemetry. + await promiseWindowDestroyed; + + // The telemetry should show one font fingerprinting attempt. + await checkHistogram(TELEMETRY_FONT_FINGERPRINTING_PER_TAB, 1 /* true */, 1); + + await clearTelemetry(); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js new file mode 100644 index 0000000000..64279ae442 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js @@ -0,0 +1,85 @@ +/* import-globals-from testHelpers.js */ + +// This test ensures that a service worker for an exempted domain is exempted when it is +// in the first party context, and not exempted when it is in a third party context. + +runTestInFirstAndThirdPartyContexts( + "ServiceWorkers - Check that RFP correctly is exempted and not exempted when FPI is enabled", + async win => { + // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency + // It can't be set externally and then captured because this function gets stringified and + // then evaled in a new scope. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstParty.isolate", false], + ["privacy.resistFingerprinting", false], + ], + }); + + var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + + await SpecialPowers.popPrefEnv(); + + // Register service worker for the first-party window. + if (!win.sw) { + win.sw = await registerServiceWorker(win, "serviceWorker.js"); + } + + // Check navigator HW concurrency from the first-party service worker. + let res = await sendAndWaitWorkerMessage( + win.sw, + win.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + console.info( + "First Party, got: " + + res.value + + " Expected: " + + DEFAULT_HARDWARE_CONCURRENCY + ); + is( + res.value, + DEFAULT_HARDWARE_CONCURRENCY, + "As a first party, HW Concurrency should not be spoofed" + ); + }, + async win => { + let SPOOFED_HW_CONCURRENCY = 2; + + // Register service worker for the third-party window. + if (!win.sw) { + win.sw = await registerServiceWorker(win, "serviceWorker.js"); + } + + // Check navigator HW concurrency from the first-party service worker. + let res = await sendAndWaitWorkerMessage( + win.sw, + win.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + console.info( + "Third Party, got: " + res.value + " Expected: " + SPOOFED_HW_CONCURRENCY + ); + is( + res.value, + SPOOFED_HW_CONCURRENCY, + "As a third party, HW Concurrency should be spoofed" + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.firstparty.isolate", true], + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", "example.com"], + ] +); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js b/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js new file mode 100644 index 0000000000..d80abfdc81 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_rfp_canvasplaceholder_pdfjs.js @@ -0,0 +1,52 @@ +const PDF_FILE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "file_pdf.pdf"; + +add_task(async function canvas_placeholder_pdfjs() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PDF_FILE); + + const data = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + function extractCanvasData() { + const canvas = document.createElement("canvas"); + canvas.width = 10; + canvas.height = 10; + + const context = canvas.getContext("2d"); + + context.fillStyle = "#332211"; + context.fillRect(0, 0, 10, 10); + + // Add the canvas element to the document + document.body.appendChild(canvas); + + return context.getImageData(0, 0, 10, 10).data; + } + + return content.eval(`(${extractCanvasData})()`); + }); + + is(data.length, 10 * 10 * 4, "correct canvas data size"); + + let failure = false; + for (var i = 0; i < 10 * 10 * 4; i += 4) { + if ( + data[i] != 0x33 || + data[i + 1] != 0x22 || + data[i + 2] != 0x11 || + data[i + 3] != 0xff + ) { + ok(false, `incorrect data ${data.slice(i, i + 4)} @ ${i}..${i + 3}`); + failure = true; + break; + } + } + ok(!failure, "canvas is correct"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js new file mode 100644 index 0000000000..a9bab38e61 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from testHelpers.js */ + +runTestInFirstAndThirdPartyContexts( + "ServiceWorkers - Ensure the fingerprinting WebCompat overrides the fingerprinting protection in third-party context.", + async win => { + let SPOOFED_HW_CONCURRENCY = 2; + + // Register service worker for the first-party window. + if (!win.sw) { + win.sw = await registerServiceWorker(win, "serviceWorker.js"); + } + + // Check navigator HW concurrency from the first-party service worker. + let res = await sendAndWaitWorkerMessage( + win.sw, + win.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + is( + res.value, + SPOOFED_HW_CONCURRENCY, + "HW Concurrency should be spoofed in the first-party context" + ); + }, + + async win => { + // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency + // It can't be set externally and then captured because this function gets stringified and + // then evaled in a new scope. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.fingerprintingProtection", false]], + }); + + var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + + await SpecialPowers.popPrefEnv(); + + // Register service worker for the third-party window. + if (!win.sw) { + win.sw = await registerServiceWorker(win, "serviceWorker.js"); + } + + // Check navigator HW concurrency from the third-party service worker. + let res = await sendAndWaitWorkerMessage( + win.sw, + win.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + is( + res.value, + DEFAULT_HARDWARE_CONCURRENCY, + "HW Concurrency should not be spoofed in the third-party context" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"], + [ + "privacy.fingerprintingProtection.granularOverrides", + JSON.stringify([ + { + id: "1", + last_modified: 1000000000000001, + overrides: "-NavigatorHWConcurrency", + firstPartyDomain: "example.net", + thirdPartyDomain: "example.com", + }, + ]), + ], + ] +); + +runTestInFirstAndThirdPartyContexts( + "ServiceWorkers - Ensure the fingerprinting WebCompat overrides the fingerprinting protection in third-party context with FPI enabled.", + async win => { + let SPOOFED_HW_CONCURRENCY = 2; + + // Register service worker for the first-party window. + if (!win.sw) { + win.sw = await registerServiceWorker(win, "serviceWorker.js"); + } + + // Check navigator HW concurrency from the first-party service worker. + let res = await sendAndWaitWorkerMessage( + win.sw, + win.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + is( + res.value, + SPOOFED_HW_CONCURRENCY, + "HW Concurrency should be spoofed in the first-party context" + ); + }, + + async win => { + // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency + // It can't be set enternally and then captured because this function gets stringified and + // then evaled in a new scope. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.fingerprintingProtection", false]], + }); + + var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + + await SpecialPowers.popPrefEnv(); + + // Register service worker for the third-party window. + if (!win.sw) { + win.sw = await registerServiceWorker(win, "serviceWorker.js"); + } + + // Check navigator HW concurrency from the third-party service worker. + let res = await sendAndWaitWorkerMessage( + win.sw, + win.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + is( + res.value, + DEFAULT_HARDWARE_CONCURRENCY, + "HW Concurrency should not be spoofed in the third-party context" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.firstparty.isolate", true], + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"], + [ + "privacy.fingerprintingProtection.granularOverrides", + JSON.stringify([ + { + id: "1", + last_modified: 1000000000000001, + overrides: "-NavigatorHWConcurrency", + firstPartyDomain: "example.net", + thirdPartyDomain: "example.com", + }, + ]), + ], + ] +); diff --git a/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html b/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html new file mode 100644 index 0000000000..d48d289529 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/canvas-fingerprinter.html @@ -0,0 +1,22 @@ + + + + A page containing a canvas fingerprinter + + + + + + + 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 @@ + + + + An empty page for testing + + + + diff --git a/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf b/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf new file mode 100644 index 0000000000..593558f9a4 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/file_pdf.pdf @@ -0,0 +1,12 @@ +%PDF-1.0 +1 0 obj<>endobj 2 0 obj<>endobj 3 0 obj<>endobj +xref +0 4 +0000000000 65535 f +0000000010 00000 n +0000000053 00000 n +0000000102 00000 n +trailer<> +startxref +149 +%EOF \ No newline at end of file diff --git a/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html b/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html new file mode 100644 index 0000000000..63e542d67b --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/font-fingerprinter.html @@ -0,0 +1,102 @@ + + + + A page containing a font fingerprinter + + + + + diff --git a/toolkit/components/resistfingerprinting/tests/browser/head.js b/toolkit/components/resistfingerprinting/tests/browser/head.js new file mode 100644 index 0000000000..9d8ec19956 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/head.js @@ -0,0 +1,227 @@ +// 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 countDifferencesInUint8Arrays(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 countDifferencesInArrayBuffers(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); + + let differences = 0; + // compare the byte values of the two typed arrays + for (let i = 0; i < buffer1.byteLength; i++) { + if (view1.getUint8(i) !== view2.getUint8(i)) { + differences += 1; + } + } + + // the two buffers are the same + return differences; +} + +function promiseObserver(topic) { + return new Promise(resolve => { + let obs = (aSubject, aTopic, aData) => { + Services.obs.removeObserver(obs, aTopic); + resolve(aSubject); + }; + Services.obs.addObserver(obs, topic); + }); +} + +/** + * + * 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, + }); + }); + }); +} + +const TEST_FIRST_PARTY_CONTEXT_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.net" + ) + "testPage.html"; +const TEST_THIRD_PARTY_CONTEXT_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "scriptExecPage.html"; + +/** + * Executes provided callbacks in both first and third party contexts. + * + * @param {string} name - Name of the test. + * @param {Function} firstPartyCallback - The callback to be executed in the first-party context. + * @param {Function} thirdPartyCallback - The callback to be executed in the third-party context. + * @param {Function} [cleanupFunction] - A cleanup function to be called after the tests. + * @param {Array} [extraPrefs] - Optional. An array of preferences to be set before running the test. + */ +function runTestInFirstAndThirdPartyContexts( + name, + firstPartyCallback, + thirdPartyCallback, + cleanupFunction, + extraPrefs +) { + add_task(async _ => { + info("Starting test `" + name + "' in first and third party contexts"); + + await SpecialPowers.flushPrefEnv(); + + if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) { + await SpecialPowers.pushPrefEnv({ set: extraPrefs }); + } + + info("Creating a new tab"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_FIRST_PARTY_CONTEXT_PAGE + ); + + info("Creating a 3rd party content and a 1st party popup"); + let firstPartyPopupPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + TEST_THIRD_PARTY_CONTEXT_PAGE, + true + ); + + let thirdPartyBC = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_THIRD_PARTY_CONTEXT_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + content.open(url); + + return ifr.browsingContext; + } + ); + let firstPartyTab = await firstPartyPopupPromise; + let firstPartyBrowser = firstPartyTab.linkedBrowser; + + info("Sending code to the 3rd party content and the 1st party popup"); + let runningCallbackTask = async obj => { + return new content.Promise(resolve => { + content.postMessage({ callback: obj.callback }, "*"); + + content.addEventListener("message", function msg(event) { + // This is the event from above postMessage. + if (event.data.callback) { + return; + } + + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + }); + }; + + let firstPartyTask = SpecialPowers.spawn( + firstPartyBrowser, + [ + { + callback: firstPartyCallback.toString(), + }, + ], + runningCallbackTask + ); + + let thirdPartyTask = SpecialPowers.spawn( + thirdPartyBC, + [ + { + callback: thirdPartyCallback.toString(), + }, + ], + runningCallbackTask + ); + + await Promise.all([firstPartyTask, thirdPartyTask]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(firstPartyTab); + }); + + add_task(async _ => { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + }); +} diff --git a/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html b/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html new file mode 100644 index 0000000000..324cc2f261 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/scriptExecPage.html @@ -0,0 +1,37 @@ + + + A content page for executing script! + + + +

Here the content!

+ + + diff --git a/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js b/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js new file mode 100644 index 0000000000..e9ee00cbf8 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/serviceWorker.js @@ -0,0 +1,15 @@ +self.addEventListener("message", async e => { + let res = {}; + + switch (e.data.type) { + case "GetHWConcurrency": + res.result = "OK"; + res.value = navigator.hardwareConcurrency; + break; + + default: + res.result = "ERROR"; + } + + e.source.postMessage(res); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js b/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js new file mode 100644 index 0000000000..3feb5dfc11 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/testHelpers.js @@ -0,0 +1,30 @@ +async function registerServiceWorker(win, url) { + let reg = await win.navigator.serviceWorker.register(url); + if (reg.installing.state !== "activated") { + await new Promise(resolve => { + let w = reg.installing; + w.addEventListener("statechange", function onStateChange() { + if (w.state === "activated") { + w.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + + return reg.active; +} + +function sendAndWaitWorkerMessage(target, worker, message) { + return new Promise(resolve => { + worker.addEventListener( + "message", + msg => { + resolve(msg.data); + }, + { once: true } + ); + + target.postMessage(message); + }); +} 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 @@ + + + + A page with a javascript URL iframe + + + + + 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 }); + }); +}; diff --git a/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml b/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml new file mode 100644 index 0000000000..6ccea5d1e0 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/chrome/chrome.toml @@ -0,0 +1,4 @@ +[DEFAULT] +skip-if = ["os == 'android'"] + +["test_spoof_english.html"] diff --git a/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html b/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html new file mode 100644 index 0000000000..750743ba01 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/chrome/test_spoof_english.html @@ -0,0 +1,117 @@ + + + + + + Test for Bug 1486258 + + + + + +Mozilla Bug 1486258 +

+ +
+
+ + + + + diff --git a/toolkit/components/resistfingerprinting/tests/gtest/moz.build b/toolkit/components/resistfingerprinting/tests/gtest/moz.build new file mode 100644 index 0000000000..dcc6a7e12e --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/gtest/moz.build @@ -0,0 +1,5 @@ +UNIFIED_SOURCES += [ + "test_reduceprecision.cpp", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp new file mode 100644 index 0000000000..5d8b598ff0 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp @@ -0,0 +1,566 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +#include + +#include "gtest/gtest.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsServiceManagerUtils.h" +#include "nsRFPService.h" + +using namespace mozilla; + +// clang-format off +/* + Hello! Are you looking at this file because you got an error you don't understand? + Perhaps something that looks like the following? + + toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp:15: Failure + Expected: reduced1 + Which is: 2064.83 + To be equal to: reduced2 + Which is: 2064.83 + + "Gosh," you might say, "They sure look equal to me. What the heck is going on here?" + + The answer lies beyond what you can see, in that which you cannot see. One must + journey into the depths, the hidden, that which the world fights its hardest to + conceal from us. + + Specifically: you need to look at more decimal places. Run the test with: + MOZ_LOG="nsResistFingerprinting:5" + + And look for two successive lines similar to the below (the format will probably + be different by the time you read this comment): + V/nsResistFingerprinting Given: 2064.83384599999999, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241692.00000000000000, Got: 2064.83383999999978 + V/nsResistFingerprinting Given: 2064.83383999999978, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241691.00000000000000, Got: 2064.83381999999983 + + Look at the last two values: + Got: 2064.83383999999978 + Got: 2064.83381999999983 + + They're supposed to be equal. They're not. But they both round to 2064.83. + Good luck and godspeed. +*/ +// clang-format on + +bool setupJitter(bool enabled) { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + bool jitterEnabled = false; + if (prefs) { + prefs->GetBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", + &jitterEnabled); + prefs->SetBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", enabled); + } + + return jitterEnabled; +} + +void cleanupJitter(bool jitterWasEnabled) { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + prefs->SetBoolPref( + "privacy.resistFingerprinting.reduceTimerPrecision.jitter", + jitterWasEnabled); + } +} + +void process(double clock, nsRFPService::TimeScale clockUnits, + double precision) { + double reduced1 = nsRFPService::ReduceTimePrecisionImpl( + clock, clockUnits, precision, -1, TimerPrecisionType::Normal); + double reduced2 = nsRFPService::ReduceTimePrecisionImpl( + reduced1, clockUnits, precision, -1, TimerPrecisionType::Normal); + ASSERT_EQ(reduced1, reduced2); +} + +TEST(ResistFingerprinting, ReducePrecision_Assumptions) +{ + ASSERT_EQ(FLT_RADIX, 2); + ASSERT_EQ(DBL_MANT_DIG, 53); +} + +TEST(ResistFingerprinting, ReducePrecision_Reciprocal) +{ + bool jitterEnabled = setupJitter(false); + // This one has a rounding error in the Reciprocal case: + process(2064.8338460, nsRFPService::TimeScale::MicroSeconds, 20); + // These are just big values + process(1516305819, nsRFPService::TimeScale::MicroSeconds, 20); + process(69053.12, nsRFPService::TimeScale::MicroSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_KnownGood) +{ + bool jitterEnabled = setupJitter(false); + process(2064.8338460, nsRFPService::TimeScale::MilliSeconds, 20); + process(69027.62, nsRFPService::TimeScale::MilliSeconds, 20); + process(69053.12, nsRFPService::TimeScale::MilliSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_KnownBad) +{ + bool jitterEnabled = setupJitter(false); + process(1054.842405, nsRFPService::TimeScale::MilliSeconds, 20); + process(273.53038600000002, nsRFPService::TimeScale::MilliSeconds, 20); + process(628.66686500000003, nsRFPService::TimeScale::MilliSeconds, 20); + process(521.28919100000007, nsRFPService::TimeScale::MilliSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Edge) +{ + bool jitterEnabled = setupJitter(false); + process(2611.14, nsRFPService::TimeScale::MilliSeconds, 20); + process(2611.16, nsRFPService::TimeScale::MilliSeconds, 20); + process(2612.16, nsRFPService::TimeScale::MilliSeconds, 20); + process(2601.64, nsRFPService::TimeScale::MilliSeconds, 20); + process(2595.16, nsRFPService::TimeScale::MilliSeconds, 20); + process(2578.66, nsRFPService::TimeScale::MilliSeconds, 20); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 2611.12); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations_HighRes) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 2611.12); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations_RFP) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 2611.12); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_Expectations_DangerouslyNone) +{ + bool jitterEnabled = setupJitter(false); + double result; + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.14, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.14); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.145, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.145); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.141, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.141); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.15999); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.15, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.15); + result = nsRFPService::ReduceTimePrecisionImpl( + 2611.13, nsRFPService::TimeScale::MilliSeconds, 20, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 2611.13); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254740990.0); + // 9007199254740995 is approximated to 9007199254740996 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254740996); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254741000.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743564 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 9007199254743564.0); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_HighRes) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254740980.0); + // 9007199254740995 is approximated to 9007199254740980 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254740980); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254741000.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743560 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::UnconditionalAKAHighRes); + ASSERT_EQ(result, 9007199254743560.0); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision_RFP) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254740990.0); + // 9007199254740995 is approximated to 9007199254740980 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254740995); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254741000.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743560 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::RFP); + ASSERT_EQ(result, 9007199254743565.0); + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, + ReducePrecision_ExpectedLossOfPrecision_DangerouslyNone) +{ + bool jitterEnabled = setupJitter(false); + double result; + // We lose integer precision at 9007199254740992 - let's confirm that. + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254740992.0); + // 9007199254740995 is approximated to 9007199254740980 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254740995); + // 9007199254740999 is approximated as 9007199254741000 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254740999.0); + // 9007199254743568 can be represented exactly, but will be clamped to + // 9007199254743560 + result = nsRFPService::ReduceTimePrecisionImpl( + 9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, -1, + TimerPrecisionType::DangerouslyNone); + ASSERT_EQ(result, 9007199254743568.0); + cleanupJitter(jitterEnabled); +} + +// Use an ugly but simple hack to turn an integer-based rand() +// function to a double-based one. +#define RAND_DOUBLE (rand() * (rand() / (double)rand())) + +// If you're doing logging, you really don't want to run this test. +#define RUN_AGGRESSIVE false + +TEST(ResistFingerprinting, ReducePrecision_Aggressive) +{ + if (!RUN_AGGRESSIVE) { + return; + } + + bool jitterEnabled = setupJitter(false); + + for (int i = 0; i < 10000; i++) { + // Test three different time magnitudes, with decimals. + // Note that we need separate variables for the different units, as scaling + // them after calculating them will erase effects of approximation. + // A magnitude in the seconds since epoch range. + double time1_s = fmod(RAND_DOUBLE, 1516305819.0); + double time1_ms = fmod(RAND_DOUBLE, 1516305819000.0); + double time1_us = fmod(RAND_DOUBLE, 1516305819000000.0); + // A magnitude in the 'couple of minutes worth of milliseconds' range. + double time2_s = fmod(RAND_DOUBLE, (60.0 * 60 * 5)); + double time2_ms = fmod(RAND_DOUBLE, (1000.0 * 60 * 60 * 5)); + double time2_us = fmod(RAND_DOUBLE, (1000000.0 * 60 * 60 * 5)); + // A magnitude in the small range + double time3_s = fmod(RAND_DOUBLE, 10); + double time3_ms = fmod(RAND_DOUBLE, 10000); + double time3_us = fmod(RAND_DOUBLE, 10000000); + + // Test two precision magnitudes, no decimals. + // A magnitude in the high milliseconds. + double precision1 = rand() % 250000; + // a magnitude in the low microseconds. + double precision2 = rand() % 200; + + process(time1_s, nsRFPService::TimeScale::Seconds, precision1); + process(time1_s, nsRFPService::TimeScale::Seconds, precision2); + process(time2_s, nsRFPService::TimeScale::Seconds, precision1); + process(time2_s, nsRFPService::TimeScale::Seconds, precision2); + process(time3_s, nsRFPService::TimeScale::Seconds, precision1); + process(time3_s, nsRFPService::TimeScale::Seconds, precision2); + + process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision1); + process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision2); + process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision1); + process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision2); + process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision1); + process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision2); + + process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision1); + process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision2); + process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision1); + process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision2); + process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision1); + process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision2); + } + cleanupJitter(jitterEnabled); +} + +TEST(ResistFingerprinting, ReducePrecision_JitterTestVectors) +{ + bool jitterEnabled = setupJitter(true); + + // clang-format off + /* + * Here's our test vector. We set the secret to the 16 byte value + * 0x0001020304050607 0x1011121314151617 + * + * We then use a stand alone XOrShift128+ generator: + * + * The midpoints are: + * 0 = 328 + * 500 = 25 + * 1000 = 73 + * 1500 = 89 + * 2000 = 73 + * 2500 = 153 + * 3000 = 9 + * 3500 = 89 + * 4000 = 73 + * 4500 = 205 + * 5000 = 253 + * 5500 = 141 + */ + // clang-format on + + // Set the secret + long long throwAway; + uint8_t hardcodedSecret[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17}; + + nsRFPService::RandomMidpoint(0, 500, -1, &throwAway, hardcodedSecret); + + // Run the test vectors + double result; + + result = nsRFPService::ReduceTimePrecisionImpl( + 1, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 0); + result = nsRFPService::ReduceTimePrecisionImpl( + 184, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 0); + result = nsRFPService::ReduceTimePrecisionImpl( + 185, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 329, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 499, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + + result = nsRFPService::ReduceTimePrecisionImpl( + 500, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 520, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 524, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 500); + result = nsRFPService::ReduceTimePrecisionImpl( + 525, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 1000); + result = nsRFPService::ReduceTimePrecisionImpl( + 930, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 1000); + result = nsRFPService::ReduceTimePrecisionImpl( + 1072, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 1000); + + result = nsRFPService::ReduceTimePrecisionImpl( + 4000, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4050, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4072, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4073, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4340, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4499, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + + result = nsRFPService::ReduceTimePrecisionImpl( + 4500, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4536, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4704, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 4500); + result = nsRFPService::ReduceTimePrecisionImpl( + 4705, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 5000); + result = nsRFPService::ReduceTimePrecisionImpl( + 4726, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 5000); + result = nsRFPService::ReduceTimePrecisionImpl( + 5106, nsRFPService::TimeScale::MicroSeconds, 500, 4000, + TimerPrecisionType::Normal); + ASSERT_EQ(result, 5000); + + cleanupJitter(jitterEnabled); +} diff --git a/toolkit/components/resistfingerprinting/tests/moz.build b/toolkit/components/resistfingerprinting/tests/moz.build new file mode 100644 index 0000000000..3a6ca329ff --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/moz.build @@ -0,0 +1,9 @@ +MOCHITEST_CHROME_MANIFESTS += [ + "chrome/chrome.toml", +] + +BROWSER_CHROME_MANIFESTS += [ + "browser/browser.toml", +] + +TEST_DIRS += ["gtest"] -- cgit v1.2.3