Files
Ensemble/lib/services/secure_storage_service.dart
Your NameandClaude Opus 4.5 02f7d501ff fix: Handle keystore signature changes gracefully
- Add decryption error handling in SecureStorageService
- Clear corrupted credentials when BadPaddingException occurs
- Add android:appCategory="audio" to manifest for proper app categorization
- Fix letter scrollbar proportional scrolling and bottom padding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 22:39:01 +00:00

212 lines
6.7 KiB
Dart

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
/// Service for securely storing sensitive data like passwords and tokens.
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
// Keys for secure storage
static const String _keyAuthToken = 'secure_auth_token';
static const String _keyMaAuthToken = 'secure_ma_auth_token';
static const String _keyAuthCredentials = 'secure_auth_credentials';
static const String _keyPassword = 'secure_password';
static const String _keyAbsApiToken = 'secure_abs_api_token';
// Auth Token (for stream requests)
static Future<String?> getAuthToken() async {
try {
return await _storage.read(key: _keyAuthToken);
} catch (e) {
// Handle decryption errors (e.g., after keystore/signature change)
if (e.toString().contains('BadPaddingException') ||
e.toString().contains('BAD_DECRYPT')) {
await _storage.delete(key: _keyAuthToken);
return null;
}
rethrow;
}
}
static Future<void> setAuthToken(String? token) async {
if (token == null || token.isEmpty) {
await _storage.delete(key: _keyAuthToken);
} else {
await _storage.write(key: _keyAuthToken, value: token);
}
}
// Music Assistant Native Auth Token (long-lived token)
static Future<String?> getMaAuthToken() async {
try {
return await _storage.read(key: _keyMaAuthToken);
} catch (e) {
// Handle decryption errors (e.g., after keystore/signature change)
if (e.toString().contains('BadPaddingException') ||
e.toString().contains('BAD_DECRYPT')) {
await _storage.delete(key: _keyMaAuthToken);
return null;
}
rethrow;
}
}
static Future<void> setMaAuthToken(String? token) async {
if (token == null || token.isEmpty) {
await _storage.delete(key: _keyMaAuthToken);
} else {
await _storage.write(key: _keyMaAuthToken, value: token);
}
}
static Future<void> clearMaAuthToken() async {
await _storage.delete(key: _keyMaAuthToken);
}
// Auth Credentials (serialized auth strategy credentials)
static Future<Map<String, dynamic>?> getAuthCredentials() async {
String? json;
try {
json = await _storage.read(key: _keyAuthCredentials);
} catch (e) {
// Handle decryption errors (e.g., after keystore/signature change)
if (e.toString().contains('BadPaddingException') ||
e.toString().contains('BAD_DECRYPT')) {
await _storage.delete(key: _keyAuthCredentials);
return null;
}
rethrow;
}
if (json == null) return null;
try {
return jsonDecode(json) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
static Future<void> setAuthCredentials(Map<String, dynamic> credentials) async {
await _storage.write(key: _keyAuthCredentials, value: jsonEncode(credentials));
}
static Future<void> clearAuthCredentials() async {
await _storage.delete(key: _keyAuthCredentials);
}
// Password
static Future<String?> getPassword() async {
try {
return await _storage.read(key: _keyPassword);
} catch (e) {
// Handle decryption errors (e.g., after keystore/signature change)
if (e.toString().contains('BadPaddingException') ||
e.toString().contains('BAD_DECRYPT')) {
await _storage.delete(key: _keyPassword);
return null;
}
rethrow;
}
}
static Future<void> setPassword(String? password) async {
if (password == null || password.isEmpty) {
await _storage.delete(key: _keyPassword);
} else {
await _storage.write(key: _keyPassword, value: password);
}
}
// Audiobookshelf API Token
static Future<String?> getAbsApiToken() async {
try {
return await _storage.read(key: _keyAbsApiToken);
} catch (e) {
// Handle decryption errors (e.g., after keystore/signature change)
if (e.toString().contains('BadPaddingException') ||
e.toString().contains('BAD_DECRYPT')) {
await _storage.delete(key: _keyAbsApiToken);
return null;
}
rethrow;
}
}
static Future<void> setAbsApiToken(String? token) async {
if (token == null || token.isEmpty) {
await _storage.delete(key: _keyAbsApiToken);
} else {
await _storage.write(key: _keyAbsApiToken, value: token);
}
}
/// Clear all secure storage (used during logout)
static Future<void> clearAll() async {
await _storage.deleteAll();
}
// Migration completed flag key
static const String _keyMigrationCompleted = 'secure_storage_migration_completed';
/// Migrate credentials from SharedPreferences to secure storage.
/// Call this once during app upgrade to migrate existing users.
/// Uses a flag to skip migration on subsequent runs for performance.
static Future<void> migrateFromSharedPreferences() async {
// Import SharedPreferences for migration
final SharedPreferences prefs = await SharedPreferences.getInstance();
// Skip if migration already completed
if (prefs.getBool(_keyMigrationCompleted) == true) {
return;
}
// Migrate auth token
final authToken = prefs.getString('auth_token');
if (authToken != null && authToken.isNotEmpty) {
await setAuthToken(authToken);
await prefs.remove('auth_token');
}
// Migrate MA auth token
final maAuthToken = prefs.getString('ma_auth_token');
if (maAuthToken != null && maAuthToken.isNotEmpty) {
await setMaAuthToken(maAuthToken);
await prefs.remove('ma_auth_token');
}
// Migrate auth credentials
final authCredentials = prefs.getString('auth_credentials');
if (authCredentials != null && authCredentials.isNotEmpty) {
try {
final decoded = jsonDecode(authCredentials) as Map<String, dynamic>;
await setAuthCredentials(decoded);
await prefs.remove('auth_credentials');
} catch (_) {
// Invalid JSON, just remove it
await prefs.remove('auth_credentials');
}
}
// Migrate password
final password = prefs.getString('password');
if (password != null && password.isNotEmpty) {
await setPassword(password);
await prefs.remove('password');
}
// Migrate ABS API token
final absApiToken = prefs.getString('abs_api_token');
if (absApiToken != null && absApiToken.isNotEmpty) {
await setAbsApiToken(absApiToken);
await prefs.remove('abs_api_token');
}
// Mark migration as completed to skip on future runs
await prefs.setBool(_keyMigrationCompleted, true);
}
}