Commit Graph

94 Commits

Author SHA1 Message Date
michael
3b4ec4541b feat(rawcli): full MIFARE Classic datasheet command set + value blocks
Completes the MIFARE Classic catalog with its datasheet wire commands (the standing rule: every
transponder exposes its full datasheet command set, like NTAG21x), each carrying its wire opcode
so it autocompletes by name and by the hex/binary opcode value, with the block memory map:

  AUTH_A 0x60 / AUTH_B 0x61   auth a sector (test whether a key works; auth is otherwise implicit)
  READ 0x30 / WRITE 0xA0      (existing)
  INCREMENT 0xC1 / DECREMENT 0xC0 / RESTORE 0xC2 / TRANSFER 0xB0   value-block ops
  PERSONALIZE_UID 0x40 / SET_MOD_TYPE 0x43   EV1 (opcode + hint; execution not yet wired)
  CHK (key finder) / HALT 0x50   (existing)

core: hf.mf.value(block, action, value, transfer_block, key, key_type) wires CMD_HF_MIFARE_VALUE
(0x0627) — payload mirrors the stock client's CmdHF14AMfValue / firmware MifareValue (key[0:6],
action[9], transferBlk[10], operand[11:15], transfer-key[27:33], nested-auth flag[33]); the op is
committed to transfer_block, or in place when None; a cross-sector transfer sets the nested-auth
flag. INCREMENT/DECREMENT/RESTORE commit in place; TRANSFER copies a value block block->dest.

No completer/memory/app changes — the machinery is data-driven off the catalog.

Hardware-verified on a MIFARE Classic 1K: identify; c1/60/b0 (and binary) surface INCREMENT/AUTH_A
/TRANSFER with the block map; AUTH_A reports the right key ok and a wrong key failed; a value
round-trip INCREMENTed 100 -> 105 with the value-block format intact (block restored after). 1370
green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:47:22 -07:00
michael
fd16fa8f90 feat(trace): live terminal output under bpython via a shared emit()
Live sniff/sim stream trace frames from a background thread. bpython replaces sys.stdout with a capture object whose write() does a greenlet switch back to the main thread — which raises off-thread; the text buffers but only repaints on the next keypress, so streamed frames 'pile up until you touch a key'. New trace/output.py emit() buffers via the repl's on_write and fires bpython's threadsafe repaint trigger; a plain write everywhere else, with a safe fallback on any internals mismatch.

Route TraceFormatter.print(), SimSession's sim-ended status, and SniffSession's state message through emit(). Flip the trace-line newline convention from leading to trailing so each emitted line self-terminates (format_sniff_line + test updated to match).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:25:16 -07:00
michael
0586e2a41a fix(sim): break the transponders<->sim circular import via lazy re-exports
Importing a transponder model first (e.g. iso15693.type5, or iso14443a via the fuzzer) triggered pm3py.sim's __init__, which eagerly re-imported that same transponder before it finished initialising — ImportError on a partially-initialised module. Masked in the full suite by import order; failed whenever a transponder was the first import.

Move sim/__init__'s ~90 transponder re-exports into sim/_models.py and load them lazily via a PEP 562 __getattr__ (public API unchanged: attribute access, from-import, __all__, dir(), and 'import *' all still work). Defer fuzzer.py's iso14443a.base import into the one method that uses it. Add tests/test_import_hygiene.py — subprocess fresh-import guards per transponder family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:23:12 -07:00
michael
4f66a93de9 feat(cli): add pm3py client — autocompleting console over the stock proxmark3 client
Wraps the standard Proxmark3 C client (so it works with any Proxmark/firmware) in a prompt_toolkit REPL: full command-tree completion sourced from the client's commands.json, option-flag completion, Tab-to-descend, a press-right help toggle, and the client's own coloured output.

Two swappable backends behind ClientDriver: in-process SWIG (_pm3) by default, a pexpect-around-the-binary fallback otherwise. Both auto-detect /dev/ttyACM* and discover the checkout via its doc/commands.json marker (walk-up + side-by-side), so no path is hardcoded.

`pm3py client --pack OUT` spins off a self-contained drop-in — a readable .py, or a .pyz with prompt_toolkit/pexpect bundled — amalgamated from the package (the tested source of truth) so it can't drift. Adds pexpect to deps; hardware-free tests in tests/test_clientcli.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:05:53 -07:00
michael
2b087a302a feat(rawcli): per-byte base intermixed entry; fix MFC hf.mf; help + strict mixing
Entry now tracks each raw byte's base, so hex and binary can share one line with a clean display:
`30` (hex) then Ctrl-/ then `00000100` (binary) shows `30 00000100` and sends 0x30 0x04. The
toggle only changes the base of the NEXT byte — it never rewrites what you've typed. Each byte
auto-seals at its base width (2 hex / 8 binary); the next digit starts a fresh byte in the current
mode; a digit invalid for its byte's base is dropped. Applies to raw byte entry only (function
args stay base-10).

- entry.type_digit / is_byte_line / reconcile_bases carry the logic (pure, unit-tested); the digit
  and backspace key bindings maintain session.byte_bases; the toggle just flips the mode.
- parser.parse_bytes/parse_line take per-byte bases (explicit 0x/0b still wins). A line mixing a
  command with raw bytes (`READ 30`) raises instead of transmitting.
- completer parses the opcode token in its own base, so `30`+binary still autocompletes the page.
- help documents all of it (per-byte base, toggle, auto-seal, base-10 args, no command/byte mix).

