Files
revkit/docs/userauthdata_android.md
T

23 KiB

Netflix MSL UserAuthData — Android 動的解析レポート

Netflix Android アプリ (com.netflix.mediaclient v9.57.0) の MSL 通信におけるユーザー認証データ (userauthdata) の生成・送信メカニズムを、Frida フックおよびキャプチャ済み暗号平文の CBOR 解析により明らかにした結果をまとめる。


1. 結論サマリー

項目 内容
userauthdata の送信タイミング 初回認証時のみ 1 回 (鍵交換 + エンティティ認証)
使用される認証スキーム NETFLIXID (netflixId + secureNetflixId)
通常リクエストの認証方式 UserIdToken (key 18) — サーバー発行のトークン
CBOR キー userauthdata = key 47, useridtoken = key 18
apiRequest 第5引数 常に null — userauthdata は MslControl 層で MSL MessageHeader に直接埋め込まれ、apiRequest パラメータとしては渡されない

2. ユーザー認証フロー

%%{init:{'theme':'dark'}}%%
sequenceDiagram
    participant App as Netflix App
    participant MSL as MslControl
    participant WV as WidevineCryptoContext
    participant Server as Netflix Server

    rect rgba(200,100,100,0.15)
        Note over App,Server: 初回認証 (1回のみ)
        App->>MSL: apiRequest(url, body, headers, userId, null)
        Note over MSL: MslControl が MessageHeader を構築
        MSL->>MSL: userauthdata を MessageHeader に埋め込み<br/>(CBOR key 47)
        Note over MSL: scheme: NETFLIXID<br/>authdata: {netflixId, secureNetflixId}
        MSL->>WV: encrypt(headerdata)
        MSL->>Server: MSL Message (暗号化済み)
        Server->>MSL: keyresponsedata + mastertoken<br/>+ UserIdToken
    end

    rect rgba(100,200,100,0.15)
        Note over App,Server: 以降のリクエスト (348回観測)
        App->>MSL: apiRequest(url, body, headers, userId, null)
        Note over MSL: MslControl が MessageHeader を構築
        MSL->>MSL: UserIdToken を MessageHeader に埋め込み<br/>(CBOR key 18)
        MSL->>WV: encrypt(headerdata)
        MSL->>Server: MSL Message (暗号化済み)
    end

3. キャプチャデータによる証明

3.1 MSL MessageHeader の認証キー分布

353 件の暗号化前 MSL MessageHeader (Widevine encrypt 平文) を CBOR 解析した結果:

認証方式 CBOR キー 件数 割合
UserIdToken のみ key 18 348 98.6%
userauthdata のみ (NETFLIXID) key 47 1 0.3%
どちらもなし (鍵交換 handshake) — 5 1.1%
両方 key 18 + 47 0 0%

3.2 userauthdata が送信されたヘッダー (1件)

ファイル: logs/android_20260313/android14.prod.ftl.netflix.com/0052_msl.widevine.encrypt.json

MSL MessageHeader CBOR キー:
  [17, 19, 20, 21, 22, 24, 36, 40, 41, 42, 47]
    17: mastertoken (list)
    19: renewable = false
    20: sender = "" (ESN)
    21: handshake = true        ← 鍵交換ハンドシェイク
    22: messageid = 2491352839960294
    24: timestamp = 1773392548
    40: peer = true
    41: nonreplayable_id = 10
    42: keyrequestdata (WIDEVINE)
    47: userauthdata            ← ★ ここにユーザー認証データ

3.3 userauthdata の構造 (CBOR key 47)

{
  "30": "NETFLIXID",
  "35": {
    "56": "<netflixId>",
    "60": "<secureNetflixId>"
  }
}
CBOR キー フィールド名 値
30 scheme NETFLIXID
35 authdata (dict)
35.56 netflixId v=3&mac=AQEAEQABABQxZyPp7r91RNj4pmPTIatYcL3jjIUVbvQ.&dt=1773392547767
35.60 secureNetflixId v=3&ct=<暗号化トークン 672文字>&pg=ZEULH5S2GNGCRAABCSG6J2EGGA&ch=<HMAC>

3.4 netflixId の構造

v=3&mac=AQEAEQABABQxZyPp7r91RNj4pmPTIatYcL3jjIUVbvQ.&dt=1773392547767
パラメータ 値 意味
v 3 バージョン
mac AQEAEQABABQxZyPp7r91RNj4pmPTIatYcL3jjIUVbvQ. HMAC 署名 (改ざん検知)
dt 1773392547767 デバイスタイムスタンプ (Unix ms)

