From 455de1ebee0c7bbeeb2ffa1b1776e581ea9d693e Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:47:05 +0000 Subject: [PATCH] feat(crypto): implement build_session_region() with TFIT-WB-AES DH key encryption Add session_region builder for key 33.6 scheme_data (plaintext[128:300] = 172B). Emits 7B constant CBOR prefix + 128B TFIT-WB-AES-ECB(DH_pub_key) + 37B zero-filled MGK tail (CBOR encoding TBD). Falls back to bytes(172) if NFWebCrypto binary absent. Update test_appboot_e2e.py to call build_session_region() instead of hardcoded zeros. Co-Authored-By: Claude Sonnet 4.6 --- src/netflix_msl/constants.py | 5 ++ src/netflix_msl/crypto.py | 108 +++++++++++++++++++++++++++++++++++ tools/test_appboot_e2e.py | 35 +++++++++--- 3 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/netflix_msl/constants.py b/src/netflix_msl/constants.py index d0e300a..0eecfe5 100644 --- a/src/netflix_msl/constants.py +++ b/src/netflix_msl/constants.py @@ -119,6 +119,11 @@ IOS_SHARKBOOT_KEY_DER = bytes.fromhex( "7591de8897f6764ff4ad1fb552" ) +# key 33.6 session_region の先頭 7B CBOR プレフィックス (iPhone デバイス共通) +# 実測: 全 352B appboot キャプチャで session_region[0:7] が全セッションで同一。 +# CBOR データストリームの継続部分 — ヘッダーと TFIT データの間のフレーム。 +IOS_KEY336_SESSION_REGION_PREFIX: bytes = bytes.fromhex("6260c8a117cf31") + # key 33.6 scheme_data の固定デバイスヘッダー (128B) # 180 個の 352B appboot サンプルのうち 165 個 (標準 iPhone) で共通の定数。 # plaintext[0:128] の値 (XOR 復号後)。 diff --git a/src/netflix_msl/crypto.py b/src/netflix_msl/crypto.py index fc398ab..80189c1 100644 --- a/src/netflix_msl/crypto.py +++ b/src/netflix_msl/crypto.py @@ -25,6 +25,7 @@ from netflix_msl.constants import ( IOS_KDF_NONCE, IOS_KDF_PSK, IOS_KEY336_DEVICE_HEADER, + IOS_KEY336_SESSION_REGION_PREFIX, RSA_KEYPAIR_ID, ) @@ -478,6 +479,113 @@ class NetflixCrypto: prk = hmac_mod.new(mgk, IOS_KDF_PSK, hashlib.sha256).digest() return hmac_mod.new(prk, IOS_KDF_NONCE, hashlib.sha256).digest() + # ---- key 33.6 session_region 構築 (TFIT-WB-AES-128-ECB) ---- + + @staticmethod + def build_session_region( + dh_pub_key: bytes, + enc_key_0: bytes, + sign_key_0: bytes, + ) -> bytes: + """key 33.6 の session_region (172B) を TFIT エミュレーションで構築する. + + session_region (172B) の構成: + [0:7] 7B CBOR プレフィックス (iPhone デバイス共通定数) + [7:135] 128B TFIT-WB-AES-128-ECB(DH_pub_key) — 8 ブロック × 16B + [135:172] 37B MGK テール — TFIT 暗号化 enc_key_0/sign_key_0 と CBOR フレーム + ※ 詳細な CBOR エンコーディングは未解明のためゼロ埋め + + TFIT エミュレーションの前提: + - NFWebCrypto.framework バイナリが + /tmp/nfwc/Payload/Argo.app/Frameworks/NFWebCrypto.framework/NFWebCrypto + に存在する必要がある。 + - バイナリが存在しない場合は 172B ゼロ埋めにフォールバック (警告表示)。 + + Args: + dh_pub_key: DH 公開鍵 (128 bytes, big-endian) + enc_key_0: Phase 0 MGK 暗号化鍵 (16 bytes) + sign_key_0: Phase 0 MGK 署名鍵 (32 bytes) + + Returns: + 172 bytes の session_region + + Raises: + ValueError: dh_pub_key が 128B でない場合 + ValueError: enc_key_0 が 16B でない場合 + ValueError: sign_key_0 が 32B でない場合 + """ + if len(dh_pub_key) != 128: + raise ValueError(f"dh_pub_key must be 128 bytes, got {len(dh_pub_key)}") + if len(enc_key_0) != 16: + raise ValueError(f"enc_key_0 must be 16 bytes, got {len(enc_key_0)}") + if len(sign_key_0) != 32: + raise ValueError(f"sign_key_0 must be 32 bytes, got {len(sign_key_0)}") + + import sys + from pathlib import Path + + BINARY_PATH = Path( + "/tmp/nfwc/Payload/Argo.app/Frameworks/NFWebCrypto.framework/NFWebCrypto" + ) + + if not BINARY_PATH.exists(): + print( + " [WARN] build_session_region: NFWebCrypto binary not found at " + f"{BINARY_PATH}. session_region はゼロ埋めにフォールバック。" + ) + return bytes(172) + + try: + # tools/emulate_tfit.py をモジュールとしてインポート + tools_dir = str(Path(__file__).resolve().parent.parent.parent / "tools") + if tools_dir not in sys.path: + sys.path.insert(0, tools_dir) + + import importlib.util + + spec = importlib.util.spec_from_file_location( + "emulate_tfit", Path(tools_dir) / "emulate_tfit.py" + ) + if spec is None or spec.loader is None: + raise ImportError("emulate_tfit.py のロードに失敗") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # type: ignore[arg-type] + + import lief + + binary_data = BINARY_PATH.read_bytes() + binary = lief.MachO.parse(str(BINARY_PATH)).at(0) + + emu = mod.TFITEmulator(binary_data, binary) + ks = mod.load_key_schedule(mod.MGK_TYPE_IPHONE, binary_data) + + # TFIT-WB-AES-128-ECB: 8 ブロック × 16B = 128B + tfit_dh_pub = bytearray() + for i in range(8): + block = dh_pub_key[i * 16 : (i + 1) * 16] + tfit_dh_pub.extend(emu.encrypt_block(ks, block)) + + except Exception as e: + print( + f" [WARN] build_session_region: TFIT エミュレーション失敗 ({e}). " + "session_region はゼロ埋めにフォールバック。" + ) + return bytes(172) + + # session_region[135:172] = 37B MGK テール + # 構成: CBOR フレーム + TFIT(enc_key_0) + TFIT(sign_key_0[:16]) の一部 + # ※ 正確な CBOR エンコーディングは未解明のためゼロ埋め + # TODO: MGK テールの正確な CBOR エンコーディングを解明して実装する + mgk_tail = bytes(37) + + result = ( + IOS_KEY336_SESSION_REGION_PREFIX # 7B: 定数 CBOR プレフィックス + + bytes(tfit_dh_pub) # 128B: TFIT(DH_pub_key) + + mgk_tail # 37B: MGK テール (未解明) + ) + assert len(result) == 172, f"session_region length {len(result)} != 172" + return result + # ---- key 33.6 scheme_data 構築 (Scheme 3 / appboot) ---- @staticmethod diff --git a/tools/test_appboot_e2e.py b/tools/test_appboot_e2e.py index 794bbed..1a91fb0 100644 --- a/tools/test_appboot_e2e.py +++ b/tools/test_appboot_e2e.py @@ -460,15 +460,32 @@ def run_e2e_test( print(f" DH pub_key: {dh_pub_key[:16].hex()}... ({len(dh_pub_key)}B)") # ------------------------------------------------------------------ - # session_region を DH 公開鍵からゼロパディングで仮構築 + # session_region を TFIT-WB-AES で構築 (NFWebCrypto.framework が必要) # ------------------------------------------------------------------ - # TODO: 本来は TFIT-WB-AES (tools/emulate_tfit.py の session_region 導出) が必要。 - # 現在は 172B のゼロ埋めプレースホルダーを使用。 - # このため key 33.6 の内容は正しくなく、サーバーは鍵交換を拒否する。 - # Phase 2 以降の DH 鍵合意は成立しない。 - # 正式フローでは emulate_tfit.py で DH 公開鍵を TFIT-WB-AES 暗号化し - # session_region (172B) に格納する。 - session_region_placeholder = b"\x00" * 172 + # TFIT エミュレーションで DH 公開鍵を WB-AES-128-ECB 暗号化して session_region を構築。 + # NFWebCrypto バイナリが存在しない場合は 172B ゼロ埋めにフォールバック。 + # NOTE: session_region[135:172] (37B MGK テール) は CBOR エンコーディングが未解明のため + # 現状はゼロ埋め。サーバーが鍵交換を拒否する可能性がある。 + print() + print("[Phase 1a'] session_region を TFIT エミュレーションで構築中...") + session_region = NetflixCrypto.build_session_region( + dh_pub_key=dh_pub_key, + enc_key_0=enc_key_0, + sign_key_0=sign_key_0, + ) + is_zero_filled = session_region == bytes(172) + if is_zero_filled: + print( + " session_region: ゼロ埋め (TFIT バイナリ未検出またはエミュレーション失敗)" + ) + else: + print( + f" session_region: TFIT 暗号化済み {session_region[:7].hex()}..." + f" ({len(session_region)}B)" + ) + print(f" prefix (7B): {session_region[:7].hex()}") + print(f" TFIT[0] (16B): {session_region[7:23].hex()}") + print(f" TFIT[-1] (16B): {session_region[119:135].hex()}") # セパレータは実測キャプチャから取得した既知の値を使用 # (セッション固有値のため、実際の接続では Frida キャプチャが必要) @@ -489,7 +506,7 @@ def run_e2e_test( ) key_request_bytes, k9_xor_nonce, nonce_7b = build_key_request_data( - session_region=session_region_placeholder, + session_region=session_region, s1=s1, s2=s2, s3=s3,