add fingerprinting configuration

This commit is contained in:
Fabian Freund
2025-10-11 14:52:09 +02:00
parent 723f22e04c
commit 36a67ee09a
24 changed files with 642 additions and 10 deletions
@@ -0,0 +1,174 @@
import 'package:exceptions/exceptions.dart';
import 'package:fast_equatable/fast_equatable.dart';
class FingerprintOverrides with FastEquatable {
static final pattern = RegExp('([+-])([a-zA-Z_][a-zA-Z0-9_]{1,64})');
final bool? allTargets;
final Map<String, bool> targets;
FingerprintOverrides(this.allTargets, this.targets);
//Monitor https://searchfox.org/firefox-main/source/toolkit/components/resistfingerprinting/RFPTargetsDefault.inc
FingerprintOverrides.defaults()
: this(false, {
'CanvasRandomization': true,
'EfficientCanvasRandomization': true,
'FontVisibilityLangPack': true,
'JSMathFdlibm': true,
'ScreenAvailToResolution': true,
'NavigatorHWConcurrencyTiered': true,
'MaxTouchPointsCollapse': true,
});
FingerprintOverrides.hardenedDefaults()
: this(false, {
'TouchEvents': true,
'PointerEvents': true,
'KeyboardEvents': true,
'ScreenOrientation': true,
'SpeechSynthesis': true,
'CSSPrefersReducedMotion': true,
'CSSPrefersContrast': true,
'CanvasRandomization': true,
'CanvasExtractionFromThirdPartiesIsBlocked': true,
'JSLocale': true,
'NavigatorAppVersion': true,
'NavigatorBuildID': true,
'NavigatorHWConcurrency': true,
'NavigatorOscpu': true,
'NavigatorPlatform': true,
'NavigatorUserAgent': true,
'PointerId': true,
'StreamVideoFacingMode': true,
'JSDateTimeUTC': true,
'JSMathFdlibm': true,
'Gamepad': true,
'HttpUserAgent': true,
'WindowOuterSize': true,
'WindowScreenXY': true,
'WindowInnerScreenXY': true,
'ScreenPixelDepth': true,
'ScreenRect': true,
'ScreenAvailRect': true,
'VideoElementMozFrames': true,
'VideoElementMozFrameDelay': true,
'VideoElementPlaybackQuality': true,
'ReduceTimerPrecision': true,
'WidgetEvents': true,
'MediaDevices': true,
'MediaCapabilities': true,
'AudioSampleRate': true,
'NetworkConnection': true,
'WindowDevicePixelRatio': true,
'MouseEventScreenPoint': true,
'FontVisibilityBaseSystem': true,
'FontVisibilityLangPack': true,
'DeviceSensors': true,
'RoundWindowSize': true,
'UseStandinsForNativeColors': true,
'AudioContext': true,
'MediaError': true,
'DOMStyleOsxFontSmoothing': true,
'CSSDeviceSize': true,
'CSSColorInfo': true,
'CSSResolution': true,
'CSSPrefersReducedTransparency': true,
'CSSInvertedColors': true,
'CSSVideoDynamicRange': true,
'CSSPointerCapabilities': true,
'WebGLRenderCapability': true,
'WebGLRenderInfo': true,
'SiteSpecificZoom': true,
'FontVisibilityRestrictGenerics': true,
'WebVTT': true,
'WebGPULimits': true,
'WebGPUIsFallbackAdapter': true,
'WebGPUSubgroupSizes': true,
'JSLocalePrompt': true,
'ScreenAvailToResolution': true,
'UseHardcodedFontSubstitutes': true,
'DiskStorageLimit': true,
'WebCodecs': true,
'MaxTouchPoints': true,
'MaxTouchPointsCollapse': true,
'NavigatorHWConcurrencyTiered': true,
});
static Result<FingerprintOverrides> parse(
String input,
Set<String> availableTargets,
) {
final cleaned = input.replaceAll(RegExp(r'\s'), '');
if (cleaned.isEmpty) return Result.success(FingerprintOverrides(null, {}));
bool? allTargets;
final targets = <String, bool>{};
for (final word in cleaned.split(',')) {
final match = pattern.firstMatch(word);
if (match == null) {
return Result.failure(
const ErrorMessage(source: 'FpParser', message: 'Invalid Override'),
);
}
final enabled = match.group(1) != '-';
final name = match.group(2)!;
if (name == 'AllTargets') {
allTargets = enabled;
continue;
}
if (!availableTargets.contains(name)) {
return Result.failure(
const ErrorMessage(
source: 'FpParser',
message: 'Invalid target name',
),
);
}
targets[name] = enabled;
}
return Result.success(FingerprintOverrides(allTargets, targets));
}
@override
String toString() {
final sb = StringBuffer();
if (allTargets != null) {
sb.write('${allTargets! ? '+' : '-'}AllTargets');
if (targets.isNotEmpty) {
sb.write(',');
}
}
sb.write(
targets.entries.map((e) => (e.value ? '+' : '-') + e.key).join(','),
);
return sb.toString();
}
FingerprintOverrides copyWithAllTargetsEnabled(bool value) {
return FingerprintOverrides(
value,
Map.fromEntries(targets.entries.where((e) => e.value != value)),
);
}
FingerprintOverrides copyWithTarget(String name, bool value) {
if (value && allTargets == true) {
return this;
}
return FingerprintOverrides(allTargets, {...targets, name: value});
}
@override
List<Object?> get hashParameters => [allTargets, targets];
}
@@ -17,11 +17,16 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:exceptions/exceptions.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:nullability/nullability.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/user/data/providers.dart';
import 'package:weblibre/features/user/domain/entities/fingerprint_overrides.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/features/user/domain/services/fingerprinting.dart';
part 'providers.g.dart';
@@ -47,3 +52,24 @@ bool incognitoModeEnabled(Ref ref) {
),
);
}
@Riverpod()
Future<Result<FingerprintOverrides>> fingerprintOverrideSettings(
Ref ref,
) async {
final fingerprintTargets = await ref.watch(fingerprintTargetsProvider.future);
final fingerprintTargetSet = fingerprintTargets.map((e) => e.name).toSet();
final overrides = ref.watch(
engineSettingsWithDefaultsProvider.select(
(settings) =>
settings.fingerprintingProtectionOverrides.mapNotNull(
(settings) =>
FingerprintOverrides.parse(settings, fingerprintTargetSet),
) ??
Result.success(FingerprintOverrides.defaults()),
),
);
return overrides;
}
@@ -117,3 +117,46 @@ final class IncognitoModeEnabledProvider
String _$incognitoModeEnabledHash() =>
r'36957b70a5261f9d3ad228e07cc8dd5c8f616082';
@ProviderFor(fingerprintOverrideSettings)
const fingerprintOverrideSettingsProvider =
FingerprintOverrideSettingsProvider._();
final class FingerprintOverrideSettingsProvider
extends
$FunctionalProvider<
AsyncValue<Result<FingerprintOverrides>>,
Result<FingerprintOverrides>,
FutureOr<Result<FingerprintOverrides>>
>
with
$FutureModifier<Result<FingerprintOverrides>>,
$FutureProvider<Result<FingerprintOverrides>> {
const FingerprintOverrideSettingsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'fingerprintOverrideSettingsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$fingerprintOverrideSettingsHash();
@$internal
@override
$FutureProviderElement<Result<FingerprintOverrides>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<Result<FingerprintOverrides>> create(Ref ref) {
return fingerprintOverrideSettings(ref);
}
}
String _$fingerprintOverrideSettingsHash() =>
r'd4d40ec425098fb1f5a2f0c4944f058829a41a0a';
@@ -114,6 +114,11 @@ class EngineSettingsRepository extends _$EngineSettingsRepository {
DriftSqlType.string,
db.typeMapping,
),
'fingerprintingProtectionOverrides':
settings['fingerprintingProtectionOverrides']?.readAs(
DriftSqlType.string,
db.typeMapping,
),
});
}
@@ -34,7 +34,7 @@ final class EngineSettingsRepositoryProvider
}
String _$engineSettingsRepositoryHash() =>
r'5e403757036edb4ef964b01cdcf96cbcbdc4d809';
r'33acd89e783d8d91b9b56e86411fdfcab24a2c54';
abstract class _$EngineSettingsRepository
extends $StreamNotifier<EngineSettings> {
@@ -0,0 +1,20 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/user/data/models/rfp_target.dart';
part 'fingerprinting.g.dart';
@Riverpod(keepAlive: true)
Future<List<RFPTarget>> fingerprintTargets(Ref ref) async {
final json =
await rootBundle
.loadString('assets/preferences/rfp_targets.json')
.then(jsonDecode)
as List<dynamic>;
return json
.map((e) => RFPTarget.fromJson(e as Map<String, dynamic>))
.toList();
}
@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'fingerprinting.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(fingerprintTargets)
const fingerprintTargetsProvider = FingerprintTargetsProvider._();
final class FingerprintTargetsProvider
extends
$FunctionalProvider<
AsyncValue<List<RFPTarget>>,
List<RFPTarget>,
FutureOr<List<RFPTarget>>
>
with $FutureModifier<List<RFPTarget>>, $FutureProvider<List<RFPTarget>> {
const FingerprintTargetsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'fingerprintTargetsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$fingerprintTargetsHash();
@$internal
@override
$FutureProviderElement<List<RFPTarget>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<RFPTarget>> create(Ref ref) {
return fingerprintTargets(ref);
}
}
String _$fingerprintTargetsHash() =>
r'1ec5933a82941b84fdad2130ccf1ba2156ddc33a';