Bug 1217238 - Reduce time precision when privacy.resistFingerprinting is on. draft
authorJonathan Hao <jhao@mozilla.com>
Tue, 06 Jun 2017 11:45:14 +0800
changeset 591428 09e60b4a6e70c57f927309b9497a0188feef38e8
parent 591427 9327ff05097fa2877ed0d5e9da1b42244cf7b2b3
child 593938 cc886c2ecffbc68a75cb8d6f49003b2087e8ed3c
push id63055
push userbmo:jhao@mozilla.com
push dateFri, 09 Jun 2017 02:56:03 +0000
bugs1217238, 1517
milestone55.0a1
Bug 1217238 - Reduce time precision when privacy.resistFingerprinting is on. This patch is adapted from Tor bug 1517. To offer some protection against timing attacks by JS content pages, in this patch we round the various time-exposing APIs (such as Date and Event.timeStamps) to the nearest 100 ms when the pref "privacy.resistFingerprinting" is on. MozReview-Commit-ID: eGucM9nGTn
dom/events/Event.cpp
dom/events/Event.h
dom/file/BaseBlobImpl.cpp
dom/file/MultipartBlobImpl.cpp
dom/media/DOMMediaStream.cpp
dom/media/webaudio/AudioContext.cpp
dom/performance/Performance.cpp
js/public/Date.h
js/src/jsdate.cpp
layout/style/nsAnimationManager.h
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/nsRFPService.h
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -27,16 +27,17 @@
 #include "nsIFrame.h"
 #include "nsIContent.h"
 #include "nsIDocument.h"
 #include "nsIPresShell.h"
 #include "nsIScrollableFrame.h"
 #include "nsJSEnvironment.h"
 #include "nsLayoutUtils.h"
 #include "nsPIWindowRoot.h"
