Bug 1425462 When reducing the precision of timestamps, also apply fuzzytime to them r=bkelly
authorTom Ritter <tom@mozilla.com>
Thu, 01 Mar 2018 00:07:03 -0600
changeset 406446 3052a1cf3b1c20b4d9bcc6c03bb1e8d45fddca0b
parent 406445 c2307a921159af7238d7fa9f4ccc9593928fec1f
child 406447 dd7b7e91140a3c42f395b6a0a58e5db90e28db1b
push id33558
push userrgurzau@mozilla.com
push dateSat, 03 Mar 2018 21:46:37 +0000
treeherdermozilla-central@8cced2a46f73 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbkelly
bugs1425462
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1425462 When reducing the precision of timestamps, also apply fuzzytime to them r=bkelly Fuzzytime deterministically generates a random midpoint between two clamped values, and if the unreduced timestamp is above the midpoint, the time is rounded upwards. This allows safe time jittering to occur, as time will never go backwards on a given timeline. It _is_ possible for time to go backwards when comparing different (but related) timelines, such as a relative timeline in one page (with its own performance.timeOrigin) and a relative timeline in an iframe or Worker (which also has its own performance.timeOrigin). This is the same behavior as the 2ms timer reduction we previously landed; jitter doesn't make this any better or worse. MozReview-Commit-ID: IdRLxcWDQBZ
browser/app/profile/firefox.js
dom/ipc/ContentPrefs.cpp
js/public/Date.h
js/src/jsdate.cpp
modules/libpref/init/all.js
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/nsRFPService.h
toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1226,16 +1226,17 @@ pref("services.sync.prefs.sync.privacy.c
 pref("services.sync.prefs.sync.privacy.clearOnShutdown.siteSettings", true);
 pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true);
 pref("services.sync.prefs.sync.privacy.sanitize.sanitizeOnShutdown", true);
 pref("services.sync.prefs.sync.privacy.trackingprotection.enabled", true);
 pref("services.sync.prefs.sync.privacy.trackingprotection.pbmode.enabled", true);
 pref("services.sync.prefs.sync.privacy.resistFingerprinting", true);
 pref("services.sync.prefs.sync.privacy.reduceTimerPrecision", true);
 pref("services.sync.prefs.sync.privacy.resistFingerprinting.reduceTimerPrecision.microseconds", true);
+pref("services.sync.prefs.sync.privacy.resistFingerprinting.reduceTimerPrecision.jitter", true);
 pref("services.sync.prefs.sync.security.OCSP.enabled", true);
 pref("services.sync.prefs.sync.security.OCSP.require", true);
 pref("services.sync.prefs.sync.security.default_personal_cert", true);
 pref("services.sync.prefs.sync.security.tls.version.min", true);
 pref("services.sync.prefs.sync.security.tls.version.max", true);
 pref("services.sync.prefs.sync.services.sync.syncedTabs.showRemoteIcons", true);
 pref("services.sync.prefs.sync.signon.rememberSignons", true);
 pref("services.sync.prefs.sync.spellchecker.dictionary", true);
--- a/dom/ipc/ContentPrefs.cpp
+++ b/dom/ipc/ContentPrefs.cpp
@@ -288,16 +288,17 @@ const char* mozilla::dom::ContentPrefs::
   "network.tcp.sendbuffer",
   "nglayout.debug.invalidation",
   "privacy.donottrackheader.enabled",
   "privacy.firstparty.isolate",
   "privacy.firstparty.isolate.restrict_opener_access",
   "privacy.reduceTimerPrecision",
   "privacy.resistFingerprinting",
   "privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts",
+  "privacy.resistFingerprinting.reduceTimerPrecision.jitter",
   "privacy.resistFingerprinting.reduceTimerPrecision.microseconds",
   "privacy.resistFingerprinting.target_video_res",
   "privacy.resistFingerprinting.video_dropped_ratio",
   "privacy.resistFingerprinting.video_frames_per_sec",
   "privacy.trackingprotection.lower_network_priority",
   "privacy.window.maxInnerHeight",
   "privacy.window.maxInnerWidth",
   "security.csp.enable",
--- a/js/public/Date.h
+++ b/js/public/Date.h
@@ -169,13 +169,13 @@ DayFromYear(double year);
 // 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);
+SetTimeResolutionUsec(uint32_t resolution, bool jitter);
 
 } // namespace JS
 
 #endif /* js_Date_h */
--- a/js/src/jsdate.cpp
+++ b/js/src/jsdate.cpp
@@ -58,16 +58,18 @@ using mozilla::Relaxed;
 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, Relaxed> sResolutionUsec;
