Bug 1441157 Switch from using a SHA-256 DRBG to a AES-CTR based one draft
authorTom Ritter <tom@mozilla.com>
Mon, 12 Mar 2018 16:30:57 -0500
changeset 767546 7d8da35d88259446742466bdf52ec16e6f3b9cf7
parent 767545 955f659d538c84bcce1c3ffee2e0831d20abee47
push id102629
push userbmo:tom@mozilla.com
push dateWed, 14 Mar 2018 19:27:31 +0000
bugs1441157
milestone61.0a1
Bug 1441157 Switch from using a SHA-256 DRBG to a AES-CTR based one MozReview-Commit-ID: 1HVN2KsfHaw
toolkit/components/resistfingerprinting/nsRFPService.cpp
old mode 100755
new mode 100644
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -31,16 +31,17 @@
 #include "nsIPrefService.h"
 #include "nsIRandomGenerator.h"
 #include "nsIXULAppInfo.h"
 #include "nsIXULRuntime.h"
 #include "nsJSUtils.h"
 
 #include "prenv.h"
 #include "nss.h"
+#include "ScopedNSSTypes.h"
 
 #include "js/Date.h"
 
 using namespace mozilla;
 using namespace std;
 
 static mozilla::LazyLogModule gResistFingerprintingLog("nsResistFingerprinting");
 
@@ -136,31 +137,31 @@ nsRFPService::IsTimerPrecisionReductionE
 }
 
 /*
  * 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 is thread-safe.
  */
 #define LRU_CACHE_SIZE         (45)
-#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)
 
 class LRUCache final
 {
 public:
   LRUCache()
     : mLock("mozilla.resistFingerprinting.LRUCache")
   {
     this->cache.SetLength(LRU_CACHE_SIZE);
   }
 
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LRUCache)
 
-  nsCString
+  shared_ptr<uint8_t>
   Get(long long aKeyPart1, long long aKeyPart2)
   {
     for (auto & cacheEntry : this->cache) {
       // Read optimistically befor locking
       if (cacheEntry.keyPart1 == aKeyPart1 &&
           cacheEntry.keyPart2 == aKeyPart2) {
         MutexAutoLock lock(mLock);
 
@@ -168,67 +169,69 @@ public:
         if (MOZ_UNLIKELY(cacheEntry.keyPart1 != aKeyPart1 ||
                          cacheEntry.keyPart2 != aKeyPart2)) {
           // Got evicted in a race
           long long tmp_keyPart1 = cacheEntry.keyPart1;
           long long tmp_keyPart2 = cacheEntry.keyPart2;
           MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
             ("LRU Cache HIT-MISS with %lli != %lli and %lli != %lli",
               aKeyPart1, tmp_keyPart1, aKeyPart2, tmp_keyPart2));
-          return EmptyCString();
+          return nullptr;
         }
 
         cacheEntry.accessTime = PR_Now();
         MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
           ("LRU Cache HIT with %lli %lli", aKeyPart1, aKeyPart2));
         return cacheEntry.data;
       }
     }
 
-    return EmptyCString();
+    return nullptr;
   }
 
-  void
-  Store(long long aKeyPart1, long long aKeyPart2, const nsCString& aValue)
+  shared_ptr<uint8_t>
+  Store(long long aKeyPart1, long long aKeyPart2, uint8_t* aValue)
   {
-    MOZ_DIAGNOSTIC_ASSERT(aValue.Length() == HASH_DIGEST_SIZE_BYTES);
     MutexAutoLock lock(mLock);
 
     CacheEntry* lowestKey = &this->cache[0];
     for (auto & cacheEntry : this->cache) {
       if (MOZ_UNLIKELY(cacheEntry.keyPart1 == aKeyPart1 &&
                        cacheEntry.keyPart2 == aKeyPart2)) {
         // Another thread inserted before us, don't insert twice
         MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
           ("LRU Cache DOUBLE STORE with %lli %lli", aKeyPart1, aKeyPart2));
-        return;
+        return cacheEntry.data;
       }
       if (cacheEntry.accessTime < lowestKey->accessTime) {
         lowestKey = &cacheEntry;
       }
     }
 
     lowestKey->keyPart1 = aKeyPart1;
     lowestKey->keyPart2 = aKeyPart2;
-    lowestKey->data = aValue;
+    // REVIEWER: I found this idiom online; I think it is okay but not 100% sure
+    lowestKey->data = std::shared_ptr<uint8_t>(new uint8_t[AES_BLOCK_SIZE_BYTES], std::default_delete<uint8_t[]>());
+    memcpy(lowestKey->data.get(), aValue, AES_BLOCK_SIZE_BYTES);
     lowestKey->accessTime = PR_Now();
     MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
       ("LRU Cache STORE with %lli %lli", aKeyPart1, aKeyPart2));
