mirror of
https://github.com/qtmleap/revkit.git
synced 2026-09-28 14:01:50 +02:00
chore: prepare for commit message generation without code changes
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
# Netflix Android 9.57.0 — HTTP ヘッダー & Cookie リファレンス
|
||||
|
||||
Frida `hook_headers.js` (Cronet フック) により 1,274 リクエストをキャプチャして整理。
|
||||
|
||||
---
|
||||
|
||||
## 通信経路
|
||||
|
||||
| 経路 | 用途 | ライブラリ |
|
||||
|---|---|---|
|
||||
| Cronet (メイン) | 通常 HTTP (API, config, GraphQL, logs) | `org.chromium.net.impl.CronetUrlRequest` |
|
||||
| OkHttp | WebSocket 接続のみ | `okhttp3.internal.connection.RealCall` |
|
||||
|
||||
---
|
||||
|
||||
## Cookie 一覧
|
||||
|
||||
| Cookie 名 | 出現頻度 | 説明 |
|
||||
|---|---|---|
|
||||
| `nfvdid` | 1,271/1,274 | Netflix Virtual Device ID。Base64url エンコードされたバイナリ |
|
||||
| `flwssn` | 1,271/1,274 | Flow Session ID。UUID v4 形式 |
|
||||
| `gsid` | 1,166/1,274 | GraphQL Session ID。UUID v4 形式。ログイン後に付与 |
|
||||
| `NetflixId` | 105/1,274 | ユーザー認証 Cookie。未ログイン時は送信されない |
|
||||
| `SecureNetflixId` | 105/1,274 | HMAC 署名付き認証 Cookie。NetflixId と常にペア |
|
||||
|
||||
### Cookie 値の構造
|
||||
|
||||
```
|
||||
nfvdid=BQFmAAEBEEPj84LzHGpQ_ldxaVuQv8tgIBE9VAe3w-WeF5En4w5goMB6eLYVXqxblfzh23QC62wkeecr...
|
||||
→ Base64url エンコードされたバイナリトークン
|
||||
|
||||
flwssn=183a999e-9ce9-4f29-ac9f-3f8b3982c817
|
||||
→ UUID v4 (セッション単位で変化)
|
||||
|
||||
gsid=d4779eb6-16d4-4179-89af-a069f7f1fc07
|
||||
→ UUID v4 (ログイン後に生成)
|
||||
|
||||
NetflixId=v%3D3%26ct%3DBgjHlOvcAxKoAo...
|
||||
→ URL エンコード済み。デコードすると: v=3&ct=<base64url>&pg=<profile_guid>&ch=<checksum>
|
||||
|
||||
SecureNetflixId=v%3D3%26mac%3DAQEAEQABABTPJ-U_...%26dt%3D1773402757629
|
||||
→ URL エンコード済み。デコードすると: v=3&mac=<hmac>&dt=<epoch_ms>
|
||||
```
|
||||
|
||||
### Cookie の送信パターン
|
||||
|
||||
| エンドポイント種別 | nfvdid | flwssn | gsid | NetflixId | SecureNetflixId |
|
||||
|---|---|---|---|---|---|
|
||||
| appboot | - | - | - | - | - |
|
||||
| config (GET, 未ログイン) | ✓ | ✓ | - | ✓ | ✓ |
|
||||
| graphql (ftl, 未ログイン) | ✓ | ✓ | - | ✓ | ✓ |
|
||||
| pathEvaluator | ✓ | ✓ | - | ✓ | ✓ |
|
||||
| cl/2 (ログ) | ✓ | ✓ | - | ✓ | ✓ |
|
||||
| graphql (MSL 暗号化) | ✓ | ✓ | ✓ | - | - |
|
||||
| config (POST, MSL) | ✓ | ✓ | ✓ | - | - |
|
||||
| logblob | ✓ | ✓ | ✓ | - | - |
|
||||
|
||||
**パターン**: MSL 暗号化 (`Content-Encoding: msl_v1`) リクエストでは `NetflixId`/`SecureNetflixId` が省略され、代わりに `gsid` が付与される。平文リクエストではその逆。
|
||||
|
||||
---
|
||||
|
||||
## ヘッダー一覧
|
||||
|
||||
### 共通ヘッダー (ほぼ全リクエストに存在)
|
||||
|
||||
| ヘッダー | 値の例 | 説明 |
|
||||
|---|---|---|
|
||||
| `X-Netflix.Request.Client.Context` | `{"appState":"background","appView":"login"}` | アプリ状態の JSON |
|
||||
| `X-Netflix.zuul.brotli.allowed` | `true` | Brotli 圧縮許可 |
|
||||
| `X-Netflix.Request.Attempt` | `1` | リトライ番号 |
|
||||
| `X-Netflix.Request.Id` | `afa97a02e3a349807ca65a0517e386c7` | リクエスト一意 ID (hex) |
|
||||
| `X-Netflix.Client.Request.Name` | `FetchConfigDataWebRequest` | リクエスト種別名 |
|
||||
|
||||
### デバイス識別ヘッダー
|
||||
|
||||
| ヘッダー | 値 | 説明 |
|
||||
|---|---|---|
|
||||
| `X-Netflix.clientType` | `samurai` | クライアント種別 (Android = samurai) |
|
||||
| `X-Netflix.deviceMemoryLevel` | `HIGH` | メモリレベル |
|
||||
| `X-Netflix.appVer` | `9.57.0` | アプリバージョン |
|
||||
| `X-Netflix.esnPrefix` | `NFANDROID1-PRV-P-L3-` | ESN プレフィックス (PRV) |
|
||||
| `X-Netflix.androidApi` | `34` | Android API レベル |
|
||||
| `X-Netflix.esn` | `NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-...` | フル PXA ESN |
|
||||
| `X-Netflix.deviceFormFactor` | `PHONE` | フォームファクタ |
|
||||
|
||||
### セッション・コンテキストヘッダー
|
||||
|
||||
| ヘッダー | 値の例 | 説明 |
|
||||
|---|---|---|
|
||||
| `X-Netflix.session.id` | `1773552898264781980` | セッション ID (数値) |
|
||||
| `x-netflix.client.current-profile-guid` | `ZEULH5S2GNGCRAABCSG6J2EGGA` | 現在のプロファイル GUID |
|
||||
| `X-Netflix.request.uuid` | UUID v4 | リクエスト UUID |
|
||||
| `X-Netflix.request.toplevel.uuid` | UUID v4 | トップレベルリクエスト UUID |
|
||||
| `X-Netflix.tracing.cl.userActionId` | UUID v4 | ユーザーアクション追跡 ID |
|
||||
|
||||
### コンテキストヘッダー (`x-netflix.context.*`)
|
||||
|
||||
| ヘッダー | 値 | 説明 |
|
||||
|---|---|---|
|
||||
| `x-netflix.context.os-version` | `34` | OS バージョン |
|
||||
| `x-netflix.context.app-version` | `9.57.0` | アプリバージョン |
|
||||
| `x-netflix.context.locales` | `en-US` or `en-JP` | ロケール |
|
||||
| `x-netflix.context.ui-flavor` | `android` | UI プラットフォーム |
|
||||
| `x-netflix.context.form-factor` | `phone` | デバイス形状 |
|
||||
| `x-netflix.context.android.installer-source` | `com.android.vending` | インストール元 |
|
||||
| `x-netflix.context.operation-name` | `CurrentCountryQuery` | GraphQL オペレーション名 |
|
||||
| `x-netflix.context.feature-capabilities` | `supportsStudioBranding` | 機能フラグ |
|
||||
| `x-netflix.context.hawkins-version` | `5.13.0` | Hawkins バージョン |
|
||||
|
||||
### MSL 関連ヘッダー
|
||||
|
||||
| ヘッダー | 値 | 説明 |
|
||||
|---|---|---|
|
||||
| `Content-Encoding` | `msl_v1` | MSL 暗号化ボディ |
|
||||
| `x-netflix.client.android.mslrequest` | `true` | MSL リクエストフラグ |
|
||||
| `x-netflix.client.request.transport` | `http` | トランスポート種別 |
|
||||
|
||||
### その他
|
||||
|
||||
| ヘッダー | 値 | 説明 |
|
||||
|---|---|---|
|
||||
| `X-Netflix-Internal-Volley-Priority` | `NORMAL` / `HIGH` / `null` | Volley 優先度 |
|
||||
| `x-netflix.playback.main-content-viewable-id` | `81650338` | 再生中コンテンツ ID |
|
||||
| `X-Netflix.Request.Routing` | JSON | ルーティング設定 |
|
||||
| `X-Netflix.Request.NqTracking` | リクエスト名 | NQ 追跡用 |
|
||||
| `debugRequest` | `true` | デバッグフラグ (ログ系) |
|
||||
| `x-netflix.request.clcs.bucket` | `high` | CLCS バケット |
|
||||
|
||||
---
|
||||
|
||||
## エンドポイント一覧
|
||||
|
||||
### 1. appboot (POST)
|
||||
```
|
||||
POST https://android14.appboot.netflix.com/appboot/NFANDROID1-PRV-P-L3-?keyVersion=1&suspended=false
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Cookies: なし
|
||||
```
|
||||
アプリ起動時の初期化。ESN プレフィックスが URL パスに含まれる。Cookie なし。
|
||||
|
||||
### 2. config (GET)
|
||||
```
|
||||
GET https://android14.prod.ftl.netflix.com/nq/androidui/samurai/v1/config?method=get&path=...
|
||||
Cookies: nfvdid, flwssn, NetflixId, SecureNetflixId
|
||||
```
|
||||
デバイス設定・機能フラグ取得。URL クエリに大量のデバイス情報を含む。
|
||||
|
||||
### 3. samurai API (POST)
|
||||
```
|
||||
POST https://android14.prod.ftl.netflix.com/nq/androidui/samurai/~9.0.0/api?method=get&...
|
||||
Cookies: nfvdid, flwssn (+ NetflixId/SecureNetflixId or gsid)
|
||||
```
|
||||
メイン API エンドポイント。再生マニフェスト取得 (`licensedManifest`) 等。
|
||||
|
||||
### 4. GraphQL — ftl (POST)
|
||||
```
|
||||
POST https://android14.prod.ftl.netflix.com/graphql
|
||||
Content-Type: application/json
|
||||
Cookies: nfvdid, flwssn, NetflixId, SecureNetflixId
|
||||
```
|
||||
GraphQL API (平文)。`CurrentCountryQuery` 等。
|
||||
|
||||
### 5. GraphQL — cloud (POST, MSL)
|
||||
```
|
||||
POST https://android.prod.cloud.netflix.com/graphql
|
||||
Content-Encoding: msl_v1
|
||||
Content-Type: application/json
|
||||
Cookies: flwssn, nfvdid, gsid
|
||||
```
|
||||
GraphQL API (MSL 暗号化)。`InterstitialHook`, `AccountQuery` 等。
|
||||
|
||||
### 6. pathEvaluator (POST)
|
||||
```
|
||||
POST https://android.prod.cloud.netflix.com/nq/aui/endpoint/^1.0.0-mobile/pathEvaluator
|
||||
Cookies: nfvdid, flwssn, NetflixId, SecureNetflixId
|
||||
```
|
||||
UI パス評価。Falcor ベースのデータフェッチ。
|
||||
|
||||
### 7. cl/2 ログ (POST)
|
||||
```
|
||||
POST https://android14.logs.netflix.com/log/android/cl/2?TAG=LOG_CLV2
|
||||
Content-Type: application/json
|
||||
Cookies: nfvdid, flwssn, NetflixId, SecureNetflixId
|
||||
```
|
||||
クライアントログ送信 (平文)。
|
||||
|
||||
### 8. logblob (POST, MSL)
|
||||
```
|
||||
POST https://android14.logs.netflix.com/log/android/logblob/1
|
||||
Content-Encoding: msl_v1
|
||||
Cookies: flwssn, nfvdid, gsid
|
||||
```
|
||||
バイナリログ送信 (MSL 暗号化)。
|
||||
|
||||
---
|
||||
|
||||
## cURL 例
|
||||
|
||||
### config 取得 (全ヘッダー付き)
|
||||
```bash
|
||||
curl -X GET \
|
||||
"https://android14.prod.ftl.netflix.com/nq/androidui/samurai/v1/config?method=get&responseFormat=json" \
|
||||
-H "X-Netflix.clientType: samurai" \
|
||||
-H "X-Netflix.appVer: 9.57.0" \
|
||||
-H "X-Netflix.esnPrefix: NFANDROID1-PRV-P-L3-" \
|
||||
-H "X-Netflix.esn: NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-..." \
|
||||
-H "X-Netflix.androidApi: 34" \
|
||||
-H "X-Netflix.deviceFormFactor: PHONE" \
|
||||
-H "X-Netflix.deviceMemoryLevel: HIGH" \
|
||||
-H "X-Netflix.session.id: 7994808" \
|
||||
-H "X-Netflix.zuul.brotli.allowed: true" \
|
||||
-H "X-Netflix.Request.Attempt: 1" \
|
||||
-H "X-Netflix.Request.Id: $(uuidgen | tr -d '-')" \
|
||||
-H "X-Netflix.Client.Request.Name: FetchConfigDataWebRequest" \
|
||||
-H "X-Netflix.Request.Client.Context: {\"appState\":\"foreground\",\"appView\":\"home\"}" \
|
||||
-H "x-netflix.context.os-version: 34" \
|
||||
-H "x-netflix.context.app-version: 9.57.0" \
|
||||
-H "x-netflix.context.locales: en-US" \
|
||||
-H "x-netflix.context.ui-flavor: android" \
|
||||
-H "x-netflix.context.form-factor: phone" \
|
||||
-H "x-netflix.context.android.installer-source: com.android.vending" \
|
||||
-b "nfvdid=<NFVDID>; flwssn=<UUID>; NetflixId=<NETFLIX_ID>; SecureNetflixId=<SECURE_NETFLIX_ID>"
|
||||
```
|
||||
|
||||
### GraphQL (MSL 暗号化)
|
||||
```bash
|
||||
curl -X POST \
|
||||
"https://android.prod.cloud.netflix.com/graphql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Content-Encoding: msl_v1" \
|
||||
-H "X-Netflix.clientType: samurai" \
|
||||
-H "X-Netflix.appVer: 9.57.0" \
|
||||
-H "X-Netflix.esnPrefix: NFANDROID1-PRV-P-L3-" \
|
||||
-H "X-Netflix.androidApi: 34" \
|
||||
-H "X-Netflix.deviceFormFactor: PHONE" \
|
||||
-H "X-Netflix.deviceMemoryLevel: HIGH" \
|
||||
-H "X-Netflix.zuul.brotli.allowed: true" \
|
||||
-H "x-netflix.client.android.mslrequest: true" \
|
||||
-H "x-netflix.context.operation-name: AccountQuery" \
|
||||
-H "X-Netflix.Client.Request.Name: AccountQuery" \
|
||||
-H "X-Netflix.Request.Attempt: 1" \
|
||||
-H "X-Netflix.Request.Id: $(uuidgen | tr -d '-')" \
|
||||
-H "accept: multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" \
|
||||
-b "flwssn=<UUID>; nfvdid=<NFVDID>; gsid=<UUID>" \
|
||||
-d '<MSL_ENCRYPTED_BODY>'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESN の送信箇所 (HTTP ヘッダー経由)
|
||||
|
||||
| ヘッダー | 値の種類 | 出現頻度 |
|
||||
|---|---|---|
|
||||
| `X-Netflix.esn` | フル PXA ESN | 707/1,274 |
|
||||
| `X-Netflix.esnPrefix` | PRV プレフィックス (`NFANDROID1-PRV-P-L3-`) | 1,210/1,274 |
|
||||
| URL パス (appboot) | PRV プレフィックス | appboot のみ |
|
||||
|
||||
**注**: `X-Netflix.esn` は PXA (Proxy) ESN が送られる。PRV (Private) ESN はプレフィックスのみ。
|
||||
@@ -0,0 +1,375 @@
|
||||
# Netflix Widevine DRM License Exchange - Reference Data
|
||||
|
||||
> Android アプリ (v9.57.0 / build 63928) から MSL 経由で取得した実データ。
|
||||
> 2026-03-13 にキャプチャ。デバイス: Pixel 4a (5G) / Android 14 / Widevine CDM v17.0.0
|
||||
> **環境**: L3 (ソフトウェア) 強制。レスポンスは Widevine decrypt → CBOR → GZIP → JSON で復元。
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
Netflix Android アプリは **Widevine CDM** DRM を使用してコンテンツを保護している。
|
||||
iOS 版と異なり、マニフェスト取得と初回ライセンス取得が `/licensedManifest` として統合されている:
|
||||
|
||||
1. **licensedManifest (PRE_FETCH)** → ストリーム一覧 + CDN URL + 初回 Widevine ライセンス (limited) を一括取得
|
||||
2. **License Request (standard)** → 再生開始時に standard ライセンスを取得 (Widevine CDM protobuf チャレンジ)
|
||||
3. **License Response** → Widevine ライセンスバイナリを受信、コンテンツ鍵を取得
|
||||
4. **Release License** → 再生終了時にライセンスを解放
|
||||
|
||||
ライセンスリクエスト/レスポンスはすべて **Widevine CryptoContext** で暗号化された MSL ペイロードとして送信される。以下のデータはアプリ内部で MSL 暗号化される前/復号後の平文。
|
||||
|
||||
---
|
||||
|
||||
## エンドポイント
|
||||
|
||||
```
|
||||
POST /nq/androidui/samurai/~9.0.0/api
|
||||
```
|
||||
|
||||
MSL URL としては:
|
||||
```
|
||||
/licensedManifest (マニフェスト + 初回ライセンス一括)
|
||||
/license?licenseType={standard|limited}&playbackContextId={id}&esn={esn}&drmContextId={id}
|
||||
```
|
||||
|
||||
### URL パラメータ
|
||||
|
||||
| パラメータ | 説明 |
|
||||
|---|---|
|
||||
| `licenseType` | `standard` (再生開始時) or `limited` (PRE_FETCH / LDL) |
|
||||
| `playbackContextId` | licensedManifest レスポンスで取得した再生セッション ID (Base64url-like) |
|
||||
| `esn` | Netflix デバイス ESN。`NFANDROID1-PRV-P-{L3}-{MODEL}-{userId}-{fingerprint}` 形式 |
|
||||
| `drmContextId` | DRM コンテキスト ID (数値)。licensedManifest レスポンスの `packageId` と対応 |
|
||||
|
||||
---
|
||||
|
||||
## 1. licensedManifest Request (PRE_FETCH)
|
||||
|
||||
コンテンツ選択時に送信される。iOS の `/manifest` + `/license` を統合した Android 固有の API。
|
||||
バッチリクエストにより複数 viewableId を同時に指定可能。
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"url": "/licensedManifest",
|
||||
"languages": ["en-JP"],
|
||||
"common": {
|
||||
"challenge": "<共通 Widevine CDM protobuf>"
|
||||
},
|
||||
"params": [
|
||||
{
|
||||
"viewableId": "81756595",
|
||||
"method": "licensedManifest",
|
||||
"flavor": "PRE_FETCH",
|
||||
"drmType": "widevine",
|
||||
"manifestVersion": "v2",
|
||||
"licenseType": "limited",
|
||||
"challenges": {
|
||||
"primary": [{
|
||||
"challengeBase64": "<Widevine CDM protobuf>",
|
||||
"drmSessionId": 1,
|
||||
"clientTime": 1773373148
|
||||
}]
|
||||
},
|
||||
"profiles": ["playready-h264mpl30-dash", "playready-h264hpl22-dash", "..."],
|
||||
"profileGroups": [{"name": "primary", "profiles": ["..."]}]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### iOS との違い
|
||||
|
||||
| 項目 | iOS (`/manifest` + `/license`) | Android (`/licensedManifest`) |
|
||||
|---|---|---|
|
||||
| API パス | 分離 (2 リクエスト) | 統合 (1 リクエスト) |
|
||||
| バッチ | 1 viewableId / リクエスト | 複数 viewableId / リクエスト |
|
||||
| DRM チャレンジ | FairPlay SPC (個別) | `common.challenge` + 個別 `challenges.primary` |
|
||||
| DRM タイプ | `fairplay` | `widevine` |
|
||||
| チャレンジ形式 | FairPlay SPC (JSON: CHALLENGES[].PAYLOAD) | Widevine CDM protobuf (バイナリ) |
|
||||
|
||||
---
|
||||
|
||||
## 2. licensedManifest Response
|
||||
|
||||
L3 Widevine decrypt → CBOR (key 62) → GZIP 展開で復元。8 チャンク、合計 456KB。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"version": 2,
|
||||
"serverTime": 1773373149628,
|
||||
"result": [
|
||||
{
|
||||
"movieId": "81756595",
|
||||
"packageId": "2596051",
|
||||
"duration": 8523000,
|
||||
"drmContextId": "2596051",
|
||||
"playbackContextId": "E3-Bgj5tevc...",
|
||||
"video_tracks": [{"..."}],
|
||||
"audio_tracks": [{"..."}],
|
||||
"timedtexttracks": [{"..."}],
|
||||
"servers": [{"..."}],
|
||||
"links": {
|
||||
"events": {"href": "/events?playbackContextId=...&esn=..."},
|
||||
"ldl": {"href": "/license?licenseType=limited&..."},
|
||||
"license": {"href": "/license?licenseType=standard&..."}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### result フィールド解説
|
||||
|
||||
| フィールド | 型 | 説明 |
|
||||
|---|---|---|
|
||||
| `movieId` | string | コンテンツ ID |
|
||||
| `packageId` | string | DRM パッケージ ID (`drmContextId` と対応) |
|
||||
| `duration` | number | 再生時間 (ミリ秒)。`8523000` = 2時間22分3秒 |
|
||||
| `drmContextId` | string | DRM コンテキスト ID |
|
||||
| `playbackContextId` | string | 再生セッション ID (後続の `/license`, `/events` URL に埋め込み) |
|
||||
| `video_tracks` | array | 映像トラック一覧 (複数ビットレート/解像度) |
|
||||
| `audio_tracks` | array | 音声トラック一覧 (多言語 × 複数ビットレート) |
|
||||
| `timedtexttracks` | array | 字幕トラック一覧 (IMSC 1.1) |
|
||||
| `servers` | array | CDN サーバー一覧 (Open Connect Appliance) |
|
||||
| `links` | object | 後続 API エンドポイント (`events`, `ldl`, `license`) |
|
||||
|
||||
### video_tracks の構造
|
||||
|
||||
```json
|
||||
{
|
||||
"trackType": "PRIMARY",
|
||||
"new_track_id": "V:2:1;2;;primary;-1;none;-1;",
|
||||
"dimensionsLabel": "2D",
|
||||
"streams": [
|
||||
{
|
||||
"content_profile": "playready-h264hpl30-dash",
|
||||
"bitrate": 1050,
|
||||
"peakBitrate": 2250,
|
||||
"res_w": 960,
|
||||
"res_h": 540,
|
||||
"framerate_value": 24000,
|
||||
"framerate_scale": 1001,
|
||||
"size": 1181780966,
|
||||
"downloadable_id": "1496730611",
|
||||
"vmaf": 87,
|
||||
"isDrm": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
L3 環境での最大解像度は **960x540** (SD)。7 ストリーム (80〜1050 kbps)。
|
||||
|
||||
### servers の構造
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 140368,
|
||||
"name": "c062.osa001.ix.nflxvideo.net",
|
||||
"rank": 1,
|
||||
"type": "OPEN_CONNECT_APPLIANCE",
|
||||
"dns": {
|
||||
"host": "ipv4-c062-osa001-ix.1.oca.nflxvideo.net",
|
||||
"ipv4": "45.57.82.139",
|
||||
"ipv6": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3 台の Open Connect Appliance (大阪: osa001, osa003)。`rank` でフェイルオーバー優先度を指定。
|
||||
|
||||
### iOS manifest レスポンスとの比較
|
||||
|
||||
| 項目 | iOS (`/manifest`) | Android (`/licensedManifest`) |
|
||||
|---|---|---|
|
||||
| レスポンス取得 | **未取得** | **復元済み** (L3 decrypt 経由) |
|
||||
| result 構造 | 単一オブジェクト (推定) | 配列 (バッチ対応) |
|
||||
| links | events, license (推定) | events, ldl, license |
|
||||
| servers | CDN URL 一覧 (推定) | Open Connect Appliance (id, name, rank, dns) |
|
||||
| video_tracks | 不明 | streams[] (bitrate, res, vmaf, downloadable_id) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Standard License Request
|
||||
|
||||
再生開始時に送信される。licensedManifest レスポンスの `links.license.href` を使用。
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"url": "/license?licenseType=standard&playbackContextId=E3-Bgj5tevc...&esn=NFANDROID1-PRV-P-L3-GOOGLPIXEL%3D4A%3D%3D5G%3D-22594-...&drmContextId=2596051",
|
||||
"params": {
|
||||
"clientTime": 1773373168,
|
||||
"challengeBase64": "<Widevine CDM protobuf, ~3.4KB>",
|
||||
"xid": "7616579701369171345"
|
||||
},
|
||||
"languages": ["en-JP"]
|
||||
}
|
||||
```
|
||||
|
||||
### params フィールド解説
|
||||
|
||||
| フィールド | 型 | 説明 |
|
||||
|---|---|---|
|
||||
| `clientTime` | number | クライアント時刻 (Unix epoch, seconds) |
|
||||
| `challengeBase64` | string | Base64 エンコードされた Widevine CDM protobuf ライセンスチャレンジ |
|
||||
| `xid` | string | リクエスト ID (トランザクション ID) |
|
||||
|
||||
### challengeBase64 の内容
|
||||
|
||||
iOS の FairPlay SPC が JSON (`CHALLENGES[].PAYLOAD`) であるのに対し、Widevine は **protobuf バイナリ**。
|
||||
内部に以下の情報が含まれる:
|
||||
|
||||
| フィールド | 値 | 説明 |
|
||||
|---|---|---|
|
||||
| `esn` | `NFANDROID1-PRV-P-L3-GOOGLPIXEL=4A==5G=-22594-...` | デバイス ESN (L3 マーカー含む) |
|
||||
| `movieid` | `"81756595"` | コンテンツ ID |
|
||||
| `issuetime` | `1773373148` | 発行時刻 |
|
||||
| `salt` | `"35982634558192635..."` | ランダムソルト |
|
||||
| `oem_crypto_build_information` | `"OEMCrypto Level3 Code"` | OEMCrypto セキュリティレベル |
|
||||
| `widevine_cdm_version` | `"17.0.0"` | CDM バージョン |
|
||||
| `device_name` | `"bramble"` | デバイスコードネーム |
|
||||
| `architecture_name` | `"arm64-v8a"` | CPU アーキテクチャ |
|
||||
|
||||
---
|
||||
|
||||
## 4. Standard License Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"version": 2,
|
||||
"serverTime": 1773373169221,
|
||||
"result": {
|
||||
"licenseResponseBase64": "<Base64-encoded Widevine license protobuf>",
|
||||
"secureStopExpected": false,
|
||||
"links": {
|
||||
"releaseLicense": {
|
||||
"rel": "releaseLicense",
|
||||
"href": "/releaseLicense?drmLicenseContextId=E3-Bgj5tevc...;EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED;STANDARD;1773373169202"
|
||||
}
|
||||
},
|
||||
"drmGroupId": "132",
|
||||
"licenseType": "standard",
|
||||
"expiration": 1773373168000
|
||||
},
|
||||
"common": {},
|
||||
"from": "playapi"
|
||||
}
|
||||
```
|
||||
|
||||
### result フィールド解説
|
||||
|
||||
| フィールド | 型 | 説明 |
|
||||
|---|---|---|
|
||||
| `licenseResponseBase64` | string | Base64 エンコードされた Widevine ライセンスバイナリ (protobuf)。`provideKeyResponse()` に渡す |
|
||||
| `secureStopExpected` | boolean | Secure Stop (再生終了通知) が必要か |
|
||||
| `links.releaseLicense.href` | string | ライセンス解放用 URL。再生終了時にこの URL にリクエストを送る |
|
||||
| `drmGroupId` | string | DRM グループ ID。`"132"` (iOS は `"191-192"` のようにハイフン区切り) |
|
||||
| `licenseType` | string | `"standard"` |
|
||||
| `expiration` | number | ライセンス有効期限 (Unix epoch, milliseconds) |
|
||||
|
||||
### licenseResponseBase64 の内容
|
||||
|
||||
iOS の FairPlay CKC が JSON (`VERSION`, `MEDIASESSIONID`, `RESPONSES[].PAYLOAD`) であるのに対し、
|
||||
Widevine のライセンスレスポンスは **protobuf バイナリ**。内部には:
|
||||
|
||||
- **暗号化されたコンテンツ鍵**: Widevine CDM のセッション鍵で暗号化
|
||||
- **埋め込み JSON**: `version`, `esn`, `issuetime`, `movieid`, `salt`
|
||||
- **鍵 ID**: `CE66E4B447F964AC19000000000000000`
|
||||
- **ライセンスメタデータ**: security_level, expiry_duration
|
||||
|
||||
`android.media.MediaDrm.provideKeyResponse(sessionId, licenseResponseBase64)` に渡すことで
|
||||
コンテンツ復号鍵が Widevine CDM にインストールされる。
|
||||
|
||||
---
|
||||
|
||||
## 5. Release License (再生終了)
|
||||
|
||||
再生終了時、サーバーに通知する。`links.releaseLicense.href` を使用。
|
||||
|
||||
iOS と同じく、レスポンスは空の `actions` オブジェクト (推定):
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"actions": {}
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"common": {},
|
||||
"from": "playapi",
|
||||
"serverTime": 1773373xxx000,
|
||||
"version": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. drmSessionId / videoTrackName フォーマット
|
||||
|
||||
```
|
||||
V:2:1;2;;primary;-1;none;-1;
|
||||
```
|
||||
|
||||
セミコロン区切り。推定される構造:
|
||||
- `V:2:1` — ビデオトラック識別 (V=Video, 以降はインデックス)
|
||||
- `2` — DRM グループ数
|
||||
- (空) — 予約
|
||||
- `primary` — プロファイルグループ (iOS は `ce4` = HEVC)
|
||||
- `-1` — 品質レベル (-1 = auto)
|
||||
- `none` — HDR タイプ (none = SDR)
|
||||
- `-1` — 予約
|
||||
|
||||
---
|
||||
|
||||
## 7. ESN (Entertainment Service Name) フォーマット
|
||||
|
||||
```
|
||||
NFANDROID1-PRV-P-L3-GOOGLPIXEL=4A==5G=-22594-3E369F1C9B189ED664E13DEEEE9BACB67684B4FE6283537D1A67810BFE73E371
|
||||
```
|
||||
|
||||
- `NFANDROID1` — Netflix Android プラットフォーム (iOS は `NFAPPL`)
|
||||
- `PRV` — プロビジョニングタイプ
|
||||
- `P` — 製品タイプ
|
||||
- `L3` — Widevine セキュリティレベル (L3=ソフトウェア、L1=TEE)。iOS には存在しない
|
||||
- `GOOGLPIXEL=4A==5G=` — デバイスモデル (Pixel 4a (5G)、`=` は `,` のエスケープ)
|
||||
- `22594` — ユーザー ID
|
||||
- 末尾 64 文字 — デバイス固有ハッシュ (SHA-256)
|
||||
|
||||
### iOS ESN との比較
|
||||
|
||||
| 項目 | iOS | Android |
|
||||
|---|---|---|
|
||||
| プレフィックス | `NFAPPL` | `NFANDROID1` |
|
||||
| DRM バージョン | `02` (FairPlay v2) | なし |
|
||||
| セキュリティレベル | なし | `L3` or `L1` |
|
||||
| モデル | `IPHONE9=1` | `GOOGLPIXEL=4A==5G=` |
|
||||
| ユーザー ID | なし | あり (`22594`) |
|
||||
|
||||
---
|
||||
|
||||
## 8. データの関連性まとめ
|
||||
|
||||
```
|
||||
licensedManifest Response
|
||||
├─ playbackContextId ──→ /license Request URL
|
||||
├─ drmContextId / packageId ──→ /license Request URL
|
||||
├─ video_tracks / audio_tracks ──→ CDN ストリーム URL
|
||||
├─ servers[] ──→ CDN サーバー (Open Connect Appliance)
|
||||
└─ links
|
||||
├─ events ──→ /events URL (再生イベント報告)
|
||||
├─ ldl ──→ /license?licenseType=limited (LDL)
|
||||
└─ license ──→ /license?licenseType=standard
|
||||
|
||||
License Request
|
||||
└─ challengeBase64 (Widevine protobuf) ──→ License Response.licenseResponseBase64
|
||||
|
||||
License Response
|
||||
├─ licenseResponseBase64 ──→ MediaDrm.provideKeyResponse() → コンテンツ鍵
|
||||
├─ links.releaseLicense.href ──→ Release License Request URL
|
||||
└─ expiration ──→ standard: ライセンス有効期限
|
||||
```
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"_description": "Netflix Android Widevine DRM license exchange data (captured 2026-03-13, L3 forced). Payloads are truncated for size. challengeBase64 contains Widevine CDM protobuf.",
|
||||
"_environment": "Widevine L3 (software) forced. Device: Pixel 4a (5G) / Android 14 / CDM v17.0.0",
|
||||
"licensed_manifest_request": {
|
||||
"_note": "Android combines /manifest + /license into single /licensedManifest request. Supports batch (multiple viewableIds).",
|
||||
"url": "/licensedManifest",
|
||||
"method": "licensedManifest",
|
||||
"flavor": "PRE_FETCH",
|
||||
"drmType": "widevine",
|
||||
"manifestVersion": "v2",
|
||||
"licenseType": "limited",
|
||||
"params_per_viewable": {
|
||||
"viewableId": "81756595",
|
||||
"challenges": {
|
||||
"primary": [{
|
||||
"challengeBase64": "<Widevine CDM protobuf, ~2378 bytes>",
|
||||
"drmSessionId": 1,
|
||||
"clientTime": 1773373148
|
||||
}]
|
||||
},
|
||||
"common_challenge": "<shared Widevine CDM protobuf for all viewableIds>"
|
||||
},
|
||||
"batch_viewable_ids": ["81756595", "80243261", "81774276"]
|
||||
},
|
||||
"licensed_manifest_response": {
|
||||
"_note": "Recovered via L3 Widevine decrypt → CBOR (key 62) → GZIP → JSON. 8 chunks, 456KB total.",
|
||||
"id": 1,
|
||||
"version": 2,
|
||||
"serverTime": 1773373149628,
|
||||
"result": [
|
||||
{
|
||||
"_viewable": "81756595 (MOVIE, 142 min)",
|
||||
"movieId": "81756595",
|
||||
"packageId": "2596051",
|
||||
"duration": 8523000,
|
||||
"drmContextId": "2596051",
|
||||
"playbackContextId": "E3-Bgj5tevcAxKrBD8Cc_xgU8lnmqvxiyt4LKYtO-sU... (truncated)",
|
||||
"video_tracks": [
|
||||
{
|
||||
"trackType": "PRIMARY",
|
||||
"new_track_id": "V:2:1;2;;primary;-1;none;-1;",
|
||||
"dimensionsLabel": "2D",
|
||||
"streams_summary": [
|
||||
{"content_profile": "playready-h264hpl22-dash", "bitrate": 80, "res_w": 480, "res_h": 270, "isDrm": true},
|
||||
{"content_profile": "playready-h264hpl22-dash", "bitrate": 100, "res_w": 480, "res_h": 270, "isDrm": true},
|
||||
{"content_profile": "playready-h264hpl30-dash", "bitrate": 200, "res_w": 608, "res_h": 342, "isDrm": true},
|
||||
{"content_profile": "playready-h264hpl30-dash", "bitrate": 350, "res_w": 608, "res_h": 342, "isDrm": true},
|
||||
{"content_profile": "playready-h264hpl30-dash", "bitrate": 560, "res_w": 768, "res_h": 432, "isDrm": true},
|
||||
{"content_profile": "playready-h264hpl30-dash", "bitrate": 750, "res_w": 768, "res_h": 432, "isDrm": true},
|
||||
{"content_profile": "playready-h264hpl30-dash", "bitrate": 1050, "res_w": 960, "res_h": 540, "isDrm": true}
|
||||
],
|
||||
"_note": "L3 max resolution: 960x540. L1 would include up to 1920x1080+ with HEVC HDR10."
|
||||
}
|
||||
],
|
||||
"audio_tracks_summary": {
|
||||
"languages": ["ja", "en", "pt-BR", "es", "es-ES", "fr", "de", "it", "pl", "fil", "hu"],
|
||||
"profiles": ["xheaac-dash", "heaac-2-dash"],
|
||||
"channels": "2.0",
|
||||
"bitrates": [32, 64, 96, 192],
|
||||
"total_tracks": 19,
|
||||
"_note": "Each language has xheaac-dash + heaac-2-dash tracks with 4 bitrate options"
|
||||
},
|
||||
"timedtexttracks_summary": {
|
||||
"total_tracks": 57,
|
||||
"format": "imsc1.1",
|
||||
"languages": ["en", "ja", "de", "es", "es-ES", "fr", "fil", "hu", "it", "pl", "pt-BR"],
|
||||
"types": ["subtitles", "closedcaptions"],
|
||||
"_note": "Each has downloadableIds.imsc1.1 with CDN URLs and sha1 hash"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"id": 140368,
|
||||
"name": "c062.osa001.ix.nflxvideo.net",
|
||||
"rank": 1,
|
||||
"type": "OPEN_CONNECT_APPLIANCE",
|
||||
"dns": {"host": "ipv4-c062-osa001-ix.1.oca.nflxvideo.net", "ipv4": "45.57.82.139"}
|
||||
},
|
||||
{
|
||||
"id": 140566,
|
||||
"name": "c010.osa003.ix.nflxvideo.net",
|
||||
"rank": 2,
|
||||
"type": "OPEN_CONNECT_APPLIANCE",
|
||||
"dns": {"host": "ipv4-c010-osa003-ix.1.oca.nflxvideo.net", "ipv4": "23.246.52.11"}
|
||||
},
|
||||
{
|
||||
"id": 159199,
|
||||
"name": "c106.osa001.ix.nflxvideo.net",
|
||||
"rank": 3,
|
||||
"type": "OPEN_CONNECT_APPLIANCE",
|
||||
"dns": {"host": "ipv4-c106-osa001-ix.1.oca.nflxvideo.net", "ipv4": "45.57.84.3"}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"events": {
|
||||
"href": "/events?playbackContextId=E3-Bgj5tevc...&esn=NFANDROID1-PRV-P-L3-GOOGLPIXEL%3D4A%3D%3D5G%3D-22594-...",
|
||||
"rel": "events"
|
||||
},
|
||||
"ldl": {
|
||||
"href": "/license?licenseType=limited&playbackContextId=E3-Bgj5tevc...&esn=NFANDROID1-PRV-P-L3-...&drmContextId=2596051",
|
||||
"rel": "license"
|
||||
},
|
||||
"license": {
|
||||
"href": "/license?licenseType=standard&playbackContextId=E3-Bgj5tevc...&esn=NFANDROID1-PRV-P-L3-...&drmContextId=2596051",
|
||||
"rel": "license"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"_note_batch": "result is an array with one entry per viewableId. 3 entries in this capture."
|
||||
},
|
||||
"standard_license_request": {
|
||||
"_note": "Sent after /licensedManifest, using playbackContextId from manifest response links.license.href",
|
||||
"url": "/license?licenseType=standard&playbackContextId=E3-Bgj5tevcAxKrBD8Cc_xgU8lnmqvxiy...&esn=NFANDROID1-PRV-P-L3-GOOGLPIXEL%3D4A%3D%3D5G%3D-22594-...&drmContextId=2596051",
|
||||
"params": {
|
||||
"clientTime": 1773373168,
|
||||
"challengeBase64": "<Widevine CDM protobuf, ~3.4KB — contains device cert, OEMCrypto Level3 Code, ESN with L3 marker>",
|
||||
"challengeBase64_contents": {
|
||||
"device_certificate": {
|
||||
"esn": "NFANDROID1-PRV-P-L3-GOOGLPIXEL=4A==5G=-22594-3E369F1C9B189ED664E13DEEEE9BACB67684B4FE6283537D1A67810BFE73E371",
|
||||
"movieid": "81756595",
|
||||
"issuetime": 1773373148,
|
||||
"salt": "3598263455819263519576642966605"
|
||||
},
|
||||
"oem_crypto_build_information": "OEMCrypto Level3 Code May 20 2022 21:36:54",
|
||||
"widevine_cdm_version": "17.0.0",
|
||||
"device_name": "bramble",
|
||||
"architecture_name": "arm64-v8a"
|
||||
},
|
||||
"xid": "7616579701369171345"
|
||||
},
|
||||
"languages": ["en-JP"]
|
||||
},
|
||||
"standard_license_response": {
|
||||
"_note": "Recovered via L3 Widevine decrypt → CBOR (key 62) → GZIP → JSON",
|
||||
"id": 1,
|
||||
"version": 2,
|
||||
"serverTime": 1773373169221,
|
||||
"result": {
|
||||
"licenseResponseBase64": "<Base64-encoded Widevine license binary, contains encrypted content keys>",
|
||||
"licenseResponseBase64_contents": {
|
||||
"_note": "Unlike iOS FairPlay (JSON with CHALLENGES/RESPONSES), Widevine license is a protobuf binary",
|
||||
"embedded_json": {
|
||||
"version": "1.0",
|
||||
"esn": "NFANDROID1-PRV-P-L3-GOOGLPIXEL=4A==5G=-22594-...",
|
||||
"issuetime": 1773373148,
|
||||
"movieid": "81756595",
|
||||
"salt": "35982634558192635195766429666057"
|
||||
},
|
||||
"license_metadata": {
|
||||
"security_level": "0x0784",
|
||||
"expiry_duration": "0xC8D102",
|
||||
"key_id": "CE66E4B447F964AC190000000000000"
|
||||
}
|
||||
},
|
||||
"secureStopExpected": false,
|
||||
"links": {
|
||||
"releaseLicense": {
|
||||
"href": "/releaseLicense?drmLicenseContextId=E3-Bgj5tevc...;EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED;STANDARD;1773373169202",
|
||||
"rel": "releaseLicense"
|
||||
}
|
||||
},
|
||||
"expiration": 1773373168000,
|
||||
"drmGroupId": "132",
|
||||
"licenseType": "standard"
|
||||
},
|
||||
"common": {},
|
||||
"from": "playapi"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
# MSL CBOR 整数キーマッピング — リファレンス
|
||||
|
||||
MSL プロトコルの CBOR エンコード時に使用される固定整数キー。
|
||||
JSON の文字列キーをサイズ削減のため整数に置換する仕組みで、`MslEncoderFactory` にハードコードされている。
|
||||
|
||||
353 件の暗号化前 MessageHeader + 403 件の PayloadChunk の CBOR 解析により確定。
|
||||
|
||||
---
|
||||
|
||||
## 全整数キー一覧
|
||||
|
||||
| CBOR キー | JSON フィールド名 | 使用コンテキスト |
|
||||
|---|---|---|
|
||||
| `11` | `issuedate` | UserIdToken.tokendata |
|
||||
| `12` | `mtserialnum` | UserIdToken.tokendata |
|
||||
| `13` | `expiration` | UserIdToken.tokendata |
|
||||
| `14` | `sequencenumber` | PayloadChunk |
|
||||
| `15` | `tokendata` | MasterToken, UserIdToken, keyrequestdata (共通) |
|
||||
| `16` | `signature` | MasterToken, UserIdToken, keyrequestdata (共通) |
|
||||
| `17` | `mastertoken` | MessageHeader |
|
||||
| `18` | `useridtoken` | MessageHeader |
|
||||
| `19` | `renewable` | MessageHeader |
|
||||
| `20` | `sender` | MessageHeader |
|
||||
| `21` | `handshake` | MessageHeader |
|
||||
| `22` | `messageid` | MessageHeader, PayloadChunk |
|
||||
| `24` | `timestamp` | MessageHeader |
|
||||
| `25` | `serialnumber` | MasterToken.tokendata, UserIdToken.tokendata |
|
||||
| `26` | `renewable` | MasterToken.tokendata |
|
||||
| `27` | `issuer` | MasterToken.tokendata |
|
||||
| `28` | `identity` | MasterToken.tokendata |
|
||||
| `29` | `userdata` | UserIdToken.tokendata |
|
||||
| `30` | `scheme` | keyrequestdata, userauthdata |
|
||||
| `31` | `keydata` | keyrequestdata |
|
||||
| `35` | `authdata` | userauthdata |
|
||||
| `36` | `capabilities` | MessageHeader |
|
||||
| `37` | `compressionalgos` | capabilities |
|
||||
| `38` | `languages` | capabilities |
|
||||
| `39` | `encoders` | capabilities |
|
||||
| `40` | `peer` | MessageHeader |
|
||||
| `41` | `nonreplayableid` | MessageHeader |
|
||||
| `42` | `keyrequestdata` | MessageHeader |
|
||||
| `43` | `sequencenumber` | MasterToken.tokendata |
|
||||
| `44` | `compressionalgo` | PayloadChunk |
|
||||
| `47` | `userauthdata` | MessageHeader |
|
||||
| `50` | `cdmsg` | keyrequestdata.keydata (Widevine) |
|
||||
| `56` | `netflixid` | userauthdata.authdata (NETFLIXID) |
|
||||
| `60` | `securenetflixid` | userauthdata.authdata (NETFLIXID) |
|
||||
| `62` | `data` | PayloadChunk |
|
||||
| `63` | `endofmsg` | PayloadChunk |
|
||||
|
||||
## 文字列キー (整数マッピングなし)
|
||||
|
||||
| キー | 使用コンテキスト |
|
||||
|---|---|
|
||||
| `maxpayloadchunksize` | capabilities |
|
||||
| `profileid` | UserIdToken.tokendata |
|
||||
| `duid` | keyrequestdata.keydata |
|
||||
|
||||
---
|
||||
|
||||
## MessageHeader 構造
|
||||
|
||||
```
|
||||
MessageHeader (暗号化前 headerdata):
|
||||
├─ 17: mastertoken → list[ {15: tokendata, 16: signature} ]
|
||||
├─ 18: useridtoken → {15: tokendata, 16: signature}
|
||||
├─ 19: renewable → bool
|
||||
├─ 20: sender → string (ESN)
|
||||
├─ 21: handshake → bool
|
||||
├─ 22: messageid → int
|
||||
├─ 24: timestamp → int (Unix秒)
|
||||
├─ 36: capabilities
|
||||
│ ├─ 37: compressionalgos → list
|
||||
│ ├─ 38: languages → list
|
||||
│ ├─ 39: encoders → list
|
||||
│ └─ maxpayloadchunksize → int
|
||||
├─ 40: peer → bool
|
||||
├─ 41: nonreplayableid → int
|
||||
├─ 42: keyrequestdata → list[ {30: scheme, 31: keydata} ]
|
||||
└─ 47: userauthdata → {30: scheme, 35: authdata}
|
||||
```
|
||||
|
||||
## PayloadChunk 構造
|
||||
|
||||
```
|
||||
PayloadChunk (暗号化前):
|
||||
├─ 14: sequencenumber → int
|
||||
├─ 22: messageid → int
|
||||
├─ 44: compressionalgo → string ("GZIP")
|
||||
├─ 62: data → bytes (GZIP圧縮ペイロード)
|
||||
└─ 63: endofmsg → bool
|
||||
```
|
||||
|
||||
## MasterToken tokendata 構造
|
||||
|
||||
```
|
||||
MasterToken.tokendata (key 17[].15 を CBORデコード):
|
||||
├─ 25: serialnumber → int
|
||||
├─ 26: renewable → bool
|
||||
├─ 27: issuer → string ("sf", "cad")
|
||||
├─ 28: identity → bytes (暗号化済み)
|
||||
└─ 43: sequencenumber → int
|
||||
```
|
||||
|
||||
## UserIdToken tokendata 構造
|
||||
|
||||
```
|
||||
UserIdToken.tokendata (key 18.15 を CBORデコード):
|
||||
├─ 11: issuedate → int (Unix秒)
|
||||
├─ 12: mtserialnum → int (紐づくMasterTokenシリアル)
|
||||
├─ 13: expiration → int (Unix秒, 発行から14日後)
|
||||
├─ 25: serialnumber → int
|
||||
├─ 29: userdata → bytes (暗号化済み)
|
||||
└─ profileid → string (プロファイルGUID)
|
||||
```
|
||||
|
||||
## userauthdata 構造 (NETFLIXID スキーム)
|
||||
|
||||
```
|
||||
userauthdata (key 47):
|
||||
├─ 30: scheme → "NETFLIXID"
|
||||
└─ 35: authdata
|
||||
├─ 56: netflixid → string (URLエンコード)
|
||||
│ 例: v=3&mac=AQEAEQABABQxZyPp7r91RNj4pmPTIatYcL3jjIUVbvQ.&dt=1773392547767
|
||||
└─ 60: securenetflixid → string (URLエンコード)
|
||||
例: v=3&ct=<暗号化トークン>&pg=<プロファイルGUID>&ch=<HMAC>
|
||||
```
|
||||
|
||||
### netflixid のパラメータ
|
||||
|
||||
| パラメータ | 説明 |
|
||||
|---|---|
|
||||
| `v` | バージョン (`3`) |
|
||||
| `mac` | HMAC 署名 |
|
||||
| `dt` | デバイスタイムスタンプ (Unix ms) |
|
||||
|
||||
### securenetflixid のパラメータ
|
||||
|
||||
| パラメータ | 説明 |
|
||||
|---|---|
|
||||
| `v` | バージョン (`3`) |
|
||||
| `ct` | 暗号化認証トークン (protobuf base64url) |
|
||||
| `pg` | プロファイル GUID |
|
||||
| `ch` | チャネル HMAC |
|
||||
|
||||
---
|
||||
|
||||
## ユーザー認証フロー
|
||||
|
||||
1. **初回認証 (1回)**: key 47 (`userauthdata`) に NETFLIXID スキームで netflixId + secureNetflixId を送信
|
||||
2. **サーバー応答**: UserIdToken を発行 (有効期間 14日)
|
||||
3. **以降のリクエスト (348回観測)**: key 18 (`useridtoken`) で認証。userauthdata は送信しない
|
||||
|
||||
353件の MSL ヘッダーのうち userauthdata を含むのは **1件のみ** (0.3%)。
|
||||
apiRequest の第5引数 (`UserAuthenticationData`) は常に null — MslControl 層が内部で設定する。
|
||||
|
||||
---
|
||||
|
||||
## キャプチャ実データ
|
||||
|
||||
### Phase 1: 初回ハンドシェイク (Widevine 鍵交換)
|
||||
|
||||
ソース: `crypto/0002_msl.widevine.encrypt.json` (2026-03-13T09:00:02.108Z)
|
||||
|
||||
userauthdata なし、useridtoken なし。Widevine 鍵交換のみ。
|
||||
|
||||
```
|
||||
MessageHeader:
|
||||
key 17 (mastertoken): list (len=1)
|
||||
[0].15 (tokendata): <bytes 153>
|
||||
[0].16 (signature): <bytes 44>
|
||||
key 19 (renewable): false
|
||||
key 20 (sender): ""
|
||||
key 21 (handshake): true
|
||||
key 22 (messageid): 5608206180487888
|
||||
key 24 (timestamp): 1773392401
|
||||
key 36 (capabilities):
|
||||
37 (compressionalgos): []
|
||||
38 (languages): ["GZIP", "LZW"]
|
||||
39 (encoders): ["CBOR"]
|
||||
maxpayloadchunksize: -1
|
||||
key 40 (peer): false
|
||||
key 42 (keyrequestdata): list (len=1)
|
||||
[0].30 (scheme): "WIDEVINE"
|
||||
[0].31 (keydata): {duid: <bytes 32>, 50 (cdmsg): <bytes 2577>}
|
||||
```
|
||||
|
||||
### Phase 2: userauthdata 送信 (NETFLIXID 認証, 1回のみ)
|
||||
|
||||
ソース: `android14.prod.ftl.netflix.com/0052_msl.widevine.encrypt.json` (2026-03-13T09:02:28.168Z)
|
||||
|
||||
```
|
||||
MessageHeader:
|
||||
key 17 (mastertoken): list (len=1)
|
||||
key 19 (renewable): false
|
||||
key 20 (sender): ""
|
||||
key 21 (handshake): true
|
||||
key 22 (messageid): 2491352839960294
|
||||
key 24 (timestamp): 1773392548
|
||||
key 36 (capabilities): {37: [], 38: ["GZIP","LZW"], 39: ["CBOR"], maxpayloadchunksize: -1}
|
||||
key 40 (peer): true
|
||||
key 41 (nonreplayableid): 10
|
||||
key 42 (keyrequestdata): list (len=1)
|
||||
[0].30 (scheme): "WIDEVINE"
|
||||
key 47 (userauthdata):
|
||||
30 (scheme): "NETFLIXID"
|
||||
35 (authdata):
|
||||
56 (netflixid): "v=3&mac=AQEAEQABABQxZyPp7r91RNj4pmPTIatYcL3jjIUVbvQ.&dt=1773392547767"
|
||||
60 (securenetflixid): "v=3&ct=BgjHlOvcAxLcA5Nned7dv-Iq5m2f7a5MNrKy8VAkRdBzSXaHjTsiFmEI
|
||||
-etnBrrNnCGzam9tRfUqnfVpjLaK3eFk79_6Jcsspf05U_HMC9CnJ3vy0tUhQatCl9ma2mrGqIWMm6fBWX11
|
||||
CofiJwnM-OpAzHO3fO5AJwHFSMwa7pCucoXbyjee_5tLcNdykJrEGPRnjJRc6DPfaYAJN4sDgCJptsO1Ssfw
|
||||
A0CTujgVhkf4r7S-HloF7BOktKT_dwzw2ULtuZXg5uC0XySU6Phk9y-jRjqQ9LvpVD-E9mXrKMAH23yxAI5
|
||||
Ku-YRnmTOEMhk92gJBVTwnHJAe13NihHhF-aHxXDfBcMkWPHDT6-raPJ3HClitacWIoWQ2AH0q5WRrm1XBMg
|
||||
A4KuNIepWOfJV9AGRVAfGoKpxbLjU6ajSx1KiD9o5KkaZ1yvEvlxtAxbISivsKhVocRN7Lt12W_9jsuUZbL-f
|
||||
urw4jGezz5lHUNLSkh4suuiVYdAMMkhyPnbC1pTKE-fRxd2stJ0uuOAord3hrUbKyDK5g_nbvRSXZFW1sLb22
|
||||
691pj5FnCxhkJquZtoW67Iwa7hVo9Jg5cUVSNvbuVLC0hmRotilPJujiaLz-XQty0SUaWv-WArtj-aEXjYKa3
|
||||
DuGAYiDgoMGowgnSHIWWhHgSZz&pg=ZEULH5S2GNGCRAABCSG6J2EGGA&ch=AQEAEAABABTp79nN9l_2MuRh
|
||||
qTXl0-SjAqcm83QU8vw."
|
||||
```
|
||||
|
||||
#### netflixid デコード
|
||||
|
||||
```
|
||||
v=3
|
||||
mac=AQEAEQABABQxZyPp7r91RNj4pmPTIatYcL3jjIUVbvQ.
|
||||
dt=1773392547767
|
||||
```
|
||||
|
||||
#### securenetflixid デコード
|
||||
|
||||
```
|
||||
v=3
|
||||
ct=BgjHlOvcAxLcA5Nned7dv-Iq5m2f7a5MNrKy8VAk... (672文字, 暗号化認証トークン)
|
||||
pg=ZEULH5S2GNGCRAABCSG6J2EGGA (プロファイル GUID)
|
||||
ch=AQEAEAABABTp79nN9l_2MuRhqTXl0-SjAqcm83QU8vw. (チャネル HMAC)
|
||||
```
|
||||
|
||||
### Phase 3: 通常リクエスト (UserIdToken 認証, 348回)
|
||||
|
||||
ソース: `android14.prod.cloud.netflix.com/0002_msl.widevine.encrypt.json` (2026-03-13T09:00:02.415Z)
|
||||
|
||||
```
|
||||
MessageHeader:
|
||||
key 17 (mastertoken): list (len=2)
|
||||
key 18 (useridtoken):
|
||||
15 (tokendata): <bytes 296>
|
||||
16 (signature): 01010081000101204005d2ae93a88d111b34aecb30b911f634e542d41c97f8f8a305af3850d3e9a22de3595d
|
||||
key 19 (renewable): false
|
||||
key 20 (sender): ""
|
||||
key 21 (handshake): false
|
||||
key 22 (messageid): 491822966207012
|
||||
key 24 (timestamp): 1773392402
|
||||
key 36 (capabilities): {37: [], 38: ["GZIP","LZW"], 39: ["CBOR"], maxpayloadchunksize: -1}
|
||||
key 40 (peer): false
|
||||
```
|
||||
|
||||
#### UserIdToken tokendata (CBOR decoded)
|
||||
|
||||
```
|
||||
11 (issuedate): 1773420422 (2026-03-13T16:47:02+00:00)
|
||||
12 (mtserialnum): 8079134327147185
|
||||
13 (expiration): 1774601222 (2026-03-27T08:47:02+00:00) ← 発行から14日後
|
||||
25 (serialnumber): 5970945423176003
|
||||
29 (userdata): <bytes 217> (暗号化済み)
|
||||
profileid: "ZEULH5S2GNGCRAABCSG6J2EGGA"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ログインフロー キャプチャ実データ
|
||||
|
||||
セッション `android_20260313` で実際に観測されたログイン〜認証完了までの全リクエスト。
|
||||
ドメインが `android.prod.cloud.netflix.com` (MSL v14 ではない旧ドメイン) の場合は `userId: null` で未認証状態。
|
||||
|
||||
### Step 1: RenewSSOToken (既存セッション更新の試行)
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:00:02.245Z
|
||||
domain: android14.prod.cloud.netflix.com
|
||||
userId: ZEULH5S2GNGCRAABCSG6J2EGGA
|
||||
userauthdata: null
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"operationName": "RenewSSOToken",
|
||||
"variables": {
|
||||
"ssoToken": "BgiHtuvcAxL4AWLUqSE7W14gsbHZBDGMle4StHvRnr70_-VHofvb_wqAIEYDS3SmOscABWcD-0O-ibsEZPZYT3R7kUeCsJ14vV6crTlZY5DozcjvtZbtjykI8sqaQzMNtrOq1Af9lZemVLhi5FqnmNp7uy2RpwwiCqojwvVWzmKRzeOKStqUHDSIdzAofrErd6kAsfoJZleWTV87eSx2gyAa9mIauY_A51-rE2lOaTj19CeVaTeqSrWZMqWzuZ0OaYfqVZkuUf7CZ1bW1NTtlxS3mo17dmxIhpIfX8CFf26dp1Ggui93k_2dNbOF4_8e_whkxNBpTNh1RMmalk11gJocGAYiDgoMYoDsMZdYw3vL-Wdq"
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 102,
|
||||
"id": "a4d00303-b02d-47c9-a53f-776b6a63b001"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: InterstitialHook (ログイン画面初期化)
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:01:29.542Z
|
||||
domain: android.prod.cloud.netflix.com
|
||||
userId: null ← 未認証
|
||||
userauthdata: null
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"operationName": "InterstitialHook",
|
||||
"variables": {
|
||||
"flowName": "loginMobile",
|
||||
"format": "HTML",
|
||||
"resolutionMode": "ANDROID_XHDPI",
|
||||
"imageFormat": "PNG",
|
||||
"parameters": null,
|
||||
"commonParameters": {
|
||||
"isConsumptionOnly": true,
|
||||
"isNetflixPreloaded": false,
|
||||
"channelId": "",
|
||||
"androidInstallType": "regular"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 102,
|
||||
"id": "bb592c79-c026-44e3-a989-ddba298d5eaf"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: InterstitialScreenUpdate — メールアドレス送信 + reCAPTCHA
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:02:12.214Z
|
||||
domain: android.prod.cloud.netflix.com
|
||||
userId: null ← 未認証
|
||||
userauthdata: null
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"operationName": "InterstitialScreenUpdate",
|
||||
"variables": {
|
||||
"serverState": {
|
||||
"realm": "growth",
|
||||
"name": "IDENTIFICATION",
|
||||
"clcsSessionId": "74a682e9-d991-4d03-a2e4-aa130cdd1623",
|
||||
"sessionContext": {
|
||||
"session-breadcrumbs": { "funnel_name": "loginMobile" },
|
||||
"MobileLoginSessionContext": {}
|
||||
},
|
||||
"hellfireSessionId": "d1141445-134a-37ae-b91f-efe6902155e1"
|
||||
},
|
||||
"serverScreenUpdate": {
|
||||
"realm": "custom",
|
||||
"name": "growthProcessLogin",
|
||||
"metadata": {
|
||||
"recaptchaSiteKey": "6LeWeOoUAAAAAJB9vW-OBEYmBwbF9R7PILe6U_ML"
|
||||
},
|
||||
"loggingAction": "Submitted",
|
||||
"loggingCommand": "SubmitCommand"
|
||||
},
|
||||
"inputFields": [
|
||||
{ "name": "userLoginId", "value": { "stringValue": "lemonandchan+5@gmail.com" } },
|
||||
{ "name": "countryCode", "value": { "stringValue": "81" } },
|
||||
{ "name": "countryIsoCode", "value": { "stringValue": "JP" } },
|
||||
{ "name": "password", "value": { "stringValue": "" } },
|
||||
{ "name": "recaptchaResponseTime", "value": { "intValue": 1231 } },
|
||||
{ "name": "recaptchaResponseToken","value": { "stringValue": "<reCAPTCHA token ~4KB>" } }
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 102,
|
||||
"id": "d1a0c5d5-2c35-4b98-8b9c-11d8e7706148"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注**: `password` は空文字列。パスワードレスログイン (MFA OTP) が使用されている。
|
||||
> `recaptchaSiteKey`: `6LeWeOoUAAAAAJB9vW-OBEYmBwbF9R7PILe6U_ML` (Google reCAPTCHA v3)
|
||||
|
||||
### Step 4: InterstitialScreenUpdate — MFA OTP 送信
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:02:27.251Z
|
||||
domain: android.prod.cloud.netflix.com
|
||||
userId: null ← まだ未認証
|
||||
userauthdata: null
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"operationName": "InterstitialScreenUpdate",
|
||||
"variables": {
|
||||
"serverState": {
|
||||
"realm": "growth",
|
||||
"name": "MFA_COLLECT_OTP_EMAIL_INPUT",
|
||||
"clcsSessionId": "74a682e9-d991-4d03-a2e4-aa130cdd1623",
|
||||
"sessionContext": {
|
||||
"session-breadcrumbs": { "funnel_name": "loginMobile" },
|
||||
"MobileLoginSessionContext": {}
|
||||
},
|
||||
"hellfireSessionId": "d1141445-134a-37ae-b91f-efe6902155e1"
|
||||
},
|
||||
"serverScreenUpdate": {
|
||||
"realm": "custom",
|
||||
"name": "growthVerifyMfaChallenge",
|
||||
"metadata": { "validateLength": 4 },
|
||||
"loggingAction": "Submitted",
|
||||
"loggingCommand": "SubmitCommand"
|
||||
},
|
||||
"inputFields": [
|
||||
{ "name": "challengeOtp", "value": { "stringValue": "8104" } }
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 102,
|
||||
"id": "d1a0c5d5-2c35-4b98-8b9c-11d8e7706148"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注**: MFA OTP は 4 桁 (`validateLength: 4`)。メールに送信された OTP `8104` を入力。
|
||||
|
||||
### Step 5: InterstitialSendFeedback — ログイン完了、プロフィール画面へ遷移
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:02:30.453Z
|
||||
domain: android.prod.cloud.netflix.com
|
||||
userId: ZEULH5S2GNGCRAABCSG6J2EGGA ← 認証完了
|
||||
userauthdata: null
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"operationName": "InterstitialSendFeedback",
|
||||
"variables": {
|
||||
"serverState": {
|
||||
"realm": "growth",
|
||||
"name": "MFA_COLLECT_OTP_EMAIL_INPUT",
|
||||
"clcsSessionId": "74a682e9-d991-4d03-a2e4-aa130cdd1623"
|
||||
},
|
||||
"serverFeedback": {
|
||||
"name": "system.inAppNavigation",
|
||||
"metadata": { "loggingCommand": "Navigating to /profiles" }
|
||||
},
|
||||
"inputFields": []
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 102,
|
||||
"id": "4718d209-37d8-4b43-ae43-858cd07c6c0b"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: AccountQuery (認証後, userId 確定)
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:02:29.644Z
|
||||
domain: android14.prod.cloud.netflix.com
|
||||
userId: TEMP_PROFILE_ID ← 一時プロファイル
|
||||
|
||||
ts: 2026-03-13T09:02:31.123Z
|
||||
domain: android14.prod.cloud.netflix.com
|
||||
userId: ZEULH5S2GNGCRAABCSG6J2EGGA ← 確定プロファイル
|
||||
```
|
||||
|
||||
### Step 7: userauthdata (NETFLIXID) 送信 — MSL 層で初回ユーザー認証
|
||||
|
||||
```
|
||||
ts: 2026-03-13T09:02:28.168Z ← Step 4〜5 の間
|
||||
domain: android14.prod.ftl.netflix.com
|
||||
```
|
||||
|
||||
→ Phase 2 のデータ参照 (前述)。ログイン完了直後に MslControl が netflixId + secureNetflixId を MSL MessageHeader に埋め込み。
|
||||
|
||||
### ログインフロー時系列まとめ
|
||||
|
||||
| 時刻 | ステップ | userId | ドメイン | 内容 |
|
||||
|---|---|---|---|---|
|
||||
| 09:00:02 | RenewSSOToken | `ZEULH5S..` | android14.prod.cloud | 既存セッションの SSO トークン更新 |
|
||||
| 09:01:29 | InterstitialHook | null | android.prod.cloud | ログイン画面初期化 (`loginMobile`) |
|
||||
| 09:02:12 | InterstitialScreenUpdate | null | android.prod.cloud | メールアドレス送信 + reCAPTCHA (`IDENTIFICATION`) |
|
||||
| 09:02:27 | InterstitialScreenUpdate | null | android.prod.cloud | MFA OTP `8104` 送信 (`MFA_COLLECT_OTP_EMAIL_INPUT`) |
|
||||
| 09:02:28 | **MSL userauthdata** | — | android14.prod.ftl | **NETFLIXID 認証** (netflixId + secureNetflixId) |
|
||||
| 09:02:29 | AccountQuery | `TEMP_PROFILE_ID` | android14.prod.cloud | 一時プロファイルでアカウント照会 |
|
||||
| 09:02:30 | InterstitialSendFeedback | `ZEULH5S..` | android.prod.cloud | ログイン完了 → `/profiles` 遷移 |
|
||||
| 09:02:31 | AccountQuery | `ZEULH5S..` | android14.prod.cloud | 確定プロファイルでアカウント照会 |
|
||||
|
||||
---
|
||||
|
||||
## 実際の Cookie とリクエストヘッダー
|
||||
|
||||
2026-03-13 キャプチャから取得した実データ。
|
||||
|
||||
### Cookie 一覧
|
||||
|
||||
3 つの Cookie が送信される:
|
||||
|
||||
| Cookie 名 | 説明 | サンプル値 |
|
||||
|---|---|---|
|
||||
| `nfvdid` | Netflix デバイス ID | `BQFmAAEBEEPj84LzHGpQ_ldxaVuQv8tg...` (Base64, 約130文字) |
|
||||
| `NetflixId` | メイン認証トークン | `v=3&ct=BgjHlOvc...&pg=ZEULH5S2GNG...&ch=AQEAEAABABTp79nN...` (約600文字) |
|
||||
| `SecureNetflixId` | セキュア認証トークン (HMAC) | `v=3&mac=AQEAEQABABSdkuDd...&dt=1773391620952` |
|
||||
|
||||
### NetflixId の構造
|
||||
|
||||
```
|
||||
v=3
|
||||
ct=BgjHlOvcAxLcA6sY6f_QqKsJWi9efmzU7gW0d6XdGtEbMELNpOs-Ws2jKWjrRgmY8LTraOV1L5gpMfa5YJ63VaUYZrzppkA0wm-r_7A_XkrygJ4mEYMVax3K5POwTnVKZK-k2h5ITFTGYp3sHXwBQy3BbuBTJ8rD4nQOXGgRbm3RilTMKcOCHYyahc4YSmE-E3CPdoASBsLzpHAszy7XN8Mwc4iGr198Hac49DdcXpLhUaP6LGm8j9_3nI0R2gg8B-ntAZIKumUrbQXSsb_zMv71GxZYcKnjNVfwFAKppf3gydKKhcvzuFKw7jZvebCSYp83l56MRB88yd_vUjmXsDUdZS1AXjYMk9zVfHoARiF-2-c0FSfS3zb2SQWQkUcgTR_SabzN5u6_w6zXgffoYlVJDPsmy8Z9pDGYFzOGL3Xqc_UNIz4KdEPCDkSGmQC9nhVOJXwXYb-LBhVC7qE1Cz1vpZ98mYEwE7LomPN3-34eGIWBeeMR8E4VXfQJnL4iBnO6hBfRmy8xNRgDA7VlAjJiYYolnK_x4_gTu2-x7A5AK1WYqobiCkcOClMQIioAC8ZlH17Wf_eN2lFfOEP6G50KJpYPhUezIpPzfV5osQT9Q0AsfUrzRF3MWXDM9ukNDmHiYfN0GAYiDgoMSvU3vVSCd907UQKa
|
||||
pg=ZEULH5S2GNGCRAABCSG6J2EGGA
|
||||
ch=AQEAEAABABTp79nN9l_2MuRhqTXl0-SjAqcm83QU8vw.
|
||||
```
|
||||
|
||||
- `v`: バージョン (3)
|
||||
- `ct`: 暗号化トークン本体 (Base64url)
|
||||
- `pg`: プロファイル GUID
|
||||
- `ch`: チャンネル/チェックサム
|
||||
|
||||
### SecureNetflixId の構造
|
||||
|
||||
```
|
||||
v=3
|
||||
mac=AQEAEQABABSdkuDdxX0gGJU2IJOTCYiVHmFkqRtHQj0.
|
||||
dt=1773391620952
|
||||
```
|
||||
|
||||
- `v`: バージョン (3)
|
||||
- `mac`: HMAC 署名 (Base64url)
|
||||
- `dt`: タイムスタンプ (epoch ms)
|
||||
|
||||
---
|
||||
|
||||
## cURL 形式リクエスト例
|
||||
|
||||
### 共通ヘッダー
|
||||
|
||||
```
|
||||
User-Agent: com.netflix.mediaclient/63928 (Linux; U; Android 14; en; Pixel 4a (5G); Build/UP1A.231005.007)
|
||||
X-Netflix-ProxyEsn: NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202Q7INAHS2TKI5GTQESPDEHTFK7MG1BKUU7QAPUQP2QMI641A8HN08CE40C5H2K4J15NCLBC5DGJI0M03TMV0VGS1ER8VACIG0257E
|
||||
```
|
||||
|
||||
### 1. Push 通知 WebSocket 接続
|
||||
|
||||
```bash
|
||||
curl -v \
|
||||
-H 'Origin: http://www.netflix.com' \
|
||||
-H 'User-Agent: com.netflix.mediaclient/63928 (Linux; U; Android 14; en; Pixel 4a (5G); Build/UP1A.231005.007)' \
|
||||
-H 'X-Netflix-ProxyEsn: NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202Q7INAHS2TKI5GTQESPDEHTFK7MG1BKUU7QAPUQP2QMI641A8HN08CE40C5H2K4J15NCLBC5DGJI0M03TMV0VGS1ER8VACIG0257E' \
|
||||
-H 'Upgrade: websocket' \
|
||||
-H 'Connection: Upgrade' \
|
||||
-H 'Sec-WebSocket-Version: 13' \
|
||||
-H 'Sec-WebSocket-Extensions: permessage-deflate' \
|
||||
-b 'nfvdid=BQFmAAEBEEPj84LzHGpQ_ldxaVuQv8tgIBE9VAe3w-WeF5En4w5goMB6eLYVXqxblfzh23QC62wkeecrOuKtsfIRNC5GkWZ80HdbCeAoFLJ8LL5stGc-h87ykqdvoTT1Vg5jhdcHity8mbE5rciYfRNlegiejB_j; NetflixId=v%3D3%26ct%3DBgjHlOvcAxLcA6sY6f_QqKsJWi9efmzU7gW0d6XdGtEbMELNpOs-Ws2jKWjrRgmY8LTraOV1L5gpMfa5YJ63VaUYZrzppkA0wm-r_7A_XkrygJ4mEYMVax3K5POwTnVKZK-k2h5ITFTGYp3sHXwBQy3BbuBTJ8rD4nQOXGgRbm3RilTMKcOCHYyahc4YSmE-E3CPdoASBsLzpHAszy7XN8Mwc4iGr198Hac49DdcXpLhUaP6LGm8j9_3nI0R2gg8B-ntAZIKumUrbQXSsb_zMv71GxZYcKnjNVfwFAKppf3gydKKhcvzuFKw7jZvebCSYp83l56MRB88yd_vUjmXsDUdZS1AXjYMk9zVfHoARiF-2-c0FSfS3zb2SQWQkUcgTR_SabzN5u6_w6zXgffoYlVJDPsmy8Z9pDGYFzOGL3Xqc_UNIz4KdEPCDkSGmQC9nhVOJXwXYb-LBhVC7qE1Cz1vpZ98mYEwE7LomPN3-34eGIWBeeMR8E4VXfQJnL4iBnO6hBfRmy8xNRgDA7VlAjJiYYolnK_x4_gTu2-x7A5AK1WYqobiCkcOClMQIioAC8ZlH17Wf_eN2lFfOEP6G50KJpYPhUezIpPzfV5osQT9Q0AsfUrzRF3MWXDM9ukNDmHiYfN0GAYiDgoMSvU3vVSCd907UQKa%26pg%3DZEULH5S2GNGCRAABCSG6J2EGGA%26ch%3DAQEAEAABABTp79nN9l_2MuRhqTXl0-SjAqcm83QU8vw.; SecureNetflixId=v%3D3%26mac%3DAQEAEQABABSdkuDdxX0gGJU2IJOTCYiVHmFkqRtHQj0.%26dt%3D1773391620952' \
|
||||
'https://android14.push.prod.netflix.com/ws'
|
||||
```
|
||||
|
||||
### 2. PlayExchange WebSocket 接続
|
||||
|
||||
```bash
|
||||
curl -v \
|
||||
-H 'Origin: http://www.netflix.com' \
|
||||
-H 'User-Agent: com.netflix.mediaclient/63928 (Linux; U; Android 14; en; Pixel 4a (5G); Build/UP1A.231005.007)' \
|
||||
-H 'X-Netflix-ProxyEsn: NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202Q7INAHS2TKI5GTQESPDEHTFK7MG1BKUU7QAPUQP2QMI641A8HN08CE40C5H2K4J15NCLBC5DGJI0M03TMV0VGS1ER8VACIG0257E' \
|
||||
-H 'x-netflix.socketrouter.schema.version: 2' \
|
||||
-H 'X-Netflix.Request.Client.Context: {"appstate":"foreground"}' \
|
||||
-H 'x-netflix.socketrouter.group.name: Test80913.Cell2|Test80897.Cell2|Test80905.Cell2' \
|
||||
-H 'Upgrade: websocket' \
|
||||
-H 'Connection: Upgrade' \
|
||||
-H 'Sec-WebSocket-Version: 13' \
|
||||
-H 'Sec-WebSocket-Extensions: permessage-deflate' \
|
||||
-b 'nfvdid=BQFmAAEBEEPj84LzHGpQ_ldxaVuQv8tgIBE9VAe3w-WeF5En4w5goMB6eLYVXqxblfzh23QC62wkeecrOuKtsfIRNC5GkWZ80HdbCeAoFLJ8LL5stGc-h87ykqdvoTT1Vg5jhdcHity8mbE5rciYfRNlegiejB_j; NetflixId=v%3D3%26ct%3DBgjHlOvc...%26pg%3DZEULH5S2GNGCRAABCSG6J2EGGA%26ch%3DAQEAEAABABTp79nN9l_2MuRhqTXl0-SjAqcm83QU8vw.; SecureNetflixId=v%3D3%26mac%3DAQEAEQABABSdkuDdxX0gGJU2IJOTCYiVHmFkqRtHQj0.%26dt%3D1773391620952' \
|
||||
'https://android14.ws.prod.cloud.netflix.com/playexchange'
|
||||
```
|
||||
|
||||
### 3. MSL API リクエスト (Samurai API)
|
||||
|
||||
MSL API は WebSocket 上の MSL プロトコルで送信される。HTTP レベルでは上記 WebSocket 接続のみ。
|
||||
MSL メッセージ内部で以下のエンドポイントにルーティングされる:
|
||||
|
||||
```
|
||||
https://android14.prod.ftl.netflix.com/nq/androidui/samurai/~9.0.0/api
|
||||
https://android14.prod.ftl.netflix.com/nq/androidui/samurai/v1/config
|
||||
https://android14.prod.ftl.netflix.com/playapi/android/event/1
|
||||
https://android14.prod.cloud.netflix.com/graphql
|
||||
https://android.prod.cloud.netflix.com/graphql
|
||||
https://android14.logs.netflix.com/log/android/logblob/1
|
||||
```
|
||||
|
||||
> **注意**: MSL メッセージボディは CBOR エンコード → Widevine CryptoContext で暗号化されるため、
|
||||
> cURL で直接再現するには暗号化済みバイナリを POST する必要がある。
|
||||
> Cookie は HTTP トランスポート層で送信され、MSL レイヤーの `userauthdata` (CBOR key 47) にも
|
||||
> 同じ `netflixId`/`secureNetflixId` が埋め込まれる(二重送信)。
|
||||
|
||||
### ESN (デバイス識別子) の構造
|
||||
|
||||
```
|
||||
NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202Q7INAHS2TKI5GTQESPDEHTFK7MG1BKUU7QAPUQP2QMI641A8HN08CE40C5H2K4J15NCLBC5DGJI0M03TMV0VGS1ER8VACIG0257E
|
||||
```
|
||||
|
||||
| セグメント | 値 | 意味 |
|
||||
|---|---|---|
|
||||
| プラットフォーム | `NFANDROID1` | Netflix Android |
|
||||
| セキュリティ | `PXA-P` | ProGuard eXtended Authentication - Production |
|
||||
| DRM レベル | `L3` | Widevine Level 3 |
|
||||
| デバイスモデル | `GOOGLPIXEL=4A==5G=` | Google Pixel 4a (5G) |
|
||||
| Widevine System ID | `22594` | CDM System ID |
|
||||
| デバイス固有 ID | `0202Q7INA...0257E` | ハードウェア固有識別子 |
|
||||
|
||||
---
|
||||
|
||||
## ESN 送信箇所の全体マップ
|
||||
|
||||
2026-03-13 キャプチャデータの分析結果。ESN は複数の場所・形式で送信される。
|
||||
|
||||
### 2 種類の ESN
|
||||
|
||||
同一デバイスに対して **2 種類の ESN** が使い分けられている:
|
||||
|
||||
| 種別 | プレフィックス | 用途 | デバイス固有ID |
|
||||
|---|---|---|---|
|
||||
| **PXA (ProxyEsn)** | `NFANDROID1-PXA-P-L3-` | HTTP ヘッダー (`X-Netflix-ProxyEsn`) | `0202Q7INA...0257E` |
|
||||
| **PRV (Private)** | `NFANDROID1-PRV-P-L3-` | ライセンス要求、DRM、再生イベント | `3E369F1C...E371` |
|
||||
|
||||
- `PXA` = ProGuard eXtended Authentication。WebSocket 接続時のデバイス識別に使用。
|
||||
- `PRV` = Private。DRM ライセンスおよびコンテンツ再生関連で使用。
|
||||
- デバイスモデル部分 (`GOOGLPIXEL=4A==5G=`) と Widevine System ID (`22594`) は共通。
|
||||
- デバイス固有 ID 部分は種別ごとに異なる。
|
||||
|
||||
### ESN が送信される全箇所
|
||||
|
||||
| # | 送信箇所 | ESN 種別 | レイヤー | フック可能性 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `X-Netflix-ProxyEsn` HTTP ヘッダー | PXA | HTTP (OkHttp) | OkHttp `Request.newBuilder()` で差し替え |
|
||||
| 2 | `/license` URL クエリパラメータ `esn=` | PRV (完全) | MSL API パラメータ | `apiRequest` body 書き換え |
|
||||
| 3 | `/events` URL クエリパラメータ `esn=` | PRV (完全) | MSL API パラメータ | `apiRequest` body 書き換え |
|
||||
| 4 | `challengeBase64` (Widevine CDM protobuf) | PRV (完全) | DRM (MediaDrm) | `MediaDrm.getProvisionRequest` / `setPropertyString` |
|
||||
| 5 | `logblob` ボディ内 | 短縮 (`NFANDROID1-GOOGLPIXEL=...`) | MSL API パラメータ | `apiRequest` body 書き換え |
|
||||
| 6 | MSL MessageHeader `sender` (CBOR key 20) | **空文字列** | MSL CBOR | 使われていない(書き換え不要) |
|
||||
|
||||
### 各箇所の詳細
|
||||
|
||||
#### 1. X-Netflix-ProxyEsn (HTTP ヘッダー)
|
||||
|
||||
WebSocket 接続 (`/ws`, `/playexchange`) 時に送信される。
|
||||
|
||||
```
|
||||
X-Netflix-ProxyEsn: NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202Q7INAHS2TKI5GTQESPDEHTFK7MG1BKUU7QAPUQP2QMI641A8HN08CE40C5H2K4J15NCLBC5DGJI0M03TMV0VGS1ER8VACIG0257E
|
||||
```
|
||||
|
||||
**フック方法**: OkHttp `RealCall.getResponseWithInterceptorChain` 内で `request.newBuilder().removeHeader().addHeader()` で差し替え。
|
||||
|
||||
#### 2-3. /license, /events URL クエリパラメータ
|
||||
|
||||
MSL API ペイロード内の URL パスに `esn=` として埋め込まれる。
|
||||
|
||||
```
|
||||
/license?licenseType=standard&playbackContextId=E3-...&esn=NFANDROID1-PRV-P-L3-GOOGLPIXEL%3D4A%3D%3D5G%3D-22594-3E369F1C9B189ED664E13DEEEE9BACB67684B4FE6283537D1A67810BFE73E371&drmContextId=2596051
|
||||
```
|
||||
|
||||
**フック方法**: `ApiHandlerImpl.apiRequest` の body (arguments[1]) を書き換え。または ESN 提供元をフックすればアプリが自動的に新 ESN で URL を組み立てる。
|
||||
|
||||
#### 4. challengeBase64 (Widevine CDM protobuf)
|
||||
|
||||
DRM ライセンス要求の `challengeBase64` フィールドに含まれる Widevine CDM protobuf 内に ESN が埋め込まれている。
|
||||
|
||||
```json
|
||||
{
|
||||
"challengeBase64_contents": {
|
||||
"device_certificate": {
|
||||
"esn": "NFANDROID1-PRV-P-L3-GOOGLPIXEL=4A==5G=-22594-3E369F1C9B189ED664E13DEEEE9BACB67684B4FE6283537D1A67810BFE73E371",
|
||||
"movieid": "81756595",
|
||||
"issuetime": 1773373148,
|
||||
"salt": "3598263455819263519576642966605"
|
||||
},
|
||||
"oem_crypto_build_information": "OEMCrypto Level3 Code May 20 2022 21:36:54",
|
||||
"widevine_cdm_version": "17.0.0",
|
||||
"device_name": "bramble",
|
||||
"architecture_name": "arm64-v8a"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
この protobuf は `MediaDrm` CDM が生成するため、**直接書き換えが最も難しい箇所**。
|
||||
|
||||
**フック方法 (候補)**:
|
||||
- `MediaDrm.setPropertyString("esn", ...)` をフックして差し替え — CDM が protobuf 生成前に ESN を取得する段階で介入
|
||||
- `MediaDrm.getProvisionRequest()` の戻り値 (byte[]) 内を文字列置換 — protobuf 構造が壊れるリスクあり
|
||||
- CDM より上流の ESN 提供クラス (SharedPreferences / DeviceInfo) をフック — CDM に渡る前に差し替えるため protobuf 構造は保持される
|
||||
|
||||
> **注意**: `challengeBase64` 内の ESN は Widevine サーバー側で検証される可能性がある。
|
||||
> ESN を差し替えても CDM の `device_certificate` 署名と不整合が生じると、
|
||||
> ライセンス取得がエラーになる場合がある。
|
||||
|
||||
#### 5. logblob ボディ (テレメトリ)
|
||||
|
||||
テレメトリログ送信時に短縮 ESN が含まれる:
|
||||
|
||||
```
|
||||
NFANDROID1-GOOGLPIXEL=4A==5G=S-3269A5B59F399B066558C80DCB36AE97F9A8B477F8854DFED2C14C8453D6F556
|
||||
```
|
||||
|
||||
セキュリティ種別 (`PXA`/`PRV`) を含まない短縮形式。デバイス固有 ID も PXA/PRV とは異なる第3の値。
|
||||
|
||||
#### 6. MSL MessageHeader sender (CBOR key 20)
|
||||
|
||||
キャプチャした全 353 ヘッダーで **sender は空文字列 `""`** だった。
|
||||
Android 版では MSL MessageHeader の sender フィールドは使われておらず、ESN は上記 1-5 の経路で送信される。
|
||||
|
||||
### ESN 書き換え戦略まとめ
|
||||
|
||||
```
|
||||
最上流 (推奨)
|
||||
├── SharedPreferences / DeviceInfo → ESN 文字列の保持元
|
||||
│ └── ここを書き換えれば #1, #2, #3, #5 は自動的に反映
|
||||
│
|
||||
├── MediaDrm.setPropertyString("esn") → CDM への ESN 設定
|
||||
│ └── ここを書き換えれば #4 (challengeBase64) にも反映
|
||||
│
|
||||
最下流 (個別対応)
|
||||
├── OkHttp Request.newBuilder() → #1 のみ
|
||||
├── apiRequest body replace → #2, #3, #5
|
||||
└── protobuf byte[] replace → #4 (構造破壊リスク)
|
||||
```
|
||||
|
||||
**理想的なアプローチ**: ESN の提供元 (SharedPreferences / EsnManager 相当クラス) と `MediaDrm.setPropertyString` の 2 箇所をフックすれば、全箇所に波及する。
|
||||
@@ -0,0 +1,383 @@
|
||||
# Netflix FairPlay DRM License Exchange - Reference Data
|
||||
|
||||
> iOS アプリ (CLIENT-15.48.1) から MSL 経由で取得した実データ。
|
||||
> 2026-03-12 にキャプチャ。デバイス: iPhone 7 (iPhone9,1) / iOS 15.8.3
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
Netflix iOS アプリは **FairPlay Streaming (FPS)** DRM を使用してコンテンツを保護している。
|
||||
ライセンス交換は MSL (Message Security Layer) の上で行われ、以下のフローで進む:
|
||||
|
||||
1. **Manifest 取得** → 再生可能なストリーム一覧 + `playbackContextId` を取得
|
||||
2. **License Request (standard)** → 初回の FairPlay SPC (Server Playback Context) を送信
|
||||
3. **License Response** → CKC (Content Key Context) を受信、復号鍵を取得
|
||||
4. **License Request (limited)** → 再生中にビットレート切り替え等で追加ライセンスを取得
|
||||
5. **Release License** → 再生終了時にライセンスを解放
|
||||
|
||||
ライセンスリクエスト/レスポンスはすべて **AES-128-CBC + HMAC-SHA256** で暗号化された MSL ペイロードとして送信される。以下のデータはアプリ内部で MSL 暗号化される前の平文。
|
||||
|
||||
---
|
||||
|
||||
## エンドポイント
|
||||
|
||||
```
|
||||
POST /nq/iosplatform/pbo_license/~1.0.0/router
|
||||
```
|
||||
|
||||
MSL URL としては:
|
||||
```
|
||||
/license?licenseType={standard|limited}&playbackContextId={id}&esn={esn}&drmContextId={id}
|
||||
```
|
||||
|
||||
### URL パラメータ
|
||||
|
||||
| パラメータ | 説明 |
|
||||
|---|---|
|
||||
| `licenseType` | `standard` (初回) or `limited` (追加/ビットレート変更時の LDL) |
|
||||
| `playbackContextId` | Manifest レスポンスで取得した再生セッション ID (Base64url-like) |
|
||||
| `esn` | Netflix デバイス ESN。`NFAPPL-02-{MODEL}-{HASH}` 形式 |
|
||||
| `drmContextId` | DRM コンテキスト ID (数値) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Standard License Request (初回)
|
||||
|
||||
再生開始時に送信される。Manifest 取得直後、2つの FairPlay SPC チャレンジを含む。
|
||||
|
||||
```json
|
||||
{
|
||||
"mslTimeout": 19,
|
||||
"preferredlanguages": {
|
||||
"appselectedlanguages": ["en-JP", "en"],
|
||||
"platformselectedlanguages": ["en-JP"]
|
||||
},
|
||||
"url": "/license?licenseType=standard&playbackContextId=E3-Bgj5tevc...&esn=NFAPPL-02-IPHONE9%3D1-5CB1D229...&drmContextId=2596051",
|
||||
"params": [
|
||||
{
|
||||
"drmSessionId": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"xid": "7118417868969003867",
|
||||
"clientTime": 1773331407,
|
||||
"videoTrackName": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"challengeBase64": "<Base64-encoded JSON, see below>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### params フィールド解説
|
||||
|
||||
| フィールド | 型 | 説明 |
|
||||
|---|---|---|
|
||||
| `drmSessionId` | string | DRM セッション識別子。`V:2:1;2;;ce4;-1;none;-1;` のようなセミコロン区切りフォーマット。ビデオトラック情報を含む |
|
||||
| `xid` | string | リクエスト ID (トランザクション ID) |
|
||||
| `clientTime` | number | クライアント時刻 (Unix epoch, seconds) |
|
||||
| `videoTrackName` | string | `drmSessionId` と同値。再生対象のビデオトラック識別子 |
|
||||
| `challengeBase64` | string | Base64エンコードされた JSON。FairPlay SPC を含む (後述) |
|
||||
|
||||
### challengeBase64 のデコード結果
|
||||
|
||||
```json
|
||||
{
|
||||
"CHALLENGES": [
|
||||
{
|
||||
"ID": "18C92565-21D0-4AAF-93C2-A306AB1565AD",
|
||||
"PAYLOAD": "<Base64-encoded FairPlay SPC binary, 10348 chars = ~7760 bytes>"
|
||||
},
|
||||
{
|
||||
"ID": "EEF401EF-2D78-4065-80C8-EB607EA4279D",
|
||||
"PAYLOAD": "<Base64-encoded FairPlay SPC binary, 9240 chars = ~6928 bytes>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **CHALLENGES**: 配列。通常2つのチャレンジを含む (ビデオ/オーディオのDRMグループに対応)
|
||||
- **ID**: UUID。レスポンスの RESPONSES と対応する
|
||||
- **PAYLOAD**: FairPlay SPC (Server Playback Context) バイナリ。先頭4バイトは `00000001` (magic number)。Apple CDM が生成する不透明なバイナリデータで、コンテンツキーのリクエストを含む
|
||||
|
||||
---
|
||||
|
||||
## 2. Standard License Response
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"licenseResponseBase64": "<Base64-encoded JSON, see below>",
|
||||
"secureStopExpected": false,
|
||||
"links": {
|
||||
"releaseLicense": {
|
||||
"rel": "releaseLicense",
|
||||
"href": "/releaseLicense?drmLicenseContextId=E3-Bgj5tevc...;29701FE4-...;STANDARD;1773331408336"
|
||||
}
|
||||
},
|
||||
"drmGroupId": "191-192",
|
||||
"licenseType": "standard",
|
||||
"expiration": 1773374608348
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"common": {},
|
||||
"from": "playapi",
|
||||
"serverTime": 1773331408353,
|
||||
"version": 2
|
||||
}
|
||||
```
|
||||
|
||||
### result フィールド解説
|
||||
|
||||
| フィールド | 型 | 説明 |
|
||||
|---|---|---|
|
||||
| `licenseResponseBase64` | string | Base64エンコードされた JSON。FairPlay CKC を含む (後述) |
|
||||
| `secureStopExpected` | boolean | Secure Stop (再生終了通知) が必要か |
|
||||
| `links.releaseLicense.href` | string | ライセンス解放用 URL。再生終了時にこのURLにリクエストを送る |
|
||||
| `drmGroupId` | string | DRM グループ ID。`"191-192"` のようにハイフン区切り |
|
||||
| `licenseType` | string | `"standard"` |
|
||||
| `expiration` | number | ライセンス有効期限 (Unix epoch, milliseconds)。standard は約12時間 |
|
||||
|
||||
### licenseResponseBase64 のデコード結果
|
||||
|
||||
```json
|
||||
{
|
||||
"VERSION": 1,
|
||||
"MEDIASESSIONID": "G3iR0pT5pxY=",
|
||||
"RESPONSES": [
|
||||
{
|
||||
"ID": "18C92565-21D0-4AAF-93C2-A306AB1565AD",
|
||||
"PAYLOAD": "<Base64-encoded FairPlay CKC binary, 2088 chars = ~1566 bytes>"
|
||||
},
|
||||
{
|
||||
"ID": "EEF401EF-2D78-4065-80C8-EB607EA4279D",
|
||||
"PAYLOAD": "<Base64-encoded FairPlay CKC binary, 1724 chars = ~1293 bytes>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **VERSION**: プロトコルバージョン (常に `1`)
|
||||
- **MEDIASESSIONID**: Base64エンコードされたメディアセッション識別子
|
||||
- **RESPONSES**: 配列。CHALLENGES の各 ID に対応する CKC を含む
|
||||
- **ID**: Challenge の ID と一致する UUID
|
||||
- **PAYLOAD**: FairPlay CKC (Content Key Context) バイナリ。Apple CDM に渡すことでコンテンツ復号鍵が得られる
|
||||
|
||||
---
|
||||
|
||||
## 3. Limited Duration License (LDL) Request
|
||||
|
||||
再生中のビットレート変更、チャプター切り替え時等に追加ライセンスを取得する。
|
||||
構造は Standard と同じだが `licenseType=limited`。
|
||||
|
||||
```json
|
||||
{
|
||||
"mslTimeout": null,
|
||||
"url": "/license?licenseType=limited&playbackContextId=E3-Bgj5tevc...&esn=NFAPPL-02-IPHONE9%3D1-...&drmContextId=2596051",
|
||||
"params": [
|
||||
{
|
||||
"drmSessionId": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"videoTrackName": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"xid": "7118765455155927307",
|
||||
"clientTime": 1773331477,
|
||||
"challengeBase64": "<Base64-encoded JSON>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Standard との差異:
|
||||
- `mslTimeout`: `null` (タイムアウトなし)
|
||||
- `licenseType`: `limited`
|
||||
- `playbackContextId`: 異なるコンテキストIDの場合あり (別エピソードへの切り替え等)
|
||||
|
||||
### LDL challengeBase64 デコード結果
|
||||
|
||||
```json
|
||||
{
|
||||
"CHALLENGES": [
|
||||
{
|
||||
"ID": "9B3BEEFC-B11A-4019-9966-F238CF4070BD",
|
||||
"PAYLOAD": "<FairPlay SPC, 11096 chars = ~8322 bytes>"
|
||||
},
|
||||
{
|
||||
"ID": "A41B0EA1-EECC-483C-A79A-237E5BD2E65A",
|
||||
"PAYLOAD": "<FairPlay SPC, 10496 chars = ~7872 bytes>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Limited Duration License Response
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"licenseResponseBase64": "<Base64-encoded JSON>",
|
||||
"secureStopExpected": false,
|
||||
"links": {
|
||||
"releaseLicense": {
|
||||
"rel": "releaseLicense",
|
||||
"href": "/releaseLicense?drmLicenseContextId=...;LIMITED_DURATION;1773331477178"
|
||||
}
|
||||
},
|
||||
"drmGroupId": "191-192",
|
||||
"licenseType": "limited_duration",
|
||||
"expiration": 1773331537178
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"common": {},
|
||||
"from": "playapi",
|
||||
"serverTime": 1773331477194,
|
||||
"version": 2
|
||||
}
|
||||
```
|
||||
|
||||
Standard との差異:
|
||||
- `licenseType`: `"limited_duration"`
|
||||
- `expiration`: 有効期限が非常に短い (約60秒)。再生中に定期的に更新される
|
||||
|
||||
### LDL licenseResponseBase64 デコード結果
|
||||
|
||||
```json
|
||||
{
|
||||
"VERSION": 1,
|
||||
"MEDIASESSIONID": "znvDeutRb/4=",
|
||||
"RESPONSES": [
|
||||
{
|
||||
"ID": "9B3BEEFC-B11A-4019-9966-F238CF4070BD",
|
||||
"PAYLOAD": "<FairPlay CKC, 1552 chars = ~1164 bytes>"
|
||||
},
|
||||
{
|
||||
"ID": "A41B0EA1-EECC-483C-A79A-237E5BD2E65A",
|
||||
"PAYLOAD": "<FairPlay CKC, 1532 chars = ~1149 bytes>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Release License (再生終了)
|
||||
|
||||
再生終了時、サーバーに通知する。レスポンスは空の `actions` オブジェクト:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"actions": {}
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"common": {},
|
||||
"from": "playapi",
|
||||
"serverTime": 1773331482636,
|
||||
"version": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 再生イベントログから見るタイミング
|
||||
|
||||
`startplayevents` ログの `eventlist` から、各イベントの相対タイミング (UIPresented=0 基準, ms):
|
||||
|
||||
```
|
||||
PlaybackRequested : -4343ms
|
||||
ManifestRequestStart : -4330ms
|
||||
MovieHeaderDownloadStart: -4011ms
|
||||
ManifestRequestEnd : -4049ms
|
||||
PlaylistBuildStart : -3987ms
|
||||
MovieHeaderDownloadEnd : -3990ms
|
||||
PlaylistBuildEnd : -3930ms
|
||||
LicenseChallengeRequested: -3925ms
|
||||
LicenseChallengeReceived : -3925ms
|
||||
SPLDLicenseRequestStart : -3876ms
|
||||
FirstLDLTry : -3575ms
|
||||
SPLDLicenseRequestEnd : -3594ms
|
||||
Variant-A-Begin-1 : -3485ms
|
||||
Variant-A-End-1 : -3481ms
|
||||
V-Start-433 : -3482ms
|
||||
V-Stop-433 : -3477ms
|
||||
Variant-V-Begin-1 : -3482ms
|
||||
Variant-V-End-1 : -3477ms
|
||||
V-Start-1637 : -3290ms
|
||||
V-Stop-1637 : -3285ms
|
||||
Variant-V-Begin-2 : -3290ms
|
||||
Variant-V-End-2 : -3285ms
|
||||
UIPresented : 0ms
|
||||
PlayerUIPresented : 0ms
|
||||
PlaybackStarted : +169ms
|
||||
```
|
||||
|
||||
注目ポイント:
|
||||
- Manifest取得からLicenseChallenge生成まで約400ms
|
||||
- LicenseRequest (SPLDLicenseRequestStart→End) は約280ms
|
||||
- UIPresented から PlaybackStarted まで約170ms
|
||||
- 全体で Playback Request → Playback Started は約4.5秒
|
||||
|
||||
### licenseAudit
|
||||
|
||||
```json
|
||||
{
|
||||
"licenseAudit": {
|
||||
"ldl": [282],
|
||||
"": [250]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `ldl`: Limited Duration License の取得時間 (282ms)
|
||||
- `""`: Standard License の取得時間 (250ms)
|
||||
|
||||
---
|
||||
|
||||
## 7. drmSessionId / videoTrackName フォーマット
|
||||
|
||||
```
|
||||
V:2:1;2;;ce4;-1;none;-1;
|
||||
```
|
||||
|
||||
セミコロン区切り。推定される構造:
|
||||
- `V:2:1` — ビデオトラック識別 (V=Video, 以降はインデックス)
|
||||
- `2` — DRM グループ数
|
||||
- (空) — 予約
|
||||
- `ce4` — コーデック識別 (HEVC = ce4)
|
||||
- `-1` — 品質レベル (-1 = auto)
|
||||
- `none` — HDR タイプ (none = SDR)
|
||||
- `-1` — 予約
|
||||
|
||||
---
|
||||
|
||||
## 8. ESN (Entertainment Service Name) フォーマット
|
||||
|
||||
```
|
||||
NFAPPL-02-IPHONE9=1-5CB1D229FE1FC4DBA556753BB3D84634599DF9C15AD474BAFBFD37965D4162EC
|
||||
```
|
||||
|
||||
- `NFAPPL` — Netflix Apple プラットフォーム
|
||||
- `02` — DRM バージョン (FairPlay v2)
|
||||
- `IPHONE9=1` — デバイスモデル (iPhone 7 = iPhone9,1、`=` は `,` のエスケープ)
|
||||
- 末尾 64文字 — デバイス固有ハッシュ (SHA-256)
|
||||
|
||||
---
|
||||
|
||||
## 9. データの関連性まとめ
|
||||
|
||||
```
|
||||
Manifest Response
|
||||
└─ playbackContextId ──→ License Request URL
|
||||
└─ video_tracks / audio_tracks ──→ drmSessionId / videoTrackName
|
||||
|
||||
License Request
|
||||
└─ challengeBase64.CHALLENGES[].ID ──→ License Response.RESPONSES[].ID (1:1 対応)
|
||||
└─ CHALLENGES[].PAYLOAD (FairPlay SPC) ──→ RESPONSES[].PAYLOAD (FairPlay CKC)
|
||||
|
||||
License Response
|
||||
└─ links.releaseLicense.href ──→ Release License Request URL
|
||||
└─ expiration ──→ standard: ~12h, limited_duration: ~60s
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"_description": "Netflix iOS FairPlay DRM license exchange data (captured 2026-03-12). Payloads are truncated for size. challengeBase64 is shown decoded.",
|
||||
"standard_license_request": {
|
||||
"mslTimeout": 19,
|
||||
"preferredlanguages": {
|
||||
"appselectedlanguages": [
|
||||
"en-JP",
|
||||
"en"
|
||||
],
|
||||
"platformselectedlanguages": [
|
||||
"en-JP"
|
||||
]
|
||||
},
|
||||
"url": "/license?licenseType=standard&playbackContextId=E3-Bgj5tevcAxLxBEiSznO_e72JheeHLtj33gHzHYBxbZ3B_kjqYCzKGG9AbT6OAQIp4f6KS7SR8GXk0uJ1swLIBt1cEL3gsr7Hkr_gxaFc-L_sdlDzjXgjTs9JTqSmjioJKmeR2eQ6ZWciBVhlYbk4Y_MrmPA_QJtpWt_RjR-lMdS1wfwHyTQ18ZGiSM--JhlokPiE24AkaBrYL6gATrXizI0XjPoTLSLDNo0uSBWQaI47GjHeFwrsvbBufKCLci7RMakL7c5IcDa4RN11roJLekdEq3hGq2xTYwz-EXH35BlTePTvpX6ry7JjNFLEevLMku6w-_sWyOlBTIQaOsO4fKTrPEoUqGOVVWMg-k1DRIm9HUT2llWUYxNNd_Y2zVrsEio6zn-ExYDqVqwqUb-pSiVs6Zxphyuh9WYWO_b5-UlJ2J25t6zDqMMKeiNTIjK6UW-06rDtx43iWc7Xtvq876Jf7nGJzQfExqrV4chRUdHtdiMdutLXmG0XTQifKuhAyZ6gs0CatDxBmR_u-ys466diNEalshfetuEMn-M_a7Ecj7oZTJ2xiViqG9r0yJ9T22WOKJmeUkl-zUeY8UJ-exajFcr8Mg5cpXOYsbNQYeBg5XIjLnrgUX9C-e_8tU6TDhVoSSEpTuZkR6RJ-sgxz9hWr3_VYaPfH-gzRQ6G6BiZEYP3ZCASj9f2ENkpuebvnJvfPph0Fi8Cqdet_Zk89dyF9TZ7LSSpkCbReBPlZo0wB7p8avvPG5_ZQPTuZdqHrzq1FDZEMpG66Ko-iiRo5TmGxwyZov7GUxdWdbxK3xRuqabOpEv3pFce1cPP-4TXezoopLlfBhj37bAYBiIOCgymagQHhwEwkzuQOHk%3D&esn=NFAPPL-02-IPHONE9%3D1-5CB1D229FE1FC4DBA556753BB3D84634599DF9C15AD474BAFBFD37965D4162EC&drmContextId=2596051",
|
||||
"params": [
|
||||
{
|
||||
"drmSessionId": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"xid": "7118417868969003867",
|
||||
"clientTime": 1773331407,
|
||||
"videoTrackName": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"challengeBase64_decoded": {
|
||||
"CHALLENGES": [
|
||||
{
|
||||
"ID": "18C92565-21D0-4AAF-93C2-A306AB1565AD",
|
||||
"PAYLOAD": "AAAAAQAAAABrGW7dT9jZ8hmmiQRP0pAQt2Qy+6xA7gZkVeYRsBC1g4Zy9KoGIScHF1e7ByeYfR0Qp/pD... (10348 chars total, ~7761 bytes)"
|
||||
},
|
||||
{
|
||||
"ID": "EEF401EF-2D78-4065-80C8-EB607EA4279D",
|
||||
"PAYLOAD": "AAAAAQAAAABiMBWMk9SNBE6hY23sChMoZdBTn+U//x+CfmdO8ewMBUFHMNt6Gm5JzYXH3v/mD5aDSaUT... (9240 chars total, ~6930 bytes)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"standard_license_response": {
|
||||
"result": [
|
||||
{
|
||||
"licenseResponseBase64_decoded": {
|
||||
"VERSION": 1,
|
||||
"MEDIASESSIONID": "G3iR0pT5pxY=",
|
||||
"RESPONSES": [
|
||||
{
|
||||
"ID": "18C92565-21D0-4AAF-93C2-A306AB1565AD",
|
||||
"PAYLOAD": "AAAAAQAAAAD2ZCdwGHIcWItMwCql2TvGAAAGANDx9oL5ZKFmoofWe+LJ8wyVi4GZq7xR+So/gO+C3FP/... (2088 chars total, ~1566 bytes)"
|
||||
},
|
||||
{
|
||||
"ID": "EEF401EF-2D78-4065-80C8-EB607EA4279D",
|
||||
"PAYLOAD": "AAAAAQAAAACvhMrcN8hN2NxJJSYJI0uDAAAE8GCxS3QcIc0GDpcmCUZb3DaVSxrLHzcEwAa0oJQhLS7P... (1724 chars total, ~1293 bytes)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"secureStopExpected": false,
|
||||
"links": {
|
||||
"releaseLicense": {
|
||||
"rel": "releaseLicense",
|
||||
"href": "/releaseLicense?drmLicenseContextId=E3-Bgj5tevcAxLxBEiSznO_e72JheeHLtj33gHzHYBxbZ3B_kjqYCzKGG9AbT6OAQIp4f6KS7SR8GXk0uJ1swLIBt1cEL3gsr7Hkr_gxaFc-L_sdlDzjXgjTs9JTqSmjioJKmeR2eQ6ZWciBVhlYbk4Y_MrmPA_QJtpWt_RjR-lMdS1wfwHyTQ18ZGiSM--JhlokPiE24AkaBrYL6gATrXizI0XjPoTLSLDNo0uSBWQaI47GjHeFwrsvbBufKCLci7RMakL7c5IcDa4RN11roJLekdEq3hGq2xTYwz-EXH35BlTePTvpX6ry7JjNFLEevLMku6w-_sWyOlBTIQaOsO4fKTrPEoUqGOVVWMg-k1DRIm9HUT2llWUYxNNd_Y2zVrsEio6zn-ExYDqVqwqUb-pSiVs6Zxphyuh9WYWO_b5-UlJ2J25t6zDqMMKeiNTIjK6UW-06rDtx43iWc7Xtvq876Jf7nGJzQfExqrV4chRUdHtdiMdutLXmG0XTQifKuhAyZ6gs0CatDxBmR_u-ys466diNEalshfetuEMn-M_a7Ecj7oZTJ2xiViqG9r0yJ9T22WOKJmeUkl-zUeY8UJ-exajFcr8Mg5cpXOYsbNQYeBg5XIjLnrgUX9C-e_8tU6TDhVoSSEpTuZkR6RJ-sgxz9hWr3_VYaPfH-gzRQ6G6BiZEYP3ZCASj9f2ENkpuebvnJvfPph0Fi8Cqdet_Zk89dyF9TZ7LSSpkCbReBPlZo0wB7p8avvPG5_ZQPTuZdqHrzq1FDZEMpG66Ko-iiRo5TmGxwyZov7GUxdWdbxK3xRuqabOpEv3pFce1cPP-4TXezoopLlfBhj37bAYBiIOCgymagQHhwEwkzuQOHk%3D;29701FE4-3CC7-4A34-8C5B-AE90C7439A47;STANDARD;1773331408336"
|
||||
}
|
||||
},
|
||||
"drmGroupId": "191-192",
|
||||
"licenseType": "standard",
|
||||
"expiration": 1773374608348
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"from": "playapi",
|
||||
"serverTime": 1773331408353,
|
||||
"version": 2
|
||||
},
|
||||
"limited_license_request": {
|
||||
"mslTimeout": null,
|
||||
"preferredlanguages": {
|
||||
"appselectedlanguages": [
|
||||
"en-JP",
|
||||
"en"
|
||||
],
|
||||
"platformselectedlanguages": [
|
||||
"en-JP"
|
||||
]
|
||||
},
|
||||
"url": "/license?licenseType=limited&playbackContextId=E3-Bgj5tevcAxLxBL5NAnkqpROV-Vv5c009gnelUKwWLsoz1xrjjCoijIahfybTJrUn2skQMT6LDT7xvyl1gZTowkKcS4pchPbAP8vpjPfCV6TGpM5GzSJmvgS8hYdU82kbP09lWDXYCPnoArh_5NLGHkxfuZYi_fWzXSdIyBPppI4fC5e8JwdhDfm4nauZMJlL8BcEOzkbv8pU1HEJ8XE0Y6AoOONLj3PUBtxIsY0rj9uB0IYHwJOXZoXw9AjB-0m1LEtF9Vf3C7ZaifLCgSrt3_SJpqVf-7P3WpOjKjduSU0OUHI2hJWhlluwlhUDu4ae59Mj9W6geIePu-1ZwCvE6HJ4vvfUUttR-fb3rlfV1bo037dpFspmeGcu-1f96mDKA3NuGipNlIObDrYRGSr8Bp9d9CNDREE7biE0R-vBhcXud621IyUW8sJo0A22tfvpA7Mo9apoJzgh1g7FhJiUyAq5HB0HunlVKNpZRWoIt95CUoz-MLlmqpzw3IAGOYGMLOq7V2pJrFoOq3IG_b7t9LQmtdbPCDKYdRB8seBxkJuxilC7tiobYRiAp8COxikTfrBtlW8J6ryggWUbr1MubvLKmpfo5WOBcl0ws_KC308K2RkGn0i0w2ISN6pkh6wXGCLtUlfVMc1SisoXBZ4iLAB1rIVcxQx0b9S4n4PHQW9dkLNL3MMpQMn6RCJuyxunk8jgYNILPQyqxsFXnXlwQTbgMdyRxls99QuATzPqHssrL7Wve6ZtQBwzPS6SwWS1g9Tn_RmhY64xBrz1OISU2vC0VlCHkJ1KVlSGxcXlalu5KaAHbjFHAOfMj2Bj3qe-_C9MVjTrr_O4jy0YBiIOCgzQOuElk9kZDQXw1Lk%3D&esn=NFAPPL-02-IPHONE9%3D1-5CB1D229FE1FC4DBA556753BB3D84634599DF9C15AD474BAFBFD37965D4162EC&drmContextId=2596051",
|
||||
"params": [
|
||||
{
|
||||
"drmSessionId": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"xid": "7118765455155927307",
|
||||
"clientTime": 1773331475,
|
||||
"videoTrackName": "V:2:1;2;;ce4;-1;none;-1;",
|
||||
"challengeBase64_decoded": {
|
||||
"CHALLENGES": [
|
||||
{
|
||||
"ID": "9B3BEEFC-B11A-4019-9966-F238CF4070BD",
|
||||
"PAYLOAD": "AAAAAQAAAAC801WquE7coG9bDqj3kHbbBOewafkJv9iCYrMSV08ZPHyp462GdKj84Oe5vaz8nysV49lh... (11096 chars total, ~8322 bytes)"
|
||||
},
|
||||
{
|
||||
"ID": "A41B0EA1-EECC-483C-A79A-237E5BD2E65A",
|
||||
"PAYLOAD": "AAAAAQAAAAAyyIOTwFbNFNvJgEuEqdxaPYLSOaIVd8ukY9+xSm/7sA2u0aRlFTa1NFB/l769l8M+r7bc... (10496 chars total, ~7872 bytes)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"limited_license_response": {
|
||||
"result": [
|
||||
{
|
||||
"licenseResponseBase64_decoded": {
|
||||
"VERSION": 1,
|
||||
"MEDIASESSIONID": "znvDeutRb/4=",
|
||||
"RESPONSES": [
|
||||
{
|
||||
"ID": "9B3BEEFC-B11A-4019-9966-F238CF4070BD",
|
||||
"PAYLOAD": "AAAAAQAAAACicCtA7Mlwl64lM3kfCZlBAAAEcCfqSlc/hnHCVaHXdjoUk8WuMLVzG6l0HbOBg3jRwaG5... (1552 chars total, ~1164 bytes)"
|
||||
},
|
||||
{
|
||||
"ID": "A41B0EA1-EECC-483C-A79A-237E5BD2E65A",
|
||||
"PAYLOAD": "AAAAAQAAAABANmpkGxS6jV17E1gHG2NAAAAEYNG9NfD6lWPjs9TWRWe2xf61zHkIMCmrvhbVFjpkxYoI... (1532 chars total, ~1149 bytes)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"secureStopExpected": false,
|
||||
"links": {
|
||||
"releaseLicense": {
|
||||
"rel": "releaseLicense",
|
||||
"href": "/releaseLicense?drmLicenseContextId=E3-Bgj5tevcAxLxBL5NAnkqpROV-Vv5c009gnelUKwWLsoz1xrjjCoijIahfybTJrUn2skQMT6LDT7xvyl1gZTowkKcS4pchPbAP8vpjPfCV6TGpM5GzSJmvgS8hYdU82kbP09lWDXYCPnoArh_5NLGHkxfuZYi_fWzXSdIyBPppI4fC5e8JwdhDfm4nauZMJlL8BcEOzkbv8pU1HEJ8XE0Y6AoOONLj3PUBtxIsY0rj9uB0IYHwJOXZoXw9AjB-0m1LEtF9Vf3C7ZaifLCgSrt3_SJpqVf-7P3WpOjKjduSU0OUHI2hJWhlluwlhUDu4ae59Mj9W6geIePu-1ZwCvE6HJ4vvfUUttR-fb3rlfV1bo037dpFspmeGcu-1f96mDKA3NuGipNlIObDrYRGSr8Bp9d9CNDREE7biE0R-vBhcXud621IyUW8sJo0A22tfvpA7Mo9apoJzgh1g7FhJiUyAq5HB0HunlVKNpZRWoIt95CUoz-MLlmqpzw3IAGOYGMLOq7V2pJrFoOq3IG_b7t9LQmtdbPCDKYdRB8seBxkJuxilC7tiobYRiAp8COxikTfrBtlW8J6ryggWUbr1MubvLKmpfo5WOBcl0ws_KC308K2RkGn0i0w2ISN6pkh6wXGCLtUlfVMc1SisoXBZ4iLAB1rIVcxQx0b9S4n4PHQW9dkLNL3MMpQMn6RCJuyxunk8jgYNILPQyqxsFXnXlwQTbgMdyRxls99QuATzPqHssrL7Wve6ZtQBwzPS6SwWS1g9Tn_RmhY64xBrz1OISU2vC0VlCHkJ1KVlSGxcXlalu5KaAHbjFHAOfMj2Bj3qe-_C9MVjTrr_O4jy0YBiIOCgzQOuElk9kZDQXw1Lk%3D;29701FE4-3CC7-4A34-8C5B-AE90C7439A47;LIMITED;1773331477163"
|
||||
}
|
||||
},
|
||||
"drmGroupId": "191-192",
|
||||
"licenseType": "limited_duration",
|
||||
"expiration": 1773331537178
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"from": "playapi",
|
||||
"serverTime": 1773331477180,
|
||||
"version": 2
|
||||
},
|
||||
"release_license_response": {
|
||||
"result": [
|
||||
{
|
||||
"actions": {}
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"common": {},
|
||||
"from": "playapi",
|
||||
"serverTime": 1773331482636,
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# PXA ESN — LLM 向けリファレンス
|
||||
|
||||
## PXA ESN とは
|
||||
|
||||
Netflix Android アプリがサーバーから取得するデバイス識別子。ローカル生成の Base ESN (PRV) に対し、サーバーが fingerprint を付与して発行する。API リクエストの `X-Netflix.esn` ヘッダーで使用される。
|
||||
|
||||
## 取得プロトコル
|
||||
|
||||
- **経路:** MSL (Message Security Layer) over HTTPS
|
||||
- **エンドポイント:** `/getProxyEsn`
|
||||
- **取得方法:** Cookie ではなく、MSL レスポンスの JSON ボディで返される
|
||||
|
||||
### リクエスト
|
||||
|
||||
```json
|
||||
{"url": "/getProxyEsn"}
|
||||
```
|
||||
|
||||
MSL VolleyRequest のヘッダーに `router: getProxyEsn` が設定される。HTTP レベルのヘッダーは MSL 暗号化されるため外部からは見えない。
|
||||
|
||||
### レスポンス
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"version": 2,
|
||||
"serverTime": 1773478742949,
|
||||
"result": {
|
||||
"esn": "NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-02028KVLM5OU1MSB..."
|
||||
},
|
||||
"common": {},
|
||||
"from": "playapi"
|
||||
}
|
||||
```
|
||||
|
||||
PXA ESN は `result.esn` に格納されている。
|
||||
|
||||
## 有効期限
|
||||
|
||||
- 設定値: `EsnHendrixConfig.refreshProxyEsnTimeInMs = 0`
|
||||
- TTL が 0 のため **無期限キャッシュ**
|
||||
- 一度取得したら SharedPreferences に永続化され、以下の場合のみ再取得:
|
||||
- 初回インストール
|
||||
- アプリデータクリア
|
||||
- SharedPreferences の消失
|
||||
- `masterTokenSerialNumber` の変更
|
||||
|
||||
## fingerprint の特性
|
||||
|
||||
- サーバー側で生成され、ローカルでは再現不可
|
||||
- 同一デバイスでも取得ごとに異なる fingerprint が返される
|
||||
- 実測で 3 回取得し、毎回異なることを確認
|
||||
|
||||
## ESN 構造
|
||||
|
||||
```
|
||||
NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-0202{fingerprint}
|
||||
```
|
||||
|
||||
| セグメント | 値 | 説明 |
|
||||
|---|---|---|
|
||||
| Platform | `NFANDROID1-` | Android プレフィックス |
|
||||
| Type | `PXA` | Proxy (サーバー発行) |
|
||||
| Category | `P` | Phone (T=Tablet, B=TV, C=ChromeOS, E=Display) |
|
||||
| Security | `L3` | Widevine Security Level |
|
||||
| Model | `GOOGLPIXEL=4A==5G=` | サニタイズ済みデバイスモデル |
|
||||
| systemId | `22594` | Widevine systemId |
|
||||
| fingerprint | `0202...` | サーバー発行 (毎回異なる) |
|
||||
|
||||
## 保存先
|
||||
|
||||
SharedPreferences に 2 つのキーで保存:
|
||||
|
||||
| キー | 型 | 内容 |
|
||||
|---|---|---|
|
||||
| `nf_drm_esn` | String | PXA ESN 文字列 |
|
||||
| `nf_drm_proxy_esn` | String (JSON) | `{"esn":"...","ts":epoch_ms,"sn":serial_number}` |
|
||||
|
||||
## 使用箇所
|
||||
|
||||
| ヘッダー | 使用先 |
|
||||
|---|---|
|
||||
| `X-Netflix.esn` | GraphQL / Cronet リクエスト |
|
||||
| `X-Netflix-ProxyEsn` | WebSocket / MSL リクエスト |
|
||||
|
||||
## コードパス
|
||||
|
||||
```
|
||||
WidevineEntityAuthEsnProviderImpl.c(serialNumber) -- 再取得が必要か判定
|
||||
→ ProxyEsnMslRequest -- MSL リクエスト送信
|
||||
→ body: {"url": "/getProxyEsn"}
|
||||
→ Netflix playapi サーバー
|
||||
← onSuccess(JSONObject) -- レスポンス受信
|
||||
→ result.esn を抽出
|
||||
→ ProxyEsn.onKnown(serialNumber, esn) -- SharedPreferences に保存
|
||||
```
|
||||
|
||||
## 関連クラス
|
||||
|
||||
| クラス | ファイルパス | 役割 |
|
||||
|---|---|---|
|
||||
| `ProxyEsnMslRequest` | `mslagent/impl/ProxyEsnMslRequest.java` | MSL リクエスト発行・レスポンス処理 |
|
||||
| `ProxyEsn` | `esn/impl/ProxyEsn.java` | キャッシュ管理・永続化 |
|
||||
| `WidevineEntityAuthEsnProviderImpl` | `esn/impl/WidevineEntityAuthEsnProviderImpl.java` | ESN 統合管理 |
|
||||
| `EsnHendrixConfig` (`o.fkQ`) | `o/C13212fkQ.java` | TTL 設定 (`refreshProxyEsnTimeInMs`) |
|
||||
|
||||
## キャプチャ方法
|
||||
|
||||
`hook_msl.js` を使用。`ProxyEsn.$init` で expired フラグを強制 true にすることで、キャッシュの有無に関係なく `getProxyEsn` を発火させられる。
|
||||
|
||||
```bash
|
||||
./run_android.sh hook_msl.js
|
||||
```
|
||||
|
||||
出力イベント: `proxyEsn.forceExpired`, `proxyEsn.request`, `proxyEsn.requestHeaders`, `proxyEsn.response`, `proxyEsn.onKnown`, `proxyEsn.error`
|
||||
Reference in New Issue
Block a user