Files
sepot/app/lib/providers/settings_provider.dart
T
Julius Joska 2f6b542f24 fix(security): require a fresh PIN or biometric check for sensitive actions
An unlocked phone was enough to read the 24-word recovery phrase. The
phrase, backup export and import, the duress PIN, a longer auto-lock,
turning off screen security, enabling biometrics and deleting the
account now ask for the PIN (or biometrics with biometricOnly) every
time. The duress session never shows the phrase. PIN checks outside the
lock screen count towards the same failure throttle.
2026-10-23 10:50:16 +02:00

169 lines
5.9 KiB
Dart

import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_protector/screen_protector.dart';
import 'package:sepot/core/backup/backup_service.dart';
import 'package:sepot/core/services/messaging_service.dart';
import 'package:sepot/providers/messaging_provider.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Služba šifrovaných záloh historie.
final backupServiceProvider = Provider<BackupService>(
(ref) => BackupService(ref.read(storageProvider)));
/// Je nastavený nátlakový (decoy) PIN?
final hasDecoyPinProvider = FutureProvider<bool>(
(ref) => ref.read(storageProvider).hasDecoyPin());
/// Potvrzení přečtení (read receipts). Default VYPNUTO — soukromí.
/// Je reciproční: když je vypnuté, appka je neposílá ani nezobrazuje.
class ReadReceiptsNotifier extends AsyncNotifier<bool> {
static const key = 'read_receipts_enabled';
@override
Future<bool> build() async {
final storage = ref.read(storageProvider);
return (await storage.getSetting<bool>(key)) ?? false;
}
Future<void> set(bool value) async {
await ref.read(storageProvider).setSetting(key, value);
state = AsyncData(value);
}
}
final readReceiptsProvider =
AsyncNotifierProvider<ReadReceiptsNotifier, bool>(ReadReceiptsNotifier.new);
/// „píše…“ — stejná pravidla jako potvrzení přečtení: výchozí vypnuto,
/// reciproční (kdo nesdílí, nevidí).
class TypingIndicatorsNotifier extends AsyncNotifier<bool> {
@override
Future<bool> build() async {
final storage = ref.read(storageProvider);
return (await storage.getSetting<bool>(
MessagingService.typingSettingKey)) ??
false;
}
Future<void> set(bool value) async {
await ref
.read(storageProvider)
.setSetting(MessagingService.typingSettingKey, value);
state = AsyncData(value);
}
}
final typingIndicatorsProvider =
AsyncNotifierProvider<TypingIndicatorsNotifier, bool>(
TypingIndicatorsNotifier.new);
/// Zabezpečení obrazovky: blok screenshotů (Android FLAG_SECURE) + skrytí
/// obsahu v přepínači appek (iOS). Výchozí ZAPNUTO jako u Signalu — stejné
/// jako nativní default v MainActivity, takže přepínač už neukazuje
/// „vypnuto" u obrazovky, která chráněná je.
class ScreenSecurityNotifier extends AsyncNotifier<bool> {
static const key = 'screen_security_enabled';
// Debug build bez ochrany: QA na emulátoru potřebuje screenshoty.
static const vychozi = !kDebugMode;
@override
Future<bool> build() async {
final storage = ref.read(storageProvider);
// Před odemčením se nastavení přečíst nedá; nic neaplikovat (nativní
// default platí) a po unlocku se provider invaliduje.
if (!storage.isUnlocked) return vychozi;
final v = (await storage.getSetting<bool>(key)) ?? vychozi;
await _apply(v);
return v;
}
Future<void> set(bool value) async {
await ref.read(storageProvider).setSetting(key, value);
await _apply(value);
state = AsyncData(value);
}
/// Po dobu [body] drží ochranu zapnutou bez ohledu na nastavení a pak
/// vrátí uložený stav. Pro 24 slov: vypnutý přepínač nesmí pustit frázi
/// do screenshotu ani do náhledu v přepínači aplikací. Ladicí build
/// výjimku má stejně jako MainActivity — QA potřebuje snímky z emulátoru.
Future<T> enforceWhile<T>(Future<T> Function() body) async {
if (kDebugMode) return body();
await _apply(true);
try {
return await body();
} finally {
await _apply(state.valueOrNull ?? vychozi);
}
}
Future<void> _apply(bool on) async {
try {
if (on) {
await ScreenProtector.preventScreenshotOn();
await ScreenProtector.protectDataLeakageOn();
} else {
await ScreenProtector.preventScreenshotOff();
await ScreenProtector.protectDataLeakageOff();
}
} catch (_) {
// Nepodporovaná platforma / kanál — bezpečně ignoruj.
}
}
}
final screenSecurityProvider =
AsyncNotifierProvider<ScreenSecurityNotifier, bool>(
ScreenSecurityNotifier.new);
/// Automatický zámek: po kolika sekundách na pozadí se appka zamkne.
/// `0` = nikdy. Do 0.3.0 dlaždice tvrdila „5 minut", ale nezamykalo se
/// vůbec (jen studený start) — což je horší než žádná funkce.
///
/// Hodnota žije v šifrované DB, takže se čte jen s odemčeným úložištěm.
class AutoLockNotifier extends AsyncNotifier<int> {
static const key = 'auto_lock_seconds';
static const vychozi = 300;
/// Nabídka v UI (sekundy). Popisky skládá obrazovka z lokalizace
/// (settingsAutoLockNever/1min/5min/15min) — provider texty nemá.
static const moznostiSekundy = [0, 60, 300, 900];
@override
Future<int> build() async {
final storage = ref.read(storageProvider);
if (!storage.isUnlocked) return vychozi;
return (await storage.getSetting<int>(key)) ?? vychozi;
}
Future<void> set(int seconds) async {
await ref.read(storageProvider).setSetting(key, seconds);
state = AsyncData(seconds);
}
}
final autoLockProvider =
AsyncNotifierProvider<AutoLockNotifier, int>(AutoLockNotifier.new);
/// Routování přes Tor (SOCKS5/Orbot na `.onion`). Default VYPNUTO —
/// přímé WSS je rychlejší a nevyžaduje Orbot. Zapnutí skryje IP klienta
/// před relayem. Hodnota žije v šifrované DB.
class TorNotifier extends AsyncNotifier<bool> {
static const key = 'tor_enabled';
@override
Future<bool> build() async {
final storage = ref.read(storageProvider);
if (!storage.isUnlocked) return false;
return (await storage.getSetting<bool>(key)) ?? false;
}
Future<void> set(bool value) async {
// Uloží volbu a přestaví běžící spojení na .onion / zpět.
await ref.read(messagingServiceProvider).setTorEnabled(value);
state = AsyncData(value);
}
}
final torProvider = AsyncNotifierProvider<TorNotifier, bool>(TorNotifier.new);