0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Flutterで安全にOAuthトークンを保存するための完全ガイド

Last updated at Posted at 2025-11-18

はじめに

— 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_checker
  • flutter_jailbreak_detection
  • device_info_plus + 自前の判定

Root を検出したら:

  • トークン保存禁止
  • Refresh Token ローテーション短縮
  • API リクエスト制限

などの戦略が取れる。


4.2 biometric + AES による二段レイヤー化

refresh token は flutter_secure_storage だけだと「暗号化キー」が端末依存になり弱い。

より安全にするなら:

  1. ユーザーの生体認証(指紋/FaceID)
  2. 生体認証から AES key(session key)生成
  3. その 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 の短寿命化

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?