managing-secure-storage — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited managing-secure-storage (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Implement encrypted key-value storage for sensitive data using flutter_secure_storage. Leverages platform-native secure storage (iOS Keychain, Android Keystore) for maximum security.
dependencies:
flutter_secure_storage: ^10.0.0Android (android/app/build.gradle):
android {
compileSdkVersion 34 // Minimum 18
defaultConfig {
minSdkVersion 18
}
}Android Backup Exclusion (AndroidManifest.xml):
<application
android:fullBackupContent="@xml/backup_rules"
android:allowBackup="true">Android Backup Rules (res/xml/backup_rules.xml):
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Exclude secure storage from Google Drive backups -->
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>iOS: No additional configuration required (uses Keychain by default).
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorage {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
resetOnError: true, // Auto-reset on biometric change invalidation
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
accountName: AppleOptions.defaultAccountName,
),
);
// Access Token
static Future<String?> getAccessToken() => _storage.read(key: _Keys.accessToken);
static Future<void> setAccessToken(String value) => _storage.write(key: _Keys.accessToken, value: value);
static Future<void> deleteAccessToken() => _storage.delete(key: _Keys.accessToken);
// Refresh Token
static Future<String?> getRefreshToken() => _storage.read(key: _Keys.refreshToken);
static Future<void> setRefreshToken(String value) => _storage.write(key: _Keys.refreshToken, value: value);
// API Key
static Future<String?> getApiKey() => _storage.read(key: _Keys.apiKey);
static Future<void> setApiKey(String value) => _storage.write(key: _Keys.apiKey, value: value);
// User Credentials (for biometric unlock)
static Future<String?> getStoredPassword() => _storage.read(key: _Keys.password);
static Future<void> setStoredPassword(String value) => _storage.write(key: _Keys.password, value: value);
// Clear all secure data (logout)
static Future<void> clearAll() => _storage.deleteAll();
// Check if key exists
static Future<bool> hasAccessToken() async {
final token = await getAccessToken();
return token != null && token.isNotEmpty;
}
}
class _Keys {
static const String accessToken = 'access_token';
static const String refreshToken = 'refresh_token';
static const String apiKey = 'api_key';
static const String password = 'stored_password';
}Authentication Flow:
// Login - store tokens
Future<void> login(String username, String password) async {
final response = await authApi.login(username, password);
await SecureStorage.setAccessToken(response.accessToken);
await SecureStorage.setRefreshToken(response.refreshToken);
}
// Check authentication status
Future<bool> isAuthenticated() async {
return await SecureStorage.hasAccessToken();
}
// Logout - clear tokens
Future<void> logout() async {
await SecureStorage.clearAll();
}
// Refresh token
Future<String> refreshAccessToken() async {
final refreshToken = await SecureStorage.getRefreshToken();
if (refreshToken == null) throw Exception('No refresh token');
final response = await authApi.refresh(refreshToken);
await SecureStorage.setAccessToken(response.accessToken);
return response.accessToken;
}API Client Integration:
class ApiClient {
Future<Response> get(String path) async {
final token = await SecureStorage.getAccessToken();
return http.get(
Uri.parse('$baseUrl$path'),
headers: {
'Authorization': 'Bearer $token',
},
);
}
}With Riverpod:
final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier() : super(AuthState.loading()) {
_checkAuthStatus();
}
Future<void> _checkAuthStatus() async {
final hasToken = await SecureStorage.hasAccessToken();
state = hasToken ? AuthState.authenticated() : AuthState.unauthenticated();
}
Future<void> login(String username, String password) async {
state = AuthState.loading();
try {
final response = await authApi.login(username, password);
await SecureStorage.setAccessToken(response.accessToken);
await SecureStorage.setRefreshToken(response.refreshToken);
state = AuthState.authenticated();
} catch (e) {
state = AuthState.error(e.toString());
}
}
Future<void> logout() async {
await SecureStorage.clearAll();
state = AuthState.unauthenticated();
}
}Problem: User adds/removes fingerprint → Android Keystore keys become invalid Solution: Enable resetOnError: true in AndroidOptions
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(
resetOnError: true, // Auto-reset on key invalidation
),
);User Communication:
try {
final token = await SecureStorage.getAccessToken();
} catch (e) {
if (e.toString().contains('KeyPermanentlyInvalidatedException')) {
// Show user-friendly message
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Security Settings Changed'),
content: Text('Your device security settings have changed. Please log in again.'),
actions: [
TextButton(
onPressed: () => Navigator.pushReplacementNamed(context, '/login'),
child: Text('Re-login'),
),
],
),
);
}
}Android Migration Path:
Migration Example:
class SecureStorageMigration {
static Future<void> migrateFromSharedPreferences() async {
final prefs = await SharedPreferences.getInstance();
// Migrate access token
final oldAccessToken = prefs.getString('access_token');
if (oldAccessToken != null) {
await SecureStorage.setAccessToken(oldAccessToken);
await prefs.remove('access_token');
}
// Migrate refresh token
final oldRefreshToken = prefs.getString('refresh_token');
if (oldRefreshToken != null) {
await SecureStorage.setRefreshToken(oldRefreshToken);
await prefs.remove('refresh_token');
}
// Mark migration complete
await prefs.setBool('migrated_to_secure_storage', true);
}
static Future<bool> needsMigration() async {
final prefs = await SharedPreferences.getInstance();
return !(prefs.getBool('migrated_to_secure_storage') ?? false);
}
}
// In main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (await SecureStorageMigration.needsMigration()) {
await SecureStorageMigration.migrateFromSharedPreferences();
}
runApp(MyApp());
}CRITICAL: Test upgrade from ALL previous app versions.
Problem: Keystore keys don't restore from Google Drive backups → app crashes on new device Solution: Exclude FlutterSecureStorage from backups (see Phase 2 configuration)
Verification:
# Check if backup exclusion is working
adb shell bmgr list transports
adb shell bmgr backupnow --allAccessibility Options:
const storage = FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock, // Recommended
// Other options:
// - unlocked: Accessible only when device unlocked
// - first_unlock_this_device: Never migrates to new device
// - unlocked_this_device: Most restrictive
),
);Common Issues:
accessControlFlags for biometric gating → Use biometryAny or biometryCurrentSetBiometric Gating Example:
// iOS only - require biometric for read
const storage = FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.unlocked,
// Requires biometric authentication
accessControl: AccessControlOptions(
biometryCurrentSet: true,
userPresence: true,
),
),
);clearAll() on logout to prevent token leakage.resetOnError scenario with user-friendly re-authentication flow.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.