3.5 secureNetflixId の構造

v=3&ct=<暗号化トークン>&pg=ZEULH5S2GNGCRAABCSG6J2EGGA&ch=<HMAC>
パラメータ 値 意味
v 3 バージョン
ct BgjHlOvcAxLcA5Nne... (672 文字) 暗号化された認証トークン (protobuf base64url)
pg ZEULH5S2GNGCRAABCSG6J2EGGA プロファイル GUID
ch AQEAEAABABTp79nN9l_2MuRhqTXl0-SjAqcm83QU8vw. チャネル HMAC

4. UserIdToken の構造 (通常リクエスト)

初回認証後、サーバーから発行された UserIdToken が以降の全リクエストで使用される。

4.1 CBOR 構造 (key 18)

{
  "15": "<tokendata (bytes, CBOR エンコード)>",
  "16": "<signature (bytes, 44B HMAC)>"
}

4.2 tokendata の CBOR 構造

CBOR キー フィールド名 値 (キャプチャ例)
29 userdata <bytes 217> (暗号化されたユーザーデータ)
25 serialnumber 5970945423176003
11 issuedate 1773420422 (2026-03-13 16:47:02 UTC)
12 mtserialnum 8079134327147185 (紐づく MasterToken のシリアル)
profileid profileid ZEULH5S2GNGCRAABCSG6J2EGGA
13 expiration 1774601222 (2026-03-27 08:47:02 UTC, 14日後)

4.3 トークンライフサイクル

発行: 2026-03-13 16:47:02 UTC
有効期限: 2026-03-27 08:47:02 UTC
有効期間: 14日間

同一セッション内で全 348 リクエストが同じ UserIdToken (serialnumber: 5970945423176003) を使用。


5. 認証スキーム一覧

Netflix MSL は 5 つのユーザー認証スキームをサポート (iOS ドキュメントから):

スキーム クラス (Android) 送信データ 用途
NETFLIXID com.netflix.msl.userauth.NetflixIdAuthenticationData (ProGuard 難読化) netflixId, secureNetflixId 本キャプチャで観測 — Cookie ベース認証
EMAIL_PASSWORD ProGuard 難読化 (ClassNotFound) email, password ログインフォーム認証
USER_ID_TOKEN com.netflix.msl.userauth.UserIdTokenAuthenticationData 既存 UserIdToken トークン再認証
SSO_TOKEN ProGuard 難読化 (ClassNotFound) ssoToken SSO 認証
SWITCH_PROFILE ProGuard 難読化 (ClassNotFound) switchGUID, originalGUID プロファイル切替

5.1 ProGuard 難読化の影響

クラス 状態
UserAuthenticationData (基底クラス) 存在確認済み — メソッド a(2), b(2), c(2) に難読化
UserIdTokenAuthenticationData 存在確認済み — コンストラクタフック成功 (ただし呼び出しなし)
EmailPasswordAuthenticationData ClassNotFoundException
NetflixIdAuthenticationData ClassNotFoundException
SsoTokenAuthenticationData ClassNotFoundException
SwitchProfileAuthenticationData ClassNotFoundException

→ 4 つのサブクラスが ProGuard で難読化/インライン化されている。UserIdTokenAuthenticationData のみ元のクラス名が残存。


6. MSL CBOR 整数キーマッピング (固定値)

MSL プロトコルは JSON と CBOR の 2 つのエンコーディングをサポートする。CBOR エンコード時は、JSON の文字列キーがプロトコル仕様で定義された固定の整数キーに置換される。これはメッセージサイズ削減のための最適化であり、Netflix の MSL ライブラリ (MslEncoderFactory) にハードコードされている。

以下のマッピングは、353 件の暗号化前 MSL MessageHeader と 403 件の PayloadChunk の CBOR 解析、および iOS 版 MSL ドキュメント (msl_ios.md §3) の JSON フィールド名との照合により確定したものである。

6.1 MSL メッセージ外殻 (Header / MasterToken / EntityAuthData)

JSON 構造:

{
  "headerdata": "<Base64 暗号文>",
  "signature": "<Base64 HMAC-SHA256>",
  "mastertoken": { "tokendata": "...", "signature": "..." }
}
CBOR キー JSON フィールド名 型 説明 観測値例
15 tokendata bytes トークンデータ (CBOR エンコード、暗号化済み) <bytes 153〜296>
16 signature bytes HMAC 署名 <bytes 44> (HMAC-SHA256)

