Backed out changeset e5c32a502e0f (bug 1915419) Backed out changeset 8cbcbb25fc99 (bug 1915419) Backed out changeset 947379a2f167 (bug 1915419) Backed out changeset 4cfe3a954ade (bug 1915419) Backed out changeset 94fba1a2786b (bug 1915419) Backed out changeset 477b4f7c2760 (bug 1915419) Backed out changeset ed6c8d9d39d4 (bug 1915419) Backed out changeset e534c390101c (bug 1915419) Backed out changeset bc3fe74a8b03 (bug 1915419) Backed out changeset afefdce605f9 (bug 1915419) Backed out changeset e58887b90d39 (bug 1915419) Backed out changeset 114298b7b855 (bug 1915419)
2421 lines
82 KiB
C++
2421 lines
82 KiB
C++
/* -*- 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 https://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/AntiTrackingUtils.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/dom/MediaDeviceInfoBinding.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 "nsUserCharacteristics.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");
|
|
|
|
static mozilla::LazyLogModule gTimestamps("Timestamps");
|
|
|
|
#define RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF \
|
|
"privacy.fingerprintingProtection.overrides"
|
|
#define GLEAN_DATA_SUBMISSION_PREF "datareporting.healthreport.uploadEnabled"
|
|
#define USER_CHARACTERISTICS_UUID_PREF \
|
|
"toolkit.telemetry.user_characteristics_ping.uuid"
|
|
|
|
#define RFP_TIMER_UNCONDITIONAL_VALUE 20
|
|
#define LAST_PB_SESSION_EXITED_TOPIC "last-pb-context-exited"
|
|
#define IDLE_TOPIC "browser-idle-startup-tasks-finished"
|
|
#define GFX_FEATURES "gfx-features-ready"
|
|
#define USER_CHARACTERISTICS_TEST_REQUEST \
|
|
"user-characteristics-testing-please-populate-data"
|
|
|
|
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)
|
|
// NOLINTNEXTLINE(bugprone-macro-parentheses)
|
|
# define ANDROID_DEFAULT(name) RFPTarget::name |
|
|
# define DESKTOP_DEFAULT(name)
|
|
#else
|
|
# define ANDROID_DEFAULT(name)
|
|
// NOLINTNEXTLINE(bugprone-macro-parentheses)
|
|
# define DESKTOP_DEFAULT(name) RFPTarget::name |
|
|
#endif
|
|
|
|
const RFPTarget kDefaultFingerprintingProtections =
|
|
#include "RFPTargetsDefault.inc"
|
|
static_cast<RFPTarget>(0);
|
|
|
|
#undef ANDROID_DEFAULT
|
|
#undef DESKTOP_DEFAULT
|
|
|
|
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,
|
|
GLEAN_DATA_SUBMISSION_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);
|
|
|
|
rv = obs->AddObserver(this, IDLE_TOPIC, false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obs->AddObserver(this, GFX_FEATURES, false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obs->AddObserver(this, USER_CHARACTERISTICS_TEST_REQUEST, 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 SPOOFED_MAX_TOUCH_POINTS > 0
|
|
if (aTarget == RFPTarget::PointerId) {
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
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);
|
|
obs->RemoveObserver(this, IDLE_TOPIC);
|
|
obs->RemoveObserver(this, GFX_FEATURES);
|
|
obs->RemoveObserver(this, USER_CHARACTERISTICS_TEST_REQUEST);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
MOZ_LOG(gResistFingerprintingLog, LogLevel::Info,
|
|
("Pref Changed: %s", aPref));
|
|
nsDependentCString pref(aPref);
|
|
|
|
if (pref.EqualsLiteral(RESIST_FINGERPRINTINGPROTECTION_OVERRIDE_PREF)) {
|
|
UpdateFPPOverrideList();
|
|
} else if (pref.EqualsLiteral(GLEAN_DATA_SUBMISSION_PREF)) {
|
|
if (XRE_IsParentProcess() &&
|
|
!Preferences::GetBool(GLEAN_DATA_SUBMISSION_PREF, false)) {
|
|
MOZ_LOG(gResistFingerprintingLog, LogLevel::Info, ("Clearing UUID"));
|
|
// If the user has unset the telemetry pref, wipe out the UUID pref value
|
|
// (The data will also be erased server-side via the "deletion-request"
|
|
// ping)
|
|
Preferences::SetCString(USER_CHARACTERISTICS_UUID_PREF, ""_ns);
|
|
}
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsRFPService::Observe(nsISupports* aObject, const char* aTopic,
|
|
const char16_t* aMessage) {
|
|
const int kNumTopicsForUserCharacteristics = 2;
|
|
static int seenTopicsForUserCharacteristics = 0;
|
|
|
|
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(IDLE_TOPIC, aTopic) || !strcmp(GFX_FEATURES, aTopic)) {
|
|
seenTopicsForUserCharacteristics++;
|
|
|
|
if (seenTopicsForUserCharacteristics == kNumTopicsForUserCharacteristics) {
|
|
nsUserCharacteristics::MaybeSubmitPing();
|
|
}
|
|
}
|
|
|
|
if (!strcmp(USER_CHARACTERISTICS_TEST_REQUEST, aTopic) &&
|
|
xpc::IsInAutomation()) {
|
|
nsUserCharacteristics::PopulateDataAndEventuallySubmit(
|
|
/* aUpdatePref = */ false, /* aTesting = */ true);
|
|
}
|
|
|
|
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 noticeable.
|
|
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(
|
|
gTimestamps, 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=Timestamps: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(
|
|
gTimestamps, 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.
|
|
bool foreignByAncestorContext =
|
|
AntiTrackingUtils::IsThirdPartyChannel(aChannel) &&
|
|
loadInfo->GetIsThirdPartyContextToTopWindow();
|
|
attrs.SetPartitionKey(topLevelURI, foreignByAncestorContext);
|
|
|
|
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;
|
|
}
|
|
|
|
// static
|
|
Maybe<nsTArray<uint8_t>> nsRFPService::GenerateKeyForServiceWorker(
|
|
nsIURI* aURI, bool aForeignByAncestorContext) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
MOZ_ASSERT(aURI);
|
|
|
|
RefPtr<nsRFPService> service = GetOrCreate();
|
|
|
|
RefPtr<nsIPrincipal> principal =
|
|
BasePrincipal::CreateContentPrincipal(aURI->GetSpecOrDefault());
|
|
OriginAttributes attrs = principal->OriginAttributesRef();
|
|
attrs.SetPartitionKey(aURI, aForeignByAncestorContext);
|
|
|
|
nsAutoCString oaSuffix;
|
|
attrs.CreateSuffix(oaSuffix);
|
|
|
|
nsID sessionKey = {};
|
|
if (NS_FAILED(service->GetBrowsingSessionKey(attrs, sessionKey))) {
|
|
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, false);
|
|
ClearBrowsingSessionKey(attrs);
|
|
|
|
// We must also include the cross-site embeds of this principal that end up
|
|
// re-embedded back into the same principal's top level, otherwise state will
|
|
// persist for this target
|
|
attrs.SetPartitionKey(uri, true);
|
|
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, false);
|
|
|
|
// Create a originAttributesPattern and set the http partitionKey to the
|
|
// pattern.
|
|
OriginAttributesPattern pattern;
|
|
pattern.mPartitionKey.Reset();
|
|
pattern.mPartitionKey.Construct(attrs.mPartitionKey);
|
|
ClearBrowsingSessionKey(pattern);
|
|
|
|
// We must also include the cross-site embeds of this principal that end up
|
|
// re-embedded back into the same principal's top level, otherwise state will
|
|
// persist for this target
|
|
attrs.SetPartitionKey(httpURI, true);
|
|
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, false);
|
|
pattern.mPartitionKey.Reset();
|
|
pattern.mPartitionKey.Construct(attrs.mPartitionKey);
|
|
ClearBrowsingSessionKey(pattern);
|
|
|
|
// We must also include the cross-site embeds of this principal that end up
|
|
// re-embedded back into the same principal's top level, otherwise state will
|
|
// persist for this target
|
|
attrs.SetPartitionKey(httpsURI, true);
|
|
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, false);
|
|
|
|
// Set the partitionKey to the pattern.
|
|
pattern.mPartitionKey.Reset();
|
|
pattern.mPartitionKey.Construct(attrs.mPartitionKey);
|
|
|
|
ClearBrowsingSessionKey(pattern);
|
|
|
|
// We must also include the cross-site embeds of this principal that end up
|
|
// re-embedded back into the same principal's top level, otherwise state will
|
|
// persist for this target
|
|
attrs.SetPartitionKey(httpURI, true);
|
|
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, false);
|
|
pattern.mPartitionKey.Reset();
|
|
pattern.mPartitionKey.Construct(attrs.mPartitionKey);
|
|
ClearBrowsingSessionKey(pattern);
|
|
|
|
// We must also include the cross-site embeds of this principal that end up
|
|
// re-embedded back into the same principal's top level, otherwise state will
|
|
// persist for this target
|
|
attrs.SetPartitionKey(httpsURI, true);
|
|
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 */
|
|
bool nsRFPService::IsSoftwareRenderingOptionExposed(JSContext* aCx,
|
|
JSObject* aObj) {
|
|
if (!NS_IsMainThread()) {
|
|
return false;
|
|
}
|
|
|
|
nsIPrincipal* principal = nsContentUtils::SubjectPrincipal(aCx);
|
|
if (principal->IsSystemPrincipal()) {
|
|
return true;
|
|
}
|
|
|
|
return principal->Equals(
|
|
nsContentUtils::GetFingerprintingProtectionPrincipal());
|
|
}
|
|
|
|
/* 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 (!AntiTrackingUtils::IsThirdPartyChannel(aChannel)) {
|
|
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::CanonicalBrowsingContext> topBC = bc->Top()->Canonical();
|
|
RefPtr<dom::WindowGlobalParent> topWGP = topBC->GetCurrentWindowGlobal();
|
|
|
|
if (NS_WARN_IF(!topWGP)) {
|
|
return Nothing();
|
|
}
|
|
|
|
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
|
|
DebugOnly<nsresult> rv =
|
|
loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
MOZ_ASSERT(cookieJarSettings);
|
|
|
|
uint64_t topWindowContextIdFromCJS =
|
|
net::CookieJarSettings::Cast(cookieJarSettings)
|
|
->GetTopLevelWindowContextId();
|
|
|
|
// The top-level window could be navigated away when we get the fingerprinting
|
|
// override here. For example, the beacon requests. In this case, the
|
|
// top-level windowContext id won't match the inner window id of the top-level
|
|
// windowGlobalParent. So, we cannot rely on the URI from the top-level
|
|
// windowGlobalParent because it could be different from the one that creates
|
|
// the channel. Instead, we fallback to use the partitionKey in the
|
|
// cookieJarSettings to get the top-level URI.
|
|
if (topWGP->InnerWindowId() != topWindowContextIdFromCJS) {
|
|
nsAutoString partitionKey;
|
|
rv = cookieJarSettings->GetPartitionKey(partitionKey);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
|
|
// Bail out early if the partitionKey is empty.
|
|
if (partitionKey.IsEmpty()) {
|
|
return Nothing();
|
|
}
|
|
|
|
nsAutoString scheme;
|
|
nsAutoString domain;
|
|
int32_t unused;
|
|
bool unused2;
|
|
if (!OriginAttributes::ParsePartitionKey(partitionKey, scheme, domain,
|
|
unused, unused2)) {
|
|
MOZ_ASSERT(false);
|
|
return Nothing();
|
|
}
|
|
|
|
nsCOMPtr<nsIURI> topURI;
|
|
rv = NS_NewURI(getter_AddRefs(topURI), scheme + u"://"_ns + domain);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
|
|
return GetOverriddenFingerprintingSettingsForURI(topURI, uri);
|
|
}
|
|
|
|
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.
|
|
nsAutoString partitionKey;
|
|
cookieJarSettings->GetPartitionKey(partitionKey);
|
|
|
|
OriginAttributes attrs;
|
|
attrs.SetPartitionKey(topURI, false);
|
|
|
|
OriginAttributes attrsForeignByAncestor;
|
|
attrsForeignByAncestor.SetPartitionKey(topURI, true);
|
|
|
|
// 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) ||
|
|
attrsForeignByAncestor.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;
|
|
}
|
|
|
|
/* static */
|
|
void nsRFPService::GetMediaDeviceName(nsString& aName,
|
|
dom::MediaDeviceKind aKind) {
|
|
switch (aKind) {
|
|
case dom::MediaDeviceKind::Audioinput:
|
|
aName.Assign(u"Internal Microphone"_ns);
|
|
break;
|
|
case dom::MediaDeviceKind::Videoinput:
|
|
aName = u"Internal Camera"_ns;
|
|
break;
|
|
case dom::MediaDeviceKind::Audiooutput:
|
|
aName = u"Internal Speaker"_ns;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* static */
|
|
void nsRFPService::GetMediaDeviceGroup(nsString& aGroup,
|
|
dom::MediaDeviceKind aKind) {
|
|
switch (aKind) {
|
|
case dom::MediaDeviceKind::Audioinput:
|
|
aGroup.Assign(u"Audio Device Group"_ns);
|
|
break;
|
|
case dom::MediaDeviceKind::Videoinput:
|
|
aGroup = u"Video Device Group"_ns;
|
|
break;
|
|
case dom::MediaDeviceKind::Audiooutput:
|
|
aGroup = u"Speaker Device Group"_ns;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* static */
|
|
uint16_t nsRFPService::ViewportSizeToAngle(int32_t aWidth, int32_t aHeight) {
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
bool neutral = aHeight >= aWidth;
|
|
#else
|
|
bool neutral = aWidth >= aHeight;
|
|
#endif
|
|
if (neutral) {
|
|
return 0;
|
|
}
|
|
return 90;
|
|
}
|
|
|
|
/* static */
|
|
dom::OrientationType nsRFPService::ViewportSizeToOrientationType(
|
|
int32_t aWidth, int32_t aHeight) {
|
|
if (aWidth >= aHeight) {
|
|
return dom::OrientationType::Landscape_primary;
|
|
}
|
|
return dom::OrientationType::Portrait_primary;
|
|
}
|
|
|
|
/* static */
|
|
dom::OrientationType nsRFPService::GetDefaultOrientationType() {
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
return dom::OrientationType::Portrait_primary;
|
|
#else
|
|
return dom::OrientationType::Landscape_primary;
|
|
#endif
|
|
}
|