# Runbook: Mobile Security (Parky Flutter App)

最終更新: 2026-04-28 (Wave 3A / S-03 + S-04)

Parky モバイルアプリの「セキュリティ三層」(RASP / 生体認証 / 証明書ピンニング) の運用手順。

| 層 | 目的 | 実装ファイル | 実装パッケージ |
|---|---|---|---|
| RASP | ジェイルブレイク・改ざん検知 | `lib/services/security/jailbreak_check.dart` | `freerasp` |
| 生体認証 | 重要アクション直前の step-up | `lib/services/security/biometric_gate.dart` | `local_auth` |
| 証明書ピンニング | MITM 防止 (Supabase / Parky API) | `lib/services/security/cert_pinning_service.dart` | `http_certificate_pinning` |

## 1. freerasp (RASP) 本番化チェックリスト

### 1.1 Android signing cert hash の取得

`android/app/build.gradle` の release keystore を CI に配備した後、SHA-256 fingerprint を取得して
`jailbreak_check.dart` の `signingCertHashes` を差し替える。

```bash
# 取得 (release.jks を持っているマシンで実行)
keytool -list -v -keystore release.jks -alias parky \
  | awk '/SHA256:/ { print $2 }'                      \
  | tr -d ':' | xxd -r -p | base64
```

差し替え対象:

```dart
// lib/services/security/jailbreak_check.dart
final androidConfig = AndroidConfig(
  packageName: 'co.parky.app',
  signingCertHashes: <String>[
    // 上記コマンドの出力をここに貼る
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==',
  ],
  ...
);
```

`FIXME(2026-04-28)` マーカーがあるので解消したらコメントごと削除する。

### 1.2 iOS Team ID の取得

