# PXA ESN (Proxy ESN) — 取得フロー > **対象:** Netflix Android v9.57.0 (build 63928) > **取得方法:** Frida フック (`hook_msl.js` → `ProxyEsnMslRequest` / `ProxyEsn`) > **検証日:** 2026-03-14 --- ## 概要 PXA ESN はサーバーが発行するデバイス識別子。ローカル生成の Base ESN (PRV) と異なり、サーバー側で fingerprint を付与して返す。MSL 通信や GraphQL API で `X-Netflix.esn` / `X-Netflix-ProxyEsn` ヘッダーとして使用される。 --- ## 取得フロー ### 1. トリガー `WidevineEntityAuthEsnProviderImpl.c(Long serialNumber)` が呼ばれたとき、以下の条件で `getProxyEsn` が発行される: ```java // WidevineEntityAuthEsnProviderImpl.c() if (serialNumber == null) return true; // serialNumber なし → 再取得 if (proxyEsn.c != serialNumber) return true; // serialNumber 不一致 → 再取得 return proxyEsn.d; // expired フラグ → true なら再取得 ``` ### 2. キャッシュ有効期限 `ProxyEsn` コンストラクタで有効期限を判定: ```java // ProxyEsn.$init() long ttl = this.f; // EsnHendrixConfig.refreshProxyEsnTimeInMs if (ttl < 1) { this.d = false; // expired = false → 無期限キャッシュ return; } boolean expired = C20986jOb.c(ttl, this.g); // TTL ベースの期限判定 this.d = expired; ``` **実測値: `refreshProxyEsnTimeInMs = 0`** TTL が 0 のため、**PXA ESN は一度取得したら無期限にキャッシュされる**。`getProxyEsn` が再発行されるのは以下のケースのみ: - 初回インストール時 - アプリデータクリア時 - SharedPreferences が消えたとき - `masterTokenSerialNumber` が変わったとき ### 3. MSL リクエスト `ProxyEsnMslRequest` が MSL 経由で `/getProxyEsn` にリクエストを送信する。 **リクエストボディ:** ```json {"url": "/getProxyEsn"} ``` **リクエストヘッダー:** MSL 層で暗号化されるため、HTTP レベルでのヘッダーは空。MSL VolleyRequest のヘッダーとして `router: getProxyEsn` が設定される。 ```java // ProxyEsnMslRequest.getHeaders() C20979jNv.e(d, "router", "getProxyEsn", true, true); d.remove("x-netflix.nq-shadow.id"); d.remove("x-netflix.nq-shadow"); ``` ### 4. MSL レスポンス サーバー (`playapi`) が PXA ESN を JSON で返す。 **レスポンス:** ```json { "id": 1, "version": 2, "serverTime": 1773478742949, "result": { "esn": "NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-02028KVLM5OU1MSBUS4RUV6011TPPJVL5CS65GVMMH6PD5I6TSR8TMAC49OTSG3I4BJ3P78GD7ECQ6CVIIFHPN52CC20RJ2CPEE0A3FF" }, "common": {}, "from": "playapi" } ``` | フィールド | 説明 | |---|---| | `id` | リクエスト ID | | `version` | プロトコルバージョン | | `serverTime` | サーバー時刻 (epoch ms) | | `result.esn` | PXA ESN 本体 | | `from` | 発行元サーバー (`playapi`) | - Cookie ではない。MSL レスポンスの JSON ボディで返される。 - fingerprint 部分はサーバー側で生成されるため、ローカルでは再現不可。 - **起動ごとに異なる fingerprint が返される** (同一デバイスでも毎回異なる)。 ### 5. 保存 `ProxyEsn.onKnown(Long serialNumber, String esn)` で SharedPreferences に永続化。 ```java // ProxyEsn.onKnown() this.b = esn; jNK.b(this.e, "nf_drm_esn", esn); // PXA ESN 文字列 this.g = System.currentTimeMillis(); this.c = serialNumber; JSONObject json = new JSONObject(); json.put("esn", this.b); json.put("ts", this.g); // 取得時刻 json.put("sn", this.c); // masterTokenSerialNumber jNK.b(this.e, "nf_drm_proxy_esn", json.toString()); ``` **SharedPreferences キー:** | キー | 値 | 例 | |---|---|---| | `nf_drm_esn` | PXA ESN 文字列 | `NFANDROID1-PXA-P-L3-GOOGLPIXEL=...` | | `nf_drm_proxy_esn` | JSON メタデータ | `{"esn":"...","ts":1773478742949,"sn":8572399748193023}` | ### 6. 使用 取得後、以下のヘッダーで API リクエストに付与される: | ヘッダー | 用途 | |---|---| | `X-Netflix.esn` | GraphQL / Cronet リクエスト | | `X-Netflix-ProxyEsn` | WebSocket / MSL リクエスト | --- ## PXA ESN 構造 ``` NFANDROID1-PXA-P-L3-GOOGLPIXEL=4A==5G=-22594-02028KVLM5OU1MSB... │ │ │ │ │ │ └─ サーバー発行 fingerprint (毎回異なる) │ │ │ │ │ └─ Widevine systemId │ │ │ │ └─ sanitized model (Base ESN と同じ) │ │ │ └─ Security Level (L3) │ │ └─ Device Category (P=Phone) │ └─ Type: PXA = Proxy (サーバー発行) └─ Platform prefix ``` --- ## タイムライン (実測) ``` 08:41:58.240 ProxyEsn.$init() expired=true (キャッシュなし or 期限切れ) 08:41:58.240 getHeaders() router=getProxyEsn 08:41:58.248 getBodyForNq() {"url":"/getProxyEsn"} ── MSL リクエスト送信 ── 08:41:59.567 onSuccess() レスポンス受信 (約1.3秒後) 08:41:59.569 ProxyEsn.onKnown() SharedPreferences に保存 (2ms後) ── 以後のAPIリクエストで PXA ESN を使用 ── ``` --- ## 関連クラス | クラス | 役割 | |---|---| | `ProxyEsnMslRequest` | `/getProxyEsn` MSL リクエスト発行 | | `ProxyEsn` | PXA ESN のキャッシュ管理・永続化 | | `WidevineEntityAuthEsnProviderImpl` | ESN プロバイダー (PRV/PXA 統合管理) | | `EsnHendrixConfig` (`o.fkQ`) | `refreshProxyEsnTimeInMs` 設定値 | | `InterfaceC13214fkS` | `onKnown` コールバック | --- ## キャプチャ方法 `hook_msl.js` で以下のフックが有効: | フック | イベント名 | 内容 | |---|---|---| | `ProxyEsn.$init` | `proxyEsn.forceExpired` | expired を強制 true にして再取得を発火 | | `ProxyEsnMslRequest.getBodyForNq` | `proxyEsn.request` | リクエストボディ | | `ProxyEsnMslRequest.getHeaders` | `proxyEsn.requestHeaders` | リクエストヘッダー | | `ProxyEsnMslRequest.onSuccess` | `proxyEsn.response` | レスポンス JSON | | `ProxyEsnMslRequest.e` | `proxyEsn.error` | エラー | | `ProxyEsn.onKnown` | `proxyEsn.onKnown` | 保存される ESN と serialNumber | ```bash ./run_android.sh hook_msl.js ```