Source code

Revision control

Other Tools

1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* This Source Code Form is subject to the terms of the Mozilla Public
3
* License, v. 2.0. If a copy of the MPL was not distributed with this
4
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6
#include "nsRFPService.h"
7
8
#include <algorithm>
9
#include <memory>
10
#include <time.h>
11
12
#include "mozilla/ClearOnShutdown.h"
13
#include "mozilla/dom/Element.h"
14
#include "mozilla/Logging.h"
15
#include "mozilla/Mutex.h"
16
#include "mozilla/Preferences.h"
17
#include "mozilla/Services.h"
18
#include "mozilla/StaticPtr.h"
19
#include "mozilla/StaticPrefs_privacy.h"
20
#include "mozilla/TextEvents.h"
21
#include "mozilla/dom/KeyboardEventBinding.h"
22
23
#include "nsCOMPtr.h"
24
#include "nsCoord.h"
25
#include "nsServiceManagerUtils.h"
26
#include "nsString.h"
27
#include "nsXULAppAPI.h"
28
#include "nsPrintfCString.h"
29
30
#include "nsICryptoHash.h"
31
#include "nsIObserverService.h"
32
#include "nsIRandomGenerator.h"
33
#include "nsIXULAppInfo.h"
34
#include "nsJSUtils.h"
35
36
#include "prenv.h"
37
#include "nss.h"
38
39
#include "js/Date.h"
40
41
using namespace mozilla;
42
43
static mozilla::LazyLogModule gResistFingerprintingLog(
44
"nsResistFingerprinting");
45
46
#define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting"
47
#define RFP_TIMER_PREF "privacy.reduceTimerPrecision"
48
#define RFP_TIMER_UNCONDITIONAL_PREF \
49
"privacy.reduceTimerPrecision.unconditional"
50
#define RFP_TIMER_UNCONDITIONAL_VALUE 20
51
#define RFP_TIMER_VALUE_PREF \
52
"privacy.resistFingerprinting.reduceTimerPrecision.microseconds"
53
#define RFP_JITTER_VALUE_PREF \
54
"privacy.resistFingerprinting.reduceTimerPrecision.jitter"
55
#define PROFILE_INITIALIZED_TOPIC "profile-initial-state"
56
57
static constexpr uint32_t kVideoFramesPerSec = 30;
58
static constexpr uint32_t kVideoDroppedRatio = 5;
59
60
#define RFP_DEFAULT_SPOOFING_KEYBOARD_LANG KeyboardLang::EN
61
#define RFP_DEFAULT_SPOOFING_KEYBOARD_REGION KeyboardRegion::US
62
63
NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver)
64
65
static StaticRefPtr<nsRFPService> sRFPService;
66
static bool sInitialized = false;
67
nsDataHashtable<KeyboardHashKey, const SpoofingKeyboardCode*>*
68
nsRFPService::sSpoofingKeyboardCodes = nullptr;
69
static mozilla::StaticMutex sLock;
70
71
/* static */
72
nsRFPService* nsRFPService::GetOrCreate() {
73
if (!sInitialized) {
74
sRFPService = new nsRFPService();
75
nsresult rv = sRFPService->Init();
76
77
if (NS_FAILED(rv)) {
78
sRFPService = nullptr;
79
return nullptr;
80
}
81
82
ClearOnShutdown(&sRFPService);
83
sInitialized = true;
84
}
85
86
return sRFPService;
87
}
88
89
/* static */
90
double nsRFPService::TimerResolution() {
91
double prefValue = StaticPrefs::
92
privacy_resistFingerprinting_reduceTimerPrecision_microseconds();
93
if (nsRFPService::IsResistFingerprintingEnabled()) {
94
return std::max(100000.0, prefValue);
95
}
96
return prefValue;
97
}
98
99
/* static */
100
bool nsRFPService::IsResistFingerprintingEnabled() {
101
return StaticPrefs::privacy_resistFingerprinting();
102
}
103
104
/* static */
105
bool nsRFPService::IsTimerPrecisionReductionEnabled(TimerPrecisionType aType) {
106
if (aType == TimerPrecisionType::RFPOnly) {
107
return IsResistFingerprintingEnabled();
108
}
109
110
return (StaticPrefs::privacy_reduceTimerPrecision() ||
111
IsResistFingerprintingEnabled()) &&
112
TimerResolution() > 0;
113
}
114
115
/*
116
* The below is a simple time-based Least Recently Used cache used to store the
117
* result of a cryptographic hash function. It has LRU_CACHE_SIZE slots, and
118
* will be used from multiple threads. It is thread-safe.
119
*/
120
#define LRU_CACHE_SIZE (45)
121
#define HASH_DIGEST_SIZE_BITS (256)
122
#define HASH_DIGEST_SIZE_BYTES (HASH_DIGEST_SIZE_BITS / 8)
123
124
class LRUCache final {
125
public:
126
LRUCache() : mLock("mozilla.resistFingerprinting.LRUCache") {
127
this->cache.SetLength(LRU_CACHE_SIZE);
128
}
129
130
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LRUCache)
131
132
nsCString Get(long long aKeyPart1, long long aKeyPart2) {
133
for (auto& cacheEntry : this->cache) {
134
// Read optimistically befor locking
135
if (cacheEntry.keyPart1 == aKeyPart1 &&
136
cacheEntry.keyPart2 == aKeyPart2) {
137
MutexAutoLock lock(mLock);
138
139
// Double check after we have a lock
140
if (MOZ_UNLIKELY(cacheEntry.keyPart1 != aKeyPart1 ||
141
cacheEntry.keyPart2 != aKeyPart2)) {
142
// Got evicted in a race
143
long long tmp_keyPart1 = cacheEntry.keyPart1;
144
long long tmp_keyPart2 = cacheEntry.keyPart2;
145
MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
146
("LRU Cache HIT-MISS with %lli != %lli and %lli != %lli",
147
aKeyPart1, tmp_keyPart1, aKeyPart2, tmp_keyPart2));
148
return EmptyCString();
149
}
150
151
cacheEntry.accessTime = PR_Now();
152
MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
153
("LRU Cache HIT with %lli %lli", aKeyPart1, aKeyPart2));
154
return cacheEntry.data;
155
}
156
}
157
158
return EmptyCString();
159
}
160
161
void Store(long long aKeyPart1, long long aKeyPart2,
162
const nsCString& aValue) {
163
MOZ_DIAGNOSTIC_ASSERT(aValue.Length() == HASH_DIGEST_SIZE_BYTES);
164
MutexAutoLock lock(mLock);
165
166
CacheEntry* lowestKey = &this->cache[0];
167
for (auto& cacheEntry : this->cache) {
168
if (MOZ_UNLIKELY(cacheEntry.keyPart1 == aKeyPart1 &&
169
cacheEntry.keyPart2 == aKeyPart2)) {
170
// Another thread inserted before us, don't insert twice
171
MOZ_LOG(
172
gResistFingerprintingLog, LogLevel::Verbose,
173
("LRU Cache DOUBLE STORE with %lli %lli", aKeyPart1, aKeyPart2));
174
return;
175
}
176
if (cacheEntry.accessTime < lowestKey->accessTime) {
177
lowestKey = &cacheEntry;
178
}
179
}
180
181
lowestKey->keyPart1 = aKeyPart1;
182
lowestKey->keyPart2 = aKeyPart2;
183
lowestKey->data = aValue;
184
lowestKey->accessTime = PR_Now();
185
MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
186
("LRU Cache STORE with %lli %lli", aKeyPart1, aKeyPart2));
187
}
188
189
private:
190
~LRUCache() = default;
191
192
struct CacheEntry {
193
Atomic<long long, Relaxed> keyPart1;
194
Atomic<long long, Relaxed> keyPart2;
195
PRTime accessTime = 0;
196
nsCString data;
197
198
CacheEntry() {
199
this->keyPart1 = 0xFFFFFFFFFFFFFFFF;
200
this->keyPart2 = 0xFFFFFFFFFFFFFFFF;
201
this->accessTime = 0;
202
this->data = nullptr;
203
}
204
CacheEntry(const CacheEntry& obj) {
205
this->keyPart1.exchange(obj.keyPart1);
206
this->keyPart2.exchange(obj.keyPart2);
207
this->accessTime = obj.accessTime;
208
this->data = obj.data;
209
}
210
};
211
212
AutoTArray<CacheEntry, LRU_CACHE_SIZE> cache;
213
mozilla::Mutex mLock;
214
};
215
216
// We make a single LRUCache
217
static StaticRefPtr<LRUCache> sCache;
218
219
/**
220
* The purpose of this function is to deterministicly generate a random midpoint
221
* between a lower clamped value and an upper clamped value. Assuming a clamping
222
* resolution of 100, here is an example:
223
*
224
* |---------------------------------------|--------------------------|
225
* lower clamped value (e.g. 300) | upper clamped value (400)
226
* random midpoint (e.g. 360)
227
*
228
* If our actual timestamp (e.g. 325) is below the midpoint, we keep it clamped
229
* downwards. If it were equal to or above the midpoint (e.g. 365) we would
230
* round it upwards to the largest clamped value (in this example: 400).
231
*
232
* The question is: does time go backwards?
233
*
234
* The midpoint is deterministicly random and generated from three components:
235
* a secret seed, a per-timeline (context) 'mix-in', and a clamped time.
236
*
237
* When comparing times across different seed values: time may go backwards.
238
* For a clamped time of 300, one seed may generate a midpoint of 305 and
239
* another 395. So comparing an (actual) timestamp of 325 and 351 could see the
240
* 325 clamped up to 400 and the 351 clamped down to 300. The seed is
241
* per-process, so this case occurs when one can compare timestamps
242
* cross-process. This is uncommon (because we don't have site isolation.) The
243
* circumstances this could occur are BroadcastChannel, Storage Notification,
244
* and in theory (but not yet implemented) SharedWorker. This should be an
245
* exhaustive list (at time of comment writing!).
246
*
247
* Aside from cross-process communication, derived timestamps across different
248
* time origins may go backwards. (Specifically, derived means adding two
249
* timestamps together to get an (approximate) absolute time.)
250
* Assume a page and a worker. If one calls performance.now() in the page and
251
* then triggers a call to performance.now() in the worker, the following
252
* invariant should hold true:
253
* page.performance.timeOrigin + page.performance.now() <
254
* worker.performance.timeOrigin + worker.performance.now()
255
*
256
* We break this invariant.
257
*
258
* The 'Context Mix-in' is a securely generated random seed that is unique for
259
* each timeline that starts over at zero. It is needed to ensure that the
260
* sequence of midpoints (as calculated by the secret seed and clamped time)
261
* does not repeat. In RelativeTimeline.h, we define a 'RelativeTimeline' class
262
* that can be inherited by any object that has a relative timeline. The most
263
* obvious examples are Documents and Workers. An attacker could let time go
264
* forward and observe (roughly) where the random midpoints fall. Then they
265
* create a new object, time starts back over at zero, and they know
266
* (approximately) where the random midpoints are.
267
*
268
* When the timestamp given is a non-relative timestamp (e.g. it is relative to
269
* the unix epoch) it is not possible to replay a sequence of random values.
270
* Thus, providing a zero context pointer is an indicator that the timestamp
271
* given is absolute and does not need any additional randomness.
272
*
273
* @param aClampedTimeUSec [in] The clamped input time in microseconds.
274
* @param aResolutionUSec [in] The current resolution for clamping in
275
* microseconds.
276
* @param aMidpointOut [out] The midpoint, in microseconds, between [0,
277
* aResolutionUSec].
278
* @param aContextMixin [in] An opaque random value for relative
279
* timestamps. 0 for absolute timestamps
280
* @param aSecretSeed [in] TESTING ONLY. When provided, the current seed
281
* will be replaced with this value.
282
* @return A nsresult indicating success of failure. If the
283
* function failed, nothing is written to aMidpointOut
284
*/
285
286
/* static */
287
nsresult nsRFPService::RandomMidpoint(long long aClampedTimeUSec,
288
long long aResolutionUSec,
289
int64_t aContextMixin,
290
long long* aMidpointOut,
291
uint8_t* aSecretSeed /* = nullptr */) {
292
nsresult rv;
293
const int kSeedSize = 16;
294
const int kClampTimesPerDigest = HASH_DIGEST_SIZE_BITS / 32;
295
static uint8_t* sSecretMidpointSeed = nullptr;
296
297
if (MOZ_UNLIKELY(!aMidpointOut)) {
298
return NS_ERROR_INVALID_ARG;
299
}
300
301
RefPtr<LRUCache> cache;
302
{
303
StaticMutexAutoLock lock(sLock);
304
cache = sCache;
305
}
306
307
if (!cache) {
308
return NS_ERROR_FAILURE;
309
}
310
311
/*
312
* Below, we will call a cryptographic hash function. That's expensive. We
313
* look for ways to make it more efficient.
314
*
315
* We only need as much output from the hash function as the maximum
316
* resolution we will ever support, because we will reduce the output modulo
317
* that value. The maximum resolution we think is likely is in the low seconds
318
* value, or about 1-10 million microseconds. 2**24 is 16 million, so we only
319
* need 24 bits of output. Practically speaking though, it's way easier to
320
* work with 32 bits.
321
*
322
* So we're using 32 bits of output and throwing away the other DIGEST_SIZE -
323
* 32 (in the case of SHA-256, DIGEST_SIZE is 256.) That's a lot of waste.
324
*
325
* Instead of throwing it away, we're going to use all of it. We can handle
326
* DIGEST_SIZE / 32 Clamped Time's per hash function - call that , so we
327
* reduce aClampedTime to a multiple of kClampTimesPerDigest (just like we
328
* reduced the real time value to aClampedTime!)
329
*
330
* Then we hash _that_ value (assuming it's not in the cache) and index into
331
* the digest result the appropriate bit offset.
332
*/
333
long long reducedResolution = aResolutionUSec * kClampTimesPerDigest;
334
long long extraClampedTime =
335
(aClampedTimeUSec / reducedResolution) * reducedResolution;
336
337
nsCString hashResult = cache->Get(extraClampedTime, aContextMixin);
338
339
if (hashResult.Length() != HASH_DIGEST_SIZE_BYTES) { // Cache Miss =(
340
// If someone has pased in the testing-only parameter, replace our seed with
341
// it
342
if (aSecretSeed != nullptr) {
343
StaticMutexAutoLock lock(sLock);
344
345
delete[] sSecretMidpointSeed;
346
347
sSecretMidpointSeed = new uint8_t[kSeedSize];
348
memcpy(sSecretMidpointSeed, aSecretSeed, kSeedSize);
349
}
350
351
// If we don't have a seed, we need to get one.
352
if (MOZ_UNLIKELY(!sSecretMidpointSeed)) {
353
nsCOMPtr<nsIRandomGenerator> randomGenerator =
354
do_GetService("@mozilla.org/security/random-generator;1", &rv);
355
if (NS_WARN_IF(NS_FAILED(rv))) {
356
return rv;
357
}
358
359
StaticMutexAutoLock lock(sLock);
360
if (MOZ_LIKELY(!sSecretMidpointSeed)) {
361
rv = randomGenerator->GenerateRandomBytes(kSeedSize,
362
&sSecretMidpointSeed);
363
if (NS_WARN_IF(NS_FAILED(rv))) {
364
return rv;
365
}
366
}
367
}
368
369
/*
370
* Use a cryptographicly secure hash function, but do _not_ use an HMAC.
371
* Obviously we're not using this data for authentication purposes, but
372
* even still an HMAC is a perfect fit here, as we're hashing a value
373
* using a seed that never changes, and an input that does. So why not
374
* use one?
375
*
376
* Basically - we don't need to, it's two invocations of the hash function,
377
* and speed really counts here.
378
*
379
* With authentication off the table, the properties we would get by
380
* using an HMAC here would be:
381
* - Resistence to length extension
382
* - Resistence to collision attacks on the underlying hash function
383
* - Resistence to chosen prefix attacks
384
*
385
* There is no threat of length extension here. Nor is there any real
386
* practical threat of collision: not only are we using a good hash
387
* function (you may mock me in 10 years if it is broken) but we don't
388
* provide the attacker much control over the input. Nor do we let them
389
* have the prefix.
390
*/
391
392
// Then hash extraClampedTime and store it in the cache
393
nsCOMPtr<nsICryptoHash> hasher =
394
do_CreateInstance("@mozilla.org/security/hash;1", &rv);
395
NS_ENSURE_SUCCESS(rv, rv);
396
397
rv = hasher->Init(nsICryptoHash::SHA256);
398
NS_ENSURE_SUCCESS(rv, rv);
399
400
rv = hasher->Update(sSecretMidpointSeed, kSeedSize);
401
NS_ENSURE_SUCCESS(rv, rv);
402
403
rv = hasher->Update((const uint8_t*)&aContextMixin, sizeof(aContextMixin));
404
NS_ENSURE_SUCCESS(rv, rv);
405
406
rv = hasher->Update((const uint8_t*)&extraClampedTime,
407
sizeof(extraClampedTime));
408
NS_ENSURE_SUCCESS(rv, rv);
409
410
nsAutoCStringN<HASH_DIGEST_SIZE_BYTES> derivedSecret;
411
rv = hasher->Finish(false, derivedSecret);
412
NS_ENSURE_SUCCESS(rv, rv);
413
414
// Finally, store it in the cache
415
cache->Store(extraClampedTime, aContextMixin, derivedSecret);
416
hashResult = derivedSecret;
417
}
418
419
// Offset the appropriate index into the hash output, and then turn it into a
420
// random midpoint between 0 and aResolutionUSec. Sometimes out input time is
421
// negative, we ride the negative out to the end until we start doing pointer
422
// math. (We also triple check we're in bounds.)
423
int byteOffset =
424
abs(((aClampedTimeUSec - extraClampedTime) / aResolutionUSec) * 4);
425
if (MOZ_UNLIKELY(byteOffset > (HASH_DIGEST_SIZE_BYTES - 4))) {
426
byteOffset = 0;
427
}
428
uint32_t deterministiclyRandomValue = *BitwiseCast<uint32_t*>(
429
PromiseFlatCString(hashResult).get() + byteOffset);
430
deterministiclyRandomValue %= aResolutionUSec;
431
*aMidpointOut = deterministiclyRandomValue;
432
433
return NS_OK;
434
}
435
436
/**
437
* Given a precision value, this function will reduce a given input time to the
438
* nearest multiple of that precision.
439
*
440
* It will check if it is appropriate to clamp the input time according to the
441
* values of the privacy.resistFingerprinting and privacy.reduceTimerPrecision
442
* preferences. Note that while it will check these prefs, it will use
443
* whatever precision is given to it, so if one desires a minimum precision for
444
* Resist Fingerprinting, it is the caller's responsibility to provide the
445
* correct value. This means you should pass TimerResolution(), which enforces
446
* a minimum vale on the precision based on preferences.
447
*
448
* It ensures the given precision value is greater than zero, if it is not it
449
* returns the input time.
450
*
451
* @param aTime [in] The input time to be clamped.
452
* @param aTimeScale [in] The units the input time is in (Seconds,
453
* Milliseconds, or Microseconds).
454
* @param aResolutionUSec [in] The precision (in microseconds) to clamp to.
455
* @param aContextMixin [in] An opaque random value for relative timestamps.
456
* 0 for absolute timestamps
457
* @return If clamping is appropriate, the clamped value of the
458
* input, otherwise the input.
459
*/
460
/* static */
461
double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale,
462
double aResolutionUSec,
463
int64_t aContextMixin,
464
TimerPrecisionType aType) {
465
// This boolean will serve as a flag indicating we are clamping the time
466
// unconditionally. We do this when timer reduction preference is off; but we
467
// still want to apply 20us clamping to al timestamps to avoid leaking
468
// nano-second precision.
469
bool unconditionalClamping = false;
470
if (!IsTimerPrecisionReductionEnabled(aType)) {
471
if (!StaticPrefs::privacy_reduceTimerPrecision_unconditional()) {
472
return aTime;
473
}
474
unconditionalClamping = true;
475
aResolutionUSec = RFP_TIMER_UNCONDITIONAL_VALUE; // 20 microseconds
476
aContextMixin = 0; // Just clarifies our logging statement at the end,
477
// otherwise unused
478
}
479
480
if (aResolutionUSec <= 0) {
481
return aTime;
482
}
483
484
// Increase the time as needed until it is in microseconds.
485
// Note that a double can hold up to 2**53 with integer precision. This gives
486
// us only until June 5, 2255 in time-since-the-epoch with integer precision.
487
// So we will be losing microseconds precision after that date.
488
// We think this is okay, and we codify it in some tests.
489
double timeScaled = aTime * (1000000 / aTimeScale);
490
// Cut off anything less than a microsecond.
491
long long timeAsInt = timeScaled;
492
493
// If we have a blank context mixin, this indicates we (should) have an
494
// absolute timestamp. We check the time, and if it less than a unix timestamp
495
// about 10 years in the past, we output to the log and, in debug builds,
496
// assert. This is an error case we want to understand and fix: we must have
497
// given a relative timestamp with a mixin of 0 which is incorrect. Anyone
498
// running a debug build _probably_ has an accurate clock, and if they don't,
499
// they'll hopefully find this message and understand why things are crashing.
500
const long long kFeb282008 = 1204233985000;
501
if (!unconditionalClamping && aContextMixin == 0 &&
502
aType == TimerPrecisionType::All && timeAsInt < kFeb282008) {
503
MOZ_LOG(
504
gResistFingerprintingLog, LogLevel::Error,
505
("About to assert. aTime=%lli<%lli aContextMixin=%" PRId64 " aType=%s",
506
timeAsInt, kFeb282008, aContextMixin,
507
(aType == TimerPrecisionType::RFPOnly ? "RFPOnly" : "All")));
508
MOZ_ASSERT(
509
false,
510
"ReduceTimePrecisionImpl was given a relative time "
511
"with an empty context mix-in (or your clock is 10+ years off.) "
512
"Run this with MOZ_LOG=nsResistFingerprinting:1 to get more details.");
513
}
514
515
// Cast the resolution (in microseconds) to an int.
516
long long resolutionAsInt = aResolutionUSec;
517
// Perform the clamping.
518
// We do a cast back to double to perform the division with doubles, then
519
// floor the result and the rest occurs with integer precision. This is
520
// because it gives consistency above and below zero. Above zero, performing
521
// the division in integers truncates decimals, taking the result closer to
522
// zero (a floor). Below zero, performing the division in integers truncates
523
// decimals, taking the result closer to zero (a ceil). The impact of this is
524
// that comparing two clamped values that should be related by a constant
525
// (e.g. 10s) that are across the zero barrier will no longer work. We need to
526
// round consistently towards positive infinity or negative infinity (we chose
527
// negative.) This can't be done with a truncation, it must be done with
528
// floor.
529
long long clamped =
530
floor(double(timeAsInt) / resolutionAsInt) * resolutionAsInt;
531
532
long long midpoint = 0;
533
long long clampedAndJittered = clamped;
534
if (!unconditionalClamping &&
535
StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter()) {
536
if (!NS_FAILED(RandomMidpoint(clamped, resolutionAsInt, aContextMixin,
537
&midpoint)) &&
538
timeAsInt >= clamped + midpoint) {
539
clampedAndJittered += resolutionAsInt;
540
}
541
}
542
543
// Cast it back to a double and reduce it to the correct units.
544
double ret = double(clampedAndJittered) / (1000000.0 / aTimeScale);
545
546
MOZ_LOG(
547
gResistFingerprintingLog, LogLevel::Verbose,
548
("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding %s with (%lli, "
549
"Originally %.*f), "
550
"Intermediate: (%lli), Clamped: (%lli) Jitter: (%i Context: %" PRId64
551
" Midpoint: %lli) "
552
"Final: (%lli Converted: %.*f)",
553
DBL_DIG - 1, aTime, DBL_DIG - 1, timeScaled, timeAsInt,
554
(unconditionalClamping ? "unconditionally" : "normally"),
555
resolutionAsInt, DBL_DIG - 1, aResolutionUSec,
556
(long long)floor(double(timeAsInt) / resolutionAsInt), clamped,
557
StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter(),
558
aContextMixin, midpoint, clampedAndJittered, DBL_DIG - 1, ret));
559
560
return ret;
561
}
562
563
/* static */
564
double nsRFPService::ReduceTimePrecisionAsUSecs(
565
double aTime, int64_t aContextMixin,
566
TimerPrecisionType aType /* = TimerPrecisionType::All */) {
567
return nsRFPService::ReduceTimePrecisionImpl(
568
aTime, MicroSeconds, TimerResolution(), aContextMixin, aType);
569
}
570
571
/* static */
572
double nsRFPService::ReduceTimePrecisionAsUSecsWrapper(double aTime) {
573
return nsRFPService::ReduceTimePrecisionImpl(
574
aTime, MicroSeconds, TimerResolution(),
575
0, /* For absolute timestamps (all the JS engine does), supply zero
576
context mixin */
577
TimerPrecisionType::All);
578
}
579
580
/* static */
581
double nsRFPService::ReduceTimePrecisionAsMSecs(
582
double aTime, int64_t aContextMixin,
583
TimerPrecisionType aType /* = TimerPrecisionType::All */) {
584
return nsRFPService::ReduceTimePrecisionImpl(
585
aTime, MilliSeconds, TimerResolution(), aContextMixin, aType);
586
}
587
588
/* static */
589
double nsRFPService::ReduceTimePrecisionAsSecs(
590
double aTime, int64_t aContextMixin,
591
TimerPrecisionType aType /* = TimerPrecisionType::All */) {
592
return nsRFPService::ReduceTimePrecisionImpl(
593
aTime, Seconds, TimerResolution(), aContextMixin, aType);
594
}
595
596
/* static */
597
uint32_t nsRFPService::CalculateTargetVideoResolution(uint32_t aVideoQuality) {
598
return aVideoQuality * NSToIntCeil(aVideoQuality * 16 / 9.0);
599
}
600
601
/* static */
602
uint32_t nsRFPService::GetSpoofedTotalFrames(double aTime) {
603
double precision = TimerResolution() / 1000 / 1000;
604
double time = floor(aTime / precision) * precision;
605
606
return NSToIntFloor(time * kVideoFramesPerSec);
607
}
608
609
/* static */
610
uint32_t nsRFPService::GetSpoofedDroppedFrames(double aTime, uint32_t aWidth,
611
uint32_t aHeight) {
612
uint32_t targetRes = CalculateTargetVideoResolution(
613
StaticPrefs::privacy_resistFingerprinting_target_video_res());
614
615
// The video resolution is less than or equal to the target resolution, we
616
// report a zero dropped rate for this case.
617
if (targetRes >= aWidth * aHeight) {
618
return 0;
619
}
620
621
double precision = TimerResolution() / 1000 / 1000;
622
double time = floor(aTime / precision) * precision;
623
// Bound the dropped ratio from 0 to 100.
624
uint32_t boundedDroppedRatio = std::min(kVideoDroppedRatio, 100U);
625
626
return NSToIntFloor(time * kVideoFramesPerSec *
627
(boundedDroppedRatio / 100.0));
628
}
629
630
/* static */
631
uint32_t nsRFPService::GetSpoofedPresentedFrames(double aTime, uint32_t aWidth,
632
uint32_t aHeight) {
633
uint32_t targetRes = CalculateTargetVideoResolution(
634
StaticPrefs::privacy_resistFingerprinting_target_video_res());
635
636
// The target resolution is greater than the current resolution. For this
637
// case, there will be no dropped frames, so we report total frames directly.
638
if (targetRes >= aWidth * aHeight) {
639
return GetSpoofedTotalFrames(aTime);
640
}
641
642
double precision = TimerResolution() / 1000 / 1000;
643
double time = floor(aTime / precision) * precision;
644
// Bound the dropped ratio from 0 to 100.
645
uint32_t boundedDroppedRatio = std::min(kVideoDroppedRatio, 100U);
646
647
return NSToIntFloor(time * kVideoFramesPerSec *
648
((100 - boundedDroppedRatio) / 100.0));
649
}
650
651
static uint32_t GetSpoofedVersion() {
652
// If we can't get the current Firefox version, use a hard-coded ESR version.
653
const uint32_t kKnownEsrVersion = 60;
654
655
nsresult rv;
656
nsCOMPtr<nsIXULAppInfo> appInfo =
657
do_GetService("@mozilla.org/xre/app-info;1", &rv);
658
NS_ENSURE_SUCCESS(rv, kKnownEsrVersion);
659
660
nsAutoCString appVersion;
661
rv = appInfo->GetVersion(appVersion);
662
NS_ENSURE_SUCCESS(rv, kKnownEsrVersion);
663
664
// The browser version will be spoofed as the last ESR version.
665
// By doing so, the anonymity group will cover more versions instead of one
666
// version.
667
uint32_t firefoxVersion = appVersion.ToInteger(&rv);
668
NS_ENSURE_SUCCESS(rv, kKnownEsrVersion);
669
670
#ifdef DEBUG
671
// If we are running in Firefox ESR, determine whether the formula of ESR
672
// version has changed. Once changed, we must update the formula in this
673
// function.
674
if (!strcmp(MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL), "esr")) {
675
MOZ_ASSERT(((firefoxVersion % 8) == 4),
676
"Please update ESR version formula in nsRFPService.cpp");
677
}
678
#endif // DEBUG
679
680
// Starting with Firefox 52, a new ESR version will be released every
681
// eight Firefox versions: 52, 60, 68, ...
682
// We infer the last and closest ESR version based on this rule.
683
return firefoxVersion - ((firefoxVersion - 4) % 8);
684
}
685
686
/* static */
687
void nsRFPService::GetSpoofedUserAgent(nsACString& userAgent,
688
bool isForHTTPHeader) {
689
// This function generates the spoofed value of User Agent.
690
// We spoof the values of the platform and Firefox version, which could be
691
// used as fingerprinting sources to identify individuals.
692
// Reference of the format of User Agent:
695
696
uint32_t spoofedVersion = GetSpoofedVersion();
697
const char* spoofedOS = isForHTTPHeader ? SPOOFED_HTTP_UA_OS : SPOOFED_UA_OS;
698
userAgent.Assign(nsPrintfCString(
699
"Mozilla/5.0 (%s; rv:%d.0) Gecko/%s Firefox/%d.0", spoofedOS,
700
spoofedVersion, LEGACY_UA_GECKO_TRAIL, spoofedVersion));
701
}
702
703
static const char* gCallbackPrefs[] = {
704
RESIST_FINGERPRINTING_PREF, RFP_TIMER_PREF,
705
RFP_TIMER_UNCONDITIONAL_PREF, RFP_TIMER_VALUE_PREF,
706
RFP_JITTER_VALUE_PREF, nullptr,
707
};
708
709
nsresult nsRFPService::Init() {
710
MOZ_ASSERT(NS_IsMainThread());
711
712
nsresult rv;
713
714
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
715
NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE);
716
717
rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
718
NS_ENSURE_SUCCESS(rv, rv);
719
720
#if defined(XP_WIN)
721
rv = obs->AddObserver(this, PROFILE_INITIALIZED_TOPIC, false);
722
NS_ENSURE_SUCCESS(rv, rv);
723
#endif
724
725
Preferences::RegisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs,
726
this);
727
728
// We backup the original TZ value here.
729
const char* tzValue = PR_GetEnv("TZ");
730
if (tzValue != nullptr) {
731
mInitialTZValue = nsCString(tzValue);
732
}
733
734
// Call Update here to cache the values of the prefs and set the timezone.
735
UpdateRFPPref();
736
737
// Create the LRU Cache when we initialize, to avoid accidently trying to
738
// create it (and call ClearOnShutdown) on a non-main-thread
739
if (sCache == nullptr) {
740
sCache = new LRUCache();
741
}
742
743
return rv;
744
}
745
746
// This function updates only timing-related fingerprinting items
747
void nsRFPService::UpdateTimers() {
748
MOZ_ASSERT(NS_IsMainThread());
749
750
if (StaticPrefs::privacy_resistFingerprinting() ||
751
StaticPrefs::privacy_reduceTimerPrecision()) {
752
JS::SetTimeResolutionUsec(
753
TimerResolution(),
754
StaticPrefs::
755
privacy_resistFingerprinting_reduceTimerPrecision_jitter());
756
JS::SetReduceMicrosecondTimePrecisionCallback(
757
nsRFPService::ReduceTimePrecisionAsUSecsWrapper);
758
} else if (StaticPrefs::privacy_reduceTimerPrecision_unconditional()) {
759
JS::SetTimeResolutionUsec(RFP_TIMER_UNCONDITIONAL_VALUE, false);
760
JS::SetReduceMicrosecondTimePrecisionCallback(
761
nsRFPService::ReduceTimePrecisionAsUSecsWrapper);
762
} else if (sInitialized) {
763
JS::SetTimeResolutionUsec(0, false);
764
}
765
}
766
767
// This function updates every fingerprinting item necessary except
768
// timing-related
769
void nsRFPService::UpdateRFPPref() {
770
MOZ_ASSERT(NS_IsMainThread());
771
772
UpdateTimers();
773
774
bool privacyResistFingerprinting =
775
StaticPrefs::privacy_resistFingerprinting();
776
if (privacyResistFingerprinting) {
777
PR_SetEnv("TZ=UTC");
778
} else if (sInitialized) {
779
// We will not touch the TZ value if 'privacy.resistFingerprinting' is false
780
// during the time of initialization.
781
if (!mInitialTZValue.IsEmpty()) {
782
nsAutoCString tzValue = NS_LITERAL_CSTRING("TZ=") + mInitialTZValue;
783
static char* tz = nullptr;
784
785
// If the tz has been set before, we free it first since it will be
786
// allocated a new value later.
787
if (tz != nullptr) {
788
free(tz);
789
}
790
// PR_SetEnv() needs the input string been leaked intentionally, so
791
// we copy it here.
792
tz = ToNewCString(tzValue);
793
if (tz != nullptr) {
794
PR_SetEnv(tz);
795
}
796
} else {
797
#if defined(XP_WIN)
798
// For Windows, we reset the TZ to an empty string. This will make Windows
799
// to use its system timezone.
800
PR_SetEnv("TZ=");
801
#else
802
// For POSIX like system, we reset the TZ to the /etc/localtime, which is
803
// the system timezone.
804
PR_SetEnv("TZ=:/etc/localtime");
805
#endif
806
}
807
}
808
809
// If and only if the time zone was changed above, propagate the change to the
810
// <time.h> functions and the JS runtime.
811
if (privacyResistFingerprinting || sInitialized) {
812
// localtime_r (and other functions) may not call tzset, so do this here
813
// after changing TZ to ensure all <time.h> functions use the new time zone.
814
#if defined(XP_WIN)
815
_tzset();
816
#else
817
tzset();
818
#endif
819
820
nsJSUtils::ResetTimeZone();
821
}
822
}
823
824
void nsRFPService::StartShutdown() {
825
MOZ_ASSERT(NS_IsMainThread());
826
827
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
828
829
StaticMutexAutoLock lock(sLock);
830
{ sCache = nullptr; }
831
832
if (obs) {
833
obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
834
}
835
Preferences::UnregisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs,
836
this);
837
}
838
839
/* static */
840
void nsRFPService::MaybeCreateSpoofingKeyCodes(const KeyboardLangs aLang,
841
const KeyboardRegions aRegion) {
842
if (sSpoofingKeyboardCodes == nullptr) {
843
sSpoofingKeyboardCodes =
844
new nsDataHashtable<KeyboardHashKey, const SpoofingKeyboardCode*>();
845
}
846
847
if (KeyboardLang::EN == aLang) {
848
switch (aRegion) {
849
case KeyboardRegion::US:
850
MaybeCreateSpoofingKeyCodesForEnUS();
851
break;
852
}
853
}
854
}
855
856
/* static */
857
void nsRFPService::MaybeCreateSpoofingKeyCodesForEnUS() {
858
MOZ_ASSERT(sSpoofingKeyboardCodes);
859
860
static bool sInitialized = false;
861
const KeyboardLangs lang = KeyboardLang::EN;
862
const KeyboardRegions reg = KeyboardRegion::US;
863
864
if (sInitialized) {
865
return;
866
}
867
868
static const SpoofingKeyboardInfo spoofingKeyboardInfoTable[] = {
869
#define KEY(key_, _codeNameIdx, _keyCode, _modifier) \
870
{KEY_NAME_INDEX_USE_STRING, \
871
NS_LITERAL_STRING(key_), \
872
{CODE_NAME_INDEX_##_codeNameIdx, _keyCode, _modifier}},
873
#define CONTROL(keyNameIdx_, _codeNameIdx, _keyCode) \
874
{KEY_NAME_INDEX_##keyNameIdx_, \
875
EmptyString(), \
876
{CODE_NAME_INDEX_##_codeNameIdx, _keyCode, MODIFIER_NONE}},
877
#include "KeyCodeConsensus_En_US.h"
878
#undef CONTROL
879
#undef KEY
880
};
881
882
for (const auto& keyboardInfo : spoofingKeyboardInfoTable) {
883
KeyboardHashKey key(lang, reg, keyboardInfo.mKeyIdx, keyboardInfo.mKey);
884
MOZ_ASSERT(!sSpoofingKeyboardCodes->Lookup(key),
885
"Double-defining key code; fix your KeyCodeConsensus file");
886
sSpoofingKeyboardCodes->Put(key, &keyboardInfo.mSpoofingCode);
887
}
888
889
sInitialized = true;
890
}
891
892
/* static */
893
void nsRFPService::GetKeyboardLangAndRegion(const nsAString& aLanguage,
894
KeyboardLangs& aLocale,
895
KeyboardRegions& aRegion) {
896
nsAutoString langStr;
897
nsAutoString regionStr;
898
uint32_t partNum = 0;
899
900
for (const nsAString& part : aLanguage.Split('-')) {
901
if (partNum == 0) {
902
langStr = part;
903
} else {
904
regionStr = part;
905
break;
906
}
907
908
partNum++;
909
}
910
911
// We test each language here as well as the region. There are some cases that
912
// only the language is given, we will use the default region code when this
913
// happens. The default region should depend on the given language.
914
if (langStr.EqualsLiteral(RFP_KEYBOARD_LANG_STRING_EN)) {
915
aLocale = KeyboardLang::EN;
916
// Give default values first.
917
aRegion = KeyboardRegion::US;
918
919
if (regionStr.EqualsLiteral(RFP_KEYBOARD_REGION_STRING_US)) {
920
aRegion = KeyboardRegion::US;
921
}
922
} else {
923
// There is no spoofed keyboard locale for the given language. We use the
924
// default one in this case.
925
aLocale = RFP_DEFAULT_SPOOFING_KEYBOARD_LANG;
926
aRegion = RFP_DEFAULT_SPOOFING_KEYBOARD_REGION;
927
}
928
}
929
930
/* static */
931
bool nsRFPService::GetSpoofedKeyCodeInfo(
932
const dom::Document* aDoc, const WidgetKeyboardEvent* aKeyboardEvent,
933
SpoofingKeyboardCode& aOut) {
934
MOZ_ASSERT(aKeyboardEvent);
935
936
KeyboardLangs keyboardLang = RFP_DEFAULT_SPOOFING_KEYBOARD_LANG;
937
KeyboardRegions keyboardRegion = RFP_DEFAULT_SPOOFING_KEYBOARD_REGION;
938
// If the document is given, we use the content language which is get from the
939
// document. Otherwise, we use the default one.
940
if (aDoc != nullptr) {
941
nsAutoString language;
942
aDoc->GetContentLanguage(language);
943
944
// If the content-langauge is not given, we try to get langauge from the
945
// HTML lang attribute.
946
if (language.IsEmpty()) {
947
dom::Element* elm = aDoc->GetHtmlElement();
948
949
if (elm != nullptr) {
950
elm->GetLang(language);
951
}
952
}
953
954
// If two or more languages are given, per HTML5 spec, we should consider
955
// it as 'unknown'. So we use the default one.
956
if (!language.IsEmpty() && !language.Contains(char16_t(','))) {
957
language.StripWhitespace();
958
GetKeyboardLangAndRegion(language, keyboardLang, keyboardRegion);
959
}
960
}
961
962
MaybeCreateSpoofingKeyCodes(keyboardLang, keyboardRegion);
963
964
KeyNameIndex keyIdx = aKeyboardEvent->mKeyNameIndex;
965
nsAutoString keyName;
966
967
if (keyIdx == KEY_NAME_INDEX_USE_STRING) {
968
keyName = aKeyboardEvent->mKeyValue;
969
}
970
971
KeyboardHashKey key(keyboardLang, keyboardRegion, keyIdx, keyName);
972
const SpoofingKeyboardCode* keyboardCode = sSpoofingKeyboardCodes->Get(key);
973
974
if (keyboardCode != nullptr) {
975
aOut = *keyboardCode;
976
return true;
977
}
978
979
return false;
980
}
981
982
/* static */
983
bool nsRFPService::GetSpoofedModifierStates(
984
const dom::Document* aDoc, const WidgetKeyboardEvent* aKeyboardEvent,
985
const Modifiers aModifier, bool& aOut) {
986
MOZ_ASSERT(aKeyboardEvent);
987
988
// For modifier or control keys, we don't need to hide its modifier states.
989
if (aKeyboardEvent->mKeyNameIndex != KEY_NAME_INDEX_USE_STRING) {
990
return false;
991
}
992
993
// We will spoof the modifer state for Alt, Shift, and AltGraph.
994
// We don't spoof the Control key, because it is often used
995
// for command key combinations in web apps.
996
if ((aModifier & (MODIFIER_ALT | MODIFIER_SHIFT | MODIFIER_ALTGRAPH)) != 0) {
997
SpoofingKeyboardCode keyCodeInfo;
998
999
if (GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) {
1000
aOut = ((keyCodeInfo.mModifierStates & aModifier) != 0);
1001
return true;
1002
}
1003
}
1004
1005
return false;
1006
}
1007
1008
/* static */
1009
bool nsRFPService::GetSpoofedCode(const dom::Document* aDoc,
1010
const WidgetKeyboardEvent* aKeyboardEvent,
1011
nsAString& aOut) {
1012
MOZ_ASSERT(aKeyboardEvent);
1013
1014
SpoofingKeyboardCode keyCodeInfo;
1015
1016
if (!GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) {
1017
return false;
1018
}
1019
1020
WidgetKeyboardEvent::GetDOMCodeName(keyCodeInfo.mCode, aOut);
1021
1022
// We need to change the 'Left' with 'Right' if the location indicates
1023
// it's a right key.
1024
if (aKeyboardEvent->mLocation ==
1025
dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_RIGHT &&
1026
StringEndsWith(aOut, NS_LITERAL_STRING("Left"))) {
1027
aOut.ReplaceLiteral(aOut.Length() - 4, 4, u"Right");
1028
}
1029
1030
return true;
1031
}
1032
1033
/* static */
1034
bool nsRFPService::GetSpoofedKeyCode(const dom::Document* aDoc,
1035
const WidgetKeyboardEvent* aKeyboardEvent,
1036
uint32_t& aOut) {
1037
MOZ_ASSERT(aKeyboardEvent);
1038
1039
SpoofingKeyboardCode keyCodeInfo;
1040
1041
if (GetSpoofedKeyCodeInfo(aDoc, aKeyboardEvent, keyCodeInfo)) {
1042
aOut = keyCodeInfo.mKeyCode;
1043
return true;
1044
}
1045
1046
return false;
1047
}
1048
1049
// static
1050
void nsRFPService::PrefChanged(const char* aPref, void* aSelf) {
1051
static_cast<nsRFPService*>(aSelf)->PrefChanged(aPref);
1052
}
1053
1054
void nsRFPService::PrefChanged(const char* aPref) {
1055
nsDependentCString pref(aPref);
1056
1057
if (pref.EqualsLiteral(RFP_TIMER_PREF) ||
1058
pref.EqualsLiteral(RFP_TIMER_UNCONDITIONAL_PREF) ||
1059
pref.EqualsLiteral(RFP_TIMER_VALUE_PREF) ||
1060
pref.EqualsLiteral(RFP_JITTER_VALUE_PREF)) {
1061
UpdateTimers();
1062
} else if (pref.EqualsLiteral(RESIST_FINGERPRINTING_PREF)) {
1063
UpdateRFPPref();
1064
1065
#if defined(XP_WIN)
1066
if (!XRE_IsE10sParentProcess()) {
1067
// Windows does not follow POSIX. Updates to the TZ environment variable
1068
// are not reflected immediately on that platform as they are on UNIX
1069
// systems without this call.
1070
_tzset();
1071
}
1072
#endif
1073
}
1074
}
1075
1076
NS_IMETHODIMP
1077
nsRFPService::Observe(nsISupports* aObject, const char* aTopic,
1078
const char16_t* aMessage) {
1079
if (strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, aTopic) == 0) {
1080
StartShutdown();
1081
}
1082
#if defined(XP_WIN)
1083
else if (!strcmp(PROFILE_INITIALIZED_TOPIC, aTopic)) {
1084
// If we're e10s, then we don't need to run this, since the child process
1085
// will simply inherit the environment variable from the parent process, in
1086
// which case it's unnecessary to call _tzset().
1087
if (XRE_IsParentProcess() && !XRE_IsE10sParentProcess()) {
1088
// Windows does not follow POSIX. Updates to the TZ environment variable
1089
// are not reflected immediately on that platform as they are on UNIX
1090
// systems without this call.
1091
_tzset();
1092
}
1093
1094
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
1095
NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE);
1096
1097
nsresult rv = obs->RemoveObserver(this, PROFILE_INITIALIZED_TOPIC);
1098
NS_ENSURE_SUCCESS(rv, rv);
1099
}
1100
#endif
1101
1102
return NS_OK;
1103
}