import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'preferences_storage.dart'; import 'storage_keys.dart'; abstract interface class SecureKeyValueStorage { Future read(String key); Future write(String key, String value); Future delete(String key); } class FlutterSecureKeyValueStorage implements SecureKeyValueStorage { const FlutterSecureKeyValueStorage(); static const _storage = FlutterSecureStorage(); @override Future read(String key) => _storage.read(key: key); @override Future write(String key, String value) => _storage.write(key: key, value: value); @override Future delete(String key) => _storage.delete(key: key); } /// Sensitive identity values stay behind one injectable storage boundary. /// Legacy plaintext preferences are migrated only after a secure write works. class SecureIdentityStorage { SecureIdentityStorage( this._preferences, { SecureKeyValueStorage secureStorage = const FlutterSecureKeyValueStorage(), }) : _storage = secureStorage; final PreferencesStorage _preferences; final SecureKeyValueStorage _storage; Future restoreAccessToken() async { final legacyPreferences = _normalized( _preferences.getString(storageLegacyAccessToken), ); final hasInstallSession = _preferences.getBool(storageAuthSessionActive); // A plaintext token proves that this installation predates secure storage // migration. A Keychain token by itself does not: iOS may retain it after // uninstall while SharedPreferences (including this marker) is reset. if (!hasInstallSession && legacyPreferences == null) { await _deleteAllAuthCredentials(); await _preferences.remove(storageAuthSessionActive); return null; } final current = _normalized(await _storage.read(secureAccessToken)); final legacySecure = current == null ? _normalized(await _storage.read(secureLegacyJwtToken)) : null; final token = current ?? legacyPreferences ?? legacySecure; if (current == null && token != null) { await _storage.write(secureAccessToken, token); } await _storage.delete(secureLegacyJwtToken); await _preferences.remove(storageLegacyAccessToken); if (token == null) { await _preferences.remove(storageAuthSessionActive); } else { await _preferences.setBool(storageAuthSessionActive, true); } return token; } Future readJwt() async { if (!_preferences.getBool(storageAuthSessionActive) || _preferences.containsKey(storageLegacyAccessToken)) { return restoreAccessToken(); } final token = _normalized(await _storage.read(secureAccessToken)); if (token == null) await _preferences.remove(storageAuthSessionActive); return token; } Future saveJwt(String token) async { final normalized = _normalized(token); if (normalized == null) { throw ArgumentError.value(token, 'token', 'must not be empty'); } await _storage.write(secureAccessToken, normalized); await _storage.delete(secureLegacyJwtToken); await _preferences.remove(storageLegacyAccessToken); await _preferences.setBool(storageAuthSessionActive, true); } Future clearJwt() async { await _deleteAllAuthCredentials(); await _preferences.remove(storageLegacyAccessToken); await _preferences.remove(storageAuthSessionActive); } Future clearSessionCredentials() async { await clearJwt(); await _storage.delete(secureMallCredential); await _storage.delete(secureRefreshToken); } Future clearAccountCredentials() async { await clearSessionCredentials(); await _storage.delete(secureBiometricCredential); } Future readDeviceId() => _storage.read(secureDeviceId); Future saveDeviceId(String deviceId) => _storage.write(secureDeviceId, deviceId); Future _deleteAllAuthCredentials() async { await _storage.delete(secureAccessToken); await _storage.delete(secureLegacyJwtToken); } } final secureIdentityStorageProvider = Provider( (ref) => SecureIdentityStorage(ref.watch(preferencesStorageProvider)), ); /// Process-memory session snapshot. Interceptors read this synchronously; /// secure storage is touched only when a session starts, restores, or ends. class AuthSessionStorage { AuthSessionStorage(this._identityStorage); final SecureIdentityStorage _identityStorage; String? _accessToken; int _generation = 0; Future? _restoreFuture; Future _operationTail = Future.value(); String? get accessToken => _accessToken; int get generation => _generation; Future restore() => _restoreFuture ??= _serialize(_restore); Future _restore() async { _accessToken = await _identityStorage.restoreAccessToken(); _generation++; return _accessToken; } Future save(String token) async { await _serialize(() async { await _identityStorage.saveJwt(token); _accessToken = token.trim(); _generation++; _restoreFuture = Future.value(_accessToken); }); } Future clear({bool accountDeleted = false}) async { await _serialize(() async { if (accountDeleted) { await _identityStorage.clearAccountCredentials(); } else { await _identityStorage.clearSessionCredentials(); } _accessToken = null; _generation++; _restoreFuture = Future.value(); }); } Future _serialize(Future Function() operation) { final result = _operationTail.then((_) => operation()); _operationTail = result.then( (_) {}, onError: (Object _, StackTrace _) {}, ); return result; } } final authSessionStorageProvider = Provider( (ref) => AuthSessionStorage(ref.watch(secureIdentityStorageProvider)), ); String? _normalized(String? value) { final normalized = value?.trim() ?? ''; return normalized.isEmpty ? null : normalized; }