From 3aab155004d045eb6459d635cbea7aafdd840bbb Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 5 Jul 2026 01:27:10 -0700 Subject: [PATCH] docs: rewrite README against current API (live sniff/sim, mfu, inventory) Full refresh of the stale 2026-03 README. Fixes sub-module names (iso14a not a14, mf not mfc), adds MIFARE Ultralight/NTAG (mfu) and iso15 inventory() with AFI/DSFID filtering, documents live SniffSession/SimSession streaming + tag.sync() live edits + the transponder/sim framework, and the sync-proxy dir()/help()/tab-completion introspection. Every code example verified against the source; hw.led carries a caveat that it needs the pending CMD_LED_CONTROL firmware port. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 501 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 410 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 03070c2..c927593 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # pm3py -Pure-Python library for talking to a [Proxmark3](https://github.com/RfidResearchGroup/proxmark3) over its NG wire protocol. Returns structured Python dicts instead of parsing terminal output. Async-first with a sync wrapper for REPL use. +Pure-Python async client for the [Proxmark3](https://github.com/RfidResearchGroup/proxmark3), speaking its NG wire protocol directly over USB serial. Structured Python dicts instead of parsed terminal text, with a sync proxy for REPL use. -**No dependency on the C client.** Speaks the wire protocol directly over USB serial. +- **Pure Python** — no dependency on the C client binary; speaks the wire protocol directly over USB serial. +- **Structured results** — every command returns a Python dict, not scraped terminal output. +- **Async-first, REPL-friendly** — `async`/`await` core with a synchronous proxy (`Proxmark3.sync()`) that supports `dir()`, `help()`, and tab-completion. +- **Live sniff & simulation** — stream decoded reader↔tag frames into your REPL in real time; edit an emulated tag and push it live. +- **Software-defined tags** — pure-Python transponder models (ISO 14443-A, ISO 15693, MIFARE Classic, NTAG/Type-2, NXP ICODE/SLIX2/NTAG5, LF) served from a firmware response table. ## Install @@ -11,169 +15,484 @@ cd pm3py pip install -e . ``` -Requires Python 3.10+ and `pyserial-asyncio`. +Requires Python 3.10+ and `pyserial-asyncio>=0.6` (installed automatically). ## Quick Start +pm3py is async-first, but the sync REPL proxy is the easiest way to start. `Proxmark3.sync()` auto-detects the serial port, connects, probes the firmware, and returns a proxy where every command runs synchronously: + ```python from pm3py import Proxmark3 -# Sync mode — auto-detects the device -pm3 = Proxmark3.sync() -print(pm3.firmware.version_string) -print(pm3.hw.ping()) -print(pm3.hw.version()) -pm3.close() +pm3 = Proxmark3.sync() # auto-detect; or Proxmark3.sync("/dev/ttyACM0") +print(pm3.firmware) # version + compatibility, populated on connect +print(pm3.hw.ping()) # {'success': True, 'length': 32, 'data': '00010203...'} +print(pm3.hw.version()["version_string"]) +print(pm3.hf.iso14a.scan()) # scan a 14443-A card +pm3.close() # disconnect + close the event loop ``` -Or with async: +The sync proxy is REPL-friendly: `dir(pm3)`, `dir(pm3.hf)`, tab-completion, and `help(pm3.hf.iso15.rdbl)` all work and report each command's real signature (the proxy uses `functools.wraps` + `__dir__`, so wrapped methods keep their `__doc__` and parameters instead of showing `(*args, **kwargs)`). + +Prefer async? Use the context manager directly: ```python import asyncio from pm3py import Proxmark3 async def main(): - async with Proxmark3() as pm3: + async with Proxmark3() as pm3: # auto-detect, or Proxmark3("/dev/ttyACM0") print(await pm3.hw.ping()) - print(await pm3.hf.a14.scan()) + print(await pm3.hf.iso14a.scan()) asyncio.run(main()) ``` -## API +All command methods return structured Python dicts, not parsed terminal text. The command tree mirrors the C client: -All commands return Python dicts. The API mirrors the C client's command tree: +| Sub-module | Protocol | +|------------|----------| +| `pm3.hw.*` | Hardware / device control | +| `pm3.lf.*`, `pm3.lf.t55.*` | Low frequency + T55xx | +| `pm3.hf.*` | HF core (tune, search, sniff, dropfield) | +| `pm3.hf.iso14a.*` | ISO 14443-A | +| `pm3.hf.iso15.*` | ISO 15693 | +| `pm3.hf.mf.*` | MIFARE Classic | +| `pm3.hf.mfu.*` | MIFARE Ultralight / NTAG | + +## API ### Hardware — `pm3.hw.*` ```python -pm3.hw.ping() # verify connectivity -pm3.hw.version() # firmware version, chip ID -pm3.hw.status() # device status -pm3.hw.capabilities() # compiled features, platform (Easy vs RDV4) -pm3.hw.tune() # full antenna tuning (LF + HF voltages) -pm3.hw.led("green", on=True) # LED control with color names -pm3.hw.led("red", pulse=True, speed=300)# PWM effects (validates hardware support) -pm3.hw.led("all", on=False) # turn off -pm3.hw.dbg(level=2) # set debug level -pm3.hw.break_loop() # stop long-running operations -pm3.hw.reset() # reset device +pm3.hw.ping() # {'success': True, 'length': 32, 'data': '...'} +pm3.hw.ping(length=64) # ping with a custom echo payload length +pm3.hw.version() # {'chip_id': '0x...', 'section_size': ..., 'version_string': '...'} +pm3.hw.status() # {'status': 0} +pm3.hw.capabilities() # {'version', 'baudrate', 'bigbuf_size', 'is_rdv4', + # 'compiled_with_lf', 'compiled_with_iso14443a', + # 'compiled_with_iso15693'} +pm3.hw.tune() # full antenna tuning — nested {'lf': {...}, 'hf': {...}} +pm3.hw.dbg() # get debug level -> {'level': N} +pm3.hw.dbg(level=2) # set debug level 0-4 +pm3.hw.led("green", on=True) # LED control by color name +pm3.hw.led("all", off=True) # turn everything off +pm3.hw.break_loop() # stop a long-running firmware operation +pm3.hw.reset() # reset the device (fire-and-forget) ``` -LED colors are platform-aware — `"green"` maps to the correct physical LED on both PM3 Easy and RDV4. PWM operations (brightness, pulse, fade) validate hardware capability and raise `PM3Error` if the LED doesn't support it. +`hw.tune()` runs the full LF+HF antenna sweep and returns both bands at once: + +```python +t = pm3.hw.tune() +print(t["lf"]["125kHz_V"], t["lf"]["134kHz_V"], t["lf"]["peak_freq_kHz"]) +print(t["hf"]["13.56MHz_V"]) +``` + +> ⚠️ **`hw.led` needs firmware with `CMD_LED_CONTROL`** (our LED port — currently a +> pending follow-up). On stock/current firmware `0x011A` is `CMD_SET_HF_FIELD_TIMEOUT`, +> so `hw.led` is presently a no-op. The API below is what it will drive once that +> firmware lands. + +**LED control** is platform-aware — color names map to the correct physical LED on both PM3 Easy and RDV4. Accepts color names (`"green"`, `"red"`, `"orange"`, `"blue"`/`"red2"`, `"all"`), letters (`"a"`-`"d"`), comma-separated combos (`"green,red"`), or a bitmask int. Pick one action kwarg: + +```python +pm3.hw.led("red", on=True) # on +pm3.hw.led("a", toggle=True) # toggle +pm3.hw.led("green", blink=True, speed=300, count=10) # blink (all LEDs) +pm3.hw.led("green", pulse=True) # PWM pulse — PWM-capable LEDs only +pm3.hw.led("green", brightness=50) # PWM brightness 0-100 +``` + +PWM effects (`brightness`, `pulse`, `fade`) validate hardware support and raise `PM3Error` if the target LED can't do PWM (Easy: A,B; RDV4: A,D). + +Also available: `pm3.hw.tearoff(delay_us=..., on=True)` and `pm3.hw.standalone(mode="14a")` for tear-off timing and standalone modes. ### Low Frequency — `pm3.lf.*` ```python -pm3.lf.tune(divisor=95) # LF antenna voltage at frequency -pm3.lf.read(samples=30000) # capture ADC samples -pm3.lf.config() # get sampling config -pm3.lf.config(divisor=88) # set config (134 kHz) -pm3.lf.search() # capture + return LFSearchResult -pm3.lf.sniff() # alias for read -pm3.lf.sim(gap=0, data=b"...") # simulate from buffer - -# T55xx tags -pm3.lf.t55.readbl(block=0) -pm3.lf.t55.writebl(block=1, data=0xDEADBEEF) -pm3.lf.t55.wakeup(password=0x12345678) +pm3.lf.tune() # LF antenna voltage @125kHz (divisor 95) +pm3.lf.tune(divisor=88) # @134kHz — see return shape below +pm3.lf.read(samples=12288) # capture ADC samples into device buffer +pm3.lf.sniff() # alias for read() +pm3.lf.config() # get current sampling config +pm3.lf.config(divisor=88) # set config (134 kHz); fire-and-forget then re-read +pm3.lf.search() # capture + return an LFSearchResult +pm3.lf.sim(gap=0, data=b"...") # simulate an LF tag from the sample buffer ``` -`lf.search()` returns an `LFSearchResult` with callable next steps: +`lf.tune(divisor=...)` sends the START then MEASURE steps and returns the antenna voltage: + +```python +pm3.lf.tune(divisor=95) +# {'voltage_mV': 24875, 'voltage_V': 24.875, 'frequency_kHz': 125.0, 'divisor': 95} +``` + +Frequency is `12000 / (divisor + 1)` kHz — `95` = 125 kHz, `88` = 134 kHz. + +`lf.search()` returns an `LFSearchResult` with callable next steps (it captures raw samples; full EM410x/HID/AWID demodulation still needs the C client's DSP stack): ```python result = pm3.lf.search() -print(result) # shows available actions -raw = result.download() # download raw ADC samples -result.tune() # check antenna voltage -result.read_t55xx(block=0) # try reading as T55xx +print(result) # prints available actions + sample count +raw = result.download() # raw ADC samples as bytes (1 byte/sample, 0-255) +result.tune() # check antenna voltage (tag present?) +result.read_t55xx(block=0) # try reading it as a T55xx ``` -Full tag demodulation (EM410x, HID, AWID, etc.) requires the C client's DSP stack. +**T55xx tags — `pm3.lf.t55.*`:** + +```python +pm3.lf.t55.readbl(block=0) # -> {'status', 'data': hex, 'raw': bytes} +pm3.lf.t55.readbl(block=1, password=0x12345678) # password read +pm3.lf.t55.writebl(block=1, data=0xDEADBEEF) # write a 32-bit block +pm3.lf.t55.writebl(block=1, data=0x00, password=0x12345678) +pm3.lf.t55.wakeup(password=0x12345678) # password wakeup +pm3.lf.t55.config() # downlink timing table (client-side) +``` ### High Frequency — `pm3.hf.*` -```python -pm3.hf.tune() # HF antenna voltage -pm3.hf.search() # try 14a then 15693 -pm3.hf.sniff() # sniff HF traffic -pm3.hf.dropfield() # turn off field -``` - -### ISO 14443-A — `pm3.hf.a14.*` +Core HF operations. `tune()` runs the firmware START/MEASURE/STOP state machine internally and reports the antenna voltage; `search()` probes 14443-A then 15693. ```python -pm3.hf.a14.scan() # → {uid, atqa, sak, ats} -pm3.hf.a14.raw(data=b"\x50\x00") # raw APDU -pm3.hf.a14.sniff() -pm3.hf.a14.sim(uid=b"\x01\x02\x03\x04") +pm3.hf.tune() # → {'voltage_mV': 4521, 'voltage_V': 4.521} +pm3.hf.search() # try 14a then 15693 → {'iso14443a': {...}, 'found': True} +pm3.hf.sniff() # sniff raw HF, returns {'status', 'samples'} +pm3.hf.dropfield() # turn the HF field off (fire-and-forget) ``` +`search()` returns a dict with an `iso14443a` and/or `iso15693` key when a tag answers, plus `found: bool`. `sniff()` accepts `samples_to_skip`, `triggers_to_skip`, `skip_mode`, `skip_ratio` and returns the captured sample count; for decoded, protocol-aware sniffing (including live streaming) use `SniffSession` instead. + +### ISO 14443-A — `pm3.hf.iso14a.*` + +```python +pm3.hf.iso14a.scan() # → {'found', 'uid', 'atqa', 'sak', 'ats', ...} +pm3.hf.iso14a.raw(b"\x30\x00") # raw frame → {'status', 'data', 'raw'} +pm3.hf.iso14a.sniff() # sniff 14443-A, returns {'status'} +pm3.hf.iso14a.sim(tagtype=1, uid=b"\x01\x02\x03\x04") # simulate a tag +``` + +`scan()` selects the card and returns UID/ATQA/SAK plus `ats` and a `select_status` (1=OK+ATS, 2=OK no ATS, 3=proprietary). `raw()` sends a raw frame with the RAW + NO_DISCONNECT flags already set; pass extra `flags` and a `timeout_14a` if needed. `sim()` emulates a tag — `tagtype` picks the model (1=MIFARE Classic 1k, 2=Ultralight, 3=DESFire, 4=ISO14443-4, 7=MFU EV1/NTAG215, 8=MFC 4k, 11=JCOP, 13=ULC, 14=ULAES, ...), `uid` is 4/7/10 raw bytes (empty = take UID from emulator memory), and `exit_after` stops after N reader reads. + +For a full software-defined tag served from the firmware response table (NDEF, custom commands, live trace), use `SimSession` with an `NfcType2Tag` — see the simulation section. + ### ISO 15693 — `pm3.hf.iso15.*` ```python -pm3.hf.iso15.scan() # → {uid, dsfid} -pm3.hf.iso15.rdbl(block=0) # read block -pm3.hf.iso15.wrbl(block=0, data="00112233") -pm3.hf.iso15.sniff() +pm3.hf.iso15.scan() # quick single-tag → {'found', 'uid', 'dsfid', 'response_flags'} +pm3.hf.iso15.inventory() # 16-slot anti-collision → list of tag dicts +pm3.hf.iso15.rdbl(0) # read block 0 +pm3.hf.iso15.wrbl(0, "00112233") # write block 0 +pm3.hf.iso15.sniff() # sniff 15693 (blocks until button/timeout) ``` +`scan()` is the "is one tag present" convenience and returns a single dict. `inventory()` runs a real anti-collision round: + +```python +# 16-slot round (default) → LIST of tags (empty list if none) +pm3.hf.iso15.inventory() +# → [{'uid': 'e0040150...', 'dsfid': 0, 'flags': 0, 'slot': 3}, ...] + +# single slot → ONE tag dict (or None) +pm3.hf.iso15.inventory(slots=1) +# → {'uid': 'e0040150...', 'dsfid': 0, 'flags': 0} + +# filter by AFI (request-level, only matching tags answer) +pm3.hf.iso15.inventory(afi=0x00) + +# filter by DSFID (client-side, on the returned records) +pm3.hf.iso15.inventory(dsfid=0) +``` + +UIDs come back in display order (already reversed from wire order). Block I/O optionally takes a UID for addressed mode: + +```python +pm3.hf.iso15.rdbl(4) # unaddressed read +pm3.hf.iso15.rdbl(4, uid="e004015012345678") # addressed read +# → {'success': True, 'block': 4, 'locked': False, 'data': '00112233', 'raw': b'...'} + +pm3.hf.iso15.wrbl(4, "00112233") # unaddressed write (bytes or hex str) +pm3.hf.iso15.wrbl(4, b"\x00\x11\x22\x33", uid="e004015012345678") +# → {'success': True, 'block': 4} +``` + +`raw_inventory(inv_flags=0x26)` is a diagnostic that returns the unparsed device response (`0x26` = 1 slot, `0x06` = 16 slots). For decoded/live sniffing use `SniffSession`. + ### MIFARE Classic — `pm3.hf.mf.*` +> **Renamed:** the MIFARE Classic sub-module used to be `pm3.hf.mfc`. It is now +> `pm3.hf.mf` (the backing module is still `pm3py/core/hf_mfc.py`). Update any +> old scripts. + +Keys are 6 bytes, given as a hex string or `bytes`. `key_type` is `0` for +key A (default) or `1` for key B. + ```python -pm3.hf.mf.rdbl(block=0) # read block (default key FF..FF) -pm3.hf.mf.rdbl(block=4, key="A0A1A2A3A4A5", key_type=1) # key B -pm3.hf.mf.wrbl(block=4, key="FFFFFFFFFFFF", data="00" * 16) -pm3.hf.mf.rdsc(sector=0) # read entire sector -pm3.hf.mf.chk(block=0, keys=["FFFFFFFFFFFF", "A0A1A2A3A4A5"]) -pm3.hf.mf.sniff() -pm3.hf.mf.sim(uid="01020304", size="1k") -pm3.hf.mf.nested(block=0, key="FFFFFFFFFFFF") -pm3.hf.mf.cident() # identify magic card type +pm3 = Proxmark3.sync() + +# Read one block (default key FFFFFFFFFFFF, key A) +pm3.hf.mf.rdbl(0) +# → {'success': True, 'block': 0, 'data': '0403...', 'raw': b'\x04\x03...'} + +pm3.hf.mf.rdbl(4, key="A0A1A2A3A4A5", key_type=1) # key B + +# Write a block (data must be 16 bytes) +pm3.hf.mf.wrbl(4, data="00112233445566778899AABBCCDDEEFF", + key="FFFFFFFFFFFF", key_type=0) +# → {'success': True, 'block': 4} + +# Read a whole sector (sectors 0–31 = 4 blocks, 32–39 = 16 blocks) +pm3.hf.mf.rdsc(0) # → {'sector': 0, 'first_block': 0, 'num_blocks': 4, 'blocks': {...}} + +# Key-check a block against a list of candidate keys +pm3.hf.mf.chk(0, keys=["FFFFFFFFFFFF", "A0A1A2A3A4A5", "D3F7D3F7D3F7"]) +# → {'found': True, 'key': 'ffffffffffff', 'key_type': 0} (or {'found': False}) ``` -### Raw Commands +**Nested attack** — recover an unknown key using one known key on the card: -For anything not wrapped: +```python +pm3.hf.mf.nested(block=0, key="FFFFFFFFFFFF", key_type=0, + target_block=4, target_key_type=1, calibrate=True) +# → {'success': True, 'is_ok': 0, 'block': 4, 'key_type': 1, +# 'cuid': '...', 'nt_a': '...', 'ks_a': '...', 'nt_b': '...', 'ks_b': '...'} +``` + +**Magic-card identification** — detect gen1/gen2 "magic" cards: + +```python +pm3.hf.mf.cident() +# → {'status': 0, 'magic': 0, 'is_magic': False} +``` + +**Sniff / simulate:** + +```python +pm3.hf.mf.sniff() # routed through the ISO14443-A sniffer + +# Simulate a card. size ∈ {"mini","1k","2k","4k"}; UID is 4, 7, or 10 bytes. +pm3.hf.mf.sim(uid="01020304", size="1k") +pm3.hf.mf.sim(uid="04AABBCCDDEE80", size="4k", + atqa="0044", sak=0x18, exit_after=1, interactive=True) +``` + +For full software-defined MIFARE Classic simulation (response table served from +firmware, live trace), see the sim framework section and `MifareClassicTag`. + +### MIFARE Ultralight / NTAG — `pm3.hf.mfu.*` + +New sub-module for ISO14443-3A tags with 4-byte pages (MIFARE Ultralight, +UL-C, UL-EV1, UL-AES, and the NTAG21x family). A `READ` returns 4 consecutive +pages (16 bytes) at once; a `WRITE` sets a single 4-byte page. + +`keytype` selects the auth scheme (pass the matching `key`): + +| keytype | scheme | key length | +|---------|--------|-----------| +| `0` | none (default) | — | +| `1` | UL-C (3DES) | 16 bytes | +| `2` | UL-EV1 / NTAG password | 4 bytes | +| `3` | UL-AES | 16 bytes | + +```python +pm3 = Proxmark3.sync() + +# Read starting at page 4 → 16 bytes (pages 4,5,6,7) +pm3.hf.mfu.rdbl(4) +# → {'success': True, 'block': 4, 'data': '01020304...', 'raw': b'\x01\x02...'} + +# Password-authenticated read (UL-EV1 / NTAG, 4-byte pwd) +pm3.hf.mfu.rdbl(4, key="FFFFFFFF", keytype=2) + +# Write one 4-byte page +pm3.hf.mfu.wrbl(4, data="DEADBEEF") +# → {'success': True, 'block': 4} + +# Dump pages (each underlying READ pulls 4 pages; stops early on failure) +pm3.hf.mfu.dump(start=0, pages=16) +# → {'success': True, 'pages': 16, 'data': '...', 'raw': b'...'} +``` + +For software-defined NTAG/Type-2 simulation (NDEF, served from a firmware +response table with live trace), use `NfcType2Tag` with `SimSession` — see the +sim framework section. + +## Live Sniff & Simulation + +pm3py can drive the Proxmark3 as a **live sniffer** and as a **software-defined card simulator**, with reader↔tag traffic streamed into your REPL in real time. Both use custom firmware (the `firmware/` submodule / `proxmark3-pm3py`) and open the serial port directly, so they run from the plain sync REPL — no asyncio needed. + +### Live sniff — `SniffSession` + +`SniffSession` starts a firmware sniff and streams decoded frames to the console from a background thread. The Proxmark3 button **toggles pause/resume** while streaming, so you can walk a card up to the field, pause, inspect, and resume without touching the keyboard. Captured frames accumulate across pause/resume cycles in `session.entries`. + +```python +from pm3py.sniff.session import SniffSession + +s = SniffSession.open() # opens /dev/ttyACM0 by default +s.start_15693(live=True) # streams ISO 15693 trace live; PM3 button pauses/resumes +# ... present a tag to the antenna; decoded reader/tag frames print as they arrive ... +s.stop() # end the sniff (sends BREAK_LOOP) + +print(len(s.entries)) # all captured frames across pause/resume cycles +print(s.state) # 'sniffing' | 'paused' | 'stopped' +s.entries[0] # {'protocol', 'direction', 'duration', 'timestamp', 'data'} +s.close() # stop (if needed) and close the serial port +``` + +- `SniffSession.open(port="/dev/ttyACM0", baudrate=115200)` — open the port. +- `start_15693(live=True, idle_timeout_ms=400)` — live streaming with button-toggle. Pass `live=False` for the legacy BigBuf batch path (for batch decoding you can also use `pm3.hf.iso15.sniff()`). +- `s.state`, `s.entries`, `s.clear()` — inspect and reset accumulated capture. +- `s.stop()` / `s.close()` — halt streaming / release the port. + +Status transitions (Started / Paused / Resumed / Stopped) print inline as the button is pressed. + +### Live sim — `SimSession` + +`SimSession` compiles a Python tag model into a firmware **response table**, uploads it, and starts the tag simulation. The firmware answers standard commands (inventory, read, write) autonomously at wire speed. With `trace=True`, every reader↔tag frame is decoded and streamed live from a background thread. You can then **edit the tag in the REPL and push the changes live** with `tag.sync()` — no restart of the sim required. + +```python +from pm3py.sim import SimSession, IcodeSlix2Tag, ndef_uri + +# Build a tag model (uid=None auto-generates a valid E0 04 02.. SLIX2 UID) +tag = IcodeSlix2Tag(uid=None, ndef_message=ndef_uri("https://dngr.us")) + +s = SimSession.open() # opens /dev/ttyACM0 by default +s.start_15693(tag, compile=True, trace=True) # upload table, start sim, stream frames live +# ... a phone / reader now reads the emulated tag; frames print as they arrive ... + +# Live edit: change the NDEF payload and push it to the firmware emulator memory +tag.set_ndef(ndef_uri("https://example.org/new")) +tag.sync() # firmware now serves the updated content + +s.stop() # stop the sim (sends BREAK_LOOP) +s.close() # stop (if needed) and close the port +``` + +- `SimSession.open(port="/dev/ttyACM0", baudrate=115200)` — open the port for a synchronous sim. +- `start_15693(tag, compile=True, trace=False)` — compile+upload the response table and start the 15693 sim; `trace=True` streams decoded reader↔tag traffic (and reports RF field strength via the optional `on_field_strength` callback). +- `tag.sync()` — push in-REPL edits (UID, memory, NDEF, access conditions) to firmware emulator memory. It does a smart sync: EML memory push for data changes, response-table recompile for access-condition changes. The tag must be bound to a live session (`start_15693` binds it automatically). +- `s.stop()` / `s.close()` — halt the sim / release the port. + +A 14443-A sim path (`await s.start_14a(tag)`, WTX relay) also exists but is **async and experimental** — it requires the async `PM3Transport` (not the synchronous `SimSession.open()` port above) and is still under development. + +### Card simulation + +The simulation framework (`pm3py/sim/` infrastructure, `pm3py/transponders/` tag models) provides pure-Python transponder models served from the firmware response table. Import everything from `pm3py.sim`. + +**ISO 15693 (`start_15693`)** +- `NfcType5Tag` — generic NFC Forum Type 5 tag with NDEF (`ndef_text`, `ndef_uri`, `ndef_mime`). +- `IcodeSlix2Tag` (and `IcodeSlixTag`, `Icode3Tag`, `IcodeDnaTag`, `NxpIcodeTag`) — NXP ICODE family with passwords, privacy mode, EAS/AFI, originality signature. +- `Ntag5SwitchTag`, `Ntag5LinkTag`, `Ntag5BoostTag` (base `Ntag5PlatformTag`) — NXP NTAG 5 dual-interface family. + +**ISO 14443-A (`start_14a`, async/experimental)** +- `NfcType2Tag` — NFC Forum Type 2 (MIFARE Ultralight / NTAG-style) with NDEF. +- `NfcType4Tag`, `MifareClassicTag`, `DesfireTag` — Type 4, MIFARE Classic (Crypto1), and DESFire models. + +```python +from pm3py.sim import SimSession, NfcType5Tag, ndef_uri + +# uid=None auto-generates a valid E0-prefixed 15693 UID +tag = NfcType5Tag(uid=None, ndef_message=ndef_uri("https://dngr.us")) +s = SimSession.open() +s.start_15693(tag, trace=True) # emulate an NFC Type 5 tag to a 15693 reader +s.stop(); s.close() +``` + +NDEF helpers `ndef_text(text, lang="en")`, `ndef_uri(uri)`, and `ndef_mime(mime_type, data)` build the message bytes passed as `ndef_message=` (or via `tag.set_ndef(...)` for live edits). `uid=None` on the 15693 models auto-generates a valid `E0`-prefixed UID with the correct manufacturer/IC-type bytes so phones identify the IC correctly. + +## Raw Commands + +For anything not yet wrapped, drop down to the raw NG/MIX frame senders. Both are +regular client methods, so they work through the sync REPL proxy or `await` in async +code: ```python from pm3py import Cmd -resp = pm3.send_ng(Cmd.STATUS) # NG frame -resp = pm3.send_mix(Cmd.HF_ISO14443A_READER, arg0=0x0103) # MIX frame +# NG frame: cmd + raw payload bytes +resp = pm3.send_ng(Cmd.STATUS) +resp = pm3.send_ng(Cmd.CAPABILITIES) + +# MIX frame: cmd + three uint64 args + optional payload +resp = pm3.send_mix(Cmd.HF_ISO14443A_READER, arg0=0x0103) + +# resp is a PM3Response: resp.cmd, resp.status, resp.data (bytes) +print(resp.status, resp.data.hex(), len(resp.data)) ``` +Signatures: `send_ng(cmd, payload=b"", timeout=2.0)` and +`send_mix(cmd, arg0=0, arg1=0, arg2=0, payload=b"", timeout=2.0)`. Both return a +`PM3Response` (fields: `cmd`, `status`, `reason`, `ng`, `data`, `oldarg`). Use these +only for commands the firmware replies to — fire-and-forget commands (e.g. +`HF_DROPFIELD`) send no response and would block until the timeout; call the wrapped +method (`pm3.hf.dropfield()`) for those. + +> **REPL discovery:** the sync proxy returned by `Proxmark3.sync()` implements +> `__dir__` and uses `functools.wraps` on every wrapped method, so `dir(pm3)`, +> tab-completion, and `help(pm3.hf.iso15.rdbl)` all surface the real sub-modules, +> method names, and signatures (e.g. `rdbl(block, uid=None)`) instead of an opaque +> `(*args, **kwargs)` wrapper. + ## Firmware Compatibility -On connect, pm3py pings the device and fetches the firmware version. Results are in `pm3.firmware`: +On connect, pm3py pings the device and fetches the firmware version. The result is on +`pm3.firmware`: ```python pm3 = Proxmark3.sync() -print(pm3.firmware.compatible) # True if NG protocol works -print(pm3.firmware.version_string) # firmware version +print(pm3.firmware.compatible) # True if the NG protocol handshake worked +print(pm3.firmware.version_string) # firmware version string print(pm3.firmware.chip_id) # e.g. "0x270B0A40" -print(pm3.firmware.warnings) # any issues detected +print(pm3.firmware.warnings) # any issues detected during probe ``` +Core reader/sniff/HW commands work against **stock RfidResearchGroup/proxmark3** +firmware. The **live streaming** features (streaming sniff and the response-table / +trace card simulation) require our firmware fork, tracked as the `firmware/` git +submodule (`proxmark3-pm3py`, rebased onto upstream RfidResearchGroup/proxmark3). +Those add the `CMD_HF_SNIFF_STREAM` live-frame path and the BigBuf response-table + +WTX relay used by `SimSession`. Everything else degrades gracefully on stock firmware. + ## Architecture ``` pm3py/ -├── protocol.py # Wire protocol constants, CRC-16/A, command IDs, error codes -├── transport.py # Frame encode/decode, async serial I/O, PM3Transport -├── client.py # Proxmark3 class, sync proxy, firmware probe -├── hw.py # hw.* commands -├── lf.py # lf.* commands + T55xx -├── hf.py # hf.* commands (tune, search, sniff) -├── hf_14a.py # hf.14a.* (ISO 14443-A) -├── hf_15.py # hf.15.* (ISO 15693) -└── hf_mf.py # hf.mf.* (MIFARE Classic) +├── core/ # wire protocol + device command modules +│ ├── protocol.py # constants, CRC-16/A, Cmd enum, PM3Status +│ ├── transport.py # NG/MIX frame encode/decode, PM3Transport (async serial) +│ ├── client.py # Proxmark3, _SyncProxy, FirmwareInfo +│ ├── hw.py # hw.* (ping, version, tune, LEDs, dbg, reset) +│ ├── hf.py # hf.* core (tune, search, sniff, dropfield) +│ ├── hf_iso14a.py # hf.iso14a.* (ISO 14443-A) +│ ├── hf_iso15.py # hf.iso15.* (ISO 15693) +│ ├── hf_mfc.py # hf.mf.* (MIFARE Classic) +│ ├── hf_mfu.py # hf.mfu.* (MIFARE Ultralight / NTAG) +│ └── lf.py # lf.* + lf.t55.* (T55xx) + LFSearchResult +├── trace/ # firmware trace parsing + annotation (shared by sniff & sim) +│ ├── trace.py # parse_tracelog +│ ├── decode_iso14a.py / decode_iso15.py # per-protocol decoders +│ ├── ndef.py # NDEF TLV/record decode +│ └── format.py # ANSI color trace formatting +├── sniff/ # SniffSession — protocol detection, live streaming lifecycle +├── sim/ # card-sim infrastructure: SimSession, response-table compiler, +│ # Medium/Reader models, MCU bridge, replay/relay/fuzzer +└── transponders/ # pure-Python tag models + ├── hf/iso14443a/ # Tag14443A, MifareClassic, DESFire, NfcType2/4 + ├── hf/iso15693/ # Tag15693, NfcType5, ICODE/SLIX2/DNA/NTAG5 + └── lf/ # EM4100, HID, T5577 ``` -- **Transport layer** handles serial I/O, CRC, NG/MIX frame encoding/decoding -- **Command modules** send structured commands and parse responses into dicts -- **USB connections** skip CRC (matching the C client behavior) -- **FPC UART connections** use CRC-16/A with byte-swapped wire format +- **`core/`** is the wire stack: `transport.py` handles serial I/O and NG/MIX frame + encode/decode; command modules send structured commands and parse responses into + dicts. USB connections skip CRC (matching the C client); FPC UART uses CRC-16/A with + byte-swapped wire encoding. +- **`trace/`** parses the firmware trace log and annotates it (per-protocol decoders + + NDEF), used by both sniff and sim. +- **`sniff/`** orchestrates protocol detection and the live `CMD_HF_SNIFF_STREAM` + streaming loop. +- **`sim/`** + **`transponders/`** implement software-defined tags served from a + firmware BigBuf response table, with a WTX relay for real-time Layer-4 crypto. ## Tests @@ -182,4 +501,4 @@ cd pm3py python -m pytest tests/ -v ``` -All tests use mock transports — no hardware required. +All tests use mock transports — **no hardware required**.