author | Tom Ritter <tom@mozilla.com> |
Thu, 01 Mar 2018 00:07:03 -0600 | |
changeset 406446 | 3052a1cf3b1c20b4d9bcc6c03bb1e8d45fddca0b |
parent 406445 | c2307a921159af7238d7fa9f4ccc9593928fec1f |
child 406447 | dd7b7e91140a3c42f395b6a0a58e5db90e28db1b |
push id | 33558 |
push user | rgurzau@mozilla.com |
push date | Sat, 03 Mar 2018 21:46:37 +0000 |
treeherder | mozilla-central@8cced2a46f73 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | bkelly |
bugs | 1425462 |
milestone | 60.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
|
--- 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); +}