+// This is not implemented yet, but we will use this to know to jitter the time in the JS shell
+static Atomic<bool, Relaxed> sJitter;
 
 /*
  * The JS 'Date' object is patterned after the Java 'Date' object.
  * Here is a script:
  *
  *    today = new Date();
  *
  *    print(today.toLocaleString());
@@ -400,19 +402,20 @@ 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)
+JS::SetTimeResolutionUsec(uint32_t resolution, bool jitter)
 {
     sResolutionUsec = resolution;
+    sJitter = jitter;
 }
 
 /*
  * 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.
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1428,16 +1428,18 @@ pref("privacy.resistFingerprinting", fal
 // information so we can understand why it is needed.
 pref("privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts", true);
 // A subset of Resist Fingerprinting protections focused specifically on timers for testing
 // This affects the Animation API, the performance APIs, Date.getTime, Event.timestamp,
 //   File.lastModified, audioContext.currentTime, canvas.captureStream.currentTime
 pref("privacy.reduceTimerPrecision", true);
 // Dynamically tune the resolution of the timer reduction for both of the two above prefs
 pref("privacy.resistFingerprinting.reduceTimerPrecision.microseconds", 2000);
+// Enable jittering the clock one precision value forward
+pref("privacy.resistFingerprinting.reduceTimerPrecision.jitter", false);
 // Lower the priority of network loads for resources on the tracking protection list.
 // Note that this requires the privacy.trackingprotection.annotate_channels pref to be on in order to have any effect.
 #ifdef NIGHTLY_BUILD
 pref("privacy.trackingprotection.lower_network_priority", true);
 #else
 pref("privacy.trackingprotection.lower_network_priority", false);
 #endif
 
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -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/. */
 
 #include "nsRFPService.h"
 
 #include <algorithm>
+#include <memory>
 #include <time.h>
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Logging.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/TextEvents.h"
@@ -18,19 +19,21 @@
 
 #include "nsCOMPtr.h"
 #include "nsCoord.h"
 #include "nsServiceManagerUtils.h"
 #include "nsString.h"
 #include "nsXULAppAPI.h"
 #include "nsPrintfCString.h"
 
+#include "nsICryptoHash.h"
 #include "nsIObserverService.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
+#include "nsIRandomGenerator.h"
 #include "nsIXULAppInfo.h"
 #include "nsIXULRuntime.h"
 #include "nsJSUtils.h"
 
 #include "prenv.h"
 
 #include "js/Date.h"
 
@@ -40,16 +43,18 @@ using namespace std;
 #ifdef DEBUG
 static mozilla::LazyLogModule gResistFingerprintingLog("nsResistFingerprinting");
 #endif
 
 #define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting"
 #define RFP_TIMER_PREF "privacy.reduceTimerPrecision"
 #define RFP_TIMER_VALUE_PREF "privacy.resistFingerprinting.reduceTimerPrecision.microseconds"
 #define RFP_TIMER_VALUE_DEFAULT 2000
+#define RFP_JITTER_VALUE_PREF "privacy.resistFingerprinting.reduceTimerPrecision.jitter"
+#define RFP_JITTER_VALUE_DEFAULT false
 #define RFP_SPOOFED_FRAMES_PER_SEC_PREF "privacy.resistFingerprinting.video_frames_per_sec"
 #define RFP_SPOOFED_DROPPED_RATIO_PREF  "privacy.resistFingerprinting.video_dropped_ratio"
 #define RFP_TARGET_VIDEO_RES_PREF "privacy.resistFingerprinting.target_video_res"
 #define RFP_SPOOFED_FRAMES_PER_SEC_DEFAULT 30
 #define RFP_SPOOFED_DROPPED_RATIO_DEFAULT  5
 #define RFP_TARGET_VIDEO_RES_DEFAULT 480
 #define PROFILE_INITIALIZED_TOPIC "profile-initial-state"
 
