Šepot — end-to-end encrypted messenger

Initial public release. Anonymous on-device identity (no phone number,
e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double
Ratchet, sealed sender) via libsignal, a metadata-minimizing relay,
group chats, disappearing messages, encrypted local storage, optional
Tor transport, and a UI localized into Czech, English, German, Russian,
and Italian.

Monorepo: Flutter client (app/), Rust relay + wire types (rust/),
protobuf definitions (proto/), self-host deploy (deploy/).

Licensed under AGPL-3.0 with an app-store distribution exception (§7).
This commit is contained in:
Julius Joska
2026-07-11 17:06:39 +02:00
commit 8081d98c67
309 changed files with 44102 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/core/errors.dart';
import 'package:sepot/models/contact.dart';
import 'package:sepot/models/identity.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Stav seznamu kontaktů
class ContactsNotifier extends AsyncNotifier<List<Contact>> {
@override
Future<List<Contact>> build() async {
final storage = ref.read(storageProvider);
return storage.loadContacts();
}
/// Přidej kontakt z veřejné identity
Future<Contact> addContact(PublicIdentity identity, {bool verified = false}) async {
final storage = ref.read(storageProvider);
final contacts = await future;
// Kontrola duplicity
if (contacts.any((c) => c.publicIdentity.id == identity.id)) {
throw const ContactAlreadyExistsError();
}
final contact = Contact.fromPublicIdentity(identity, verified: verified);
await storage.saveContact(contact);
state = AsyncData([...contacts, contact]);
return contact;
}
/// Smaž kontakt
Future<void> removeContact(String contactId) async {
final storage = ref.read(storageProvider);
await storage.deleteContact(contactId);
final contacts = await future;
state = AsyncData(contacts.where((c) => c.id != contactId).toList());
}
// Pozn.: dřívější verifyContact() (slepé přepnutí flagu) odstraněna —
// ověření jde VÝHRADNĚ přes safety number (ContactVerifyScreen).
/// Zablokuj / odblokuj kontakt. Blokovaný: příchozí zprávy se zahodí,
/// odesílání je v UI zakázané.
Future<void> setBlocked(String contactId, bool blocked) async {
final storage = ref.read(storageProvider);
final contacts = await future;
final contact = contacts.where((c) => c.id == contactId).firstOrNull;
if (contact == null) return;
contact.blocked = blocked;
await storage.saveContact(contact);
state = AsyncData([...contacts]);
}
/// Najdi kontakt podle identity ID
Contact? findByIdentityId(String identityId) {
final contacts = state.valueOrNull;
if (contacts == null) return null;
return contacts.where((c) => c.publicIdentity.id == identityId).firstOrNull;
}
}
final contactsProvider =
AsyncNotifierProvider<ContactsNotifier, List<Contact>>(
ContactsNotifier.new,
);
@@ -0,0 +1,253 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/models/contact.dart';
import 'package:sepot/models/conversation.dart';
import 'package:sepot/models/group.dart';
import 'package:sepot/models/message.dart';
import 'package:sepot/providers/messaging_provider.dart';
import 'package:sepot/providers/storage_provider.dart';
import 'package:sepot/providers/identity_provider.dart';
/// Stav konverzací
class ConversationsNotifier extends AsyncNotifier<List<Conversation>> {
@override
Future<List<Conversation>> build() async {
final storage = ref.read(storageProvider);
return storage.loadConversations();
}
/// Odešli zprávu — E2E šifrovaně přes relay. `replyToId` = odpověď.
/// `contactId` může být i gid skupiny (fanout členům).
Future<void> sendMessage(String contactId, String text,
{String? replyToId}) async {
final storage = ref.read(storageProvider);
final service = ref.read(messagingServiceProvider);
final group = await _groupOrNull(contactId);
if (group != null) {
await service.sendGroupText(group, text, replyToId: replyToId);
} else {
final contact = await _contact(contactId);
// sendText zprávu po odeslání sám uloží (s reálným delivery stavem)
await service.sendText(contact, text, replyToId: replyToId);
}
final conversations = await storage.loadConversations();
state = AsyncData(conversations);
}
Future<Contact> _contact(String contactId) async {
final contacts = await ref.read(storageProvider).loadContacts();
final contact = contacts.where((c) => c.id == contactId).firstOrNull;
if (contact == null) throw StateError('Contact not found');
return contact;
}
/// Skupina, pokud id konverzace patří skupině (a neopustil jsem ji).
Future<Group?> _groupOrNull(String id) async {
final group = await ref.read(storageProvider).loadGroup(id);
if (group == null || group.left) return null;
return group;
}
/// Reakce emotikonem na zprávu (null = odebrat moji reakci).
Future<void> reactToMessage(
String contactId, String messageId, String? emoji) async {
final service = ref.read(messagingServiceProvider);
final group = await _groupOrNull(contactId);
if (group != null) {
await service.sendGroupReaction(group, messageId, emoji);
} else {
await service.sendReaction(await _contact(contactId), messageId, emoji);
}
state = AsyncData(await ref.read(storageProvider).loadConversations());
}
/// Úprava textu odeslané zprávy.
Future<void> editMessage(
String contactId, String messageId, String newText) async {
final service = ref.read(messagingServiceProvider);
final group = await _groupOrNull(contactId);
if (group != null) {
await service.editGroupMessage(group, messageId, newText);
} else {
await service.editMessage(await _contact(contactId), messageId, newText);
}
state = AsyncData(await ref.read(storageProvider).loadConversations());
}
/// Smazání zprávy pro všechny.
Future<void> deleteMessage(String contactId, String messageId) async {
final service = ref.read(messagingServiceProvider);
final group = await _groupOrNull(contactId);
if (group != null) {
await service.deleteGroupMessage(group, messageId);
} else {
await service.deleteMessage(await _contact(contactId), messageId);
}
state = AsyncData(await ref.read(storageProvider).loadConversations());
}
/// Ztlumit / odtlumit konverzaci (lokální). `duration == null` = navždy.
Future<void> setMuted(String contactId, bool muted, {Duration? duration}) async {
final storage = ref.read(storageProvider);
final conv = await storage.loadConversation(contactId);
if (conv == null) return;
conv.muted = muted;
conv.mutedUntil = (muted && duration != null)
? DateTime.now().add(duration)
: null;
await storage.saveConversation(conv);
state = AsyncData(await storage.loadConversations());
}
/// Archivovat / odarchivovat konverzaci (lokální).
Future<void> setArchived(String contactId, bool archived) async {
final storage = ref.read(storageProvider);
final conv = await storage.loadConversation(contactId);
if (conv == null) return;
conv.archived = archived;
await storage.saveConversation(conv);
state = AsyncData(await storage.loadConversations());
}
/// Smazání zprávy jen u mě (lokálně) — protistraně se nic neposílá.
Future<void> deleteLocalMessage(String contactId, String messageId) async {
final storage = ref.read(storageProvider);
final conv = await storage.loadConversation(contactId);
if (conv == null) return;
conv.messages.removeWhere((m) => m.id == messageId);
await storage.saveConversation(conv);
state = AsyncData(await storage.loadConversations());
}
/// Odešli přílohu (fotku/dokument) — zpracování, šifrování a upload
/// řeší MessagingService.sendAttachment.
Future<void> sendAttachment(
String contactId, {
required Uint8List bytes,
required String fileName,
String? mime,
}) async {
final storage = ref.read(storageProvider);
final service = ref.read(messagingServiceProvider);
final group = await _groupOrNull(contactId);
if (group != null) {
await service.sendGroupAttachment(
group,
bytes: bytes,
fileName: fileName,
mime: mime,
);
} else {
final contact = await _contact(contactId);
await service.sendAttachment(
contact,
bytes: bytes,
fileName: fileName,
mime: mime,
);
}
final conversations = await storage.loadConversations();
state = AsyncData(conversations);
}
/// Odešli hlasovou zprávu (audio příloha s délkou nahrávky).
Future<void> sendVoiceMessage(
String contactId, {
required Uint8List bytes,
required int durationMs,
String mime = 'audio/mp4',
}) async {
final storage = ref.read(storageProvider);
final service = ref.read(messagingServiceProvider);
final group = await _groupOrNull(contactId);
if (group != null) {
await service.sendGroupAttachment(
group,
bytes: bytes,
fileName: 'voice-message.m4a',
mime: mime,
durationMs: durationMs,
);
} else {
await service.sendAttachment(
await _contact(contactId),
bytes: bytes,
fileName: 'voice-message.m4a',
mime: mime,
durationMs: durationMs,
);
}
state = AsyncData(await storage.loadConversations());
}
/// Ruční opakování downloadu blobu (stav failed).
Future<void> retryAttachmentDownload(
String contactId, Message message) async {
final service = ref.read(messagingServiceProvider);
await service.downloadAttachment(contactId, message);
final conversations = await ref.read(storageProvider).loadConversations();
state = AsyncData(conversations);
}
/// Označ konverzaci jako přečtenou
Future<void> markAsRead(String contactId) async {
final storage = ref.read(storageProvider);
final identity = ref.read(identityProvider).valueOrNull;
if (identity == null) return;
final conv = await storage.loadConversation(contactId);
if (conv == null) return;
// Příchozí zprávy, které jsem právě přečetl → pošlu za ně potvrzení
// přečtení (sendReadReceipts sám zkontroluje, jestli je funkce zapnutá).
final justRead = conv.messages
.where((m) => !m.isMine(identity.id) && !m.isRead)
.map((m) => m.id)
.toList();
conv.markAllRead(identity.id);
await storage.saveConversation(conv);
state = AsyncData(await storage.loadConversations());
// Potvrzení přečtení ve skupině v1 neposíláme (fanout N receipts
// za každé otevření chatu; per-member matice až v2).
if (justRead.isNotEmpty && await _groupOrNull(contactId) == null) {
final contacts = await storage.loadContacts();
final contact = contacts.where((c) => c.id == contactId).firstOrNull;
if (contact != null) {
await ref
.read(messagingServiceProvider)
.sendReadReceipts(contact, justRead);
}
}
}
/// Celkový počet nepřečtených zpráv
int get totalUnread {
final conversations = state.valueOrNull ?? [];
final identity = ref.read(identityProvider).valueOrNull;
if (identity == null) return 0;
return conversations.fold(
0,
(sum, conv) => sum + conv.unreadCount(identity.id),
);
}
}
final conversationsProvider =
AsyncNotifierProvider<ConversationsNotifier, List<Conversation>>(
ConversationsNotifier.new,
);
/// Provider pro jednu konverzaci
final conversationProvider =
FutureProvider.family<Conversation?, String>((ref, contactId) async {
final storage = ref.read(storageProvider);
return storage.loadConversation(contactId);
});
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/models/contact.dart';
import 'package:sepot/models/group.dart';
import 'package:sepot/providers/conversations_provider.dart';
import 'package:sepot/providers/messaging_provider.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Stav skupin. Zápisové operace jdou přes
/// MessagingService (fanout členům), tady se jen drží seznam pro UI.
class GroupsNotifier extends AsyncNotifier<List<Group>> {
@override
Future<List<Group>> build() async {
return ref.read(storageProvider).loadGroups();
}
Future<Group> createGroup(String name, List<Contact> members) async {
final group =
await ref.read(messagingServiceProvider).createGroup(name, members);
await _refresh();
return group;
}
Future<void> addMembers(Group group, List<Contact> added) async {
await ref.read(messagingServiceProvider).addGroupMembers(group, added);
await _refresh();
}
Future<void> rename(Group group, String newName) async {
await ref.read(messagingServiceProvider).renameGroup(group, newName);
await _refresh();
}
Future<void> leave(Group group) async {
await ref.read(messagingServiceProvider).leaveGroup(group);
await _refresh();
}
Future<void> _refresh() async {
state = AsyncData(await ref.read(storageProvider).loadGroups());
ref.invalidate(conversationsProvider);
}
}
final groupsProvider = AsyncNotifierProvider<GroupsNotifier, List<Group>>(
GroupsNotifier.new,
);
/// Jedna skupina podle id (null = id nepatří skupině → 1:1 konverzace).
final groupProvider = FutureProvider.family<Group?, String>((ref, id) async {
// Závislost na seznamu — invalidace groupsProvider obnoví i family.
ref.watch(groupsProvider);
return ref.read(storageProvider).loadGroup(id);
});
+63
View File
@@ -0,0 +1,63 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/models/identity.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Stav identity uživatele
class IdentityNotifier extends AsyncNotifier<LocalIdentity?> {
@override
Future<LocalIdentity?> build() async {
final storage = ref.read(storageProvider);
return storage.loadIdentity();
}
/// Vytvoř novou identitu
Future<LocalIdentity> createIdentity(String displayName) async {
final identity = await LocalIdentity.create(displayName);
final storage = ref.read(storageProvider);
await storage.saveIdentity(identity);
state = AsyncData(identity);
return identity;
}
/// Obnov identitu z 24slovné zálohovací fráze (recovery flow).
///
/// Obnoví IDENTITU (stejný klíč → stejný SEP kód a safety number, kontakty
/// tě dál poznají), ne historii zpráv. `LocalIdentity.recover` vyhodí
/// [FormatException] při neplatné frázi. Po uložení messaging_provider
/// sám spustí registraci u relaye (nová delivery adresa + bundle).
Future<LocalIdentity> recoverIdentity(
List<String> words, String displayName) async {
final identity = await LocalIdentity.recover(
mnemonicWords: words,
displayName: displayName,
);
final storage = ref.read(storageProvider);
await storage.saveIdentity(identity);
await storage.saveSeedPhrase(identity.mnemonic);
state = AsyncData(identity);
return identity;
}
/// Znovu načti z úložiště
Future<void> reload() async {
final storage = ref.read(storageProvider);
state = AsyncData(await storage.loadIdentity());
}
}
final identityProvider =
AsyncNotifierProvider<IdentityNotifier, LocalIdentity?>(
IdentityNotifier.new,
);
/// Zkontroluj, zda je onboarding hotový
final isOnboardingCompleteProvider = FutureProvider<bool>((ref) async {
final storage = ref.read(storageProvider);
return storage.isOnboardingComplete();
});
/// Zkontroluj, zda existuje PIN
final hasPinProvider = FutureProvider<bool>((ref) async {
final storage = ref.read(storageProvider);
return storage.hasPin();
});
+47
View File
@@ -0,0 +1,47 @@
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Ruční volba jazyka aplikace. `null` = řídit se systémem.
///
/// Hodnota žije v OS keystore (NE v SQLCipher DB) — PIN lock a onboarding
/// se zobrazují před odemčením úložiště a musí už být ve zvoleném jazyce.
/// Jazyk není citlivý údaj; keystore je tu jen kvůli dostupnosti před
/// unlockem (stejný vzor jako režim pushů v PushBootstrap).
class LocaleNotifier extends AsyncNotifier<Locale?> {
static const _key = 'app_locale';
static const _store = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
@override
Future<Locale?> build() => readStored();
/// Přečte uloženou volbu i bez Riverpodu — pro headless start (push
/// budíček skládá notifikaci před postavením widget tree).
static Future<Locale?> readStored() async {
try {
final code = await _store.read(key: _key);
return (code == null || code.isEmpty) ? null : Locale(code);
} catch (_) {
// Keystore nedostupný (např. těsně po restartu) → systémový jazyk.
return null;
}
}
/// Nastaví jazyk aplikace; `null` vrátí řízení systému.
Future<void> set(Locale? locale) async {
if (locale == null) {
await _store.delete(key: _key);
} else {
await _store.write(key: _key, value: locale.languageCode);
}
state = AsyncData(locale);
}
}
final localeProvider =
AsyncNotifierProvider<LocaleNotifier, Locale?>(LocaleNotifier.new);
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/core/network/relay_client.dart';
import 'package:sepot/core/push/push_notifier.dart';
import 'package:sepot/core/services/messaging_service.dart';
import 'package:sepot/providers/contacts_provider.dart';
import 'package:sepot/providers/conversations_provider.dart';
import 'package:sepot/providers/groups_provider.dart';
import 'package:sepot/providers/identity_provider.dart';
import 'package:sepot/providers/storage_provider.dart';
/// Jediná instance messaging služby (krypto + relay + storage).
final messagingServiceProvider = Provider<MessagingService>((ref) {
final service = MessagingService(storage: ref.read(storageProvider));
ref.onDispose(service.stop);
return service;
});
/// Nastartuje službu, jakmile existuje identita. Watchovat z první
/// obrazovky po odemčení (chat list) — do té doby se nic nespojuje.
/// Předpokládá odemčený storage — na /chats se jde vždy až po unlocku.
final messagingStartProvider = FutureProvider<void>((ref) async {
final identity = await ref.watch(identityProvider.future);
if (identity == null) return;
final service = ref.read(messagingServiceProvider);
if (service.isStarted) return;
service.onUpdate = () {
ref.invalidate(conversationsProvider);
// celá family — jinak se otevřená konverzace nedozví o nové zprávě
// ani o změně stavu přílohy
ref.invalidate(conversationProvider);
ref.invalidate(contactsProvider);
// příchozí ginv/gleave/gmeta mění skupiny
ref.invalidate(groupsProvider);
};
// Appka běží, ale není v popředí (jiná appka / zhasnutý displej se
// živým procesem) → systémová notifikace. Jen jméno, žádný obsah.
// Ztlumená konverzace notifikaci potlačí (unread badge běží dál).
service.onMessageReceived = (contact, message) async {
final visible = WidgetsBinding.instance.lifecycleState ==
AppLifecycleState.resumed;
if (visible) return;
final conv = await ref.read(storageProvider).loadConversation(contact.id);
if (conv?.isMuted == true) return;
PushNotifier.showMessageNotification(contact.displayName);
};
await service.start(identity);
});
/// Stav připojení k relayi pro UI indikátor.
final relayConnectionProvider = StreamProvider<RelayConnectionState>((ref) {
ref.watch(messagingStartProvider);
final service = ref.read(messagingServiceProvider);
return service.connectionStream ?? const Stream.empty();
});
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/core/push/push_service.dart';
import 'package:sepot/providers/messaging_provider.dart';
/// Push vrstva nad messagingem.
final pushServiceProvider = Provider<PushService>((ref) {
return PushService(ref.read(messagingServiceProvider));
});
/// Zaregistruje push callbacky, jakmile messaging běží. Push se sám
/// NEzapíná — jen připraví handlery. Pokud už uživatel dřív distributora
/// zvolil, obnoví registraci (endpoint se mohl změnit; relay se re-registruje
/// po AUTH). Watchovat z chat listu vedle messagingStartProvider.
final pushInitProvider = FutureProvider<void>((ref) async {
await ref.watch(messagingStartProvider.future);
final push = ref.read(pushServiceProvider);
// Obnoví uložený režim (off/UnifiedPush/FCM) — endpoint/token se
// re-registruje u relaye po každém startu.
await push.restoreMode();
});
+122
View File
@@ -0,0 +1,122 @@
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/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);
/// Zabezpečení obrazovky: blok screenshotů (Android FLAG_SECURE) + skrytí
/// obsahu v přepínači appek (iOS). Default VYPNUTO — blokuje i vlastní
/// screenshoty, tak ať se uživatel rozhodne sám.
class ScreenSecurityNotifier extends AsyncNotifier<bool> {
static const key = 'screen_security_enabled';
@override
Future<bool> build() async {
final v = (await ref.read(storageProvider).getSetting<bool>(key)) ?? false;
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);
+7
View File
@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sepot/core/storage/secure_storage.dart';
/// Globální instance úložiště
final storageProvider = Provider<SecureStorageService>((ref) {
return SecureStorageService();
});