リバースエンジニアリング対策 / セキュリティ・ハードニング 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.

大原則:
  • クライアントに秘密を置かない。
  • クライアントで決定しない(料金・権限・抽選・ポイント等はすべてサーバー確定)。
  • 隠蔽はあくまでコスト増加策。機能的な防御はサーバー側で担う。
Guiding principles:
  • 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

クライアントで「見えるもの」の三層 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.4Manual 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_detectionLow (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

シンボルファイルの運用(重要) 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 bucketmobile-symbols/
Sentry / Firebase Crashlytics を使う場合は、それぞれの symbol uploader に 同じファイルを投入する。
⚠ Losing 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/)
If you use Sentry or Firebase Crashlytics, push the same files into their symbol uploaders.

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

11.6 リリース前チェックリスト 11.6 Pre-release checklist

ストア提出前に以下を全て満たすこと。

Verify every item before store submission.

11.7 将来の強化項目 11.7 Future hardening