Files
revkit/docs/spec/00_common.md
T
tkgstratorandClaude Opus 4.6 d91af8380b docs: Netflix仕様書の再構成とiOS解析ドキュメント追加
- 旧ドキュメント (docs/netflix/, docs/instructions/ 等) を削除
- docs/spec/: appboot ピンニング解析、iOS再生フロー、
  MSL CBOR鍵交換解析、キャプチャ復号状況、暗号化フロー図を追加
- docs/netflix_client_spec.md, platform_flow_comparison.md を追加
- iOS認証・マニフェスト・ライセンス取得フロー詳細を追加

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 02:06:14 +00:00

236 lines
7.3 KiB
Markdown

# Netflix MSL クライアント共通仕様
プラットフォーム共通の MSL プロトコル、マニフェスト構造、セグメントダウンロード、復号の仕様。
プラットフォーム固有の認証・エンドポイント・DRM は各プラットフォーム別仕様を参照。
---
## 1. MSL メッセージ暗号化
### 1.1 エンベロープ構造
MSL レスポンスは複数の JSON オブジェクトが連結された形式:
```
{headerdata, signature, mastertoken}{payload, signature}{payload, signature}...
```
- 1つ目: ヘッダー (mastertoken 含む)
- 2つ目以降: ペイロードチャンク
### 1.2 ペイロード暗号化フォーマット
各ペイロードチャンクの `payload` を base64 デコードすると:
```json
{
"ciphertext": "<base64(AES-CBC encrypted data)>",
"sha256": "<base64(HMAC-SHA256)>",
"keyid": "{ESN}_{sequence}",
"iv": "<base64(16-byte IV)>"
}
```
### 1.3 復号手順
1. `iv` を base64 デコード (16バイト IV)
2. `ciphertext` を base64 デコード (暗号文)
3. MSL セッションの `encryption_key` (AES-128) と IV で AES-CBC 復号
4. PKCS7 パディング除去
5. 結果は JSON: `{data, messageid, compressionalgo, sequencenumber}`
6. `data` を base64 デコード → 圧縮されたペイロード本体
7. 圧縮展開 (gzip または LZW) → JSON
### 1.4 圧縮方式
| `compressionalgo` | 方式 | 使用プラットフォーム |
|-------------------|------|-------------------|
| (gzip magic `1f8b`) | gzip | StreamFab, Android |
| `LZW` | LZW | Chrome |
---
## 2. マニフェストレスポンス構造
### 2.1 トップレベル
```json
{
"id": 1775356074,
"version": 2,
"serverTime": 1775356074742,
"result": {
"movieId": 81756595,
"duration": 8523000,
"drmType": "widevine",
"drmVersion": 25,
"drmContextId": "2596051",
"playbackContextId": "E3-Bgilt-vcAxKKB...",
"expiration": 1775399301816,
"video_tracks": [...],
"audio_tracks": [...],
"timedtexttracks": [...],
"servers": [...],
"links": {
"license": { "href": "/license?licenseType=standard&playbackContextId=...&esn=...&drmContextId=..." },
"ldl": { "href": "/license?licenseType=limited&playbackContextId=...&esn=...&drmContextId=..." },
"events": { "href": "/events?playbackContextId=...&esn=..." }
}
},
"common": { "cadToken": "C1-Bgjtt-vcAxKMAQX..." },
"from": "playapi"
}
```
`links.license.href` はライセンスチャレンジ時にそのまま使用する相対パス。
### 2.2 ビデオトラック
```json
{
"trackType": "PRIMARY",
"maxWidth": 1920,
"maxHeight": 1080,
"drmHeader": {
"bytes": "AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAloudsAAAAAAAAAAA==",
"keyId": "AAAAAAloudsAAAAAAAAAAA==",
"drmHeaderId": "000000000968b9db0000000000000000",
"resolution": { "height": 540, "width": 960 }
},
"streams": [...]
}
```
- `drmHeader.bytes`: **PSSH ボックス** (base64)。Widevine CDM の `generateRequest` に渡す `initData`
- `drmHeader` は track レベルに 1 つだが、**KID 境界は stream レベル** (SD 鍵 / HD 鍵)
### 2.3 ビデオストリーム
```json
{
"content_profile": "av1-main-L40-dash-cbcs-prk",
"bitrate": 1412,
"peakBitrate": 9675,
"res_w": 1920,
"res_h": 1080,
"framerate_value": 24000,
"framerate_scale": 1001,
"size": 1511911636,
"startByteOffset": 67372,
"vmaf": 97,
"drmHeaderId": "000000000968b9de0000000000000000",
"downloadable_id": "1500103607",
"isDrm": true,
"moov": { "offset": 116, "size": 992 },
"sidx": { "offset": 1108, "size": 33140 },
"ssix": { "offset": 34248, "size": 33124 },
"urls": [
{ "url": "https://{cdn_host}/?o=1&v=21&e={expiration}&t={token}", "cdn_id": 140752 }
]
}
```
| フィールド | 説明 |
|-----------|------|
| `content_profile` | コーデック + 暗号化スキーム識別子 |
| `drmHeaderId` | この stream の DRM Key ID (hex 32文字) |
| `downloadable_id` | Netflix 内部ストリーム ID |
| `urls[].url` | CDN ダウンロード URL (署名付き、有効期限あり) |
| `moov/sidx/ssix` | MP4 ボックスオフセット (セグメントインデックスに必要) |
| `startByteOffset` | メディアデータ開始位置 |
| `size` | 全セグメント合計サイズ (bytes) |
### 2.4 DRM スキーム対応表
| content_profile パターン | 暗号化スキーム | 暗号化方式 | 使用コーデック |
|-------------------------|--------------|-----------|-------------|
| `*-playready-*` | `cenc` | AES-128-CTR (全サンプル) | H.264 |
| `*-cenc-*` | `cenc` | AES-128-CTR (全サンプル) | HEVC |
| `*-cbcs-*` | `cbcs` | AES-128-CBC (サブサンプル 1/10) | AV1 |
**コーデックごとに異なる KID ペアが割り当てられる。** コーデック切り替え時はライセンス再取得が必要。
### 2.5 KID 境界 (実測)
同一コーデック内で 2 つの KID が使い分けられる:
- **SD** (540p 以下): SD 用 Key ID
- **HD** (720p 以上): HD 用 Key ID
### 2.6 オーディオトラック
| ソース | プロファイル | チャンネル | ビットレート |
|--------|------------|-----------|------------|
| licensedmanifest | `ddplus-5.1-dash`, `ddplus-5.1hq-dash` | 5.1 | 192, 256, 384, 448, 640 kbps |
| manifest API | `xheaac-dash` | 2.0 | 32, 64, 96, 192 kbps |
licensedmanifest は 19 言語で 5.1ch を提供。manifest API は 2.0ch のみ。
### 2.7 CDN サーバー
```json
{
"dns": { "host": "...", "ipv6": "...", "ipv4": "..." },
"id": 140752,
"rank": 1,
"type": "OPEN_CONNECT_APPLIANCE",
"name": "c014.tyo013.ix.nflxvideo.net"
}
```
`rank` が低いほど優先。`urls[].cdn_id` と `servers[].id` で対応付ける。
---
## 3. セグメントダウンロード
### 3.1 CDN URL 構造
```
https://{cdn_host}/?o=1&v={version}&e={expiration_unix}&t={signature_token}
```
| パラメータ | 説明 |
|-----------|------|
| `o` | 常に 1 |
| `v` | CDN バージョン (21, 22 等) |
| `e` | URL 有効期限 (Unix timestamp) |
| `t` | 署名トークン (base64url) |
### 3.2 DASH セグメントダウンロード手順
1. `Range: bytes=0-{moov.offset + moov.size - 1}` で初期化セグメントを取得
2. `sidx` をパースしてセグメント一覧 (バイトレンジ + 時間) を構築
3. 各セグメントを `Range` リクエストで取得
---
## 4. 復号
### 4.1 復号フロー
```
encrypted_segment + content_key(KID) → decrypted_segment
```
1. セグメントの `moof` ボックスから `tenc` (Track Encryption Box) を読み取り
2. `senc` (Sample Encryption Box) から各サンプルの IV を取得
3. Key ID に対応するコンテンツ鍵で AES-CTR (cenc) または AES-CBC (cbcs) 復号
### 4.2 Widevine ライセンスフロー (共通)
1. マニフェストの `drmHeader.bytes` (PSSH, base64) をデコード
2. Widevine CDM `generateRequest("cenc", psshBytes)` → `license-request` (protobuf, ~4000 bytes)
3. MSL 経由で `/pbo_licenses/router` に POST
4. レスポンスから `license-response` (protobuf, ~1000 bytes) 取得
5. CDM `update(licenseResponse)` でコンテンツ鍵インストール
---
## 5. 有効期限
| 項目 | フィールド | 典型値 |
|------|-----------|-------|
| マニフェスト | `expiration` | ~8 時間 (`manifestExpirationDuration`) |
| CDN URL | `e` パラメータ | ~12 時間 (`urlExpirationDuration`) |
| Cookie | セッション依存 | 数日〜数週間 |