注: key 15/16 は MasterToken, UserIdToken, keyrequestdata 等のトークン系構造体で共通使用される汎用キー。

6.2 MessageHeader (復号後の headerdata)

JSON 構造 (iOS 版 msl_ios.md §3.2):

{
  "messageid": 12345,
  "sender": "NFANDROID1-PRV-P-...",
  "renewable": false,
  "handshake": false,
  "timestamp": 1773392401,
  "capabilities": { ... },
  "useridtoken": { "tokendata": "...", "signature": "..." },
  "userauthdata": { "scheme": "NETFLIXID", "authdata": { ... } },
  "keyrequestdata": [ ... ],
  "nonreplayableid": 10
}
CBOR キー JSON フィールド名 型 説明 観測数
17 mastertoken list[dict{15,16}] MasterToken (tokendata + signature) 18 件
18 useridtoken dict{15,16} UserIdToken (tokendata + signature) 348 件
19 renewable bool トークン更新可能フラグ 353 件 (全ヘッダー)
20 sender string 送信者 ESN (空文字列 = デフォルト) 353 件
21 handshake bool ハンドシェイクメッセージフラグ 353 件
22 messageid int メッセージ一意 ID 353 件
24 timestamp int Unix タイムスタンプ (秒) 353 件
36 capabilities dict クライアント能力情報 353 件
40 peer bool ピアツーピアフラグ 353 件
41 nonreplayableid int リプレイ防止 ID 2 件
42 keyrequestdata list[dict{30,31}] 鍵交換リクエストデータ 3 件
47 userauthdata dict{30,35} ユーザー認証データ 1 件

6.3 capabilities (key 36 の内部)

CBOR キー JSON フィールド名 型 説明 観測値
37 compressionalgos list[string] サポート圧縮アルゴリズム [] (空)
38 languages list[string] サポート言語 ["GZIP", "LZW"]
39 encoders list[string] サポートエンコーダー ["CBOR"]
maxpayloadchunksize maxpayloadchunksize int 最大ペイロードチャンクサイズ -1 (無制限)

注: maxpayloadchunksize は文字列キーのまま。Netflix 独自拡張のため整数キーが未割当と推定。

6.4 keyrequestdata / keyresponsedata (key 42 の内部要素)

CBOR キー JSON フィールド名 型 説明 観測値
30 scheme string 鍵交換スキーム "WIDEVINE"
31 keydata dict 鍵データ Widevine デバイス証明書
31.duid keydata.duid bytes デバイス一意 ID (32B) Widevine deviceUniqueId
31.50 keydata.cdmsg bytes CDM メッセージ (protobuf) Widevine ライセンスチャレンジ (~2577B)

6.5 userauthdata (key 47 の内部)

CBOR キー JSON フィールド名 型 説明 観測値
30 scheme string 認証スキーム "NETFLIXID"
35 authdata dict スキーム固有の認証データ dict{56, 60}

NETFLIXID スキームの authdata (key 35 の内部)

CBOR キー JSON フィールド名 型 説明
56 netflixid string NetflixId Cookie (URL エンコード)
60 securenetflixid string SecureNetflixId Cookie (URL エンコード)

6.6 MasterToken tokendata (key 17[].15 を CBOR デコード)

CBOR キー JSON フィールド名 型 説明 観測値例
25 serialnumber int MasterToken シリアル番号 5970945423176003
26 renewable bool 更新可能フラグ true
27 issuer string 発行者 "sf", "cad"
28 identity bytes エンティティ ID (暗号化済み) <bytes 125〜226>
43 sequencenumber int シーケンス番号 8079134327147185

6.7 UserIdToken tokendata (key 18.15 を CBOR デコード)

CBOR キー JSON フィールド名 型 説明 観測値例
11 issuedate int 発行日 (Unix タイムスタンプ秒) 1773420422
12 mtserialnum int 紐づく MasterToken のシリアル番号 8079134327147185
13 expiration int 有効期限 (Unix タイムスタンプ秒) 1774601222
25 serialnumber int UserIdToken シリアル番号 5970945423176003
29 userdata bytes 暗号化されたユーザーデータ <bytes 217>
profileid profileid string プロファイル GUID "ZEULH5S2GNGCRAABCSG6J2EGGA"

