Files
jinzhi/lib/common/storage/secure_identity_storage.dart

186 lines
6.0 KiB
Dart

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<String?> read(String key);
Future<void> write(String key, String value);
Future<void> delete(String key);
}
class FlutterSecureKeyValueStorage implements SecureKeyValueStorage {
const FlutterSecureKeyValueStorage();
static const _storage = FlutterSecureStorage();
@override
Future<String?> read(String key) => _storage.read(key: key);
@override
Future<void> write(String key, String value) =>
_storage.write(key: key, value: value);
@override
Future<void> 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<String?> 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<String?> 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<void> 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<void> clearJwt() async {
await _deleteAllAuthCredentials();
await _preferences.remove(storageLegacyAccessToken);
await _preferences.remove(storageAuthSessionActive);
}
Future<void> clearSessionCredentials() async {
await clearJwt();
await _storage.delete(secureMallCredential);
await _storage.delete(secureRefreshToken);
}
Future<void> clearAccountCredentials() async {
await clearSessionCredentials();
await _storage.delete(secureBiometricCredential);
}
Future<String?> readDeviceId() => _storage.read(secureDeviceId);
Future<void> saveDeviceId(String deviceId) =>
_storage.write(secureDeviceId, deviceId);
Future<void> _deleteAllAuthCredentials() async {
await _storage.delete(secureAccessToken);
await _storage.delete(secureLegacyJwtToken);
}
}
final secureIdentityStorageProvider = Provider<SecureIdentityStorage>(
(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<String?>? _restoreFuture;
Future<void> _operationTail = Future<void>.value();
String? get accessToken => _accessToken;
int get generation => _generation;
Future<String?> restore() => _restoreFuture ??= _serialize(_restore);
Future<String?> _restore() async {
_accessToken = await _identityStorage.restoreAccessToken();
_generation++;
return _accessToken;
}
Future<void> save(String token) async {
await _serialize(() async {
await _identityStorage.saveJwt(token);
_accessToken = token.trim();
_generation++;
_restoreFuture = Future<String?>.value(_accessToken);
});
}
Future<void> clear({bool accountDeleted = false}) async {
await _serialize(() async {
if (accountDeleted) {
await _identityStorage.clearAccountCredentials();
} else {
await _identityStorage.clearSessionCredentials();
}
_accessToken = null;
_generation++;
_restoreFuture = Future<String?>.value();
});
}
Future<T> _serialize<T>(Future<T> Function() operation) {
final result = _operationTail.then((_) => operation());
_operationTail = result.then<void>(
(_) {},
onError: (Object _, StackTrace _) {},
);
return result;
}
}
final authSessionStorageProvider = Provider<AuthSessionStorage>(
(ref) => AuthSessionStorage(ref.watch(secureIdentityStorageProvider)),
);
String? _normalized(String? value) {
final normalized = value?.trim() ?? '';
return normalized.isEmpty ? null : normalized;
}