Files
zapstore/lib/services/secure_storage_service.dart
T
2026-04-09 21:49:08 -03:00

217 lines
7.5 KiB
Dart

import 'dart:convert';
import 'package:amber_signer/amber_signer.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Service for securely storing sensitive data (NWC connection strings,
/// app catalog relays) using platform-native secure storage
/// (Keychain on iOS, KeyStore on Android).
///
/// This does NOT require user authentication - data is encrypted at rest
/// by the platform's secure storage mechanism.
class SecureStorageService {
SecureStorageService();
// Use explicit options for reliability across platforms
static final _storage = FlutterSecureStorage(
aOptions: const AndroidOptions(encryptedSharedPreferences: true),
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
),
);
static const _nwcKey = 'nwc_connection_string';
static const _appCatalogRelaysKey = 'app_catalog_relays';
/// Get the stored NWC connection string
Future<String?> getNWCString() async {
final value = await _storage.read(key: _nwcKey);
return (value?.isNotEmpty == true) ? value : null;
}
/// Store an NWC connection string
Future<void> setNWCString(String connectionString) async {
await _storage.write(key: _nwcKey, value: connectionString);
}
/// Clear the stored NWC connection string
Future<void> clearNWCString() async {
await _storage.delete(key: _nwcKey);
}
/// Check if an NWC connection string is stored
Future<bool> hasNWCString() async {
final value = await _storage.read(key: _nwcKey);
return value?.isNotEmpty == true;
}
// =========================================================================
// App Open Tracking (for background notification throttling)
// =========================================================================
static const _lastAppOpenedKey = 'last_app_opened';
/// Get the last time the user opened the app.
Future<DateTime?> getLastAppOpenedTime() async {
final value = await _storage.read(key: _lastAppOpenedKey);
if (int.tryParse(value ?? '') case final ms?) {
return DateTime.fromMillisecondsSinceEpoch(ms);
}
return null;
}
/// Store the last app opened time.
Future<void> setLastAppOpenedTime(DateTime time) async {
await _storage.write(
key: _lastAppOpenedKey,
value: '${time.millisecondsSinceEpoch}',
);
}
// =========================================================================
// Seen Until Timestamp (for background notification deduplication)
// =========================================================================
static const _seenUntilKey = 'seen_until';
/// Get the "seen until" timestamp.
/// Updates with release.createdAt <= this timestamp have already been notified.
Future<DateTime?> getSeenUntil() async {
final value = await _storage.read(key: _seenUntilKey);
if (int.tryParse(value ?? '') case final ms?) {
return DateTime.fromMillisecondsSinceEpoch(ms);
}
return null;
}
/// Store the "seen until" timestamp.
/// Called when a notification is shown, set to now() so future checks
/// only notify about releases created after this time.
Future<void> setSeenUntil(DateTime time) async {
await _storage.write(
key: _seenUntilKey,
value: '${time.millisecondsSinceEpoch}',
);
}
// =========================================================================
// App Catalog Relays
// =========================================================================
/// Get the stored app catalog relay URLs.
///
/// Returns null if no relays have been stored (use defaults).
/// Returns empty set if user explicitly cleared all relays (invalid state,
/// but handled gracefully).
Future<Set<String>?> getAppCatalogRelays() async {
final json = await _storage.read(key: _appCatalogRelaysKey);
if (json == null || json.isEmpty) return null;
try {
final list = jsonDecode(json) as List;
return Set<String>.from(list.cast<String>());
} catch (e) {
// Corrupted data - treat as unset
return null;
}
}
// =========================================================================
// Deletion Sync Timestamp (for NIP-09 incremental checks)
// =========================================================================
static const _deletionSyncedUntilKey = 'deletion_synced_until';
/// Get the timestamp of the last successful NIP-09 deletion sync.
/// Used as `since` on the next kind-5 query so checks are incremental.
Future<DateTime?> getDeletionsSyncedUntil() async {
final value = await _storage.read(key: _deletionSyncedUntilKey);
if (int.tryParse(value ?? '') case final ms?) {
return DateTime.fromMillisecondsSinceEpoch(ms);
}
return null;
}
/// Persist the deletion sync cursor after a successful kind-5 fetch.
Future<void> setDeletionsSyncedUntil(DateTime time) async {
await _storage.write(
key: _deletionSyncedUntilKey,
value: '${time.millisecondsSinceEpoch}',
);
}
// =========================================================================
// App Catalog Relays
// =========================================================================
/// Store app catalog relay URLs.
///
/// This is the local source of truth for relay configuration,
/// used to initialize the app before sign-in.
Future<void> setAppCatalogRelays(Set<String> relays) async {
await _storage.write(
key: _appCatalogRelaysKey,
value: jsonEncode(relays.toList()),
);
}
// =========================================================================
// Installed Apps Backup
// =========================================================================
static const _installedAppsBackupKey = 'installed_apps_backup_enabled';
Future<bool> isInstalledAppsBackupEnabled() async {
final value = await _storage.read(key: _installedAppsBackupKey);
return value == 'true';
}
Future<void> setInstalledAppsBackupEnabled(bool enabled) async {
await _storage.write(key: _installedAppsBackupKey, value: '$enabled');
}
}
/// Persists the AmberSigner pubkey in flutter_secure_storage.
/// This survives app data clears (database deletion) and is encrypted.
class SecureStoragePubkeyPersistence implements AmberPubkeyPersistence {
static const _key = 'amber_pubkey';
@override
Future<void> persistPubkey(String pubkey) async {
await SecureStorageService._storage.write(key: _key, value: pubkey);
}
@override
Future<String?> loadPubkey() async {
return SecureStorageService._storage.read(key: _key);
}
@override
Future<void> clearPubkey() async {
await SecureStorageService._storage.delete(key: _key);
}
}
final secureStorageServiceProvider = Provider<SecureStorageService>(
(ref) => SecureStorageService(),
);
/// Whether an NWC connection string is currently stored.
///
/// Use `ref.invalidate(hasNwcStringProvider)` after updating or clearing the
/// stored string to refresh UI.
final hasNwcStringProvider = FutureProvider.autoDispose<bool>((ref) async {
final secureStorage = ref.watch(secureStorageServiceProvider);
return secureStorage.hasNWCString();
});
/// Whether the installed apps backup setting is enabled.
///
/// Use `ref.invalidate(installedAppsBackupEnabledProvider)` after toggling
/// the setting to refresh UI.
final installedAppsBackupEnabledProvider =
FutureProvider.autoDispose<bool>((ref) async {
final secureStorage = ref.watch(secureStorageServiceProvider);
return secureStorage.isInstalledAppsBackupEnabled();
});