feat(hitag): Hitag 2 software-sim transponder + reader (Tier 1)
First piece of Hitag support. Adds the Hitag2 48-bit stream cipher and a software-sim transponder/reader over SoftwareMedium (parity with the other LF tags). - hitag2_crypto.py: pure-Python port of the Proxmark reference cipher (firmware tools/hitag2crack/hitag2_gen_nRaR.py). Validated bit-for-bit against the classic MIKRON golden vector (keystream 0xD7237FCE, full 16-byte sequence). Hitag2Cipher wraps a running session (bits() for the authenticator, transcrypt() for link encryption). - hitag2.py: Hitag2Tag (8 x 32-bit pages, UID/key/config/password) and Hitag2Reader. Faithful UID request (5-bit START_AUTH), password-mode auth (page-1 RWD password), and crypto-mode auth (64-bit nR||aR + aR verify + transcrypt). Wrong key is rejected; UID page is read-only. Software-sim command framing between our own tag/reader; the crypto and auth handshake are bit-faithful to real tags. 12 tests. Hitag S / Hitag u, the reader client commands, and sniff/trace decode follow in subsequent commits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
109
tests/test_hitag2.py
Normal file
109
tests/test_hitag2.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Hitag2 cipher + software-sim transponder/reader. Hardware-free."""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from pm3py.sim import SoftwareMedium, Hitag2Tag, Hitag2Reader
|
||||
from pm3py.transponders.lf.nxp.hitag2 import DEFAULT_KEY
|
||||
from pm3py.transponders.lf.nxp.hitag2_crypto import (
|
||||
hitag2_init, keystream, authenticator, Hitag2Cipher,
|
||||
)
|
||||
|
||||
UID = b"\x11\x22\x33\x44"
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
# --- The MIKRON golden vector pins the cipher to the Proxmark reference (hitag2_gen_nRaR.py) ---
|
||||
class TestHitag2Cipher:
|
||||
def test_mikron_keystream(self):
|
||||
ks, _ = keystream(hitag2_init(0x4F4E4D494B52, 0x49435769, 0x656E4572), 32)
|
||||
assert ks == 0xD7237FCE
|
||||
|
||||
def test_mikron_authenticator(self):
|
||||
assert authenticator(0x4F4E4D494B52, 0x49435769, 0x656E4572) == 0xD7237FCE
|
||||
|
||||
def test_mikron_16_bytes(self):
|
||||
ks, _ = keystream(hitag2_init(0x4F4E4D494B52, 0x49435769, 0x656E4572), 128)
|
||||
assert ks.to_bytes(16, "big").hex().upper() == "D7237FCE8CD037A95749C1E648008AB6"
|
||||
|
||||
def test_transcrypt_roundtrip(self):
|
||||
a = Hitag2Cipher(0x4F4E4D494B52, 0x49435769, 0x656E4572)
|
||||
b = Hitag2Cipher(0x4F4E4D494B52, 0x49435769, 0x656E4572)
|
||||
a.bits(32) # advance both past aR, identically
|
||||
b.bits(32)
|
||||
data = b"\xDE\xAD\xBE\xEF"
|
||||
enc = a.transcrypt(data)
|
||||
assert enc != data
|
||||
assert b.transcrypt(enc) == data # b decrypts what a encrypted
|
||||
|
||||
|
||||
async def _setup(**tag_kw):
|
||||
m = SoftwareMedium()
|
||||
tag = Hitag2Tag(uid=UID, key=DEFAULT_KEY,
|
||||
data=[b"AAAA", b"BBBB", b"CCCC", b"DDDD"], **tag_kw)
|
||||
await m.attach(tag)
|
||||
return m, tag, Hitag2Reader(m, key=DEFAULT_KEY)
|
||||
|
||||
|
||||
class TestHitag2Sim:
|
||||
def test_request_uid(self):
|
||||
async def go():
|
||||
_, _, r = await _setup()
|
||||
return await r.request_uid()
|
||||
assert run(go()) == UID
|
||||
|
||||
def test_crypto_auth_success(self):
|
||||
async def go():
|
||||
_, _, r = await _setup(config=0x06)
|
||||
return await r.crypto_auth()
|
||||
page3 = run(go())
|
||||
assert page3 is not None
|
||||
assert page3[0] == 0x06 # decrypted config byte
|
||||
|
||||
def test_crypto_auth_wrong_key(self):
|
||||
async def go():
|
||||
m, _, _ = await _setup()
|
||||
bad = Hitag2Reader(m, key=bytes(6))
|
||||
return await bad.crypto_auth()
|
||||
assert run(go()) is None
|
||||
|
||||
def test_crypto_read_page(self):
|
||||
async def go():
|
||||
_, _, r = await _setup()
|
||||
await r.crypto_auth()
|
||||
return await r.read_page(4)
|
||||
assert run(go()) == b"AAAA" # transcrypt lockstep decrypts correctly
|
||||
|
||||
def test_crypto_write_then_read(self):
|
||||
async def go():
|
||||
_, _, r = await _setup()
|
||||
await r.crypto_auth()
|
||||
ok = await r.write_page(5, b"WXYZ")
|
||||
return ok, await r.read_page(5)
|
||||
ok, val = run(go())
|
||||
assert ok is True and val == b"WXYZ"
|
||||
|
||||
def test_read_requires_auth(self):
|
||||
async def go():
|
||||
_, _, r = await _setup()
|
||||
await r.request_uid() # no auth
|
||||
return await r.read_page(4)
|
||||
assert run(go()) is None
|
||||
|
||||
def test_password_auth(self):
|
||||
async def go():
|
||||
_, _, r = await _setup(config=0x06)
|
||||
return await r.password_auth(DEFAULT_KEY[0:4]) # page 1 = key[0:4]
|
||||
page3 = run(go())
|
||||
assert page3 is not None and page3[0] == 0x06
|
||||
|
||||
def test_uid_is_read_only(self):
|
||||
async def go():
|
||||
_, tag, r = await _setup()
|
||||
await r.crypto_auth()
|
||||
await r.write_page(0, b"\xFF\xFF\xFF\xFF") # must be rejected
|
||||
return tag.uid
|
||||
assert run(go()) == UID
|
||||
Reference in New Issue
Block a user