feat(crypto): 48B HMAC鍵の導出を実装 — SHA384(session_bind[:16])

48B HMAC-SHA384 鍵の生成チェーンが完全に解明:
  session_check = HMAC-SHA256(PSK, enc_key_0 || sign_key_0)
  session_bind  = HMAC-SHA256(session_check, nonce)
  48B_key       = SHA384(session_bind[:16])

これにより保存済みセッション鍵があれば Tweak/Frida なしで
全鍵を Python のみで導出可能になった。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
tkgstrator
2026-04-08 18:04:17 +00:00
co-authored by Claude Opus 4.6
parent 2f82b34289
commit b9930226f6
2 changed files with 53 additions and 6 deletions
+33 -5
View File
@@ -253,22 +253,50 @@ class NetflixCrypto:
return new_enc_key, new_sign_key
# ---- Phase 2: TFIT KDF (初期セッション鍵導出) ----
# ---- Phase 2: 初期セッション鍵導出 ----
@staticmethod
def derive_hmac384_key(
psk: bytes,
enc_key_0: bytes,
sign_key_0: bytes,
nonce: bytes,
) -> bytes:
"""Phase 3 KDF の session_bind から 48B HMAC-SHA384 鍵を導出.
session_check = HMAC-SHA256(PSK, enc_key_0 || sign_key_0)
session_bind = HMAC-SHA256(session_check, nonce)
48B_key = SHA384(session_bind[:16])
Args:
psk: Pre-Shared Key (16 bytes)
enc_key_0: 保存済み暗号化鍵 (16 bytes)
sign_key_0: 保存済み署名鍵 (32 bytes)
nonce: ハードコード nonce (16 bytes)
Returns:
48 バイトの HMAC-SHA384 鍵
"""
session_check = hmac_mod.new(psk, enc_key_0 + sign_key_0, hashlib.sha256).digest()
session_bind = hmac_mod.new(session_check, nonce, hashlib.sha256).digest()
return hashlib.sha384(session_bind[:16]).digest()
@staticmethod
def derive_initial_session_keys(
tfit_key: bytes,
dh_shared_secret: bytes,
) -> tuple[bytes, bytes]:
"""Phase 2 初期セッション鍵導出 (TFIT KDF).
"""Phase 2 初期セッション鍵導出.
HMAC-SHA384(TFIT_KEY_48B, 0x00 || DH_SHARED_SECRET_128B) → 48 bytes
HMAC-SHA384(48B_KEY, 0x00 || DH_SHARED_SECRET_128B) → 48 bytes
enc_key = output[0:16] (AES-128 暗号化鍵)
sign_key = output[16:48] (HMAC-SHA256 署名鍵)
48B_KEY は derive_hmac384_key() で導出するか、直接指定する。
Args:
tfit_key: TFIT ホワイトボックス AES から導出した 48 バイト鍵
dh_shared_secret: DH_compute_key から得た 128 バイト共有秘密
tfit_key: 48 バイト HMAC-SHA384
dh_shared_secret: DH_compute_key から得た 128 バイト共有秘密
Returns:
(enc_key, sign_key) — それぞれ 16 bytes, 32 bytes
+20 -1
View File
@@ -46,8 +46,27 @@ def check(label: str, got: bytes, expected: bytes) -> bool:
return ok
ENC_KEY_0 = bytes.fromhex("0817065e29e6d1c8668473af9e13b3c2")
SIGN_KEY_0 = bytes.fromhex(
"91f752f76d7ab4c2dc6e5b3ec1c0e5a16864421fe449be5457459602e298ebc1"
)
def main() -> int:
print("=== Phase 2 KDF (TFIT / HMAC-SHA384) ===")
print("=== 48B HMAC key 導出 (SHA384) ===")
# 48B 鍵を保存済みセッション鍵から導出
derived_48b = NetflixCrypto.derive_hmac384_key(
IOS_KDF_PSK, ENC_KEY_0, SIGN_KEY_0, IOS_KDF_NONCE
)
ok_48b = derived_48b == TFIT_KEY
print(f" [{'PASS' if ok_48b else 'FAIL'}] derive_hmac384_key → 48B key")
if not ok_48b:
print(f" expected: {TFIT_KEY.hex()}")
print(f" got: {derived_48b.hex()}")
print()
print("=== Phase 2 KDF (HMAC-SHA384) ===")
# 入力長の確認
assert len(TFIT_KEY) == 48, f"TFIT_KEY must be 48 bytes, got {len(TFIT_KEY)}"