はじめに
flutter_secure_storage はどのように鍵を生成し、データを読み書きし、そしてAndroidのバージョン差異を吸収しているのでしょうか?
参考: DeepWiki:flutter_secure_storage
初回鍵生成
データが初めて書き込まれる際、それを保護するための暗号鍵が生成されます。このプロセスは、AndroidのAPIレベルによって異なります。
APIレベル23以上 (EncryptedSharedPreferencesStorage)
Jetpack Securityライブラリの MasterKeys.getOrCreate() メソッドを通じて、Android Keystore内にマスターキーを生成します。
このメソッドは、指定された設定(KeyGenParameterSpec)に基づき、ハードウェアに保護された(可能な場合)AES-256-GCMキーを安全に作成します。
このキーはアプリ専用であり、他のアプリからアクセスすることはできません。一度生成されたキーは、以降の読み書きで再利用されます。
Q: resetOnError=true でストレージがリセットされたとき、マスターキーも消える?
A: resetOnError=trueでストレージがリセットされた場合、マスターキー自体はリセットされません。
Flutter Secure StorageのAndroid実装では、resetOnErrorオプションが有効な場合にストレージの初期化でエラーが発生すると、EncryptedSharedPreferencesに保存されているデータがクリアされます (FlutterSecureStorage.java:31)。
しかし、この処理はdeleteAllメソッドと同様に、EncryptedSharedPreferencesのedit().clear().apply()を使用してデータを削除するものであり(FlutterSecureStorage.java:118-120)、Android Keystoreに保存されているマスターキー自体は削除されません。
マスターキーは_androidx_security_master_key_というエイリアスでAndroid Keystoreに保存されており(MasterKey.java:132-143)、このキーはEncryptedSharedPreferencesの初期化時に再利用されます(FlutterSecureStorage.java:133-141)。
resetOnErrorオプションは、暗号化ストレージの初期化で問題が発生した際にアプリのクラッシュを防ぐためのセーフティメカニズムです(CHANGELOG.md:98)。このオプションを有効にすると、エラー発生時にすべての保存データが永続的に削除されますが、マスターキーは保持されるため、ストレージは正常に再初期化できます。マスターキー自体を削除する場合は、Android Keystore APIを直接使用するか、アプリデータの完全なクリアが必要になります。
Q: AES-GCMキーは「特殊なハードウェア」がある端末でしか作成されないのか?
A: いいえ。「256bitかつハードウェア保護」は 可能なら選択される最上位オプションというだけで、ハードウェアが無い端末でも AES-GCM は生成され、アプリ側の API 仕様は変わりません。
Q: StorageCipherとKeyCipherとは?
A: StorageCipher が「データ本体」を守り、KeyCipher がその「暗号鍵」をさらに守る二重構造になっています。攻撃者がファイルを抜き取っても Keystore を経由しなければ復号できない設計です。StorageCipherはSharedPreferences に保存する value を暗号化・復号するクラス。KeyCipherは上記 AES 秘密鍵(secretKey)そのものをラップ/アンラップ(wrap/unwrap)するための RSA ベースのクラス。
APIレベル18-22 (LegacyKeystoreStorage)
このバージョンでは、Android Keystoreの機能が限定的であるため、より手動のプロセスを踏みます。
- RSA鍵ペアの生成: Android Keystore内に、データの暗号化・復号に直接使うのではなく、"鍵を暗号化するための鍵"として機能するRSAの公開鍵/秘密鍵ペアを生成します。
- AESキーの生成: 実際にデータを暗号化するためのAESキーは、アプリがランダムに生成し、SharedPreferencesに(暗号化して)保存します。
set()
write(key: String, value: String) が呼び出されると、プラグインは受け取ったキーとバリューを暗号化して永続化します。
APIレベル23以上
EncryptedSharedPreferencesがすべての複雑な処理を担います。
-
keyは、同じ入力に対して常に同じ暗号化出力を生成する決定論的暗号化を用いて暗号化されます。これにより、暗号化されたキーをSharedPreferencesファイル内で検索できます。 -
valueは、同じ入力でも毎回異なる暗号化出力を生成する非決定論的暗号化を用いて暗号化されます。こちらの方がより高いセキュリティ強度を提供します。 - 暗号化されたキーとバリューのペアが、通常のSharedPreferencesファイル(実体はXML)に書き込まれます。
Q. 非決定論的暗号化とは?
A. 非決定論的暗号化とは、同じ平文と鍵でも暗号化時に乱数を加え、毎回異なる暗号文を生成する方式。これにより攻撃者は平文と暗号文の対応を推測しにくくなり、選択平文攻撃などへの耐性が向上。公開鍵でも対称鍵でも利用され、ランダム化で安全性を高める現代の標準技術。TLSやメッセージ暗号化で必須。
APIレベル18-22
- 保存すべき
valueごとに、新しいAESキーがランダムに生成されます。 - Keystoreに保存されているRSA公開鍵を使って、このAESキーを暗号化(ラップ)します。
- 生成したAESキーを使って、
valueを暗号化します。 - 「暗号化された
value」と「RSAで暗号化されたAESキー」のペアが、SharedPreferencesに保存されます。
get()
read(key: String) が呼び出されると、プラグインは暗号化されたデータを復号して返します。
APIレベル23以上
-
keyを決定論的暗号化し、それを基にSharedPreferencesから対応する「暗号化されたvalue」を取得します。 - Android Keystoreに格納されているマスターキーを使い、この「暗号化された
value」を復号します。 - 平文になった
valueをFlutter側に返します。
Q: Android Keystoreとは何か?
A: Android Keystoreは、秘密鍵や暗号鍵などの機密情報を安全に保管・使用できるシステムサービスです。ハードウェアセキュリティモジュール(HSM)を活用できるデバイスでは、鍵がアプリやOSに露出せず、安全に暗号化・復号処理が行われます。これにより、高セキュリティな鍵管理が実現されています。
APIレベル18-22
- SharedPreferencesから「暗号化された
value」と「RSAで暗号化されたAESキー」を取得します。 - Keystoreに保存されているRSA秘密鍵を使って、「RSAで暗号化されたAESキー」を復号し、平文のAESキーを取り出します。
- 取り出したAESキーを使って、「暗号化された
value」を復号します。 - 平文になった
valueをFlutter側に返します。
APIレベル分岐ロジック
flutter_secure_storageは、プラグインが初期化される際に、実行中のデバイスのAndroidバージョンを確認し、適切なストレージ戦略を選択します。
FlutterSecureStoragePlugin.kt の中で、以下のようなロジックによって処理が分岐されています。
// (簡易的な擬似コード)
private fun getStorage(): Storage {
if (_storage != null) {
return _storage!!
}
val sdkInt = Build.VERSION.SDK_INT
_storage = if (sdkInt >= Build.VERSION_CODES.M) {
// APIレベル23 (Marshmallow) 以上
EncryptedSharedPreferencesStorage(context)
} else {
// APIレベル22 (Lollipop_MR1) 以下
LegacyKeystoreStorage(context)
}
return _storage!!
}
この分岐により、開発者はAPIレベルの違いを意識することなく、read/write APIを呼び出すだけで、ライブラリが最適な方法でデータを保護してくれます。
Flutter↔︎Androidブリッジ
FlutterとAndroidネイティブコード間の通信は、MethodChannel を介して行われます。これは、プラットフォームの壁を越えて非同期にメッセージをやり取りするための橋渡し役です。
-
呼び出し (Flutter → Android):
- Flutter側で
_channel.invokeMethod('write', options)のようなコードが実行されます。 - メソッド名(例: 'write')と引数(キー、バリュー等を含む
Map)がシリアライズされ、Android側に送信されます。
- Flutter側で
-
処理 (Android):
-
FlutterSecureStoragePluginのonMethodCallメソッドが、Flutterからの呼び出しを受け取ります。 - メソッド名に応じて、前述の
getStorage()で取得したストレージ実装(EncryptedSharedPreferencesStorage等)の対応するメソッドを呼び出します。
-
-
結果 (Android → Flutter):
- 処理が成功すれば、
result.success(value)を通じて、読み取ったデータやnullがFlutter側に返されます。 - 例外が発生した場合は、
result.error(code, message, details)を通じてエラー情報が返されます。
- 処理が成功すれば、
この非同期メッセージングにより、暗号化などの重い処理がUIスレッドを妨げることなく、スムーズなユーザー体験が維持されます。