Files
revkit/docs/pxa_esn.md
T

6.3 KiB

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 が発行される:

// WidevineEntityAuthEsnProviderImpl.c()
if (serialNumber == null) return true;           // serialNumber なし → 再取得
if (proxyEsn.c != serialNumber) return true;     // serialNumber 不一致 → 再取得
return proxyEsn.d;                               // expired フラグ → true なら再取得

2. キャッシュ有効期限

ProxyEsn コンストラクタで有効期限を判定:

// 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 にリクエストを送信する。

リクエストボディ:

{"url": "/getProxyEsn"}

リクエストヘッダー:

MSL 層で暗号化されるため、HTTP レベルでのヘッダーは空。MSL VolleyRequest のヘッダーとして router: getProxyEsn が設定される。

// 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 で返す。

レスポンス:

{
  "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 に永続化。

// 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
./run_android.sh hook_msl.js