+    return lowestKey->data;
   }
 
 
 private:
   ~LRUCache() = default;
 
   struct CacheEntry
   {
     Atomic<long long, Relaxed> keyPart1;
     Atomic<long long, Relaxed> keyPart2;
     PRTime accessTime = 0;
-    nsCString data;
+    shared_ptr<uint8_t> data;
 
     CacheEntry()
     {
       this->keyPart1 = 0xFFFFFFFFFFFFFFFF;
       this->keyPart2 = 0xFFFFFFFFFFFFFFFF;
       this->accessTime = 0;
       this->data = nullptr;
     }
@@ -316,34 +319,34 @@ nsresult
 nsRFPService::RandomMidpoint(long long aClampedTimeUSec,
                              long long aResolutionUSec,
                              int64_t aContextMixin,
                              long long* aMidpointOut,
                              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 * sSecretMidpointSeed = nullptr;
 
   if(MOZ_UNLIKELY(!aMidpointOut)) {
     return NS_ERROR_INVALID_ARG;
   }
 
   RefPtr<LRUCache> cache;
   {
     StaticMutexAutoLock lock(sLock);
     cache = sCache;
   }
 
   if(!cache) {
     return NS_ERROR_FAILURE;
   }
 
-  /*
+  /* TODOFIXME
    * 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.
@@ -356,19 +359,19 @@ nsRFPService::RandomMidpoint(long long a
    * 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 = cache->Get(extraClampedTime, aContextMixin);
+  shared_ptr<uint8_t> deterministiclyRandomBytes = cache->Get(extraClampedTime, aContextMixin);
 
-  if(hashResult.Length() != HASH_DIGEST_SIZE_BYTES) { // Cache Miss =(
+  if(deterministiclyRandomBytes == nullptr) { // Cache Miss =(
     // If someone has pased in the testing-only parameter, replace our seed with it
     if (aSecretSeed != nullptr) {
       StaticMutexAutoLock lock(sLock);
       if (sSecretMidpointSeed) {
         delete[] sSecretMidpointSeed;
       }
       sSecretMidpointSeed = new uint8_t[kSeedSize];
       memcpy(sSecretMidpointSeed, aSecretSeed, kSeedSize);
@@ -382,72 +385,72 @@ nsRFPService::RandomMidpoint(long long a
             do_GetService("@mozilla.org/security/random-generator;1", &rv);
         if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
         rv = randomGenerator->GenerateRandomBytes(kSeedSize, &sSecretMidpointSeed);
         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.
-     */
+    // We'd like to reuse as much of this as possible, but that doesn't seem possible with
+    // the PK11 interface.
+    UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+    MOZ_ASSERT(slot);
+
+    UniqueSECItem secItem(::SECITEM_AllocItem(nullptr, nullptr, AES_BLOCK_SIZE_BYTES));
+    MOZ_ASSERT(secItem);
+    memcpy(secItem->data, sSecretMidpointSeed, AES_BLOCK_SIZE_BYTES);
+
+    UniquePK11SymKey symKey(PK11_ImportSymKey(slot.get(), CKM_AES_CTR,
+                                              PK11_OriginUnwrap, CKA_ENCRYPT,
+                                              secItem.get(), nullptr));
+    MOZ_ASSERT(symKey);
 
-     // 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, kSeedSize);
-     NS_ENSURE_SUCCESS(rv, rv);
+    // Set up CTR Parameters
+    SECItem param = { siBuffer, nullptr, 0 };
+    CK_AES_CTR_PARAMS ctrParams;
+    ctrParams.ulCounterBits = AES_BLOCK_SIZE_BITS;
+    // 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 and the ontext mix-in
+    memset(&ctrParams.cb, 0x00, AES_BLOCK_SIZE_BYTES);
+    memcpy(&ctrParams.cb,                           &extraClampedTime, sizeof(extraClampedTime));
+    memcpy(&ctrParams.cb[sizeof(extraClampedTime)], &aContextMixin,    sizeof(aContextMixin));
+    param.type = siBuffer;
+    param.data = (unsigned char*) &ctrParams;
+    param.len  = sizeof(ctrParams);
 
-     rv = hasher->Update((const uint8_t *)&aContextMixin, sizeof(aContextMixin));
-     NS_ENSURE_SUCCESS(rv, rv);
+    // 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 };
 
-     rv = hasher->Update((const uint8_t *)&extraClampedTime, sizeof(extraClampedTime));
-     NS_ENSURE_SUCCESS(rv, rv);
+    // Where to store the result
+    uint8_t ciphertext[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
 
-     nsAutoCStringN<HASH_DIGEST_SIZE_BYTES> derivedSecret;
-     rv = hasher->Finish(false, derivedSecret);
-     NS_ENSURE_SUCCESS(rv, rv);
+    // Perform the encryption/decryption
+    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);
+    MOZ_ASSERT(outLen == sizeof(ciphertext));
 
-     // Finally, store it in the cache
-     cache->Store(extraClampedTime, aContextMixin, derivedSecret);
-     hashResult = derivedSecret;
+    // Finally, store it in the cache
+    deterministiclyRandomBytes = cache->Store(extraClampedTime, aContextMixin, ciphertext);
   }
 
   // Offset the appropriate index into the hash output, and then turn it into a random midpoint
   // between 0 and aResolutionUSec. Sometimes out input time is negative, we ride the negative
   // out to the end until we start doing pointer math. (We also triple check we're in bounds.)
   int byteOffset = abs(((aClampedTimeUSec - extraClampedTime) / aResolutionUSec) * 4);
-  if (MOZ_UNLIKELY(byteOffset > (HASH_DIGEST_SIZE_BYTES - 4))) {
+  if (MOZ_UNLIKELY(byteOffset > (AES_BLOCK_SIZE_BYTES - 4))) {
     byteOffset = 0;
   }
-  uint32_t deterministiclyRandomValue = *BitwiseCast<uint32_t*>(PromiseFlatCString(hashResult).get() + byteOffset);
+  uint32_t deterministiclyRandomValue = *BitwiseCast<uint32_t*>(deterministiclyRandomBytes.get() + byteOffset);
   deterministiclyRandomValue %= aResolutionUSec;
   *aMidpointOut = deterministiclyRandomValue;
 
   return NS_OK;
 }
 
 
 /**