# 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= ``` ### 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): [0].16 (signature): 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: , 50 (cdmsg): } ``` ### 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): 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): (暗号化済み) 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": "" } } ] }, "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 箇所をフックすれば、全箇所に波及する。