npx claudepluginhub davidortinau/maui-skills --plugin maui-skillsThis skill uses the workspace's default tool permissions.
Auto Backup restores encrypted preferences to a new device where the encryption key is invalid — this throws **unrecoverable exceptions**. You must either disable Auto Backup or exclude secure storage files from backup. See `references/secure-storage-api.md` for setup options.
Adds SQLite local database storage to .NET MAUI apps using sqlite-net-pcl: ORM attributes, async lazy-init DI service, WAL mode, file management. For offline CRUD persistence.
Detects insecure local storage of sensitive data (credentials, tokens, PII) in files, SharedPreferences, NSUserDefaults, SQLite, or localStorage without encryption. Suggests keychain, EncryptedSharedPreferences, or in-memory alternatives.
Guides secure mobile app development with practices for input validation, WebView security, encrypted storage like Keychain/Keystore, and mobile vulnerabilities in iOS/Android.
Share bugs, ideas, or general feedback.
Auto Backup restores encrypted preferences to a new device where the encryption key is invalid — this throws unrecoverable exceptions. You must either disable Auto Backup or exclude secure storage files from backup. See references/secure-storage-api.md for setup options.
Corrupted values from backup restoration throw exceptions. Never call GetAsync unprotected:
// ❌ Unprotected — crashes on corrupted backup data
var value = await SecureStorage.Default.GetAsync("key");
// ✅ Protected — handles corruption gracefully
try
{
var value = await SecureStorage.Default.GetAsync("key");
}
catch (Exception)
{
SecureStorage.Default.RemoveAll();
}
Add keychain access groups for Simulator builds, but remove before physical device / App Store builds — they cause signing issues on devices where they aren't needed.
Unlike Android, uninstalling an iOS app does not remove its Keychain entries. Values persist and are available if the app is reinstalled. Design for this — don't assume a fresh install means empty storage.
Values may sync across devices via iCloud Keychain if the user has it enabled. This is platform behavior, not controllable from MAUI. Don't store device-specific tokens that shouldn't roam.
// ❌ Storing large data — SecureStorage is for small secrets only
await SecureStorage.Default.SetAsync("profile_image", base64EncodedImage);
// ✅ Store tokens, passwords, short secrets
await SecureStorage.Default.SetAsync("auth_token", jwtToken);
// ❌ Logging secret values
_logger.LogInformation("Token: {Token}", await SecureStorage.Default.GetAsync("auth_token"));
// ✅ Log existence, not value
_logger.LogInformation("Token exists: {Exists}", token is not null);
// ❌ Storing complex objects without serialization (values are strings only)
await SecureStorage.Default.SetAsync("user", userObject);
// ✅ Serialize to JSON first
await SecureStorage.Default.SetAsync("user", JsonSerializer.Serialize(user));
| Question | Answer |
|---|---|
| Storing a token, password, or API key? | ✅ Use SecureStorage |
| Storing user preferences or settings? | ❌ Use Preferences instead |
| Storing large files or blobs? | ❌ Use file system + encryption |
| Need cross-device sync? | ⚠️ iOS syncs via iCloud Keychain automatically |
| Need data cleared on uninstall? | ⚠️ Only works on Android, not iOS |
Never call SecureStorage.Default directly from ViewModels — wrap it in an ISecureStorageService interface for testability. See references/secure-storage-api.md for the full DI wrapper pattern with mock examples.
// ❌ Direct static access — untestable
public class LoginViewModel
{
public async Task SaveToken(string token)
=> await SecureStorage.Default.SetAsync("auth_token", token);
}
// ✅ Inject interface — testable and mockable
public class LoginViewModel(ISecureStorageService secure)
{
public async Task SaveToken(string token)
=> await secure.SetAsync("auth_token", token);
}
GetAsync calls wrapped in try/catchSecureStorage.Default accessed via DI wrapper, not directly from ViewModels