flutter_secure_storageのエラーハンドリング
エラー/異常系ハンドリング
堅牢なアプリケーションを構築するためには、正常系のシナリオだけでなく、予期せぬエラーや非正常系のケースにいかに正しく対処するかが鍵となります。flutter_secure_storage のようなセキュリティに関わるライブラリでは、その重要性はさらに増します。そこで、発生しうる例外とその対処法、そして安全なログ運用について見ていきたいと思います。
例外マトリクス
flutter_secure_storage のメソッドは、様々な理由で失敗し、PlatformException をスローする可能性があります。この例外をただキャッチするだけでなく、その根本原因を推測し、適切なユーザー体験に繋げることが重要です。
以下に、代表的なエラーの原因と対処法のマトリクスを示します。
| 根本原因 (ネイティブ層の例外) | 状況の例 | Flutterでの検知 | 推奨される対処法 |
|---|---|---|---|
KeyPermanentlyInvalidatedException |
ユーザーが画面ロックを無効化/変更した。 |
read()/write()でPlatformExceptionが発生。 |
回復不能なエラー。 ユーザーに再ログインを促し、トークン等のデータを再取得・再保存する。 |
UserNotAuthenticatedException |
生体認証がキャンセルまたは失敗した。 |
PlatformExceptionが発生。エラーメッセージに"User not authenticated"等が含まれる。 |
一時的なエラー。 ユーザーに再度操作を試すよう促すか、操作をキャンセルするか選択させる。 |
KeyStoreException / ProviderException
|
Keystoreが一時的に利用不能、または破損している。 |
PlatformExceptionが発生。 |
回復不能な可能性が高い。 まずは再ログインを試してもらい、それでも解決しない場合はアプリの再インストール等を案内する。 |
IllegalBlockSizeException / BadPaddingException
|
データの復号に失敗した。データ破損の可能性。 |
PlatformExceptionが発生。 |
回復不能なエラー。 破損データとみなし、該当データを削除して再生成するフローに誘導する。 |
Future<String?> _readValue(String key) async {
try {
return await storage.read(key: key);
} on PlatformException catch (e) {
// エラーコードやメッセージに応じて分岐
if (e.code == 'KeyPermanentlyInvalidatedException') {
// 再ログイン処理へ
} else {
// その他の汎用エラー処理
}
return null;
}
}
リトライ/マイグレーション
リトライ戦略
OSの一時的な不整合などで、I/O処理が瞬間的に失敗することもあります。しかし、flutter_secure_storageで発生するエラーの多くは、鍵の失効など、リトライしても解決しない永続的な問題です。そのため、むやみにリトライ処理を実装するのは得策ではありません。もしリトライを実装するとしても、回数に上限を設け、特定の回復可能なエラー(例:ネットワーク起因など、このライブラリでは稀)に限定すべきです。
データマイグレーション
より重要なのは、ライブラリのバージョンアップに伴うデータ形式の変更への対応です。flutter_secure_storageは、過去に内部実装を大きく変更しており、古いバージョンで保存されたデータを新しいバージョンで読み込むためのマイグレーションパスを提供してきました。
例えば、プラグインの初期化時に、古いストレージ形式の存在をチェックし、見つかった場合はそのデータを新しい形式(EncryptedSharedPreferences)に変換して書き直す、という処理が内部で行われます。
開発者は、ライブラリのメジャーバージョンを上げる際には、必ずCHANGELOGを熟読し、データ移行に関する記述がないかを確認する必要があります。これを怠ると、アップデートした途端にすべてのユーザーが保存したデータにアクセスできなくなる、といった深刻な事態に繋がりかねません。
## ログの機密値マスク
デバッグや障害調査のために、アプリケーションの動作をログに記録することは一般的です。しかし、セキュリティライブラリを扱う際は、細心の注意が必要です。
警告: flutter_secure_storage で扱うキーやバリューは、決してログに出力してはいけません。
try-catch で捕捉した例外オブジェクトを、安易に print(e) や FirebaseCrashlytics.instance.recordError(e, stack) のように記録すると、例外メッセージの中に機密情報(トークンの一部や、データ識別のためのキー名など)が含まれてしまう危険性があります。
// やってはいけない例
try {
await storage.write(key: 'auth_token', value: '...very secret...');
} catch (e) {
// このログには 'auth_token' というキー名が含まれてしまう可能性がある
print('Failed to write to secure storage: $e');
}
// 推奨される例
try {
await storage.write(key: 'auth_token', value: '...very secret...');
} catch (e) {
// どのようなキーで失敗したかに関わらず、固定のメッセージを出力する
print('Failed to write to secure storage. Key: [MASKED]');
// Crashlyticsなどにも、加工した情報を送る
FirebaseCrashlytics.instance.recordError(
Exception('SecureStorageWriteError'),
stack,
reason: 'Failed to write key: auth_token', // 可変部分はカスタムキーとして送るなど工夫する
);
}
ログ収集基盤やクラッシュレポートサービスに送られる情報に機密情報が混入しないよう、ログに出力する情報は慎重に選び、常にマスキングを意識した実装を心がけてください。
Discussion