mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
217 lines
7.5 KiB
Dart
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();
|
|
});
|