注: profileid は文字列キーのまま。Netflix 独自拡張フィールド。

6.8 PayloadChunk (暗号化前)

CBOR キー JSON フィールド名 型 説明 観測値例
14 sequencenumber int チャンクシーケンス番号 1, 2, ... 8
22 messageid int メッセージ ID (MessageHeader と一致) 1170337128274032
44 compressionalgo string 圧縮アルゴリズム "GZIP"
62 data bytes ペイロードデータ (GZIP 圧縮) <bytes 40〜3497>
63 endofmsg bool 最終チャンクフラグ true / false

6.9 全整数キー一覧 (ソート順)

キャプチャデータから観測された全 28 整数キーの完全一覧:

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

6.10 文字列キー (整数マッピングなし)

キー 使用コンテキスト 説明
maxpayloadchunksize capabilities 最大ペイロードチャンクサイズ
profileid UserIdToken.tokendata プロファイル GUID
duid keyrequestdata.keydata デバイス一意 ID

これらは Netflix 独自の拡張フィールドであり、MSL 標準の整数キーが割り当てられていないと推定される。


7. userauthdata の生成メカニズム

7.1 生成レイヤー

userauthdata は apiRequest のパラメータとしてではなく、MslControl 層で MSL MessageHeader に直接埋め込まれる:

Netflix App
  ↓ apiRequest(url, body, headers, userId=GUID, auth=null)
ApiHandlerImpl
  ↓ (auth は null のまま渡される)
MslControl
  ↓ MessageHeader を構築
  ↓ ← ここで userauthdata を設定
  ↓   (初回: NETFLIXID スキーム)
  ↓   (以降: UserIdToken を使用)
WidevineCryptoContext.encrypt(headerdata)
  ↓
暗号化された MSL メッセージ → サーバー送信

7.2 netflixId / secureNetflixId の生成元

これらの値は Android の Cookie ストレージ から取得される:

Cookie 名 生成元 保存場所
netflixId (=NetflixId) サーバーが Set-Cookie で発行 Android CookieManager / SharedPreferences
secureNetflixId (=SecureNetflixId) サーバーが Set-Cookie で発行 (Secure 属性) Android CookieManager / SharedPreferences

ログイン成功時にサーバーからこれらの Cookie が発行され、MSL ユーザー認証に使用される。

7.3 RenewSSOToken との関係

キャプチャで RenewSSOToken GraphQL オペレーションが観測されている:

{
  "operationName": "RenewSSOToken",
  "variables": {
    "ssoToken": "BgiHtuvcAxL4AWLUqSE7W14gsbHZBDGMle4StHvRnr70..."
  }
}

これは MSL レベルの userauthdata とは別の、GraphQL レベルの SSO トークン更新。MSL 層では既に UserIdToken で認証されており、SSO トークン更新は MSL ペイロード内で行われる。


8. 認証フロー全体図

%%{init:{'theme':'dark'}}%%
sequenceDiagram
    participant User as ユーザー
    participant App as Netflix App
    participant MSL as MslControl
    participant Server as Netflix Server

    rect rgba(255,200,200,0.15)
        Note over User,Server: Phase 1: ログイン (本キャプチャ前)
        User->>App: メールアドレス + パスワード入力
        App->>Server: HTTPS POST /login
        Server->>App: Set-Cookie: NetflixId, SecureNetflixId
        Note over App: Cookie をローカルに保存
    end

    rect rgba(200,200,255,0.15)
        Note over User,Server: Phase 2: MSL 初回認証 (キャプチャ済み, 1回)
        App->>MSL: apiRequest(..., userId=GUID, auth=null)
        MSL->>MSL: MessageHeader 構築
        Note over MSL: key 42: keyrequestdata (WIDEVINE)<br/>key 47: userauthdata (NETFLIXID)<br/>  → netflixId + secureNetflixId
        MSL->>Server: 暗号化 MSL メッセージ
        Server->>MSL: keyresponsedata + MasterToken<br/>+ UserIdToken (有効期間 14日)
        Note over MSL: UserIdToken をメモリに保持
    end

    rect rgba(200,255,200,0.15)
        Note over User,Server: Phase 3: 通常リクエスト (348回観測)
        App->>MSL: apiRequest(..., userId=GUID, auth=null)
        MSL->>MSL: MessageHeader 構築
        Note over MSL: key 18: useridtoken<br/>  → serialnumber + profileid + expiration
        MSL->>Server: 暗号化 MSL メッセージ
        Server->>MSL: MSL レスポンス
    end

    rect rgba(255,255,200,0.15)
        Note over User,Server: Phase 4: トークン更新 (15回観測)
        Note over MSL: MasterToken + keyrequestdata で<br/>トークンを更新 (key 17 + key 18)
        MSL->>Server: 更新リクエスト
        Server->>MSL: 新しい MasterToken
    end

