Files
pm3py/pm3py/transponders/hf/iso14443a/nxp/ultralight.py
michael 2063a33a2b feat(registers): self-describing config/frame registers
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>
2026-07-14 08:36:40 -07:00

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