Bug 1425462 Convert from a hash function to a AES-CTR based DRBG draft
authorTom Ritter <tom@mozilla.com>
Wed, 21 Feb 2018 13:56:54 -0600
changeset 759189 80dcb7b182b3334ddd187d600c6a7fdd80d9d8c2
parent 758824 97fc7819957a1831a29ce99d7da64173899b1f33
child 759190 129d69f4f4e952803dcccd7ee006c869a4942d9b
push id100301
push userbmo:tom@mozilla.com
push dateFri, 23 Feb 2018 21:10:18 +0000
bugs1425462
milestone60.0a1
Bug 1425462 Convert from a hash function to a AES-CTR based DRBG MozReview-Commit-ID: KnASHoM1nqk
toolkit/components/resistfingerprinting/moz.build
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
--- a/toolkit/components/resistfingerprinting/moz.build
+++ b/toolkit/components/resistfingerprinting/moz.build
@@ -14,8 +14,12 @@ FINAL_LIBRARY = 'xul'
 
 EXPORTS += [
     'nsRFPService.h',
 ]
 
 EXTRA_JS_MODULES += [
     'LanguagePrompt.jsm',
 ]
+
+LOCAL_INCLUDES += [
+    '/security/pkix/include',
+]
\ No newline at end of file
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -19,17 +19,19 @@
 
 #include "nsCOMPtr.h"
 #include "nsCoord.h"
 #include "nsServiceManagerUtils.h"
 #include "nsString.h"
 #include "nsXULAppAPI.h"
 #include "nsPrintfCString.h"
 
-#include "nsICryptoHash.h"
+#include "ScopedNSSTypes.h"
+#include "nsNSSComponent.h"
+
 #include "nsIObserverService.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
 #include "nsIRandomGenerator.h"
 #include "nsIXULAppInfo.h"
 #include "nsIXULRuntime.h"
 #include "nsJSUtils.h"
 
@@ -129,75 +131,78 @@ nsRFPService::IsTimerPrecisionReductionE
   if (aType == TimerPrecisionType::RFPOnly) {
     return IsResistFingerprintingEnabled();
   }
 
   return (sPrivacyTimerPrecisionReduction || IsResistFingerprintingEnabled()) &&
          TimerResolution() > 0;
 }
 
-#define HASH_DIGEST_SIZE_BITS  (256)
-#define HASH_DIGEST_SIZE_BYTES (HASH_DIGEST_SIZE_BITS / 8)
+#define AES_BLOCK_SIZE_BITS  (128)
+#define AES_BLOCK_SIZE_BYTES (AES_BLOCK_SIZE_BITS / 8)
 
 // TODO: Fix Race Conditions
 class LRUCache
 {
 public:
   LRUCache() {
-    for (int i=0; i<kCacheSize; i++) {
-      this->cache[i].accessTime = 0;
-      this->cache[i].key = 0xFFFFFFFFFFFFFFFF;
-      this->cache[i].data = nullptr;
+    for (auto & cacheEntry : this->cache) {
+      cacheEntry.accessTime = 0;
+      cacheEntry.key = 0xFFFFFFFFFFFFFFFF;
+      cacheEntry.data = nullptr;
     }
   }
 
-  shared_ptr<nsAutoCStringN<HASH_DIGEST_SIZE_BYTES>> get(long long aKey) {
-    for (int i=0; i<kCacheSize; i++) {
-      if (this->cache[i].key == aKey) {
-        this->cache[i].accessTime = PR_Now();
+  shared_ptr<uint8_t> 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 index %i",
-            aKey, this->cache[i].key, i));
+          ("LRU Cache HIT with %lli == %lli",
+            aKey, cacheEntry.key));
 #endif
-        return this->cache[i].data;
+        return cacheEntry.data;
       }
     }
     return nullptr;
   }