9. iOS 版との比較

観点 Android iOS
userauthdata スキーム NETFLIXID (netflixId + secureNetflixId) NETFLIXID (同一)
CBOR キー 整数キー (47, 18, etc.) 整数キー (同一仕様)
apiRequest パラメータ 第5引数 UserAuthenticationData auth = null makeUserAuthData() メソッドで生成
userauthdata 設定レイヤー MslControl (Java) IosMslClient (C++)
暗号化 WidevineCryptoContext AES-CBC (C++)
UserIdToken 有効期間 14 日 不明
ProGuard 影響 サブクラス 4/5 が難読化で ClassNotFound N/A (C++ バイナリ)

10. 解析に使用したコード

10.1 CBOR デコードスクリプト

import base64, json, cbor2
from urllib.parse import unquote

# Widevine encrypt 平文から MSL MessageHeader を解析
with open("logs/android_20260313/android14.prod.ftl.netflix.com/0052_msl.widevine.encrypt.json") as f:
    data = json.load(f)

raw = base64.b64decode(data["plaintext_b64"])
header = cbor2.loads(raw)

# userauthdata (key 47) を取得
if 47 in header:
    uad = header[47]
    scheme = uad[30]           # "NETFLIXID"
    authdata = uad[35]
    netflix_id = unquote(authdata[56])
    secure_netflix_id = unquote(authdata[60])
    print(f"scheme: {scheme}")
    print(f"netflixId: {netflix_id}")
    print(f"secureNetflixId: {secure_netflix_id}")

# UserIdToken (key 18) を取得
if 18 in header:
    uit = header[18]
    tokendata = cbor2.loads(uit[15])
    print(f"profileid: {tokendata.get('profileid')}")
    print(f"serialnumber: {tokendata.get(25)}")
    print(f"expiration: {tokendata.get(13)}")

10.2 Frida フックによるキャプチャ

hook_netflix_android.js に以下のフックを実装:

フック対象 目的 結果
ApiHandlerImpl.apiRequest 第5引数 UserAuthenticationData オブジェクト取得 常に null (MslControl 層で設定されるため)
UserIdTokenAuthenticationData.$init コンストラクタ監視 フック成功、呼び出しなし (トークンはサーバーから受信)
EmailPasswordAuthenticationData.$init コンストラクタ監視 ClassNotFoundException (ProGuard)
NetflixIdAuthenticationData.$init コンストラクタ監視 ClassNotFoundException (ProGuard)
SsoTokenAuthenticationData.$init コンストラクタ監視 ClassNotFoundException (ProGuard)
SwitchProfileAuthenticationData.$init コンストラクタ監視 ClassNotFoundException (ProGuard)
UserAuthenticationData.getScheme スキーム名取得 メソッド名が難読化 (ProGuard)
WidevineCryptoContext.encrypt 暗号化前平文取得 成功 — CBOR 解析で userauthdata 発見

11. まとめ

  1. userauthdata は MSL MessageHeader の CBOR key 47 として暗号化前に埋め込まれる。apiRequest のパラメータ(第5引数)としては渡されず、MslControl 層が Cookie ストレージから netflixId / secureNetflixId を読み取って設定する。

  2. NETFLIXID スキームが使用され、netflixId (HMAC 付きデバイストークン) と secureNetflixId (暗号化認証トークン + プロファイル GUID) の 2 つの値で構成される。

  3. 初回の MSL ハンドシェイク時に 1 回だけ送信される。サーバーは応答として UserIdToken (有効期間 14 日) を発行し、以降の全リクエストはこの UserIdToken (key 18) で認証される。

  4. 353 件の MSL ヘッダーのうち userauthdata を含むのは 1 件のみ (0.3%)。残りの 98.6% は UserIdToken による認証。

  5. ProGuard 難読化により NetflixIdAuthenticationData 等のサブクラスは直接フックできないが、Widevine encrypt の平文データを CBOR 解析することで userauthdata の完全な内容を取得できた。