Add a BitField descriptor + Register base and per-IC config/frame
registers so opaque words decode themselves and build correctly:
- T5577Config LF block-0 config word, mode-dependent data rate
- EM4100Code 64-bit frame + all parities (EM4102/EM4200 too)
- Type2Config shared NXP CFG0/CFG1 base (auth0/prot/cfglck/authlim)
- Ntag21xConfig + mirror / MIRROR_PAGE / NFC counter fields
- UltralightEV1Config EV1 CFG0/CFG1
- UltralightCConfig UL-C AUTH0 + AUTH1 protect scope
- MifareAccessConditions per-block C1/C2/C3 -> English, inverted-copy
validation, build() that fills the inverted copy
Fields are documented attributes, so `<tab>` completes them in the shell
(bpython/ipython show each field's doc as the tooltip) and `repr` prints
the decode table. Bound to tags via `tag.config` / `tag.access(sector)`
/ `tag.code`. Bit layouts cross-checked against the firmware submodule
(cmdlft55xx, mifare4.c) and the NXP/EM datasheets. 40 new tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
302 lines
12 KiB
Python
302 lines
12 KiB
Python
"""MIFARE Ultralight family — original, Ultralight C, Ultralight EV1.
|
|
|
|
Sourced from the NXP datasheets:
|
|
- MF0ICU2 = MIFARE Ultralight C (Rev. 3.5) — 3DES AUTHENTICATE, 16-bit counter.
|
|
- MF0ULX1 = MIFARE Ultralight EV1 (Rev. 3.3) — MF0UL11 / MF0UL21.
|
|
The original MIFARE Ultralight (MF0ICU1) datasheet was not supplied; MifareUltralight
|
|
below models its well-established behaviour (16 pages, READ/WRITE/COMPAT_WRITE, OTP).
|
|
|
|
All three share the Type 2 command core in ``type2.NxpType2Tag`` (READ, WRITE,
|
|
COMPATIBILITY_WRITE, OTP/lock OR-on-write). Product-specific pieces are added here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from pm3py.sim.frame import RFFrame
|
|
from pm3py.transponders.bitfield import BitField, Register
|
|
from .type2 import (
|
|
NxpType2Tag, Type2Config, _ack, _nak, NAK_ARG, NAK_AUTH,
|
|
READ_CNT, INCR_CNT, CHECK_TEARING, READ_CMD, WRITE_CMD,
|
|
)
|
|
|
|
AUTHENTICATE = 0x1A # Ultralight C 3DES authenticate
|
|
AUTH_CONT = 0xAF # continuation byte (part 2)
|
|
|
|
|
|
class UltralightEV1Config(Type2Config):
|
|
"""Ultralight EV1 CFG0/CFG1 — the shared :class:`Type2Config` fields (auth0, prot, cfglck,
|
|
authlim, strg_mod_en). EV1 has no ASCII mirror or NFC counter, so nothing is added."""
|
|
|
|
_label = "Ultralight EV1 config (CFG0/CFG1)"
|
|
|
|
|
|
class UltralightCConfig(Register):
|
|
"""Ultralight C protection config: AUTH0 (page 2Ah byte 0) + the AUTH1 protection bit
|
|
(page 2Bh byte 0), packed as ``auth0 << 8 | auth1``. Protection from AUTH0 onward is
|
|
enforced by 3DES AUTHENTICATE rather than a password."""
|
|
|
|
_width_bits = 16
|
|
_label = "Ultralight C auth config"
|
|
|
|
auth0 = BitField(15, 8, doc="first page (>= 3) protected; 0x30+ ⇒ protection disabled")
|
|
auth1_prot = BitField(0, values={0: "read+write", 1: "write-only"},
|
|
doc="protection scope from AUTH0 onward")
|
|
|
|
# NXP factory default Ultralight C 3DES key (K1||K2, 16 bytes)
|
|
UL_C_DEFAULT_KEY = bytes.fromhex("49454D4B41455242214E4143554F5946")
|
|
|
|
|
|
def _rotl1(b: bytes) -> bytes:
|
|
"""Rotate an 8-byte block left by one byte."""
|
|
return b[1:] + b[:1]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Original MIFARE Ultralight (MF0ICU1)
|
|
# --------------------------------------------------------------------------
|
|
|
|
class MifareUltralight(NxpType2Tag):
|
|
"""Original MIFARE Ultralight (MF0ICU1) — 64 bytes, 16 pages.
|
|
|
|
Pages 0-1 UID, page 2 internal + static lock, page 3 OTP, pages 4-15 user.
|
|
Commands: READ (0x30), WRITE (0xA2), COMPATIBILITY_WRITE (0xA0). No
|
|
GET_VERSION / FAST_READ / PWD_AUTH — those are EV1 features.
|
|
"""
|
|
_version_bytes = None
|
|
_has_fast_read = False
|
|
_has_read_sig = False
|
|
_has_pwd_auth = False
|
|
_has_cc = False
|
|
_ndef_cc = b"\xE1\x10\x06\x00" # 48 bytes user
|
|
|
|
def __init__(self, uid: bytes | None = None, ndef_message: bytes = b""):
|
|
if uid is None:
|
|
uid = b"\x04" + os.urandom(6)
|
|
super().__init__(uid=uid, ndef_message=ndef_message, total_pages=16,
|
|
atqa=b"\x44\x00", sak=0x00)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# MIFARE Ultralight C (MF0ICU2)
|
|
# --------------------------------------------------------------------------
|
|
|
|
class MifareUltralightC(NxpType2Tag):
|
|
"""MIFARE Ultralight C (MF0ICU2) — 192 bytes, 48 pages, 3DES auth.
|
|
|
|
Layout: page 3 OTP, pages 04h-27h user, page 28h lock bytes 2/3,
|
|
page 29h 16-bit counter, page 2Ah AUTH0, page 2Bh AUTH1, pages 2Ch-2Fh
|
|
16-byte 3DES key. Protection from AUTH0 onward is gated by a successful
|
|
3DES AUTHENTICATE (0x1A) rather than PWD_AUTH.
|
|
"""
|
|
_version_bytes = None
|
|
_has_fast_read = False
|
|
_has_read_sig = False
|
|
_has_pwd_auth = False
|
|
_has_cc = False
|
|
_ndef_cc = b"\xE1\x10\x12\x00" # 144 bytes user
|
|
|
|
AUTH0_PAGE = 0x2A
|
|
AUTH1_PAGE = 0x2B
|
|
KEY_PAGE = 0x2C # 4 pages: 2C..2F
|
|
COUNTER_PAGE = 0x29
|
|
|
|
def __init__(self, uid: bytes | None = None, ndef_message: bytes = b"",
|
|
key: bytes = UL_C_DEFAULT_KEY, auth0: int = 0x30,
|
|
auth1_read_protect: bool = True):
|
|
if uid is None:
|
|
uid = b"\x04" + os.urandom(6)
|
|
self._auth_state = "NONE" # NONE, WAIT_RESP, AUTHENTICATED
|
|
self._rnd_b = b""
|
|
self._resp_iv = b""
|
|
super().__init__(uid=uid, ndef_message=ndef_message, total_pages=48,
|
|
atqa=b"\x44\x00", sak=0x00)
|
|
# Install key + AUTH0/AUTH1 into their pages.
|
|
for i in range(4):
|
|
self._set_page(self.KEY_PAGE + i, key[i * 4:i * 4 + 4])
|
|
self._set_page(self.AUTH0_PAGE, bytes([auth0, 0, 0, 0]))
|
|
self._set_page(self.AUTH1_PAGE, bytes([0x00 if auth1_read_protect else 0x01, 0, 0, 0]))
|
|
self._counter = 0
|
|
|
|
@property
|
|
def key(self) -> bytes:
|
|
return b"".join(self._get_page(self.KEY_PAGE + i) for i in range(4))
|
|
|
|
@property
|
|
def auth0(self) -> int:
|
|
return self._get_page(self.AUTH0_PAGE)[0]
|
|
|
|
@property
|
|
def _auth1_read_protected(self) -> bool:
|
|
return (self._get_page(self.AUTH1_PAGE)[0] & 0x01) == 0x00
|
|
|
|
@property
|
|
def config(self) -> UltralightCConfig:
|
|
"""AUTH0 + AUTH1 protection decoded as a self-describing register. Assign one back
|
|
(``tag.config = cfg``) to program the AUTH0 / AUTH1 pages."""
|
|
return UltralightCConfig(
|
|
(self._get_page(self.AUTH0_PAGE)[0] << 8) | self._get_page(self.AUTH1_PAGE)[0]
|
|
)
|
|
|
|
@config.setter
|
|
def config(self, value: "UltralightCConfig | int") -> None:
|
|
v = int(value)
|
|
self._set_page(self.AUTH0_PAGE, bytes([(v >> 8) & 0xFF, 0, 0, 0]))
|
|
self._set_page(self.AUTH1_PAGE, bytes([v & 0x01, 0, 0, 0]))
|
|
|
|
async def power_on(self) -> None:
|
|
await super().power_on()
|
|
self._auth_state = "NONE"
|
|
|
|
# UL-C protection is gated by 3DES auth, not PWD_AUTH.
|
|
def _read_protected(self, page: int) -> bool:
|
|
return (self._auth1_read_protected and self._auth_state != "AUTHENTICATED"
|
|
and page >= self.auth0 and self.auth0 <= self._total_pages)
|
|
|
|
def _write_protected(self, page: int) -> bool:
|
|
return (self._auth_state != "AUTHENTICATED"
|
|
and page >= self.auth0 and self.auth0 <= self._total_pages)
|
|
|
|
def _is_config_page(self, page: int) -> bool:
|
|
# The 3DES key never reads back.
|
|
return self.KEY_PAGE <= page < self.KEY_PAGE + 4
|
|
|
|
def _handle_application(self, frame: RFFrame) -> RFFrame | None:
|
|
if not frame.data:
|
|
return None
|
|
# Second phase of AUTHENTICATE (reader sends 0xAF + 16 bytes).
|
|
if self._auth_state == "WAIT_RESP" and frame.data[0] == AUTH_CONT:
|
|
return self._handle_auth_part2(frame)
|
|
if frame.data[0] == AUTHENTICATE:
|
|
return self._handle_auth_part1(frame)
|
|
if frame.data[0] == WRITE_CMD and len(frame.data) >= 2 and frame.data[1] == self.COUNTER_PAGE:
|
|
return self._handle_counter_write(frame)
|
|
return super()._handle_application(frame)
|
|
|
|
# ----- 16-bit one-way counter (page 0x29) -----
|
|
|
|
def _handle_counter_write(self, frame: RFFrame) -> RFFrame | None:
|
|
"""WRITE to the counter page increments the 16-bit one-way counter."""
|
|
if len(frame.data) < 6:
|
|
return _nak(NAK_ARG)
|
|
incr = frame.data[2] | (frame.data[3] << 8)
|
|
self._counter = min(0xFFFF, self._counter + incr)
|
|
self._set_page(self.COUNTER_PAGE,
|
|
bytes([self._counter & 0xFF, (self._counter >> 8) & 0xFF, 0, 0]))
|
|
return _ack()
|
|
|
|
# ----- 3DES AUTHENTICATE (0x1A / 0xAF) -----
|
|
|
|
def _handle_auth_part1(self, frame: RFFrame) -> RFFrame | None:
|
|
"""AUTHENTICATE part 1: tag returns ek(RndB)."""
|
|
try:
|
|
from Crypto.Cipher import DES3
|
|
except ImportError:
|
|
return _nak(NAK_AUTH)
|
|
self._rnd_b = os.urandom(8)
|
|
cipher = DES3.new(self.key, DES3.MODE_CBC, iv=bytes(8))
|
|
ek_rnd_b = cipher.encrypt(self._rnd_b)
|
|
self._resp_iv = ek_rnd_b # IV that the reader will chain from
|
|
self._auth_state = "WAIT_RESP"
|
|
return RFFrame.from_bytes(bytes([AUTH_CONT]) + ek_rnd_b)
|
|
|
|
def _handle_auth_part2(self, frame: RFFrame) -> RFFrame | None:
|
|
"""AUTHENTICATE part 2: verify reader token, return ek(RndA')."""
|
|
from Crypto.Cipher import DES3
|
|
token = bytes(frame.data[1:17])
|
|
if len(token) != 16:
|
|
self._auth_state = "NONE"
|
|
return _nak(NAK_AUTH)
|
|
cipher = DES3.new(self.key, DES3.MODE_CBC, iv=self._resp_iv)
|
|
plain = cipher.decrypt(token)
|
|
rnd_a = plain[0:8]
|
|
rnd_b_prime = plain[8:16]
|
|
if rnd_b_prime != _rotl1(self._rnd_b):
|
|
self._auth_state = "NONE"
|
|
return _nak(NAK_AUTH)
|
|
# Success: return ek(RndA'), IV chains from last received cipher block.
|
|
enc = DES3.new(self.key, DES3.MODE_CBC, iv=token[8:16])
|
|
ek_rnd_a_prime = enc.encrypt(_rotl1(rnd_a))
|
|
self._auth_state = "AUTHENTICATED"
|
|
return RFFrame.from_bytes(bytes([0x00]) + ek_rnd_a_prime)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# MIFARE Ultralight EV1 (MF0UL11 / MF0UL21)
|
|
# --------------------------------------------------------------------------
|
|
|
|
class MifareUltralightEV1(NxpType2Tag):
|
|
"""MIFARE Ultralight EV1 base — GET_VERSION, FAST_READ, PWD_AUTH, READ_SIG,
|
|
plus three independent 24-bit one-way counters (READ_CNT / INCR_CNT /
|
|
CHECK_TEARING_EVENT). Concrete ICs: MF0UL11, MF0UL21."""
|
|
|
|
_has_cc = False # page 3 is OTP; format for NDEF to add a CC
|
|
_config_cls = UltralightEV1Config
|
|
|
|
def __init__(self, uid: bytes | None = None, ndef_message: bytes = b"",
|
|
password: bytes = b"\xFF\xFF\xFF\xFF", pack: bytes = b"\x00\x00",
|
|
auth0: int = 0xFF, prot: bool = False,
|
|
signature: bytes | None = None):
|
|
if uid is None:
|
|
uid = b"\x04" + os.urandom(6)
|
|
self._counters = [0, 0, 0]
|
|
self._tearing = [0xBD, 0xBD, 0xBD] # BD = no tearing event
|
|
super().__init__(uid=uid, password=password, pack=pack, auth0=auth0,
|
|
prot=prot, signature=signature, cc=None,
|
|
ndef_message=ndef_message,
|
|
total_pages=self._total_pages_cls,
|
|
atqa=b"\x44\x00", sak=0x00)
|
|
|
|
def _handle_application(self, frame: RFFrame) -> RFFrame | None:
|
|
if frame.data:
|
|
cmd = frame.data[0]
|
|
if cmd == READ_CNT:
|
|
return self._handle_read_cnt(frame)
|
|
if cmd == INCR_CNT:
|
|
return self._handle_incr_cnt(frame)
|
|
if cmd == CHECK_TEARING:
|
|
return self._handle_check_tearing(frame)
|
|
return super()._handle_application(frame)
|
|
|
|
def _handle_read_cnt(self, frame: RFFrame) -> RFFrame | None:
|
|
if len(frame.data) < 2 or frame.data[1] > 2:
|
|
return _nak(NAK_ARG)
|
|
return RFFrame.from_bytes(self._counters[frame.data[1]].to_bytes(3, "little"))
|
|
|
|
def _handle_incr_cnt(self, frame: RFFrame) -> RFFrame | None:
|
|
if len(frame.data) < 6 or frame.data[1] > 2:
|
|
return _nak(NAK_ARG)
|
|
n = frame.data[1]
|
|
incr = frame.data[2] | (frame.data[3] << 8) | (frame.data[4] << 16)
|
|
if self._counters[n] + incr > 0xFFFFFF:
|
|
return _nak(NAK_AUTH) # counter overflow (NAK 0x4)
|
|
self._counters[n] += incr
|
|
return _ack()
|
|
|
|
def _handle_check_tearing(self, frame: RFFrame) -> RFFrame | None:
|
|
if len(frame.data) < 2 or frame.data[1] > 2:
|
|
return _nak(NAK_ARG)
|
|
return RFFrame.from_bytes(bytes([self._tearing[frame.data[1]]]))
|
|
|
|
|
|
class MF0UL11(MifareUltralightEV1):
|
|
"""MIFARE Ultralight EV1 MF0UL11 — 48 bytes user (pages 04h-0Fh)."""
|
|
_total_pages_cls = 20
|
|
_version_bytes = b"\x00\x04\x03\x01\x01\x00\x0B\x03"
|
|
_ndef_cc = b"\xE1\x10\x06\x00" # 48 bytes user
|
|
_cfg0_page = 0x10
|
|
_cfg1_page = 0x11
|
|
_pwd_page = 0x12
|
|
_pack_page = 0x13
|
|
|
|
|
|
class MF0UL21(MifareUltralightEV1):
|
|
"""MIFARE Ultralight EV1 MF0UL21 — 128 bytes user (pages 04h-23h)."""
|
|
_total_pages_cls = 41
|
|
_version_bytes = b"\x00\x04\x03\x01\x01\x00\x0E\x03"
|
|
_ndef_cc = b"\xE1\x10\x10\x00" # 128 bytes user
|
|
_cfg0_page = 0x25
|
|
_cfg1_page = 0x26
|
|
_pwd_page = 0x27
|
|
_pack_page = 0x28
|