diff options
Diffstat (limited to 'toolkit/components/resistfingerprinting/nsRFPService.cpp')
-rw-r--r-- | toolkit/components/resistfingerprinting/nsRFPService.cpp | 2137 |
1 files changed, 2137 insertions, 0 deletions
diff --git a/toolkit/components/resistfingerprinting/nsRFPService.cpp b/toolkit/components/resistfingerprinting/nsRFPService.cpp new file mode 100644 index 0000000000..8579fe2a3b --- /dev/null +++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp @@ -0,0 +1,2137 @@ +/* -*- 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 <cfloat> +#include <cinttypes> +#include <cmath> +#include <cstdlib> +#include <cstring> +#include <ctime> +#include <new> +#include <type_traits> +#include <utility> + +#include "MainThreadUtils.h" +#include "ScopedNSSTypes.h" + +#include "mozilla/ArrayIterator.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Casting.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/HelperMacros.h" +#include "mozilla/Likely.h" +#include "mozilla/Logging.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Services.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_javascript.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanvasRenderingContextHelper.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/fallible.h" +#include "mozilla/XorShift128PlusRNG.h" + +#include "nsAboutProtocolUtils.h" +#include "nsBaseHashtable.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsCoord.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsEffectiveTLDService.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsJSUtils.h" +#include "nsLiteralString.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsTArray.h" +#include "nsTLiteralString.h" +#include "nsTPromiseFlatString.h" +#include "nsTStringRepr.h" +#include "nsXPCOM.h" + +#include "nsICookieJarSettings.h" +#include "nsICryptoHash.h" +#include "nsIGlobalObject.h" +#include "nsILoadInfo.h" +#include "nsIObserverService.h" +#include "nsIRandomGenerator.h" +#include "nsIScriptSecurityManager.h" +#include "nsIUserIdleService.h" +#include "nsIWebProgressListener.h" +#include "nsIXULAppInfo.h" + +#include "nscore.h" +#include "prenv.h" +#include "prtime.h" +#include "xpcpublic.h" + +#include "js/Date.h" + +using namespace mozilla; + +static mozilla::LazyLogModule gResistFingerprintingLog( + "nsResistFingerprinting"); + +static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection"); + +#define RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF \ + "privacy.fingerprintingProtection.overrides" +#define RFP_TIMER_UNCONDITIONAL_VALUE 20 +#define LAST_PB_SESSION_EXITED_TOPIC "last-pb-context-exited" + +static constexpr uint32_t kVideoFramesPerSec = 30; +static constexpr uint32_t kVideoDroppedRatio = 5; + +#define RFP_DEFAULT_SPOOFING_KEYBOARD_LANG KeyboardLang::EN +#define RFP_DEFAULT_SPOOFING_KEYBOARD_REGION KeyboardRegion::US + +#define FP_OVERRIDES_DOMAIN_KEY_DELIMITER ',' + +// Fingerprinting protections that are enabled by default. This can be +// overridden using the privacy.fingerprintingProtection.overrides pref. +#if defined(MOZ_WIDGET_ANDROID) +const RFPTarget kDefaultFingerprintingProtections = + RFPTarget::CanvasRandomization; +#else +const RFPTarget kDefaultFingerprintingProtections = + RFPTarget::CanvasRandomization | RFPTarget::FontVisibilityLangPack; +#endif + +static constexpr uint32_t kSuspiciousFingerprintingActivityThreshold = 1; + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Structural Stuff & Pref Observing + +NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver, nsIRFPService) + +static StaticRefPtr<nsRFPService> sRFPService; +static bool sInitialized = false; + +// Actually enabled fingerprinting protections. +static Atomic<RFPTarget> sEnabledFingerprintingProtections; + +/* static */ +already_AddRefed<nsRFPService> nsRFPService::GetOrCreate() { + if (!sInitialized) { + sRFPService = new nsRFPService(); + nsresult rv = sRFPService->Init(); + + if (NS_FAILED(rv)) { + sRFPService = nullptr; + return nullptr; + } + + ClearOnShutdown(&sRFPService); + sInitialized = true; + } + + return do_AddRef(sRFPService); +} + +static const char* gCallbackPrefs[] = { + RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF, + nullptr, +}; + +nsresult nsRFPService::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + 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 (XRE_IsParentProcess()) { + rv = obs->AddObserver(this, LAST_PB_SESSION_EXITED_TOPIC, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obs->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + Preferences::RegisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs, + this); + + JS::SetReduceMicrosecondTimePrecisionCallback( + nsRFPService::ReduceTimePrecisionAsUSecsWrapper); + + // Called from here to get the initial list of enabled fingerprinting + // protections. + UpdateFPPOverrideList(); + + return rv; +} + +/* static */ +bool nsRFPService::IsRFPPrefEnabled(bool aIsPrivateMode) { + if (StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() || + (aIsPrivateMode && + StaticPrefs::privacy_resistFingerprinting_pbmode_DoNotUseDirectly())) { + return true; + } + return false; +} + +/* static */ +bool nsRFPService::IsRFPEnabledFor( + bool aIsPrivateMode, RFPTarget aTarget, + const Maybe<RFPTarget>& aOverriddenFingerprintingSettings) { + MOZ_ASSERT(aTarget != RFPTarget::AllTargets); + + if (StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() || + (aIsPrivateMode && + StaticPrefs::privacy_resistFingerprinting_pbmode_DoNotUseDirectly())) { + if (aTarget == RFPTarget::JSLocale) { + return StaticPrefs::privacy_spoof_english() == 2; + } + return true; + } + + if (StaticPrefs::privacy_fingerprintingProtection_DoNotUseDirectly() || + (aIsPrivateMode && + StaticPrefs:: + privacy_fingerprintingProtection_pbmode_DoNotUseDirectly())) { + if (aTarget == RFPTarget::IsAlwaysEnabledForPrecompute) { + return true; + } + + if (aOverriddenFingerprintingSettings) { + return bool(aOverriddenFingerprintingSettings.ref() & aTarget); + } + + return bool(sEnabledFingerprintingProtections & aTarget); + } + + return false; +} + +void nsRFPService::UpdateFPPOverrideList() { + nsAutoString targetOverrides; + nsresult rv = Preferences::GetString( + RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF, targetOverrides); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Could not get fingerprinting override pref value")); + return; + } + + RFPTarget enabled = CreateOverridesFromText( + targetOverrides, kDefaultFingerprintingProtections); + + sEnabledFingerprintingProtections = enabled; +} + +/* static */ +Maybe<RFPTarget> nsRFPService::TextToRFPTarget(const nsAString& aText) { +#define ITEM_VALUE(name, value) \ + if (aText.EqualsLiteral(#name)) { \ + return Some(RFPTarget::name); \ + } + +#include "RFPTargets.inc" +#undef ITEM_VALUE + + return Nothing(); +} + +void nsRFPService::StartShutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + if (XRE_IsParentProcess()) { + obs->RemoveObserver(this, LAST_PB_SESSION_EXITED_TOPIC); + obs->RemoveObserver(this, OBSERVER_TOPIC_IDLE_DAILY); + } + } + + if (mWebCompatService) { + mWebCompatService->Shutdown(); + } + + Preferences::UnregisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs, + this); +} + +// static +void nsRFPService::PrefChanged(const char* aPref, void* aSelf) { + static_cast<nsRFPService*>(aSelf)->PrefChanged(aPref); +} + +void nsRFPService::PrefChanged(const char* aPref) { + nsDependentCString pref(aPref); + + if (pref.EqualsLiteral(RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF)) { + UpdateFPPOverrideList(); + } +} + +NS_IMETHODIMP +nsRFPService::Observe(nsISupports* aObject, const char* aTopic, + const char16_t* aMessage) { + if (strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, aTopic) == 0) { + StartShutdown(); + } + + if (strcmp(LAST_PB_SESSION_EXITED_TOPIC, aTopic) == 0) { + // Clear the private session key when the private session ends so that we + // can generate a new key for the new private session. + OriginAttributesPattern pattern; + pattern.mPrivateBrowsingId.Construct(1); + ClearBrowsingSessionKey(pattern); + } + + if (!strcmp(OBSERVER_TOPIC_IDLE_DAILY, aTopic)) { + if (StaticPrefs:: + privacy_resistFingerprinting_randomization_daily_reset_enabled()) { + OriginAttributesPattern pattern; + pattern.mPrivateBrowsingId.Construct( + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID); + ClearBrowsingSessionKey(pattern); + } + + if (StaticPrefs:: + privacy_resistFingerprinting_randomization_daily_reset_private_enabled()) { + OriginAttributesPattern pattern; + pattern.mPrivateBrowsingId.Construct(1); + ClearBrowsingSessionKey(pattern); + } + } + + if (nsCRT::strcmp(aTopic, "profile-after-change") == 0 && + XRE_IsParentProcess()) { + // Get the singleton of the remote override service if we are in the parent + // process. + nsresult rv; + mWebCompatService = + do_GetService(NS_FINGERPRINTINGWEBCOMPATSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mWebCompatService->Init(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Reduce Timer Precision Stuff + +constexpr double RFP_TIME_ATOM_MS = 16.667; // 60Hz, 1000/60 but rounded. +/* +In RFP RAF always runs at 60Hz, so we're ~0.02% off of 1000/60 here. +```js +extra_frames_per_frame = 16.667 / (1000/60) - 1 // 0.00028 +sec_per_extra_frame = 1 / (extra_frames_per_frame * 60) // 833.33 +min_per_extra_frame = sec_per_extra_frame / 60 // 13.89 +``` +We expect an extra frame every ~14 minutes, which is enough to be smooth. +16.67 would be ~1.4 minutes, which is OK, but is more noticable. +Put another way, if this is the only unacceptable hitch you have across 14 +minutes, I'm impressed, and we might revisit this. +*/ + +/* static */ +double nsRFPService::TimerResolution(RTPCallerType aRTPCallerType) { + double prefValue = StaticPrefs:: + privacy_resistFingerprinting_reduceTimerPrecision_microseconds(); + if (aRTPCallerType == RTPCallerType::ResistFingerprinting) { + return std::max(RFP_TIME_ATOM_MS * 1000.0, prefValue); + } + return prefValue; +} + +/** + * 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 three components: + * a secret seed, a per-timeline (context) 'mix-in', 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. + * + * The 'Context Mix-in' is a securely generated random seed that is unique for + * each timeline that starts over at zero. It is needed to ensure that the + * sequence of midpoints (as calculated by the secret seed and clamped time) + * does not repeat. In RelativeTimeline.h, we define a 'RelativeTimeline' class + * that can be inherited by any object that has a relative timeline. The most + * obvious examples are 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 over at zero, and they know + * (approximately) where the random midpoints are. + * + * When the timestamp given is a non-relative timestamp (e.g. it is relative to + * the unix epoch) it is not possible to replay a sequence of random values. + * Thus, providing a zero context pointer is an indicator that the timestamp + * given is absolute and does not need any additional randomness. + * + * @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 aContextMixin [in] An opaque random value for relative + * timestamps. 0 for absolute timestamps + * @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, + int64_t aContextMixin, + long long* aMidpointOut, + uint8_t* aSecretSeed /* = nullptr */) { + nsresult rv; + const int kSeedSize = 16; + static Atomic<uint8_t*> sSecretMidpointSeed; + + if (MOZ_UNLIKELY(!aMidpointOut)) { + return NS_ERROR_INVALID_ARG; + } + + /* + * Below, we will use three different values to seed a fairly simple random + * number generator. On the first run we initiate the secret seed, which + * is mixed in with the time epoch and the context mix in to seed the RNG. + * + * This isn't the most secure method of generating a random midpoint but is + * reasonably performant and should be sufficient for our purposes. + */ + + // If we don't have a seed, we need to get one. + if (MOZ_UNLIKELY(!sSecretMidpointSeed)) { + nsCOMPtr<nsIRandomGenerator> randomGenerator = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint8_t* temp = nullptr; + rv = randomGenerator->GenerateRandomBytes(kSeedSize, &temp); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (MOZ_UNLIKELY(!sSecretMidpointSeed.compareExchange(nullptr, temp))) { + // Some other thread initted this first, never mind! + free(temp); + } + } + + // sSecretMidpointSeed is now set, and invariant. The contents of the buffer + // it points to is also invariant, _unless_ this function is called with a + // non-null |aSecretSeed|. + uint8_t* seed = sSecretMidpointSeed; + MOZ_RELEASE_ASSERT(seed); + + // If someone has passed in the testing-only parameter, replace our seed with + // it. We do _not_ re-allocate the buffer, since that can lead to UAF below. + // The math could still be racy if the caller supplies a new secret seed while + // some other thread is calling this function, but since this is arcane + // test-only functionality that is used in only one test-case presently, we + // put the burden of using this particular footgun properly on the test code. + if (MOZ_UNLIKELY(aSecretSeed != nullptr)) { + memcpy(seed, aSecretSeed, kSeedSize); + } + + // Seed and create our random number generator. + non_crypto::XorShift128PlusRNG rng(aContextMixin ^ *(uint64_t*)(seed), + aClampedTimeUSec ^ *(uint64_t*)(seed + 8)); + + // Retrieve the output midpoint value. + if (MOZ_UNLIKELY(aResolutionUSec <= 0)) { // ??? Bug 1718066 + return NS_ERROR_FAILURE; + } + *aMidpointOut = rng.next() % aResolutionUSec; + + 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 given TimerPrecisionType. Note that if one desires a minimum + * precision for Resist Fingerprinting, it is the caller's responsibility to + * provide the correct value. This means you should pass TimerResolution(), + * which enforces a minimum value on the precision based on preferences. + * + * It ensures the given precision value is greater than zero, if it is not it + * returns the input time. + * + * While the correct thing to pass is TimerResolution() we expose it as an + * argument for testing purposes only. + * + * @param aTime [in] The input time to be clamped. + * @param aTimeScale [in] The units the input time is in (Seconds, + * Milliseconds, or Microseconds). + * @param aResolutionUSec [in] The precision (in microseconds) to clamp to. + * @param aContextMixin [in] An opaque random value for relative timestamps. + * 0 for absolute timestamps + * @return If clamping is appropriate, the clamped value of the + * input, otherwise the input. + */ +/* static */ +double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale, + double aResolutionUSec, + int64_t aContextMixin, + TimerPrecisionType aType) { + if (aType == TimerPrecisionType::DangerouslyNone) { + return aTime; + } + + // This boolean will serve as a flag indicating we are clamping the time + // unconditionally. We do this when timer reduction preference is off; but we + // still want to apply 20us clamping to al timestamps to avoid leaking + // nano-second precision. + bool unconditionalClamping = false; + if (aType == UnconditionalAKAHighRes || aResolutionUSec <= 0) { + unconditionalClamping = true; + aResolutionUSec = RFP_TIMER_UNCONDITIONAL_VALUE; // 20 microseconds + aContextMixin = 0; // Just clarifies our logging statement at the end, + // otherwise unused + } + + // Increase the time as needed until it is in microseconds. + // Note that a double can hold up to 2**53 with integer precision. This gives + // us only until June 5, 2255 in time-since-the-epoch with integer precision. + // So we will be losing microseconds precision after that date. + // We think this is okay, and we codify it in some tests. + double timeScaled = aTime * (1000000 / aTimeScale); + // Cut off anything less than a microsecond. + long long timeAsInt = timeScaled; + + // If we have a blank context mixin, this indicates we (should) have an + // absolute timestamp. We check the time, and if it less than a unix timestamp + // about 10 years in the past, we output to the log and, in debug builds, + // assert. This is an error case we want to understand and fix: we must have + // given a relative timestamp with a mixin of 0 which is incorrect. Anyone + // running a debug build _probably_ has an accurate clock, and if they don't, + // they'll hopefully find this message and understand why things are crashing. + const long long kFeb282008 = 1204233985000; + if (aContextMixin == 0 && timeAsInt < kFeb282008 && !unconditionalClamping && + aType != TimerPrecisionType::RFP) { + nsAutoCString type; + TypeToText(aType, type); + MOZ_LOG( + gResistFingerprintingLog, LogLevel::Error, + ("About to assert. aTime=%lli<%lli aContextMixin=%" PRId64 " aType=%s", + timeAsInt, kFeb282008, aContextMixin, type.get())); + MOZ_ASSERT( + false, + "ReduceTimePrecisionImpl was given a relative time " + "with an empty context mix-in (or your clock is 10+ years off.) " + "Run this with MOZ_LOG=nsResistFingerprinting:1 to get more details."); + } + + // Cast the resolution (in microseconds) to an int. + long long resolutionAsInt = aResolutionUSec; + // Perform the clamping. + // We do a cast back to double to perform the division with doubles, then + // floor the result and the rest occurs with integer precision. This is + // because it gives consistency above and below zero. Above zero, performing + // the 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; + long long clampedAndJittered = clamped; + if (!unconditionalClamping && + StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter()) { + if (!NS_FAILED(RandomMidpoint(clamped, resolutionAsInt, aContextMixin, + &midpoint)) && + timeAsInt >= clamped + midpoint) { + clampedAndJittered += resolutionAsInt; + } + } + + // Cast it back to a double and reduce it to the correct units. + double ret = double(clampedAndJittered) / (1000000.0 / double(aTimeScale)); + + MOZ_LOG( + gResistFingerprintingLog, LogLevel::Verbose, + ("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding %s with (%lli, " + "Originally %.*f), " + "Intermediate: (%lli), Clamped: (%lli) Jitter: (%i Context: %" PRId64 + " Midpoint: %lli) " + "Final: (%lli Converted: %.*f)", + DBL_DIG - 1, aTime, DBL_DIG - 1, timeScaled, timeAsInt, + (unconditionalClamping ? "unconditionally" : "normally"), + resolutionAsInt, DBL_DIG - 1, aResolutionUSec, + (long long)floor(double(timeAsInt) / resolutionAsInt), clamped, + StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter(), + aContextMixin, midpoint, clampedAndJittered, DBL_DIG - 1, ret)); + + return ret; +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsUSecs(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType) { + const auto type = GetTimerPrecisionType(aRTPCallerType); + return nsRFPService::ReduceTimePrecisionImpl(aTime, MicroSeconds, + TimerResolution(aRTPCallerType), + aContextMixin, type); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsMSecs(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType) { + const auto type = GetTimerPrecisionType(aRTPCallerType); + return nsRFPService::ReduceTimePrecisionImpl(aTime, MilliSeconds, + TimerResolution(aRTPCallerType), + aContextMixin, type); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly( + double aTime, int64_t aContextMixin, RTPCallerType aRTPCallerType) { + return nsRFPService::ReduceTimePrecisionImpl( + aTime, MilliSeconds, TimerResolution(aRTPCallerType), aContextMixin, + GetTimerPrecisionTypeRFPOnly(aRTPCallerType)); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsSecs(double aTime, + int64_t aContextMixin, + RTPCallerType aRTPCallerType) { + const auto type = GetTimerPrecisionType(aRTPCallerType); + return nsRFPService::ReduceTimePrecisionImpl( + aTime, Seconds, TimerResolution(aRTPCallerType), aContextMixin, type); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsSecsRFPOnly( + double aTime, int64_t aContextMixin, RTPCallerType aRTPCallerType) { + return nsRFPService::ReduceTimePrecisionImpl( + aTime, Seconds, TimerResolution(aRTPCallerType), aContextMixin, + GetTimerPrecisionTypeRFPOnly(aRTPCallerType)); +} + +/* static */ +double nsRFPService::ReduceTimePrecisionAsUSecsWrapper( + double aTime, JS::RTPCallerTypeToken aCallerType, JSContext* aCx) { + MOZ_ASSERT(aCx); + +#ifdef DEBUG + nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx); + MOZ_ASSERT(global->GetRTPCallerType() == RTPCallerTypeFromToken(aCallerType)); +#endif + + RTPCallerType callerType = RTPCallerTypeFromToken(aCallerType); + return nsRFPService::ReduceTimePrecisionImpl( + aTime, MicroSeconds, TimerResolution(callerType), + 0, /* For absolute timestamps (all the JS engine does), supply zero + context mixin */ + GetTimerPrecisionType(callerType)); +} + +/* static */ +TimerPrecisionType nsRFPService::GetTimerPrecisionType( + RTPCallerType aRTPCallerType) { + if (aRTPCallerType == RTPCallerType::SystemPrincipal) { + return DangerouslyNone; + } + + if (aRTPCallerType == RTPCallerType::ResistFingerprinting) { + return RFP; + } + + if (StaticPrefs::privacy_reduceTimerPrecision() && + aRTPCallerType == RTPCallerType::CrossOriginIsolated) { + return UnconditionalAKAHighRes; + } + + if (StaticPrefs::privacy_reduceTimerPrecision()) { + return Normal; + } + + if (StaticPrefs::privacy_reduceTimerPrecision_unconditional()) { + return UnconditionalAKAHighRes; + } + + return DangerouslyNone; +} + +/* static */ +TimerPrecisionType nsRFPService::GetTimerPrecisionTypeRFPOnly( + RTPCallerType aRTPCallerType) { + if (aRTPCallerType == RTPCallerType::ResistFingerprinting) { + return RFP; + } + + if (StaticPrefs::privacy_reduceTimerPrecision_unconditional() && + aRTPCallerType != RTPCallerType::SystemPrincipal) { + return UnconditionalAKAHighRes; + } + + return DangerouslyNone; +} + +/* static */ +void nsRFPService::TypeToText(TimerPrecisionType aType, nsACString& aText) { + switch (aType) { + case TimerPrecisionType::DangerouslyNone: + aText.AssignLiteral("DangerouslyNone"); + return; + case TimerPrecisionType::Normal: + aText.AssignLiteral("Normal"); + return; + case TimerPrecisionType::RFP: + aText.AssignLiteral("RFP"); + return; + case TimerPrecisionType::UnconditionalAKAHighRes: + aText.AssignLiteral("UnconditionalAKAHighRes"); + return; + default: + MOZ_ASSERT(false, "Shouldn't go here"); + aText.AssignLiteral("Unknown Enum Value"); + return; + } +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Video Statistics Spoofing + +/* static */ +uint32_t nsRFPService::CalculateTargetVideoResolution(uint32_t aVideoQuality) { + return aVideoQuality * NSToIntCeil(aVideoQuality * 16 / 9.0); +} + +/* static */ +uint32_t nsRFPService::GetSpoofedTotalFrames(double aTime) { + double precision = + TimerResolution(RTPCallerType::ResistFingerprinting) / 1000 / 1000; + double time = floor(aTime / precision) * precision; + + return NSToIntFloor(time * kVideoFramesPerSec); +} + +/* static */ +uint32_t nsRFPService::GetSpoofedDroppedFrames(double aTime, uint32_t aWidth, + uint32_t aHeight) { + uint32_t targetRes = CalculateTargetVideoResolution( + StaticPrefs::privacy_resistFingerprinting_target_video_res()); + + // The video resolution is less than or equal to the target resolution, we + // report a zero dropped rate for this case. + if (targetRes >= aWidth * aHeight) { + return 0; + } + + double precision = + TimerResolution(RTPCallerType::ResistFingerprinting) / 1000 / 1000; + double time = floor(aTime / precision) * precision; + // Bound the dropped ratio from 0 to 100. + uint32_t boundedDroppedRatio = std::min(kVideoDroppedRatio, 100U); + + return NSToIntFloor(time * kVideoFramesPerSec * + (boundedDroppedRatio / 100.0)); +} + +/* static */ +uint32_t nsRFPService::GetSpoofedPresentedFrames(double aTime, uint32_t aWidth, + uint32_t aHeight) { + uint32_t targetRes = CalculateTargetVideoResolution( + StaticPrefs::privacy_resistFingerprinting_target_video_res()); + + // The target resolution is greater than the current resolution. For this + // case, there will be no dropped frames, so we report total frames directly. + if (targetRes >= aWidth * aHeight) { + return GetSpoofedTotalFrames(aTime); + } + + double precision = + TimerResolution(RTPCallerType::ResistFingerprinting) / 1000 / 1000; + double time = floor(aTime / precision) * precision; + // Bound the dropped ratio from 0 to 100. + uint32_t boundedDroppedRatio = std::min(kVideoDroppedRatio, 100U); + + return NSToIntFloor(time * kVideoFramesPerSec * + ((100 - boundedDroppedRatio) / 100.0)); +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// User-Agent/Version Stuff + +/* static */ +void nsRFPService::GetSpoofedUserAgent(nsACString& userAgent, + bool isForHTTPHeader) { + // This function generates the spoofed value of User Agent. + // We spoof the values of the platform and Firefox version, which could be + // used as fingerprinting sources to identify individuals. + // Reference of the format of User Agent: + // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorID/userAgent + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent + + // These magic numbers are the lengths of the UA string literals below. + // Assume three-digit Firefox version numbers so we have room to grow. + size_t preallocatedLength = + 13 + + (isForHTTPHeader ? mozilla::ArrayLength(SPOOFED_HTTP_UA_OS) + : mozilla::ArrayLength(SPOOFED_UA_OS)) - + 1 + 5 + 3 + 10 + mozilla::ArrayLength(LEGACY_UA_GECKO_TRAIL) - 1 + 9 + 3 + + 2; + userAgent.SetCapacity(preallocatedLength); + + // "Mozilla/5.0 (%s; rv:%d.0) Gecko/%d Firefox/%d.0" + userAgent.AssignLiteral("Mozilla/5.0 ("); + + if (isForHTTPHeader) { + userAgent.AppendLiteral(SPOOFED_HTTP_UA_OS); + } else { + userAgent.AppendLiteral(SPOOFED_UA_OS); + } + + userAgent.AppendLiteral("; rv:" MOZILLA_UAVERSION ") Gecko/"); + +#if defined(ANDROID) + userAgent.AppendLiteral(MOZILLA_UAVERSION); +#else + userAgent.AppendLiteral(LEGACY_UA_GECKO_TRAIL); +#endif + + userAgent.AppendLiteral(" Firefox/" MOZILLA_UAVERSION); + + MOZ_ASSERT(userAgent.Length() <= preallocatedLength); +} + +/* static */ +nsCString nsRFPService::GetSpoofedJSLocale() { return "en-US"_ns; } + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Keyboard Spoofing Stuff + +nsTHashMap<KeyboardHashKey, const SpoofingKeyboardCode*>* + nsRFPService::sSpoofingKeyboardCodes = nullptr; + +KeyboardHashKey::KeyboardHashKey(const KeyboardLangs aLang, + const KeyboardRegions aRegion, + const KeyNameIndexType aKeyIdx, + const nsAString& aKey) + : mLang(aLang), mRegion(aRegion), mKeyIdx(aKeyIdx), mKey(aKey) {} + +KeyboardHashKey::KeyboardHashKey(KeyTypePointer aOther) + : mLang(aOther->mLang), + mRegion(aOther->mRegion), + mKeyIdx(aOther->mKeyIdx), + mKey(aOther->mKey) {} + +KeyboardHashKey::KeyboardHashKey(KeyboardHashKey&& aOther) noexcept + : PLDHashEntryHdr(std::move(aOther)), + mLang(std::move(aOther.mLang)), + mRegion(std::move(aOther.mRegion)), + mKeyIdx(std::move(aOther.mKeyIdx)), + mKey(std::move(aOther.mKey)) {} + +KeyboardHashKey::~KeyboardHashKey() = default; + +bool KeyboardHashKey::KeyEquals(KeyTypePointer aOther) const { + return mLang == aOther->mLang && mRegion == aOther->mRegion && + mKeyIdx == aOther->mKeyIdx && mKey == aOther->mKey; +} + +KeyboardHashKey::KeyTypePointer KeyboardHashKey::KeyToPointer(KeyType aKey) { + return &aKey; +} + +PLDHashNumber KeyboardHashKey::HashKey(KeyTypePointer aKey) { + PLDHashNumber hash = mozilla::HashString(aKey->mKey); + return mozilla::AddToHash(hash, aKey->mRegion, aKey->mKeyIdx, aKey->mLang); +} + +/* static */ +void nsRFPService::MaybeCreateSpoofingKeyCodes(const KeyboardLangs aLang, + const KeyboardRegions aRegion) { + if (sSpoofingKeyboardCodes == nullptr) { + sSpoofingKeyboardCodes = + new nsTHashMap<KeyboardHashKey, const SpoofingKeyboardCode*>(); + } + + if (KeyboardLang::EN == aLang) { + switch (aRegion) { + case KeyboardRegion::US: + MaybeCreateSpoofingKeyCodesForEnUS(); + break; + } + } +} + +/* static */ +void nsRFPService::MaybeCreateSpoofingKeyCodesForEnUS() { + MOZ_ASSERT(sSpoofingKeyboardCodes); + + static bool sInitialized = false; + const KeyboardLangs lang = KeyboardLang::EN; + const KeyboardRegions reg = KeyboardRegion::US; + + if (sInitialized) { + return; + } + + static const SpoofingKeyboardInfo spoofingKeyboardInfoTable[] = { +#define KEY(key_, _codeNameIdx, _keyCode, _modifier) \ + {NS_LITERAL_STRING_FROM_CSTRING(key_), \ + KEY_NAME_INDEX_USE_STRING, \ + {CODE_NAME_INDEX_##_codeNameIdx, _keyCode, _modifier}}, +#define CONTROL(keyNameIdx_, _codeNameIdx, _keyCode) \ + {u""_ns, \ + KEY_NAME_INDEX_##keyNameIdx_, \ + {CODE_NAME_INDEX_##_codeNameIdx, _keyCode, MODIFIER_NONE}}, +#include "KeyCodeConsensus_En_US.h" +#undef CONTROL +#undef KEY + }; + + for (const auto& keyboardInfo : spoofingKeyboardInfoTable) { + KeyboardHashKey key(lang, reg, keyboardInfo.mKeyIdx, keyboardInfo.mKey); + MOZ_ASSERT(!sSpoofingKeyboardCodes->Contains(key), + "Double-defining key code; fix your KeyCodeConsensus file"); + sSpoofingKeyboardCodes->InsertOrUpdate(key, &keyboardInfo.mSpoofingCode); + } + + sInitialized = true; +} + +/* static */ +void nsRFPService::GetKeyboardLangAndRegion(const nsAString& aLanguage, + KeyboardLangs& aLocale, + KeyboardRegions& aRegion) { + nsAutoString langStr; + nsAutoString regionStr; + uint32_t partNum = 0; + + for (const nsAString& part : aLanguage.Split('-')) { + if (partNum == 0) { + langStr = part; + } else { + regionStr = part; + break; + } + + partNum++; + } + + // We test each language here as well as the region. There are some cases that + // only the language is given, we will use the default region code when this + // happens. The default region should depend on the given language. + if (langStr.EqualsLiteral(RFP_KEYBOARD_LANG_STRING_EN)) { + aLocale = KeyboardLang::EN; + // Give default values first. + aRegion = KeyboardRegion::US; + + if (regionStr.EqualsLiteral(RFP_KEYBOARD_REGION_STRING_US)) { + aRegion = KeyboardRegion::US; + } + } else { + // There is no spoofed keyboard locale for the given language. We use the + // default one in this case. + aLocale = RFP_DEFAULT_SPOOFING_KEYBOARD_LANG; + aRegion = RFP_DEFAULT_SPOOFING_KEYBOARD_REGION; + } +} + +/* static */ +bool nsRFPService::GetSpoofedKeyCodeInfo( + const dom::Document* aDoc, const WidgetKeyboardEvent* aKeyboardEvent, + SpoofingKeyboardCode& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + KeyboardLangs keyboardLang = RFP_DEFAULT_SPOOFING_KEYBOARD_LANG; + KeyboardRegions keyboardRegion = RFP_DEFAULT_SPOOFING_KEYBOARD_REGION; + // If the document is given, we use the content language which is get from the + // document. Otherwise, we use the default one. + if (aDoc) { + nsAtom* lang = aDoc->GetContentLanguage(); + + // If the content-langauge is not given, we try to get langauge from the + // HTML lang attribute. + if (!lang) { + if (dom::Element* elm = aDoc->GetHtmlElement()) { + lang = elm->GetLang(); + } + } + + // If two or more languages are given, per HTML5 spec, we should consider + // it as 'unknown'. So we use the default one. + if (lang) { + nsDependentAtomString langStr(lang); + if (!langStr.Contains(char16_t(','))) { + langStr.StripWhitespace(); + GetKeyboardLangAndRegion(langStr, keyboardLang, keyboardRegion); + } + } + } + + MaybeCreateSpoofingKeyCodes(keyboardLang, keyboardRegion); + + KeyNameIndex keyIdx = aKeyboardEvent->mKeyNameIndex; + nsAutoString keyName; + + if (keyIdx == KEY_NAME_INDEX_USE_STRING) { + keyName = aKeyboardEvent->mKeyValue; + } + + KeyboardHashKey key(keyboardLang, keyboardRegion, keyIdx, keyName); + const SpoofingKeyboardCode* keyboardCode = sSpoofingKeyboardCodes->Get(key); + + if (keyboardCode != nullptr) { + aOut = *keyboardCode; + return true; + } + + return false; +} + +/* static */ +bool nsRFPService::GetSpoofedModifierStates( + const dom::Document* aDoc, const WidgetKeyboardEvent* aKeyboardEvent, + const Modifiers aModifier, bool& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + // For modifier or control keys, we don't need to hide its modifier states. + if (aKeyboardEvent->mKeyNameIndex != KEY_NAME_INDEX_USE_STRING) { + return false; + } + + // We will spoof the modifer state for Alt, Shift, and AltGraph. + // We don't spoof the Control key, because it is often used + // for command key combinations in web apps. + if ((aModifier & (MODIFIER_ALT | MODIFIER_SHIFT | MODIFIER_ALTGRAPH)) != 0) { + SpoofingKeyboardCode keyCodeInfo; + + if (GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) { + aOut = ((keyCodeInfo.mModifierStates & aModifier) != 0); + return true; + } + } + + return false; +} + +/* static */ +bool nsRFPService::GetSpoofedCode(const dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + nsAString& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + SpoofingKeyboardCode keyCodeInfo; + + if (!GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) { + return false; + } + + WidgetKeyboardEvent::GetDOMCodeName(keyCodeInfo.mCode, aOut); + + // We need to change the 'Left' with 'Right' if the location indicates + // it's a right key. + if (aKeyboardEvent->mLocation == + dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_RIGHT && + StringEndsWith(aOut, u"Left"_ns)) { + aOut.ReplaceLiteral(aOut.Length() - 4, 4, u"Right"); + } + + return true; +} + +/* static */ +bool nsRFPService::GetSpoofedKeyCode(const dom::Document* aDoc, + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t& aOut) { + MOZ_ASSERT(aKeyboardEvent); + + SpoofingKeyboardCode keyCodeInfo; + + if (GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) { + aOut = keyCodeInfo.mKeyCode; + return true; + } + + return false; +} + +// ============================================================================ +// ============================================================================ +// ============================================================================ +// Randomization Stuff +nsresult nsRFPService::GetBrowsingSessionKey( + const OriginAttributes& aOriginAttributes, nsID& aBrowsingSessionKey) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsAutoCString oaSuffix; + aOriginAttributes.CreateSuffix(oaSuffix); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Info, + ("Get the browsing session key for the originAttributes: %s\n", + oaSuffix.get())); + + // If any fingerprinting randomization protection is enabled, we generate the + // browsing session key. + // Note that there is only canvas randomization protection currently. + if (!nsContentUtils::ShouldResistFingerprinting( + "Checking the target activation globally without local context", + RFPTarget::CanvasRandomization)) { + return NS_ERROR_NOT_AVAILABLE; + } + + Maybe<nsID> sessionKey = mBrowsingSessionKeys.MaybeGet(oaSuffix); + + // The key has been generated, bail out earlier. + if (sessionKey) { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Info, + ("The browsing session key exists: %s\n", + sessionKey.ref().ToString().get())); + aBrowsingSessionKey = sessionKey.ref(); + return NS_OK; + } + + nsID& newKey = + mBrowsingSessionKeys.InsertOrUpdate(oaSuffix, nsID::GenerateUUID()); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug, + ("Generated browsing session key: %s\n", newKey.ToString().get())); + aBrowsingSessionKey = newKey; + + return NS_OK; +} + +void nsRFPService::ClearBrowsingSessionKey( + const OriginAttributesPattern& aPattern) { + MOZ_ASSERT(XRE_IsParentProcess()); + + for (auto iter = mBrowsingSessionKeys.Iter(); !iter.Done(); iter.Next()) { + nsAutoCString key(iter.Key()); + OriginAttributes attrs; + Unused << attrs.PopulateFromSuffix(key); + + // Remove the entry if the origin attributes pattern matches + if (aPattern.Matches(attrs)) { + iter.Remove(); + } + } +} + +void nsRFPService::ClearBrowsingSessionKey( + const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsAutoCString key; + aOriginAttributes.CreateSuffix(key); + + mBrowsingSessionKeys.Remove(key); +} + +// static +Maybe<nsTArray<uint8_t>> nsRFPService::GenerateKey(nsIChannel* aChannel) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aChannel); + +#ifdef DEBUG + // Ensure we only compute random key for top-level loads. + { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + MOZ_ASSERT(loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT); + } +#endif + + nsCOMPtr<nsIURI> topLevelURI; + Unused << aChannel->GetURI(getter_AddRefs(topLevelURI)); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug, + ("Generating the randomization key for top-level URI: %s\n", + topLevelURI->GetSpecOrDefault().get())); + + RefPtr<nsRFPService> service = GetOrCreate(); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + + // Set the partitionKey using the top level URI to ensure that the key is + // specific to the top level site. + attrs.SetPartitionKey(topLevelURI); + + nsAutoCString oaSuffix; + attrs.CreateSuffix(oaSuffix); + + MOZ_LOG(gResistFingerprintingLog, LogLevel::Debug, + ("Get the key using OriginAttributes: %s\n", oaSuffix.get())); + + nsID sessionKey = {}; + if (NS_FAILED(service->GetBrowsingSessionKey(attrs, sessionKey))) { + return Nothing(); + } + + // Return nothing if fingerprinting randomization is disabled for the given + // channel. + // + // Note that canvas randomization is the only fingerprinting randomization + // protection currently. + if (!nsContentUtils::ShouldResistFingerprinting( + aChannel, RFPTarget::CanvasRandomization)) { + return Nothing(); + } + auto sessionKeyStr = sessionKey.ToString(); + + // Generate the key by using the hMAC. The key is based on the session key and + // the partitionKey, i.e. top-level site. + HMAC hmac; + + nsresult rv = hmac.Begin( + SEC_OID_SHA256, + Span(reinterpret_cast<const uint8_t*>(sessionKeyStr.get()), NSID_LENGTH)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + // Using the OriginAttributes to get the top level site. The site is composed + // of scheme, host, and port. + NS_ConvertUTF16toUTF8 topLevelSite(attrs.mPartitionKey); + rv = hmac.Update(reinterpret_cast<const uint8_t*>(topLevelSite.get()), + topLevelSite.Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + Maybe<nsTArray<uint8_t>> key; + key.emplace(); + + rv = hmac.End(key.ref()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + return key; +} + +NS_IMETHODIMP +nsRFPService::CleanAllRandomKeys() { + MOZ_ASSERT(XRE_IsParentProcess()); + mBrowsingSessionKeys.Clear(); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::CleanRandomKeyByPrincipal(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + NS_ENSURE_ARG_POINTER(aPrincipal); + NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE); + + OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + nsCOMPtr<nsIURI> uri = aPrincipal->GetURI(); + attrs.SetPartitionKey(uri); + + ClearBrowsingSessionKey(attrs); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::CleanRandomKeyByDomain(const nsACString& aDomain) { + MOZ_ASSERT(XRE_IsParentProcess()); + + // Get http URI from the domain. + nsCOMPtr<nsIURI> httpURI; + nsresult rv = NS_NewURI(getter_AddRefs(httpURI), "http://"_ns + aDomain); + NS_ENSURE_SUCCESS(rv, rv); + + // Use the originAttributes to get the partitionKey. + OriginAttributes attrs; + attrs.SetPartitionKey(httpURI); + + // Create a originAttributesPattern and set the http partitionKey to the + // pattern. + OriginAttributesPattern pattern; + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); + + ClearBrowsingSessionKey(pattern); + + // Get https URI from the domain. + nsCOMPtr<nsIURI> httpsURI; + rv = NS_NewURI(getter_AddRefs(httpsURI), "https://"_ns + aDomain); + NS_ENSURE_SUCCESS(rv, rv); + + // Use the originAttributes to get the partitionKey and set to the pattern. + attrs.SetPartitionKey(httpsURI); + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); + + ClearBrowsingSessionKey(pattern); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::CleanRandomKeyByHost(const nsACString& aHost, + const nsAString& aPattern) { + MOZ_ASSERT(XRE_IsParentProcess()); + + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + // Get http URI from the host. + nsCOMPtr<nsIURI> httpURI; + nsresult rv = NS_NewURI(getter_AddRefs(httpURI), "http://"_ns + aHost); + NS_ENSURE_SUCCESS(rv, rv); + + // Use the originAttributes to get the partitionKey. + OriginAttributes attrs; + attrs.SetPartitionKey(httpURI); + + // Set the partitionKey to the pattern. + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); + + ClearBrowsingSessionKey(pattern); + + // Get https URI from the host. + nsCOMPtr<nsIURI> httpsURI; + rv = NS_NewURI(getter_AddRefs(httpsURI), "https://"_ns + aHost); + NS_ENSURE_SUCCESS(rv, rv); + + // Use the originAttributes to get the partitionKey and set to the pattern. + attrs.SetPartitionKey(httpsURI); + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); + + ClearBrowsingSessionKey(pattern); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::CleanRandomKeyByOriginAttributesPattern( + const nsAString& aPattern) { + MOZ_ASSERT(XRE_IsParentProcess()); + + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + ClearBrowsingSessionKey(pattern); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::TestGenerateRandomKey(nsIChannel* aChannel, + nsTArray<uint8_t>& aKey) { + MOZ_ASSERT(XRE_IsParentProcess()); + NS_ENSURE_ARG_POINTER(aChannel); + + Maybe<nsTArray<uint8_t>> key = GenerateKey(aChannel); + + if (!key) { + return NS_OK; + } + + aKey = key.ref().Clone(); + return NS_OK; +} + +// static +nsresult nsRFPService::GenerateCanvasKeyFromImageData( + nsICookieJarSettings* aCookieJarSettings, uint8_t* aImageData, + uint32_t aSize, nsTArray<uint8_t>& aCanvasKey) { + NS_ENSURE_ARG_POINTER(aCookieJarSettings); + + nsTArray<uint8_t> randomKey; + nsresult rv = + aCookieJarSettings->GetFingerprintingRandomizationKey(randomKey); + + // There is no random key for this cookieJarSettings. This means that the + // randomization is disabled. So, we can bail out from here without doing + // anything. + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + // Generate the key for randomizing the canvas data using hMAC. The key is + // based on the random key of the document and the canvas data itself. So, + // different canvas would have different keys. + HMAC hmac; + + rv = hmac.Begin(SEC_OID_SHA256, Span(randomKey)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hmac.Update(aImageData, aSize); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hmac.End(aCanvasKey); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +nsresult nsRFPService::RandomizePixels(nsICookieJarSettings* aCookieJarSettings, + uint8_t* aData, uint32_t aWidth, + uint32_t aHeight, uint32_t aSize, + gfx::SurfaceFormat aSurfaceFormat) { + NS_ENSURE_ARG_POINTER(aData); + + if (!aCookieJarSettings) { + return NS_OK; + } + + if (aSize <= 4) { + return NS_OK; + } + + // Don't randomize if all pixels are uniform. + static constexpr size_t bytesPerPixel = 4; + MOZ_ASSERT(aSize == aWidth * aHeight * bytesPerPixel, + "Pixels must be tightly-packed"); + const bool allPixelsMatch = [&]() { + auto itr = RangedPtr<const uint8_t>(aData, aSize); + const auto itrEnd = itr + aSize; + for (; itr != itrEnd; itr += bytesPerPixel) { + if (memcmp(itr.get(), aData, bytesPerPixel) != 0) { + return false; + } + } + return true; + }(); + if (allPixelsMatch) { + return NS_OK; + } + + auto timerId = + glean::fingerprinting_protection::canvas_noise_calculate_time.Start(); + + nsTArray<uint8_t> canvasKey; + nsresult rv = GenerateCanvasKeyFromImageData(aCookieJarSettings, aData, aSize, + canvasKey); + if (NS_FAILED(rv)) { + glean::fingerprinting_protection::canvas_noise_calculate_time.Cancel( + std::move(timerId)); + return rv; + } + + // Calculate the number of pixels based on the given data size. One pixel uses + // 4 bytes that contains ARGB information. + uint32_t pixelCnt = aSize / 4; + + // Generate random values that will decide the RGB channel and the pixel + // position that we are going to introduce the noises. The channel and + // position are predictable to ensure we have a consistant result with the + // same canvas in the same browsing session. + + // Seed and create the first random number generator which will be used to + // select RGB channel and the pixel position. The seed is the first half of + // the canvas key. + non_crypto::XorShift128PlusRNG rng1( + *reinterpret_cast<uint64_t*>(canvasKey.Elements()), + *reinterpret_cast<uint64_t*>(canvasKey.Elements() + 8)); + + // Use the last 8 bits as the number of noises. + uint8_t rnd3 = canvasKey.LastElement(); + + // Clear the last 8 bits. + canvasKey.ReplaceElementAt(canvasKey.Length() - 1, 0); + + // Use the remaining 120 bits to seed and create the second random number + // generator. The random number will be used to decided the noise bit that + // will be added to the lowest order bit of the channel of the pixel. + non_crypto::XorShift128PlusRNG rng2( + *reinterpret_cast<uint64_t*>(canvasKey.Elements() + 16), + *reinterpret_cast<uint64_t*>(canvasKey.Elements() + 24)); + + // Ensure at least 20 random changes may occur. + uint8_t numNoises = std::clamp<uint8_t>(rnd3, 20, 255); + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code" +#endif + if (false) { + // For debugging purposes you can dump the image with this code + // then convert it with the image-magick command + // convert -size WxH -depth 8 rgba:$i $i.png + // Depending on surface format, the alpha and color channels might be mixed + // up... + static int calls = 0; + char filename[256]; + SprintfLiteral(filename, "rendered_image_%dx%d_%d_pre", aWidth, aHeight, + calls); + FILE* outputFile = fopen(filename, "wb"); // "wb" for binary write mode + fwrite(aData, 1, aSize, outputFile); + fclose(outputFile); + calls++; + } +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + + while (numNoises--) { + // Choose which RGB channel to add a noise. The pixel data is in either + // the BGRA or the ARGB format depending on the endianess. To choose the + // color channel we need to add the offset according the endianess. + uint32_t channel; + if (aSurfaceFormat == gfx::SurfaceFormat::B8G8R8A8) { + channel = rng1.next() % 3; + } else if (aSurfaceFormat == gfx::SurfaceFormat::A8R8G8B8) { + channel = rng1.next() % 3 + 1; + } else { + return NS_ERROR_INVALID_ARG; + } + + uint32_t idx = 4 * (rng1.next() % pixelCnt) + channel; + uint8_t bit = rng2.next(); + + // 50% chance to XOR a 0x2 or 0x1 into the existing byte + aData[idx] = aData[idx] ^ (0x2 >> (bit & 0x1)); + } + + glean::fingerprinting_protection::canvas_noise_calculate_time + .StopAndAccumulate(std::move(timerId)); + + return NS_OK; +} + +static const char* CanvasFingerprinterToString( + ContentBlockingNotifier::CanvasFingerprinter aFingerprinter) { + switch (aFingerprinter) { + case ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS: + return "FingerprintJS"; + case ContentBlockingNotifier::CanvasFingerprinter::eAkamai: + return "Akamai"; + case ContentBlockingNotifier::CanvasFingerprinter::eVariant1: + return "Variant1"; + case ContentBlockingNotifier::CanvasFingerprinter::eVariant2: + return "Variant2"; + case ContentBlockingNotifier::CanvasFingerprinter::eVariant3: + return "Variant3"; + case ContentBlockingNotifier::CanvasFingerprinter::eVariant4: + return "Variant4"; + case ContentBlockingNotifier::CanvasFingerprinter::eMaybe: + return "Maybe"; + } + return "<error>"; +} + +static void MaybeCurrentCaller(nsACString& aFilename, uint32_t& aLineNum, + uint32_t& aColumnNum) { + aFilename.AssignLiteral("<unknown>"); + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + return; + } + + JS::AutoFilename scriptFilename; + JS::ColumnNumberOneOrigin columnNum; + if (JS::DescribeScriptedCaller(cx, &scriptFilename, &aLineNum, &columnNum)) { + if (const char* file = scriptFilename.get()) { + aFilename = nsDependentCString(file); + } + } + aColumnNum = columnNum.oneOriginValue(); +} + +/* static */ void nsRFPService::MaybeReportCanvasFingerprinter( + nsTArray<CanvasUsage>& aUses, nsIChannel* aChannel, + nsACString& aOriginNoSuffix) { + if (!aChannel) { + return; + } + + uint32_t extractedWebGL = 0; + bool seenExtractedWebGL_300x150 = false; + + uint32_t extracted2D = 0; + bool seenExtracted2D_16x16 = false; + bool seenExtracted2D_122x110 = false; + bool seenExtracted2D_240x60 = false; + bool seenExtracted2D_280x60 = false; + bool seenExtracted2D_860x6 = false; + CanvasFeatureUsage featureUsage = CanvasFeatureUsage::None; + + uint32_t extractedOther = 0; + + for (const auto& usage : aUses) { + int32_t width = usage.mSize.width; + int32_t height = usage.mSize.height; + + if (width > 2000 || height > 1000) { + // Canvases used for fingerprinting are usually relatively small. + continue; + } + + if (usage.mType == dom::CanvasContextType::Canvas2D) { + featureUsage |= usage.mFeatureUsage; + extracted2D++; + if (width == 16 && height == 16) { + seenExtracted2D_16x16 = true; + } else if (width == 240 && height == 60) { + seenExtracted2D_240x60 = true; + } else if (width == 122 && height == 110) { + seenExtracted2D_122x110 = true; + } else if (width == 280 && height == 60) { + seenExtracted2D_280x60 = true; + } else if (width == 860 && height == 6) { + seenExtracted2D_860x6 = true; + } + } else if (usage.mType == dom::CanvasContextType::WebGL1) { + extractedWebGL++; + if (width == 300 && height == 150) { + seenExtractedWebGL_300x150 = true; + } + } else { + extractedOther++; + } + } + + Maybe<ContentBlockingNotifier::CanvasFingerprinter> fingerprinter; + if (seenExtractedWebGL_300x150 && seenExtracted2D_240x60 && + seenExtracted2D_122x110) { + fingerprinter = + Some(ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS); + } else if (seenExtractedWebGL_300x150 && seenExtracted2D_280x60 && + seenExtracted2D_16x16) { + fingerprinter = Some(ContentBlockingNotifier::CanvasFingerprinter::eAkamai); + } else if (seenExtractedWebGL_300x150 && extracted2D > 0 && + (featureUsage & CanvasFeatureUsage::SetFont)) { + fingerprinter = + Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant1); + } else if (extractedWebGL > 0 && extracted2D > 1 && seenExtracted2D_860x6) { + fingerprinter = + Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant2); + } else if (extractedOther > 0 && (extractedWebGL > 0 || extracted2D > 0)) { + fingerprinter = + Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant3); + } else if (extracted2D > 0 && (featureUsage & CanvasFeatureUsage::SetFont) && + (featureUsage & + (CanvasFeatureUsage::FillRect | CanvasFeatureUsage::LineTo | + CanvasFeatureUsage::Stroke))) { + fingerprinter = + Some(ContentBlockingNotifier::CanvasFingerprinter::eVariant4); + } else if (extractedOther + extractedWebGL + extracted2D > 1) { + // This I added primarily to not miss anything, but it can cause false + // positives. + fingerprinter = Some(ContentBlockingNotifier::CanvasFingerprinter::eMaybe); + } + + bool knownFingerprintText = + bool(featureUsage & CanvasFeatureUsage::KnownFingerprintText); + if (!knownFingerprintText && fingerprinter.isNothing()) { + return; + } + + if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Info)) { + nsAutoCString filename; + uint32_t lineNum = 0; + uint32_t columnNum = 0; + MaybeCurrentCaller(filename, lineNum, columnNum); + + nsAutoCString origin(aOriginNoSuffix); + MOZ_LOG( + gFingerprinterDetection, LogLevel::Info, + ("Detected a potential canvas fingerprinter on %s in script %s:%d:%d " + "(KnownFingerprintText: %s, CanvasFingerprinter: %s)", + origin.get(), filename.get(), lineNum, columnNum, + knownFingerprintText ? "true" : "false", + fingerprinter.isSome() + ? CanvasFingerprinterToString(fingerprinter.value()) + : "<none>")); + } + + ContentBlockingNotifier::OnEvent( + aChannel, false, + nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING, + aOriginNoSuffix, Nothing(), fingerprinter, + Some(featureUsage & CanvasFeatureUsage::KnownFingerprintText)); +} + +/* static */ void nsRFPService::MaybeReportFontFingerprinter( + nsIChannel* aChannel, nsACString& aOriginNoSuffix) { + if (!aChannel) { + return; + } + + if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Info)) { + nsAutoCString filename; + uint32_t lineNum = 0; + uint32_t columnNum = 0; + MaybeCurrentCaller(filename, lineNum, columnNum); + + nsAutoCString origin(aOriginNoSuffix); + MOZ_LOG(gFingerprinterDetection, LogLevel::Info, + ("Detected a potential font fingerprinter on %s in script %s:%d:%d", + origin.get(), filename.get(), lineNum, columnNum)); + } + + ContentBlockingNotifier::OnEvent( + aChannel, false, + nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING, + aOriginNoSuffix); +} + +/* static */ +bool nsRFPService::CheckSuspiciousFingerprintingActivity( + nsTArray<ContentBlockingLog::LogEntry>& aLogs) { + if (aLogs.Length() == 0) { + return false; + } + + uint32_t cnt = 0; + // We use these two booleans to prevent counting duplicated fingerprinting + // events. + bool foundCanvas = false; + bool foundFont = false; + + // Iterate through the logs to see if there are suspicious fingerprinting + // activities. + for (auto& log : aLogs) { + // If it's a known canvas fingerprinter, we can directly return true from + // here. + if (log.mCanvasFingerprinter && + (log.mCanvasFingerprinter.ref() == + ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS || + log.mCanvasFingerprinter.ref() == + ContentBlockingNotifier::CanvasFingerprinter::eAkamai)) { + return true; + } else if (!foundCanvas && log.mType == + nsIWebProgressListener:: + STATE_ALLOWED_CANVAS_FINGERPRINTING) { + cnt++; + foundCanvas = true; + } else if (!foundFont && + log.mType == + nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) { + cnt++; + foundFont = true; + } + } + + // If the number of suspicious fingerprinting activity exceeds the threshold, + // we return true to indicates there is a suspicious fingerprinting activity. + return cnt > kSuspiciousFingerprintingActivityThreshold; +} + +/* static */ +nsresult nsRFPService::CreateOverrideDomainKey( + nsIFingerprintingOverride* aOverride, nsACString& aDomainKey) { + MOZ_ASSERT(aOverride); + + aDomainKey.Truncate(); + + nsAutoCString firstPartyDomain; + nsresult rv = aOverride->GetFirstPartyDomain(firstPartyDomain); + NS_ENSURE_SUCCESS(rv, rv); + + // The first party domain shouldn't be empty. And it shouldn't contain a comma + // because we use a comma as a delimiter. + if (firstPartyDomain.IsEmpty() || + firstPartyDomain.Contains(FP_OVERRIDES_DOMAIN_KEY_DELIMITER)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString thirdPartyDomain; + rv = aOverride->GetThirdPartyDomain(thirdPartyDomain); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't accept both domains are wildcards. + if (firstPartyDomain.EqualsLiteral("*") && + thirdPartyDomain.EqualsLiteral("*")) { + return NS_ERROR_FAILURE; + } + + if (thirdPartyDomain.IsEmpty()) { + aDomainKey.Assign(firstPartyDomain); + } else { + // Ensure the third-party domain doesn't contain a delimiter. + if (thirdPartyDomain.Contains(FP_OVERRIDES_DOMAIN_KEY_DELIMITER)) { + return NS_ERROR_FAILURE; + } + + aDomainKey.Assign(firstPartyDomain); + aDomainKey.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER); + aDomainKey.Append(thirdPartyDomain); + } + + return NS_OK; +} + +/* static */ +RFPTarget nsRFPService::CreateOverridesFromText(const nsString& aOverridesText, + RFPTarget aBaseOverrides) { + RFPTarget result = aBaseOverrides; + + for (const nsAString& each : aOverridesText.Split(',')) { + Maybe<RFPTarget> mappedValue = + nsRFPService::TextToRFPTarget(Substring(each, 1, each.Length() - 1)); + if (mappedValue.isSome()) { + RFPTarget target = mappedValue.value(); + if (target == RFPTarget::IsAlwaysEnabledForPrecompute) { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("RFPTarget::%s is not a valid value", + NS_ConvertUTF16toUTF8(each).get())); + } else if (each[0] == '+') { + result |= target; + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Mapped value %s (0x%" PRIx64 + "), to an addition, now we have 0x%" PRIx64, + NS_ConvertUTF16toUTF8(each).get(), uint64_t(target), + uint64_t(result))); + } else if (each[0] == '-') { + result &= ~target; + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Mapped value %s (0x%" PRIx64 + ") to a subtraction, now we have 0x%" PRIx64, + NS_ConvertUTF16toUTF8(each).get(), uint64_t(target), + uint64_t(result))); + } else { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Mapped value %s (0x%" PRIx64 + ") to an RFPTarget Enum, but the first " + "character wasn't + or -", + NS_ConvertUTF16toUTF8(each).get(), uint64_t(target))); + } + } else { + MOZ_LOG(gResistFingerprintingLog, LogLevel::Warning, + ("Could not map the value %s to an RFPTarget Enum", + NS_ConvertUTF16toUTF8(each).get())); + } + } + + return result; +} + +NS_IMETHODIMP +nsRFPService::SetFingerprintingOverrides( + const nsTArray<RefPtr<nsIFingerprintingOverride>>& aOverrides) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Clear all overrides before importing. + mFingerprintingOverrides.Clear(); + + for (const auto& fpOverride : aOverrides) { + nsAutoCString domainKey; + + nsresult rv = nsRFPService::CreateOverrideDomainKey(fpOverride, domainKey); + // Skip the current overrides if we fail to create the domain key. + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString overridesText; + rv = fpOverride->GetOverrides(overridesText); + NS_ENSURE_SUCCESS(rv, rv); + + RFPTarget targets = nsRFPService::CreateOverridesFromText( + NS_ConvertUTF8toUTF16(overridesText), + mFingerprintingOverrides.Contains(domainKey) + ? mFingerprintingOverrides.Get(domainKey) + : sEnabledFingerprintingProtections); + + // The newly added one will replace the existing one for the given domain + // key. + mFingerprintingOverrides.InsertOrUpdate(domainKey, targets); + } + + if (Preferences::GetBool( + "privacy.fingerprintingProtection.remoteOverrides.testing", false)) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE); + + obs->NotifyObservers(nullptr, "fpp-test:set-overrides-finishes", nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::GetEnabledFingerprintingProtections(uint64_t* aProtections) { + RFPTarget enabled = sEnabledFingerprintingProtections; + + *aProtections = uint64_t(enabled); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::GetFingerprintingOverrides(const nsACString& aDomainKey, + uint64_t* aOverrides) { + MOZ_ASSERT(XRE_IsParentProcess()); + + Maybe<RFPTarget> overrides = mFingerprintingOverrides.MaybeGet(aDomainKey); + + if (!overrides) { + return NS_ERROR_FAILURE; + } + + *aOverrides = uint64_t(overrides.ref()); + return NS_OK; +} + +NS_IMETHODIMP +nsRFPService::CleanAllOverrides() { + MOZ_ASSERT(XRE_IsParentProcess()); + mFingerprintingOverrides.Clear(); + return NS_OK; +} + +/* static */ +Maybe<RFPTarget> nsRFPService::GetOverriddenFingerprintingSettingsForChannel( + nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsIURI> uri; + Unused << aChannel->GetURI(getter_AddRefs(uri)); + + if (uri->SchemeIs("about") && !NS_IsContentAccessibleAboutURI(uri)) { + return Nothing(); + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + MOZ_ASSERT(loadInfo); + + RefPtr<dom::BrowsingContext> bc; + loadInfo->GetTargetBrowsingContext(getter_AddRefs(bc)); + if (!bc || !bc->IsContent()) { + return Nothing(); + } + + // The channel is for the first-party load. + if (!loadInfo->GetIsThirdPartyContextToTopWindow()) { + return GetOverriddenFingerprintingSettingsForURI(uri, nullptr); + } + + // The channel is for the third-party load. We get the first-party URI from + // the top-level window global parent. + RefPtr<dom::WindowGlobalParent> topWGP = + bc->Top()->Canonical()->GetCurrentWindowGlobal(); + + if (NS_WARN_IF(!topWGP)) { + return Nothing(); + } + + nsCOMPtr<nsIPrincipal> topPrincipal = topWGP->DocumentPrincipal(); + if (NS_WARN_IF(!topPrincipal)) { + return Nothing(); + } + + // Only apply the override if the top is content. In testing, the top level + // document could be a null principal. We don't need to apply override in this + // case. + if (!topPrincipal->GetIsContentPrincipal()) { + return Nothing(); + } + + nsCOMPtr<nsIURI> topURI = topWGP->GetDocumentURI(); + if (NS_WARN_IF(!topURI)) { + return Nothing(); + } + + // The top-level page could be navigated to an error page. We cannot get + // the correct override in this case. So, we return nothing from here. + if (nsContentUtils::IsErrorPage(topURI)) { + return Nothing(); + } + +#ifdef DEBUG + // Verify if the top URI matches the partitionKey of the channel. + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + + nsAutoString partitionKey; + cookieJarSettings->GetPartitionKey(partitionKey); + + OriginAttributes attrs; + attrs.SetPartitionKey(topURI); + + // The partitionKey of the channel could haven't been set here if the loading + // channel is top-level. + MOZ_ASSERT_IF(!partitionKey.IsEmpty(), + attrs.mPartitionKey.Equals(partitionKey)); +#endif + + return GetOverriddenFingerprintingSettingsForURI(topURI, uri); +} + +/* static */ +Maybe<RFPTarget> nsRFPService::GetOverriddenFingerprintingSettingsForURI( + nsIURI* aFirstPartyURI, nsIURI* aThirdPartyURI) { + MOZ_ASSERT(aFirstPartyURI); + MOZ_ASSERT(XRE_IsParentProcess()); + + RefPtr<nsRFPService> service = GetOrCreate(); + if (NS_WARN_IF(!service)) { + return Nothing(); + } + + // The fingerprinting overrides with a specific scope will replace the + // overrides with a more general scope. For example, the {first-party domain} + // will take over {first-party domain, *} because the latter one has a smaller + // scope. + + // First, we get the overrides that applies to every context. + Maybe<RFPTarget> result = service->mFingerprintingOverrides.MaybeGet("*"_ns); + + RefPtr<nsEffectiveTLDService> eTLDService = + nsEffectiveTLDService::GetInstance(); + if (NS_WARN_IF(!eTLDService)) { + return Nothing(); + } + + nsAutoCString firstPartyDomain; + nsresult rv = eTLDService->GetBaseDomain(aFirstPartyURI, 0, firstPartyDomain); + if (NS_FAILED(rv)) { + return Nothing(); + } + + // The check is for a first-party load. A first-party load can be a + // top-level load or a first-party subresource/iframe load. The first-party + // load can match the following two scopes. + // 1. {first-party domain, *}: Every context that is under the given + // first-party domain, including itself. + // 2. {first-party domain}: First-party contexts that load the given + // first-party domain. + if (!aThirdPartyURI) { + // Test the {first-party domain, *} scope. + nsAutoCString key; + key.Assign(firstPartyDomain); + key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER); + key.Append("*"); + + Maybe<RFPTarget> fpOverrides = + service->mFingerprintingOverrides.MaybeGet(key); + if (fpOverrides) { + result = fpOverrides; + } + + // Test the {first-party domain} scope. + fpOverrides = service->mFingerprintingOverrides.MaybeGet(firstPartyDomain); + if (fpOverrides) { + result = fpOverrides; + } + + return result; + } + + // The check is for a third-party load. The third-party load can match the + // following three scopes. + // 1. {first-party domain, *}: Every context that is under the given + // first-party domain. + // 2. {*, third-party domain}: Every third-party context that loads the + // given third-party domain. + // 3. {first-party domain, third-party domain}: The third-party context that + // is under the given first-party domain. + + nsAutoCString thirdPartyDomain; + rv = eTLDService->GetBaseDomain(aThirdPartyURI, 0, thirdPartyDomain); + if (NS_FAILED(rv)) { + return Nothing(); + } + + // Test {first-party domain, *} scope. + nsAutoCString key; + key.Assign(firstPartyDomain); + key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER); + key.Append("*"); + Maybe<RFPTarget> fpOverrides = + service->mFingerprintingOverrides.MaybeGet(key); + if (fpOverrides) { + result = fpOverrides; + } + + // Test {*, third-party domain} scope. + key.Assign("*"); + key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER); + key.Append(thirdPartyDomain); + fpOverrides = service->mFingerprintingOverrides.MaybeGet(key); + if (fpOverrides) { + result = fpOverrides; + } + + // Test {first-party domain, third-party domain} scope. + key.Assign(firstPartyDomain); + key.Append(FP_OVERRIDES_DOMAIN_KEY_DELIMITER); + key.Append(thirdPartyDomain); + fpOverrides = service->mFingerprintingOverrides.MaybeGet(key); + if (fpOverrides) { + result = fpOverrides; + } + + return result; +} |