maui-secure-storage について
.NET MAUI向けに、Android、iOS、Windowsでのセキュアストレージの注意点と実践方法を解説するスキルです。機密情報を安全に扱い、プラットフォームの挙動を考慮し、依存性注入でストレージアクセスをテスト可能にします。
主な機能
- 01プラットフォーム別のストレージ注意点
- 02Androidのバックアップと例外処理
- 03iOS Keychainの挙動ガイド
- 04Windowsのストレージ制限
- 05依存性注入によるテストパターン
- 06機密情報の取り扱いチェックリスト
活用シーン
- 01MAUIアプリでトークンやパスワードを保存
- 02Androidのバックアップデータによる読み込み失敗を防止
- 03再インストール後も残るKeychainデータを考慮
- 04ViewModelのストレージアクセスをモック可能にする
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-secure-storageインストール後、Claude Code で/maui-secure-storageを入力して読み込まれたか確認します。
同梱ファイルreferences/secure-storage-api.md
詳細
セキュアストレージ — 注意点とベストプラクティス
プラットフォームの重大な落とし穴
⚠️ Android:自動バックアップで暗号化された値が壊れる
自動バックアップでは、暗号化キーが無効な新しいデバイスに暗号化された設定が復元されるため、回復不能な例外が発生します。自動バックアップを無効にするか、セキュアストレージのファイルをバックアップ対象から除外する必要があります。設定方法については、references/secure-storage-api.md を参照してください。
⚠️ Android:必ず try/catch で囲む
バックアップから復元した値が破損していると、例外が発生します。保護せずに GetAsync を呼び出さないでください。
// ❌ 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();
}
⚠️ iOS:シミュレーターでのキーチェーンエンタイトルメント
シミュレーター用ビルドではキーチェーンアクセスグループを追加しますが、実機/App Store 用ビルドの前には削除してください。不要なデバイスで署名の問題が発生します。
⚠️ iOS:アンインストールしてもキーチェーンは消去されない
Android とは異なり、iOS アプリをアンインストールしても、キーチェーンのエントリーは削除されません。値は保持され、アプリを再インストールすると利用できます。この動作を前提に設計してください。新規インストール時にストレージが空だと想定しないでください。
⚠️ iOS:iCloud キーチェーンの同期
ユーザーが iCloud キーチェーンを有効にしている場合、値は iCloud キーチェーンを通じてデバイス間で同期されることがあります。これはプラットフォームの動作であり、MAUI から制御できません。同期されるべきでないデバイス固有のトークンは保存しないでください。
Windows の制限
- キー名:最大 255 文字
- 値:設定ごとに最大 8 KB
- 複合ストレージ:合計最大 64 KB
よくある間違い
// ❌ 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));
判断の指針
| 質問 | 回答 |
|---|---|
| トークン、パスワード、API キーを保存しますか? | ✅ SecureStorage を使用します |
| ユーザーの設定や環境設定を保存しますか? | ❌ 代わりに Preferences を使用します |
| 大きなファイルやバイナリデータを保存しますか? | ❌ ファイルシステムと暗号化を使用します |
| デバイス間の同期が必要ですか? | ⚠️ iOS では iCloud キーチェーンを通じて自動的に同期されます |
| アンインストール時にデータを消去する必要がありますか? | ⚠️ Android でのみ可能で、iOS ではできません |
テスト容易性:必ず DI を使用する
テスト容易性を確保するため、ViewModel から SecureStorage.Default を直接呼び出さないでください。代わりに ISecureStorageService インターフェイスでラップしてください。モックの例を含む DI ラッパーの完全なパターンについては、references/secure-storage-api.md を参照してください。
// ❌ 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);
}
チェックリスト
- Android:自動バックアップを無効にするか、セキュアストレージをバックアップ対象から除外する
- Android:すべての
GetAsync呼び出しを try/catch で囲む - iOS:キーチェーンエンタイトルメントを設定する(シミュレーターのみ。実機用ビルドでは削除する)
- 値は文字列のみとし、複雑なデータは JSON にシリアライズする
- シークレット値をどこにもログ出力しない
-
SecureStorage.Defaultは ViewModel から直接ではなく、DI ラッパー経由でアクセスする