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:
@@ -76,6 +76,7 @@ from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader, EM4100Code
|
|||||||
from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
|
from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
|
||||||
from pm3py.transponders.bitfield import BitField, Register
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config
|
from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config
|
||||||
|
from pm3py.transponders.lf.nxp.hitag2 import Hitag2Tag, Hitag2Reader
|
||||||
from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag
|
from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -92,6 +93,7 @@ __all__ = [
|
|||||||
"EM4100Tag", "EM4100Reader", "EM4100Code",
|
"EM4100Tag", "EM4100Reader", "EM4100Code",
|
||||||
"HIDProxTag", "HIDReader",
|
"HIDProxTag", "HIDReader",
|
||||||
"T5577Tag", "T5577Reader", "T5577Config",
|
"T5577Tag", "T5577Reader", "T5577Config",
|
||||||
|
"Hitag2Tag", "Hitag2Reader",
|
||||||
"BitField", "Register",
|
"BitField", "Register",
|
||||||
"NfcType2Tag", "NfcType4Tag", "NfcType5Tag",
|
"NfcType2Tag", "NfcType4Tag", "NfcType5Tag",
|
||||||
"NxpType2Tag", "Type2Config",
|
"NxpType2Tag", "Type2Config",
|
||||||
|
|||||||
1
pm3py/transponders/lf/nxp/__init__.py
Normal file
1
pm3py/transponders/lf/nxp/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""NXP LF transponders (Hitag family)."""
|
||||||
206
pm3py/transponders/lf/nxp/hitag2.py
Normal file
206
pm3py/transponders/lf/nxp/hitag2.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
"""Hitag2 (NXP PCF7936/46/47/52) — software-sim transponder + reader driver.
|
||||||
|
|
||||||
|
Models the real Hitag2 memory (8 x 32-bit pages) and both authentication modes using the
|
||||||
|
validated cipher in :mod:`hitag2_crypto`:
|
||||||
|
|
||||||
|
- **UID request** — the reader's 5-bit START_AUTH; the tag answers with its serial (page 0).
|
||||||
|
- **Password mode** — the reader presents the 32-bit RWD password (page 1); the tag answers
|
||||||
|
with its configuration page (page 3).
|
||||||
|
- **Crypto mode** — the reader sends a 64-bit ``nR || aR`` challenge; the tag reruns the
|
||||||
|
cipher, verifies aR, and thereafter encrypts the link (transcrypt).
|
||||||
|
|
||||||
|
The authentication is bit-faithful to real tags (START_AUTH length, nR/aR, the aR check and
|
||||||
|
the transcrypt convention all match the Proxmark firmware). Post-auth read/write *command*
|
||||||
|
framing is a self-consistent software protocol between :class:`Hitag2Tag` and
|
||||||
|
:class:`Hitag2Reader` (page data responses are transcrypted exactly like the real link).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bitarray import bitarray
|
||||||
|
|
||||||
|
from pm3py.sim.frame import RFFrame
|
||||||
|
from ..base import TagLF, ReaderLF, Modulation
|
||||||
|
from .hitag2_crypto import Hitag2Cipher
|
||||||
|
|
||||||
|
# 5-bit HITAG2_START_AUTH (0b11000) and the software-sim read/write markers
|
||||||
|
_START_AUTH = bitarray("11000")
|
||||||
|
READ_CMD = 0x30
|
||||||
|
WRITE_CMD = 0xA0
|
||||||
|
|
||||||
|
#: NXP transport / I.C. Wiener "MIKRON" default key
|
||||||
|
DEFAULT_KEY = bytes.fromhex("4F4E4D494B52")
|
||||||
|
|
||||||
|
|
||||||
|
def _to_int(value: "int | bytes", nbytes: int) -> int:
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value & ((1 << (8 * nbytes)) - 1)
|
||||||
|
b = bytes(value)
|
||||||
|
if len(b) != nbytes:
|
||||||
|
raise ValueError(f"expected {nbytes} bytes, got {len(b)}")
|
||||||
|
return int.from_bytes(b, "big")
|
||||||
|
|
||||||
|
|
||||||
|
class Hitag2Tag(TagLF):
|
||||||
|
"""A simulated Hitag2 transponder.
|
||||||
|
|
||||||
|
``uid`` (4 bytes) is the serial. ``key`` (6 bytes) is the 48-bit crypto key; its first four
|
||||||
|
bytes double as the page-1 RWD password for password-mode auth. ``config`` is the page-3
|
||||||
|
configuration byte, ``password`` the 3-byte tag password (page 3 bytes 1-3), and ``data`` an
|
||||||
|
optional list of 4-byte user pages (pages 4-7).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, uid: "int | bytes" = b"\x00\x00\x00\x00", key: "int | bytes" = DEFAULT_KEY,
|
||||||
|
config: int = 0x06, password: bytes = b"\x00\x00\x00",
|
||||||
|
data: "list[bytes] | None" = None):
|
||||||
|
super().__init__(modulation=Modulation.ASK)
|
||||||
|
self._uid = _to_int(uid, 4)
|
||||||
|
self._key = _to_int(key, 6)
|
||||||
|
self._pages = [bytearray(4) for _ in range(8)]
|
||||||
|
self._pages[0][:] = self._uid.to_bytes(4, "big")
|
||||||
|
kb = self._key.to_bytes(6, "big")
|
||||||
|
self._pages[1][:] = kb[0:4] # page 1: key low / RWD password
|
||||||
|
self._pages[2][0:2] = kb[4:6] # page 2: key high
|
||||||
|
self._pages[3][0] = config & 0xFF
|
||||||
|
self._pages[3][1:4] = bytes(password[:3]).ljust(3, b"\x00")
|
||||||
|
for i, page in enumerate(data or []):
|
||||||
|
if 4 + i < 8:
|
||||||
|
self._pages[4 + i][:] = bytes(page).ljust(4, b"\x00")[:4]
|
||||||
|
self._authenticated = False
|
||||||
|
self._cipher: Hitag2Cipher | None = None
|
||||||
|
self._pending_write: int | None = None
|
||||||
|
|
||||||
|
async def power_on(self) -> None:
|
||||||
|
await super().power_on()
|
||||||
|
self._reset_session()
|
||||||
|
|
||||||
|
def _reset_session(self) -> None:
|
||||||
|
self._authenticated = False
|
||||||
|
self._cipher = None
|
||||||
|
self._pending_write = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pages(self) -> list[bytearray]:
|
||||||
|
return self._pages
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uid(self) -> bytes:
|
||||||
|
return self._uid.to_bytes(4, "big")
|
||||||
|
|
||||||
|
async def handle_frame(self, frame: RFFrame) -> RFFrame | None:
|
||||||
|
if not self._powered:
|
||||||
|
return None
|
||||||
|
n = frame.bit_count
|
||||||
|
if n == 5: # START_AUTH -> UID (clear)
|
||||||
|
if frame.bits[:5] != _START_AUTH:
|
||||||
|
return None
|
||||||
|
self._reset_session()
|
||||||
|
return RFFrame.from_bytes(bytes(self._pages[0]))
|
||||||
|
if n == 64: # crypto challenge nR||aR
|
||||||
|
return self._crypto_auth(frame)
|
||||||
|
if n == 16: # read/write command
|
||||||
|
return self._command(frame)
|
||||||
|
if n == 32: # write data or RWD password
|
||||||
|
return self._data_or_password(frame)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _crypto_auth(self, frame: RFFrame) -> RFFrame | None:
|
||||||
|
d = frame.data
|
||||||
|
nR = int.from_bytes(d[0:4], "big")
|
||||||
|
aR = int.from_bytes(d[4:8], "big")
|
||||||
|
cipher = Hitag2Cipher(self._key, self._uid, nR)
|
||||||
|
if cipher.bits(32) != aR:
|
||||||
|
return None # bad key: tag stays silent
|
||||||
|
self._cipher = cipher
|
||||||
|
self._authenticated = True
|
||||||
|
return RFFrame.from_bytes(cipher.transcrypt(bytes(self._pages[3])))
|
||||||
|
|
||||||
|
def _command(self, frame: RFFrame) -> RFFrame | None:
|
||||||
|
cmd, page = frame.data[0], frame.data[1]
|
||||||
|
if page >= 8 or not self._authenticated:
|
||||||
|
return None
|
||||||
|
if cmd == READ_CMD:
|
||||||
|
payload = bytes(self._pages[page])
|
||||||
|
if self._cipher is not None:
|
||||||
|
payload = self._cipher.transcrypt(payload)
|
||||||
|
return RFFrame.from_bytes(payload)
|
||||||
|
if cmd == WRITE_CMD:
|
||||||
|
if page == 0: # UID is read-only
|
||||||
|
return None
|
||||||
|
self._pending_write = page
|
||||||
|
return RFFrame.from_bytes(bytes(frame.data)) # ack by echo
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _data_or_password(self, frame: RFFrame) -> RFFrame | None:
|
||||||
|
d = frame.data
|
||||||
|
if self._pending_write is not None:
|
||||||
|
page, self._pending_write = self._pending_write, None
|
||||||
|
if self._cipher is not None:
|
||||||
|
d = self._cipher.transcrypt(d)
|
||||||
|
self._pages[page][:] = d
|
||||||
|
return None
|
||||||
|
if d == bytes(self._pages[1]): # password-mode auth
|
||||||
|
self._authenticated = True
|
||||||
|
return RFFrame.from_bytes(bytes(self._pages[3]))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Hitag2Reader(ReaderLF):
|
||||||
|
"""Drives a :class:`Hitag2Tag` over a medium: request UID, authenticate (password or
|
||||||
|
crypto), then read/write pages. ``key`` is the 48-bit crypto key; ``nonce`` is the reader
|
||||||
|
challenge nR used for crypto-mode auth."""
|
||||||
|
|
||||||
|
def __init__(self, medium=None, key: "int | bytes" = DEFAULT_KEY, nonce: int = 0x11223344):
|
||||||
|
super().__init__(medium)
|
||||||
|
self._key = _to_int(key, 6)
|
||||||
|
self._nonce = nonce & 0xFFFFFFFF
|
||||||
|
self._uid: int | None = None
|
||||||
|
self._cipher: Hitag2Cipher | None = None
|
||||||
|
|
||||||
|
async def _exchange(self, frame: RFFrame) -> RFFrame | None:
|
||||||
|
await self._medium.transmit_reader(frame)
|
||||||
|
return await self._medium.receive_reader()
|
||||||
|
|
||||||
|
async def request_uid(self) -> bytes | None:
|
||||||
|
resp = await self._exchange(RFFrame(bits=bitarray(_START_AUTH), bit_count=5))
|
||||||
|
if resp is None:
|
||||||
|
return None
|
||||||
|
self._uid = int.from_bytes(resp.data, "big")
|
||||||
|
self._cipher = None
|
||||||
|
return resp.data
|
||||||
|
|
||||||
|
async def crypto_auth(self) -> bytes | None:
|
||||||
|
"""Full crypto-mode authentication. Returns the decrypted config page (page 3), or
|
||||||
|
``None`` if the key was wrong."""
|
||||||
|
if self._uid is None and await self.request_uid() is None:
|
||||||
|
return None
|
||||||
|
cipher = Hitag2Cipher(self._key, self._uid, self._nonce)
|
||||||
|
aR = cipher.bits(32)
|
||||||
|
frame = RFFrame.from_bytes(self._nonce.to_bytes(4, "big") + aR.to_bytes(4, "big"))
|
||||||
|
resp = await self._exchange(frame)
|
||||||
|
if resp is None:
|
||||||
|
self._cipher = None
|
||||||
|
return None
|
||||||
|
self._cipher = cipher
|
||||||
|
return cipher.transcrypt(resp.data)
|
||||||
|
|
||||||
|
async def password_auth(self, password: bytes) -> bytes | None:
|
||||||
|
"""Password-mode authentication. Returns the config page (page 3), or ``None``."""
|
||||||
|
if self._uid is None and await self.request_uid() is None:
|
||||||
|
return None
|
||||||
|
resp = await self._exchange(RFFrame.from_bytes(bytes(password)))
|
||||||
|
return None if resp is None else resp.data
|
||||||
|
|
||||||
|
async def read_page(self, page: int) -> bytes | None:
|
||||||
|
resp = await self._exchange(RFFrame.from_bytes(bytes([READ_CMD, page & 0xFF])))
|
||||||
|
if resp is None:
|
||||||
|
return None
|
||||||
|
return self._cipher.transcrypt(resp.data) if self._cipher is not None else resp.data
|
||||||
|
|
||||||
|
async def write_page(self, page: int, data: bytes) -> bool:
|
||||||
|
ack = await self._exchange(RFFrame.from_bytes(bytes([WRITE_CMD, page & 0xFF])))
|
||||||
|
if ack is None:
|
||||||
|
return False
|
||||||
|
payload = self._cipher.transcrypt(data) if self._cipher is not None else bytes(data)
|
||||||
|
await self._medium.transmit_reader(RFFrame.from_bytes(payload))
|
||||||
|
await self._medium.receive_reader()
|
||||||
|
return True
|
||||||
92
pm3py/transponders/lf/nxp/hitag2_crypto.py
Normal file
92
pm3py/transponders/lf/nxp/hitag2_crypto.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""Hitag2 48-bit stream cipher — pure-Python port of the Proxmark3 reference implementation
|
||||||
|
(firmware/tools/hitag2crack/hitag2_gen_nRaR.py, by Aram Verstegen), the same cipher the fork's
|
||||||
|
crack tools use.
|
||||||
|
|
||||||
|
Validated against the classic "MIKRON" golden vector (see the tests): with key 0x4F4E4D494B52,
|
||||||
|
UID 0x49435769 and reader nonce 0x656E4572 the first 32 keystream bits are 0x28DC8031, so the
|
||||||
|
authenticator aR = ~0x28DC8031 = 0xD7237FCE.
|
||||||
|
|
||||||
|
Integers are plain: key is 48-bit, UID and nonce are 32-bit; keystream is emitted MSB-first.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
_MASK48 = (1 << 48) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def _i4(x: int, a: int, b: int, c: int, d: int) -> int:
|
||||||
|
return (((x >> a) & 1) * 8) + (((x >> b) & 1) * 4) + (((x >> c) & 1) * 2) + ((x >> d) & 1)
|
||||||
|
|
||||||
|
|
||||||
|
def f20(state: int) -> int:
|
||||||
|
"""The nonlinear filter: one keystream bit from the 48-bit state."""
|
||||||
|
s0 = (0x3C65 >> _i4(state, 2, 3, 5, 6)) & 1
|
||||||
|
s1 = (0x0EE5 >> _i4(state, 8, 12, 14, 15)) & 1
|
||||||
|
s2 = (0x0EE5 >> _i4(state, 17, 21, 23, 26)) & 1
|
||||||
|
s3 = (0x0EE5 >> _i4(state, 28, 29, 31, 33)) & 1
|
||||||
|
s4 = (0x3C65 >> _i4(state, 34, 43, 44, 46)) & 1
|
||||||
|
return (0xDD3929B >> ((s0 * 16) + (s1 * 8) + (s2 * 4) + (s3 * 2) + s4)) & 1
|
||||||
|
|
||||||
|
|
||||||
|
def hitag2_init(key: int, uid: int, nonce: int) -> int:
|
||||||
|
"""Initialise the cipher from the 48-bit key, 32-bit UID and 32-bit reader nonce."""
|
||||||
|
state = 0
|
||||||
|
for i in range(32, 48):
|
||||||
|
state = (state << 1) | ((key >> i) & 1)
|
||||||
|
for i in range(0, 32):
|
||||||
|
state = (state << 1) | ((uid >> i) & 1)
|
||||||
|
for i in range(0, 32):
|
||||||
|
nonce_bit = f20(state) ^ ((nonce >> (31 - i)) & 1)
|
||||||
|
state = (state >> 1) | (((nonce_bit ^ (key >> (31 - i))) & 1) << 47)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def _lfsr_feedback(state: int) -> int:
|
||||||
|
return (((state >> 0) ^ (state >> 2) ^ (state >> 3) ^ (state >> 6) ^ (state >> 7)
|
||||||
|
^ (state >> 8) ^ (state >> 16) ^ (state >> 22) ^ (state >> 23) ^ (state >> 26)
|
||||||
|
^ (state >> 30) ^ (state >> 41) ^ (state >> 42) ^ (state >> 43) ^ (state >> 46)
|
||||||
|
^ (state >> 47)) & 1)
|
||||||
|
|
||||||
|
|
||||||
|
def lfsr(state: int) -> int:
|
||||||
|
"""Advance the LFSR one step."""
|
||||||
|
return (state >> 1) + (_lfsr_feedback(state) << 47)
|
||||||
|
|
||||||
|
|
||||||
|
def keystream(state: int, length: int = 48) -> tuple[int, int]:
|
||||||
|
"""Emit ``length`` keystream bits (MSB first) and return ``(bits, new_state)``.
|
||||||
|
|
||||||
|
This is the "authenticator" convention (matches the firmware's ``cipher_authenticate``,
|
||||||
|
which compares against the *complement* of the raw keystream). The raw keystream used to
|
||||||
|
encrypt/decrypt link data is the bitwise inverse — see :meth:`Hitag2Cipher.transcrypt`."""
|
||||||
|
c = 0
|
||||||
|
for _ in range(length):
|
||||||
|
c = (c << 1) | f20(state)
|
||||||
|
state = lfsr(state)
|
||||||
|
return c, state
|
||||||
|
|
||||||
|
|
||||||
|
def authenticator(key: int, uid: int, nonce: int) -> int:
|
||||||
|
"""The 32-bit authenticator aR the reader sends and the tag verifies (first 32 bits of the
|
||||||
|
authenticator-convention keystream). MIKRON: aR = 0xD7237FCE."""
|
||||||
|
ks, _ = keystream(hitag2_init(key, uid, nonce), 32)
|
||||||
|
return ks
|
||||||
|
|
||||||
|
|
||||||
|
class Hitag2Cipher:
|
||||||
|
"""A running Hitag2 cipher session (key + UID + reader nonce), used by both the tag and the
|
||||||
|
reader models. ``bits()`` pulls authenticator-convention keystream (for aR); ``transcrypt()``
|
||||||
|
encrypts/decrypts link data with the raw (inverse) keystream, exactly like the firmware."""
|
||||||
|
|
||||||
|
def __init__(self, key: int, uid: int, nonce: int):
|
||||||
|
self.state = hitag2_init(key, uid, nonce)
|
||||||
|
|
||||||
|
def bits(self, n: int) -> int:
|
||||||
|
c, self.state = keystream(self.state, n)
|
||||||
|
return c
|
||||||
|
|
||||||
|
def transcrypt(self, data: bytes) -> bytes:
|
||||||
|
out = bytearray()
|
||||||
|
for b in data:
|
||||||
|
ks_byte, self.state = keystream(self.state, 8)
|
||||||
|
out.append(b ^ (~ks_byte & 0xFF)) # raw keystream = ~authenticator keystream
|
||||||
|
return bytes(out)
|
||||||
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