[Apple Developer Account](https://developer.apple.com/account) → Membership → Team ID (10桁英数)。

```dart
final iosConfig = IOSConfig(
  bundleIds: <String>['co.parky.app'],
  teamId: 'XXXXXXXXXX', // ← Team ID
);
```

### 1.3 Android Gradle 要件

freerasp Android 実装は以下を要求する可能性が高い (実機ビルドで要確認):

- `compileSdkVersion >= 34`
- `minSdkVersion >= 23`
- AGP 8 系で namespace 必須 (旧 `flutter_jailbreak_detection` の AGP 8 障害は freerasp で解決済)

ビルド失敗時は `android/app/build.gradle` の `compileSdkVersion` / `minSdkVersion` を確認。
※ Windows 開発機では Flutter Android ビルドが通せないため、変更は Mac/Linux の CI 環境で検証する。

### 1.4 検知シグナルの Sentry tag

freerasp が tampering を検知すると、`jailbreak_check.dart` の `_reportThreat` が
Sentry に以下の tag 付きで `captureMessage` を送信する:

| Tag | 値 | 用途 |
|---|---|---|
| `component` | `jailbreak_check` | コンポーネント絞り込み |
| `device.tampered` | `true` | tampering 全般のフィルタ |
| `tamper.{signal}` | `true` | 個別シグナル別の集計 (`tamper.privilegedAccess` 等) |

監視対象 signal: `privilegedAccess` / `debug` / `simulator` / `appIntegrity` / `hooks` /
`secureHardware` / `systemVPN` / `passcode` / `deviceBinding` / `obfuscationIssues` /
`devMode` / `unofficialStore`

### 1.5 アプリブロック方針

> Parky は freerasp で「アプリをブロック」しない。検知はリスク警告 + テレメトリ送信にとどめる。

`TalsecConfig.killOnBypass` は default の `false` のまま。誤検知で正規ユーザーが弾かれる
ほうがビジネス影響が大きいため、検知 → 通報 → 運用判断のフローで運用する。

## 2. local_auth (生体認証) 本番化チェックリスト

### 2.1 iOS Info.plist

`ios/Runner/Info.plist` に以下を追加 (Mac で要編集、Windows 機からは触らない):

```xml
<key>NSFaceIDUsageDescription</key>
<string>サインアウトの確認等、重要な操作の本人確認に使用します。</string>
```

未追加の場合、FaceID 端末で `LocalAuthentication.authenticate` がクラッシュする。
TouchID は文言不要だが、FaceID/TouchID 共通の説明として上記を必ず入れる。

### 2.2 Android AndroidManifest.xml

`android/app/src/main/AndroidManifest.xml` の `<manifest>` 直下に追加:

```xml
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- API < 28 互換 (BiometricPrompt → FingerprintManager fallback) -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
```

### 2.3 適用箇所 (戦略)

> 全画面に貼ると UX が壊れる。リスクの高い strategic な箇所だけ。

現状の適用箇所:

| 画面 | アクション | 適用済 |
|---|---|---|
| `lib/screens/profile/settings_screen.dart` | サインアウト | YES (Wave 3A) |
| `lib/features/premium/...` (TBD) | IAP 購入確定 | FIXME(2026-04-28) — IAP 画面確定後に配線 |
| `lib/features/profile/.../profile_edit_screen.dart` | メアド変更 | FIXME(2026-04-28) — 変更検知後のみ要 step-up |
| 退会 (deleted_at flag 設定) | 退会確定 | FIXME(2026-04-28) — 退会導線実装時に配線 |

### 2.4 失敗時の挙動

`BiometricGate.requireForAction` の戻り値:

- `success`: アクション続行
- `failed`: 中断 (UI 側で snackbar 等を出す判断は呼び出し側)
- `userCanceled`: 中断 (静かに戻す)
- `unavailable`: 端末未対応 → **アクションを通す** (誤動作で UX を壊さない)

## 3. cert pinning (証明書ピンニング) 運用

実装: `lib/services/security/cert_pinning_service.dart`

### 3.1 ピン取得コマンド

```bash
openssl s_client -connect <host>:443 -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -pubkey \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary \
  | base64
```

### 3.2 現行 pin set (2026-04-28)

| Host | Pin (SPKI SHA-256) | 種別 | Rotation |
|---|---|---|---|
| Supabase data plane | `kIdp6NNEd8wsugYyyIYFsi1ylMCED3hZbSR8ZFsa/A4=` | GTS WE1 intermediate | ~3 年 |
| Supabase root backup | `mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=` | GTS Root R4 | ~10 年 |
| Supabase leaf | `GU2W4j1P24T3sqlI+o6YTnidzz0PI8fB/Gvd2ITfSZE=` | dev project leaf | ~12 ヶ月 |
| Parky API (CF Workers) | `kIdp6NNEd8wsugYyyIYFsi1ylMCED3hZbSR8ZFsa/A4=` | GTS WE1 intermediate | ~3 年 |
| Parky API (Xserver 旧) | `ngVpmshTjI2mGNZdkmE0BseYAQxwUyN7hAFExDPDwsg=` | CloudSecure RSA DV CA 2 | FIXME(2026-04-28) prod CF 移行後に削除 |
| Parky leaf | `MGpvY+qynViULn0WpbTi10bP+WLmW4pRfFuL6NUwWgw=` | dev-api leaf | ~12 ヶ月 |

### 3.3 ローテーション手順

1. 新証明書がデプロイされる **前** に新フィンガープリントを取得
2. `_kSupabasePins` / `_kParkyPins` に新旧 2 本を**並列登録**したアプリをリリース
3. 旧証明書失効後、旧フィンガープリントだけを別 PR で削除 (移行期間で 2 リリース)

### 3.4 緊急時 bypass

CI / 統合テストで pin に阻害されるケース用に環境変数 bypass を用意:

```bash
flutter build apk --dart-define=CERT_PINNING_DISABLED=true
```

ただし production リリースで `CERT_PINNING_DISABLED=true` が混入することがないよう、
リリース CI workflow では渡さないこと。debug build では `kDebugMode` で常に bypass される。

## 4. リリース前チェックリスト

- [ ] freerasp `signingCertHashes` を release keystore の SHA-256 で差し替えた
- [ ] freerasp `teamId` を 10桁の Apple Team ID で差し替えた
- [ ] freerasp `bundleIds` / `packageName` が release applicationId と一致
- [ ] iOS `Info.plist` に `NSFaceIDUsageDescription` を追加した
- [ ] Android `AndroidManifest.xml` に `USE_BIOMETRIC` / `USE_FINGERPRINT` を追加した
- [ ] cert pin が release エンドポイント (api.parky.co.jp) のフィンガープリントを含む
- [ ] Sentry に `device.tampered=true` の alert rule を追加 (week-over-week で異常検知)
- [ ] BiometricGate を強化したい次の画面 (IAP / 退会 / メアド変更) を計画 doc 化
