mirror of
https://github.com/qtmleap/revkit.git
synced 2026-09-28 14:01:50 +02:00
197 lines
6.3 KiB
Markdown
197 lines
6.3 KiB
Markdown
# 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
|
|
```
|