⚠️

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', // 可変部分はカスタムキーとして送るなど工夫する
  );
}

ログ収集基盤やクラッシュレポートサービスに送られる情報に機密情報が混入しないよう、ログに出力する情報は慎重に選び、常にマスキングを意識した実装を心がけてください。

関連

Androidにおけるflutter_secure_storageの仕組み

Discussion