-  void store(long long aKey, nsAutoCStringN<HASH_DIGEST_SIZE_BYTES> aValue) {
+  shared_ptr<uint8_t> store(long long aKey, uint8_t* data) {
     PRTime lowestTime;
     memset(&lowestTime, 0xFF, sizeof(PRTime));
     lowestTime &= ~(1ULL << ((sizeof(PRTime) * 8) - 1));
 
     int lowestKey = 0;
     for (int i=0; i<kCacheSize; i++) {
       if (this->cache[i].accessTime < lowestTime) {
         lowestTime = this->cache[i].accessTime;
         lowestKey = i;
       }
     }
 
     this->cache[lowestKey].key = aKey;
-    this->cache[lowestKey].data = make_shared<nsAutoCStringN<HASH_DIGEST_SIZE_BYTES>>(aValue);
+    this->cache[lowestKey].data = std::shared_ptr<uint8_t>(new uint8_t[AES_BLOCK_SIZE_BYTES], std::default_delete<uint8_t[]>());
+    memcpy(this->cache[lowestKey].data.get(), data, AES_BLOCK_SIZE_BYTES);
     this->cache[lowestKey].accessTime = PR_Now();
 #if defined(DEBUG)
     MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
       ("LRU Cache STORE with %lli index %i",
       aKey, lowestKey));
 #endif
+
+    return this->cache[lowestKey].data;
   }
 
 
 private:
   struct cache_entry {
     long long key;
     PRTime accessTime = 0;
-    shared_ptr<nsAutoCStringN<HASH_DIGEST_SIZE_BYTES>> data;
+    shared_ptr<uint8_t> data;
   };
 
   static const int kCacheSize = 45;
   cache_entry cache[kCacheSize];
 };
 
 /*
  * The purpose of this function is to deterministicly generate a random midpoint
@@ -258,125 +263,175 @@ nsresult
 nsRFPService::RandomMidpoint(long long aClampedTime,
                              long long aResolutionUSec,
                              long long* aMidpoint,
                              void* aContextPointer,
                              uint8_t * aSecretSeed /* = nullptr */)
 {
   nsresult rv;
   const int kSeedSize = 16;
-  const int kClampTimesPerDigest = HASH_DIGEST_SIZE_BITS / 32;
+  const int kClampTimesPerDigest = AES_BLOCK_SIZE_BITS / 32;
   static uint8_t * secretSeed = nullptr;
+  static bool doubleCheckNSS = false;
+  static UniquePK11SlotInfo slot;
+  static UniquePLArenaPool arena;
+  static UniquePK11SymKey symKey;
+
 
   if (!sCache) {
     sCache = new LRUCache();
   }
 
   /*
-   * Below, we will call a cryptographic hash function. That's expensive. We look for ways to
-   * make it more efficient.
+   * Below, we will use AES in CTR Mode as a DRBG.
+   *
+   * This is a non-stand use of crypto, so it seems dangerous. The primary security guarentee
+   * we want to make is that if an attacker observes prior outputs, they cannot predict future
+   * ones. (Technically the attacker cannot directly observe outputs, they would need to
+   * calculate the output from a jittered clock, but we'll assume they can do this even
+   * though it seems quit difficult.)
+   *
+   * CTR works by XORing the 'Intermediate Value' (result of encrypting a counter) with the
+   * Plaintext. We need to assign the AES Key, the IV, and the Plaintext from amoung our
+   * variables of a random seed, the context pointer (currently 0!), and the clamped time.
+   *
+   * Clearly the AES Key needs to be completely random; we make it the random seed.
    *
-   * 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
+   * For now, in this iteration before we have a real context pointer, we choose to make the
+   * IV the Clamped Time, and encrypt an all-zero plaintext. After observing an output, an
+   * attacker will learn the plaintext and resulting intermediate value (and ciphertext), but
+   * will be unable to predict future values (which would amount to a Known-plaintext attack
+   * on AES.) In the future, when we have a context pointer and know what its value will be
+   * (i.e. a random vaue or a pointer or something else) we will revisit.
+   *
+   * Besides the security concerns, it's expensive. We look for ways to make it more efficient.
+   *
+   * The first is to understand how we can share the results of one encryption among other
+   * calls. This comes to a choice of what values we make the Key, IV, and Plaintext - but
+   * we don't want to skimp on the security there. Right now, encrypting the clamped time
+   * with no context pointer means we can reuse the value across different contexts.
+   *
+   * We only need as much output from the DRBG as the maximum time precision we will
+   * ever support, because we will reduce the output modulo that value. The maximum precision
    * 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.
+   * So we're using 32 bits of output and throwing away the other BLOCK_SIZE - 32 (in the case of
+   * AES-CTR, BLOCK_SIZE is 128.)  That's 96 bits 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
+   * Instead of throwing it away, we're going to use all of it. We can handle BLOCK_SIZE / 32
+   * Clamped Time's per DRBG function-call. 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.
+   * Then we use _that_ value as input to the DRBG (assuming it's not in the cache) and
+   * index into the output result the appropriate bit offset.
    */
   long long reducedResolution = aResolutionUSec * kClampTimesPerDigest;
   long long extraClampedTime = (aClampedTime / reducedResolution) * reducedResolution;
 
-  // While this code compiled and runs I am really unsure I am doing the right thing
-  // with shared_ptr and string types.
-  nsAutoCStringN<HASH_DIGEST_SIZE_BYTES> fred;
-  shared_ptr<nsAutoCStringN<HASH_DIGEST_SIZE_BYTES>> bob = sCache->get(extraClampedTime);
+  shared_ptr<uint8_t> deterministiclyRandomBytes = sCache->get(extraClampedTime);
 
-  if(!bob) { // Cache Miss =(
+  if(deterministiclyRandomBytes == nullptr) { // Cache Miss =(
     // If someone has pased in the testing-only parameter, replace our seed with it
     if (aSecretSeed != nullptr) {
       if (secretSeed) {
+        // If we had an existing seed, we need to wipe it and the AES key derived from it
         free(secretSeed);
+        PK11_FreeSymKey(symKey.release());
       }
       secretSeed = new uint8_t[kSeedSize];
       memcpy(secretSeed, aSecretSeed, kSeedSize);
     }
 
     // If we don't have a seed, we need to get one
     if(!secretSeed) {
       nsCOMPtr<nsIRandomGenerator> randomGenerator =
           do_GetService("@mozilla.org/security/random-generator;1", &rv);
       if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
       rv = randomGenerator->GenerateRandomBytes(kSeedSize, &secretSeed);
       if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
     }
 
-    /*
-     * 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.
-     */
+    // It's possible that NSS hasn't been initialized, observed when running gtests
+    // This will double check that it has been initialized. It will assert if not done
+    // in the parent process, which doesn't make any sense to me.
+    // REVIEWER: This is certainly wrong, but I'm not sure what it is I'm supposed to do.
+    // REVIEWER: gtest complains:
+    //    WARNING: NSS_Shutdown failed - some NSS resources are still in use (see bugs 1417680 and 1230312): file xpcom/build/XPCOMInit.cpp, line 1019
+    //    Assertion failed: (GLOBAL(int, mutexIsInit)), function sqlite3MutexAlloc, file db/sqlite3/src/sqlite3.c, line 23769.
+    if(!doubleCheckNSS && XRE_IsParentProcess()) {
+      nsCOMPtr<nsINSSComponent> component(do_GetService(PSM_COMPONENT_CONTRACTID));
+      MOZ_ASSERT(component);
+      doubleCheckNSS = true;
+    }
 
-     // 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(secretSeed, kSeedSize);
-     NS_ENSURE_SUCCESS(rv, rv);
+    // TODO: Make this threadsafe
+    // If we don't have an AES key set up, set that up
+    if (!slot) {
+      slot = UniquePK11SlotInfo(PK11_GetInternalSlot());
+      MOZ_ASSERT(slot.get());
+    }
+    if (!arena) {
+      arena = UniquePLArenaPool(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
+      MOZ_ASSERT(arena);
+    }
+    if (!symKey || !symKey.get()) {
+      SECItem keyItem = { siBuffer, nullptr, 0 };
+      if (!::SECITEM_AllocItem(arena.get(), &keyItem, (128 / 8))) {
+        return NS_ERROR_FAILURE;
+      }
+      memcpy(keyItem.data, secretSeed, (128 / 8));
+      
+      symKey = UniquePK11SymKey(PK11_ImportSymKey(slot.get(), CKM_AES_CTR,
+                                                PK11_OriginUnwrap, CKA_ENCRYPT,
+                                                &keyItem, nullptr));
+    }
+    MOZ_ASSERT(symKey.get());
 
-     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);
+    // Set up CTR Parameters
+    SECItem param = { siBuffer, nullptr, 0 };
+    CK_AES_CTR_PARAMS ctrParams;
+    // REVIEWER: This is the length of the entire IV right? Encompassing nonce || counter
+    ctrParams.ulCounterBits = 128;
+    // This is the value of the entire IV, normally encompassing 64 bits of nonce and 64
+    // bits of counter. We set the IV to be the clamped time
+    memset(&ctrParams.cb, 0x00, 16);
+    memcpy(&ctrParams.cb, &aClampedTime, sizeof(aClampedTime));
+    param.data = (unsigned char*) &ctrParams;
+    param.len  = sizeof(ctrParams);
 
-     // Finally, store it in the cache
-     sCache->store(extraClampedTime, derivedSecret);
-     fred = derivedSecret;
-  }
-  else { // Cache Hit!
-    fred = *bob;
+    // Plaintext
+    // We set the plaintext to be all zeros
+    uint8_t plaintext[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+    // Where to store the result
+    uint8_t ciphertext[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+    // Perform the encryption/decryption
+    // REVIEWER: Is this thread-safe? Can we call this multiple times at the same time with the same key object?
+    uint32_t outLen = 0;
+    rv = MapSECStatus(PK11_Encrypt(symKey.get(), CKM_AES_CTR, &param,
+                                   ciphertext, &outLen, sizeof(ciphertext), 
+                                   plaintext, sizeof(plaintext)));
+
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    deterministiclyRandomBytes = sCache->store(extraClampedTime, ciphertext);
   }
 
-  // Offset the appropriate index into the hash output, and then turn it into a random midpoint
+  // Offset the appropriate index into the DRBG output, and then turn it into a random midpoint
   // between 0 and aResolutionUSec
   int byteOffset = ((aClampedTime - extraClampedTime) / aResolutionUSec) * 4;
-  uint32_t deterministiclyRandomValue = *BitwiseCast<uint32_t*>(PromiseFlatCString(fred).get() + byteOffset);
-  deterministiclyRandomValue %= aResolutionUSec;
-  *aMidpoint = deterministiclyRandomValue;
+  uint32_t deterministiclyRandomUint = *BitwiseCast<uint32_t*>(deterministiclyRandomBytes.get() + byteOffset);
+  deterministiclyRandomUint %= aResolutionUSec;
+  *aMidpoint = deterministiclyRandomUint;
 
   return NS_OK;
 }
 
 
 /**
  * Given a precision value, this function will reduce a given input time to the nearest
  * multiple of that precision.
@@ -445,17 +500,17 @@ nsRFPService::ReduceTimePrecisionImpl(
   double ret = double(clampedAndJittered) / (1000000.0 / aTimeScale);
 
 #if defined(DEBUG)
   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,
-     floor(double(timeAsInt) / resolutionAsInt), clamped, tmp_jitter, midpoint, clampedAndJittered, DBL_DIG-1, ret));
+     (long long)floor(double(timeAsInt) / resolutionAsInt), clamped, tmp_jitter, midpoint, clampedAndJittered, DBL_DIG-1, ret));
 #endif
 
   return ret;
 }
 
 
 
 /* static */
--- a/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
+++ b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
@@ -214,93 +214,90 @@ TEST(ResistFingerprinting, ReducePrecisi
   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], ...
+   *  Per-Hash  Buckets: [2000, 2500], [2500, 3000], ...
    *
-   * 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 first two AES-CTR cihertext values should be
+   *    0:    AES-CTR k=0x00010203040506071011121314151617 iv=0x00000000000000000000000000000000 E(0x00000000000000000000000000000000)
+   *          e7e9a79f faadd53e fea23818 e9dfd7e8
+   *    2000: AES-CTR k=0x00010203040506071011121314151617 iv=0xD0070000000000000000000000000000 E(0x00000000000000000000000000000000)
+   *          af524854 dd3829dd ab57fc33 97aa46b9
    *
-   * 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
+   * The midpoints are: (one much switch endianness of the hexadecimal values)
+   *   0   : e7e9a79f % 500 = 235
+   *   500 : faadd53e % 500 = 98
+   *   1500: fea23818 % 500 = 426
+   *   2000: e9dfd7e8 % 500 = 173
+   *   2500: af524854 % 500 = 403
+   *   3000: dd3829dd % 500 = 269
+   *   3500: ab57fc33 % 500 = 31
+   *   4000: 97aa46b9 % 500 = 151
    */
 
   // 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, nullptr, hardcodedSecret);
 
   // Run the test vectors
   double result;
 
   result = nsRFPService::ReduceTimePrecisionImpl(1, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 0);
-  result = nsRFPService::ReduceTimePrecisionImpl(129, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(234, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 0);
-  result = nsRFPService::ReduceTimePrecisionImpl(130, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(235, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 500);
-  result = nsRFPService::ReduceTimePrecisionImpl(131, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(236, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 500);
   result = nsRFPService::ReduceTimePrecisionImpl(499, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 500);
 
   result = nsRFPService::ReduceTimePrecisionImpl(500, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 500);
-  result = nsRFPService::ReduceTimePrecisionImpl(600, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(550, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 500);
-  result = nsRFPService::ReduceTimePrecisionImpl(928, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(597, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 500);
-  result = nsRFPService::ReduceTimePrecisionImpl(929, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(598, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 1000);
-  result = nsRFPService::ReduceTimePrecisionImpl(930, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(599, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 1000);
-  result = nsRFPService::ReduceTimePrecisionImpl(1255, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(1425, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
   ASSERT_EQ(result, 1000);
 
-  result = nsRFPService::ReduceTimePrecisionImpl(4000, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4000);
-  result = nsRFPService::ReduceTimePrecisionImpl(4295, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4000);
-  result = nsRFPService::ReduceTimePrecisionImpl(4296, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4000);
-  result = nsRFPService::ReduceTimePrecisionImpl(4297, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4500);
-  result = nsRFPService::ReduceTimePrecisionImpl(4298, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4500);
-  result = nsRFPService::ReduceTimePrecisionImpl(4499, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4500);
+  result = nsRFPService::ReduceTimePrecisionImpl(2000, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2000);
+  result = nsRFPService::ReduceTimePrecisionImpl(2402, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2000);
+  result = nsRFPService::ReduceTimePrecisionImpl(2402.9, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2000);
+  result = nsRFPService::ReduceTimePrecisionImpl(2403, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2500);
+  result = nsRFPService::ReduceTimePrecisionImpl(2404, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2500);
+  result = nsRFPService::ReduceTimePrecisionImpl(2499, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2500);
 
-  result = nsRFPService::ReduceTimePrecisionImpl(4500, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4500);
-  result = nsRFPService::ReduceTimePrecisionImpl(4536, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4500);
-  result = nsRFPService::ReduceTimePrecisionImpl(4537, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 4500);
-  result = nsRFPService::ReduceTimePrecisionImpl(4538, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 5000);
-  result = nsRFPService::ReduceTimePrecisionImpl(4539, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 5000);
-  result = nsRFPService::ReduceTimePrecisionImpl(5106, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
-  ASSERT_EQ(result, 5000);
+  result = nsRFPService::ReduceTimePrecisionImpl(2500, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2500);
+  result = nsRFPService::ReduceTimePrecisionImpl(2768.7, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2500);
+  result = nsRFPService::ReduceTimePrecisionImpl(2768.9, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2500);
+  result = nsRFPService::ReduceTimePrecisionImpl(2769, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 3000);
+  result = nsRFPService::ReduceTimePrecisionImpl(2769.1, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 3000);
+  result = nsRFPService::ReduceTimePrecisionImpl(3030, nsRFPService::TimeScale::MicroSeconds, 500, nullptr, TimerPrecisionType::All);
+  ASSERT_EQ(result, 3000);
 
   cleanupJitter(jitterEnabled);
 }