Also fixes a real bug found on hardware: the MFC catalog called hf.mfc (nonexistent) — it's
hf.mf. Hardware-verified on a MIFARE Classic 1K: identify, CHK(0)=FFFFFFFFFFFF, READ(0) returns
the manufacturer block, READ(4)/READ(1) the data blocks. Intermixed entry verified end-to-end in
the live prompt (30 00000100 -> 0x30 0x04). 1351 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:43:04 -07:00
michael
f04703c309 feat(rawcli): MIFARE Classic authenticated read/write/chk
Classic block ops are gated by a crypto1 auth per sector, so a bare 0x30 does nothing. Wire the
MFC catalog to hf.mfc (which does the auth + op in firmware), run-based like the T5577 commands:

  READ(<block>[, <12-hex key>][, A|B])   -> auth + read the 16-byte block  (default FFFFFFFFFFFF/A)
  WRITE(<block>, <32-hex>[, key][, A|B]) -> auth + write
  CHK(<block>[, A|B])                    -> try a default key list, report the working key

build() still exposes the wire opcode (0x30/0xA0) so hex/binary completion and the raw-byte page
map keep working; execution goes through run(). Key type A/B accepted as letters or 0/1.

Mock-tested (rdbl/wrbl/chk call args + result lines, key override, failure path). Hardware verify
needs an actual MIFARE Classic card on the antenna (current tag is the NTAG213). 1340 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:41:53 -07:00
michael
260c1adad8 feat(rawcli): MIFARE Classic block-level memory map
MFC gets the same memory-map hinting as NTAG, at the block level:

  block 0        manufacturer block — UID, BCC, SAK, ATQA, vendor data
  sector trailer Key A [0-5], access bits [6-8] + GPB [9], Key B [10-15]
  other blocks   data block (sector N)

