リバースエンジニアリング対策 / セキュリティ・ハードニング Reverse Engineering Hardening
Flutter 製 Parky モバイルアプリに対するリバースエンジニアリング(以下 RE)の 現実的な脅威と、Parky として採用する多層防御(Defense in Depth)を記述する。 本書の位置づけは 非機能要件(§10.4 セキュリティ) を補完する「実装・運用ガイド」。ビルドコマンド・外部ダッシュボード設定・リリース前 チェックリストまで踏み込む。
This document covers the realistic reverse-engineering (RE) threats against Parky's Flutter mobile app and the defense-in-depth measures Parky adopts. It complements Non-functional Requirements (§10.4 Security) as an implementation & operations guide — build commands, external dashboard settings, and a pre-release checklist.
- クライアントに秘密を置かない。
- クライアントで決定しない(料金・権限・抽選・ポイント等はすべてサーバー確定)。
- 隠蔽はあくまでコスト増加策。機能的な防御はサーバー側で担う。
- Never ship a secret in the client.
- Never let the client decide (fees, authorization, lotteries, points — always server-confirmed).
- Obfuscation just raises the attacker's cost. Functional defense lives on the server.
11.1 脅威モデル — Flutter AOT の現実 11.1 Threat model — the reality of Flutter AOT
Flutter のリリースビルドは Dart を AOT コンパイルしてネイティブ ARM / x64
バイナリ(Android: libapp.so、iOS: App.framework)に落とす。
React Native や Cordova のように JavaScript がまるごと読めるわけではないが、
完全なブラックボックスでもない。
Flutter release builds AOT-compile Dart into native ARM / x64 binaries
(Android: libapp.so, iOS: App.framework). Unlike
React Native or Cordova, the JavaScript isn't there to read — but it is
not a black box either.
代表的な攻撃ツール Common attack tools
- reFlutter — Flutter engine 自体を patch して通信フック / SSL Pinning bypass。Flutter 界隈の定番。
- Blutter / Doldrums — Dart snapshot をパースしてクラス名・メソッド名・文字列リテラルを復元。
- Frida / objection — 実行時にフック・関数書き換え。
- apktool / Ghidra / IDA Pro — バイナリの静的解析。
- reFlutter — patches the Flutter engine itself for traffic hooking / SSL pinning bypass. The go-to tool in the Flutter scene.
- Blutter / Doldrums — parses the Dart snapshot to recover class names, method names, string literals.
- Frida / objection — runtime hooking and function replacement.
- apktool / Ghidra / IDA Pro — static binary analysis.
クライアントで「見えるもの」の三層 What's visible in the client — three tiers
| 層Tier | 内容Contents | 抽出コストExtraction cost |
|---|---|---|
| A 平文で抜けるPlaintext, trivially extracted |
--dart-define で渡した SUPABASE_URL / SUPABASE_ANON_KEY /
MAPBOX_ACCESS_TOKEN / PARKY_API_BASE_URL、
API エンドポイント URL、ディープリンクスキーム、assets/ 配下の画像・JSON・フォント、
エラーメッセージ、pubspec の依存パッケージ名
--dart-define values (SUPABASE_URL, SUPABASE_ANON_KEY,
MAPBOX_ACCESS_TOKEN, PARKY_API_BASE_URL), API endpoints,
deep link schemes, anything under assets/, error messages, pubspec dependency names.
|
極小(strings で抜ける)Near zero (strings does it) |
| B そこそこ復元できるRecoverable with effort | Dart のクラス名・メソッド名、定数、URL パス、SQL 断片 Dart class/method names, constants, URL paths, SQL fragments |
中(Blutter 等)—
--obfuscate で大幅に上げられる
Medium (Blutter et al.) —
significantly raised by --obfuscate
|
| C 読みづらいHard to read | 関数の実装ロジック(ARM アセンブリまで落ちている) Function implementation logic (down to ARM assembly) | 高。ただし「不可能」ではない。隠蔽に頼るな High — but not impossible. Don't rely on obscurity |
想定攻撃者と現実的な守備範囲 Attacker profile vs. realistic defense
| 攻撃者Attacker | 動機Motivation | Parky の防御可否Can Parky defend? |
|---|---|---|
| 趣味(curl で叩いて遊ぶ)Hobbyist (hitting the API with curl) | 興味・遊びCuriosity | ほぼ完全に抑止可能Near-total deterrence |
| 量産 bot / クラックツールMass bots / crack tools | 金銭(レビュー水増し、アカウント大量作成等)Money (fake reviews, mass accounts, etc.) | App Check + rate limit + 異常検知で抑止可能Containable with App Check + rate limit + anomaly detection |
| 競合(真剣に機能パクる)Competitor (serious cloning) | 模倣Imitation | ロジック隠蔽は不可能。特許・契約・開発速度で勝つObscuring logic won't work. Win via patents, contracts, and speed |
| 国家 / APTState / APT | — | 対象外(スコープ外)Out of scope |
11.2 対応ロードマップ 11.2 Mitigation roadmap
| 対策Measure | 効果Effect | コストCost | 状態Status |
|---|---|---|---|
| サーバー側で金額・権限・抽選を確定Server-confirmed fees / auth / lotteries | 改ざん無効化(最重要)Nullifies tampering (most critical) | 設計依存Design-dependent | ✅ 実装方針として確定済み(§10.4 改ざん防止)Confirmed policy (§10.4) |
| Supabase anon key + 全テーブル RLSSupabase anon key + RLS on all tables | key 漏洩しても自分の行しか触れないEven if the key leaks, users can only touch their own rows | — | ✅ 既存(DB レビュー 100% 準拠)Already in place (DB review 100% compliant) |
--obfuscate + --split-debug-info |
B 層(クラス/メソッド名)の抽出コストを大幅増Dramatically raises extraction cost for Tier B | 小(ビルドスクリプトに組込済)Low (baked into build script) | ✅ scripts/build_release.sh で常時適用Always applied via scripts/build_release.sh |
| Mapbox token の URL / Bundle ID 制限Mapbox token URL / Bundle ID restriction | token 漏洩しても他アプリ・他ドメインから使えないLeaked token useless outside the registered app/domain | 小(dashboard 設定のみ)Low (dashboard only) | ⚠ 手動対応(§11.4)Manual step (§11.4) |
| リリースでログを落とすStrip logs in release | A 層(文字列)の情報量を削減Reduces Tier A string leakage | 小Low | ✅ lib/ 直下は 0 件(2026-04-22 監査)0 occurrences under lib/ (2026-04-22 audit) |
| Firebase App Check(Play Integrity + App Attest)Firebase App Check (Play Integrity + App Attest) | 改造版アプリ / curl / emulator からの API 叩きを弾けるBlocks API calls from modified apps / curl / emulators | 中Medium | 🟡 計画中(FCM 導線に相乗り)Planned (piggyback on FCM integration) |
| SSL PinningSSL Pinning | MITM プロキシ・量産 bot 抑止(reFlutter で外される前提)Blocks MITM proxies / mass bots (assumed bypassable via reFlutter) | 中Medium | 🟡 App Check と合わせて検討To be considered with App Check |
| Root / Jailbreak 検出Root / Jailbreak detection | 改造端末からの利用を警告(強制停止はしない)Warns on modified devices (no hard block) | 小(flutter_jailbreak_detection)Low (flutter_jailbreak_detection) |
🟡 非機能要件に既記載(§10.4)、未実装Documented in §10.4, not yet implemented |
| 最低バージョン強制アップデートMinimum version force update | 脆弱性修正を確実に全ユーザーに届けるEnsures vulnerability fixes reach every user | 小(app_config or Remote Config)Low (app_config or Remote Config) |
🟡 非機能要件に既記載(§10.6)、未実装Documented in §10.6, not yet implemented |
| API rate limit / 異常検知API rate limit / anomaly detection | クラック済アプリからの大量リクエストも抑止Throttles even cracked-app floods | 中(Workers BFF 側)Medium (Workers BFF side) | 🟡 BFF 実装と並行Parallel to BFF implementation |
11.3 リリースビルド手順(必須) 11.3 Release build procedure (required)
リリース提出用のビルドは、必ず
mobileapp/prototype/flutter/scripts/build_release.sh
(Windows では build_release.cmd)経由で作成する。
手動で flutter build を叩くと --obfuscate や
--split-debug-info のつけ忘れでストアに平文バイナリを上げる事故が発生する。
Always produce store-submission builds via
mobileapp/prototype/flutter/scripts/build_release.sh
(or build_release.cmd on Windows).
Running flutter build by hand risks forgetting
--obfuscate / --split-debug-info and shipping a
plaintext binary to the store.
事前準備 Prerequisites
以下の環境変数を export してから実行する。値は 1Password の
PJ|Parky 配下(Supabase/プロジェクト認証,
Mapbox/アクセストークン)から取得。
Export these environment variables before running. Values come from 1Password under
PJ|Parky (Supabase/project auth, Mapbox/access token).
export SUPABASE_URL="https://<project>.supabase.co"
export SUPABASE_ANON_KEY="eyJhbGci..."
export MAPBOX_ACCESS_TOKEN="pk.eyJ1..."
# (任意)BFF baseUrl を dev 以外に向けたい場合
# export PARKY_API_BASE_URL="https://api.parky.co.jp"
実行 Run
# macOS / Linux — Android + iOS 両方
bash scripts/build_release.sh all
# 個別
bash scripts/build_release.sh android # AAB のみ
bash scripts/build_release.sh ios # IPA のみ(macOS 限定)
# Windows(Android のみ)
scripts\build_release.cmd
スクリプトがやること What the script does
flutter pub getflutter build appbundle --release --obfuscate --split-debug-info=build/symbols/<ts>/androidflutter build ipa --release --obfuscate --split-debug-info=build/symbols/<ts>/ios(macOS 時)- 必要な
--dart-defineを環境変数から自動で組み立てて注入 - シンボル出力先を JST タイムスタンプ付きで保存(
build/symbols/YYYY-MM-DD_HH-MM/)
flutter pub getflutter build appbundle --release --obfuscate --split-debug-info=build/symbols/<ts>/androidflutter build ipa --release --obfuscate --split-debug-info=build/symbols/<ts>/ios(on macOS)- Assembles required
--dart-defineflags from env vars - Writes symbols to a JST-timestamped dir (
build/symbols/YYYY-MM-DD_HH-MM/)
シンボルファイルの運用(重要) Symbol file operations (critical)
build/symbols/ は失うと取り返しがつかない。
--split-debug-info で分離したシンボルファイル(app.android-arm64.symbols 等)は、
本番ユーザーのクラッシュレポートから元のクラス名・メソッド名・行番号を復元するために必須。
難読化済みバイナリを後から取得しても、このシンボルがなければ Dart スタックトレースは
永久に読めない。Git にはコミットしない(.gitignore で除外済み)が、
ビルドごとに必ず以下のいずれかに退避すること:
- GitHub Actions artifact(CI ビルド時は自動)
- 1Password の PJ|Parky 配下にアップロード(バージョンタグ + 日付)
- Supabase Storage の private bucket(
mobile-symbols/)
build/symbols/ is irrecoverable.
The separated symbol files (app.android-arm64.symbols, etc.) are required
to recover original class names, method names, and line numbers from production
crash reports. Without them, the Dart stack trace is permanently unreadable —
even if you still have the obfuscated binary. They are not committed
(.gitignored), so every build must be archived to one of:
- GitHub Actions artifacts (automatic in CI)
- Uploaded to 1Password under PJ|Parky (tagged by version + date)
- Supabase Storage private bucket (
mobile-symbols/)
11.4 外部ダッシュボード設定チェックリスト 11.4 External dashboard settings checklist
token がバイナリから抜かれても別のアプリ / ドメインから使えなくする設定。 これがダッシュボード側の一度きりの作業で最も費用対効果が高い。
Settings that make extracted tokens useless outside the registered app/domain. One-time dashboard work — highest ROI of any measure here.
| サービスService | やることAction | 場所Where |
|---|---|---|
| Mapbox | 本番用 public token に URL Restriction(Web)と Bundle ID 制限(iOS)/ Android application ID 制限を設定。 Set URL Restriction (Web) and Bundle ID (iOS) / Android application ID restrictions on the production public token. | Mapbox Account ↗ |
| Firebase (FCM) | Firebase プロジェクトの App Check を「Enforce」モードに切り替え、 Play Integrity(Android)と App Attest(iOS)を有効化。 Enable App Check in Enforce mode; turn on Play Integrity (Android) and App Attest (iOS). | Firebase Console ↗ |
| Supabase | 全テーブルの RLS が enable で、anon ロールに対して 「自分の行のみ」のポリシーが張られているか改めて確認。 service_role は絶対にクライアントに入れない。 Re-verify that RLS is enabled on every table, and that policies restrict anon role to "rows they own". Never put service_role in the client. | Supabase Dashboard ↗ |
| Google Cloud (Maps / Places 将来利用時) | API キーに Application restrictions(Android package + SHA-1 / iOS bundle ID)と API restrictions(使用 API のみ許可)を両方設定。 Apply Application restrictions (Android package + SHA-1 / iOS bundle ID) and API restrictions (allow only APIs actually used). | GCP Credentials ↗ |
| Workers BFF (Cloudflare) | API ルートに IP / JWT / App Check token ベースの rate limit を設定。 既定値は「未認証: 60 req/min、認証済: 600 req/min」。 Apply IP / JWT / App Check token-based rate limits on API routes. Baseline: "60 req/min unauthenticated, 600 req/min authenticated". | Cloudflare Dashboard ↗ |
11.5 クライアントコードの書き方ルール 11.5 Client-side coding rules
- 秘密鍵・service_role・管理者 token はクライアントに絶対に入れない。値として書かないのはもちろん、assets にも置かない。
- 金額確定 / 権限判定 / 抽選 / ポイント付与 / プロモコード検証は必ずサーバー RPC / BFF で行う。クライアントは UI 表示用の「見せる値」だけを扱う。
- リリースで
print()/debugPrint()を残さない。必要な場合はloggerパッケージで level を Warning 以上に絞る。 - デバッグフラグ・test hook は
kReleaseModeガードで確実に無効化。 - 機密情報は
flutter_secure_storage(Keychain / Keystore)以外に置かない。SharedPreferencesは平文なので NG。 - Deep Link のクエリ / パスは受け取ったら必ずバリデーション。パラメータから DB 操作に直結させない。
- debug ビルドを絶対にストアに提出しない(AOT でなく JIT、Dart コードが丸見え)。
- OpenAPI 生成物(
lib/generated/parky_api/)にクライアント固有の隠したいロジックを足さない。公開仕様そのものなので隠蔽意味なし。
- Never ship secret keys, service_role, or admin tokens in the client — not in code, not in assets.
- Always do fee confirmation, authorization, lotteries, point grants, and promo-code checks via server RPC / BFF. The client only handles "what we show" values.
- No leftover
print()/debugPrint()in release. Useloggerwith level ≥ Warning when needed. - Gate debug flags and test hooks with
kReleaseModereliably. - Sensitive data only in
flutter_secure_storage(Keychain / Keystore).SharedPreferencesis plaintext — not acceptable. - Always validate deep link query / path parameters. Never wire them straight into DB ops.
- Never submit a debug build to the stores (JIT, Dart is wide open).
- Don't sneak hide-worthy logic into the generated
lib/generated/parky_api/. It is the public spec — obscurity is impossible.
11.6 リリース前チェックリスト 11.6 Pre-release checklist
ストア提出前に以下を全て満たすこと。
Verify every item before store submission.
- ☐
scripts/build_release.sh経由でビルドした(素のflutter buildは使っていない)。 - ☐
build/symbols/<ts>/を CI artifact / 1Password / Supabase Storage のいずれかに退避した。 - ☐ Mapbox token に URL / Bundle ID 制限が入っている(§11.4)。
- ☐ Supabase 全テーブル RLS enable、anon ポリシーは自分の行のみ。
- ☐
print()/debugPrint()の新規混入がlib/直下に無い(generated/は除外)。 - ☐
kReleaseModeガードがテスト用 UI / モックデータ切替に付いている。 - ☐ デバッグビルドを間違えて出していない(Flutter は
flutter runが JIT なので注意)。 - ☐ 最低バージョンチェックが
app_configまたは Remote Config に設定済み。 - ☐ Sentry / Crashlytics のシンボルアップロードが完了している(クラッシュ解析可能な状態)。
- ☐ Built via
scripts/build_release.sh(no bareflutter build). - ☐
build/symbols/<ts>/archived to CI artifacts / 1Password / Supabase Storage. - ☐ Mapbox token has URL / Bundle ID restrictions (§11.4).
- ☐ RLS enabled on every Supabase table; anon policies restricted to own rows.
- ☐ No new
print()/debugPrint()underlib/(excludinggenerated/). - ☐
kReleaseModeguards present around test UI / mock-data switches. - ☐ No accidental debug submission (watch out —
flutter runis JIT). - ☐ Minimum version check configured in
app_configor Remote Config. - ☐ Sentry / Crashlytics symbol upload completed (crash reports decodable).
11.7 将来の強化項目 11.7 Future hardening
- Firebase App Check(Play Integrity + App Attest) 導入 — FCM 導入に相乗りして、Workers BFF 側で App Check token を必須検証にする。
- SSL Pinning(
dio_certificate_pinning)— reFlutter で外される前提だが、量産 bot / MITM プロキシ抑止には有効。 - Root / Jailbreak 検出(
flutter_jailbreak_detection)— 警告のみ。ユーザー体験を壊さない範囲で。 - 文字列暗号化 — API エンドポイント等のリテラルを実行時まで暗号化(効果は限定的、過剰最適化の典型)。
- Play Integrity / App Attest 直接統合 — App Check を使わず個別統合する場合のオプション。
- Firebase App Check (Play Integrity + App Attest) — ride along with FCM integration; require App Check tokens on the Workers BFF.
- SSL Pinning (
dio_certificate_pinning) — assumed bypassable by reFlutter but still blocks mass bots / MITM proxies. - Root / Jailbreak detection (
flutter_jailbreak_detection) — warning only; keep UX intact. - String encryption — encrypt API-endpoint literals until runtime (limited gain; the classic over-optimization).
- Direct Play Integrity / App Attest integration — alternative if not using App Check.
- §10 非機能要件(§10.4 セキュリティの親項目)
- §2 システム全体像
- モバイルパートナー向け: NFR とストア対応
- §10 Non-functional requirements (parent of §10.4 Security)
- §2 System architecture
- For the mobile partner: NFR and store submission