tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_canvas_fingerprinting_resistance.js (8132B)


      1 /**
      2 * When "privacy.resistFingerprinting" is set to true, user permission is
      3 * required for canvas data extraction.
      4 * This tests whether the site permission prompt for canvas data extraction
      5 * works properly.
      6 * Canvas data extraction should result in random data.
      7 */
      8 "use strict";
      9 
     10 const kUrl = "https://example.com/";
     11 const kPrincipal = Services.scriptSecurityManager.createContentPrincipal(
     12  Services.io.newURI(kUrl),
     13  {}
     14 );
     15 const kPermission = "canvas";
     16 
     17 function initTab() {
     18  let contentWindow = content.wrappedJSObject;
     19 
     20  let drawCanvas = (fillStyle, id) => {
     21    let contentDocument = contentWindow.document;
     22    let width = 64,
     23      height = 64;
     24    let canvas = contentDocument.createElement("canvas");
     25    if (id) {
     26      canvas.setAttribute("id", id);
     27    }
     28    canvas.setAttribute("width", width);
     29    canvas.setAttribute("height", height);
     30    contentDocument.body.appendChild(canvas);
     31 
     32    let context = canvas.getContext("2d");
     33    context.fillStyle = fillStyle;
     34    context.fillRect(0, 0, width, height);
     35 
     36    if (id) {
     37      let button = contentDocument.createElement("button");
     38      button.addEventListener("click", function () {
     39        canvas.toDataURL();
     40      });
     41      button.setAttribute("id", "clickme");
     42      button.innerHTML = "Click Me!";
     43      contentDocument.body.appendChild(button);
     44    }
     45 
     46    return canvas;
     47  };
     48 
     49  let canvas = drawCanvas("cyan", "canvas-id-canvas");
     50  contentWindow.kPlacedData = canvas.toDataURL();
     51  is(
     52    canvas.toDataURL(),
     53    contentWindow.kPlacedData,
     54    "privacy.resistFingerprinting = false, canvas data == placed data"
     55  );
     56 }
     57 
     58 function enableResistFingerprinting(autoDeclineNoInput) {
     59  return SpecialPowers.pushPrefEnv({
     60    set: [
     61      ["privacy.fingerprintingProtection", true],
     62      [
     63        "privacy.fingerprintingProtection.overrides",
     64        "+CanvasRandomization,+CanvasImageExtractionPrompt,+CanvasExtractionFromThirdPartiesIsBlocked" +
     65          (autoDeclineNoInput
     66            ? ",+CanvasExtractionBeforeUserInputIsBlocked"
     67            : ""),
     68      ],
     69    ],
     70  });
     71 }
     72 
     73 function promisePopupShown() {
     74  return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
     75 }
     76 
     77 function promisePopupHidden() {
     78  return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
     79 }
     80 
     81 function extractCanvasData(grantPermission) {
     82  let contentWindow = content.wrappedJSObject;
     83  let canvas = contentWindow.document.getElementById("canvas-id-canvas");
     84  let canvasData = canvas.toDataURL();
     85  if (grantPermission) {
     86    is(
     87      canvasData,
     88      contentWindow.kPlacedData,
     89      "privacy.resistFingerprinting = true, permission granted, canvas data == placed data"
     90    );
     91  } else if (grantPermission === false) {
     92    isnot(
     93      canvasData,
     94      contentWindow.kPlacedData,
     95      "privacy.resistFingerprinting = true, permission denied, canvas data != placed data"
     96    );
     97  } else {
     98    isnot(
     99      canvasData,
    100      contentWindow.kPlacedData,
    101      "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data"
    102    );
    103  }
    104 }
    105 
    106 function triggerCommand(button) {
    107  let notifications = PopupNotifications.panel.children;
    108  let notification = notifications[0];
    109  EventUtils.synthesizeMouseAtCenter(notification[button], {});
    110 }
    111 
    112 function triggerMainCommand() {
    113  triggerCommand("button");
    114 }
    115 
    116 function triggerSecondaryCommand() {
    117  triggerCommand("secondaryButton");
    118 }
    119 
    120 function testPermission() {
    121  return Services.perms.testPermissionFromPrincipal(kPrincipal, kPermission);
    122 }
    123 
    124 async function withNewTabNoInput(grantPermission, browser) {
    125  await SpecialPowers.spawn(browser, [], initTab);
    126  await enableResistFingerprinting(false);
    127  let popupShown = promisePopupShown();
    128  await SpecialPowers.spawn(browser, [], extractCanvasData);
    129  await popupShown;
    130  let popupHidden = promisePopupHidden();
    131  if (grantPermission) {
    132    triggerMainCommand();
    133    await popupHidden;
    134    is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
    135  } else {
    136    triggerSecondaryCommand();
    137    await popupHidden;
    138    is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
    139  }
    140  await SpecialPowers.spawn(browser, [grantPermission], extractCanvasData);
    141  await SpecialPowers.popPrefEnv();
    142 }
    143 
    144 async function doTestNoInput(grantPermission) {
    145  await BrowserTestUtils.withNewTab(
    146    kUrl,
    147    withNewTabNoInput.bind(null, grantPermission)
    148  );
    149  Services.perms.removeFromPrincipal(kPrincipal, kPermission);
    150 }
    151 
    152 // With auto-declining disabled (not the default)
    153 // Tests clicking "Don't Allow" button of the permission prompt.
    154 add_task(doTestNoInput.bind(null, false));
    155 
    156 // Tests clicking "Allow" button of the permission prompt.
    157 add_task(doTestNoInput.bind(null, true));
    158 
    159 async function withNewTabAutoBlockNoInput(browser) {
    160  await SpecialPowers.spawn(browser, [], initTab);
    161  await enableResistFingerprinting(true);
    162 
    163  let noShowHandler = () => {
    164    ok(false, "The popup notification should not show in this case.");
    165  };
    166  PopupNotifications.panel.addEventListener("popupshown", noShowHandler, {
    167    once: true,
    168  });
    169 
    170  let promisePopupObserver = TestUtils.topicObserved(
    171    "PopupNotifications-updateNotShowing"
    172  );
    173 
    174  // Try to extract canvas data without user inputs.
    175  await SpecialPowers.spawn(browser, [], extractCanvasData);
    176 
    177  await promisePopupObserver;
    178  info("There should be no popup shown on the panel.");
    179 
    180  // Check that the icon of canvas permission is shown.
    181  let canvasNotification = PopupNotifications.getNotification(
    182    "canvas-permissions-prompt",
    183    browser
    184  );
    185 
    186  is(
    187    canvasNotification.anchorElement.getAttribute("showing"),
    188    "true",
    189    "The canvas permission icon is correctly shown."
    190  );
    191  PopupNotifications.panel.removeEventListener("popupshown", noShowHandler);
    192 
    193  await SpecialPowers.popPrefEnv();
    194 }
    195 
    196 async function doTestAutoBlockNoInput() {
    197  await BrowserTestUtils.withNewTab(
    198    kUrl,
    199    withNewTabAutoBlockNoInput.bind(null)
    200  );
    201 }
    202 
    203 add_task(doTestAutoBlockNoInput.bind(null));
    204 
    205 function extractCanvasDataUserInput(grantPermission) {
    206  let contentWindow = content.wrappedJSObject;
    207  let canvas = contentWindow.document.getElementById("canvas-id-canvas");
    208  let canvasData = canvas.toDataURL();
    209  if (grantPermission) {
    210    is(
    211      canvasData,
    212      contentWindow.kPlacedData,
    213      "privacy.resistFingerprinting = true, permission granted, canvas data == placed data"
    214    );
    215  } else if (grantPermission === false) {
    216    isnot(
    217      canvasData,
    218      contentWindow.kPlacedData,
    219      "privacy.resistFingerprinting = true, permission denied, canvas data != placed data"
    220    );
    221  } else {
    222    isnot(
    223      canvasData,
    224      contentWindow.kPlacedData,
    225      "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data"
    226    );
    227  }
    228 }
    229 
    230 async function withNewTabInput(grantPermission, browser) {
    231  await SpecialPowers.spawn(browser, [], initTab);
    232  await enableResistFingerprinting(true);
    233  let popupShown = promisePopupShown();
    234  await SpecialPowers.spawn(browser, [], function () {
    235    E10SUtils.wrapHandlingUserInput(content, true, function () {
    236      var button = content.document.getElementById("clickme");
    237      button.click();
    238    });
    239  });
    240  await popupShown;
    241  let popupHidden = promisePopupHidden();
    242  if (grantPermission) {
    243    triggerMainCommand();
    244    await popupHidden;
    245    is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
    246  } else {
    247    triggerSecondaryCommand();
    248    await popupHidden;
    249    is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
    250  }
    251  await SpecialPowers.spawn(
    252    browser,
    253    [grantPermission],
    254    extractCanvasDataUserInput
    255  );
    256  await SpecialPowers.popPrefEnv();
    257 }
    258 
    259 async function doTestInput(grantPermission) {
    260  await BrowserTestUtils.withNewTab(
    261    kUrl,
    262    withNewTabInput.bind(null, grantPermission)
    263  );
    264  Services.perms.removeFromPrincipal(kPrincipal, kPermission);
    265 }
    266 
    267 // With auto-declining enabled (the default)
    268 // Tests clicking "Don't Allow" button of the permission prompt.
    269 add_task(doTestInput.bind(null, false));
    270 
    271 // Tests clicking "Allow" button of the permission prompt.
    272 add_task(doTestInput.bind(null, true));