Handles 1K (16 sectors), Mini (5 sectors) and 4K — including the eight 16-block sectors 32-39
(trailer = last block, e.g. 143 = sector 32, 255 = sector 39). landmark_pages lists block 0 +
every sector trailer (the map skeleton; data blocks are uniform), so READ( / raw 30 surface it
in the completion dropdown. Also routes "MIFARE Mini" to the MFC catalog (was falling through to
Type 2).

Tests cover the block roles, large-sector trailers, bounds, Mini routing, and the dropdown map.
Path verified end-to-end with a mocked SAK-0x08 scan (identify -> catalog -> completer). Live
hardware verify still needs an actual Classic card on the antenna. 1339 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:35:33 -07:00
michael
a5c7702398 feat(rawcli): complete + family-accurate NTAG21x / Ultralight EV1 memory map
The page hinting was coarse and partly wrong: pages 0-2 were all "UID", page 3 was always
"CC" (it's OTP on Ultralight), config fields were abbreviated, and the NTAG210/212 (no counter)
and Ultralight (no ASCII mirror) differences were ignored. Now every named region is covered
and the fields track the actual IC:

  00-01  UID / serial number
  02     static lock bytes + internal (locks pages 03-0F)
  03     Capability Container (CC)   [NTAG]   /   OTP (one-time programmable)   [Ultralight]
  04..N  user memory
  <lock> dynamic lock bytes          (present on 212/213/215/216 and MF0UL21; absent on 210/UL11)
  CFG0   AUTH0, MIRROR (mode/page/byte on NTAG only), STRG_MOD_EN
  CFG1   ACCESS: PROT, CFGLCK, [NFC_CNT_EN, NFC_CNT_PWD_PROT on 213/215/216], AUTHLIM
  PWD    32-bit password
  PACK   password acknowledge + RFUI

_LAYOUTS gains a family tag ("ntag"/"ul") and a counter flag; page_role/landmark_pages derive
page 3 (CC vs OTP) and the CFG0/CFG1 field lists from them. The generic fallback (unknown model)
now also names the static-lock and CC/OTP header pages. Raw-byte completion matches the
fixed-width byte form only (so partial "2" no longer wrongly hits page 02).

Hardware-verified on NTAG213: both READ( and raw "30 " list the full 9-region map. Tests cover
the family differences (counter bits, mirror, OTP, dynamic-lock presence) and layout/model drift
for the Ultralight parts too. 1330 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:18:05 -07:00
michael
65be53da8c fix(rawcli): all hints live in the completion dropdown, not the bottom bar
The memory-location info was being surfaced on the bottom toolbar (input_hint). It belongs in
the autocomplete dropdown — the same popup where identify/READ/… appear. Moved it there and
took it off the bottom bar entirely.

- Bottom toolbar shows only the entry-mode status now; the input_hint machinery is removed.
- Raw hex/binary entry: a matched opcode inserts the RAW BYTE (30, 00110000), labelled with
  the command name — not "READ(". You're building a raw byte string, so accepting a hint keeps
  you in raw bytes.
- New: after a page-command opcode in raw entry, the next byte gets the tag's memory map as raw
  bytes (30 04 -> "04  user memory"), matching what READ( offers. Renders in the entry base.
- Function-call page args still insert 0x-hex addresses (the chosen display).

Verified through the real interactive TUI (pty): typing "30 " pops the memory map in the
completion menu as raw bytes; the old bottom-bar hint string no longer appears anywhere. 1328
green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:09:16 -07:00
michael
a3cef180d2 fix(rawcli): memory locations live only in suggestions; base-10 call args
Two corrections:

1. Memory-location names are a SUGGESTION, never command output. Removed the region annotation
   printed after each exchange (both the function-call and raw-hex paths) along with its dead
   machinery (region_annotation, pages_for_payload, TagCommand.pages). The location now shows in
   exactly two places: the completion menu (page landmarks) and the live bottom-bar hint —
   which now also covers raw byte entry (30 04 -> "READ(page) … · page 0x04 → user memory")
   via Catalog.by_opcode.

2. Function-call arguments are base 10 by default. READ(04) crashed on int("04", 0) (Python
   rejects leading-zero decimals in base 0). parser.parse_arg_int parses a call argument as
   decimal unless it carries a 0x/0b/0o prefix, so READ(04)/READ(40) work and READ(0x28) still
   overrides to hex. Raw byte entry keeps its opposite default (hex, 0b to intermix) — unchanged.

Hardware-verified on NTAG213: READ(04) sends 30 04 with no region line in the output; the hint
shows the page role for READ(4), READ(04), READ(0x28), raw 30 04 and 30 28; completion still
lists the memory map. 1331 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:38:53 -07:00
michael
899142f23a fix(rawcli): memory annotations survive a flaky/absent GET_VERSION
The page-role annotations and completion were gated on identify resolving an *exact* model
string (NTAG216). On a flaky NG link a single lost GET_VERSION dropped identify to the generic
SAK name "MIFARE Ultralight / NTAG", which isn't in the layout table — so every region
annotation and memory-map completion silently went blank. (The happy-path checks always got a
clean identify, so this never showed in earlier verification.)

Two fixes:
- _probe_version retries GET_VERSION up to 3x, re-selecting between attempts, so a lost shot on
  a flaky link no longer costs the exact model (and its full memory map).
- memory._resolve_layout falls back to a generic Type 2 map (UID / CC / the always-user pages
  0x04-0x0F) when only the NTAG/Ultralight family is known — so READ(0)/READ(4) still annotate
  even when the exact model can't be determined (or an original Ultralight has no GET_VERSION).
  Config pages differ by model and are left unnamed rather than guessed.

Tests: retry recovers the model after two lost shots; a never-answering GET_VERSION yields the
generic name yet still annotates the universal pages; generic names resolve UID/CC/user but not
model-specific pages. 1328 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:23:38 -07:00
michael
33e713549d feat(rawcli): annotate raw hex/binary commands with datasheet memory regions too
Raw '30 04' is a READ(4), so it now earns the same region line as the function-call form —
the annotation follows the bytes, not the syntax:

  30 04       -> 04-07 → user memory
  3A 00 06    -> 00-02 → UID/serial · 03 → CC · 04-06 → user memory
  30 E3       -> E3 → CFG0 · E4 → CFG1 · E5 → PWD · E6 → PACK

catalog.pages_for_payload() reverse-maps a raw payload to the pages it touches by matching the
opcode (payload[0]) to a page/block command and reading its page argument from payload[1:].
dispatch's raw path prints region_annotation() after the exchange, mirroring _handle_call.
Works from either entry mode (it operates on the decoded bytes).

Along the way: TagCommand.opcode() centralises opcode extraction, tolerating data args (valid
hex placeholder) and two-phase (multi-frame) builds — fixing a latent bug where COMPAT_WRITE's
opcode came back as bytes/None. The completer now keys landmark suggestions on the first
parameter being a page/block/start address, so FAST_READ( gets the memory map too.

Hardware-verified on a real NTAG216 (raw 30 04 / 3A 00 06 / 30 E3, and the binary form
00110000 00000100). Tests for the reverse-map, the two-phase opcode, and the raw dispatch
annotation. 1274 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:03:37 -07:00
michael
c53c087e84 feat(rawcli): render page-argument completion in the active entry mode
The memory-map completion for a page/block argument now follows the entry mode instead of
always emitting hex:

  hex mode     READ(  -> 0x04        user memory
  binary mode  READ(  -> 0b00000100  user memory   (full 8 bits — each bit visible)

Matching narrows in the same base (a binary partial filters bit-prefix-wise), and a 0x/0b
prefix on the argument overrides the session mode. Binary matters here because for the config
pages each bit carries its own meaning (CFG0 AUTH0, CFG1 PROT/CFGLCK/AUTHLIM), so seeing the
whole pattern is the point.

Hardware-verified against a real NTAG216 in binary mode (0b00000000..0b11100110 with roles).
Tests for binary rendering, bit-prefix filtering, and prefix-override. 1271 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:29:37 -07:00
michael
b6fdb7bca4 feat(rawcli): suggest the tag's memory map when completing a page argument
Typing the page/block argument of a READ/WRITE now pops the identified tag's memory landmarks
as completions, each labelled with its datasheet role, so you pick a location by meaning:

  READ(      -> 0x00  UID / serial number
                0x03  Capability Container (CC)
                0x04  user memory
                0xE3  CFG0 — MIRROR / MIRROR_PAGE / AUTH0
                0xE5  PWD (32-bit password)  ...
  READ(0xE   -> filters to the E-page landmarks

memory.landmark_pages() derives the notable pages from the same _LAYOUTS the hint/annotation use
(NTAG21x / Ultralight EV1 landmarks; T5577 blocks 0-7). The completer matches the partial in
0x-, bare-hex, or decimal form and inserts canonical 0xNN. A comma ends the page argument, so
data args get no page suggestions; tags with no known layout (MIFARE Classic) offer nothing.

Hardware-verified: completer run against a real identified NTAG216 lists 0x00..0xE6 with roles.
Tests for the map, partial filtering, second-arg guard, T5577 blocks, and the no-layout case.
1269 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:24:45 -07:00
michael
260cb8eb9e feat(rawcli): annotate read/write output with datasheet memory regions
A READ now names the memory locations the pages cover, per the tag's datasheet layout,
grouping consecutive same-role pages:

  READ(0)  -> 00-02 → UID / serial number  ·  03 → Capability Container (CC)
  READ(4)  -> 04-07 → user memory
  READ(0xE3) -> E3 → CFG0 · E4 → CFG1 · E5 → PWD · E6 → PACK

TagCommand gains a pages() callable (which page/block numbers a command touches: READ = 4
pages from <page>, FAST_READ = <start>..<end>, WRITE = one page). memory.region_annotation()
groups those into "lo-hi → role" via the existing page_role layouts; _handle_call prints it
after the exchange. No layout for the tag (e.g. MIFARE Classic) -> no annotation.

Hardware-verified on NTAG216 (READ 0/4/0xE3, FAST_READ). Tests for the grouping + the
dispatch path. 1264 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:18:36 -07:00
michael
a501759c43 fix(transport): auto-resync after a read timeout (the "04 BB stale UID" desync)
On the flaky NG link a command's response can arrive *after* its read times out, and the
next command's read then picks up that stale frame — observed in rawcli as every raw 14a
command returning "04 BB" (the truncated UID from _ensure_selected's timed-out scan) instead
of its own response.

read_response/_read_any_response now set a _resync_needed flag on timeout, and the next
send_frame drains any late/stale input before writing. Cheap — only fires after a timeout
(rare on a healthy link) and no-ops when the stream is clean. Hardware-verified: identify ->
NTAG216, READ(4) 6/6 correct, GET_VERSION correct, with the hardening in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:35:21 -07:00
michael
a0b70a1984 feat(rawcli): command catalogs + hints for identified LF chips
catalog_for() returned None for LF, so after identifying a T5577/EM4100 the completer and
input_hint had nothing — no command suggestions, no hex/binary opcode hints. Now LF chips
get catalogs like the HF ones:

- T5577 catalog: READ(block) / WRITE(block, data) / WAKE(password) / DETECT(), with the
  downlink opcode exposed via build() for hex+binary completion, block-role hints (0=config,
  7=password, 1-6=data), and execution via a new run() path (LF isn't a raw-byte exchange —
  it drives lf.t55 / the demod). READ(0)/DETECT use the reliable rotation-fixed config read;
  data-block reads are best-effort and labelled as such. capture.read_t55xx_block added.
- LF read-only credentials (native EM4100/HID/AWID/FDX-B) get a minimal catalog (INFO -> re-read).
- catalog_for dispatches protocol "lf": "T5577" in the label -> T5577, else read-only.
- TagCommand gains an optional run(device, *args); completer/_opcode tolerate build=None.

Hardware-verified: identify -> catalog T5577, help lists the commands, DETECT()/READ(0) return
config 00148040 (EM4100). Tests: LF resolver, T5577 opcodes, T5577 block-role hints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:53:15 -07:00
michael
6c5409c111 fix(lf): T55xx config via rotation-to-preset match; label the emitted credential
Hardware-validated. Two real bugs in the T55xx config decode, both fixed by requiring a
bit-rotation of the recovered repeating word to match a known preset:

- Rotation ambiguity: a block read recovers the 32-bit config at an arbitrary bit offset.
  A real capture came back 0x000A4020 = the EM4100 word 0x00148040 rotated by one; the old
  "looks sane" scorer accepted the rotation verbatim and mislabeled it FSK1/RF-6. Now every
  rotation is tried and only a preset match (EM4100/HID/Indala/FDX-B/Viking/default) is
  accepted -> resolves to 0x00148040 = EM4100.
- False positives: garbage config captures (all-0, all-1, a rotation of the tag's own
  emission) no longer pass -> decode returns None and the reliable emitted-stream decode
  carries the result.

identify_lf label now names the chip + the actual decoded credential:
"T5577 (EM4100 00FFFFFFFF)" instead of the config's guess. On real hardware this is now
stable 4/4: config 00148040, emitted EM4100 00FFFFFFFF.

Tests: rotation recovery (0x000A4020 -> EM4100), no-false-positive on all-1s/random,
updated label expectation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:26:56 -07:00
michael
e82b94a490 fix(lf): identify_lf fails clearly on a raw async device (not "coroutine has no len")
capture drives the device synchronously (as Proxmark3.sync()'s proxy exposes it). Passing a
bare async Proxmark3() — whose methods return un-awaited coroutines, and which isn't even
connected — blew up deep in the demod with "object of type 'coroutine' has no len()". Now
identify_lf detects a coroutine-function device up front and raises an actionable TypeError
pointing at Proxmark3.sync().

The mock-based tests returned plain values, so they never exercised the async method shape;
added a test using a real coroutine function that reproduces and guards it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:58:30 -07:00
michael
26da98ede6 feat(lf): validate + retry LF capture against a flaky NG link
On this device the NG link is intermittent — a read/download that returns clean samples
one minute comes back as desynced BigBuf memory the next, and lf.read()'s reported count
is sometimes garbage (so sizing the download off it over-reads into memory). Rather than
trust either, capture now:

- downloads a fixed safe span (_DOWNLOAD_BYTES) instead of the flaky read-count,
- validates the result is an envelope, not memory (rejects the 0xDEADBEEF stack canary,
  an embedded PM3 response magic, or a mostly-zero unwritten buffer),
- retries the read+download a few times, giving up cleanly to b'' if the device only
  returns memory (needs a power-cycle / lower debug level).

read_emitted/read_config go through this, so identify recovers from transient desyncs
instead of demodulating garbage. Tests: validator rejects memory patterns; capture retries
past a garbage download to the good one, and gives up when all attempts are garbage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:41:03 -07:00
michael
a06f4d12b8 fix(lf): download only the acquired sample count, not a fixed span
With the debug-frame fix the BigBuf download now returns clean sample frames (verified on
hardware: offsets 0..N, real envelope data, then ACK). But identify downloaded a fixed
15000 bytes while the acquisition captured 12288 samples (98304 bits / 8), so the tail was
BigBuf memory past the samples (heap / the 0xDEADBEEF stack canary) — which flagged as
"garbage" and poisoned the demod.

read_emitted now sizes the download to lf.read()'s reported count (firmware returns the
size in BITS; /8 = samples at 8 bits/sample); read_config downloads the readbl acquisition
span. No trailing memory -> the demod runs on clean samples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:34:23 -07:00
michael
938a571155 fix(transport): skip async debug frames — the real cause of garbage LF reads
Debug-heavy firmware emits DEBUG_PRINT_STRING/INTEGERS/BYTES (0x0100-0x0102) frames
asynchronously, independent of any command. pm3py's read_response returned the FIRST
frame it saw, so a debug line landing mid-command was misparsed as the reply — observed
on hardware as garbage LF sample counts (e.g. 1593903105), a config-SET that "timed
out", and desynced BigBuf downloads (reading firmware memory / the 0xDEADBEEF stack
canary). The C client routes debug frames to its logger and keeps reading for the real
reply; pm3py did not.

read_response and _read_any_response now loop past DEBUG_PRINT_* (and malformed) frames
up to a bound, returning the actual command reply — for both NG replies and the OLD-frame
bulk-download stream. This is what makes LF read/download reliable on this device.

Tests: single + multiple interleaved debug frames are skipped to reach the real reply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:17:33 -07:00
michael
f09946c4a3 fix(lf): flush stale input before bulk download (desync -> garbage/DEADBEEF)
A real download came back as BigBuf memory (0xDEADBEEF poison) with a PM3 response
magic (0x62334d50) embedded — the classic signature of a desynced read: the stream
had stale bytes (a prior partial transaction, or the C client having touched the
port), and the bulk reader consumes fixed 544-byte OLD frames that carry no magic to
resync on, so once misaligned it stays misaligned and reads memory as "samples".

transport.download_bulk now flushes the input first (reset_input_buffer + drain the
asyncio StreamReader until an 80ms quiet gap), so the transfer always begins byte-
aligned. Drains no-op when disconnected. Test asserts the flush runs before the request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:52:22 -07:00
michael
b536294a7a fix(lf): read() acquired with the field OFF (SNIFF) — no tag was ever powered
lf.read() sent CMD_LF_SNIFF_RAW_ADC (passive sniff, reader field OFF), so it captured
only a dead field — a real hardware dump came back flat at 0x7e-0x80 (±1 count of ADC
noise) with no tag modulation, which is why LF identify found nothing. Reader-mode reads
must use CMD_LF_ACQ_RAW_ADC (SampleLF -> field ON) to power the tag and capture its load
modulation.

- read() -> CMD_LF_ACQ_RAW_ADC (field on); sniff() -> CMD_LF_SNIFF_RAW_ADC (field off),
  no longer delegating to read(). Shared _acquire() packs the real lf_sample_payload_t
  (PACKED bitfield samples:30/realtime:1/verbose:1 + cotag byte = 5 bytes) instead of a
  bare uint32.

Tests: read() uses ACQ with a 5-byte payload and correct samples field; sniff() uses SNIFF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:17:11 -07:00
michael
2f221644f9 fix(lf): BigBuf download spoke the wrong protocol — LF sample download hung
download_samples sent a DOWNLOAD_BIGBUF request per chunk and read replies with the
NG-only reader. But the firmware answers ONE request by streaming the data as OLD-format
CMD_DOWNLOADED_BIGBUF frames (no magic) then a single NG ACK (appmain.c) — so the NG
reader scanned for a magic that never came and timed out (hardware repro: lf.read() ok,
download_samples -> Response timeout). This blocked all LF identify on real hardware.

- transport.download_bulk: send the request once, then read with the existing NG/OLD-
  agnostic reader, accumulating CMD_DOWNLOADED_BIGBUF payloads (trimmed to oldarg[1], not
  the 512-byte padding) until the ACK or `count` bytes; drains the trailing ACK so it
  can't poison the next command. Reusable for EML/SPIFFS/flashmem bulk reads.
- download_samples: one call into download_bulk instead of the broken per-chunk loop.

Tests: count-terminated + ACK-terminated streams, oldarg-length trimming, single request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:11:11 -07:00
michael
5dd8e086cc fix(lf): per-protocol identify label (native HID/AWID/FDX-B no longer KeyError)
identify_lf built its label from emitted['id_hex'], which only EM4100 provides — a
native HID/AWID/FDX-B tag (no T5577 config) would KeyError. Add credential_label() to
format each decoder's own fields: "HID Prox H10301 FC.. Card..", "AWID-26 FC.. Card..",
"FDX-B 124-000000000555 (animal)", "EM4100 <id>". Tests for native FDX-B + HID labels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:00:14 -07:00
michael
42d5f800cb feat(lf): AWID FSK decode (parity-validated Wiegand)
protocols.decode_awid: FSK2a (no Manchester) via the ported detectAWID path, then
removeParity — the 88 bits after the preamble are 22 groups of 3 data + 1 odd-parity
bit, so stripping parity gives 66 bits whose first byte is the format length
(26/34/36/37/50) and the rest the facility/card fields (cmdlfawid.c index map). The 22
parity checks stand in for AWID's missing CRC, so it can't false-positive. Added to the
identify decoder chain.

Tests: AWID-26 round-trip across fc/card, parity gate rejects noise/EM4100. Full suite green.

LF stack now covers EM4100, HID, AWID, FDX-B + T55xx config. Indala (PSK) queued — the
one path needing real PSK phase-tracking (DetectPSKClock/pskRawDemod), best tuned against
a hardware capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:58:38 -07:00
michael
7e8d924045 feat(lf): FDX-B (ISO 11784/85) biphase demod + CRC-validated framing
- dsp.biphase_raw_decode: faithful port of lfdemod.c BiphaseRawDecode (pairwise
  differential-Manchester decode with phase-fault offset nudge + error markers).
- protocols.decode_fdxb: ASK + biphase, find the 00000000001 preamble, de-interleave
  the 8-bit data groups (every 9th bit a control 1), reassemble the 10-bit country +
  38-bit national code. Accepted only when the CRC-16 over the eight data bytes matches
  the stored one, so it can't false-positive. Both biphase polarities tried.
- protocols.crc16 / crc16_fdxb: parametric bitwise CRC-16 (Rocksoft model) matching the
  firmware crc16_fast; FDX-B is poly 0x1021, init 0, refin=false, refout=true.
  Added to the identify decoder chain (FDX-B is common in implants).

Tests: FDX-B round-trip (country/national/animal), CRC gate rejects noise/EM4100, and
a CRC known-answer test pinning the impl to CRC-16/XMODEM (0x31C3) and KERMIT (0x2189).
Full suite green.

Indala (PSK) + AWID queued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:55:45 -07:00
michael
e4a3aec46f feat(lf): FSK stack + HID Prox demod (faithful lfdemod.c port)
New pm3py/lf/fsk.py — a function-for-function Python port of the Proxmark FSK
stack (firmware/common/lfdemod.c): fsk_wave_demod (count samples between 0->1
transitions: short wave = fc/8, long = fc/10) + aggregate_bits (runs -> clock-
resolution bits) = fskdemod, plus preamble_search, HIDdemodFSK, detectAWID hooks.
Ported to track the C reference (and thus real-tag output), not reinvented.

protocols.decode_hid: FSK2a + Manchester -> Wiegand. Format is told by frame
structure, not parity alone — the 26-bit H10301 wire frame carries a 0x20 header
at bit 37 + a sentinel 1 at bit 26 (per firmware add_HID_preamble); the 37-bit
H10304 frame is header-less (37-bit Manchester count, no 0x20). Each candidate is
still parity-validated by re-encoding through the shipped HIDProxTag, so a false
alignment can't slip through. Added to the identify decoder chain after EM4100.

Synthetic HID tests build the real add_HID_preamble framing (header+sentinel for
26-bit, bare 37-bit) and render fc/8/fc/10 square waves, so a green test means the
demod inverts what a real HID card / PM3 sim emits. Full suite 1232 green.

AWID (96-bit) framing decode + PSK/Indala + biphase/FDX-B queued next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:14:54 -07:00
michael
3c56d40667 feat(lf): Python LF demod stack — ASK/EM4100 + T55xx config; wire into identify
The firmware returns raw ADC envelope samples for LF reads (T55xx readbl replies
PM3_SUCCESS with NULL data); demodulation was always the C client's job, so pm3py
had none and LF identify could never see a tag. This adds the missing DSP stack in
Python — new pm3py.lf package:

- dsp.py: modulation-agnostic primitives — robust threshold, edge-interval clock
  recovery, ASK binarize, half-bit resample, Manchester + biphase decode.
- protocols.py: EM4100 (ASK/Manchester -> 40-bit ID, validated by EM4100Code's own
  header/row/col/stop parity checks) and best-effort T55xx block-0 config read
  (find the repeating 32-bit word, score by T5577Config sanity, name the emulation).
- capture.py: live-device orchestration — read the emitted stream + probe a T55xx via
  block-0 read, combine into "is it a T5577, and what is it (emulating)?".

rawcli identify now demodulates LF instead of the dead readbl-only probe: reports
"T5577 (EM4100 / EM4102)" for an emulator, "EM4100 <id>" for a bare credential.

Fully self-testing without hardware: the LF transponder models already encode these
protocols, so a synthetic envelope built from an encoder round-trips through the
demod. 17 demod tests (EM4100 across IDs/rates/mid-frame/noise, T55xx config, mocked
identify) + updated rawcli LF tests. Full suite 1225 green.

FSK/HID + PSK/Indala + biphase/FDX-B decoders queued (same dsp primitives).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:58:51 -07:00
michael
8726681e06 feat(rawcli): LF T5577 identify + clear stale result per identify
- identify clears the previous field/protocol/transponder up front, so
  consecutive identifies (esp. when swapping tags) never show stale data.
- LF identify: probe a T5577 by reading config block 0 (lf.t55.readbl(0)).
  A valid config confirms the T5577 and, decoded via T5577Config, names
  what it's emulating — "T5577 (EM4100 / EM4102)", "T5577 (HID Prox)", etc.
  (exact-word map, else modulation family). Tightly gated (status ok,
  non-trivial config, known modulation, sane max_block) so no-tag reads
  don't false-positive; lf.search() couldn't do this without demod.
- format_summary shows the config word for LF (no UID).

4 tests (T5577 emulation report, no-T5577, stale-clear).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:31:35 -07:00
michael
0775df4d54 fix(rawcli): identify false-positives on 15693/LF; retry 14a
With only a MIFARE Classic present, identify randomly reported ISO15693
(bogus UID) or "LF tag" — because a flaky-link 14a miss fell through to
lenient checks:
- 15693 accepted any `uid` even without `found`; now requires found AND a
  real E0-prefixed UID (a 14a miss leaves non-E0 garbage).
- LF reported a tag whenever lf.search() returned its (always-truthy)
  sample-capture result; it can't confirm a tag without demod, so drop it
  from identify (reliable LF detection is a follow-up).
- Retry the 14a scan (up to 3x) so a transient miss doesn't fall through.

3 tests (E0-gated 15693, bogus-UID + false-LF rejection).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:26:38 -07:00
michael
7b5af3f9d2 fix(14a): raw() returns whole firmware buffer instead of received bytes
hf.iso14a.raw() returned the entire receive buffer the firmware replies
with (sizeof(buf)), so a tag that didn't answer came back as a run of
zeros (~40 bytes) rather than empty. The firmware puts the real received
length in oldarg[0]; trim resp.data to it. No response -> empty (raw=b"",
data=None); adds a `received` count. 2 regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:22:39 -07:00
michael
2f9e825bd4 feat(rawcli): colorized breadcrumb prompt
Color-code the prompt so the session state reads at a glance, using the
trace formatter's palette so prompt and trace look like one UI:

  [ raw / hf / NTAG213 / 0x ]
   dim  ^bold ^cyan  ^bold-green  ^yellow(hex) | magenta(binary)  dim

- bold `raw`; cyan RF field; bold-green identified transponder; entry
  mode yellow for hex, magenta for binary (so the base is obvious and
  flips color on Ctrl-/); dim brackets/separators.
- session.colored_breadcrumb() returns the ANSI form; breadcrumb() stays
  plain (stripping the ANSI yields it — asserted). Prompt message renders
  it via ANSI().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:11:47 -07:00
michael
92b4e8771d fix(rawcli): writes take (location, data); multi-frame + CRC
- A write needs both a location and data. NTAG WRITE (0xA2) already did;
  the two-phase 0xA0 writes (Type 2 COMPAT_WRITE, MIFARE Classic WRITE)
  only took the page/block. They now take (page/block, data) and build a
  frame *list* — the command frame plus the 16-byte data frame.
- Catalog build() may now return a list of frames; _handle_call sends each
  in sequence and traces every exchange (so both phases of a write show).
- 14a catalog commands now append the ISO14443-A CRC (ISO14A_APPEND_CRC),
  which real tags require — manual raw hex is still sent verbatim.

3 new/updated tests (two-phase build + send, CRC flag on reads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:07:50 -07:00
michael
cecfc03ba7 feat(rawcli): live relevance hint — per-transponder memory map
Surfaces what the input *means* on the identified tag, in a bottom-bar
hint that updates as you type (the point, over aggressive autocomplete):

- memory.py: page_role(transponder, page) maps a page/block to its role
  on the specific IC — UID, Capability Container, user memory, dynamic
  lock, CFG0/AUTH0, CFG1/ACCESS, PWD, PACK. Layouts mirror the models'
  page attributes (NTAG210/212/213/215/216, Ultralight EV1); a test
  guards against drift.
- input_hint(session, text): the command's purpose, and — once a page
  argument is typed (even partial, hex or decimal) — where that page
  lives. e.g. "READ(4)" -> user memory, "READ(0x2B" -> PWD,
  "WRITE(0x29" -> CFG0/AUTH0.
- app: wired as the prompt's bottom_toolbar.

11 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:02:59 -07:00
michael
1370526883 fix(rawcli): raw response crash + hex/binary opcode completion
- Crash on raw hex: reader raw() returns `data` as a hex STRING; the raw
  path fed that to bytes() -> "string argument without an encoding".
  _raw_exchange now returns the bytes (`raw` field, or converts `data`);
  render_exchange accepts bytes or a hex string.
- Completion for numeric entry, transponder-dependent: typing hex ("60")
  or binary ("01100000") now matches the identified tag's command opcodes
  and surfaces the named command (GET_VERSION) with its help. Honours a
  0x/0b prefix and the current entry mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:00:31 -07:00
michael
41db552e05 fix(rawcli): render ANSI, stop mangling commands, fuller Type 2 catalog
Three fixes from live hardware use:

- Colors/ANSI now render. Output routes through prompt_toolkit's
  print_formatted_text(ANSI(...)) instead of print(), which showed the
  escape codes literally under patch_stdout. Formatters force is_tty so
  the printer decides color-vs-plain. dispatch()/handlers take an `out`.
- Auto-byte-spacing no longer mangles non-hex input. byte_space() only
  regroups a *pure* hex/binary run; command words ("help transponder",
  "identify", "close") and 0x/0b-prefixed tokens are left intact (a-f in
  words used to trigger regrouping -> failed hex parse).
- Type 2 catalog gains PWD_AUTH (0x1B), COMPAT_WRITE (0xA0), HALT — the
  NTAG213 set was missing password auth.
- identify trims the firmware buffer padding off the GET_VERSION response
  (was dumping ~40 trailing 00 bytes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:50:01 -07:00
michael
6b4a4551d4 feat(rawcli): deep, traced identify — exact model via GET_VERSION
`identify` now probes and shows its work instead of a coarse SAK guess:

- Streams every probe exchange to the trace (proxmark-style annotated):
  the reconstructed 14a activation (WUPA/ATQA/anticollision CL1+CL2/
  SELECT/SAK) plus a REAL GET_VERSION (0x60 +CRC) sent over the persistent
  connection.
- Names the exact IC from GET_VERSION: an NTAG216 now reports "NTAG216",
  not "MIFARE UL/NTAG". Exact map mirrors the transponder models'
  _version_bytes (static, to dodge an import-order circular; a test guards
  against drift). Unknown chips fall back to product family + an honest
  memory-size *range* (AN10833 storage-byte encoding), not a bogus size.
- SAK fallback table + GET_VERSION product-nibble map aligned to NXP
  AN10833 (MIFARE type identification procedure).

11 tests (exact/structural decode, map-model sync, traced probe, SAK
names). GET_VERSION is a real device exchange — the user's hardware test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:40:41 -07:00
michael
3e7f47a740 fix(rawcli): Ctrl-/ key binding crashed launch ("Invalid key: c-/")
prompt_toolkit doesn't accept "c-/". Ctrl-/ is emitted as Ctrl-_ (0x1F)
by terminals, so bind "c-_" (plus "c-t" as an always-available fallback)
for the hex/binary toggle. install_key_bindings() now ignores any key
name a prompt_toolkit build rejects, so a bad key can never crash launch.
Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:25:36 -07:00
michael
f76663f798 feat(rawcli): ipython-style completion (Phase 6)
- completer.py: RawCompleter completes the leading command token —
  control verbs + the identified transponder's catalog commands — with
  each command's help as the completion meta (tooltip). `help <cmd>`
  completes catalog command names.
- app: wire the completer with complete_while_typing (live menu) +
  AutoSuggestFromHistory (inline hints) for the ipython feel.

Completes the 6-phase rawcli plan. 4 new tests (verb/catalog completion,
tooltips, help-arg completion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:19:42 -07:00
michael
4e7181d289 feat(rawcli): TLV/NDEF formatting + multi-line editing (Phase 5)
- tlv.py: parse_tlv() walks a Type-2 TLV block (NULL/LOCK/MEM/NDEF/PROP/
  TERMINATOR, incl. 3-byte 0xFF length); format_tlv() renders an indented
  multi-line breakdown and annotates NDEF messages via the existing
  decode_ndef_annotation. prompt_tlv() opens a multi-line editor to
  compose a TLV as hex across lines.
- app/parser: `tlv <hex>` decodes + prints the breakdown; bare `tlv`
  opens the multi-line editor.

5 new tests (structure, extended length, multi-line format, command).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:15:54 -07:00
michael
9c84f9e7b0 feat(rawcli): command catalog + function-style commands (Phase 4)
- catalog.py: declarative per-transponder command sets (name, params,
  build(args)->bytes, help). Seeds: Type 2 (NTAG/UL: READ, FAST_READ,
  GET_VERSION, READ_SIG, READ_CNT, WRITE), MIFARE Classic (READ/WRITE/
  HALT), ISO 15693 (INVENTORY, READ_BLOCK, WRITE_BLOCK, GET_SYSTEM_INFO).
  catalog_for(session) resolves by protocol + identified transponder.
- app: function-style calls (READ(4) -> build -> raw exchange ->
  annotated trace); catalog-aware help (`help` lists the current tag's
  commands, `help READ` shows one). Bad args print usage.

9 new tests (catalog builds, resolver, call dispatch, help). Command
knowledge is declarative + hardware-free-testable; the exchange is the
user's device test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:13:14 -07:00
michael
e024294ec6 feat(rawcli): identify + persistent connection (Phase 3)
- identify.py: probe HF (14a -> 15693) then LF; set session field /
  protocol / transponder. The 14a scan uses NO_DISCONNECT so the tag
  stays selected (the persistent connection). SAK -> coarse family name
  for the breadcrumb.
- session: add `protocol` (drives raw-exchange routing)
- app: connect via Proxmark3.sync() (scan/raw become plain calls); wire
  identify / transponder / close control commands; protocol-aware
  _raw_exchange; breadcrumb fills in field + transponder after identify

8 new tests (identify 14a/15693/none, clear, control commands). Device
probes are mocked; live identify is the user's hardware test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:10:51 -07:00
michael
54e52f029f feat(rawcli): flexible entry + parser (Phase 2)
- parser.py: classify a line into control / function-call / raw payload;
  parse loose hex/binary with intermixed 0x/0b tokens (whitespace-
  insensitive, defaulting to the session entry mode)
- entry.py: byte_space() byte-group formatting + key bindings — Ctrl-/
  toggles hex/binary entry mode (breadcrumb updates), digits auto-space
  into byte groups as you type
- app.py: dispatch() drives the loop via parse_line (testable without a
  live prompt); identify/call are stubbed for Phases 3/4

14 new tests (parser tokens/intermix/errors, byte_space, dispatch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:07:21 -07:00
michael
028ce9c21e feat(rawcli): interactive raw-command TUI skeleton (Phase 1)
First slice of `pm3py rawcli` — the interactive, annotated raw-command
tool (separate from pyws and from the future `pm3py client` CLI).

- pm3py/cli: a top-level `pm3py` console-script dispatcher (argparse)
  with a `rawcli` subcommand; prompt_toolkit gated behind a [rawcli] extra
- rawcli/session.py: RawSession — device/field/transponder/entry-mode
  state + the segmented breadcrumb `[raw / hf|lf / $tag / 0x|0b]`
- rawcli/trace_view.py: render a reader<->tag exchange as annotated,
  Proxmark-style trace lines, reusing TraceFormatter + the 14a/15693
  decoders
- rawcli/app.py: minimal PromptSession loop (connect best-effort,
  breadcrumb prompt, help/quit, raw-hex -> annotated exchange)

Entry-mode toggle, identify/connection, the command catalog, and TLV
editing follow in later phases. 9 hardware-free tests (session, trace
render, CLI dispatch); device I/O is the user's local hardware test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:59:29 -07:00
michael
5d4af2c8e4 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>
2026-07-14 09:34:24 -07:00
michael
130bcd1a90 feat(registers): ST25DV RF_Ai_SS area-security register
Add St25dvRfAreaSS (Register) for the ST25DV per-area RF security
registers (RFA1SS..RFA4SS): pwd_ctrl (which RF password gates the area)
and rw_protection (read/write protection level). Bound via
tag.rf_area_ss(area) / tag.set_rf_area_ss(area, value). Modelled from
the model's own tested protection logic (single source of truth) and
cross-checked against _read_allowed / _write_allowed. 3 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 09:05:55 -07:00
michael
f9e56e32ef feat(registers): ICODE feature-flags decoder (GET NXP SYSTEM INFO)
Add IcodeFeatureFlags (Register) for the 16-bit ICODE feature word
reported by GET NXP SYSTEM INFO (ICODE 3 Table 113; DNA/SLIX2 report a
subset). Read-only capability decoder — decode a captured word to see
what a tag supports. Fields mirror the existing _FEATURE_* constants
one-to-one (asserted in a test). 3 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 09:01:52 -07:00
michael
e795b5bb1d feat(registers): NXP AES key-privilege register (ICODE DNA / NTAG5)
Add NxpKeyPrivileges (Register) for the Table 5 AES key-privilege byte
and bind it on the NxpAesAuth mixin (ICODE DNA + NTAG5 Link/Boost):

- read / write / privacy / destroy / eas_afi / crypto_config /
  area1_read / area1_write, each a documented bit
- NxpKeyPrivileges.build(read=True, write=True, ...) keyword builder
- tag.key_privileges(key_id) / tag.set_key_privileges(key_id, value)

Additive: the existing PRIV_* constants and has_privilege() path are
unchanged and see the same bytes. 5 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:58:24 -07:00