Files
revkit/docs/auth_flow_android.md
T

254 lines
11 KiB
Markdown

# Netflix Android 起動時認証フロー
> **対象:** Netflix Android v9.57.0 (build 63928)
> **デバイス:** Pixel 4a (5G) / bramble / Android 14
> **取得方法:** Frida フック (`hook_netflix_android.js`) + `run.py` による JSONL キャプチャ
> **キャプチャ日:** 2026-03-14
---
## 1. 概要
Netflix Android アプリの起動時に発生する認証・プロビジョニングフローを、キャプチャしたトラフィックから再構成した。
各 API のリクエスト・レスポンス詳細は [auth_flow_android_api.md](auth_flow_android_api.md) を参照。
### 1.1 通信先エンドポイント
| エンドポイント | 略称 | プロトコル | 用途 |
|---|---|---|---|
| `android14.appboot.netflix.com` | **appboot** | HTTPS (MSL レスポンス) | デバイス登録・`nfvdid` Cookie 発行 |
| `android14.prod.ftl.netflix.com` | **prod.ftl** | HTTPS + MSL | MSL API (`/nq/androidui/samurai/`) + GraphQL |
| `android14.prod.cloud.netflix.com` | **prod.cloud** | HTTPS + MSL | GraphQL via MSL |
### 1.2 通信の分類
| 分類 | Content-Encoding | ボディ形式 | 認証方式 |
|---|---|---|---|
| **MSL API** | `msl_v1` | CBOR → GZIP → JSON | MSL トークン (Master Token + User Auth Data) |
| **Non-MSL GraphQL** | なし | JSON | Cookie (`NetflixId` / `SecureNetflixId`) |
| **Appboot** | なし (レスポンスは `application/x-msl+json`) | Query String | `nfvdid` Cookie |
---
## 2. 認証フロー全体像
```mermaid
%%{init: {'theme':'dark'}}%%
sequenceDiagram
participant App as Netflix App
participant AB as appboot
participant FTL as prod.ftl<br/>(MSL API)
participant Cloud as prod.cloud<br/>(GraphQL)
Note over App: 起動
App->>App: ProxyESN 期限チェック (※)
rect rgba(180, 100, 50, 0.2)
Note over App,Cloud: Phase 1: リクエスト送信
App->>AB: POST /appboot/{BaseESN}
App->>FTL: [MSL] /getProxyEsn
App->>FTL: [MSL] /aleProvision #1 (RSA鍵交換)
App->>Cloud: [MSL] RenewSSOToken
App->>FTL: CurrentCountryQuery
App->>Cloud: [MSL] InterstitialForProfileGate
App->>Cloud: [MSL] InterstitialForLolomo
end
rect rgba(50, 180, 80, 0.2)
Note over App,Cloud: Phase 2: レスポンス受信
FTL-->>App: CurrentCountryQuery → +NetflixId, SecureNetflixId [Cookie]
FTL-->>App: getProxyEsn → MSL レスポンス
FTL-->>App: aleProvision #1 → MSL レスポンス
Cloud-->>App: RenewSSOToken → 200 OK
AB-->>App: appboot → nfvdid [Cookie] + DeviceIdToken
Cloud-->>App: InterstitialForLolomo → 200 OK
Cloud-->>App: InterstitialForProfileGate → 200 OK
end
rect rgba(60, 120, 200, 0.2)
Note over App,Cloud: Phase 3: 追加リクエスト
App->>Cloud: [MSL] AccountQuery
Cloud-->>App: AccountQuery → 200 OK
App->>FTL: [MSL] /aleProvision #2 (RSA鍵交換)
FTL-->>App: aleProvision #2 → 新 PXA ESN 発行
App->>App: proxyEsn.onKnown (新ESN保存)
App->>FTL: PromoProfileGateVideoDataQuery
FTL-->>App: 200 OK + NetflixId, SecureNetflixId [Cookie]
end
rect rgba(150, 150, 150, 0.15)
Note over App,Cloud: Phase 4: 遅延リクエスト
App->>FTL: [MSL] FetchConfigData
App->>Cloud: [MSL] AccountQuery #2
FTL-->>App: 200 OK
Cloud-->>App: 200 OK
end
```
---
## 3. フェーズ概要
### 3.1 Phase 0: ProxyESN 期限チェック
アプリ起動時、`ProxyEsn.$init` でキャッシュ済み PXA ESN の期限がチェックされる。期限切れの場合はサーバーから新しい PXA ESN を取得する。
> **※ キャプチャ時の注意:** 今回のキャプチャでは Frida フック (`hook_msl.js`) が `ProxyEsn.$init` で `expired` フラグを強制的に `true` に書き換えているため、TTL が残っていても必ず再取得が発生する。通常の動作では TTL が有効ならキャッシュがそのまま使用される。
### 3.2 Phase 1: リクエスト送信
起動後、以下のリクエストが並列に送信される:
1. **Appboot** — デバイス登録・Cookie 発行
2. **getProxyEsn** (MSL) — PXA ESN 取得
3. **aleProvision #1** (MSL) — RSA-OAEP-256 鍵交換
4. **RenewSSOToken** (MSL → GraphQL) — SSO トークン更新
5. **CurrentCountryQuery** — 国情報取得 + 認証 Cookie 発行
6. **InterstitialForProfileGate / InterstitialForLolomo** (MSL → GraphQL) — UI 中間画面チェック
### 3.3 Phase 2: レスポンス受信
Phase 1 のレスポンスが順次到着する。主要なデータ:
- **CurrentCountryQuery** のレスポンスで `NetflixId` / `SecureNetflixId` Cookie が発行される
- **appboot** のレスポンスで `nfvdid` Cookie が更新され、`x-netflix-deviceidtoken` が発行される
### 3.4 Phase 3: 追加リクエスト
Phase 2 のレスポンスを受けて追加リクエストが送信される:
1. **AccountQuery** (MSL → GraphQL) — アカウント情報取得
2. **aleProvision #2** (MSL) — 再鍵交換 → PXA ESN 確定
3. **PromoProfileGateVideoDataQuery** — プロフィール画面データ取得
### 3.5 Phase 4: 遅延リクエスト
起動から約 30 秒後、バックグラウンドで送信される:
1. **FetchConfigData** (MSL → Samurai) — デバイス・ストリーミング設定取得
2. **AccountQuery #2** (MSL → GraphQL) — アカウント情報再取得
---
## 4. 鍵交換と暗号化フロー
```mermaid
%%{init: {'theme':'dark'}}%%
sequenceDiagram
participant Client as Netflix App
participant Server as prod.ftl (MSL)
Note over Client: RSA-2048 鍵ペア生成
Client->>Server: aleProvision #1<br/>pubkey + scheme=A128GCM<br/>type=SOCKETROUTER
Server-->>Client: A128GCM セッション鍵<br/>(RSA-OAEP-256 で暗号化)
Note over Client: セッション鍵復号<br/>→ 以降の MSL 通信で使用
Client->>Server: aleProvision #2<br/>同一 pubkey
Server-->>Client: 新セッション鍵 + PXA ESN
Note over Client: PXA ESN 保存<br/>→ MSL ヘッダに使用
```
### 4.1 ALE Provision パラメータ
| パラメータ | 値 | 説明 |
|---|---|---|
| `keyx.scheme` | `RSA-OAEP-256` | 鍵交換暗号化方式 (RSA with OAEP padding, SHA-256) |
| `keyx.data.pubkey` | Base64 RSA 2048-bit | クライアントが生成する使い捨て公開鍵 |
| `scheme` | `A128GCM` | セッション暗号化方式 (AES-128-GCM) |
| `type` | `SOCKETROUTER` | プロビジョニング種別 |
| `ver` | `1` | プロトコルバージョン |
### 4.2 aleProvision が 2 回呼ばれる理由
1. **#1:** 起動直後に即座に鍵交換を開始。他のリクエストと並列。
2. **#2:** getProxyEsn のレスポンスを受けた後に再度鍵交換。PXA ESN の発行と紐づく。
同一の RSA 公開鍵を使うが、サーバー側で新しいセッション鍵が発行される。
---
## 5. Cookie フロー
```mermaid
%%{init: {'theme':'dark'}}%%
sequenceDiagram
participant App as Netflix App
participant AB as appboot
participant FTL as prod.ftl
participant Cloud as prod.cloud
Note over App: 起動時: nfvdid (既存) のみ保持
App->>AB: POST /appboot (nfvdid Cookie なし)
App->>FTL: CurrentCountryQuery
App->>Cloud: [MSL] RenewSSOToken
FTL-->>App: set-cookie: NetflixId=..., SecureNetflixId=...
Note over App: ← 認証 Cookie 取得
AB-->>App: set-cookie: nfvdid=... (更新)<br/>x-netflix-deviceidtoken: ...
Note over App: ← デバイス Cookie 更新 + Device ID Token 取得
App->>FTL: PromoProfileGateVideoDataQuery
Note over App: NetflixId / SecureNetflixId Cookie を送信
FTL-->>App: set-cookie: NetflixId=..., SecureNetflixId=...
Note over App: ← 認証 Cookie リフレッシュ
```
### 5.1 Cookie 一覧
| Cookie 名 | 発行元 | 用途 | 発行タイミング |
|---|---|---|---|
| `nfvdid` | appboot | デバイス識別子 | appboot レスポンス (set-cookie) |
| `NetflixId` | prod.ftl | ユーザー認証 | CurrentCountryQuery レスポンス |
| `SecureNetflixId` | prod.ftl | セキュア認証 (HTTPS only) | CurrentCountryQuery レスポンス |
### 5.2 重要な観察
- **MSL リクエストは Cookie に依存しない。** MSL は独自の Master Token + User Auth Data で認証を行う。
- **Non-MSL GraphQL は Cookie で認証する。** `NetflixId` / `SecureNetflixId` が必要。
- **`nfvdid` は全リクエストに付与される**が、認証には直接使われない (デバイストラッキング用)。
---
## 6. ProxyESN ライフサイクル
```mermaid
%%{init: {'theme':'dark'}}%%
stateDiagram-v2
[*] --> Cached: アプリ前回終了時に保存
Cached --> CheckTTL: 起動時に TTL チェック
CheckTTL --> Active: TTL 有効 → キャッシュ使用
CheckTTL --> Requesting: TTL 期限切れ → /getProxyEsn 送信
Requesting --> Provisioning: aleProvision #1 + #2
Provisioning --> Received: proxyEsn.response
Received --> Active: proxyEsn.onKnown<br/>(新 ESN キャッシュ保存)
Active --> [*]: MSL 通信で使用
```
### 6.1 ESN の変化
| タイミング | ESN |
|---|---|
| 起動時 (キャッシュ) | `NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-02025HJ6UHN1D...` |
| 更新後 | `NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202DQTKQKE1UP...` |
- `22594` = Widevine System ID (固定)
- 末尾の HMAC 部分のみが変化 (サーバー側で新規生成)
---
## 7. セキュリティ上の観察
1. **二重認証メカニズム:** MSL (Master Token ベース) と Cookie (NetflixId ベース) の 2 系統が並行して使われる。
2. **PXA ESN ローテーション:** PXA ESN は TTL ベースで管理され、期限切れ時にサーバーから新しい ESN を取得する。今回のキャプチャでは Frida が強制失効させているため、毎回再取得が発生している。
3. **鍵の使い捨て:** aleProvision の RSA 鍵ペアはセッション単位で生成される (2048-bit)。
4. **Persisted Query:** GraphQL クエリ本文をネットワーク上で送信しないため、API スキーマの隠蔽に寄与。
5. **Cookie の分離:** `SecureNetflixId` は Secure 属性付きで HTTPS でのみ送信される。
6. **並列リクエスト:** 起動時間を最小化するため、認証に必要な全リクエストが並列に送信される。
7. **トークンの用途限定:** `ssoToken` は `RenewSSOToken` のみ、`x-netflix-deviceidtoken` は appboot レスポンスでのみ観測された。これらのトークンが HTTP レベルで他の API に転送される様子はなく、MSL レイヤー内部での使用またはローカル保持が示唆される。