+#include "nsRFPService.h"
 #include "WorkerPrivate.h"
 
 namespace mozilla {
 namespace dom {
 
 static char *sPopupAllowedEvents;
 
 static bool sReturnHighResTimeStamp = false;
@@ -1089,17 +1090,17 @@ Event::DefaultPrevented(CallerType aCall
   // If preventDefault() has been called by content, return true.  Otherwise,
   // i.e., preventDefault() has been called by chrome, return true only when
   // this is called by chrome.
   return mEvent->DefaultPreventedByContent() ||
          aCallerType == CallerType::System;
 }
 
 double
-Event::TimeStamp() const
+Event::TimeStampImpl() const
 {
   if (!sReturnHighResTimeStamp) {
     return static_cast<double>(mEvent->mTime);
   }
 
   if (mEvent->mTimeStamp.IsNull()) {
     return 0.0;
   }
@@ -1124,16 +1125,22 @@ Event::TimeStamp() const
 
   workers::WorkerPrivate* workerPrivate =
     workers::GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(workerPrivate);
 
   return workerPrivate->TimeStampToDOMHighRes(mEvent->mTimeStamp);
 }
 
+double
+Event::TimeStamp() const
+{
+  return nsRFPService::ReduceTimePrecisionAsMSecs(TimeStampImpl());
+}
+
 bool
 Event::GetPreventDefault() const
 {
   nsCOMPtr<nsPIDOMWindowInner> win(do_QueryInterface(mOwner));
   if (win) {
     if (nsIDocument* doc = win->GetExtantDoc()) {
       doc->WarnOnceAbout(nsIDocument::eGetPreventDefault);
     }
--- a/dom/events/Event.h
+++ b/dom/events/Event.h
@@ -58,16 +58,17 @@ public:
 
 protected:
   virtual ~Event();
 
 private:
   void ConstructorInit(EventTarget* aOwner,
                        nsPresContext* aPresContext,
                        WidgetEvent* aEvent);
+  double TimeStampImpl() const;
 
 public:
   static Event* FromSupports(nsISupports* aSupports)
   {
     nsIDOMEvent* event =
       static_cast<nsIDOMEvent*>(aSupports);
 #ifdef DEBUG
     {
--- a/dom/file/BaseBlobImpl.cpp
+++ b/dom/file/BaseBlobImpl.cpp
@@ -1,15 +1,16 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=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/. */
 
 #include "mozilla/dom/BaseBlobImpl.h"
+#include "nsRFPService.h"
 #include "prtime.h"
 
 namespace mozilla {
 namespace dom {
 
 void
 BaseBlobImpl::GetName(nsAString& aName) const
 {
@@ -58,17 +59,17 @@ BaseBlobImpl::GetType(nsAString& aType)
   aType = mContentType;
 }
 
 int64_t
 BaseBlobImpl::GetLastModified(ErrorResult& aRv)
 {
   MOZ_ASSERT(mIsFile, "Should only be called on files");
   if (IsDateUnknown()) {
-    mLastModificationDate = PR_Now();
+    mLastModificationDate = nsRFPService::ReduceTimePrecisionAsUSecs(PR_Now());
   }
 
   return mLastModificationDate / PR_USEC_PER_MSEC;
 }
 
 void
 BaseBlobImpl::SetLastModified(int64_t aLastModified)
 {
--- a/dom/file/MultipartBlobImpl.cpp
+++ b/dom/file/MultipartBlobImpl.cpp
@@ -6,16 +6,17 @@
 
 #include "MultipartBlobImpl.h"
 #include "jsfriendapi.h"
 #include "mozilla/dom/BlobSet.h"
 #include "mozilla/dom/FileBinding.h"
 #include "mozilla/dom/UnionTypes.h"
 #include "nsDOMClassInfoID.h"
 #include "nsIMultiplexInputStream.h"
+#include "nsRFPService.h"
 #include "nsStringStream.h"
 #include "nsTArray.h"
 #include "nsJSUtils.h"
 #include "nsContentUtils.h"
 #include "nsIScriptError.h"
 #include "nsIXPConnect.h"
 #include <algorithm>
 
@@ -264,18 +265,18 @@ MultipartBlobImpl::SetLengthAndModifiedD
 
   mLength = totalLength;
 
   if (mIsFile) {
     // We cannot use PR_Now() because bug 493756 and, for this reason:
     //   var x = new Date(); var f = new File(...);
     //   x.getTime() < f.dateModified.getTime()
     // could fail.
-    mLastModificationDate =
-      lastModifiedSet ? lastModified * PR_USEC_PER_MSEC : JS_Now();
+    mLastModificationDate = nsRFPService::ReduceTimePrecisionAsUSecs(
+      lastModifiedSet ? lastModified * PR_USEC_PER_MSEC : JS_Now());
   }
 }
 
 void
 MultipartBlobImpl::GetMozFullPathInternal(nsAString& aFilename,
                                           ErrorResult& aRv) const
 {
   if (!mIsFromNsIFile || mBlobImpls.Length() == 0) {
--- a/dom/media/DOMMediaStream.cpp
+++ b/dom/media/DOMMediaStream.cpp
@@ -1,15 +1,16 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
 /* 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 "DOMMediaStream.h"
 #include "nsContentUtils.h"
+#include "nsRFPService.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIScriptError.h"
 #include "nsIUUIDGenerator.h"
 #include "nsPIDOMWindow.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/MediaStreamBinding.h"
 #include "mozilla/dom/MediaStreamTrackEvent.h"
 #include "mozilla/dom/LocalMediaStreamBinding.h"
@@ -548,18 +549,18 @@ DOMMediaStream::Constructor(const Global
 }
 
 double
 DOMMediaStream::CurrentTime()
 {
   if (!mPlaybackStream) {
     return 0.0;
   }
-  return mPlaybackStream->
-    StreamTimeToSeconds(mPlaybackStream->GetCurrentTime() - mLogicalStreamStartTime);
+  return nsRFPService::ReduceTimePrecisionAsSecs(mPlaybackStream->
+    StreamTimeToSeconds(mPlaybackStream->GetCurrentTime() - mLogicalStreamStartTime));
 }
 
 void
 DOMMediaStream::GetId(nsAString& aID) const
 {
   aID = mID;
 }
 
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -55,16 +55,17 @@
 #include "MediaStreamAudioDestinationNode.h"
 #include "MediaStreamAudioSourceNode.h"
 #include "MediaStreamGraph.h"
 #include "nsContentUtils.h"
 #include "nsNetCID.h"
 #include "nsNetUtil.h"
 #include "nsPIDOMWindow.h"
 #include "nsPrintfCString.h"
+#include "nsRFPService.h"
 #include "OscillatorNode.h"
 #include "PannerNode.h"
 #include "PeriodicWave.h"
 #include "ScriptProcessorNode.h"
 #include "StereoPannerNode.h"
 #include "WaveShaperNode.h"
 
 namespace mozilla {
@@ -628,17 +629,18 @@ AudioContext::DestinationStream() const
   }
   return nullptr;
 }
 
 double
 AudioContext::CurrentTime() const
 {
   MediaStream* stream = Destination()->Stream();
-  return stream->StreamTimeToSeconds(stream->GetCurrentTime());
+  return nsRFPService::ReduceTimePrecisionAsSecs(
+    stream->StreamTimeToSeconds(stream->GetCurrentTime()));
 }
 
 void AudioContext::DisconnectFromOwner()
 {
   mIsDisconnecting = true;
   Shutdown();
   DOMEventTargetHelper::DisconnectFromOwner();
 }
--- a/dom/performance/Performance.cpp
+++ b/dom/performance/Performance.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=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/. */
 
 #include "Performance.h"
 
 #include "GeckoProfiler.h"
+#include "nsRFPService.h"
 #ifdef MOZ_GECKO_PROFILER
 #include "ProfilerMarkerPayload.h"
 #endif
 #include "PerformanceEntry.h"
 #include "PerformanceMainThread.h"
 #include "PerformanceMark.h"
 #include "PerformanceMeasure.h"
 #include "PerformanceObserver.h"
@@ -247,17 +248,18 @@ Performance::ClearResourceTimings()
 
 DOMHighResTimeStamp
 Performance::RoundTime(double aTime) const
 {
   // Round down to the nearest 5us, because if the timer is too accurate people
   // can do nasty timing attacks with it.  See similar code in the worker
   // Performance implementation.
   const double maxResolutionMs = 0.005;
-  return floor(aTime / maxResolutionMs) * maxResolutionMs;
+  return nsRFPService::ReduceTimePrecisionAsMSecs(
+    floor(aTime / maxResolutionMs) * maxResolutionMs);
 }
 
 
 void
 Performance::Mark(const nsAString& aName, ErrorResult& aRv)
 {
   // Don't add the entry if the buffer is full. XXX should be removed by bug 1159003.
   if (mUserEntries.Length() >= mResourceTimingBufferSize) {
--- a/js/public/Date.h
+++ b/js/public/Date.h
@@ -168,11 +168,16 @@ DayFromYear(double year);
 
 // Takes an integer number of milliseconds since the epoch and an integer year,
 // returns the number of days in that year. If |time| is nonfinite, returns NaN.
 // Otherwise |time| *must* correspond to a time within the valid year |year|.
 // This should usually be ensured by computing |year| as |JS::DayFromYear(time)|.
 JS_PUBLIC_API(double)
 DayWithinYear(double time, double year);
 
+// Sets the time resolution for fingerprinting protection.
+// If it's set to zero, then no rounding will happen.
+JS_PUBLIC_API(void)
+SetTimeResolutionUsec(uint32_t resolution);
+
 } // namespace JS
 
 #endif /* js_Date_h */
--- a/js/src/jsdate.cpp
+++ b/js/src/jsdate.cpp
@@ -13,16 +13,17 @@
  *  might have been left to the operator."
  *
  * Frederick Brooks, 'The Second-System Effect'.
  */
 
 #include "jsdate.h"
 
 #include "mozilla/ArrayUtils.h"
+#include "mozilla/Atomics.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/Sprintf.h"
 
 #include <ctype.h>
 #include <math.h>
 #include <string.h>
 
 #include "jsapi.h"
@@ -43,27 +44,32 @@
 #include "vm/String.h"
 #include "vm/StringBuffer.h"
 #include "vm/Time.h"
 
 #include "jsobjinlines.h"
 
 using namespace js;
 
+using mozilla::Atomic;
 using mozilla::ArrayLength;
 using mozilla::IsFinite;
 using mozilla::IsNaN;
 using mozilla::NumbersAreIdentical;
+using mozilla::ReleaseAcquire;
 
 using JS::AutoCheckCannotGC;
 using JS::ClippedTime;
 using JS::GenericNaN;
 using JS::TimeClip;
 using JS::ToInteger;
 
+// When this value is non-zero, we'll round the time by this resolution.
+static Atomic<uint32_t, ReleaseAcquire> sResolutionUsec;
+
 /*
  * The JS 'Date' object is patterned after the Java 'Date' object.
  * Here is a script:
  *
  *    today = new Date();
  *
  *    print(today.toLocaleString());
  *
@@ -394,16 +400,22 @@ JS::DayFromYear(double year)
 }
 
 JS_PUBLIC_API(double)
 JS::DayWithinYear(double time, double year)
 {
     return ::DayWithinYear(time, year);
 }
 
+JS_PUBLIC_API(void)
+JS::SetTimeResolutionUsec(uint32_t resolution)
+{
+    sResolutionUsec = resolution;
+}
+
 /*
  * Find a year for which any given date will fall on the same weekday.
  *
  * This function should be used with caution when used other than
  * for determining DST; it hasn't been proven not to produce an
  * incorrect year for times near year boundaries.
  */
 static int
@@ -1245,17 +1257,21 @@ date_parse(JSContext* cx, unsigned argc,
 
     args.rval().set(TimeValue(result));
     return true;
 }
 
 static ClippedTime
 NowAsMillis()
 {
-    return TimeClip(static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_MSEC);
+    double now = PRMJ_Now();
+    if (sResolutionUsec) {
+        now = floor(now / sResolutionUsec) * sResolutionUsec;
+    }
+    return TimeClip(now / PRMJ_USEC_PER_MSEC);
 }
 
 bool
 js::date_now(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().set(TimeValue(NowAsMillis()));
     return true;
--- a/layout/style/nsAnimationManager.h
+++ b/layout/style/nsAnimationManager.h
@@ -8,16 +8,17 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/EventForwards.h"
 #include "AnimationCommon.h"
 #include "mozilla/dom/Animation.h"
 #include "mozilla/Keyframe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/TimeStamp.h"
+#include "nsRFPService.h"
 
 class nsIGlobalObject;
 class nsStyleContext;
 struct nsStyleDisplay;
 struct ServoComputedValues;
 
 namespace mozilla {
 namespace css {
@@ -46,17 +47,18 @@ struct AnimationEventInfo {
                      dom::Animation* aAnimation)
     : mElement(aElement)
     , mAnimation(aAnimation)
     , mEvent(true, aMessage)
     , mTimeStamp(aTimeStamp)
   {
     // XXX Looks like nobody initialize WidgetEvent::time
     mEvent.mAnimationName = aAnimationName;
-    mEvent.mElapsedTime = aElapsedTime.ToSeconds();
+    mEvent.mElapsedTime =
+      nsRFPService::ReduceTimePrecisionAsSecs(aElapsedTime.ToSeconds());
     mEvent.mPseudoElement =
       AnimationCollection<dom::CSSAnimation>::PseudoTypeAsString(aPseudoType);
   }
 
   // InternalAnimationEvent doesn't support copy-construction, so we need
   // to ourselves in order to work with nsTArray
   AnimationEventInfo(const AnimationEventInfo& aOther)
     : mElement(aOther.mElement)
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -16,25 +16,28 @@
 
 #include "nsIObserverService.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
 #include "nsJSUtils.h"
 
 #include "prenv.h"
 
+#include "js/Date.h"
+
 using namespace mozilla;
 
 #define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting"
 
 NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver)
 
 static StaticRefPtr<nsRFPService> sRFPService;
 static bool sInitialized = false;
-bool nsRFPService::sPrivacyResistFingerprinting = false;
+Atomic<bool, ReleaseAcquire> nsRFPService::sPrivacyResistFingerprinting;
+static uint32_t kResolutionUSec = 100000;
 
 /* static */
 nsRFPService*
 nsRFPService::GetOrCreate()
 {
   if (!sInitialized) {
     sRFPService = new nsRFPService();
     nsresult rv = sRFPService->Init();
@@ -46,16 +49,54 @@ nsRFPService::GetOrCreate()
 
     ClearOnShutdown(&sRFPService);
     sInitialized = true;
   }
 
   return sRFPService;
 }
 
+/* static */
+double
+nsRFPService::ReduceTimePrecisionAsMSecs(double aTime)
+{
+  if (!IsResistFingerprintingEnabled()) {
+    return aTime;
+  }
+  const double resolutionMSec = kResolutionUSec / 1000.0;
+  return floor(aTime / resolutionMSec) * resolutionMSec;
+}
+
+/* static */
+double
+nsRFPService::ReduceTimePrecisionAsUSecs(double aTime)
+{
+  if (!IsResistFingerprintingEnabled()) {
+    return aTime;
+  }
+  return floor(aTime / kResolutionUSec) * kResolutionUSec;
+}
+
+/* static */
+double
+nsRFPService::ReduceTimePrecisionAsSecs(double aTime)
+{
+  if (!IsResistFingerprintingEnabled()) {
+    return aTime;
+  }
+  if (kResolutionUSec < 1000000) {
+    // The resolution is smaller than one sec.  Use the reciprocal to avoid
+    // floating point error.
+    const double resolutionSecReciprocal = 1000000.0 / kResolutionUSec;
+    return floor(aTime * resolutionSecReciprocal) / resolutionSecReciprocal;
+  }
+  const double resolutionSec = kResolutionUSec / 1000000.0;
+  return floor(aTime / resolutionSec) * resolutionSec;
+}
+
 nsresult
 nsRFPService::Init()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsresult rv;
 
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
@@ -80,21 +121,24 @@ nsRFPService::Init()
   // and set the timezone.
   UpdatePref();
   return rv;
 }
 
 void
 nsRFPService::UpdatePref()
 {
+  MOZ_ASSERT(NS_IsMainThread());
   sPrivacyResistFingerprinting = Preferences::GetBool(RESIST_FINGERPRINTING_PREF);
 
   if (sPrivacyResistFingerprinting) {
     PR_SetEnv("TZ=UTC");
+    JS::SetTimeResolutionUsec(kResolutionUSec);
   } else if (sInitialized) {
+    JS::SetTimeResolutionUsec(0);
     // We will not touch the TZ value if 'privacy.resistFingerprinting' is false during
     // the time of initialization.
     if (!mInitialTZValue.IsEmpty()) {
       nsAutoCString tzValue = NS_LITERAL_CSTRING("TZ=") + mInitialTZValue;
       PR_SetEnv(tzValue.get());
     } else {
 #if defined(XP_LINUX) || defined (XP_MACOSX)
       // For POSIX like system, we reset the TZ to the /etc/localtime, which is the
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #ifndef __nsRFPService_h__
 #define __nsRFPService_h__
 
+#include "mozilla/Atomics.h"
 #include "nsIObserver.h"
 
 #include "nsString.h"
 
 namespace mozilla {
 
 class nsRFPService final : public nsIObserver
 {
@@ -19,26 +20,31 @@ public:
   NS_DECL_NSIOBSERVER
 
   static nsRFPService* GetOrCreate();
   static bool IsResistFingerprintingEnabled()
   {
     return sPrivacyResistFingerprinting;
   }
 
+  // The following Reduce methods can be called off main thread.
+  static double ReduceTimePrecisionAsMSecs(double aTime);
+  static double ReduceTimePrecisionAsUSecs(double aTime);
+  static double ReduceTimePrecisionAsSecs(double aTime);
+
 private:
   nsresult Init();
 
   nsRFPService() {}
 
   ~nsRFPService() {}
 
   void UpdatePref();
   void StartShutdown();
 
-  static bool sPrivacyResistFingerprinting;
+  static Atomic<bool, ReleaseAcquire> sPrivacyResistFingerprinting;
 
   nsCString mInitialTZValue;
 };
 
 } // mozilla namespace
 
 #endif /* __nsRFPService_h__ */