import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'storage_keys.dart'; import 'storage_service.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; static const _accessTokenKey = 'access_token'; static const _legacySecureJwtKey = 'jwt_token'; final SharedPreferences _preferences; final SecureKeyValueStorage _storage; Future restoreAccessToken() async { final current = _normalized(await _storage.read(_accessTokenKey)); final legacyPreferences = _normalized( _preferences.getString(storageLegacyAccessToken), ); final legacySecure = current == null ? _normalized(await _storage.read(_legacySecureJwtKey)) : null; final token = current ?? legacyPreferences ?? legacySecure; if (current == null && token != null) { await _storage.write(_accessTokenKey, token); } await _storage.delete(_legacySecureJwtKey); await _preferences.remove(storageLegacyAccessToken); if (token == null) { await _preferences.remove(storageAuthSessionActive); } else { await _preferences.setBool(storageAuthSessionActive, true); } return token; } Future readJwt() async { final token = _normalized(await _storage.read(_accessTokenKey)); if (token == null || _preferences.containsKey(storageLegacyAccessToken)) { return restoreAccessToken(); } 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(_accessTokenKey, normalized); await _storage.delete(_legacySecureJwtKey); await _preferences.remove(storageLegacyAccessToken); await _preferences.setBool(storageAuthSessionActive, true); } Future clearJwt() async { await _storage.delete(_accessTokenKey); await _storage.delete(_legacySecureJwtKey); await _preferences.remove(storageLegacyAccessToken); await _preferences.remove(storageAuthSessionActive); } } final secureIdentityStorageProvider = Provider( (ref) => SecureIdentityStorage(StorageService.to.preferences), ); String? _normalized(String? value) { final normalized = value?.trim() ?? ''; return normalized.isEmpty ? null : normalized; }