Files
MrbWebLibre/apps/weblibre/lib/features/onboarding/presentation/pages/privacy_hardening.dart
T

335 lines
12 KiB
Dart

/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
import 'package:weblibre/features/geckoview/features/tabs/utils/setting_groups_serializer.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/user/data/models/engine_settings.dart';
import 'package:weblibre/features/user/domain/entities/fingerprint_overrides.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
import 'package:weblibre/presentation/widgets/browser_page.dart';
class PrivacyHardeningPage extends ConsumerWidget {
const PrivacyHardeningPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return BrowserPage(
child: BrowserPageContent(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
Center(
child: Text(
'Privacy & Hardening',
style: theme.textTheme.headlineMedium,
),
),
const SizedBox(height: 24),
const _LanguageFingerprintWarning(),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Browser Languages'),
subtitle: const Text(
'Configure language preferences exposed to websites',
),
leading: const Icon(Icons.translate),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await LocaleSettingsRoute().push(context);
},
),
const SizedBox(height: 16),
const _WebEngineHardeningToggle(),
const SizedBox(height: 16),
const _FingerprintProtectionToggle(),
const SizedBox(height: 16),
const _LocalNetworkAccessSection(),
],
),
),
);
}
}
class _LanguageFingerprintWarning extends ConsumerWidget {
const _LanguageFingerprintWarning();
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final configuredLocales = ref.watch(
engineSettingsWithDefaultsProvider.select((s) => s.locales),
);
if (configuredLocales.length <= 1) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24.0),
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: theme.colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onErrorContainer,
height: 1.4,
),
children: [
TextSpan(
text: 'Multiple Languages Detected\n\n',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onErrorContainer,
fontWeight: FontWeight.w600,
),
),
const TextSpan(text: '• '),
TextSpan(
text:
'Your browser has ${configuredLocales.length} languages '
'configured (${configuredLocales.join(', ')}). '
'Websites can use your unique language combination to '
'fingerprint and track you across the web.\n\n',
),
const TextSpan(text: '• '),
const TextSpan(
text:
'Consider reducing your browser languages to a single '
'language to minimize your fingerprint surface.',
),
],
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: theme.colorScheme.onErrorContainer,
foregroundColor: theme.colorScheme.errorContainer,
),
onPressed: () async {
await LocaleSettingsRoute().push(context);
},
icon: const Icon(Icons.translate),
label: const Text('Review Languages'),
),
),
],
),
);
}
}
class _WebEngineHardeningToggle extends ConsumerWidget {
const _WebEngineHardeningToggle();
@override
Widget build(BuildContext context, WidgetRef ref) {
final preferenceGroups = ref.watch(
unifiedPreferenceSettingsRepositoryProvider(PreferencePartition.user),
);
final allGroupsActive = preferenceGroups.maybeWhen(
data: (data) =>
data.values.every((element) => element.isActiveOrOptional),
orElse: () => false,
);
return SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text('Complete Web Engine Hardening'),
subtitle: const Text(
'Apply all recommended security hardening preferences to the web engine',
),
secondary: const Icon(MdiIcons.shieldLock),
value: allGroupsActive,
onChanged: preferenceGroups.hasValue
? (value) async {
final notifier = ref.read(
unifiedPreferenceSettingsRepositoryProvider(
PreferencePartition.user,
).notifier,
);
if (value) {
await notifier.apply();
} else {
await notifier.reset();
}
}
: null,
);
}
}
class _FingerprintProtectionToggle extends ConsumerWidget {
const _FingerprintProtectionToggle();
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final currentOverrides = ref.watch(
engineSettingsWithDefaultsProvider.select(
(s) => s.fingerprintingProtectionOverrides,
),
);
final isHardened =
currentOverrides == FingerprintOverrides.hardenedDefaults().toString();
return Column(
children: [
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text('Hardened Fingerprint Protection'),
subtitle: const Text(
'Load comprehensive fingerprint protection defaults',
),
secondary: const Icon(MdiIcons.fingerprint),
value: isHardened,
onChanged: (value) async {
final overrides = value
? FingerprintOverrides.hardenedDefaults()
: FingerprintOverrides.defaults();
await ref
.read(saveEngineSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.fingerprintingProtectionOverrides(overrides.toString()),
);
},
),
AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.topCenter,
child: isHardened
? Container(
width: double.infinity,
padding: const EdgeInsets.all(24.0),
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: theme.colorScheme.error,
borderRadius: BorderRadius.circular(8.0),
),
child: Text.rich(
TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onError,
height: 1.4,
),
children: [
TextSpan(
text: 'Compatibility Warning\n\n',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onError,
fontWeight: FontWeight.w600,
),
),
const TextSpan(text: '• '),
const TextSpan(
text:
'Hardened fingerprint protection enables 60+ '
'protection targets including canvas '
'randomization, navigator spoofing, media device '
'masking, and more.\n\n',
),
const TextSpan(text: '• '),
const TextSpan(
text:
'This may cause websites to break or behave '
'unexpectedly. You can fine-tune individual '
'targets in settings.',
),
],
),
),
)
: const SizedBox.shrink(),
),
],
);
}
}
class _LocalNetworkAccessSection extends ConsumerWidget {
const _LocalNetworkAccessSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final lnaBlocking = ref.watch(
engineSettingsWithDefaultsProvider.select((s) => s.lnaBlocking),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Local Network Protection'),
subtitle: Text(
'Websites can try to reach your device and other devices '
'on your home network, like routers, printers, or smart home '
'devices. By default, known trackers are automatically blocked '
'from doing this.',
),
leading: Icon(MdiIcons.lanDisconnect),
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text('Block All Local Network Requests'),
subtitle: const Text(
'Ask for permission before any website accesses devices '
'on your home network, not just known trackers',
),
secondary: const Icon(MdiIcons.shieldLockOpen),
value: lnaBlocking ?? false,
onChanged: (value) async {
await ref
.read(saveEngineSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.lnaBlocking(value),
);
},
),
],
);
}
}