はじめに
— flutter_secure_storage だけでは安全じゃない理由と、実務で使える対策まとめ —
Flutter は “クロスプラットフォームで機能する” という特性ゆえに、
OAuth のトークン保存が Web・Android・iOSのセキュリティギャップを全部踏む地雷原 になります。
特に、
Access Token / Refresh Token の保存戦略をミスると、アプリ全体の安全性が破壊される。
この記事では、Flutter アプリにおけるトークン保存のベストプラクティスと、
flutter_secure_storage の注意点、さらに高度な防御テクニックまで紹介します。
1. 結論:Flutter のトークン保存はこうする
| トークン | 保存場所 | 理由 |
|---|---|---|
| Access Token | 保存しない(メモリ) | 盗難リスク最大なのでメモリ使い捨て |
| Refresh Token | flutter_secure_storage | 端末ごとの暗号化を自動利用できる |
しかし……
ここからが本題。
flutter_secure_storage = 安全ではないケースがある
Android の暗号化は端末依存。
root 化端末では完全に無力。
2. flutter_secure_storage が“完全ではない”理由
flutter_secure_storage は便利だが、以下の問題がある。
2.1 Android の暗号化品質が端末により異なる
Android は端末メーカー・OS バージョンでセキュリティ層が変わる。
- Android 6–8:Keystore 不完全
- 一部メーカー端末:暗号化 API がバグで弱い
- root 化済み端末:全ての保護が無効化される
flutter_secure_storage も内部実装は EncryptedSharedPreferences または KeyStore+AES に依存しているため、
プラットフォームレベルで弱い場合はそのまま弱い。
2.2 root 化端末では全て露出
これは Flutter に限らないが、root 端末では:
- KeyStore から鍵を強制ダンプ
-
/data/data/<app>/shared_prefs/を直接読む - frida / xposed で SecureStorage API を hook
- メモリダンプで復号前のトークン抽出
などが可能。
つまり:
root 化端末には勝てない。勝とうとしてはいけない。
これは業界共通の結論。
2.3 flutter_secure_storage が暗号化しないケースもある(重要)
ドキュメントに書かれていない“落とし穴”として、
- 一部 Android では「暗号化が無効化」される
- ユーザーデータ暗号化を無効化している端末では平文保存
というケースがある。
暗号化されない → ただの SharedPreferences → 即死。
3. ベストプラクティス
✔ 絶対ルール
Access Token は保存しない
- 保存する必要はない
- 保存した瞬間、攻撃対象になる
- メモリで持ち、短命(数分)で失効させるのが正しい
Refresh Token だけ secure_storage に保存
- 本命はこっち
- ここを厳重に守るべき
4. より安全にするための追加対策(実務向け)
Flutter アプリで堅牢にするなら、この3つが効く。
4.1 root / jailbreak 検出(Root Detection)
root 化端末では保護不可能なので、
アプリの使用を制限する or 警告を出すのが正しい。
例:Flutter で root 検出
root_checkerflutter_jailbreak_detection-
device_info_plus+ 自前の判定
Root を検出したら:
- トークン保存禁止
- Refresh Token ローテーション短縮
- API リクエスト制限
などの戦略が取れる。
4.2 biometric + AES による二段レイヤー化
refresh token は flutter_secure_storage だけだと「暗号化キー」が端末依存になり弱い。
より安全にするなら:
- ユーザーの生体認証(指紋/FaceID)
- 生体認証から AES key(session key)生成
- その AES でトークンを再暗号化し保存
という 二段レイヤー構造が最強。
イメージ
[トークン]
↓ AES(derived from biometric)
[暗号化トークン]
↓ flutter_secure_storage(OS暗号化)
[暗号化された暗号データ]
二重ロックで突破難易度が跳ね上がる。
4.3 Refresh Token ローテーションを短めにする
Refresh Token 漏洩のダメージを最小化する。
- 通常 → 30日
- 高セキュリティ用途 → 7日
- さらに安全 → 24–48時間(Auth0 Style)
ローテーションの度に古い token を無効化すれば、
盗まれたとしても「次の更新で切れる」。
5. 実装例(Flutterコード付き)
保存
final storage = const FlutterSecureStorage();
await storage.write(
key: "refresh_token",
value: refreshToken,
);
読み取り
final token = await storage.read(key: "refresh_token");
生体認証で AES Key を生成(擬似コード)
final auth = LocalAuthentication();
final success = await auth.authenticate(
localizedReason: 'Authenticate to decrypt token',
);
if (success) {
final aesKey = deriveAesKeyFromBiometric();
final decrypted = decryptAES(encryptedToken, aesKey);
}
root 検出(例)
import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart';
final isRooted = await FlutterJailbreakDetection.jailbroken;
if (isRooted) {
// トークン保存制限
}
6. Flutter トークン保存「安全度マップ」
| 観点 | 保存場所 | 安全度 |
|---|---|---|
| localStorage | ❌ 終わり | 0/10 |
| SharedPreferences | ❌ 平文 | 1/10 |
| flutter_secure_storage | ⚠ 標準 | 6/10 |
| flutter_secure_storage+root検出 | 👍 | 7/10 |
| さらに biometric + AES 2段暗号化 | 💪 最強構成 | 9.5/10 |
100% は存在しない
が、この構成なら“実務的に突破困難”。
まとめ
Flutter は便利だけど、セキュリティは自動で担保されない。
最低ライン
- Access Token → 保存しない
- Refresh Token → flutter_secure_storage
- root 端末 → 警告 or ブロック
- Refresh Token ローテーション短め
攻撃に強い構成
- biometric + AES の二段暗号化
- secure_storage を暗号化層の「外側」として使う
- frida/xposed 対策(Obfuscation / Hook検出)
- メモリ上 token の短寿命化