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>
This commit is contained in:
@@ -57,6 +57,10 @@ pm3py/
|
|||||||
replay.py # Trace replay
|
replay.py # Trace replay
|
||||||
access_control/ # Wiegand, OSDP
|
access_control/ # Wiegand, OSDP
|
||||||
transponders/ # tag/transponder models (extracted from sim/)
|
transponders/ # tag/transponder models (extracted from sim/)
|
||||||
|
bitfield.py # BitField descriptor + Register base — self-describing config
|
||||||
|
# registers (T5577Config, Ntag21xConfig, UL EV1/C, MFC access
|
||||||
|
# bits). Fields are documented attrs -> shell completion + repr
|
||||||
|
# decode table. Layouts cross-checked vs the firmware submodule.
|
||||||
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
|
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
|
||||||
# nxp/type2,ntag21x,ultralight,ntag_i2c
|
# nxp/type2,ntag21x,ultralight,ntag_i2c
|
||||||
# (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus)
|
# (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus)
|
||||||
|
|||||||
@@ -20,16 +20,18 @@ from pm3py.transponders.hf.iso14443a.base import (
|
|||||||
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, State14443A,
|
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, State14443A,
|
||||||
)
|
)
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
|
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag, MifareClassicReader
|
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import (
|
||||||
|
MifareClassicTag, MifareClassicReader, MifareAccessConditions,
|
||||||
|
)
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
|
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
|
||||||
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag
|
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag
|
from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag, Type2Config
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ntag21x import (
|
from pm3py.transponders.hf.iso14443a.nxp.ntag21x import (
|
||||||
Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216,
|
Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, Ntag21xConfig,
|
||||||
)
|
)
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ultralight import (
|
from pm3py.transponders.hf.iso14443a.nxp.ultralight import (
|
||||||
MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21,
|
MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21,
|
||||||
UL_C_DEFAULT_KEY,
|
UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig,
|
||||||
)
|
)
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import NtagI2C, NT3H2111, NT3H2211
|
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import NtagI2C, NT3H2111, NT3H2211
|
||||||
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K
|
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K
|
||||||
@@ -63,9 +65,10 @@ from pm3py.transponders.hf.iso15693.ti.rf430frl import (
|
|||||||
RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H,
|
RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H,
|
||||||
)
|
)
|
||||||
from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation
|
from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation
|
||||||
from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader
|
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.lf.atmel.t5577 import T5577Tag, T5577Reader
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
|
from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config
|
||||||
from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag
|
from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -76,17 +79,19 @@ __all__ = [
|
|||||||
"Reader", "ScriptedReader", "InteractiveReader", "ReaderStep", "StepResult",
|
"Reader", "ScriptedReader", "InteractiveReader", "ReaderStep", "StepResult",
|
||||||
"Tag14443A", "Tag14443A_3", "Tag14443A_4", "Reader14443A", "State14443A",
|
"Tag14443A", "Tag14443A_3", "Tag14443A_4", "Reader14443A", "State14443A",
|
||||||
"Crypto1",
|
"Crypto1",
|
||||||
"MifareClassicTag", "MifareClassicReader",
|
"MifareClassicTag", "MifareClassicReader", "MifareAccessConditions",
|
||||||
"Tag15693", "Reader15693", "State15693",
|
"Tag15693", "Reader15693", "State15693",
|
||||||
"TagLF", "ReaderLF", "Modulation",
|
"TagLF", "ReaderLF", "Modulation",
|
||||||
"EM4100Tag", "EM4100Reader",
|
"EM4100Tag", "EM4100Reader", "EM4100Code",
|
||||||
"HIDProxTag", "HIDReader",
|
"HIDProxTag", "HIDReader",
|
||||||
"T5577Tag", "T5577Reader",
|
"T5577Tag", "T5577Reader", "T5577Config",
|
||||||
|
"BitField", "Register",
|
||||||
"NfcType2Tag", "NfcType4Tag", "NfcType5Tag",
|
"NfcType2Tag", "NfcType4Tag", "NfcType5Tag",
|
||||||
"NxpType2Tag",
|
"NxpType2Tag", "Type2Config",
|
||||||
"Ntag21x", "NTAG210", "NTAG212", "NTAG213", "NTAG215", "NTAG216",
|
"Ntag21x", "NTAG210", "NTAG212", "NTAG213", "NTAG215", "NTAG216", "Ntag21xConfig",
|
||||||
"MifareUltralight", "MifareUltralightC", "MifareUltralightEV1",
|
"MifareUltralight", "MifareUltralightC", "MifareUltralightEV1",
|
||||||
"MF0UL11", "MF0UL21", "UL_C_DEFAULT_KEY",
|
"MF0UL11", "MF0UL21", "UL_C_DEFAULT_KEY",
|
||||||
|
"UltralightEV1Config", "UltralightCConfig",
|
||||||
"NtagI2C", "NT3H2111", "NT3H2211",
|
"NtagI2C", "NT3H2111", "NT3H2211",
|
||||||
"ST25TN", "ST25TN512", "ST25TN01K",
|
"ST25TN", "ST25TN512", "ST25TN01K",
|
||||||
"ST25TA", "ST25TA512B", "ST25TA02KB", "ST25TA16K", "ST25TA64K",
|
"ST25TA", "ST25TA512B", "ST25TA02KB", "ST25TA16K", "ST25TA64K",
|
||||||
|
|||||||
166
pm3py/transponders/bitfield.py
Normal file
166
pm3py/transponders/bitfield.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""Self-describing bit-field registers.
|
||||||
|
|
||||||
|
A ``Register`` is a fixed-width integer — a config word, a status byte — whose bits carry
|
||||||
|
named, documented meaning. Declare the layout once with ``BitField`` descriptors and you get,
|
||||||
|
for free:
|
||||||
|
|
||||||
|
* read / edit by field name ``cfg.modulation``, ``cfg.max_block = 3``
|
||||||
|
* ``<tab>`` completion of the field names in the shell, each field's one-line ``doc`` as the
|
||||||
|
autocomplete tooltip (bpython/ipython read the descriptor docstring)
|
||||||
|
* a decoded table when you print it — the "always-on hover"
|
||||||
|
|
||||||
|
The point is that a bare ``0x00148040`` tells you nothing, while the register spells it out.
|
||||||
|
Nobody hand-decodes these words from a datasheet if the object does it for them.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# House-style ANSI (mirrors pm3py/trace/format.py); TTY-gated so tests/pipes stay plain text.
|
||||||
|
_DIM = "\033[2m"
|
||||||
|
_CYAN = "\033[36m"
|
||||||
|
_RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def _color(code: str, text: str) -> str:
|
||||||
|
if not code or not sys.stdout.isatty():
|
||||||
|
return text
|
||||||
|
return f"{code}{text}{_RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
class BitField:
|
||||||
|
"""A named slice of a register's bits.
|
||||||
|
|
||||||
|
``BitField(hi, lo)`` covers bits ``[lo, hi]`` inclusive (``lo`` defaults to ``hi`` for a
|
||||||
|
single bit). ``doc`` is the one-line meaning shown in completion tooltips and the decode
|
||||||
|
table. ``values`` is an optional ``{raw_code: label}`` map — when given, reads return the
|
||||||
|
label and writes accept either the label or the raw code (single-bit fields with no map
|
||||||
|
read/write as ``bool``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hi: int, lo: int | None = None, *, doc: str = "",
|
||||||
|
values: dict[int, Any] | None = None):
|
||||||
|
if lo is None:
|
||||||
|
lo = hi
|
||||||
|
self.hi, self.lo = max(hi, lo), min(hi, lo)
|
||||||
|
self.width = self.hi - self.lo + 1
|
||||||
|
self.doc = doc
|
||||||
|
self.values = values
|
||||||
|
self._reverse = {v: k for k, v in values.items()} if values else {}
|
||||||
|
self.is_bool = self.width == 1 and values is None
|
||||||
|
self.name = "?"
|
||||||
|
self.__doc__ = doc # so `cfg.field?` / bpython introspection shows the meaning
|
||||||
|
|
||||||
|
def __set_name__(self, owner: type, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _mask(self) -> int:
|
||||||
|
return ((1 << self.width) - 1) << self.lo
|
||||||
|
|
||||||
|
def raw(self, reg: "Register") -> int:
|
||||||
|
"""The raw code stored in these bits, before any ``values`` mapping."""
|
||||||
|
return (reg.value >> self.lo) & ((1 << self.width) - 1)
|
||||||
|
|
||||||
|
def __get__(self, reg: "Register | None", owner: type | None = None):
|
||||||
|
if reg is None:
|
||||||
|
return self
|
||||||
|
code = self.raw(reg)
|
||||||
|
if self.values is not None:
|
||||||
|
return self.values.get(code, code)
|
||||||
|
return bool(code) if self.is_bool else code
|
||||||
|
|
||||||
|
def __set__(self, reg: "Register", value: Any) -> None:
|
||||||
|
code = self._encode(value)
|
||||||
|
if code < 0 or code >> self.width:
|
||||||
|
raise ValueError(f"{self.name}: {value!r} does not fit in {self.width} bit(s)")
|
||||||
|
reg.value = (reg.value & ~self._mask) | (code << self.lo)
|
||||||
|
|
||||||
|
def _encode(self, value: Any) -> int:
|
||||||
|
if self._reverse and value in self._reverse:
|
||||||
|
return self._reverse[value]
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return int(value)
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
raise ValueError(f"{self.name}: cannot set to {value!r}")
|
||||||
|
|
||||||
|
def display(self, reg: "Register") -> str:
|
||||||
|
if self.is_bool:
|
||||||
|
return "on" if self.raw(reg) else "off"
|
||||||
|
return str(self.__get__(reg))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bits(self) -> str:
|
||||||
|
return f"[{self.hi}]" if self.width == 1 else f"[{self.hi}:{self.lo}]"
|
||||||
|
|
||||||
|
|
||||||
|
class Register:
|
||||||
|
"""Base for a fixed-width register. Subclass, set ``_width_bits`` / ``_label``, and declare
|
||||||
|
``BitField`` class attributes. Construct from a raw value (``Reg(0x00148040)``) or empty."""
|
||||||
|
|
||||||
|
_width_bits = 32
|
||||||
|
_label = "register"
|
||||||
|
|
||||||
|
def __init__(self, value: "int | Register" = 0):
|
||||||
|
self.value = int(value) & ((1 << self._width_bits) - 1)
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __index__(self) -> int:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return self.value.to_bytes(self._width_bits // 8, "big")
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if isinstance(other, Register):
|
||||||
|
return type(self) is type(other) and self.value == other.value
|
||||||
|
if isinstance(other, int):
|
||||||
|
return self.value == other
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((type(self), self.value))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fields(cls) -> list[tuple[str, BitField]]:
|
||||||
|
"""All declared ``BitField``s, highest bit first (base classes included)."""
|
||||||
|
seen: dict[str, BitField] = {}
|
||||||
|
for klass in cls.__mro__:
|
||||||
|
for name, obj in vars(klass).items():
|
||||||
|
if isinstance(obj, BitField) and name not in seen:
|
||||||
|
seen[name] = obj
|
||||||
|
return sorted(seen.items(), key=lambda kv: kv[1].lo, reverse=True)
|
||||||
|
|
||||||
|
def _rows(self) -> list[dict]:
|
||||||
|
"""Decoded rows (dicts: name/value/bits/doc/lo), high bit first. Subclasses may add
|
||||||
|
computed rows via ``_extra_rows`` (e.g. a mode-dependent field)."""
|
||||||
|
rows = [
|
||||||
|
{"name": name, "value": bf.display(self), "bits": bf.bits, "doc": bf.doc, "lo": bf.lo}
|
||||||
|
for name, bf in self.fields()
|
||||||
|
]
|
||||||
|
rows.extend(self._extra_rows())
|
||||||
|
rows.sort(key=lambda r: r["lo"], reverse=True)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def _extra_rows(self) -> list[dict]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def decode(self) -> dict[str, Any]:
|
||||||
|
"""Plain ``{field: value}`` mapping — no ANSI, handy for tests and programmatic use."""
|
||||||
|
return {r["name"]: r["value"] for r in self._rows()}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
rows = self._rows()
|
||||||
|
hexw = self._width_bits // 4
|
||||||
|
head = f"{self._label} = 0x{self.value:0{hexw}X}"
|
||||||
|
name_w = max((len(r["name"]) for r in rows), default=0)
|
||||||
|
val_w = max((len(r["value"]) for r in rows), default=0)
|
||||||
|
lines = [head]
|
||||||
|
for r in rows:
|
||||||
|
line = f" {r['name']:<{name_w}} {r['value']:<{val_w}} {_color(_DIM, r['bits'])}"
|
||||||
|
lines.append(line.rstrip())
|
||||||
|
return _color(_CYAN, head) + "\n" + "\n".join(lines[1:])
|
||||||
@@ -5,6 +5,7 @@ import struct
|
|||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
from pm3py.sim.medium import Medium
|
from pm3py.sim.medium import Medium
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
from ..base import Tag14443A_3, Reader14443A, _compute_bcc
|
from ..base import Tag14443A_3, Reader14443A, _compute_bcc
|
||||||
from .crypto1 import Crypto1
|
from .crypto1 import Crypto1
|
||||||
|
|
||||||
@@ -18,6 +19,118 @@ NACK = 0x00
|
|||||||
|
|
||||||
BLOCK_SIZE = 16
|
BLOCK_SIZE = 16
|
||||||
|
|
||||||
|
# Per-block access-condition (C1 C2 C3) meanings. Data-block table matches Proxmark3
|
||||||
|
# mifare4.c; the trailer table is written to the NXP MF1S50 datasheet (r=read, w=write,
|
||||||
|
# A/B = which key, AB = either key, - = never).
|
||||||
|
_MFAC_DATA = {
|
||||||
|
0: "r=AB w=AB incr=AB dec/tr/rst=AB (transport)",
|
||||||
|
1: "r=AB dec/tr/rst=AB (value block)",
|
||||||
|
2: "r=AB (read-only)",
|
||||||
|
3: "r=B w=B",
|
||||||
|
4: "r=AB w=B",
|
||||||
|
5: "r=B (read-only, key B)",
|
||||||
|
6: "r=AB w=B incr=B dec/tr/rst=AB (value block)",
|
||||||
|
7: "no access",
|
||||||
|
}
|
||||||
|
_MFAC_TRAILER = {
|
||||||
|
0: "KeyA w=A; AC r=A; KeyB r=A w=A (KeyB is readable data)",
|
||||||
|
1: "KeyA w=A; AC r=A w=A; KeyB r=A w=A (transport/default)",
|
||||||
|
2: "KeyA w=-; AC r=A; KeyB r=A w=-",
|
||||||
|
3: "KeyA w=B; AC r=AB w=B; KeyB w=B",
|
||||||
|
4: "KeyA w=B; AC r=AB; KeyB w=B",
|
||||||
|
5: "AC r=AB w=B (keys locked)",
|
||||||
|
6: "KeyA w=-; AC r=AB (keys locked)",
|
||||||
|
7: "AC r=AB (all keys locked)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MifareAccessConditions(Register):
|
||||||
|
"""MIFARE Classic sector-trailer access bits — trailer bytes 6-9 (the three access bytes
|
||||||
|
plus the general-purpose byte), decoded as a 32-bit word.
|
||||||
|
|
||||||
|
Each of the sector's four blocks (three data blocks + the trailer) has a 3-bit access
|
||||||
|
condition ``C1 C2 C3``. The three access bytes store every condition *twice* — once true,
|
||||||
|
once inverted — so one flipped bit permanently bricks the sector. This spells out each
|
||||||
|
block in plain English, flags an inconsistent inverted copy (``.valid``), and, when you
|
||||||
|
:meth:`build` one, computes the inverted copy for you so it is always self-consistent::
|
||||||
|
|
||||||
|
MifareAccessConditions(0xFF078069) # decode the factory-default trailer
|
||||||
|
ac = MifareAccessConditions.build(trailer=3) # KeyB-controlled trailer
|
||||||
|
bytes(ac) # -> the 4 trailer bytes 6..9 to write
|
||||||
|
"""
|
||||||
|
|
||||||
|
_width_bits = 32
|
||||||
|
_label = "MIFARE access bits (trailer 6-9)"
|
||||||
|
|
||||||
|
gpb = BitField(7, 0, doc="general-purpose byte — free user data (e.g. the MAD GPB)")
|
||||||
|
|
||||||
|
_BLOCKS = ("block0", "block1", "block2", "trailer")
|
||||||
|
|
||||||
|
def _cond(self, blockn: int) -> int:
|
||||||
|
"""The 3-bit access condition (C1<<2 | C2<<1 | C3) for one block of the sector."""
|
||||||
|
v = self.value
|
||||||
|
c1 = (v >> (20 + blockn)) & 1 # C1 = byte7 high nibble
|
||||||
|
c2 = (v >> (8 + blockn)) & 1 # C2 = byte8 low nibble
|
||||||
|
c3 = (v >> (12 + blockn)) & 1 # C3 = byte8 high nibble
|
||||||
|
return (c1 << 2) | (c2 << 1) | c3
|
||||||
|
|
||||||
|
@property
|
||||||
|
def block0(self) -> int:
|
||||||
|
return self._cond(0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def block1(self) -> int:
|
||||||
|
return self._cond(1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def block2(self) -> int:
|
||||||
|
return self._cond(2)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trailer(self) -> int:
|
||||||
|
return self._cond(3)
|
||||||
|
|
||||||
|
def describe(self, blockn: int) -> str:
|
||||||
|
table = _MFAC_TRAILER if blockn == 3 else _MFAC_DATA
|
||||||
|
return table.get(self._cond(blockn), "?")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid(self) -> bool:
|
||||||
|
"""True iff the redundant inverted copy is the nibble-complement of the true copy."""
|
||||||
|
v = self.value
|
||||||
|
return (((v >> 24) & 0x0F) == (((v >> 20) & 0x0F) ^ 0x0F) and # ~C1 vs C1
|
||||||
|
((v >> 28) & 0x0F) == (((v >> 8) & 0x0F) ^ 0x0F) and # ~C2 vs C2
|
||||||
|
((v >> 16) & 0x0F) == (((v >> 12) & 0x0F) ^ 0x0F)) # ~C3 vs C3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build(cls, block0: int = 0, block1: int = 0, block2: int = 0,
|
||||||
|
trailer: int = 1, gpb: int = 0x00) -> "MifareAccessConditions":
|
||||||
|
"""Compose access bytes from the four per-block conditions (0..7), filling in the
|
||||||
|
inverted copy so the result always passes ``.valid``. ``trailer`` defaults to 1
|
||||||
|
(the factory/transport trailer). ``gpb`` is the general-purpose byte."""
|
||||||
|
c1 = c2 = c3 = 0
|
||||||
|
for n, cond in enumerate((block0, block1, block2, trailer)):
|
||||||
|
if not 0 <= cond <= 7:
|
||||||
|
raise ValueError(f"block {n} access condition must be 0..7, got {cond}")
|
||||||
|
c1 |= ((cond >> 2) & 1) << n
|
||||||
|
c2 |= ((cond >> 1) & 1) << n
|
||||||
|
c3 |= (cond & 1) << n
|
||||||
|
byte6 = ((c2 ^ 0x0F) << 4) | (c1 ^ 0x0F)
|
||||||
|
byte7 = (c1 << 4) | (c3 ^ 0x0F)
|
||||||
|
byte8 = (c3 << 4) | c2
|
||||||
|
return cls((byte6 << 24) | (byte7 << 16) | (byte8 << 8) | (gpb & 0xFF))
|
||||||
|
|
||||||
|
def _rows(self) -> list[dict]:
|
||||||
|
rows = [
|
||||||
|
{"name": name, "value": f"{self._cond(n)} {self.describe(n)}", "bits": "", "doc": ""}
|
||||||
|
for n, name in enumerate(self._BLOCKS)
|
||||||
|
]
|
||||||
|
rows.append({"name": "gpb", "value": f"0x{self.gpb:02X}", "bits": "[7:0]", "doc": ""})
|
||||||
|
if not self.valid:
|
||||||
|
rows.append({"name": "(!)", "value": "inverted copy inconsistent — INVALID",
|
||||||
|
"bits": "", "doc": ""})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
class MifareClassicTag(Tag14443A_3):
|
class MifareClassicTag(Tag14443A_3):
|
||||||
"""MIFARE Classic 1K/4K transponder with Crypto-1 authentication."""
|
"""MIFARE Classic 1K/4K transponder with Crypto-1 authentication."""
|
||||||
@@ -63,6 +176,12 @@ class MifareClassicTag(Tag14443A_3):
|
|||||||
else:
|
else:
|
||||||
return 128 + (sector - 32) * 16 + 15
|
return 128 + (sector - 32) * 16 + 15
|
||||||
|
|
||||||
|
def access(self, sector: int) -> MifareAccessConditions:
|
||||||
|
"""Decode the sector's trailer access bits (trailer bytes 6-9) as a self-describing
|
||||||
|
:class:`MifareAccessConditions` register."""
|
||||||
|
trailer = self.read_block_raw(self._sector_trailer_block(sector))
|
||||||
|
return MifareAccessConditions(int.from_bytes(trailer[6:10], "big"))
|
||||||
|
|
||||||
def read_block_raw(self, block: int) -> bytes:
|
def read_block_raw(self, block: int) -> bytes:
|
||||||
"""Direct read of block data (bypassing auth)."""
|
"""Direct read of block data (bypassing auth)."""
|
||||||
offset = block * BLOCK_SIZE
|
offset = block * BLOCK_SIZE
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ Feature summary:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
from .type2 import NxpType2Tag, READ_CNT, FAST_READ, READ_CMD, _nak, NAK_ARG
|
from pm3py.transponders.bitfield import BitField
|
||||||
|
from .type2 import NxpType2Tag, Type2Config, READ_CNT, FAST_READ, READ_CMD, _nak, NAK_ARG
|
||||||
|
|
||||||
# ACCESS byte bits (CFG1 byte 0)
|
# ACCESS byte bits (CFG1 byte 0)
|
||||||
ACCESS_PROT = 0x80
|
ACCESS_PROT = 0x80
|
||||||
@@ -23,11 +24,26 @@ ACCESS_NFC_CNT_EN = 0x10
|
|||||||
ACCESS_NFC_CNT_PWD_PROT = 0x08
|
ACCESS_NFC_CNT_PWD_PROT = 0x08
|
||||||
|
|
||||||
|
|
||||||
|
class Ntag21xConfig(Type2Config):
|
||||||
|
"""NTAG21x CFG0/CFG1 config word (see :class:`Type2Config`), adding the NTAG-only ASCII
|
||||||
|
mirror and NFC-counter fields. Decode ``tag.config`` or build one and assign it back."""
|
||||||
|
|
||||||
|
_label = "NTAG21x config (CFG0/CFG1)"
|
||||||
|
|
||||||
|
mirror_conf = BitField(63, 62, doc="ASCII mirror mode",
|
||||||
|
values={0: "off", 1: "UID", 2: "NFC counter", 3: "UID+counter"})
|
||||||
|
mirror_byte = BitField(61, 60, doc="byte offset within mirror_page where the mirror starts")
|
||||||
|
mirror_page = BitField(47, 40, doc="page where ASCII mirroring begins (0 = disabled)")
|
||||||
|
nfc_cnt_en = BitField(28, doc="enable the 24-bit NFC read counter (213/215/216)")
|
||||||
|
nfc_cnt_pwd_prot = BitField(27, doc="NFC counter readable only after PWD_AUTH")
|
||||||
|
|
||||||
|
|
||||||
class Ntag21x(NxpType2Tag):
|
class Ntag21x(NxpType2Tag):
|
||||||
"""Base for the NTAG21x family. Concrete ICs set the class attributes."""
|
"""Base for the NTAG21x family. Concrete ICs set the class attributes."""
|
||||||
|
|
||||||
_has_nfc_counter: bool = False # True for 213/215/216
|
_has_nfc_counter: bool = False # True for 213/215/216
|
||||||
_cc_default: bytes = b"\xE1\x10\x00\x00"
|
_cc_default: bytes = b"\xE1\x10\x00\x00"
|
||||||
|
_config_cls = Ntag21xConfig
|
||||||
|
|
||||||
def __init__(self, uid: bytes | None = None, ndef_message: bytes = b"",
|
def __init__(self, uid: bytes | None = None, ndef_message: bytes = b"",
|
||||||
password: bytes = b"\xFF\xFF\xFF\xFF",
|
password: bytes = b"\xFF\xFF\xFF\xFF",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
from pm3py.trace.decode_iso14a import decode_14443a_type2
|
from pm3py.trace.decode_iso14a import decode_14443a_type2
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
from ..ndef import NfcType2Tag, PAGE_SIZE, READ_CMD, WRITE_CMD
|
from ..ndef import NfcType2Tag, PAGE_SIZE, READ_CMD, WRITE_CMD
|
||||||
|
|
||||||
# ---- Command codes (NFC Forum Type 2 / Ultralight / NTAG) ----
|
# ---- Command codes (NFC Forum Type 2 / Ultralight / NTAG) ----
|
||||||
@@ -42,6 +43,25 @@ def _nak(code: int = NAK_ARG) -> RFFrame:
|
|||||||
return RFFrame.from_bytes(bytes([code]))
|
return RFFrame.from_bytes(bytes([code]))
|
||||||
|
|
||||||
|
|
||||||
|
class Type2Config(Register):
|
||||||
|
"""The two NXP Type 2 configuration pages (CFG0 + CFG1) decoded as one 64-bit word:
|
||||||
|
the 4 bytes of CFG0 (MSB) followed by the 4 bytes of CFG1.
|
||||||
|
|
||||||
|
Common to Ultralight EV1 and NTAG21x. Only the fields every part shares live here —
|
||||||
|
NTAG adds its mirror / NFC-counter fields in :class:`Ntag21xConfig`. Bits are numbered on
|
||||||
|
the concatenated word, so CFG0 byte 0 is bits 63:56 and CFG1 byte 0 (ACCESS) is bits 31:24.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_width_bits = 64
|
||||||
|
_label = "Type 2 config (CFG0/CFG1)"
|
||||||
|
|
||||||
|
strg_mod_en = BitField(58, doc="strong load modulation enable (CFG0 MIRROR bit 2)")
|
||||||
|
auth0 = BitField(39, 32, doc="first page needing PWD_AUTH; >= last page ⇒ protection off")
|
||||||
|
prot = BitField(31, doc="0 = write protect from AUTH0; 1 = read+write protect")
|
||||||
|
cfglck = BitField(30, doc="lock CFG0/CFG1 permanently — irreversible")
|
||||||
|
authlim = BitField(26, 24, doc="failed-PWD_AUTH attempts before lockout (0 = unlimited)")
|
||||||
|
|
||||||
|
|
||||||
class NxpType2Tag(NfcType2Tag):
|
class NxpType2Tag(NfcType2Tag):
|
||||||
"""NXP Type 2 platform: Ultralight EV1 / NTAG21x / NTAG I2C command core.
|
"""NXP Type 2 platform: Ultralight EV1 / NTAG21x / NTAG I2C command core.
|
||||||
|
|
||||||
@@ -65,6 +85,26 @@ class NxpType2Tag(NfcType2Tag):
|
|||||||
_pwd_page: int | None = None # 4-byte password (reads back as 00)
|
_pwd_page: int | None = None # 4-byte password (reads back as 00)
|
||||||
_pack_page: int | None = None # 2-byte PACK + RFUI (reads back as 00)
|
_pack_page: int | None = None # 2-byte PACK + RFUI (reads back as 00)
|
||||||
|
|
||||||
|
#: register class decoding the CFG0/CFG1 pages (subclasses with config pages set this)
|
||||||
|
_config_cls: type[Register] | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> Register:
|
||||||
|
"""CFG0/CFG1 decoded as a self-describing register (see the part's ``_config_cls``).
|
||||||
|
Assign one back (``tag.config = cfg``) to program both config pages."""
|
||||||
|
if self._config_cls is None or self._cfg0_page is None or self._cfg1_page is None:
|
||||||
|
raise AttributeError(f"{type(self).__name__} has no CFG0/CFG1 config register")
|
||||||
|
raw = self._get_page(self._cfg0_page) + self._get_page(self._cfg1_page)
|
||||||
|
return self._config_cls(int.from_bytes(raw, "big"))
|
||||||
|
|
||||||
|
@config.setter
|
||||||
|
def config(self, value: "Register | int") -> None:
|
||||||
|
if self._cfg0_page is None or self._cfg1_page is None:
|
||||||
|
raise AttributeError(f"{type(self).__name__} has no CFG0/CFG1 config register")
|
||||||
|
raw = int(value).to_bytes(8, "big")
|
||||||
|
self._set_page(self._cfg0_page, raw[0:4])
|
||||||
|
self._set_page(self._cfg1_page, raw[4:8])
|
||||||
|
|
||||||
def __init__(self, uid: bytes, *,
|
def __init__(self, uid: bytes, *,
|
||||||
password: bytes = b"\xFF\xFF\xFF\xFF",
|
password: bytes = b"\xFF\xFF\xFF\xFF",
|
||||||
pack: bytes = b"\x00\x00",
|
pack: bytes = b"\x00\x00",
|
||||||
|
|||||||
@@ -14,14 +14,35 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
from .type2 import (
|
from .type2 import (
|
||||||
NxpType2Tag, _ack, _nak, NAK_ARG, NAK_AUTH,
|
NxpType2Tag, Type2Config, _ack, _nak, NAK_ARG, NAK_AUTH,
|
||||||
READ_CNT, INCR_CNT, CHECK_TEARING, READ_CMD, WRITE_CMD,
|
READ_CNT, INCR_CNT, CHECK_TEARING, READ_CMD, WRITE_CMD,
|
||||||
)
|
)
|
||||||
|
|
||||||
AUTHENTICATE = 0x1A # Ultralight C 3DES authenticate
|
AUTHENTICATE = 0x1A # Ultralight C 3DES authenticate
|
||||||
AUTH_CONT = 0xAF # continuation byte (part 2)
|
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)
|
# NXP factory default Ultralight C 3DES key (K1||K2, 16 bytes)
|
||||||
UL_C_DEFAULT_KEY = bytes.fromhex("49454D4B41455242214E4143554F5946")
|
UL_C_DEFAULT_KEY = bytes.fromhex("49454D4B41455242214E4143554F5946")
|
||||||
|
|
||||||
@@ -109,6 +130,20 @@ class MifareUltralightC(NxpType2Tag):
|
|||||||
def _auth1_read_protected(self) -> bool:
|
def _auth1_read_protected(self) -> bool:
|
||||||
return (self._get_page(self.AUTH1_PAGE)[0] & 0x01) == 0x00
|
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:
|
async def power_on(self) -> None:
|
||||||
await super().power_on()
|
await super().power_on()
|
||||||
self._auth_state = "NONE"
|
self._auth_state = "NONE"
|
||||||
@@ -196,6 +231,7 @@ class MifareUltralightEV1(NxpType2Tag):
|
|||||||
CHECK_TEARING_EVENT). Concrete ICs: MF0UL11, MF0UL21."""
|
CHECK_TEARING_EVENT). Concrete ICs: MF0UL11, MF0UL21."""
|
||||||
|
|
||||||
_has_cc = False # page 3 is OTP; format for NDEF to add a CC
|
_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"",
|
def __init__(self, uid: bytes | None = None, ndef_message: bytes = b"",
|
||||||
password: bytes = b"\xFF\xFF\xFF\xFF", pack: bytes = b"\x00\x00",
|
password: bytes = b"\xFF\xFF\xFF\xFF", pack: bytes = b"\x00\x00",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
from ..base import TagLF, ReaderLF, Modulation
|
from ..base import TagLF, ReaderLF, Modulation
|
||||||
from ..em.em4100 import EM4100Tag
|
from ..em.em4100 import EM4100Tag
|
||||||
|
|
||||||
@@ -16,6 +17,108 @@ CMD_PWD_WRITE = 0x04
|
|||||||
# T5577 block 0 config bits for common modes
|
# T5577 block 0 config bits for common modes
|
||||||
CONFIG_EM4100 = 0x00148040 # ASK/Manchester, RF/64, 64 bits, maxblk=2
|
CONFIG_EM4100 = 0x00148040 # ASK/Manchester, RF/64, 64 bits, maxblk=2
|
||||||
|
|
||||||
|
# modulation codes (config bits 16:12) — datasheet / ATA5577C
|
||||||
|
_MODULATION = {
|
||||||
|
0x00: "direct", 0x01: "PSK1", 0x02: "PSK2", 0x03: "PSK3",
|
||||||
|
0x04: "FSK1", 0x05: "FSK2", 0x06: "FSK1a", 0x07: "FSK2a",
|
||||||
|
0x08: "ASK", 0x10: "biphase", 0x18: "biphase'",
|
||||||
|
}
|
||||||
|
# PSK carrier frequency (config bits 11:10), PSK modes only
|
||||||
|
_PSK_CF = {0x00: "RF/2", 0x01: "RF/4", 0x02: "RF/8", 0x03: "reserved"}
|
||||||
|
# master key nibble (config bits 31:28)
|
||||||
|
_MASTER_KEY = {0x0: "0 (normal)", 0x6: "6 (extended)", 0x9: "9 (extended)"}
|
||||||
|
# standard-mode data bit rate: 3-bit code (bits 20:18) <-> RF/n divider
|
||||||
|
_RF_BY_CODE = {0: 8, 1: 16, 2: 32, 3: 40, 4: 50, 5: 64, 6: 100, 7: 128}
|
||||||
|
_CODE_BY_RF = {v: k for k, v in _RF_BY_CODE.items()}
|
||||||
|
|
||||||
|
|
||||||
|
class T5577Config(Register):
|
||||||
|
"""T5577 / ATA5577C configuration word — page 0, block 0.
|
||||||
|
|
||||||
|
Decode a value you read, or build one to write::
|
||||||
|
|
||||||
|
T5577Config(0x00148040) # -> decoded table (EM4100/unique)
|
||||||
|
cfg = T5577Config.preset("em4100")
|
||||||
|
cfg.max_block = 3
|
||||||
|
cfg.st = True
|
||||||
|
lf.t55.writebl(0, int(cfg)) # int(cfg) is the raw word to program
|
||||||
|
|
||||||
|
Fields (high->low): master_key, data_bit_rate, x_mode, modulation, psk_cf, aor, otp,
|
||||||
|
max_block, pwd, st, fast_write, inverse, por_delay. ``data_bit_rate`` is mode-dependent
|
||||||
|
(see its docstring), so it is a computed property rather than a plain ``BitField``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_width_bits = 32
|
||||||
|
_label = "T5577 config (block0)"
|
||||||
|
|
||||||
|
master_key = BitField(31, 28, values=_MASTER_KEY,
|
||||||
|
doc="lock/extended-mode key: 0=normal, 6/9 enable extended (x-mode)")
|
||||||
|
x_mode = BitField(17, doc="extended mode: 6-bit data rate + fast-write / AOR / OTP semantics")
|
||||||
|
modulation = BitField(16, 12, values=_MODULATION, doc="modulation scheme (ASK, FSK, PSK, ...)")
|
||||||
|
psk_cf = BitField(11, 10, values=_PSK_CF, doc="PSK sub-carrier frequency (PSK modes only)")
|
||||||
|
aor = BitField(9, doc="answer-on-request: stay silent until woken by a read command")
|
||||||
|
otp = BitField(8, doc="one-time-programmable: block further writes once set (irreversible)")
|
||||||
|
max_block = BitField(7, 5, doc="number of data blocks the tag streams back (1..7)")
|
||||||
|
pwd = BitField(4, doc="password enabled: reads/writes require the block-7 password")
|
||||||
|
st = BitField(3, doc="sequence terminator: marks end of data for reader auto-detect")
|
||||||
|
fast_write = BitField(2, doc="fast downlink write timing (x-mode)")
|
||||||
|
inverse = BitField(1, doc="invert the data output")
|
||||||
|
por_delay = BitField(0, doc="power-on-reset delay (ride through brief field dropouts)")
|
||||||
|
|
||||||
|
#: known block-0 words (from the Proxmark3 config-block table) for common tag types
|
||||||
|
_PRESETS = {
|
||||||
|
"em4100": 0x00148040, # ASK/Manchester, RF/64, 2 blocks
|
||||||
|
"default": 0x000880E8, # ASK/Manchester, RF/32, ST, 7 blocks
|
||||||
|
"hid": 0x00107060, # FSK2a, RF/50, 3 blocks (HID 26-bit)
|
||||||
|
"indala": 0x00081040, # PSK1, RF/32, 2 blocks
|
||||||
|
"fdxb": 0x903F0082, # biphase, x-mode, RF/32, 4 blocks
|
||||||
|
"viking": 0x00088040, # ASK/Manchester, RF/32, 2 blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def preset(cls, name: str) -> "T5577Config":
|
||||||
|
try:
|
||||||
|
return cls(cls._PRESETS[name])
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
f"unknown preset {name!r}; known: {sorted(cls._PRESETS)}"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_bit_rate(self) -> int:
|
||||||
|
"""Data bit rate as ``RF/n`` (the carrier is divided by this before sampling).
|
||||||
|
|
||||||
|
Standard mode uses a 3-bit code -> RF/8, 16, 32, 40, 50, 64, 100 or 128. When
|
||||||
|
``x_mode`` is set the field is 6 bits and the rate is ``2*code + 2`` (any even RF/n)."""
|
||||||
|
raw6 = (self.value >> 18) & 0x3F
|
||||||
|
if self.x_mode:
|
||||||
|
return 2 * raw6 + 2
|
||||||
|
return _RF_BY_CODE.get(raw6 & 0x07, raw6 & 0x07)
|
||||||
|
|
||||||
|
@data_bit_rate.setter
|
||||||
|
def data_bit_rate(self, rf: int) -> None:
|
||||||
|
if self.x_mode:
|
||||||
|
if rf < 2 or rf % 2:
|
||||||
|
raise ValueError("x-mode data rate must be an even RF/n >= 2")
|
||||||
|
code = (rf - 2) // 2
|
||||||
|
if code >> 6:
|
||||||
|
raise ValueError(f"RF/{rf} too large for x-mode (6-bit field)")
|
||||||
|
self.value = (self.value & ~(0x3F << 18)) | (code << 18)
|
||||||
|
else:
|
||||||
|
if rf not in _CODE_BY_RF:
|
||||||
|
raise ValueError(
|
||||||
|
f"standard data rate must be RF/n with n in {sorted(_CODE_BY_RF)}; got RF/{rf}"
|
||||||
|
)
|
||||||
|
self.value = (self.value & ~(0x07 << 18)) | (_CODE_BY_RF[rf] << 18)
|
||||||
|
|
||||||
|
def _extra_rows(self) -> list[dict]:
|
||||||
|
hi = 23 if self.x_mode else 20
|
||||||
|
return [{
|
||||||
|
"name": "data_bit_rate", "value": f"RF/{self.data_bit_rate}",
|
||||||
|
"bits": f"[{hi}:18]", "lo": 18,
|
||||||
|
"doc": "carrier divider RF/n (see data_bit_rate)",
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
class T5577Tag(TagLF):
|
class T5577Tag(TagLF):
|
||||||
"""T5577 programmable LF transponder.
|
"""T5577 programmable LF transponder.
|
||||||
@@ -35,6 +138,16 @@ class T5577Tag(TagLF):
|
|||||||
def blocks(self) -> list[int]:
|
def blocks(self) -> list[int]:
|
||||||
return self._blocks
|
return self._blocks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> T5577Config:
|
||||||
|
"""Block 0 decoded as a self-describing :class:`T5577Config`. Assign one back
|
||||||
|
(``tag.config = cfg``) to program block 0."""
|
||||||
|
return T5577Config(self._blocks[0])
|
||||||
|
|
||||||
|
@config.setter
|
||||||
|
def config(self, value: "T5577Config | int") -> None:
|
||||||
|
self._blocks[0] = int(value)
|
||||||
|
|
||||||
async def handle_frame(self, frame: RFFrame) -> RFFrame | None:
|
async def handle_frame(self, frame: RFFrame) -> RFFrame | None:
|
||||||
if not self._powered or not frame.data:
|
if not self._powered or not frame.data:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
|
from pm3py.transponders.bitfield import Register
|
||||||
from ..base import TagLF, ReaderLF, Modulation
|
from ..base import TagLF, ReaderLF, Modulation
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +35,14 @@ class EM4100Tag(TagLF):
|
|||||||
"""Return 64-bit encoded data as list of 0/1 values."""
|
"""Return 64-bit encoded data as list of 0/1 values."""
|
||||||
return list(self._encoded)
|
return list(self._encoded)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code(self) -> "EM4100Code":
|
||||||
|
"""The tag's 64-bit frame decoded as a self-describing :class:`EM4100Code`."""
|
||||||
|
value = 0
|
||||||
|
for bit in self._encoded:
|
||||||
|
value = (value << 1) | bit
|
||||||
|
return EM4100Code(value)
|
||||||
|
|
||||||
def _get_response(self) -> RFFrame:
|
def _get_response(self) -> RFFrame:
|
||||||
"""Return encoded data as RFFrame."""
|
"""Return encoded data as RFFrame."""
|
||||||
from bitarray import bitarray
|
from bitarray import bitarray
|
||||||
@@ -85,6 +94,109 @@ class EM4100Tag(TagLF):
|
|||||||
return tag_id
|
return tag_id
|
||||||
|
|
||||||
|
|
||||||
|
class EM4100Code(Register):
|
||||||
|
"""The 64-bit EM4100 code, decoded — header, the ten 4-bit data rows with their even
|
||||||
|
row parities, the four column parities, and the stop bit. Covers EM4102 and the EM4200's
|
||||||
|
64-bit mode (same frame). Build from a 40-bit ID (parities computed for you) or decode a
|
||||||
|
captured frame and check it::
|
||||||
|
|
||||||
|
EM4100Code.build(0x0102030405) # -> frame with correct parities
|
||||||
|
EM4100Code(0xFF00...).valid # header + every parity + stop bit consistent?
|
||||||
|
code.tag_id # 40-bit ID (customer_id byte + card_number)
|
||||||
|
|
||||||
|
The redundant parities are the classic EM footgun — this validates all of them and, when
|
||||||
|
you build a code, gets them right so you never program a tag a reader won't accept.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_width_bits = 64
|
||||||
|
_label = "EM4100 64-bit code"
|
||||||
|
|
||||||
|
def _bits(self) -> list[int]:
|
||||||
|
"""The frame as 64 bits, MSB (first transmitted) first — matches ``_encode`` order."""
|
||||||
|
return [(self.value >> (63 - i)) & 1 for i in range(64)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def header(self) -> int:
|
||||||
|
return (self.value >> 55) & 0x1FF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def header_ok(self) -> bool:
|
||||||
|
return self.header == 0x1FF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tag_id(self) -> int:
|
||||||
|
"""The 40-bit ID carried in the data rows (parities stripped)."""
|
||||||
|
return EM4100Tag.decode_data(self._bits())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def customer_id(self) -> int:
|
||||||
|
"""First data byte — the EM 'customer'/version field."""
|
||||||
|
return (self.tag_id >> 32) & 0xFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def card_number(self) -> int:
|
||||||
|
"""Low 32 bits of the ID — the card number readers usually display."""
|
||||||
|
return self.tag_id & 0xFFFFFFFF
|
||||||
|
|
||||||
|
def _bad_rows(self) -> list[int]:
|
||||||
|
b = self._bits()
|
||||||
|
bad = []
|
||||||
|
for row in range(10):
|
||||||
|
start = 9 + row * 5
|
||||||
|
if b[start + 4] != sum(b[start:start + 4]) % 2:
|
||||||
|
bad.append(row)
|
||||||
|
return bad
|
||||||
|
|
||||||
|
def _bad_cols(self) -> list[int]:
|
||||||
|
b = self._bits()
|
||||||
|
return [c for c in range(4)
|
||||||
|
if b[59 + c] != sum(b[9 + r * 5 + c] for r in range(10)) % 2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rows_ok(self) -> bool:
|
||||||
|
return not self._bad_rows()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cols_ok(self) -> bool:
|
||||||
|
return not self._bad_cols()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stop_ok(self) -> bool:
|
||||||
|
return (self.value & 1) == 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid(self) -> bool:
|
||||||
|
return self.header_ok and self.rows_ok and self.cols_ok and self.stop_ok
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build(cls, tag_id: int) -> "EM4100Code":
|
||||||
|
"""Encode a 40-bit ID into a full, correctly-parity'd 64-bit code."""
|
||||||
|
value = 0
|
||||||
|
for bit in EM4100Tag._encode(tag_id):
|
||||||
|
value = (value << 1) | bit
|
||||||
|
return cls(value)
|
||||||
|
|
||||||
|
def _rows(self) -> list[dict]:
|
||||||
|
_bad_rows = self._bad_rows()
|
||||||
|
_bad_cols = self._bad_cols()
|
||||||
|
return [
|
||||||
|
{"name": "header", "bits": "[63:55]", "doc": "",
|
||||||
|
"value": f"0x{self.header:03X}" + ("" if self.header_ok else " BAD (expect 1FF)")},
|
||||||
|
{"name": "tag_id", "bits": "40-bit", "doc": "", "value": f"{self.tag_id:010X}"},
|
||||||
|
{"name": "customer_id", "bits": "", "doc": "", "value": f"0x{self.customer_id:02X}"},
|
||||||
|
{"name": "card_number", "bits": "", "doc": "",
|
||||||
|
"value": f"0x{self.card_number:08X} ({self.card_number})"},
|
||||||
|
{"name": "row_parity", "bits": "", "doc": "",
|
||||||
|
"value": "ok" if self.rows_ok else f"BAD rows {_bad_rows}"},
|
||||||
|
{"name": "col_parity", "bits": "[4:1]", "doc": "",
|
||||||
|
"value": "ok" if self.cols_ok else f"BAD cols {_bad_cols}"},
|
||||||
|
{"name": "stop", "bits": "[0]", "doc": "",
|
||||||
|
"value": f"{self.value & 1}" + ("" if self.stop_ok else " BAD (expect 0)")},
|
||||||
|
{"name": "valid", "bits": "", "doc": "",
|
||||||
|
"value": "yes" if self.valid else "NO — header/parity/stop error"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EM4100Reader(ReaderLF):
|
class EM4100Reader(ReaderLF):
|
||||||
"""EM4100 reader — decode Manchester ASK response."""
|
"""EM4100 reader — decode Manchester ASK response."""
|
||||||
|
|
||||||
|
|||||||
301
tests/test_bitfield.py
Normal file
301
tests/test_bitfield.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
"""Bit-field register infrastructure + the T5577 config register. Hardware-free."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
|
from pm3py.sim import (
|
||||||
|
T5577Config, T5577Tag,
|
||||||
|
Ntag21xConfig, NTAG213, MF0UL21, MifareUltralightC,
|
||||||
|
MifareAccessConditions, MifareClassicTag,
|
||||||
|
EM4100Code, EM4100Tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Demo(Register):
|
||||||
|
_width_bits = 16
|
||||||
|
_label = "demo"
|
||||||
|
top = BitField(15, 12, doc="top nibble")
|
||||||
|
flag = BitField(3, doc="a flag")
|
||||||
|
mode = BitField(1, 0, values={0: "a", 1: "b", 2: "c"}, doc="a labelled mode")
|
||||||
|
|
||||||
|
|
||||||
|
class TestBitFieldCore:
|
||||||
|
def test_read_fields(self):
|
||||||
|
d = Demo(0xF008) # top=0xF, flag(bit3)=1, mode=0
|
||||||
|
assert d.top == 0xF
|
||||||
|
assert d.flag is True
|
||||||
|
assert d.mode == "a"
|
||||||
|
|
||||||
|
def test_single_bit_is_bool(self):
|
||||||
|
assert Demo(0).flag is False
|
||||||
|
assert Demo(0x8).flag is True
|
||||||
|
|
||||||
|
def test_write_by_field_is_isolated(self):
|
||||||
|
d = Demo(0)
|
||||||
|
d.top = 0xA
|
||||||
|
assert d.value == 0xA000
|
||||||
|
d.flag = True
|
||||||
|
assert d.value == 0xA008
|
||||||
|
d.top = 0x5 # must not disturb the flag bit
|
||||||
|
assert d.value == 0x5008
|
||||||
|
|
||||||
|
def test_values_map_both_directions(self):
|
||||||
|
d = Demo(0)
|
||||||
|
d.mode = "c"
|
||||||
|
assert d.mode == "c" and d.value == 0x0002
|
||||||
|
d.mode = 1 # raw code still accepted
|
||||||
|
assert d.mode == "b"
|
||||||
|
|
||||||
|
def test_overflow_rejected(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Demo(0).top = 0x1F # 5 bits into a 4-bit field
|
||||||
|
|
||||||
|
def test_int_and_index(self):
|
||||||
|
d = Demo(0x1234)
|
||||||
|
assert int(d) == 0x1234
|
||||||
|
assert hex(d) == "0x1234"
|
||||||
|
|
||||||
|
def test_fields_ordered_high_to_low(self):
|
||||||
|
assert [n for n, _ in Demo.fields()] == ["top", "flag", "mode"]
|
||||||
|
|
||||||
|
def test_descriptor_doc_visible(self):
|
||||||
|
# the completion tooltip / `field?` help comes from the descriptor docstring
|
||||||
|
assert Demo.top.__doc__ == "top nibble"
|
||||||
|
|
||||||
|
|
||||||
|
# The three anchor constants below are lifted from the Proxmark3 config-block table
|
||||||
|
# (client/src/cmdlft55xx.h) with their documented meanings — they pin the bit layout.
|
||||||
|
class TestT5577Decode:
|
||||||
|
def test_em4100(self):
|
||||||
|
cfg = T5577Config(0x00148040)
|
||||||
|
assert cfg.modulation == "ASK"
|
||||||
|
assert cfg.data_bit_rate == 64
|
||||||
|
assert cfg.max_block == 2
|
||||||
|
assert cfg.x_mode is False
|
||||||
|
assert cfg.st is False
|
||||||
|
assert cfg.master_key == "0 (normal)"
|
||||||
|
|
||||||
|
def test_default_ask_manchester(self):
|
||||||
|
cfg = T5577Config(0x000880E8) # ASK, RF/32, STT, 7 data blocks
|
||||||
|
assert cfg.modulation == "ASK"
|
||||||
|
assert cfg.data_bit_rate == 32
|
||||||
|
assert cfg.max_block == 7
|
||||||
|
assert cfg.st is True
|
||||||
|
|
||||||
|
def test_pac_is_nrz(self):
|
||||||
|
cfg = T5577Config(0x00080080) # NRZ, RF/32, 4 data blocks
|
||||||
|
assert cfg.modulation == "direct"
|
||||||
|
assert cfg.max_block == 4
|
||||||
|
|
||||||
|
def test_hid_fsk(self):
|
||||||
|
cfg = T5577Config(0x00107060) # FSK2a, RF/50, 3 blocks
|
||||||
|
assert cfg.modulation == "FSK2a"
|
||||||
|
assert cfg.data_bit_rate == 50
|
||||||
|
assert cfg.max_block == 3
|
||||||
|
|
||||||
|
def test_xmode_data_rate(self):
|
||||||
|
cfg = T5577Config(0x903F0082) # fdx-b, x-mode
|
||||||
|
assert cfg.x_mode is True
|
||||||
|
assert cfg.modulation == "biphase"
|
||||||
|
assert cfg.data_bit_rate == 32 # 2*code+2 over the 6-bit field
|
||||||
|
|
||||||
|
|
||||||
|
class TestT5577Build:
|
||||||
|
def test_preset_roundtrip(self):
|
||||||
|
assert int(T5577Config.preset("em4100")) == 0x00148040
|
||||||
|
|
||||||
|
def test_unknown_preset(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
T5577Config.preset("nope")
|
||||||
|
|
||||||
|
def test_edit_by_field(self):
|
||||||
|
cfg = T5577Config.preset("em4100")
|
||||||
|
cfg.max_block = 3
|
||||||
|
cfg.st = True
|
||||||
|
# max_block bits 7:5 -> 3<<5 = 0x60; st bit3 -> 0x08; base had maxblk 2 (0x40)
|
||||||
|
assert cfg.max_block == 3 and cfg.st is True
|
||||||
|
assert int(cfg) == (0x00148040 & ~(0x07 << 5)) | (3 << 5) | (1 << 3)
|
||||||
|
|
||||||
|
def test_set_data_rate_standard(self):
|
||||||
|
cfg = T5577Config(0x00148040)
|
||||||
|
cfg.data_bit_rate = 32
|
||||||
|
assert cfg.data_bit_rate == 32
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
cfg.data_bit_rate = 33 # not a legal standard divider
|
||||||
|
|
||||||
|
def test_set_modulation_by_name(self):
|
||||||
|
cfg = T5577Config(0)
|
||||||
|
cfg.modulation = "FSK2"
|
||||||
|
assert cfg.modulation == "FSK2"
|
||||||
|
assert cfg.modulation != "ASK"
|
||||||
|
|
||||||
|
def test_decode_mapping_and_repr(self):
|
||||||
|
cfg = T5577Config(0x00148040)
|
||||||
|
d = cfg.decode()
|
||||||
|
assert d["modulation"] == "ASK"
|
||||||
|
assert d["data_bit_rate"] == "RF/64"
|
||||||
|
assert d["max_block"] == "2"
|
||||||
|
text = repr(cfg) # not a tty in tests -> plain text
|
||||||
|
assert "T5577 config (block0) = 0x00148040" in text
|
||||||
|
assert "modulation" in text and "ASK" in text
|
||||||
|
assert "data_bit_rate" in text and "RF/64" in text
|
||||||
|
|
||||||
|
|
||||||
|
class TestT5577TagBinding:
|
||||||
|
def test_config_reads_block0(self):
|
||||||
|
tag = T5577Tag()
|
||||||
|
tag.blocks[0] = 0x00148040
|
||||||
|
assert tag.config.modulation == "ASK"
|
||||||
|
assert tag.config.max_block == 2
|
||||||
|
|
||||||
|
def test_assign_config_programs_block0(self):
|
||||||
|
tag = T5577Tag()
|
||||||
|
cfg = T5577Config.preset("hid")
|
||||||
|
tag.config = cfg
|
||||||
|
assert tag.blocks[0] == 0x00107060
|
||||||
|
# accepts a raw int too
|
||||||
|
tag.config = 0x00148040
|
||||||
|
assert tag.config.modulation == "ASK"
|
||||||
|
|
||||||
|
|
||||||
|
class TestNtag21xConfig:
|
||||||
|
def test_fields_span_both_pages(self):
|
||||||
|
cfg = Ntag21xConfig(0)
|
||||||
|
cfg.auth0 = 0x04 # CFG0 byte 3
|
||||||
|
cfg.prot = True # CFG1 byte 0 bit 7 (ACCESS.PROT)
|
||||||
|
# CFG0 = 00 00 00 04, CFG1 = 80 00 00 00
|
||||||
|
assert bytes(cfg) == b"\x00\x00\x00\x04\x80\x00\x00\x00"
|
||||||
|
assert cfg.auth0 == 0x04 and cfg.prot is True
|
||||||
|
|
||||||
|
def test_mirror_and_counter_fields(self):
|
||||||
|
cfg = Ntag21xConfig(0)
|
||||||
|
cfg.mirror_conf = "UID+counter"
|
||||||
|
cfg.mirror_page = 0x10
|
||||||
|
cfg.nfc_cnt_en = True
|
||||||
|
cfg.authlim = 5
|
||||||
|
assert cfg.mirror_conf == "UID+counter"
|
||||||
|
assert cfg.mirror_page == 0x10
|
||||||
|
assert cfg.nfc_cnt_en is True and cfg.authlim == 5
|
||||||
|
# re-decoding the raw word must reproduce everything
|
||||||
|
assert Ntag21xConfig(int(cfg)).decode() == cfg.decode()
|
||||||
|
|
||||||
|
def test_tag_binding_roundtrip(self):
|
||||||
|
tag = NTAG213() # fresh: auth0=0xFF, prot off
|
||||||
|
assert tag.config.auth0 == 0xFF
|
||||||
|
assert tag.config.prot is False
|
||||||
|
cfg = tag.config
|
||||||
|
cfg.auth0 = 0x10
|
||||||
|
cfg.prot = True
|
||||||
|
tag.config = cfg # program both config pages
|
||||||
|
assert tag.config.auth0 == 0x10 and tag.config.prot is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestUltralightConfig:
|
||||||
|
def test_ev1_uses_shared_fields(self):
|
||||||
|
tag = MF0UL21()
|
||||||
|
cfg = tag.config
|
||||||
|
cfg.auth0 = 0x08
|
||||||
|
cfg.cfglck = True
|
||||||
|
tag.config = cfg
|
||||||
|
assert tag.config.auth0 == 0x08 and tag.config.cfglck is True
|
||||||
|
assert "NTAG21x" not in repr(cfg) # EV1 has no mirror/counter fields
|
||||||
|
|
||||||
|
def test_ulc_auth_config(self):
|
||||||
|
tag = MifareUltralightC(auth0=0x30, auth1_read_protect=True)
|
||||||
|
cfg = tag.config
|
||||||
|
assert cfg.auth0 == 0x30
|
||||||
|
assert cfg.auth1_prot == "read+write" # AUTH1 bit0 == 0
|
||||||
|
cfg.auth1_prot = "write-only"
|
||||||
|
cfg.auth0 = 0x2A
|
||||||
|
tag.config = cfg
|
||||||
|
assert tag.config.auth0 == 0x2A
|
||||||
|
assert tag.config.auth1_prot == "write-only"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMifareAccessConditions:
|
||||||
|
def test_factory_default_trailer(self):
|
||||||
|
# FF 07 80 access bytes + GPB 0x69 = the transport/default trailer
|
||||||
|
ac = MifareAccessConditions(0xFF078069)
|
||||||
|
assert ac.valid is True
|
||||||
|
assert ac.block0 == 0 and ac.block1 == 0 and ac.block2 == 0
|
||||||
|
assert ac.trailer == 1
|
||||||
|
assert ac.gpb == 0x69
|
||||||
|
|
||||||
|
def test_build_matches_default(self):
|
||||||
|
ac = MifareAccessConditions.build(0, 0, 0, trailer=1, gpb=0x69)
|
||||||
|
assert int(ac) == 0xFF078069
|
||||||
|
assert bytes(ac) == b"\xFF\x07\x80\x69" # trailer bytes 6..9
|
||||||
|
|
||||||
|
def test_build_is_always_valid(self):
|
||||||
|
for t in range(8):
|
||||||
|
for d in range(8):
|
||||||
|
assert MifareAccessConditions.build(d, d, d, trailer=t).valid
|
||||||
|
|
||||||
|
def test_all_locked(self):
|
||||||
|
ac = MifareAccessConditions.build(7, 7, 7, trailer=7)
|
||||||
|
assert ac.trailer == 7 and ac.block0 == 7
|
||||||
|
assert "no access" in ac.describe(0)
|
||||||
|
assert "locked" in ac.describe(3)
|
||||||
|
|
||||||
|
def test_inconsistent_copy_flagged(self):
|
||||||
|
ac = MifareAccessConditions(0x00000000) # C == ~C == 0, impossible
|
||||||
|
assert ac.valid is False
|
||||||
|
assert "INVALID" in repr(ac)
|
||||||
|
|
||||||
|
def test_bad_condition_rejected(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
MifareAccessConditions.build(trailer=8)
|
||||||
|
|
||||||
|
def test_describe_and_decode(self):
|
||||||
|
ac = MifareAccessConditions(0xFF078069)
|
||||||
|
assert "transport" in ac.describe(0)
|
||||||
|
assert "transport/default" in ac.describe(3)
|
||||||
|
d = ac.decode()
|
||||||
|
assert d["block0"].startswith("0")
|
||||||
|
assert d["trailer"].startswith("1")
|
||||||
|
|
||||||
|
def test_tag_access_reads_trailer(self):
|
||||||
|
tag = MifareClassicTag(uid=b"\x01\x02\x03\x04")
|
||||||
|
# write a full trailer for sector 1: KeyA(6) + FF0780 + GPB69 + KeyB(6)
|
||||||
|
trailer_blk = tag._sector_trailer_block(1)
|
||||||
|
trailer = b"\xA0\xA1\xA2\xA3\xA4\xA5" + b"\xFF\x07\x80\x69" + b"\xB0\xB1\xB2\xB3\xB4\xB5"
|
||||||
|
tag.write_block_raw(trailer_blk, trailer)
|
||||||
|
ac = tag.access(1)
|
||||||
|
assert ac.trailer == 1 and ac.gpb == 0x69 and ac.valid
|
||||||
|
|
||||||
|
|
||||||
|
class TestEM4100Code:
|
||||||
|
def test_build_roundtrip(self):
|
||||||
|
code = EM4100Code.build(0x0102030405)
|
||||||
|
assert code.tag_id == 0x0102030405
|
||||||
|
assert code.customer_id == 0x01
|
||||||
|
assert code.card_number == 0x02030405
|
||||||
|
assert code.valid
|
||||||
|
|
||||||
|
def test_build_is_always_valid(self):
|
||||||
|
for tid in (0x0, 0xFFFFFFFFFF, 0x1234567890, 0xDEADBEEF00, 0xA5A5A5A5A5):
|
||||||
|
assert EM4100Code.build(tid).valid
|
||||||
|
|
||||||
|
def test_matches_model_encoding(self):
|
||||||
|
# the register must agree bit-for-bit with the tag's own encoder
|
||||||
|
tag = EM4100Tag(0xCAFEBABE99)
|
||||||
|
assert tag.code.valid
|
||||||
|
assert tag.code.tag_id == 0xCAFEBABE99
|
||||||
|
assert int(tag.code) == int(EM4100Code.build(0xCAFEBABE99))
|
||||||
|
|
||||||
|
def test_flipped_data_bit_breaks_row_parity(self):
|
||||||
|
code = EM4100Code.build(0x0102030405)
|
||||||
|
broken = EM4100Code(int(code) ^ (1 << 40)) # flip a data bit in the rows region
|
||||||
|
assert broken.valid is False
|
||||||
|
assert broken._bad_rows() # at least one row parity fails
|
||||||
|
|
||||||
|
def test_bad_header_and_stop_detected(self):
|
||||||
|
good = EM4100Code.build(0)
|
||||||
|
assert good.valid
|
||||||
|
assert EM4100Code(int(good) | 1).valid is False # stop bit set
|
||||||
|
assert EM4100Code(int(good) & ~(0x1FF << 55)).valid is False # header zeroed
|
||||||
|
|
||||||
|
def test_repr_shows_id_and_validity(self):
|
||||||
|
text = repr(EM4100Code.build(0x0102030405))
|
||||||
|
assert "EM4100 64-bit code" in text
|
||||||
|
assert "0102030405" in text
|
||||||
|
assert "valid" in text and "yes" in text
|
||||||
Reference in New Issue
Block a user