Files
sepot/app/lib/providers/settings_provider.dart
T
Julius Joska e3a2378c34 fix(app): accept incoming calls, restore from recovery phrase, duress session isolation, message loss
Calls: `call` was missing from the known content types, so every offer was
acknowledged and dropped; the call screen column now fills the width (the
avatar jumped left after hang-up); a ring started from a push ends after
12 s when no offer arrives; STUN fallback when no TURN credentials.
Registration: X-Sepot-Identity-Proof lets a new device reclaim an existing
identity; retry backs off from 30 s to 10 min and exposes a conflict flag.
Prekeys: PrekeysLow triggers a batch of 50 one-time prekeys via POST
/v1/keys; the signed prekey rotates weekly, the Kyber prekey monthly.
Messaging: crypto operations run serially (concurrent encrypts derived the
same message key); stop() waits for in-flight deliveries (auto-lock between
decrypt and persist lost messages); disappearing-message cleanup no longer
overwrites fresh messages; a push wake-up reconnects stale connections; the
outbox carries polls and mentions; length limits on wire input.
Storage: a duress session never exposes the real identity or phrase, does
not start networking and cannot touch the keystore; deleting data in a
duress session leaves the real account alone; PIN guessing is throttled
(5 misses, then 30 s doubling up to 15 min); the duress blob shares the salt
so unlock time does not reveal which PIN was entered; iOS keychain items are
this-device only; biometrics are biometric-only; screen security defaults
to on and is applied after unlock; tapjacking filter in MainActivity.
Tests: content types, shared-salt unwrap, duress isolation.
2026-10-09 14:02:33 +02:00

155 lines
5.4 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);
}
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);