@@ -69,21 +74,25 @@ NS_IMPL_ISUPPORTS(nsRFPService, nsIObser
  */
 
 static StaticRefPtr<nsRFPService> sRFPService;
 static bool sInitialized = false;
 Atomic<bool, Relaxed> nsRFPService::sPrivacyResistFingerprinting;
 Atomic<bool, Relaxed> nsRFPService::sPrivacyTimerPrecisionReduction;
 // Note: anytime you want to use this variable, you should probably use TimerResolution() instead
 Atomic<uint32_t, Relaxed> sResolutionUSec;
+Atomic<bool, Relaxed> sJitter;
 static uint32_t sVideoFramesPerSec;
 static uint32_t sVideoDroppedRatio;
 static uint32_t sTargetVideoRes;
 nsDataHashtable<KeyboardHashKey, const SpoofingKeyboardCode*>*
   nsRFPService::sSpoofingKeyboardCodes = nullptr;
+UniquePtr<LRUCache> nsRFPService::sCache;
+UniquePtr<uint8_t[]> nsRFPService::sSecretMidpointSeed;
+mozilla::Mutex* nsRFPService::sLock = nullptr;
 
 /* static */
 nsRFPService*
 nsRFPService::GetOrCreate()
 {
   if (!sInitialized) {
     sRFPService = new nsRFPService();
     nsresult rv = sRFPService->Init();
@@ -123,16 +132,268 @@ nsRFPService::IsTimerPrecisionReductionE
   if (aType == TimerPrecisionType::RFPOnly) {
     return IsResistFingerprintingEnabled();
   }
 
   return (sPrivacyTimerPrecisionReduction || IsResistFingerprintingEnabled()) &&
          TimerResolution() > 0;
 }
 
+/*
+ * The below is a simple time-based Least Recently Used cache used to store the
+ * result of a cryptographic hash function. It has LRU_CACHE_SIZE slots, and will
+ * be used from multiple threads. It will be thread-safe in a future commit.
+ */
+#define LRU_CACHE_SIZE         (45)
+#define HASH_DIGEST_SIZE_BITS  (256)
+#define HASH_DIGEST_SIZE_BYTES (HASH_DIGEST_SIZE_BITS / 8)
+
+// TODO: Fix Race Conditions
+class LRUCache
+{
+public:
+  LRUCache() {
+    this->cache.SetLength(LRU_CACHE_SIZE);
+  }
+
+  nsCString Get(long long aKey) {
+    for (auto & cacheEntry : this->cache) {
+      if (cacheEntry.key == aKey) {
+        cacheEntry.accessTime = PR_Now();
+
+#if defined(DEBUG)
+        MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
+          ("LRU Cache HIT with %lli == %lli", aKey, cacheEntry.key));
+#endif
+        return cacheEntry.data;
+      }
+    }
+    return EmptyCString();
+  }
+
+  void Store(long long aKey, const nsCString& aValue) {
+    MOZ_DIAGNOSTIC_ASSERT(aValue.Length() == HASH_DIGEST_SIZE_BYTES);
+
+    CacheEntry* lowestKey = &this->cache[0];
+    for (auto & cacheEntry : this->cache) {
+      if (cacheEntry.accessTime < lowestKey->accessTime) {
+        lowestKey = &cacheEntry;
+      }
+    }
+
+    lowestKey->key = aKey;
+    lowestKey->data = aValue;
+    lowestKey->accessTime = PR_Now();
+#if defined(DEBUG)
+    MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose, ("LRU Cache STORE with %lli", aKey));
+#endif
+  }
+
+
+private:
+  struct CacheEntry {
+    long long key;
+    PRTime accessTime = 0;
+    nsCString data;
+
+    CacheEntry() {
+      this->key = 0xFFFFFFFFFFFFFFFF;
+      this->accessTime = 0;
+      this->data = nullptr;
+    }
+  };
+
+  AutoTArray<CacheEntry, LRU_CACHE_SIZE> cache;
+};
+
+/**
+ * The purpose of this function is to deterministicly generate a random midpoint
+ * between a lower clamped value and an upper clamped value. Assuming a clamping
+ * resolution of 100, here is an example:
+ *
+ * |---------------------------------------|--------------------------|
+ * lower clamped value (e.g. 300)          |           upper clamped value (400)
+ *                              random midpoint (e.g. 360)
+ *
+ * If our actual timestamp (e.g. 325) is below the midpoint, we keep it clamped
+ * downwards. If it were equal to or above the midpoint (e.g. 365) we would
+ * round it upwards to the largest clamped value (in this example: 400).
+ *
+ * The question is: does time go backwards?
+ *
+ * The midpoint is deterministicly random
+ * and generated from two components: a secret seed and a clamped time.
+ *
+ * When comparing times across different seed values: time may go backwards.
+ * For a clamped time of 300, one seed may generate a midpoint of 305 and another
+ * 395. So comparing an (actual) timestamp of 325 and 351 could see the 325 clamped
+ * up to 400 and the 351 clamped down to 300. The seed is per-process, so this case
+ * occurs when one can compare timestamps cross-process. This is uncommon (because
+ * we don't have site isolation.) The circumstances this could occur are
+ * BroadcastChannel, Storage Notification, and in theory (but not yet implemented)
+ * SharedWorker. This should be an exhaustive list (at time of comment writing!).
+ *
+ * Aside from cross-process communication, derived timestamps across different
+ * time origins may go backwards. (Specifically, derived means adding two timestamps
+ * together to get an (approximate) absolute time.)
+ * Assume a page and a worker. If one calls performance.now() in the page and then
+ * triggers a call to performance.now() in the worker, the following invariant should
+ * hold true:
+ *             page.performance.timeOrigin + page.performance.now() <
+ *                        worker.performance.timeOrigin + worker.performance.now()
+ *
+ * We break this invariant.
+ *
+ *
+ * TODO: The above comment is going to need to be entirely rewritten when we mix in
+ * a per-context shared secret. Context is 'Any new object that gets a time origin
+ * starting from zero'. The most obvious example is Documents and Workers. An attacker
+ * could let time go forward and observe (roughly) where the random midpoints fall.
+ * Then they create a new object, time starts back ovr at zero, and they know
+ * (approximately) where the random midpoints are.
+ *
+ * @param aClampedTimeUSec [in]  The clamped input time in microseconds.
+ * @param aResolutionUSec  [in]  The current resolution for clamping in microseconds.
+ * @param aMidpointOut     [out] The midpoint, in microseconds, between [0, aResolutionUSec].
+ * @param aSecretSeed      [in]  TESTING ONLY. When provided, the current seed will be
+ *                               replaced with this value.
+ * @return                 A nsresult indicating success of failure. If the function failed,
+ *                         nothing is written to aMidpointOut
+ */
+
+/* static */
+nsresult
+nsRFPService::RandomMidpoint(long long aClampedTimeUSec,
+                             long long aResolutionUSec,
+                             long long* aMidpointOut,
+                             uint8_t * aSecretSeed /* = nullptr */)
+{
+  nsresult rv;
+  const int kSeedSize = 16;
+  const int kClampTimesPerDigest = HASH_DIGEST_SIZE_BITS / 32;
+
+  if(MOZ_UNLIKELY(!sCache)) {
+    MutexAutoLock lock(*sLock);
+    if(MOZ_LIKELY(!sCache)) {
+      sCache = MakeUnique<LRUCache>();
+    }
+  }
+
+  if(MOZ_UNLIKELY(!aMidpointOut)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  /*
+   * Below, we will call a cryptographic hash function. That's expensive. We look for ways to
+   * make it more efficient.
+   *
+   * We only need as much output from the hash function as the maximum resolution we will
+   * ever support, because we will reduce the output modulo that value. The maximum resolution
+   * we think is likely is in the low seconds value, or about 1-10 million microseconds.
+   * 2**24 is 16 million, so we only need 24 bits of output. Practically speaking though,
+   * it's way easier to work with 32 bits.
+   *
+   * So we're using 32 bits of output and throwing away the other DIGEST_SIZE - 32 (in the case of
+   * SHA-256, DIGEST_SIZE is 256.)  That's a lot of waste.
+   *
+   * Instead of throwing it away, we're going to use all of it. We can handle DIGEST_SIZE / 32
+   * Clamped Time's per hash function - call that , so we reduce aClampedTime to a multiple of
+   * kClampTimesPerDigest (just like we reduced the real time value to aClampedTime!)
+   *
+   * Then we hash _that_ value (assuming it's not in the cache) and index into the digest result
+   * the appropriate bit offset.
+   */
+  long long reducedResolution = aResolutionUSec * kClampTimesPerDigest;
+  long long extraClampedTime = (aClampedTimeUSec / reducedResolution) * reducedResolution;
+
+  nsCString hashResult = sCache->Get(extraClampedTime);
+
+  if(hashResult.Length() != HASH_DIGEST_SIZE_BYTES) { // Cache Miss =(
+    // If someone has pased in the testing-only parameter, replace our seed with it
+    if (aSecretSeed != nullptr) {
+      MutexAutoLock lock(*sLock);
+      if (sSecretMidpointSeed) {
+        // Deletes the object pointed to as well
+        sSecretMidpointSeed = nullptr;
+      }
+      sSecretMidpointSeed = MakeUnique<uint8_t[]>(kSeedSize);
+      memcpy(sSecretMidpointSeed.get(), aSecretSeed, kSeedSize);
+    }
+
+    // If we don't have a seed, we need to get one.
+    if(MOZ_UNLIKELY(!sSecretMidpointSeed)) {
+      MutexAutoLock lock(*sLock);
+      if(MOZ_LIKELY(!sSecretMidpointSeed)) {
+        nsCOMPtr<nsIRandomGenerator> randomGenerator =
+            do_GetService("@mozilla.org/security/random-generator;1", &rv);
+        if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+
+        uint8_t* buffer;
+        rv = randomGenerator->GenerateRandomBytes(kSeedSize, &buffer);
+        if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+        sSecretMidpointSeed.reset(buffer);
+      }
+    }
+
+    /*
+     * Use a cryptographicly secure hash function, but do _not_ use an HMAC.
+     * Obviously we're not using this data for authentication purposes, but
+     * even still an HMAC is a perfect fit here, as we're hashing a value
+     * using a seed that never changes, and an input that does. So why not
+     * use one?
+     *
+     * Basically - we don't need to, it's two invocations of the hash function,
+     * and speed really counts here.
+     *
+     * With authentication off the table, the properties we would get by
+     * using an HMAC here would be:
+     *  - Resistence to length extension
+     *  - Resistence to collision attacks on the underlying hash function
+     *  - Resistence to chosen prefix attacks
+     *
+     * There is no threat of length extension here. Nor is there any real
+     * practical threat of collision: not only are we using a good hash
+     * function (you may mock me in 10 years if it is broken) but we don't
+     * provide the attacker much control over the input. Nor do we let them
+     * have the prefix.
+     */
+
+     // Then hash extraClampedTime and store it in the cache
+     nsCOMPtr<nsICryptoHash> hasher = do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+     NS_ENSURE_SUCCESS(rv, rv);
+
+     rv = hasher->Init(nsICryptoHash::SHA256);
+     NS_ENSURE_SUCCESS(rv, rv);
+
+     rv = hasher->Update(sSecretMidpointSeed.get(), kSeedSize);
+     NS_ENSURE_SUCCESS(rv, rv);
+
+     rv = hasher->Update((const uint8_t *)&extraClampedTime, sizeof(extraClampedTime));
+     NS_ENSURE_SUCCESS(rv, rv);
+
+     nsAutoCStringN<HASH_DIGEST_SIZE_BYTES> derivedSecret;
+     rv = hasher->Finish(false, derivedSecret);
+     NS_ENSURE_SUCCESS(rv, rv);
+
+     // Finally, store it in the cache
+     sCache->Store(extraClampedTime, derivedSecret);
+     hashResult = derivedSecret;
+  }
+
+  // Offset the appropriate index into the hash output, and then turn it into a random midpoint
+  // between 0 and aResolutionUSec
+  int byteOffset = ((aClampedTimeUSec - extraClampedTime) / aResolutionUSec) * 4;
+  uint32_t deterministiclyRandomValue = *BitwiseCast<uint32_t*>(PromiseFlatCString(hashResult).get() + byteOffset);
+  deterministiclyRandomValue %= aResolutionUSec;
+  *aMidpointOut = deterministiclyRandomValue;
+
+  return NS_OK;
+}
+
+
 /**
  * Given a precision value, this function will reduce a given input time to the nearest
  * multiple of that precision.
  *
  * It will check if it is appropriate to clamp the input time according to the values
  * of the privacy.resistFingerprinting and privacy.reduceTimerPrecision preferences.
  * Note that while it will check these prefs, it will use whatever precision is given to
  * it, so if one desires a minimum precision for Resist Fingerprinting, it is the
@@ -177,28 +438,41 @@ nsRFPService::ReduceTimePrecisionImpl(
   // division in integers truncates decimals, taking the result closer to zero (a floor).
   // Below zero, performing the division in integers truncates decimals, taking the result
   // closer to zero (a ceil).
   // The impact of this is that comparing two clamped values that should be related by a
   // constant (e.g. 10s) that are across the zero barrier will no longer work. We need to
   // round consistently towards positive infinity or negative infinity (we chose negative.)
   // This can't be done with a truncation, it must be done with floor.
   long long clamped = floor(double(timeAsInt) / resolutionAsInt) * resolutionAsInt;
+
+
+  long long midpoint = 0,
+            clampedAndJittered = clamped;
+  if (sJitter) {
+    if(!NS_FAILED(RandomMidpoint(clamped, resolutionAsInt, &midpoint)) &&
+       timeAsInt >= clamped + midpoint) {
+      clampedAndJittered += resolutionAsInt;
+    }
+  }
+
   // Cast it back to a double and reduce it to the correct units.
-  double ret = double(clamped) / (1000000.0 / aTimeScale);
+  double ret = double(clampedAndJittered) / (1000000.0 / aTimeScale);
 
 #if defined(DEBUG)
-    MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
-      ("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding with (%lli, Originally %.*f), Intermediate: (%lli), Got: (%lli Converted: %.*f)",
-      DBL_DIG-1, aTime, DBL_DIG-1, timeScaled, timeAsInt, resolutionAsInt, DBL_DIG-1, aResolutionUSec,
-      (long long)floor(double(timeAsInt) / resolutionAsInt), clamped, DBL_DIG-1, ret));
+  bool tmp_jitter = sJitter;
+  MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
+    ("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding with (%lli, Originally %.*f), "
+     "Intermediate: (%lli), Clamped: (%lli) Jitter: (%i Midpoint: %lli) Final: (%lli Converted: %.*f)",
+     DBL_DIG-1, aTime, DBL_DIG-1, timeScaled, timeAsInt, resolutionAsInt, DBL_DIG-1, aResolutionUSec,
+     (long long)floor(double(timeAsInt) / resolutionAsInt), clamped, tmp_jitter, midpoint, clampedAndJittered, DBL_DIG-1, ret));
 #endif
 
-   return ret;
- }
+  return ret;
+}
 
 /* static */
 double
 nsRFPService::ReduceTimePrecisionAsUSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
 {
   return nsRFPService::ReduceTimePrecisionImpl(aTime, MicroSeconds, TimerResolution(), aType);
 }
 
@@ -325,16 +599,18 @@ nsRFPService::GetSpoofedUserAgent(nsACSt
 
 nsresult
 nsRFPService::Init()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsresult rv;
 
+  sLock = new mozilla::Mutex("mozilla.resistFingerprinting.mLock");
+
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE);
 
   rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
 #if defined(XP_WIN)
   rv = obs->AddObserver(this, PROFILE_INITIALIZED_TOPIC, false);
@@ -348,23 +624,29 @@ nsRFPService::Init()
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = prefs->AddObserver(RFP_TIMER_PREF, this, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = prefs->AddObserver(RFP_TIMER_VALUE_PREF, this, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = prefs->AddObserver(RFP_JITTER_VALUE_PREF, this, false);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   Preferences::AddAtomicBoolVarCache(&sPrivacyTimerPrecisionReduction,
                                      RFP_TIMER_PREF,
                                      true);
 
   Preferences::AddAtomicUintVarCache(&sResolutionUSec,
                                      RFP_TIMER_VALUE_PREF,
                                      RFP_TIMER_VALUE_DEFAULT);
+  Preferences::AddAtomicBoolVarCache(&sJitter,
+                                     RFP_JITTER_VALUE_PREF,
+                                     RFP_JITTER_VALUE_DEFAULT);
   Preferences::AddUintVarCache(&sVideoFramesPerSec,
                                RFP_SPOOFED_FRAMES_PER_SEC_PREF,
                                RFP_SPOOFED_FRAMES_PER_SEC_DEFAULT);
   Preferences::AddUintVarCache(&sVideoDroppedRatio,
                                RFP_SPOOFED_DROPPED_RATIO_PREF,
                                RFP_SPOOFED_DROPPED_RATIO_DEFAULT);
   Preferences::AddUintVarCache(&sTargetVideoRes,
                                RFP_TARGET_VIDEO_RES_PREF,
@@ -383,19 +665,19 @@ nsRFPService::Init()
 }
 
 // This function updates only timing-related fingerprinting items
 void
 nsRFPService::UpdateTimers() {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (sPrivacyResistFingerprinting || sPrivacyTimerPrecisionReduction) {
-    JS::SetTimeResolutionUsec(TimerResolution());
+    JS::SetTimeResolutionUsec(TimerResolution(), sJitter);
   } else if (sInitialized) {
-    JS::SetTimeResolutionUsec(0);
+    JS::SetTimeResolutionUsec(0, false);
   }
 }
 
 
 // This function updates every fingerprinting item necessary except timing-related
 void
 nsRFPService::UpdateRFPPref()
 {
@@ -451,18 +733,25 @@ nsRFPService::StartShutdown()
     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
 
     nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
 
     if (prefs) {
       prefs->RemoveObserver(RESIST_FINGERPRINTING_PREF, this);
       prefs->RemoveObserver(RFP_TIMER_PREF, this);
       prefs->RemoveObserver(RFP_TIMER_VALUE_PREF, this);
+      prefs->RemoveObserver(RFP_JITTER_VALUE_PREF, this);
     }
   }
+
+  sSecretMidpointSeed = nullptr;
+  sCache = nullptr;
+
+  delete sLock;
+  sLock = nullptr;
 }
 
 /* static */
 void
 nsRFPService::MaybeCreateSpoofingKeyCodes(const KeyboardLangs aLang,
                                           const KeyboardRegions aRegion)
 {
   if (!sSpoofingKeyboardCodes) {
@@ -687,17 +976,19 @@ nsRFPService::GetSpoofedKeyCode(const ns
 
 NS_IMETHODIMP
 nsRFPService::Observe(nsISupports* aObject, const char* aTopic,
                       const char16_t* aMessage)
 {
   if (!strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic)) {
     NS_ConvertUTF16toUTF8 pref(aMessage);
 
-    if (pref.EqualsLiteral(RFP_TIMER_PREF) || pref.EqualsLiteral(RFP_TIMER_VALUE_PREF)) {
+    if (pref.EqualsLiteral(RFP_TIMER_PREF) ||
+        pref.EqualsLiteral(RFP_TIMER_VALUE_PREF) ||
+        pref.EqualsLiteral(RFP_JITTER_VALUE_PREF)) {
       UpdateTimers();
     }
     else if (pref.EqualsLiteral(RESIST_FINGERPRINTING_PREF)) {
       UpdateRFPPref();
 
 #if defined(XP_WIN)
       if (!XRE_IsE10sParentProcess()) {
         // Windows does not follow POSIX. Updates to the TZ environment variable
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -3,16 +3,17 @@
  * 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 "mozilla/EventForwards.h"
+#include "mozilla/Mutex.h"
 #include "nsIDocument.h"
 #include "nsIObserver.h"
 
 #include "nsDataHashtable.h"
 #include "nsString.h"
 
 // Defines regarding spoofed values of Navigator object. These spoofed values
 // are returned when 'privacy.resistFingerprinting' is true.
@@ -42,16 +43,19 @@
 #define SPOOFED_APPVERSION "5.0 (X11)"
 #define SPOOFED_OSCPU      "Linux x86_64"
 #define SPOOFED_PLATFORM   "Linux x86_64"
 #endif
 
 #define SPOOFED_APPNAME    "Netscape"
 #define LEGACY_BUILD_ID    "20100101"
 
+// Forward declare LRUCache, defined in nsRFPService.cpp
+class LRUCache;
+
 namespace mozilla {
 
 enum KeyboardLang {
   EN = 0x01
 };
 
 #define RFP_KEYBOARD_LANG_STRING_EN "en"
 
@@ -170,23 +174,27 @@ public:
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
   static double ReduceTimePrecisionAsMSecs(
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
   static double ReduceTimePrecisionAsSecs(
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
+
   // Public only for testing purposes
   static double ReduceTimePrecisionImpl(
     double aTime,
     TimeScale aTimeScale,
     double aResolutionUSec,
     TimerPrecisionType aType);
-
+  static nsresult RandomMidpoint(long long aClampedTimeUSec,
+                                 long long aResolutionUSec,
+                                 long long* aMidpointOut,
+                                 uint8_t * aSecretSeed = nullptr);
 
   // This method calculates the video resolution (i.e. height x width) based
   // on the video quality (480p, 720p, etc).
   static uint32_t CalculateTargetVideoResolution(uint32_t aVideoQuality);
 
   // Methods for getting spoofed media statistics and the return value will
   // depend on the video resolution.
   static uint32_t GetSpoofedTotalFrames(double aTime);
@@ -256,14 +264,18 @@ private:
                                     const WidgetKeyboardEvent* aKeyboardEvent,
                                     SpoofingKeyboardCode& aOut);
 
   static Atomic<bool, Relaxed> sPrivacyResistFingerprinting;
   static Atomic<bool, Relaxed> sPrivacyTimerPrecisionReduction;
 
   static nsDataHashtable<KeyboardHashKey, const SpoofingKeyboardCode*>* sSpoofingKeyboardCodes;
 
+  static mozilla::Mutex* sLock;
+  static UniquePtr<LRUCache> sCache;
+  static UniquePtr<uint8_t[]> sSecretMidpointSeed;
+
   nsCString mInitialTZValue;
 };
 
 } // mozilla namespace
 
 #endif /* __nsRFPService_h__ */
--- a/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
+++ b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
@@ -2,16 +2,18 @@
  * 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 <math.h>
 
 #include "gtest/gtest.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
 #include "nsRFPService.h"
 
 using namespace mozilla;
 
 /*
    Hello! Are you looking at this file because you got an error you don't understand?
    Perhaps something that looks like the following?
 
@@ -37,101 +39,134 @@ using namespace mozilla;
 
    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.
 */
 
+bool setupJitter(bool enabled) {
+  nsCOMPtr<nsIPrefBranch> 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<nsIPrefBranch> 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, TimerPrecisionType::All);
   double reduced2 = nsRFPService::ReduceTimePrecisionImpl(reduced1, clockUnits, precision, TimerPrecisionType::All);
   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, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
   result = nsRFPService::ReduceTimePrecisionImpl(2611.145, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
   result = nsRFPService::ReduceTimePrecisionImpl(2611.141, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
   result = nsRFPService::ReduceTimePrecisionImpl(2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
   result = nsRFPService::ReduceTimePrecisionImpl(2611.15, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
   result = nsRFPService::ReduceTimePrecisionImpl(2611.13, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.12);
+  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, TimerPrecisionType::All);
   ASSERT_EQ(result, 9007199254740990.0);
   // 9007199254740995 is approximated to 9007199254740996
   result = nsRFPService::ReduceTimePrecisionImpl(9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, TimerPrecisionType::All);
   ASSERT_EQ(result, 9007199254740996);
   // 9007199254740999 is approximated as 9007199254741000
   result = nsRFPService::ReduceTimePrecisionImpl(9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, TimerPrecisionType::All);
   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, TimerPrecisionType::All);
   ASSERT_EQ(result, 9007199254743564.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);
@@ -166,9 +201,106 @@ TEST(ResistFingerprinting, ReducePrecisi
 
     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);
+
+  /*
+   * Here's our test vector. First we set the secret to the 16 byte value
+   * 0x000102030405060708 0x101112131415161718
+   *
+   * Then we work with a resolution of 500 us which will bucket things as such:
+   *  Per-Clamp Buckets: [0, 500], [500, 1000], ...
+   *  Per-Hash  Buckets: [0, 4000], [4000, 8000], ...
+   *
+   * The first two hash values should be
+   *    0:    SHA-256(0x000102030405060708 || 0x101112131415161718 || 0x0000000000000000)
+   *          32ca0459 bdb518be c72096dc 2667cd7a a76f94e4 c33fa679 9a1bd499 bfa4ec57
+   *    4000: SHA-256(0x000102030405060708 || 0x101112131415161718 || 0xa00f000000000000)
+   *          bd0bf282 120fd8c2 459c4d05 0170179c 25136f6f 70db5c82 5807558d 148c7745
+   *
+   * The midpoints are:
+   *   0   : 32ca0459 % 500 = 130
+   *   500 : bdb518be % 500 = 429
+   *   1500: c72096dc % 500 = 311
+   *   2000: 2667cd7a % 500 = 138
+   *   2500: a76f94e4 % 500 = 159
+   *   3000: c33fa679 % 500 = 435
+   *   3500: 9a1bd499 % 500 = 246
+   *   4000: bfa4ec57 % 500 = 463
+   *   4500: bd0bf282 % 500 = 297
+   *   5000: 120fd8c2 % 500 = 38
+   *   5500: 459c4d05 % 500 = 357
+   */
+
+  // 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, &throwAway, hardcodedSecret);
+
+  // Run the test vectors
+  double result;
+
+  result = nsRFPService::ReduceTimePrecisionImpl(1, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 0);
+  result = nsRFPService::ReduceTimePrecisionImpl(129, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 0);
+  result = nsRFPService::ReduceTimePrecisionImpl(130, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 500);
+  result = nsRFPService::ReduceTimePrecisionImpl(131, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 500);
+  result = nsRFPService::ReduceTimePrecisionImpl(499, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 500);
+
+  result = nsRFPService::ReduceTimePrecisionImpl(500, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 500);
+  result = nsRFPService::ReduceTimePrecisionImpl(600, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 500);
+  result = nsRFPService::ReduceTimePrecisionImpl(928, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 500);
+  result = nsRFPService::ReduceTimePrecisionImpl(929, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 1000);
+  result = nsRFPService::ReduceTimePrecisionImpl(930, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 1000);
+  result = nsRFPService::ReduceTimePrecisionImpl(1255, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 1000);
+
+  result = nsRFPService::ReduceTimePrecisionImpl(4000, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4000);
+  result = nsRFPService::ReduceTimePrecisionImpl(4295, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4000);
+  result = nsRFPService::ReduceTimePrecisionImpl(4296, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4000);
+  result = nsRFPService::ReduceTimePrecisionImpl(4297, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4500);
+  result = nsRFPService::ReduceTimePrecisionImpl(4298, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4500);
+  result = nsRFPService::ReduceTimePrecisionImpl(4499, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4500);
+
+  result = nsRFPService::ReduceTimePrecisionImpl(4500, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4500);
+  result = nsRFPService::ReduceTimePrecisionImpl(4536, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4500);
+  result = nsRFPService::ReduceTimePrecisionImpl(4537, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 4500);
+  result = nsRFPService::ReduceTimePrecisionImpl(4538, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 5000);
+  result = nsRFPService::ReduceTimePrecisionImpl(4539, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 5000);
+  result = nsRFPService::ReduceTimePrecisionImpl(5106, nsRFPService::TimeScale::MicroSeconds, 500, TimerPrecisionType::All);
+  ASSERT_EQ(result, 5000);
+
+  cleanupJitter(jitterEnabled);
+}