diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 3fecbec7a8fe..bc885a5b5818 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -253,6 +253,7 @@ bool nsContentUtils::sIsResourceTimingEnabled = false; bool nsContentUtils::sIsUserTimingLoggingEnabled = false; bool nsContentUtils::sIsExperimentalAutocompleteEnabled = false; bool nsContentUtils::sEncodeDecodeURLHash = false; +bool nsContentUtils::sPrivacyResistFingerprinting = false; uint32_t nsContentUtils::sHandlingInputTimeout = 1000; @@ -533,6 +534,9 @@ nsContentUtils::Init() Preferences::AddBoolVarCache(&sEncodeDecodeURLHash, "dom.url.encode_decode_hash", false); + Preferences::AddBoolVarCache(&sPrivacyResistFingerprinting, + "privacy.resistFingerprinting", false); + Preferences::AddUintVarCache(&sHandlingInputTimeout, "dom.event.handling-user-input-time-limit", 1000); @@ -1989,6 +1993,16 @@ nsContentUtils::IsCallerChrome() return xpc::IsUniversalXPConnectEnabled(GetCurrentJSContext()); } +bool +nsContentUtils::ShouldResistFingerprinting(nsIDocShell* aDocShell) +{ + if (!aDocShell) { + return false; + } + bool isChrome = nsContentUtils::IsChromeDoc(aDocShell->GetDocument()); + return !isChrome && sPrivacyResistFingerprinting; +} + namespace mozilla { namespace dom { namespace workers { diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index 6817a874eec0..3c294b834959 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -199,6 +199,9 @@ public: JS::Handle aId, JS::MutableHandle aDesc); + // Check whether we should avoid leaking distinguishing information to JS/CSS. + static bool ShouldResistFingerprinting(nsIDocShell* aDocShell); + /** * Returns the parent node of aChild crossing document boundaries. * Uses the parent node in the composed document. @@ -1913,6 +1916,16 @@ public: return sEncodeDecodeURLHash; } + /* + * Returns true if the browser should attempt to prevent content scripts + * from collecting distinctive information about the browser that could + * be used to "fingerprint" and track the user across websites. + */ + static bool ResistFingerprinting() + { + return sPrivacyResistFingerprinting; + } + /** * Returns true if the doc tree branch which contains aDoc contains any * plugins which we don't control event dispatch for, i.e. do any plugins @@ -2453,6 +2466,7 @@ private: static bool sIsUserTimingLoggingEnabled; static bool sIsExperimentalAutocompleteEnabled; static bool sEncodeDecodeURLHash; + static bool sPrivacyResistFingerprinting; static nsHtml5StringParser* sHTMLFragmentParser; static nsIParser* sXMLFragmentParser; diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 44e288cfac47..9e609e8e7cd6 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -5003,6 +5003,12 @@ nsGlobalWindow::GetOuterSize(ErrorResult& aError) { MOZ_ASSERT(IsOuterWindow()); + if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) { + CSSIntSize size; + aError = GetInnerSize(size); + return nsIntSize(size.width, size.height); + } + nsCOMPtr treeOwnerAsWin = GetTreeOwnerWindow(); if (!treeOwnerAsWin) { aError.Throw(NS_ERROR_FAILURE); @@ -5167,6 +5173,11 @@ nsGlobalWindow::GetScreenXY(ErrorResult& aError) { MOZ_ASSERT(IsOuterWindow()); + // When resisting fingerprinting, always return (0,0) + if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) { + return nsIntPoint(0, 0); + } + nsCOMPtr treeOwnerAsWin = GetTreeOwnerWindow(); if (!treeOwnerAsWin) { aError.Throw(NS_ERROR_FAILURE); @@ -5240,6 +5251,11 @@ nsGlobalWindow::GetMozInnerScreenX(ErrorResult& aError) { FORWARD_TO_OUTER_OR_THROW(GetMozInnerScreenX, (aError), aError, 0); + // When resisting fingerprinting, always return 0. + if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) { + return 0.0; + } + nsRect r = GetInnerScreenRect(); return nsPresContext::AppUnitsToFloatCSSPixels(r.x); } @@ -5258,6 +5274,11 @@ nsGlobalWindow::GetMozInnerScreenY(ErrorResult& aError) { FORWARD_TO_OUTER_OR_THROW(GetMozInnerScreenY, (aError), aError, 0); + // Return 0 to prevent fingerprinting. + if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) { + return 0.0; + } + nsRect r = GetInnerScreenRect(); return nsPresContext::AppUnitsToFloatCSSPixels(r.y); } @@ -5286,6 +5307,10 @@ nsGlobalWindow::GetDevicePixelRatio(ErrorResult& aError) return 1.0; } + if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) { + return 1.0; + } + return float(nsPresContext::AppUnitsPerCSSPixel())/ presContext->AppUnitsPerDevPixel(); } diff --git a/dom/base/nsGlobalWindow.h b/dom/base/nsGlobalWindow.h index c5184b698c26..274bd542dbba 100644 --- a/dom/base/nsGlobalWindow.h +++ b/dom/base/nsGlobalWindow.h @@ -1059,6 +1059,8 @@ public: bool aShowDialog, mozilla::ErrorResult& aError); uint64_t GetMozPaintCount(mozilla::ErrorResult& aError); + bool ShouldResistFingerprinting(); + mozilla::dom::MozSelfSupport* GetMozSelfSupport(mozilla::ErrorResult& aError); already_AddRefed OpenDialog(JSContext* aCx, diff --git a/dom/base/nsScreen.cpp b/dom/base/nsScreen.cpp index dd8b440d732e..b1249691f210 100644 --- a/dom/base/nsScreen.cpp +++ b/dom/base/nsScreen.cpp @@ -68,6 +68,11 @@ NS_IMPL_RELEASE_INHERITED(nsScreen, DOMEventTargetHelper) int32_t nsScreen::GetPixelDepth(ErrorResult& aRv) { + // Return 24 to prevent fingerprinting. + if (ShouldResistFingerprinting()) { + return 24; + } + nsDeviceContext* context = GetDeviceContext(); if (!context) { @@ -111,6 +116,11 @@ nsScreen::GetDeviceContext() nsresult nsScreen::GetRect(nsRect& aRect) { + // Return window inner rect to prevent fingerprinting. + if (ShouldResistFingerprinting()) { + return GetWindowInnerRect(aRect); + } + nsDeviceContext *context = GetDeviceContext(); if (!context) { @@ -130,6 +140,11 @@ nsScreen::GetRect(nsRect& aRect) nsresult nsScreen::GetAvailRect(nsRect& aRect) { + // Return window inner rect to prevent fingerprinting. + if (ShouldResistFingerprinting()) { + return GetWindowInnerRect(aRect); + } + nsDeviceContext *context = GetDeviceContext(); if (!context) { @@ -166,22 +181,26 @@ nsScreen::Notify(const hal::ScreenConfiguration& aConfiguration) void nsScreen::GetMozOrientation(nsString& aOrientation) { - switch (mOrientation) { - case eScreenOrientation_PortraitPrimary: - aOrientation.AssignLiteral("portrait-primary"); - break; - case eScreenOrientation_PortraitSecondary: - aOrientation.AssignLiteral("portrait-secondary"); - break; - case eScreenOrientation_LandscapePrimary: + if (ShouldResistFingerprinting()) { aOrientation.AssignLiteral("landscape-primary"); - break; - case eScreenOrientation_LandscapeSecondary: - aOrientation.AssignLiteral("landscape-secondary"); - break; - case eScreenOrientation_None: - default: - MOZ_CRASH("Unacceptable mOrientation value"); + } else { + switch (mOrientation) { + case eScreenOrientation_PortraitPrimary: + aOrientation.AssignLiteral("portrait-primary"); + break; + case eScreenOrientation_PortraitSecondary: + aOrientation.AssignLiteral("portrait-secondary"); + break; + case eScreenOrientation_LandscapePrimary: + aOrientation.AssignLiteral("landscape-primary"); + break; + case eScreenOrientation_LandscapeSecondary: + aOrientation.AssignLiteral("landscape-secondary"); + break; + case eScreenOrientation_None: + default: + MOZ_CRASH("Unacceptable mOrientation value"); + } } } @@ -373,3 +392,27 @@ nsScreen::FullScreenEventListener::HandleEvent(nsIDOMEvent* aEvent) return NS_OK; } + +nsresult +nsScreen::GetWindowInnerRect(nsRect& aRect) +{ + aRect.x = 0; + aRect.y = 0; + nsCOMPtr win = GetOwner(); + if (!win) { + return NS_ERROR_FAILURE; + } + nsresult rv = win->GetInnerWidth(&aRect.width); + NS_ENSURE_SUCCESS(rv, rv); + return win->GetInnerHeight(&aRect.height); +} + +bool nsScreen::ShouldResistFingerprinting() const +{ + bool resist = false; + nsCOMPtr owner = GetOwner(); + if (owner) { + resist = nsContentUtils::ShouldResistFingerprinting(owner->GetDocShell()); + } + return resist; +} diff --git a/dom/base/nsScreen.h b/dom/base/nsScreen.h index a686cec7e00b..d13dc6c0f31e 100644 --- a/dom/base/nsScreen.h +++ b/dom/base/nsScreen.h @@ -131,6 +131,7 @@ protected: nsDeviceContext* GetDeviceContext(); nsresult GetRect(nsRect& aRect); nsresult GetAvailRect(nsRect& aRect); + nsresult GetWindowInnerRect(nsRect& aRect); mozilla::dom::ScreenOrientation mOrientation; @@ -158,6 +159,8 @@ private: bool IsDeviceSizePageSize(); + bool ShouldResistFingerprinting() const; + nsRefPtr mEventListener; }; diff --git a/dom/base/test/chrome/bug418986-1.js b/dom/base/test/chrome/bug418986-1.js new file mode 100644 index 000000000000..8ba0626d1a07 --- /dev/null +++ b/dom/base/test/chrome/bug418986-1.js @@ -0,0 +1,71 @@ +// The main test function. +let test = function (isContent) { + SimpleTest.waitForExplicitFinish(); + + let { ww } = SpecialPowers.Services; + window.chromeWindow = ww.activeWindow; + + // The pairs of values expected to be the same when + // fingerprinting resistance is enabled. + let pairs = [ + ["screenX", 0], + ["screenY", 0], + ["mozInnerScreenX", 0], + ["mozInnerScreenY", 0], + ["screen.pixelDepth", 24], + ["screen.colorDepth", 24], + ["screen.availWidth", "innerWidth"], + ["screen.availHeight", "innerHeight"], + ["screen.left", 0], + ["screen.top", 0], + ["screen.availLeft", 0], + ["screen.availTop", 0], + ["screen.width", "innerWidth"], + ["screen.height", "innerHeight"], + ["screen.mozOrientation", "'landscape-primary'"], + ["devicePixelRatio", 1] + ]; + + // checkPair: tests if members of pair [a, b] are equal when evaluated. + let checkPair = function (a, b) { + is(eval(a), eval(b), a + " should be equal to " + b); + }; + + // Returns generator object that iterates through pref values. + let prefVals = (for (prefVal of [false, true]) prefVal); + + // The main test function, runs until all pref values are exhausted. + let nextTest = function () { + let {value : prefValue, done} = prefVals.next(); + if (done) { + SimpleTest.finish(); + return; + } + SpecialPowers.pushPrefEnv({set : [["privacy.resistFingerprinting", prefValue]]}, + function () { + // We will be resisting fingerprinting if the pref is enabled, + // and we are in a content script (not chrome). + let resisting = prefValue && isContent; + // Check each of the pairs. + pairs.map(function ([item, onVal]) { + if (resisting) { + checkPair("window." + item, onVal); + } else { + if (!item.startsWith("moz")) { + checkPair("window." + item, "chromeWindow." + item); + } + } + }); + if (!resisting) { + // Hard to predict these values, but we can enforce constraints: + ok(window.mozInnerScreenX >= chromeWindow.mozInnerScreenX, + "mozInnerScreenX"); + ok(window.mozInnerScreenY >= chromeWindow.mozInnerScreenY, + "mozInnerScreenY"); + } + nextTest(); + }); + } + + nextTest(); +} diff --git a/dom/base/test/chrome/chrome.ini b/dom/base/test/chrome/chrome.ini index df4d5985f40c..3e223aed5985 100644 --- a/dom/base/test/chrome/chrome.ini +++ b/dom/base/test/chrome/chrome.ini @@ -3,6 +3,7 @@ skip-if = buildapp == 'b2g' support-files = blockNoPlugins.xml blockPluginHard.xml + bug418986-1.js cpows_child.js cpows_parent.xul file_bug391728.html @@ -31,6 +32,7 @@ support-files = [test_bug380418.html^headers^] [test_bug383430.html] [test_bug391728.html] +[test_bug418986-1.xul] [test_bug421622.xul] [test_bug429785.xul] [test_bug430050.xul] diff --git a/dom/base/test/chrome/test_bug418986-1.xul b/dom/base/test/chrome/test_bug418986-1.xul new file mode 100644 index 000000000000..aa0c34077119 --- /dev/null +++ b/dom/base/test/chrome/test_bug418986-1.xul @@ -0,0 +1,26 @@ + + + + + + + + + + Mozilla Bug 418986 (Part 1) + + + + + + diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index b205d67a8d01..28363f7359ae 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -56,6 +56,7 @@ support-files = bug704320.sjs bug704320_counter.sjs bug819051.sjs + chrome/bug418986-1.js copypaste.js delayedServerEvents.sjs echo.sjs @@ -453,6 +454,7 @@ support-files = test_bug402150.html^headers^ [test_bug417255.html] [test_bug417384.html] [test_bug418214.html] +[test_bug418986-1.html] [test_bug419132.html] [test_bug419527.xhtml] [test_bug420609.xhtml] diff --git a/dom/base/test/test_bug418986-1.html b/dom/base/test/test_bug418986-1.html new file mode 100644 index 000000000000..3ffa19fa9871 --- /dev/null +++ b/dom/base/test/test_bug418986-1.html @@ -0,0 +1,24 @@ + + + + + + Test 1/3 for Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info + + + + + + Bug 418986 +

+ +

+  
+
+
diff --git a/dom/events/Event.cpp b/dom/events/Event.cpp
index 870785855d73..7c7046f01e39 100644
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -890,6 +890,13 @@ Event::GetScreenCoords(nsPresContext* aPresContext,
                        WidgetEvent* aEvent,
                        LayoutDeviceIntPoint aPoint)
 {
+  if (!nsContentUtils::IsCallerChrome() &&
+      nsContentUtils::ResistFingerprinting()) {
+    // When resisting fingerprinting, return client coordinates instead.
+    CSSIntPoint clientCoords = GetClientCoords(aPresContext, aEvent, aPoint, CSSIntPoint(0, 0));
+    return LayoutDeviceIntPoint(clientCoords.x, clientCoords.y);
+  }
+
   if (EventStateManager::sIsPointerLocked) {
     return EventStateManager::sLastScreenPoint;
   }
diff --git a/dom/events/test/bug418986-3.js b/dom/events/test/bug418986-3.js
new file mode 100644
index 000000000000..317b5c7ad185
--- /dev/null
+++ b/dom/events/test/bug418986-3.js
@@ -0,0 +1,69 @@
+SimpleTest.waitForExplicitFinish();
+
+// The main testing function.
+let test = function (isContent) {
+  // Each definition is [eventType, prefSetting]
+  // Where we are setting the "privacy.resistFingerprinting" pref.
+  let eventDefs = [["mousedown", true],
+                   ["mouseup", true],
+                   ["mousedown", false],
+                   ["mouseup", false]];
+
+  let testCounter = 0;
+
+  // Declare ahead of time.
+  let setup;
+
+  // This function is called when the event handler fires.
+  let handleEvent = function (event, prefVal) {
+    let resisting = prefVal && isContent;
+    if (resisting) {
+      is(event.screenX, event.clientX, "event.screenX and event.clientX should be the same");
+      is(event.screenY, event.clientY, "event.screenY and event.clientY should be the same");
+    } else {
+      // We can't be sure about X coordinates not being equal, but we can test Y.
+      isnot(event.screenY, event.clientY, "event.screenY !== event.clientY");
+    }
+    ++testCounter;
+    if (testCounter < eventDefs.length) {
+      nextTest();
+    } else {
+      SimpleTest.finish();
+    }
+  };
+
+  // In this function, we set up the nth div and event handler,
+  // and then synthesize a mouse event in the div, to test
+  // whether the resulting events resist fingerprinting by
+  // suppressing absolute screen coordinates.
+  nextTest = function () {
+    let [eventType, prefVal] = eventDefs[testCounter];
+    SpecialPowers.pushPrefEnv({set:[["privacy.resistFingerprinting", prefVal]]},
+      function () {
+        // The following code creates a new div for each event in eventDefs,
+        // attaches a listener to listen for the event, and then generates
+        // a fake event at the center of the div.
+        let div = document.createElement("div");
+        div.style.width = "10px";
+        div.style.height = "10px";
+        div.style.backgroundColor = "red";
+        // Name the div after the event we're listening for.
+        div.id = eventType;
+        document.getElementById("body").appendChild(div);
+        // Seems we can't add an event listener in chrome unless we run
+        // it in a later task.
+        window.setTimeout(function() {
+          div.addEventListener(eventType, event => handleEvent(event, prefVal), false);
+          // For some reason, the following synthesizeMouseAtCenter call only seems to run if we
+          // wrap it in a window.setTimeout(..., 0).
+          window.setTimeout(function () {
+            synthesizeMouseAtCenter(div, {type : eventType});
+          }, 0);
+        }, 0);
+      });
+  };
+
+  // Now run by starting with the 0th event.
+  nextTest();
+
+};
diff --git a/dom/events/test/chrome.ini b/dom/events/test/chrome.ini
index 05b232177923..bdc8ebc16951 100644
--- a/dom/events/test/chrome.ini
+++ b/dom/events/test/chrome.ini
@@ -3,6 +3,7 @@ skip-if = buildapp == 'b2g'
 support-files =
   bug415498-doc1.html
   bug415498-doc2.html
+  bug418986-3.js
   bug591249_iframe.xul
   bug602962.xul
   file_bug679494.html
@@ -12,6 +13,7 @@ support-files =
 [test_bug336682_2.xul]
 [test_bug368835.html]
 [test_bug415498.xul]
+[test_bug418986-3.xul]
 [test_bug524674.xul]
 [test_bug586961.xul]
 [test_bug591249.xul]
diff --git a/dom/events/test/mochitest.ini b/dom/events/test/mochitest.ini
index 9d4707a47b61..a6e1a266b7f0 100644
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -7,6 +7,7 @@ support-files =
   bug426082.html
   bug648573.html
   bug656379-1.html
+  bug418986-3.js
   error_event_worker.js
   empty.js
   window_bug493251.html
@@ -38,6 +39,9 @@ support-files = test_bug336682.js
 [test_bug409604.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
 [test_bug412567.html]
+[test_bug418986-3.html]
+# Sometimes fails to finish after tests pass on 'B2G ICS Emulator'.
+skip-if = (os == 'b2g')
 [test_bug422132.html]
 skip-if = buildapp == 'b2g' || e10s # b2g(2 failures out of 8, mousewheel test) b2g-debug(2 failures out of 8, mousewheel test) b2g-desktop(2 failures out of 8, mousewheel test)
 [test_bug426082.html]
diff --git a/dom/events/test/test_bug418986-3.html b/dom/events/test/test_bug418986-3.html
new file mode 100644
index 000000000000..a92b1e0f532e
--- /dev/null
+++ b/dom/events/test/test_bug418986-3.html
@@ -0,0 +1,25 @@
+
+
+
+
+  
+  Test 3/3 for Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info
+  
+  
+  
+
+
+Bug 418986
+

+

+
+
+
+
diff --git a/dom/events/test/test_bug418986-3.xul b/dom/events/test/test_bug418986-3.xul
new file mode 100644
index 000000000000..574cda0cee72
--- /dev/null
+++ b/dom/events/test/test_bug418986-3.xul
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+        
+  
+
+
+
+Mozilla Bug 418986
+
+
+
+  
+
+
diff --git a/layout/style/nsComputedDOMStyle.cpp b/layout/style/nsComputedDOMStyle.cpp
index f835e89f7a45..6956ed5f931e 100644
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -1450,6 +1450,10 @@ nsComputedDOMStyle::DoGetFontSizeAdjust()
 CSSValue*
 nsComputedDOMStyle::DoGetOsxFontSmoothing()
 {
+  if (nsContentUtils::ShouldResistFingerprinting(
+        mPresShell->GetPresContext()->GetDocShell()))
+    return nullptr;
+
   nsROCSSPrimitiveValue* val = new nsROCSSPrimitiveValue;
   val->SetIdent(nsCSSProps::ValueToKeywordEnum(StyleFont()->mFont.smoothing,
                                                nsCSSProps::kFontSmoothingKTable));
diff --git a/layout/style/nsMediaFeatures.cpp b/layout/style/nsMediaFeatures.cpp
index d3a15e748002..838d2725f731 100644
--- a/layout/style/nsMediaFeatures.cpp
+++ b/layout/style/nsMediaFeatures.cpp
@@ -109,13 +109,19 @@ GetDeviceContextFor(nsPresContext* aPresContext)
   return aPresContext->DeviceContext();
 }
 
+static bool
+ShouldResistFingerprinting(nsPresContext* aPresContext)
+{
+    return nsContentUtils::ShouldResistFingerprinting(aPresContext->GetDocShell());
+}
+
 // A helper for three features below.
 static nsSize
 GetDeviceSize(nsPresContext* aPresContext)
 {
     nsSize size;
 
-    if (aPresContext->IsDeviceSizePageSize()) {
+    if (ShouldResistFingerprinting(aPresContext) || aPresContext->IsDeviceSizePageSize()) {
         size = GetSize(aPresContext);
     } else if (aPresContext->IsRootPaginatedDocument()) {
         // We want the page size, including unprintable areas and margins.
@@ -223,13 +229,17 @@ static nsresult
 GetColor(nsPresContext* aPresContext, const nsMediaFeature*,
          nsCSSValue& aResult)
 {
-    // FIXME:  This implementation is bogus.  nsDeviceContext
-    // doesn't provide reliable information (should be fixed in bug
-    // 424386).
-    // FIXME: On a monochrome device, return 0!
-    nsDeviceContext *dx = GetDeviceContextFor(aPresContext);
-    uint32_t depth;
-    dx->GetDepth(depth);
+    uint32_t depth = 24; // Use depth of 24 when resisting fingerprinting.
+
+    if (!ShouldResistFingerprinting(aPresContext)) {
+        // FIXME:  This implementation is bogus.  nsDeviceContext
+        // doesn't provide reliable information (should be fixed in bug
+        // 424386).
+        // FIXME: On a monochrome device, return 0!
+        nsDeviceContext *dx = GetDeviceContextFor(aPresContext);
+        dx->GetDepth(depth);
+    }
+
     // The spec says to use bits *per color component*, so divide by 3,
     // and round down, since the spec says to use the smallest when the
     // color components differ.
@@ -267,10 +277,15 @@ static nsresult
 GetResolution(nsPresContext* aPresContext, const nsMediaFeature*,
               nsCSSValue& aResult)
 {
-    // Resolution measures device pixels per CSS (inch/cm/pixel).  We
-    // return it in device pixels per CSS inches.
-    float dpi = float(nsPresContext::AppUnitsPerCSSInch()) /
-                float(aPresContext->AppUnitsPerDevPixel());
+    float dpi = 96; // Use 96 when resisting fingerprinting.
+
+    if (!ShouldResistFingerprinting(aPresContext)) {
+      // Resolution measures device pixels per CSS (inch/cm/pixel).  We
+      // return it in device pixels per CSS inches.
+      dpi = float(nsPresContext::AppUnitsPerCSSInch()) /
+            float(aPresContext->AppUnitsPerDevPixel());
+    }
+
     aResult.SetFloatValue(dpi, eCSSUnit_Inch);
     return NS_OK;
 }
@@ -299,15 +314,26 @@ static nsresult
 GetDevicePixelRatio(nsPresContext* aPresContext, const nsMediaFeature*,
                     nsCSSValue& aResult)
 {
-  float ratio = aPresContext->CSSPixelsToDevPixels(1.0f);
-  aResult.SetFloatValue(ratio, eCSSUnit_Number);
-  return NS_OK;
+    if (!ShouldResistFingerprinting(aPresContext)) {
+        float ratio = aPresContext->CSSPixelsToDevPixels(1.0f);
+        aResult.SetFloatValue(ratio, eCSSUnit_Number);
+    } else {
+        aResult.SetFloatValue(1.0, eCSSUnit_Number);
+    }
+    return NS_OK;
 }
 
 static nsresult
 GetSystemMetric(nsPresContext* aPresContext, const nsMediaFeature* aFeature,
                 nsCSSValue& aResult)
 {
+    aResult.Reset();
+    if (ShouldResistFingerprinting(aPresContext)) {
+        // If "privacy.resistFingerprinting" is enabled, then we simply don't
+        // return any system-backed media feature values. (No spoofed values returned.)
+        return NS_OK;
+    }
+
     MOZ_ASSERT(aFeature->mValueType == nsMediaFeature::eBoolInteger,
                "unexpected type");
     nsIAtom *metricAtom = *aFeature->mData.mMetric;
@@ -321,6 +347,10 @@ GetWindowsTheme(nsPresContext* aPresContext, const nsMediaFeature* aFeature,
                 nsCSSValue& aResult)
 {
     aResult.Reset();
+    if (ShouldResistFingerprinting(aPresContext)) {
+        return NS_OK;
+    }
+
 #ifdef XP_WIN
     uint8_t windowsThemeId =
         nsCSSRuleProcessor::GetWindowsThemeIdentifier();
@@ -346,6 +376,10 @@ GetOperatinSystemVersion(nsPresContext* aPresContext, const nsMediaFeature* aFea
                          nsCSSValue& aResult)
 {
     aResult.Reset();
+    if (ShouldResistFingerprinting(aPresContext)) {
+        return NS_OK;
+    }
+
 #ifdef XP_WIN
     int32_t metricResult;
     if (NS_SUCCEEDED(
diff --git a/layout/style/test/chrome/bug418986-2.js b/layout/style/test/chrome/bug418986-2.js
new file mode 100644
index 000000000000..694f21292805
--- /dev/null
+++ b/layout/style/test/chrome/bug418986-2.js
@@ -0,0 +1,274 @@
+// # Bug 418986, part 2.
+
+/* jshint esnext:true */
+/* jshint loopfunc:true */
+/* global window, screen, ok, SpecialPowers, matchMedia */
+
+SimpleTest.waitForExplicitFinish();
+
+// Expected values. Format: [name, pref_off_value, pref_on_value]
+// If pref_*_value is an array with two values, then we will match
+// any value in between those two values. If a value is null, then
+// we skip the media query.
+let expected_values = [
+  ["color", null, 8],
+  ["color-index", null, 0],
+  ["aspect-ratio", null, window.innerWidth + "/" + window.innerHeight],
+  ["device-aspect-ratio", screen.width + "/" + screen.height,
+                          window.innerWidth + "/" + window.innerHeight],
+  ["device-height", screen.height + "px", window.innerHeight + "px"],
+  ["device-width", screen.width + "px", window.innerWidth + "px"],
+  ["grid", null, 0],
+  ["height", window.innerHeight + "px", window.innerHeight + "px"],
+  ["monochrome", null, 0],
+  // Square is defined as portrait:
+  ["orientation", null,
+                  window.innerWidth > window.innerHeight ?
+                    "landscape" : "portrait"],
+  ["resolution", null, "96dpi"],
+  ["resolution", [0.999 * window.devicePixelRatio + "dppx",
+                  1.001 * window.devicePixelRatio + "dppx"], "1dppx"],
+  ["width", window.innerWidth + "px", window.innerWidth + "px"],
+  ["-moz-device-pixel-ratio", window.devicePixelRatio, 1],
+  ["-moz-device-orientation", screen.width > screen.height ?
+                                "landscape" : "portrait",
+                              window.innerWidth > window.innerHeight ?
+                                "landscape" : "portrait"]
+];
+
+// These media queries return value 0 or 1 when the pref is off.
+// When the pref is on, they should not match.
+let suppressed_toggles = [
+  "-moz-images-in-menus",
+  "-moz-mac-graphite-theme",
+  // Not available on most OSs.
+//  "-moz-maemo-classic",
+  "-moz-scrollbar-end-backward",
+  "-moz-scrollbar-end-forward",
+  "-moz-scrollbar-start-backward",
+  "-moz-scrollbar-start-forward",
+  "-moz-scrollbar-thumb-proportional",
+  "-moz-touch-enabled",
+  "-moz-windows-compositor",
+  "-moz-windows-default-theme",
+  "-moz-windows-glass",
+];
+
+// Possible values for '-moz-os-version'
+let windows_versions = [
+  "windows-xp",
+  "windows-vista",
+  "windows-win7",
+  "windows-win8"];
+
+// Possible values for '-moz-windows-theme'
+let windows_themes = [
+  "aero",
+  "luna-blue",
+  "luna-olive",
+  "luna-silver",
+  "royale",
+  "generic",
+  "zune"
+];
+
+// Read the current OS.
+let OS = SpecialPowers.Services.appinfo.OS;
+
+// If we are using Windows, add an extra toggle only
+// available on that OS.
+if (OS === "WINNT") {
+  suppressed_toggles.push("-moz-windows-classic");
+}
+
+// __keyValMatches(key, val)__.
+// Runs a media query and returns true if key matches to val.
+let keyValMatches = (key, val) => matchMedia("(" + key + ":" + val +")").matches;
+
+// __testMatch(key, val)__.
+// Attempts to run a media query match for the given key and value.
+// If value is an array of two elements [min max], then matches any
+// value in-between.
+let testMatch = function (key, val) {
+  if (val === null) {
+    return;
+  } else if (Array.isArray(val)) {
+    ok(keyValMatches("min-" + key, val[0]) && keyValMatches("max-" + key, val[1]),
+       "Expected " + key + " between " + val[0] + " and " + val[1]);
+  } else {
+    ok(keyValMatches(key, val), "Expected " + key + ":" + val);
+  }
+};
+
+// __testToggles(resisting)__.
+// Test whether we are able to match the "toggle" media queries.
+let testToggles = function (resisting) {
+  suppressed_toggles.forEach(
+    function (key) {
+      var exists = keyValMatches(key, 0) || keyValMatches(key, 1);
+      if (resisting) {
+         ok(!exists, key + " should not exist.");
+      } else {
+         ok(exists, key + " should exist.");
+      }
+    });
+};
+
+// __testWindowsSpecific__.
+// Runs a media query on the queryName with the given possible matching values.
+let testWindowsSpecific = function (resisting, queryName, possibleValues) {
+  let found = false;
+  possibleValues.forEach(function (val) {
+    found = found || keyValMatches(queryName, val);
+  });
+  if (resisting) {
+    ok(!found, queryName + " should have no match");
+  } else {
+    ok(found, queryName + " should match");
+  }
+};
+
+// __generateHtmlLines(resisting)__.
+// Create a series of div elements that look like:
+// `
resolution
`, +// where each line corresponds to a different media query. +let generateHtmlLines = function (resisting) { + let lines = ""; + expected_values.forEach( + function ([key, offVal, onVal]) { + let val = resisting ? onVal : offVal; + if (val) { + lines += "
" + key + "
\n"; + } + }); + suppressed_toggles.forEach( + function (key) { + lines += "
" + key + "
\n"; + }); + if (OS === "WINNT") { + lines += "
-moz-os-version
"; + lines += "
-moz-windows-theme
"; + } + return lines; +}; + +// __cssLine__. +// Creates a line of css that looks something like +// `@media (resolution: 1ppx) { .spoof#resolution { background-color: green; } }`. +let cssLine = function (query, clazz, id, color) { + return "@media " + query + " { ." + clazz + "#" + id + + " { background-color: " + color + "; } }\n"; +}; + +// __mediaQueryCSSLine(key, val, color)__. +// Creates a line containing a CSS media query and a CSS expression. +let mediaQueryCSSLine = function (key, val, color) { + if (val === null) { + return ""; + } + let query; + if (Array.isArray(val)) { + query = "(min-" + key + ": " + val[0] + ") and (max-" + key + ": " + val[1] + ")"; + } else { + query = "(" + key + ": " + val + ")"; + } + return cssLine(query, "spoof", key, color); +}; + +// __suppressedMediaQueryCSSLine(key, color)__. +// Creates a CSS line that matches the existence of a +// media query that is supposed to be suppressed. +let suppressedMediaQueryCSSLine = function (key, color, suppressed) { + let query = "(" + key + ": 0), (" + key + ": 1)"; + return cssLine(query, "suppress", key, color); +}; + +// __generateCSSLines(resisting)__. +// Creates a series of lines of CSS, each of which corresponds to +// a different media query. If the query produces a match to the +// expected value, then the element will be colored green. +let generateCSSLines = function (resisting) { + let lines = ".spoof { background-color: red;}\n"; + expected_values.forEach( + function ([key, offVal, onVal]) { + lines += mediaQueryCSSLine(key, resisting ? onVal : offVal, "green"); + }); + lines += ".suppress { background-color: " + (resisting ? "green" : "red") + ";}\n"; + suppressed_toggles.forEach( + function (key) { + lines += suppressedMediaQueryCSSLine(key, resisting ? "red" : "green"); + }); + if (OS === "WINNT") { + lines += ".windows { background-color: " + (resisting ? "green" : "red") + ";}\n"; + lines += windows_versions.map(val => "(-moz-os-version: " + val + ")").join(", ") + + " { #-moz-os-version { background-color: " + (resisting ? "red" : "green") + ";} }\n"; + lines += windows_themes.map(val => "(-moz-windows-theme: " + val + ")").join(",") + + " { #-moz-windows-theme { background-color: " + (resisting ? "red" : "green") + ";} }\n"; + } + return lines; +}; + +// __green__. +// Returns the computed color style corresponding to green. +let green = (function () { + let temp = document.createElement("span"); + temp.style.backgroundColor = "green"; + return getComputedStyle(temp).backgroundColor; +})(); + +// __testCSS(resisting)__. +// Creates a series of divs and CSS using media queries to set their +// background color. If all media queries match as expected, then +// all divs should have a green background color. +let testCSS = function (resisting) { + document.getElementById("display").innerHTML = generateHtmlLines(resisting); + document.getElementById("test-css").innerHTML = generateCSSLines(resisting); + let cssTestDivs = document.querySelectorAll(".spoof,.suppress"); + for (let div of cssTestDivs) { + let color = window.getComputedStyle(div).backgroundColor; + ok(color === green, "CSS for '" + div.id + "'"); + } +}; + +// __testOSXFontSmoothing(resisting)__. +// When fingerprinting resistance is enabled, the `getComputedStyle` +// should always return `undefined` for `MozOSXFontSmoothing`. +let testOSXFontSmoothing = function (resisting) { + let div = document.createElement("div"); + div.style.MozOsxFontSmoothing = "unset"; + let readBack = window.getComputedStyle(div).MozOsxFontSmoothing; + let smoothingPref = SpecialPowers.getBoolPref("layout.css.osx-font-smoothing.enabled", false); + is(readBack, resisting ? "" : (smoothingPref ? "auto" : ""), + "-moz-osx-font-smoothing"); +}; + +// An iterator yielding pref values for two consecutive tests. +let prefVals = (for (prefVal of [false, true]) prefVal); + +// __test(isContent)__. +// Run all tests. +let test = function(isContent) { + let {value: prefValue, done} = prefVals.next(); + if (done) { + SimpleTest.finish(); + return; + } + SpecialPowers.pushPrefEnv({set: [["privacy.resistFingerprinting", prefValue]]}, + function () { + let resisting = prefValue && isContent; + expected_values.forEach( + function ([key, offVal, onVal]) { + testMatch(key, resisting ? onVal : offVal); + }); + testToggles(resisting); + if (OS === "WINNT") { + testWindowsSpecific(resisting, "-moz-os-version", windows_versions); + testWindowsSpecific(resisting, "-moz-windows-theme", windows_themes); + } + testCSS(resisting); + if (OS === "Darwin") { + testOSXFontSmoothing(resisting); + } + test(isContent); + }); +}; diff --git a/layout/style/test/chrome/chrome.ini b/layout/style/test/chrome/chrome.ini index 46796ab13e93..61f0244e6931 100644 --- a/layout/style/test/chrome/chrome.ini +++ b/layout/style/test/chrome/chrome.ini @@ -1,6 +1,7 @@ [DEFAULT] skip-if = buildapp == 'b2g' support-files = + bug418986-2.js bug535806-css.css bug535806-html.html bug535806-xul.xul @@ -9,6 +10,7 @@ support-files = [test_addSheet.html] [test_additional_sheets.html] [test_author_specified_style.html] +[test_bug418986-2.xul] [test_bug1157097.html] [test_bug1160724.xul] [test_bug535806.xul] diff --git a/layout/style/test/chrome/test_bug418986-2.xul b/layout/style/test/chrome/test_bug418986-2.xul new file mode 100644 index 000000000000..8424e15ac108 --- /dev/null +++ b/layout/style/test/chrome/test_bug418986-2.xul @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index 80c28110c3cd..63677d530346 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -5,6 +5,7 @@ support-files = ccd.sjs ccd-standards.html css_properties.js + chrome/bug418986-2.js descriptor_database.js empty.html media_queries_dynamic_xbl_binding.xml @@ -75,6 +76,7 @@ skip-if = true # Bug 701060 [test_bug412901.html] skip-if = android_version == '18' # bug 1147986 [test_bug413958.html] +[test_bug418986-2.html] [test_bug437915.html] [test_bug450191.html] [test_bug453896_deck.html] diff --git a/layout/style/test/test_bug418986-2.html b/layout/style/test/test_bug418986-2.html new file mode 100644 index 000000000000..15d7c71ce44e --- /dev/null +++ b/layout/style/test/test_bug418986-2.html @@ -0,0 +1,29 @@ + + + + + + Test 2/3 for Bug #418986: Resist fingerprinting by preventing exposure of screen and system info + + + + + + + +Bug 418986 +

TEST

+ +
+
+ +