Compare commits

..

10 Commits

Author SHA1 Message Date
michael
4be8100cb6 feat: load ndef_text/ndef_uri/ndef_mime in sim REPL mode 2026-03-19 13:09:50 -07:00
michael
cb5d06da14 feat: set_uid() with validation and random generation for 15693 tags
Tag15693.set_uid() now:
- Accepts None (or no args) to generate a random valid UID with the
  correct IC-specific prefix (E0 + mfg + IC type)
- Validates length (must be 8 bytes)
- Warns on prefix mismatch (wrong IC type for product-specific tags)
- Warns on non-NXP manufacturer code
- Warns on missing E0 prefix
2026-03-19 12:56:57 -07:00
michael
06de521d02 fix(fw): don't treat NXP custom commands with OPTION flag as inventory
NXP command 0xB0 with flags 0x26 was misinterpreted as inventory,
corrupting sim state. Custom commands (>= 0xA0) use bit 2 as OPTION.
2026-03-19 12:38:56 -07:00
michael
83c8dd5546 test: comprehensive SniffSession live streaming tests
10 tests covering streaming flags, trace entry parsing, status
transitions, button-toggle lifecycle, BREAK_LOOP, entry accumulation
across pause/resume cycles, and full end-to-end workflow.
2026-03-19 12:34:10 -07:00
michael
e90a907630 feat: rewrite SniffSession with live streaming and REPL auto-load
Replace lightweight async SniffSession with persistent synchronous
session using raw pyserial (mirroring SimSession pattern). Background
reader thread decodes and prints trace frames live as they arrive.

API:
  session = SniffSession.open()
  session.start_15693()          # live streaming (default)
  session.start_15693(live=False) # legacy batch mode
  session.stop()                 # or button press to pause/resume
  session.entries                # all captured frames
  session.clear()

.pythonstartup.py auto-creates session in sniff mode.
2026-03-19 12:34:05 -07:00
michael
580ab2b63f feat(fw): live streaming sniff with button-toggle pause/resume
Add CMD_HF_SNIFF_STREAM (0x0337) and CMD_HF_SNIFF_STATUS (0x0338) for
fire-and-forget trace streaming during sniff. Unified sniff loop uses
the same proven decode path for both streaming and legacy modes —
streaming replaces LogTrace with ring buffer SNIFF_PUSH/SNIFF_FLUSH.

Firmware features:
- Idle flush: sends buffered trace entries over USB when RF is quiet
  for idle_timeout_ms (configurable, default 400ms for 15693)
- Button-toggle: pause/resume sniffing without restarting
- DMA restart on resume for clean decoder state
- BREAK_LOOP support for Python-side session.stop()
- Legacy mode (flags=0) preserved unchanged

Python constants: SNIFF_FLAG_STREAMING, SNIFF_FLAG_BUTTON_TOGGLE,
SNIFF_STATE_STARTED/PAUSED/RESUMED/STOPPED, Cmd.HF_SNIFF_STREAM/STATUS
2026-03-19 12:33:57 -07:00
michael
44f98f137e fix: update firmware submodule — suppress ISODEP spam in 15693 reader
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:50:12 -07:00
michael
874aa688b9 docs: update CLAUDE.md for completed refactor and sim status
- Package structure reflects reality: transponders/ hierarchy,
  sim/ infrastructure files, reader/ scaffold
- Remove worktree references (sim framework merged to master)
- Mark card simulation as working (not "in progress")
- Update firmware submodule (CRC fix for short 15693 frames)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:00:54 -07:00
michael
f294aba9ef fix: pm3 command for mode switching without re-sourcing venv
source activate once, then pm3/pm3 sim/pm3 sniff/pm3 all to launch
REPL with appropriate imports. Uses python -i for reliable startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 21:05:00 -07:00
michael
c1ab980036 refactor: remove all backward-compat shims from sim/
Deletes 24 shim files from sim/. All test imports now point directly
to canonical transponders/ and trace/ paths. sim/ contains only
infrastructure (15 files): frame, memory, transponder ABC, reader ABC,
medium, sim_session, table_compiler, replay, relay, fuzzer, pm3medium,
mcu_bridge, mcu_protocol, dual_session, __init__.

751 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:56:40 -07:00
55 changed files with 953 additions and 389 deletions

View File

@@ -35,6 +35,9 @@ SIM = [
("pm3py.transponders.hf.iso14443a.ndef", "NfcType4Tag"), ("pm3py.transponders.hf.iso14443a.ndef", "NfcType4Tag"),
("pm3py.transponders.hf.iso15693.base", "Tag15693"), ("pm3py.transponders.hf.iso15693.base", "Tag15693"),
("pm3py.transponders.hf.iso15693.type5", "NfcType5Tag"), ("pm3py.transponders.hf.iso15693.type5", "NfcType5Tag"),
("pm3py.transponders.hf.iso15693.type5", "ndef_text"),
("pm3py.transponders.hf.iso15693.type5", "ndef_uri"),
("pm3py.transponders.hf.iso15693.type5", "ndef_mime"),
("pm3py.transponders.hf.iso15693.nxp.nxp_icode", "NxpIcodeTag"), ("pm3py.transponders.hf.iso15693.nxp.nxp_icode", "NxpIcodeTag"),
("pm3py.transponders.hf.iso15693.nxp.icode_slix", "IcodeSlixTag"), ("pm3py.transponders.hf.iso15693.nxp.icode_slix", "IcodeSlixTag"),
("pm3py.transponders.hf.iso15693.nxp.icode_slix2", "IcodeSlix2Tag"), ("pm3py.transponders.hf.iso15693.nxp.icode_slix2", "IcodeSlix2Tag"),
@@ -95,6 +98,22 @@ for module_name, attr_name in imports:
if loaded: if loaded:
print(f"[pm3py:{_MODE}] Loaded: {', '.join(loaded.keys())}") print(f"[pm3py:{_MODE}] Loaded: {', '.join(loaded.keys())}")
if _MODE == "core":
try:
pm3 = Proxmark3.sync()
print()
print("Synchronous connection established")
print()
except Exception as e:
print(f" (Proxmark3 connection failed: {e})")
if _MODE == "sniff":
try:
session = SniffSession.open()
print("Sniff session ready -- session.start_15693()")
except Exception as e:
print(f" (Sniff session failed: {e})")
if failed: if failed:
print(f"\n[pm3py:{_MODE}] Failed:") print(f"\n[pm3py:{_MODE}] Failed:")
for name, (mod, err) in failed.items(): for name, (mod, err) in failed.items():

View File

@@ -37,25 +37,27 @@ pm3py/
format.py # ANSI color formatting, format_sniff_line format.py # ANSI color formatting, format_sniff_line
sniff/ # sniff orchestration — protocol detection, session lifecycle sniff/ # sniff orchestration — protocol detection, session lifecycle
session.py # SniffSession — start/download/decode per protocol session.py # SniffSession — start/download/decode per protocol
sim/ # card simulation framework (~7.8k LOC, 686 tests) sim/ # card simulation infrastructure (19 files)
transponder.py # Transponder ABC, MemoryRegion transponder.py # Transponder ABC, MemoryRegion
medium.py # Medium, SoftwareMedium (RF simulation) medium.py # Medium, SoftwareMedium (RF simulation)
reader.py # Reader ABC, ScriptedReader, InteractiveReader reader.py # Reader ABC, ScriptedReader, InteractiveReader
frame.py # RFFrame (bit/byte level) frame.py # RFFrame (bit/byte level)
iso14443a.py # Tag14443A, Tag14443A_3/4, Reader14443A
iso15693.py # Tag15693, Reader15693
mifare.py # MifareClassicTag + Crypto1
desfire.py # DesfireTag, DesfireReader
type5.py # NfcType5Tag (NDEF on 15693)
nxp_icode.py # NxpIcodeTag base
icode_slix.py # → icode_slix2 → icode3, icode_dna
ntag5_platform.py # → ntag5_switch, ntag5_link → ntag5_boost
sim_session.py # SimSession (table compile + WTX relay) sim_session.py # SimSession (table compile + WTX relay)
dual_session.py # DualInterfaceSession (PM3 RF + MCU I2C)
table_compiler.py # ResponseTable, TableCompiler table_compiler.py # ResponseTable, TableCompiler
trace_fmt.py # TraceFormatter (14443-A + 15693 decoders) trace_fmt.py # TraceFormatter (14443-A + 15693 decoders)
# + lf_base, em4100, hid, t5577, ndef, implants, fuzzer, relay, etc. pm3medium.py # PM3-backed Medium for real hardware
mcu_bridge.py # COBS serial bridge to MCU
mcu_protocol.py # MCU message types and protocol
fuzzer.py # Transponder fuzzer
relay.py # Card relay
replay.py # Trace replay
access_control/ # Wiegand, OSDP
transponders/ # tag/transponder models (extracted from sim/)
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
hf/iso15693/ # Tag15693, NfcType5, NXP ICODE/SLIX2/DNA/NTAG5
lf/ # EM4100, HID, T5577
reader/ # higher-level reader modes by protocol/vendor (scaffold) reader/ # higher-level reader modes by protocol/vendor (scaffold)
transponders/ # tag/transponder models independent of hardware (scaffold)
``` ```
## Client API ## Client API
@@ -97,9 +99,9 @@ pm3.hf.dropfield() # drop field
PWM-capable: Easy = A,B. RDV4 = A,D. PWM-capable: Easy = A,B. RDV4 = A,D.
## Sim framework (worktree `feature/sim-framework`) ## Sim framework
Software-defined transponder/reader simulation framework — 750+ tests. Pure-Python models for ISO 14443-A, 15693, MIFARE Classic, DESFire, JCOP, LF (EM4100, HID, T5577), NDEF, NXP ICODE/SLIX2/DNA/NTAG5, access control (Wiegand, OSDP), implant profiles. See `.worktrees/sim-framework/docs/SIM_FRAMEWORK_STATUS.md` for full status. Software-defined transponder/reader simulation framework — 750+ tests, merged to master. Pure-Python models for ISO 14443-A, 15693, MIFARE Classic, DESFire, JCOP, LF (EM4100, HID, T5577), NDEF, NXP ICODE/SLIX2/DNA/NTAG5, access control (Wiegand, OSDP), implant profiles. Transponder models in `transponders/`, sim infrastructure in `sim/`.
## Table compiler: proprietary command match patterns ## Table compiler: proprietary command match patterns
@@ -116,9 +118,9 @@ For unaddressed commands (`02 AB 04`), mfg code stays → `02 AB 04`. PREFIX mat
**Rule:** All NXP custom command table entries use `match=bytes([flags, cmd_byte])` with `MATCH_PREFIX`. Never include `0x04` in the match. **Rule:** All NXP custom command table entries use `match=bytes([flags, cmd_byte])` with `MATCH_PREFIX`. Never include `0x04` in the match.
## Python-driven card simulation (in progress) ## Python-driven card simulation
Design doc: `docs/PYTHON_SIM_DESIGN.md`. Firmware patch (~320 lines of C) + Python extensions to enable Python-controlled card simulation on unmodified PM3 Easy/RDV4 hardware. Two mechanisms: Design doc: `docs/PYTHON_SIM_DESIGN.md`. Firmware patch in `firmware/` submodule (proxmark3-pm3py) + Python sim framework. 15693 sim fully working — phone reads all blocks, NDEF, NXP custom commands. Two mechanisms:
- **Response table** in BigBuf — pre-compiled by Python, served by firmware at wire speed (86µs FDT for 14443-A Layer 3) - **Response table** in BigBuf — pre-compiled by Python, served by firmware at wire speed (86µs FDT for 14443-A Layer 3)
- **WTX relay** — firmware sends S(WTX) on Layer 4 table miss, relays APDU to Python over USB for real-time crypto (DESFire, JCOP, EMV) - **WTX relay** — firmware sends S(WTX) on Layer 4 table miss, relays APDU to Python over USB for real-time crypto (DESFire, JCOP, EMV)
- **15693 retry relay** — reader retry-based relay for unknown commands - **15693 retry relay** — reader retry-based relay for unknown commands

View File

@@ -1,21 +1,19 @@
# pm3py venv activation with mode-based auto-imports. # pm3py venv activation.
# Usage: source activate — core (Proxmark3, Cmd, etc.) # Usage: source activate
# source activate sim — + SimSession, transponder models
# source activate sniff — + SniffSession, trace decoders
# source activate all — everything
# #
# Then: python — REPL with auto-imports # Then: pm3 — REPL with core imports (Proxmark3, Cmd)
# ipython — IPython with auto-imports (if installed) # pm3 sim — + SimSession, all transponder models
# pm3 sniff — + SniffSession, trace decoders
# pm3 all — everything
_PM3PY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Activate the venv # Activate the venv
source "$(dirname "${BASH_SOURCE[0]}")/.venv/bin/activate" source "${_PM3PY_ROOT}/.venv/bin/activate"
# Set mode and PYTHONSTARTUP # pm3 command — launches REPL with mode
export PM3PY_MODE="${1:-core}" pm3() {
export PYTHONSTARTUP="$(dirname "${BASH_SOURCE[0]}")/.pythonstartup.py" PM3PY_MODE="${1:-core}" python -i "${_PM3PY_ROOT}/.pythonstartup.py"
}
# Update prompt to show mode echo "pm3py env ready. Commands: pm3, pm3 sim, pm3 sniff, pm3 all"
PS1="(pm3py:${PM3PY_MODE}) ${_OLD_VIRTUAL_PS1:-\$ }"
export PS1
echo "pm3py env ready (mode: ${PM3PY_MODE}). Run 'python' for REPL with auto-imports."

View File

@@ -202,6 +202,9 @@ class Cmd(IntEnum):
HF_ISO15693_CSETUID = 0x0316 HF_ISO15693_CSETUID = 0x0316
HF_ISO15693_EML_SETMEM = 0x0331 HF_ISO15693_EML_SETMEM = 0x0331
HF_SNIFF_STREAM = 0x0337
HF_SNIFF_STATUS = 0x0338
# Sim table commands (firmware response table) # Sim table commands (firmware response table)
SIM_TABLE_UPLOAD = 0x0900 SIM_TABLE_UPLOAD = 0x0900
SIM_TABLE_CLEAR = 0x0901 SIM_TABLE_CLEAR = 0x0901
@@ -265,3 +268,14 @@ class Cmd(IntEnum):
FPGAMEM_DOWNLOADED = 0x0803 FPGAMEM_DOWNLOADED = 0x0803
UNKNOWN = 0xFFFF UNKNOWN = 0xFFFF
# Sniff streaming flags
SNIFF_FLAG_STREAMING = 0x01
SNIFF_FLAG_BUTTON_TOGGLE = 0x02
# Sniff states
SNIFF_STATE_STARTED = 0
SNIFF_STATE_PAUSED = 1
SNIFF_STATE_RESUMED = 2
SNIFF_STATE_STOPPED = 3

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.auth_aes import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.auth_password import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso14443a.nxp.desfire import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.lf.em.em4100 import * # noqa: F401,F403

View File

@@ -6,7 +6,7 @@ from typing import Iterator
from bitarray import bitarray from bitarray import bitarray
from .frame import RFFrame from .frame import RFFrame
from .iso14443a import CL1, NVB_SELECT, _compute_bcc from pm3py.transponders.hf.iso14443a.base import CL1, NVB_SELECT, _compute_bcc
class MutationFuzzer: class MutationFuzzer:

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.lf.hid.hid import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.icode3 import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.icode_dna import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.icode_slix import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.implants import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso14443a.base import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.base import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.lf.base import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso14443a.ndef import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import * # noqa: F401,F403

View File

@@ -10,7 +10,7 @@ import serial
from .frame import RFFrame from .frame import RFFrame
from .table_compiler import ResponseTable, TableCompiler from .table_compiler import ResponseTable, TableCompiler
from .trace_fmt import TraceFormatter from pm3py.trace.format import TraceFormatter
from .transponder import Transponder from .transponder import Transponder
from ..core.protocol import Cmd from ..core.protocol import Cmd
from ..core.transport import encode_ng_frame, decode_response_frame, RESP_PREAMBLE_MAGIC, RESP_PREAMBLE_SIZE, RESP_POSTAMBLE_SIZE from ..core.transport import encode_ng_frame, decode_response_frame, RESP_PREAMBLE_MAGIC, RESP_PREAMBLE_SIZE, RESP_POSTAMBLE_SIZE
@@ -184,10 +184,10 @@ class SimSession:
table = ResponseTable(entries=[]) table = ResponseTable(entries=[])
# Check most specific IC first, fall back to base # Check most specific IC first, fall back to base
from .icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
from .icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from .icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
from .ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
if isinstance(tag, Ntag5PlatformTag): if isinstance(tag, Ntag5PlatformTag):
# NTAG 5 Switch/Link/Boost — use DNA compiler (same platform) # NTAG 5 Switch/Link/Boost — use DNA compiler (same platform)
table = TableCompiler.compile_icode_dna(tag) table = TableCompiler.compile_icode_dna(tag)

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.lf.atmel.t5577 import * # noqa: F401,F403

View File

@@ -288,7 +288,7 @@ class TableCompiler:
first_block = 128 + (sector - 32) * 16 first_block = 128 + (sector - 32) * 16
# Pre-pick a tag nonce # Pre-pick a tag nonce
from .crypto1 import Crypto1 from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
crypto = Crypto1(key) crypto = Crypto1(key)
nt = crypto.generate_nonce() nt = crypto.generate_nonce()

View File

@@ -1,4 +0,0 @@
"""Backward compat shim — moved to pm3py.trace."""
from pm3py.trace.decode_iso15 import decode_15693, decode_15693_nxp # noqa: F401
from pm3py.trace.decode_iso14a import decode_14443a # noqa: F401
from pm3py.trace.format import TraceFormatter # noqa: F401

View File

@@ -1,2 +0,0 @@
"""Backward compat shim — moved to pm3py.transponders."""
from pm3py.transponders.hf.iso15693.type5 import * # noqa: F401,F403

View File

@@ -1,75 +1,287 @@
"""Sniff sessions — start/stop/download per protocol.""" """Sniff sessions — live streaming with button-toggle, or legacy batch."""
import struct
import sys import sys
import threading
import time
from ..core.protocol import Cmd, PM3_CMD_DATA_SIZE import serial
from ..core.transport import PM3Transport, encode_ng_frame
from ..core.protocol import (
Cmd, PM3_CMD_DATA_SIZE,
SNIFF_FLAG_STREAMING, SNIFF_FLAG_BUTTON_TOGGLE,
SNIFF_STATE_STARTED, SNIFF_STATE_PAUSED,
SNIFF_STATE_RESUMED, SNIFF_STATE_STOPPED,
)
from ..core.transport import (
encode_ng_frame, decode_response_frame,
RESP_PREAMBLE_MAGIC, RESP_PREAMBLE_SIZE, RESP_POSTAMBLE_SIZE,
)
from ..trace import parse_tracelog from ..trace import parse_tracelog
from ..trace.format import format_sniff_line from ..trace.decode_iso15 import decode_15693
from ..trace.format import TraceFormatter, format_sniff_line
# Default serial port (same as SimSession)
DEFAULT_PORT = "/dev/ttyACM0"
_STATE_NAMES = {
SNIFF_STATE_STARTED: "sniffing",
SNIFF_STATE_PAUSED: "paused",
SNIFF_STATE_RESUMED: "sniffing",
SNIFF_STATE_STOPPED: "stopped",
}
_STATE_ICONS = {
SNIFF_STATE_STARTED: ">>",
SNIFF_STATE_PAUSED: "||",
SNIFF_STATE_RESUMED: ">>",
SNIFF_STATE_STOPPED: "[]",
}
_STATE_LABELS = {
SNIFF_STATE_STARTED: "Started",
SNIFF_STATE_PAUSED: "Paused",
SNIFF_STATE_RESUMED: "Resumed",
SNIFF_STATE_STOPPED: "Stopped",
}
class SniffSession: class SniffSession:
"""Sniff session that manages start/download/decode lifecycle. """Persistent sniff session with live streaming and button-toggle.
Takes a PM3Transport (not the client), since sniff has its own For live mode (default):
lifecycle that doesn't fit the stateless command pattern. session = SniffSession.open()
session.start_15693() # streams trace live, button toggles pause
session.stop() # or let button cycles accumulate
session.entries # all captured frames
For legacy mode:
session.start_15693(live=False) # blocks until button, batch download
""" """
def __init__(self, transport: PM3Transport): def __init__(self, ser):
self._t = transport self._ser = ser
self._active = False
self._entries: list[dict] = []
self._reader_thread: threading.Thread | None = None
self._formatter: TraceFormatter | None = None
self._state = "idle"
async def _sniff_15(self, timeout: float = 60.0) -> dict: @classmethod
"""Start ISO 15693 sniff. Blocks until button press or timeout.""" def open(cls, port: str = DEFAULT_PORT, baudrate: int = 115200) -> "SniffSession":
async with self._t._lock: """Open serial connection to PM3. Auto-detects port if default."""
raw = encode_ng_frame(Cmd.HF_ISO15693_SNIFF) ser = serial.Serial()
await self._t.send_frame(raw) ser.port = port
while True: ser.baudrate = baudrate
resp = await self._t.read_response(timeout=timeout) ser.bytesize = 8
if resp.cmd == Cmd.HF_ISO15693_SNIFF: ser.parity = "N"
return {"status": resp.status} ser.stopbits = 1
ser.timeout = 0.1
ser.xonxoff = False
ser.rtscts = False
ser.dsrdtr = False
ser.open()
async def _download_trace(self) -> list[dict]: ser.reset_input_buffer()
"""Download and parse the trace buffer from the device.""" ser.reset_output_buffer()
raw, trace_len = await self._t.download_bigbuf( time.sleep(0.5)
offset=0, length=PM3_CMD_DATA_SIZE, timeout=4.0) ser.reset_input_buffer()
if trace_len == 0: return cls(ser)
return []
if trace_len > PM3_CMD_DATA_SIZE: def start_15693(self, live: bool = True, idle_timeout_ms: int = 400) -> None:
raw, trace_len = await self._t.download_bigbuf( """Start 15693 sniff session.
offset=0, length=trace_len, timeout=10.0)
return parse_tracelog(raw[:trace_len]) live=True (default): streaming with button-toggle pause/resume.
live=False: legacy BigBuf batch mode, blocks until button exits.
async def iso15(self, timeout: float = 60.0) -> list[dict]:
"""Sniff ISO 15693 traffic, download trace, decode and print.
Blocks until button press or timeout. Then downloads the trace,
decodes each frame, and prints formatted output.
Returns the parsed trace entries.
""" """
import os if self._active:
self.stop()
self._protocol = 0 # 15693
flags = 0
if live:
flags = SNIFF_FLAG_STREAMING | SNIFF_FLAG_BUTTON_TOGGLE
payload = struct.pack("<HB", idle_timeout_ms, flags)
frame = encode_ng_frame(Cmd.HF_ISO15693_SNIFF, payload)
self._ser.write(frame)
if live:
self._formatter = TraceFormatter(
mode="sniff", decoder=decode_15693, crc_len=2)
self._active = True
self._state = "starting"
self._reader_thread = threading.Thread(
target=self._stream_reader, daemon=True)
self._reader_thread.start()
else:
self._legacy_sniff()
def _stream_reader(self) -> None:
"""Background thread: read and print trace frames live."""
buf = bytearray()
while self._active:
try:
chunk = self._ser.read(self._ser.in_waiting or 1)
if not chunk:
continue
buf.extend(chunk)
while self._try_decode_frame(buf):
pass
except serial.SerialException:
break
except Exception:
continue
def _try_decode_frame(self, buf: bytearray) -> bool:
"""Decode one response frame from buf. Returns True if consumed."""
if len(buf) < RESP_PREAMBLE_SIZE + RESP_POSTAMBLE_SIZE:
return False
idx = buf.find(struct.pack("<I", RESP_PREAMBLE_MAGIC))
if idx < 0:
buf[:] = buf[-3:]
return False
if idx > 0:
del buf[:idx]
if len(buf) < RESP_PREAMBLE_SIZE:
return False
length_ng = struct.unpack_from("<H", buf, 4)[0]
payload_len = length_ng & 0x7FFF
frame_len = RESP_PREAMBLE_SIZE + payload_len + RESP_POSTAMBLE_SIZE
if len(buf) < frame_len:
return False
try: try:
width = os.get_terminal_size().columns resp = decode_response_frame(bytes(buf[:frame_len]))
except (OSError, ValueError): del buf[:frame_len]
width = 120 except Exception:
is_tty = sys.stdout.isatty() del buf[:4]
return True
cmd = resp["cmd"]
data = resp.get("data", b"")
if cmd == Cmd.HF_SNIFF_STREAM.value:
self._handle_trace(data)
elif cmd == Cmd.HF_SNIFF_STATUS.value:
self._handle_status(data)
elif cmd == Cmd.HF_ISO15693_SNIFF.value:
self._active = False
return True
def _handle_trace(self, data: bytes) -> None:
"""Parse and print a trace entry from CMD_HF_SNIFF_STREAM."""
if len(data) < 8:
return
protocol, direction, duration, timestamp = struct.unpack_from("<BBHI", data)
payload = data[8:]
entry = {
"protocol": protocol,
"direction": direction,
"duration": duration,
"timestamp": timestamp,
"data": payload,
}
self._entries.append(entry)
if self._formatter:
self._formatter.print(direction, payload)
def _handle_status(self, data: bytes) -> None:
"""Handle a CMD_HF_SNIFF_STATUS state change."""
if len(data) < 3:
return
state, count = struct.unpack_from("<BH", data)
self._state = _STATE_NAMES.get(state, "unknown")
icon = _STATE_ICONS.get(state, "??")
label = _STATE_LABELS.get(state, "Unknown")
count_str = f" ({count} frames)" if count > 0 else ""
msg = f"\n[Snf] {icon} {label}{count_str}\n"
sys.stdout.write(msg)
sys.stdout.flush()
if state == SNIFF_STATE_STOPPED:
self._active = False
def _legacy_sniff(self) -> None:
"""Legacy batch mode: block until button, download BigBuf, decode."""
print("[Snf] Sniffing ISO 15693... press PM3 button to stop.") print("[Snf] Sniffing ISO 15693... press PM3 button to stop.")
result = await self._sniff_15(timeout=timeout)
print(f"[Snf] Sniff ended (status={result['status']})")
print("[Snf] Downloading trace...") # Read frames until we get the sniff completion response
entries = await self._download_trace() buf = bytearray()
while True:
chunk = self._ser.read(self._ser.in_waiting or 1)
if chunk:
buf.extend(chunk)
if not entries: idx = buf.find(struct.pack("<I", RESP_PREAMBLE_MAGIC))
print("[Snf] No trace data captured.") if idx < 0:
return [] continue
if idx > 0:
buf = buf[idx:]
print(f"[Snf] {len(entries)} frames captured:\n") if len(buf) < RESP_PREAMBLE_SIZE:
for entry in entries: continue
line = format_sniff_line(entry, width=width, is_tty=is_tty)
print(line)
return entries length_ng = struct.unpack_from("<H", buf, 4)[0]
payload_len = length_ng & 0x7FFF
frame_len = RESP_PREAMBLE_SIZE + payload_len + RESP_POSTAMBLE_SIZE
if len(buf) < frame_len:
continue
try:
resp = decode_response_frame(bytes(buf[:frame_len]))
buf = buf[frame_len:]
if resp["cmd"] == Cmd.HF_ISO15693_SNIFF.value:
break
except Exception:
buf = buf[4:]
print("[Snf] Sniff ended, downloading trace...")
# Legacy download not fully implemented in SniffSession.
# Use pm3.hf.iso15.sniff_decoded() for legacy batch mode.
print("[Snf] Legacy download not fully implemented in SniffSession.")
print("[Snf] Use pm3.hf.iso15.sniff_decoded() for legacy batch mode.")
def stop(self) -> None:
"""End sniff session from Python (sends BREAK_LOOP)."""
if self._active:
frame = encode_ng_frame(Cmd.BREAK_LOOP, b"")
self._ser.write(frame)
if self._reader_thread:
self._reader_thread.join(timeout=2.0)
self._active = False
self._state = "stopped"
def close(self) -> None:
"""Stop and close serial port."""
if self._active:
self.stop()
self._ser.close()
@property
def state(self) -> str:
"""Current session state: idle, sniffing, paused, stopped."""
return self._state
@property
def entries(self) -> list[dict]:
"""All captured trace entries across pause/resume cycles."""
return list(self._entries)
def clear(self) -> None:
"""Clear accumulated entries."""
self._entries.clear()

View File

@@ -5,7 +5,7 @@ from enum import IntEnum
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.sim.trace_fmt import decode_14443a from pm3py.trace.decode_iso14a import decode_14443a
from pm3py.sim.transponder import Transponder from pm3py.sim.transponder import Transponder
from pm3py.sim.reader import Reader from pm3py.sim.reader import Reader

View File

@@ -7,7 +7,7 @@ from enum import IntEnum
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.sim.trace_fmt import decode_15693 from pm3py.trace.decode_iso15 import decode_15693
from pm3py.sim.transponder import Transponder from pm3py.sim.transponder import Transponder
from pm3py.sim.reader import Reader from pm3py.sim.reader import Reader
@@ -128,6 +128,46 @@ class Tag15693(Transponder):
random_bytes = os.urandom(8 - len(prefix)) random_bytes = os.urandom(8 - len(prefix))
return prefix + random_bytes return prefix + random_bytes
def set_uid(self, uid: bytes | str | None = None) -> None:
"""Change the tag UID. Call sync() to push to firmware.
uid=None generates a random valid UID with the correct IC prefix.
Warns if the UID prefix doesn't match the expected IC type.
"""
import warnings
if uid is None:
self._uid = self._generate_uid()
self._uid_dirty = True
return
uid = self._parse_uid(uid)
if len(uid) != 8:
raise ValueError(f"UID must be 8 bytes, got {len(uid)}")
prefix = self._uid_prefix
if len(prefix) > 2 and uid[:len(prefix)] != prefix:
expected = prefix.hex().upper()
actual = uid[:len(prefix)].hex().upper()
cls_name = type(self).__name__
warnings.warn(
f"{cls_name}: UID prefix {actual} doesn't match expected {expected}. "
f"Phones/readers may not identify the IC type correctly. "
f"Use set_uid() with no args for a valid random UID.",
stacklevel=2,
)
elif uid[:2] != b"\xE0\x04" and uid[:1] == b"\xE0":
warnings.warn(
f"UID manufacturer code {uid[1]:02X} is not NXP (04). "
f"Phones may not recognize this as an NXP tag.",
stacklevel=2,
)
elif uid[:1] != b"\xE0":
warnings.warn(
f"UID must start with E0 (ISO 15693 IC manufacturer prefix). "
f"Got {uid[0]:02X}. This UID will not be recognized by readers.",
stacklevel=2,
)
self._uid = uid
self._uid_dirty = True
def decode_trace(self, direction: int, payload: bytes) -> str | None: def decode_trace(self, direction: int, payload: bytes) -> str | None:
return decode_15693(direction, payload) return decode_15693(direction, payload)

View File

@@ -5,7 +5,7 @@ import os
import struct import struct
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.trace_fmt import decode_15693_nxp from pm3py.trace.decode_iso15 import decode_15693_nxp
from ..type5 import NfcType5Tag from ..type5 import NfcType5Tag
# NXP manufacturer code # NXP manufacturer code

View File

@@ -5,7 +5,7 @@ from bitarray import bitarray
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso14443a import ( from pm3py.transponders.hf.iso14443a.base import (
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A,
State14443A, REQA, WUPA, HLTA, CL1, CL2, CL3, State14443A, REQA, WUPA, HLTA, CL1, CL2, CL3,
) )

View File

@@ -4,7 +4,7 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso15693 import Tag15693, Reader15693, State15693 from pm3py.transponders.hf.iso15693.base import Tag15693, Reader15693, State15693
def run(coro): def run(coro):

View File

@@ -4,7 +4,7 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso14443a import Tag14443A_3, Reader14443A from pm3py.transponders.hf.iso14443a.base import Tag14443A_3, Reader14443A
from pm3py.sim.fuzzer import MutationFuzzer, GrammarFuzzer from pm3py.sim.fuzzer import MutationFuzzer, GrammarFuzzer
from pm3py.sim.relay import RelayTransponder, MitMProxy from pm3py.sim.relay import RelayTransponder, MitMProxy
from pm3py.sim.replay import TraceRecorder, TraceReplayer, TraceEntry from pm3py.sim.replay import TraceRecorder, TraceReplayer, TraceEntry

View File

@@ -15,8 +15,8 @@ def run(coro):
class TestTam1: class TestTam1:
def test_challenge_readbuffer(self): def test_challenge_readbuffer(self):
"""Full TAM1 flow: CHALLENGE computes TResponse, READBUFFER returns it.""" """Full TAM1 flow: CHALLENGE computes TResponse, READBUFFER returns it."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
key = b"\xAA" * 16 key = b"\xAA" * 16
tag = IcodeDnaTag(aes_keys=[key, None, None, None]) tag = IcodeDnaTag(aes_keys=[key, None, None, None])
@@ -50,8 +50,8 @@ class TestTam1:
def test_challenge_key1(self): def test_challenge_key1(self):
"""TAM1 with key slot 1.""" """TAM1 with key slot 1."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
key = b"\xCC" * 16 key = b"\xCC" * 16
tag = IcodeDnaTag(aes_keys=[None, key, None, None]) tag = IcodeDnaTag(aes_keys=[None, key, None, None])
@@ -75,7 +75,7 @@ class TestTam1:
def test_challenge_inactive_key_silent(self): def test_challenge_inactive_key_silent(self):
"""CHALLENGE with inactive key: tag stays silent, READBUFFER returns nothing.""" """CHALLENGE with inactive key: tag stays silent, READBUFFER returns nothing."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag(aes_keys=[b"\xBB" * 16, None, None, None]) tag = IcodeDnaTag(aes_keys=[b"\xBB" * 16, None, None, None])
# Key header NOT active (default 0x81) # Key header NOT active (default 0x81)
@@ -92,7 +92,7 @@ class TestTam1:
def test_readbuffer_without_challenge(self): def test_readbuffer_without_challenge(self):
"""READBUFFER without prior CHALLENGE returns nothing.""" """READBUFFER without prior CHALLENGE returns nothing."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
@@ -103,8 +103,8 @@ class TestTam1:
def test_readbuffer_one_shot(self): def test_readbuffer_one_shot(self):
"""TResponse is cleared after first READBUFFER (one-shot).""" """TResponse is cleared after first READBUFFER (one-shot)."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
key = b"\xDD" * 16 key = b"\xDD" * 16
tag = IcodeDnaTag(aes_keys=[key, None, None, None]) tag = IcodeDnaTag(aes_keys=[key, None, None, None])
@@ -126,8 +126,8 @@ class TestTam1:
def test_challenge_no_key_in_slot(self): def test_challenge_no_key_in_slot(self):
"""CHALLENGE with None key slot stays silent.""" """CHALLENGE with None key slot stays silent."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
tag = IcodeDnaTag(aes_keys=[None, None, None, None]) tag = IcodeDnaTag(aes_keys=[None, None, None, None])
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
@@ -143,8 +143,8 @@ class TestTam1:
def test_challenge_invalid_key_id(self): def test_challenge_invalid_key_id(self):
"""CHALLENGE with key_id > 3 stays silent.""" """CHALLENGE with key_id > 3 stays silent."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None]) tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None])
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
@@ -156,8 +156,8 @@ class TestTam1:
def test_challenge_wrong_auth_method(self): def test_challenge_wrong_auth_method(self):
"""CHALLENGE with AuthMethod != 0x00 (not TAM1) stays silent.""" """CHALLENGE with AuthMethod != 0x00 (not TAM1) stays silent."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None]) tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None])
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
@@ -177,8 +177,8 @@ class TestMam:
"""Tests for MAM1/MAM2 mutual authentication via AUTHENTICATE (0x35).""" """Tests for MAM1/MAM2 mutual authentication via AUTHENTICATE (0x35)."""
def _make_tag(self, key=b"\xAA" * 16, key_slot=0): def _make_tag(self, key=b"\xAA" * 16, key_slot=0):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
keys = [None] * 4 keys = [None] * 4
keys[key_slot] = key keys[key_slot] = key
@@ -293,7 +293,7 @@ class TestMam:
def test_mam1_inactive_key_error(self): def test_mam1_inactive_key_error(self):
"""MAM1 with inactive key returns error.""" """MAM1 with inactive key returns error."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None]) tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None])
run(tag.power_on()) run(tag.power_on())
@@ -366,7 +366,7 @@ class TestMam:
def test_mam2_purpose_enable_privacy(self): def test_mam2_purpose_enable_privacy(self):
"""MAM2 with purpose=0x09 sets privacy mode.""" """MAM2 with purpose=0x09 sets privacy mode."""
from pm3py.sim.auth_aes import PRIV_PRIVACY from pm3py.transponders.hf.iso15693.nxp.auth_aes import PRIV_PRIVACY
key = b"\xAA" * 16 key = b"\xAA" * 16
tag = self._make_tag(key) tag = self._make_tag(key)
@@ -380,7 +380,7 @@ class TestMam:
def test_mam2_purpose_disable_privacy(self): def test_mam2_purpose_disable_privacy(self):
"""MAM2 with purpose=0x0A clears privacy mode.""" """MAM2 with purpose=0x0A clears privacy mode."""
from pm3py.sim.auth_aes import PRIV_PRIVACY from pm3py.transponders.hf.iso15693.nxp.auth_aes import PRIV_PRIVACY
key = b"\xAA" * 16 key = b"\xAA" * 16
tag = self._make_tag(key) tag = self._make_tag(key)
@@ -395,7 +395,7 @@ class TestMam:
def test_mam2_purpose_destroy(self): def test_mam2_purpose_destroy(self):
"""MAM2 with purpose=0x0B sets destroyed.""" """MAM2 with purpose=0x0B sets destroyed."""
from pm3py.sim.auth_aes import PRIV_DESTROY from pm3py.transponders.hf.iso15693.nxp.auth_aes import PRIV_DESTROY
key = b"\xAA" * 16 key = b"\xAA" * 16
tag = self._make_tag(key) tag = self._make_tag(key)

View File

@@ -4,8 +4,8 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso14443a import _compute_bcc from pm3py.transponders.hf.iso14443a.base import _compute_bcc
from pm3py.sim.desfire import DesfireTag, DesfireReader from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
def run(coro): def run(coro):

View File

@@ -4,7 +4,7 @@ import pytest
from pm3py.sim.dual_session import DualInterfaceSession from pm3py.sim.dual_session import DualInterfaceSession
from pm3py.sim.mcu_bridge import McuBridge from pm3py.sim.mcu_bridge import McuBridge
from pm3py.sim.mcu_protocol import MsgType, build_frame, cobs_decode, parse_frame from pm3py.sim.mcu_protocol import MsgType, build_frame, cobs_decode, parse_frame
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
class MockSimSession: class MockSimSession:

View File

@@ -4,7 +4,7 @@ import struct
import pytest import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.nxp_icode import ( from pm3py.transponders.hf.iso15693.nxp.nxp_icode import (
CMD_SET_EAS, CMD_READ_CONFIG, CMD_WRITE_CONFIG, CMD_READ_TT, CMD_SET_EAS, CMD_READ_CONFIG, CMD_WRITE_CONFIG, CMD_READ_TT,
) )
@@ -41,20 +41,20 @@ PWD_CONFIG = 0x20
class TestIcode3Basics: class TestIcode3Basics:
def test_import(self): def test_import(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
def test_inherits_slix2(self): def test_inherits_slix2(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
assert issubclass(Icode3Tag, IcodeSlix2Tag) assert issubclass(Icode3Tag, IcodeSlix2Tag)
def test_default_76_blocks(self): def test_default_76_blocks(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
assert tag._num_blocks == 76 assert tag._num_blocks == 76
def test_responds_to_inventory(self): def test_responds_to_inventory(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
inv = bytes([0x26, 0x01, 0x00]) inv = bytes([0x26, 0x01, 0x00])
@@ -68,14 +68,14 @@ class TestIcode3Basics:
class TestIcode3ConfigPassword: class TestIcode3ConfigPassword:
def test_config_password_auth(self): def test_config_password_auth(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
config_password=0xDEADBEEF) config_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
_authenticate(tag, PWD_CONFIG, 0xDEADBEEF) _authenticate(tag, PWD_CONFIG, 0xDEADBEEF)
def test_config_password_wrong(self): def test_config_password_wrong(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
config_password=0xDEADBEEF) config_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -91,7 +91,7 @@ class TestIcode3ConfigPassword:
class TestIcode3InheritsSlix2: class TestIcode3InheritsSlix2:
def test_eas(self): def test_eas(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, CMD_SET_EAS, 0x04]) cmd = bytes([0x02, CMD_SET_EAS, 0x04])
@@ -100,7 +100,7 @@ class TestIcode3InheritsSlix2:
assert tag.eas_enabled assert tag.eas_enabled
def test_privacy(self): def test_privacy(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
privacy_password=0xAAAAAAAA) privacy_password=0xAAAAAAAA)
run(tag.power_on()) run(tag.power_on())
@@ -108,7 +108,7 @@ class TestIcode3InheritsSlix2:
assert tag.privacy_mode assert tag.privacy_mode
def test_destroy(self): def test_destroy(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
destroy_password=0xBBBBBBBB) destroy_password=0xBBBBBBBB)
run(tag.power_on()) run(tag.power_on())
@@ -125,7 +125,7 @@ class TestIcode3InheritsSlix2:
class TestIcode3Counter: class TestIcode3Counter:
def test_counter_increments_24bit(self): def test_counter_increments_24bit(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -141,7 +141,7 @@ class TestIcode3Counter:
assert counter == 1 assert counter == 1
def test_counter_accumulates(self): def test_counter_accumulates(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -155,7 +155,7 @@ class TestIcode3Counter:
assert counter == 10 assert counter == 10
def test_counter_saturates_at_24bit(self): def test_counter_saturates_at_24bit(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -177,7 +177,7 @@ class TestIcode3Counter:
assert counter == 0xFFFFFF assert counter == 0xFFFFFF
def test_normal_blocks_still_overwrite(self): def test_normal_blocks_still_overwrite(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -198,7 +198,7 @@ class TestIcode3Counter:
class TestIcode3Config: class TestIcode3Config:
def test_read_config(self): def test_read_config(self):
"""READ CONFIG returns config memory blocks.""" """READ CONFIG returns config memory blocks."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -211,7 +211,7 @@ class TestIcode3Config:
def test_write_config(self): def test_write_config(self):
"""WRITE CONFIG writes to config memory.""" """WRITE CONFIG writes to config memory."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -228,7 +228,7 @@ class TestIcode3Config:
def test_read_config_passwords_masked(self): def test_read_config_passwords_masked(self):
"""READ CONFIG masks password blocks with 0x00.""" """READ CONFIG masks password blocks with 0x00."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
read_password=0xDEADBEEF) read_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -242,7 +242,7 @@ class TestIcode3Config:
def test_write_config_requires_auth_when_protected(self): def test_write_config_requires_auth_when_protected(self):
"""WRITE CONFIG fails without config password when protected.""" """WRITE CONFIG fails without config password when protected."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
config_password=0xAABBCCDD) config_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -254,7 +254,7 @@ class TestIcode3Config:
def test_read_config_multiple_blocks(self): def test_read_config_multiple_blocks(self):
"""READ CONFIG can read multiple blocks.""" """READ CONFIG can read multiple blocks."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -273,7 +273,7 @@ class TestIcode3Config:
class TestIcode3TagTamper: class TestIcode3TagTamper:
def test_read_tt_closed(self): def test_read_tt_closed(self):
"""READ TT returns closed status by default.""" """READ TT returns closed status by default."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=True) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=True)
run(tag.power_on()) run(tag.power_on())
@@ -287,7 +287,7 @@ class TestIcode3TagTamper:
def test_read_tt_open(self): def test_read_tt_open(self):
"""READ TT returns open status when tampered.""" """READ TT returns open status when tampered."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=True) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=True)
run(tag.power_on()) run(tag.power_on())
tag._tt_status_actual = 0x4F # 'O' = Open tag._tt_status_actual = 0x4F # 'O' = Open
@@ -298,7 +298,7 @@ class TestIcode3TagTamper:
def test_read_tt_not_supported_without_flag(self): def test_read_tt_not_supported_without_flag(self):
"""READ TT returns error when tag_tamper=False.""" """READ TT returns error when tag_tamper=False."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=False) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=False)
run(tag.power_on()) run(tag.power_on())
@@ -314,7 +314,7 @@ class TestIcode3TagTamper:
class TestIcode3SystemInfo: class TestIcode3SystemInfo:
def test_feature_flags(self): def test_feature_flags(self):
"""GET NXP SYSTEM INFO returns ICODE 3 feature flags.""" """GET NXP SYSTEM INFO returns ICODE 3 feature flags."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -338,7 +338,7 @@ class TestIcode3SystemInfo:
class TestIcode3NfcMirror: class TestIcode3NfcMirror:
def test_uid_mirror(self): def test_uid_mirror(self):
"""UID mirror overlays 16 ASCII hex bytes into user memory on read.""" """UID mirror overlays 16 ASCII hex bytes into user memory on read."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06") tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
@@ -354,7 +354,7 @@ class TestIcode3NfcMirror:
def test_uid_counter_mirror(self): def test_uid_counter_mirror(self):
"""UID + counter mirror: 16 bytes UID + 'x' + 6 bytes counter.""" """UID + counter mirror: 16 bytes UID + 'x' + 6 bytes counter."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06") tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
@@ -380,7 +380,7 @@ class TestIcode3NfcMirror:
def test_mirror_disabled(self): def test_mirror_disabled(self):
"""With mirror disabled (sel=0), reads return physical memory.""" """With mirror disabled (sel=0), reads return physical memory."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06") tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
@@ -397,7 +397,7 @@ class TestIcode3NfcMirror:
def test_mirror_with_byte_offset(self): def test_mirror_with_byte_offset(self):
"""Mirror starting at byte 2 within a block.""" """Mirror starting at byte 2 within a block."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06") tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
@@ -417,7 +417,7 @@ class TestIcode3NfcMirror:
def test_mirror_outside_block_returns_physical(self): def test_mirror_outside_block_returns_physical(self):
"""Blocks before mirror region return physical memory.""" """Blocks before mirror region return physical memory."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06") tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
@@ -438,7 +438,7 @@ class TestIcode3NfcMirror:
class TestIcode3PrivacyMode2: class TestIcode3PrivacyMode2:
def test_privacy_mode2_inventory_zeroed_uid(self): def test_privacy_mode2_inventory_zeroed_uid(self):
"""In privacy mode 2, inventory returns zeroed UID.""" """In privacy mode 2, inventory returns zeroed UID."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06", tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF) privacy_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -455,7 +455,7 @@ class TestIcode3PrivacyMode2:
def test_privacy_mode2_allows_reads(self): def test_privacy_mode2_allows_reads(self):
"""Privacy mode 2 allows READ SINGLE BLOCK.""" """Privacy mode 2 allows READ SINGLE BLOCK."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06", tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF) privacy_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -475,7 +475,7 @@ class TestIcode3PrivacyMode2:
def test_privacy_mode1_blocks_reads(self): def test_privacy_mode1_blocks_reads(self):
"""Privacy mode 1 blocks everything except GET_RANDOM/SET_PASSWORD.""" """Privacy mode 1 blocks everything except GET_RANDOM/SET_PASSWORD."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06", tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF) privacy_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -489,7 +489,7 @@ class TestIcode3PrivacyMode2:
def test_pick_random_id(self): def test_pick_random_id(self):
"""PICK RANDOM ID (0xC2) generates random UID for inventory.""" """PICK RANDOM ID (0xC2) generates random UID for inventory."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06", tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF) privacy_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -514,7 +514,7 @@ class TestIcode3PrivacyMode2:
def test_pick_random_id_fails_outside_privacy(self): def test_pick_random_id_fails_outside_privacy(self):
"""PICK RANDOM ID fails when not in privacy mode.""" """PICK RANDOM ID fails when not in privacy mode."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06") tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
@@ -530,7 +530,7 @@ class TestIcode3PrivacyMode2:
class TestIcode3Signature48: class TestIcode3Signature48:
def test_default_32_byte_signature(self): def test_default_32_byte_signature(self):
"""Default READ SIGNATURE returns 32 bytes (1 flag + 32 sig = 33).""" """Default READ SIGNATURE returns 32 bytes (1 flag + 32 sig = 33)."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -542,7 +542,7 @@ class TestIcode3Signature48:
def test_48_byte_signature_mode(self): def test_48_byte_signature_mode(self):
"""With 48-byte mode enabled, READ SIGNATURE returns 49 bytes.""" """With 48-byte mode enabled, READ SIGNATURE returns 49 bytes."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
sig48 = bytes(range(48)) sig48 = bytes(range(48))
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48)
run(tag.power_on()) run(tag.power_on())
@@ -557,7 +557,7 @@ class TestIcode3Signature48:
def test_48_byte_mode_disabled_truncates(self): def test_48_byte_mode_disabled_truncates(self):
"""With 48-byte mode disabled, READ SIGNATURE returns first 32 bytes.""" """With 48-byte mode disabled, READ SIGNATURE returns first 32 bytes."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
sig48 = bytes(range(48)) sig48 = bytes(range(48))
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48)
run(tag.power_on()) run(tag.power_on())
@@ -574,7 +574,7 @@ class TestIcode3Signature48:
class TestIcode3CounterPreset: class TestIcode3CounterPreset:
def test_value_0x000001_increments(self): def test_value_0x000001_increments(self):
"""Writing 0x000001 to block 75 increments counter by 1.""" """Writing 0x000001 to block 75 increments counter by 1."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -590,7 +590,7 @@ class TestIcode3CounterPreset:
def test_value_not_0x000001_presets(self): def test_value_not_0x000001_presets(self):
"""Writing != 0x000001 to block 75 presets (overwrites) counter.""" """Writing != 0x000001 to block 75 presets (overwrites) counter."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -610,7 +610,7 @@ class TestIcode3CounterPreset:
def test_0x000001_twice_increments_twice(self): def test_0x000001_twice_increments_twice(self):
"""Writing 0x000001 twice increments counter from 0 → 1 → 2.""" """Writing 0x000001 twice increments counter from 0 → 1 → 2."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())
@@ -625,7 +625,7 @@ class TestIcode3CounterPreset:
def test_prot_byte_written(self): def test_prot_byte_written(self):
"""PROT byte (data[3]) is written to memory alongside counter.""" """PROT byte (data[3]) is written to memory alongside counter."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4)) tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on()) run(tag.power_on())

View File

@@ -4,7 +4,7 @@ import struct
import pytest import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.nxp_icode import ( from pm3py.transponders.hf.iso15693.nxp.nxp_icode import (
CMD_SET_EAS, CMD_RESET_EAS, CMD_LOCK_EAS, CMD_EAS_ALARM, CMD_SET_EAS, CMD_RESET_EAS, CMD_LOCK_EAS, CMD_EAS_ALARM,
CMD_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID, CMD_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID,
CMD_INVENTORY_READ, CMD_FAST_INVENTORY_READ, CMD_INVENTORY_READ, CMD_FAST_INVENTORY_READ,
@@ -45,23 +45,23 @@ PWD_EAS_AFI = 0x10
class TestIcodeSlixBasics: class TestIcodeSlixBasics:
def test_import(self): def test_import(self):
"""IcodeSlixTag can be imported.""" """IcodeSlixTag can be imported."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
def test_inherits_nxp_icode(self): def test_inherits_nxp_icode(self):
"""SLIX inherits from NxpIcodeTag.""" """SLIX inherits from NxpIcodeTag."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
from pm3py.sim.nxp_icode import NxpIcodeTag from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
assert issubclass(IcodeSlixTag, NxpIcodeTag) assert issubclass(IcodeSlixTag, NxpIcodeTag)
def test_default_28_blocks(self): def test_default_28_blocks(self):
"""SLIX defaults to 28 blocks × 4 bytes.""" """SLIX defaults to 28 blocks × 4 bytes."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
assert tag._num_blocks == 28 assert tag._num_blocks == 28
assert tag._block_size == 4 assert tag._block_size == 4
def test_responds_to_inventory(self): def test_responds_to_inventory(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
inv = bytes([0x26, 0x01, 0x00]) inv = bytes([0x26, 0x01, 0x00])
@@ -69,7 +69,7 @@ class TestIcodeSlixBasics:
assert resp is not None assert resp is not None
def test_read_write_block(self): def test_read_write_block(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
# Write block 5 # Write block 5
@@ -89,7 +89,7 @@ class TestIcodeSlixBasics:
class TestIcodeSlixPassword: class TestIcodeSlixPassword:
def test_set_password_eas_afi(self): def test_set_password_eas_afi(self):
"""SET_PASSWORD with correct EAS/AFI password succeeds.""" """SET_PASSWORD with correct EAS/AFI password succeeds."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4), tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -97,7 +97,7 @@ class TestIcodeSlixPassword:
def test_set_password_wrong(self): def test_set_password_wrong(self):
"""SET_PASSWORD with wrong password fails.""" """SET_PASSWORD with wrong password fails."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4), tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -108,7 +108,7 @@ class TestIcodeSlixPassword:
def test_write_password(self): def test_write_password(self):
"""WRITE_PASSWORD changes the EAS/AFI password.""" """WRITE_PASSWORD changes the EAS/AFI password."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4), tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -122,7 +122,7 @@ class TestIcodeSlixPassword:
def test_lock_password(self): def test_lock_password(self):
"""LOCK_PASSWORD prevents WRITE_PASSWORD.""" """LOCK_PASSWORD prevents WRITE_PASSWORD."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4), tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -139,7 +139,7 @@ class TestIcodeSlixPassword:
def test_no_privacy_password(self): def test_no_privacy_password(self):
"""SLIX has no privacy — only EAS/AFI password accepted.""" """SLIX has no privacy — only EAS/AFI password accepted."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4), tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -156,7 +156,7 @@ class TestIcodeSlixPassword:
class TestIcodeSlixEas: class TestIcodeSlixEas:
def test_set_eas(self): def test_set_eas(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, CMD_SET_EAS, 0x04]) cmd = bytes([0x02, CMD_SET_EAS, 0x04])
@@ -165,7 +165,7 @@ class TestIcodeSlixEas:
assert tag.eas_enabled assert tag.eas_enabled
def test_reset_eas(self): def test_reset_eas(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
tag.set_eas(True) tag.set_eas(True)
@@ -175,7 +175,7 @@ class TestIcodeSlixEas:
assert not tag.eas_enabled assert not tag.eas_enabled
def test_eas_alarm(self): def test_eas_alarm(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
tag.set_eas(True) tag.set_eas(True)
@@ -186,7 +186,7 @@ class TestIcodeSlixEas:
assert len(resp.data) >= 33 assert len(resp.data) >= 33
def test_eas_alarm_silent_when_disabled(self): def test_eas_alarm_silent_when_disabled(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04]) cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
@@ -194,7 +194,7 @@ class TestIcodeSlixEas:
assert resp is None assert resp is None
def test_lock_eas(self): def test_lock_eas(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
tag.set_eas(True) tag.set_eas(True)
@@ -207,7 +207,7 @@ class TestIcodeSlixEas:
assert resp.data[0] & 0x01 assert resp.data[0] & 0x01
def test_password_protect_eas_afi(self): def test_password_protect_eas_afi(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4), tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -219,7 +219,7 @@ class TestIcodeSlixEas:
def test_write_eas_id(self): def test_write_eas_id(self):
"""WRITE EAS ID is SLIX2-only (EAS Selective per AN11809).""" """WRITE EAS ID is SLIX2-only (EAS Selective per AN11809)."""
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD) eas_afi_password=0xAABBCCDD)
run(tag.power_on()) run(tag.power_on())
@@ -231,7 +231,7 @@ class TestIcodeSlixEas:
def test_slix_rejects_write_eas_id(self): def test_slix_rejects_write_eas_id(self):
"""SLIX (SL2S2002) does not support WRITE EAS ID (0xA7).""" """SLIX (SL2S2002) does not support WRITE EAS ID (0xA7)."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12]) cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
@@ -245,7 +245,7 @@ class TestIcodeSlixEas:
class TestIcodeSlixInventoryRead: class TestIcodeSlixInventoryRead:
def test_inventory_read(self): def test_inventory_read(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06") tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x06, CMD_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00]) cmd = bytes([0x06, CMD_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
@@ -254,7 +254,7 @@ class TestIcodeSlixInventoryRead:
assert resp.data[0] == 0x00 assert resp.data[0] == 0x00
def test_fast_inventory_read(self): def test_fast_inventory_read(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06") tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06")
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x06, CMD_FAST_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00]) cmd = bytes([0x06, CMD_FAST_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
@@ -271,12 +271,12 @@ class TestSlix2StillWorks:
"""Verify SLIX2 inherits from SLIX and retains all features.""" """Verify SLIX2 inherits from SLIX and retains all features."""
def test_slix2_inherits_slix(self): def test_slix2_inherits_slix(self):
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
assert issubclass(IcodeSlix2Tag, IcodeSlixTag) assert issubclass(IcodeSlix2Tag, IcodeSlixTag)
def test_slix2_privacy_still_works(self): def test_slix2_privacy_still_works(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF) privacy_password=0xDEADBEEF)
run(tag.power_on()) run(tag.power_on())
@@ -284,7 +284,7 @@ class TestSlix2StillWorks:
assert tag.privacy_mode assert tag.privacy_mode
def test_slix2_destroy_still_works(self): def test_slix2_destroy_still_works(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
destroy_password=0xDEAD1234) destroy_password=0xDEAD1234)
run(tag.power_on()) run(tag.power_on())
@@ -298,7 +298,7 @@ class TestSlix2StillWorks:
def test_slix2_eas_inherited_from_slix(self): def test_slix2_eas_inherited_from_slix(self):
"""SLIX2 EAS works via inheritance from SLIX.""" """SLIX2 EAS works via inheritance from SLIX."""
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5)) tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, CMD_SET_EAS, 0x04]) cmd = bytes([0x02, CMD_SET_EAS, 0x04])
@@ -314,7 +314,7 @@ class TestSlix2StillWorks:
class TestSlixLacksPrivacy: class TestSlixLacksPrivacy:
def test_no_enable_privacy(self): def test_no_enable_privacy(self):
"""SLIX does not respond to ENABLE PRIVACY (0xBA).""" """SLIX does not respond to ENABLE PRIVACY (0xBA)."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xBA, 0x04, 0x00, 0x00, 0x00, 0x00]) cmd = bytes([0x02, 0xBA, 0x04, 0x00, 0x00, 0x00, 0x00])
@@ -324,7 +324,7 @@ class TestSlixLacksPrivacy:
def test_no_destroy(self): def test_no_destroy(self):
"""SLIX does not respond to DESTROY (0xB9).""" """SLIX does not respond to DESTROY (0xB9)."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xB9, 0x04, 0x00, 0x00, 0x00, 0x00]) cmd = bytes([0x02, 0xB9, 0x04, 0x00, 0x00, 0x00, 0x00])
@@ -333,7 +333,7 @@ class TestSlixLacksPrivacy:
def test_no_protect_page(self): def test_no_protect_page(self):
"""SLIX does not respond to PROTECT PAGE (0xB6).""" """SLIX does not respond to PROTECT PAGE (0xB6)."""
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4)) tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01]) cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01])

View File

@@ -3,12 +3,12 @@ import asyncio
import pytest import pytest
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.implants import ( from pm3py.transponders.implants import (
xEM, xNT, xM1, FlexDF, NExT, xEM, xNT, xM1, FlexDF, NExT,
MagicMifareClassicTag, MagicMifareClassicTag,
) )
from pm3py.sim.em4100 import EM4100Reader from pm3py.transponders.lf.em.em4100 import EM4100Reader
from pm3py.sim.iso14443a import Reader14443A, _compute_bcc from pm3py.transponders.hf.iso14443a.base import Reader14443A, _compute_bcc
def run(coro): def run(coro):
@@ -36,7 +36,7 @@ class TestXNT:
assert tag._total_pages >= 45 # NTAG213+ size assert tag._total_pages >= 45 # NTAG213+ size
def test_is_type2_tag(self): def test_is_type2_tag(self):
from pm3py.sim.ndef import NfcType2Tag from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag
tag = xNT() tag = xNT()
assert isinstance(tag, NfcType2Tag) assert isinstance(tag, NfcType2Tag)
@@ -77,7 +77,7 @@ class TestMagicMifareClassic:
class TestFlexDF: class TestFlexDF:
def test_creates_desfire(self): def test_creates_desfire(self):
from pm3py.sim.desfire import DesfireTag from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag
tag = FlexDF() tag = FlexDF()
assert isinstance(tag, DesfireTag) assert isinstance(tag, DesfireTag)
@@ -92,7 +92,7 @@ class TestNExT:
# LF side is T5577 configured as EM4100 # LF side is T5577 configured as EM4100
assert lf.blocks[0] != 0 assert lf.blocks[0] != 0
# HF side is NfcType2Tag (xNT) # HF side is NfcType2Tag (xNT)
from pm3py.sim.ndef import NfcType2Tag from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag
assert isinstance(hf, NfcType2Tag) assert isinstance(hf, NfcType2Tag)
def test_both_sides_work(self): def test_both_sides_work(self):

View File

@@ -4,10 +4,10 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.lf_base import TagLF, ReaderLF, Modulation from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation
from pm3py.sim.em4100 import EM4100Tag, EM4100Reader from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader
from pm3py.sim.hid import HIDProxTag, HIDReader from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
from pm3py.sim.t5577 import T5577Tag, T5577Reader from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader
def run(coro): def run(coro):

View File

@@ -140,14 +140,14 @@ class TestTransponderRegions:
class TestTag15693Regions: class TestTag15693Regions:
def test_has_user_region(self): def test_has_user_region(self):
from pm3py.sim.iso15693 import Tag15693 from pm3py.transponders.hf.iso15693.base import Tag15693
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28) tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28)
assert "user" in tag.regions assert "user" in tag.regions
assert tag.regions["user"].block_size == 4 assert tag.regions["user"].block_size == 4
assert tag.regions["user"].eml_offset == 175 assert tag.regions["user"].eml_offset == 175
def test_memory_property_aliases_region(self): def test_memory_property_aliases_region(self):
from pm3py.sim.iso15693 import Tag15693 from pm3py.transponders.hf.iso15693.base import Tag15693
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28) tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28)
tag._memory[0:4] = b"\xDE\xAD\xBE\xEF" tag._memory[0:4] = b"\xDE\xAD\xBE\xEF"
assert tag.regions["user"].data[0:4] == b"\xDE\xAD\xBE\xEF" assert tag.regions["user"].data[0:4] == b"\xDE\xAD\xBE\xEF"
@@ -156,7 +156,7 @@ class TestTag15693Regions:
class TestIcodeSlix2AccessMap: class TestIcodeSlix2AccessMap:
def test_protection_pointer_sets_access_map(self): def test_protection_pointer_sets_access_map(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=4, tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=4,
read_password=0xAABBCCDD) read_password=0xAABBCCDD)
user = tag.regions["user"] user = tag.regions["user"]
@@ -166,7 +166,7 @@ class TestIcodeSlix2AccessMap:
assert user.access_for_block(4).read_key == 1 assert user.access_for_block(4).read_key == 1
def test_no_protection_pointer_all_open(self): def test_no_protection_pointer_all_open(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5)) tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
user = tag.regions["user"] user = tag.regions["user"]
assert user.access_for_block(0).read == "open" assert user.access_for_block(0).read == "open"
@@ -177,7 +177,7 @@ class TestIcodeSlix2AccessMap:
class TestCompileFromRegions: class TestCompileFromRegions:
def test_open_blocks_have_data_entries(self): def test_open_blocks_have_data_entries(self):
from pm3py.sim.iso15693 import Tag15693 from pm3py.transponders.hf.iso15693.base import Tag15693
from pm3py.sim.table_compiler import TableCompiler from pm3py.sim.table_compiler import TableCompiler
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=4) tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=4)
tag._memory[0:4] = b"\xDE\xAD\xBE\xEF" tag._memory[0:4] = b"\xDE\xAD\xBE\xEF"
@@ -187,7 +187,7 @@ class TestCompileFromRegions:
assert entry.response[1:5] == b"\xDE\xAD\xBE\xEF" assert entry.response[1:5] == b"\xDE\xAD\xBE\xEF"
def test_protected_blocks_without_auth(self): def test_protected_blocks_without_auth(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
from pm3py.sim.table_compiler import TableCompiler from pm3py.sim.table_compiler import TableCompiler
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2, tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2,
read_password=0xAABBCCDD, block_size=4, num_blocks=4) read_password=0xAABBCCDD, block_size=4, num_blocks=4)
@@ -198,7 +198,7 @@ class TestCompileFromRegions:
assert table.lookup(bytes([0x02, 0x20, 0x02])) is None assert table.lookup(bytes([0x02, 0x20, 0x02])) is None
def test_protected_blocks_with_auth(self): def test_protected_blocks_with_auth(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag, PWD_READ from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag, PWD_READ
from pm3py.sim.table_compiler import TableCompiler from pm3py.sim.table_compiler import TableCompiler
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2, tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2,
read_password=0xAABBCCDD, block_size=4, num_blocks=4) read_password=0xAABBCCDD, block_size=4, num_blocks=4)

View File

@@ -5,9 +5,9 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.crypto1 import Crypto1 from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
from pm3py.sim.mifare import MifareClassicTag, MifareClassicReader from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag, MifareClassicReader
from pm3py.sim.iso14443a import Reader14443A, REQA, CL1, NVB_SELECT, _compute_bcc from pm3py.transponders.hf.iso14443a.base import Reader14443A, REQA, CL1, NVB_SELECT, _compute_bcc
def run(coro): def run(coro):

View File

@@ -4,8 +4,8 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso14443a import Reader14443A, _compute_bcc from pm3py.transponders.hf.iso14443a.base import Reader14443A, _compute_bcc
from pm3py.sim.ndef import NfcType2Tag, NfcType4Tag from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag
def run(coro): def run(coro):

View File

@@ -15,45 +15,45 @@ def run(coro):
class TestIcodeDna: class TestIcodeDna:
def test_inherits_slix2(self): def test_inherits_slix2(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
assert issubclass(IcodeDnaTag, IcodeSlix2Tag) assert issubclass(IcodeDnaTag, IcodeSlix2Tag)
def test_default_64_blocks(self): def test_default_64_blocks(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
assert tag._num_blocks == 64 assert tag._num_blocks == 64
def test_uid_prefix(self): def test_uid_prefix(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
assert tag._uid[:4] == b"\xE0\x04\x01\x18" assert tag._uid[:4] == b"\xE0\x04\x01\x18"
def test_counter_on_last_block(self): def test_counter_on_last_block(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
user = tag.regions["user"] user = tag.regions["user"]
assert user.access_for_block(63).write_mode == "counter" assert user.access_for_block(63).write_mode == "counter"
def test_4_aes_keys(self): def test_4_aes_keys(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
assert len(tag._aes_keys) == 4 assert len(tag._aes_keys) == 4
assert all(k is None for k in tag._aes_keys) assert all(k is None for k in tag._aes_keys)
def test_aes_keys_from_constructor(self): def test_aes_keys_from_constructor(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
key0 = b"\xAA" * 16 key0 = b"\xAA" * 16
tag = IcodeDnaTag(aes_keys=[key0, None, None, None]) tag = IcodeDnaTag(aes_keys=[key0, None, None, None])
assert tag._aes_keys[0] == key0 assert tag._aes_keys[0] == key0
def test_config_memory_48_blocks(self): def test_config_memory_48_blocks(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
assert len(tag._config_memory) == 48 * 4 assert len(tag._config_memory) == 48 * 4
def test_read_config(self): def test_read_config(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
# Read config header block 0x08 # Read config header block 0x08
@@ -64,7 +64,7 @@ class TestIcodeDna:
assert resp.data[1] == 0x81 # CONFIG_HEADER_WRITABLE default assert resp.data[1] == 0x81 # CONFIG_HEADER_WRITABLE default
def test_write_config(self): def test_write_config(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
# Write CID to block 0x09 # Write CID to block 0x09
@@ -78,7 +78,7 @@ class TestIcodeDna:
def test_key_storage_masked_when_active(self): def test_key_storage_masked_when_active(self):
"""Key blocks return zeros when key header is active.""" """Key blocks return zeros when key header is active."""
from pm3py.sim.icode_dna import IcodeDnaTag, KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag, KEY_HEADER_ACTIVE_LOCKED
tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None]) tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None])
run(tag.power_on()) run(tag.power_on())
@@ -95,7 +95,7 @@ class TestIcodeDna:
def test_key_write_rejected_when_active(self): def test_key_write_rejected_when_active(self):
"""Cannot write key blocks when header is active.""" """Cannot write key blocks when header is active."""
from pm3py.sim.icode_dna import IcodeDnaTag, KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag, KEY_HEADER_ACTIVE_LOCKED
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
@@ -106,7 +106,7 @@ class TestIcodeDna:
def test_key_header_one_way(self): def test_key_header_one_way(self):
"""Key header only goes up (lower → higher values).""" """Key header only goes up (lower → higher values)."""
from pm3py.sim.icode_dna import IcodeDnaTag, KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag, KEY_HEADER_ACTIVE_LOCKED
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
@@ -118,7 +118,7 @@ class TestIcodeDna:
def test_gch_one_way(self): def test_gch_one_way(self):
"""NFC_GCH only programs one-way.""" """NFC_GCH only programs one-way."""
from pm3py.sim.icode_dna import IcodeDnaTag, GCH_ACTIVATED from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag, GCH_ACTIVATED
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
@@ -129,19 +129,19 @@ class TestIcodeDna:
assert tag._nfc_gch == GCH_ACTIVATED assert tag._nfc_gch == GCH_ACTIVATED
def test_cid_default(self): def test_cid_default(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
assert tag._cid == 0xC000 assert tag._cid == 0xC000
def test_responds_to_inventory(self): def test_responds_to_inventory(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00])))) resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00]))))
assert resp is not None assert resp is not None
def test_inherits_eas(self): def test_inherits_eas(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xA2, 0x04]) cmd = bytes([0x02, 0xA2, 0x04])
@@ -156,12 +156,12 @@ class TestIcodeDna:
class TestNtag5Platform: class TestNtag5Platform:
def test_config_memory(self): def test_config_memory(self):
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
tag = Ntag5PlatformTag(num_blocks=128) tag = Ntag5PlatformTag(num_blocks=128)
assert len(tag._config_memory) >= 160 * 4 assert len(tag._config_memory) >= 160 * 4
def test_read_config(self): def test_read_config(self):
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
tag = Ntag5PlatformTag(num_blocks=128) tag = Ntag5PlatformTag(num_blocks=128)
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xC0, 0x04, 0x3F, 0x00]) cmd = bytes([0x02, 0xC0, 0x04, 0x3F, 0x00])
@@ -171,7 +171,7 @@ class TestNtag5Platform:
assert resp.data[1] == 0xA5 # DEV_SEC_CONFIG default assert resp.data[1] == 0xA5 # DEV_SEC_CONFIG default
def test_write_config(self): def test_write_config(self):
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
tag = Ntag5PlatformTag(num_blocks=128) tag = Ntag5PlatformTag(num_blocks=128)
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xC1, 0x04, 0x57, 0x34, 0x12, 0x00, 0x00]) cmd = bytes([0x02, 0xC1, 0x04, 0x57, 0x34, 0x12, 0x00, 0x00])
@@ -189,36 +189,36 @@ class TestNtag5Platform:
class TestNtag5Switch: class TestNtag5Switch:
def test_inherits_platform(self): def test_inherits_platform(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
assert issubclass(Ntag5SwitchTag, Ntag5PlatformTag) assert issubclass(Ntag5SwitchTag, Ntag5PlatformTag)
def test_default_128_blocks(self): def test_default_128_blocks(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
assert tag._num_blocks == 128 assert tag._num_blocks == 128
def test_no_aes(self): def test_no_aes(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
assert not tag._aes_capable assert not tag._aes_capable
def test_counter_on_block_127(self): def test_counter_on_block_127(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
user = tag.regions["user"] user = tag.regions["user"]
assert user.access_for_block(127).write_mode == "counter" assert user.access_for_block(127).write_mode == "counter"
assert user.access_for_block(0).write_mode == "normal" assert user.access_for_block(0).write_mode == "normal"
def test_responds_to_inventory(self): def test_responds_to_inventory(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
run(tag.power_on()) run(tag.power_on())
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00])))) resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00]))))
assert resp is not None assert resp is not None
def test_switch_rejects_stay_quiet_persistent(self): def test_switch_rejects_stay_quiet_persistent(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x22, 0xBC]) + tag._uid[::-1] + bytes([0x04]) cmd = bytes([0x22, 0xBC]) + tag._uid[::-1] + bytes([0x04])
@@ -233,33 +233,33 @@ class TestNtag5Switch:
class TestNtag5Link: class TestNtag5Link:
def test_inherits_platform(self): def test_inherits_platform(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
assert issubclass(Ntag5LinkTag, Ntag5PlatformTag) assert issubclass(Ntag5LinkTag, Ntag5PlatformTag)
def test_default_512_blocks(self): def test_default_512_blocks(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
assert tag._num_blocks == 512 assert tag._num_blocks == 512
def test_i2c_address(self): def test_i2c_address(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
assert tag.i2c_address == 0x54 assert tag.i2c_address == 0x54
def test_sram(self): def test_sram(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
assert len(tag.sram) == 256 assert len(tag.sram) == 256
def test_i2c_sram_roundtrip(self): def test_i2c_sram_roundtrip(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
tag.i2c_write(0x2000, b"\xAA\xBB\xCC\xDD") tag.i2c_write(0x2000, b"\xAA\xBB\xCC\xDD")
assert tag.i2c_read(0x2000, 4) == b"\xAA\xBB\xCC\xDD" assert tag.i2c_read(0x2000, 4) == b"\xAA\xBB\xCC\xDD"
def test_sram_mirror_read(self): def test_sram_mirror_read(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag._sram_enabled = True tag._sram_enabled = True
@@ -270,12 +270,12 @@ class TestNtag5Link:
assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF" assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF"
def test_password_only_variant(self): def test_password_only_variant(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag(aes_capable=False) tag = Ntag5LinkTag(aes_capable=False)
assert not tag._aes_capable assert not tag._aes_capable
def test_aes_variant(self): def test_aes_variant(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag(aes_capable=True) tag = Ntag5LinkTag(aes_capable=True)
assert tag._aes_capable assert tag._aes_capable
@@ -286,33 +286,33 @@ class TestNtag5Link:
class TestArbitration: class TestArbitration:
def test_acquire_nfc_succeeds_no_lock(self): def test_acquire_nfc_succeeds_no_lock(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
assert tag.acquire_nfc() is True assert tag.acquire_nfc() is True
assert tag.nfc_if_locked is True assert tag.nfc_if_locked is True
def test_acquire_nfc_fails_when_i2c_locked(self): def test_acquire_nfc_fails_when_i2c_locked(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
tag.set_i2c_locked(True) tag.set_i2c_locked(True)
assert tag.acquire_nfc() is False assert tag.acquire_nfc() is False
assert tag.nfc_if_locked is False assert tag.nfc_if_locked is False
def test_acquire_i2c_succeeds_no_lock(self): def test_acquire_i2c_succeeds_no_lock(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
assert tag.acquire_i2c() is True assert tag.acquire_i2c() is True
assert tag.i2c_if_locked is True assert tag.i2c_if_locked is True
def test_acquire_i2c_fails_when_nfc_locked(self): def test_acquire_i2c_fails_when_nfc_locked(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
tag.set_nfc_locked(True) tag.set_nfc_locked(True)
assert tag.acquire_i2c() is False assert tag.acquire_i2c() is False
assert tag.i2c_if_locked is False assert tag.i2c_if_locked is False
def test_release_clears_lock(self): def test_release_clears_lock(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
tag.acquire_nfc() tag.acquire_nfc()
assert tag.nfc_if_locked is True assert tag.nfc_if_locked is True
@@ -325,7 +325,7 @@ class TestArbitration:
assert tag.i2c_if_locked is False assert tag.i2c_if_locked is False
def test_nfc_sram_read_blocked_when_i2c_locked(self): def test_nfc_sram_read_blocked_when_i2c_locked(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag._sram_enabled = True tag._sram_enabled = True
@@ -339,7 +339,7 @@ class TestArbitration:
assert resp.data[1] == 0x0F assert resp.data[1] == 0x0F
def test_nfc_sram_read_works_with_nfc_lock(self): def test_nfc_sram_read_works_with_nfc_lock(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag._sram_enabled = True tag._sram_enabled = True
@@ -352,21 +352,21 @@ class TestArbitration:
assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF" assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF"
def test_i2c_sram_read_blocked_when_nfc_locked(self): def test_i2c_sram_read_blocked_when_nfc_locked(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
tag.set_nfc_locked(True) tag.set_nfc_locked(True)
result = tag.i2c_read(0x2000, 4) result = tag.i2c_read(0x2000, 4)
assert result is None assert result is None
def test_i2c_sram_write_blocked_when_nfc_locked(self): def test_i2c_sram_write_blocked_when_nfc_locked(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
tag.set_nfc_locked(True) tag.set_nfc_locked(True)
result = tag.i2c_write(0x2000, b"\x01\x02\x03\x04") result = tag.i2c_write(0x2000, b"\x01\x02\x03\x04")
assert result is False assert result is False
def test_lock_unlock_sequential_access(self): def test_lock_unlock_sequential_access(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
# NFC acquires, writes via SRAM directly, releases # NFC acquires, writes via SRAM directly, releases
@@ -391,27 +391,27 @@ class TestArbitration:
class TestNtag5Boost: class TestNtag5Boost:
def test_inherits_link(self): def test_inherits_link(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
assert issubclass(Ntag5BoostTag, Ntag5LinkTag) assert issubclass(Ntag5BoostTag, Ntag5LinkTag)
def test_aes_always(self): def test_aes_always(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
assert tag._aes_capable assert tag._aes_capable
def test_default_512_blocks(self): def test_default_512_blocks(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
assert tag._num_blocks == 512 assert tag._num_blocks == 512
def test_has_sram(self): def test_has_sram(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
assert len(tag.sram) == 256 assert len(tag.sram) == 256
def test_boost_rejects_stay_quiet_persistent(self): def test_boost_rejects_stay_quiet_persistent(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x22, 0xBC]) + tag._uid[::-1] + bytes([0x04]) cmd = bytes([0x22, 0xBC]) + tag._uid[::-1] + bytes([0x04])
@@ -428,7 +428,7 @@ class TestSessionRegisters:
"""Session register state machine for NTAG 5 platform.""" """Session register state machine for NTAG 5 platform."""
def _make_tag(self): def _make_tag(self):
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
tag = Ntag5PlatformTag(num_blocks=128) tag = Ntag5PlatformTag(num_blocks=128)
run(tag.power_on()) run(tag.power_on())
return tag return tag
@@ -553,8 +553,8 @@ class TestNtag5Aes:
def test_link_5332_tam1(self): def test_link_5332_tam1(self):
"""TAM1 (CHALLENGE + READBUFFER) works on Link NTP5332 (aes_capable=True).""" """TAM1 (CHALLENGE + READBUFFER) works on Link NTP5332 (aes_capable=True)."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
from Crypto.Cipher import AES from Crypto.Cipher import AES
key = b"\xCC" * 16 key = b"\xCC" * 16
@@ -586,8 +586,8 @@ class TestNtag5Aes:
def test_link_5332_tam1_readbuffer_one_shot(self): def test_link_5332_tam1_readbuffer_one_shot(self):
"""READBUFFER is one-shot — second read returns None.""" """READBUFFER is one-shot — second read returns None."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
key = b"\xCC" * 16 key = b"\xCC" * 16
tag = Ntag5LinkTag(aes_capable=True) tag = Ntag5LinkTag(aes_capable=True)
@@ -607,7 +607,7 @@ class TestNtag5Aes:
def test_switch_no_aes_challenge_rejected(self): def test_switch_no_aes_challenge_rejected(self):
"""CHALLENGE on Switch (no AES) does not produce a valid response.""" """CHALLENGE on Switch (no AES) does not produce a valid response."""
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
run(tag.power_on()) run(tag.power_on())
@@ -620,7 +620,7 @@ class TestNtag5Aes:
def test_link_no_aes_challenge_rejected(self): def test_link_no_aes_challenge_rejected(self):
"""CHALLENGE on Link NTP5312 (aes_capable=False) does not work.""" """CHALLENGE on Link NTP5312 (aes_capable=False) does not work."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag(aes_capable=False) tag = Ntag5LinkTag(aes_capable=False)
run(tag.power_on()) run(tag.power_on())
@@ -631,8 +631,8 @@ class TestNtag5Aes:
def test_boost_mam1(self): def test_boost_mam1(self):
"""MAM1 (AUTHENTICATE with AuthMethod=0x02) works on Boost.""" """MAM1 (AUTHENTICATE with AuthMethod=0x02) works on Boost."""
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
from Crypto.Cipher import AES from Crypto.Cipher import AES
key = b"\xDD" * 16 key = b"\xDD" * 16
@@ -663,8 +663,8 @@ class TestNtag5Aes:
def test_boost_mam2(self): def test_boost_mam2(self):
"""Full MAM1 + MAM2 mutual authentication on Boost.""" """Full MAM1 + MAM2 mutual authentication on Boost."""
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
from Crypto.Cipher import AES from Crypto.Cipher import AES
key = b"\xEE" * 16 key = b"\xEE" * 16
@@ -708,8 +708,8 @@ class TestNtag5Aes:
def test_boost_mam2_wrong_key_fails(self): def test_boost_mam2_wrong_key_fails(self):
"""MAM2 with wrong key is rejected.""" """MAM2 with wrong key is rejected."""
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
from Crypto.Cipher import AES from Crypto.Cipher import AES
key = b"\xEE" * 16 key = b"\xEE" * 16
@@ -735,7 +735,7 @@ class TestNtag5Aes:
def test_link_no_aes_authenticate_rejected(self): def test_link_no_aes_authenticate_rejected(self):
"""AUTHENTICATE on Link NTP5312 (no AES) returns error.""" """AUTHENTICATE on Link NTP5312 (no AES) returns error."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag(aes_capable=False) tag = Ntag5LinkTag(aes_capable=False)
run(tag.power_on()) run(tag.power_on())
@@ -748,7 +748,7 @@ class TestNtag5Aes:
def test_boost_inherits_slix2_commands(self): def test_boost_inherits_slix2_commands(self):
"""AES-capable Boost still handles SLIX2 commands (e.g. inventory, read).""" """AES-capable Boost still handles SLIX2 commands (e.g. inventory, read)."""
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
@@ -772,13 +772,13 @@ class TestConfigReg:
"""CONFIG_REG (session register block 0xA1) bit-field accessors and enforcement.""" """CONFIG_REG (session register block 0xA1) bit-field accessors and enforcement."""
def _make_platform_tag(self): def _make_platform_tag(self):
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
tag = Ntag5PlatformTag(num_blocks=128) tag = Ntag5PlatformTag(num_blocks=128)
run(tag.power_on()) run(tag.power_on())
return tag return tag
def _make_link_tag(self): def _make_link_tag(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
return tag return tag
@@ -869,7 +869,7 @@ class TestPassThrough:
"""SRAM pass-through mode (arbiter_mode == 2).""" """SRAM pass-through mode (arbiter_mode == 2)."""
def _make_tag(self): def _make_tag(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag, ARBITER_PASSTHROUGH from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag, ARBITER_PASSTHROUGH
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -946,7 +946,7 @@ class TestPassThrough:
def test_passthrough_not_active_in_normal_mode(self): def test_passthrough_not_active_in_normal_mode(self):
"""Pass-through behavior inactive when arbiter_mode is NORMAL.""" """Pass-through behavior inactive when arbiter_mode is NORMAL."""
from pm3py.sim.ntag5_link import Ntag5LinkTag, ARBITER_NORMAL from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag, ARBITER_NORMAL
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -958,7 +958,7 @@ class TestPassThrough:
def test_passthrough_not_active_in_mirror_mode(self): def test_passthrough_not_active_in_mirror_mode(self):
"""Pass-through behavior inactive when arbiter_mode is MIRROR.""" """Pass-through behavior inactive when arbiter_mode is MIRROR."""
from pm3py.sim.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag, ARBITER_MIRROR
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -998,7 +998,7 @@ class TestSynchDataBlock:
"""Synch data block triggers status register flags on access.""" """Synch data block triggers status register flags on access."""
def _make_tag(self): def _make_tag(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
return tag return tag
@@ -1075,7 +1075,7 @@ class TestEventDetection:
"""ED pin callbacks and trigger logic for NTAG 5 platform.""" """ED pin callbacks and trigger logic for NTAG 5 platform."""
def _make_tag(self): def _make_tag(self):
from pm3py.sim.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
tag = Ntag5PlatformTag(num_blocks=128) tag = Ntag5PlatformTag(num_blocks=128)
run(tag.power_on()) run(tag.power_on())
return tag return tag
@@ -1178,7 +1178,7 @@ class TestReadSram:
"""Direct SRAM read via NXP custom command 0xD2.""" """Direct SRAM read via NXP custom command 0xD2."""
def _make_tag(self): def _make_tag(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -1216,7 +1216,7 @@ class TestReadSram:
def test_error_when_sram_disabled(self): def test_error_when_sram_disabled(self):
"""Error (0x01, 0x0F) when SRAM is not enabled.""" """Error (0x01, 0x0F) when SRAM is not enabled."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
# sram_enable defaults to False # sram_enable defaults to False
@@ -1248,7 +1248,7 @@ class TestWriteSram:
"""Direct SRAM write via NXP custom command 0xD3.""" """Direct SRAM write via NXP custom command 0xD3."""
def _make_tag(self): def _make_tag(self):
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -1275,7 +1275,7 @@ class TestWriteSram:
def test_error_when_sram_disabled(self): def test_error_when_sram_disabled(self):
"""Error (0x01, 0x0F) when SRAM is not enabled.""" """Error (0x01, 0x0F) when SRAM is not enabled."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
cmd = bytes([0x02, 0xD3, 0x04, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF]) cmd = bytes([0x02, 0xD3, 0x04, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF])
@@ -1302,7 +1302,7 @@ class TestDualInterfaceIntegration:
def _make_tag(self, pt_dir_nfc_to_i2c: bool = True): def _make_tag(self, pt_dir_nfc_to_i2c: bool = True):
"""Create an Ntag5LinkTag configured for pass-through mode.""" """Create an Ntag5LinkTag configured for pass-through mode."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -1411,7 +1411,7 @@ class TestDualInterfaceIntegration:
def test_session_register_consistency(self): def test_session_register_consistency(self):
"""STATUS_REG reflects complete state after a cycle.""" """STATUS_REG reflects complete state after a cycle."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
tag.sram_enable = True tag.sram_enable = True
@@ -1473,7 +1473,7 @@ class TestDualInterfaceIntegration:
def test_ed_pin_fires_on_synch_data_block_access(self): def test_ed_pin_fires_on_synch_data_block_access(self):
"""ED pin fires when NFC writes to the configured synch data block.""" """ED pin fires when NFC writes to the configured synch data block."""
from pm3py.sim.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
tag = Ntag5LinkTag() tag = Ntag5LinkTag()
run(tag.power_on()) run(tag.power_on())
@@ -1516,8 +1516,8 @@ class TestNtag5Area1Passwords:
"""AREA_1 password IDs (0x40/0x80) per NTP5210/NTA5332 datasheets.""" """AREA_1 password IDs (0x40/0x80) per NTP5210/NTA5332 datasheets."""
def test_area1_password_ids_exist_on_switch(self): def test_area1_password_ids_exist_on_switch(self):
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
from pm3py.sim.ntag5_platform import PWD_AREA1_READ, PWD_AREA1_WRITE from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import PWD_AREA1_READ, PWD_AREA1_WRITE
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
assert PWD_AREA1_READ == 0x40 assert PWD_AREA1_READ == 0x40
assert PWD_AREA1_WRITE == 0x80 assert PWD_AREA1_WRITE == 0x80
@@ -1532,7 +1532,7 @@ class TestNtag5PickRandomUid:
"""PICK RANDOM UID (0xC2) per NTA5332 datasheet 8.2.4.9.15.""" """PICK RANDOM UID (0xC2) per NTA5332 datasheet 8.2.4.9.15."""
def test_pick_random_uid_in_privacy_mode(self): def test_pick_random_uid_in_privacy_mode(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
tag._privacy_mode = True tag._privacy_mode = True
@@ -1546,7 +1546,7 @@ class TestNtag5PickRandomUid:
assert tag._random_uid[:2] == b"\xE0\x04" assert tag._random_uid[:2] == b"\xE0\x04"
def test_pick_random_uid_without_privacy_mode_returns_error(self): def test_pick_random_uid_without_privacy_mode_returns_error(self):
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
assert tag._privacy_mode is False assert tag._privacy_mode is False
@@ -1559,7 +1559,7 @@ class TestNtag5PickRandomUid:
def test_switch_rejects_pick_random_uid(self): def test_switch_rejects_pick_random_uid(self):
"""NTP5210 does not support PICK RANDOM UID per datasheet rev 3.2.""" """NTP5210 does not support PICK RANDOM UID per datasheet rev 3.2."""
from pm3py.sim.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
tag = Ntag5SwitchTag() tag = Ntag5SwitchTag()
run(tag.power_on()) run(tag.power_on())
# Test outside privacy mode — should get error via custom command dispatch # Test outside privacy mode — should get error via custom command dispatch
@@ -1580,7 +1580,7 @@ class TestExtendedCommands:
def test_extended_read_single_block(self): def test_extended_read_single_block(self):
"""Read block 256 (beyond 1-byte range) using extended read single.""" """Read block 256 (beyond 1-byte range) using extended read single."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
# Write known data to block 256 # Write known data to block 256
@@ -1597,7 +1597,7 @@ class TestExtendedCommands:
def test_extended_read_single_block_addressed(self): def test_extended_read_single_block_addressed(self):
"""Extended read single in addressed mode.""" """Extended read single in addressed mode."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
offset = 300 * tag._block_size offset = 300 * tag._block_size
@@ -1612,7 +1612,7 @@ class TestExtendedCommands:
def test_extended_write_single_block(self): def test_extended_write_single_block(self):
"""Write to block 300 using extended write, then read back.""" """Write to block 300 using extended write, then read back."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
block_le = struct.pack("<H", 300) block_le = struct.pack("<H", 300)
@@ -1629,7 +1629,7 @@ class TestExtendedCommands:
def test_extended_read_multiple_blocks(self): def test_extended_read_multiple_blocks(self):
"""Read 3 blocks starting at block 256.""" """Read 3 blocks starting at block 256."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
bs = tag._block_size bs = tag._block_size
@@ -1649,7 +1649,7 @@ class TestExtendedCommands:
def test_extended_read_last_block_boost(self): def test_extended_read_last_block_boost(self):
"""Block 511 (last block of Boost) is readable via extended read.""" """Block 511 (last block of Boost) is readable via extended read."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
last = tag._num_blocks - 1 # 511 last = tag._num_blocks - 1 # 511
@@ -1664,7 +1664,7 @@ class TestExtendedCommands:
def test_extended_read_out_of_range(self): def test_extended_read_out_of_range(self):
"""Block 512 on Boost (512 blocks) returns error.""" """Block 512 on Boost (512 blocks) returns error."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
block_le = struct.pack("<H", 512) block_le = struct.pack("<H", 512)
@@ -1677,7 +1677,7 @@ class TestExtendedCommands:
def test_extended_read_multiple_overflow(self): def test_extended_read_multiple_overflow(self):
"""Extended read multiple that would go past end returns error.""" """Extended read multiple that would go past end returns error."""
import struct import struct
from pm3py.sim.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
tag = Ntag5BoostTag() tag = Ntag5BoostTag()
run(tag.power_on()) run(tag.power_on())
block_le = struct.pack("<H", 510) block_le = struct.pack("<H", 510)

View File

@@ -5,18 +5,18 @@ import pytest
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.type5 import NfcType5Tag from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag
from pm3py.sim.nxp_icode import NxpIcodeTag from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
from pm3py.sim.icode_slix import IcodeSlixTag from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
from pm3py.sim.icode_slix2 import ( from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import (
IcodeSlix2Tag, PWD_EAS_AFI, PWD_READ, PWD_WRITE, PWD_PRIVACY, PWD_DESTROY, IcodeSlix2Tag, PWD_EAS_AFI, PWD_READ, PWD_WRITE, PWD_PRIVACY, PWD_DESTROY,
) )
from pm3py.sim.nxp_icode import ( from pm3py.transponders.hf.iso15693.nxp.nxp_icode import (
CMD_SET_EAS, CMD_RESET_EAS, CMD_LOCK_EAS, CMD_EAS_ALARM, CMD_SET_EAS, CMD_RESET_EAS, CMD_LOCK_EAS, CMD_EAS_ALARM,
CMD_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID, CMD_INVENTORY_READ, CMD_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID, CMD_INVENTORY_READ,
CMD_FAST_INVENTORY_READ, CMD_FAST_INVENTORY_READ,
) )
from pm3py.sim.iso15693 import Reader15693 from pm3py.transponders.hf.iso15693.base import Reader15693
def run(coro): def run(coro):

View File

@@ -5,16 +5,16 @@ import pytest
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from pm3py.sim.iso14443a import Tag14443A_3, Tag14443A_4, _compute_bcc from pm3py.transponders.hf.iso14443a.base import Tag14443A_3, Tag14443A_4, _compute_bcc
from pm3py.sim.mifare import MifareClassicTag from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag
from pm3py.sim.iso15693 import Tag15693 from pm3py.transponders.hf.iso15693.base import Tag15693
from pm3py.sim.desfire import DesfireTag from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag
from pm3py.sim.table_compiler import ( from pm3py.sim.table_compiler import (
TableEntry, ResponseTable, TableCompiler, TableEntry, ResponseTable, TableCompiler,
EML_READ, EML_WRITE, MATCH_PREFIX, EML_READ, EML_WRITE, MATCH_PREFIX,
) )
from pm3py.sim.nxp_icode import NxpIcodeTag from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
def run(coro): def run(coro):
@@ -415,14 +415,14 @@ class TestTableCompilerEasAlarm:
class TestTableCompilerIcode3: class TestTableCompilerIcode3:
def test_compile_icode3_exists(self): def test_compile_icode3_exists(self):
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag() tag = Icode3Tag()
table = TableCompiler.compile_icode3(tag) table = TableCompiler.compile_icode3(tag)
assert len(table.entries) > 0 assert len(table.entries) > 0
def test_compile_icode3_includes_slix2_entries(self): def test_compile_icode3_includes_slix2_entries(self):
"""ICODE 3 table includes SLIX2 base entries.""" """ICODE 3 table includes SLIX2 base entries."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag() tag = Icode3Tag()
table = TableCompiler.compile_icode3(tag) table = TableCompiler.compile_icode3(tag)
# Should have GET_RANDOM, GET_NXP_SYSTEM_INFO, READ_SIGNATURE # Should have GET_RANDOM, GET_NXP_SYSTEM_INFO, READ_SIGNATURE
@@ -432,7 +432,7 @@ class TestTableCompilerIcode3:
def test_compile_icode3_system_info_has_feature_flags(self): def test_compile_icode3_system_info_has_feature_flags(self):
"""ICODE 3 GET NXP SYSTEM INFO has proper feature flags.""" """ICODE 3 GET NXP SYSTEM INFO has proper feature flags."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag() tag = Icode3Tag()
table = TableCompiler.compile_icode3(tag) table = TableCompiler.compile_icode3(tag)
entry = table.lookup(bytes([0x02, 0xAB])) entry = table.lookup(bytes([0x02, 0xAB]))
@@ -444,7 +444,7 @@ class TestTableCompilerIcode3:
def test_compile_icode3_config_password(self): def test_compile_icode3_config_password(self):
"""ICODE 3 table includes config password (pwd_id 0x20).""" """ICODE 3 table includes config password (pwd_id 0x20)."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
tag = Icode3Tag(config_password=0xAABBCCDD) tag = Icode3Tag(config_password=0xAABBCCDD)
table = TableCompiler.compile_icode3(tag) table = TableCompiler.compile_icode3(tag)
auth_entries = [e for e in table.entries if e.set_auth != 0] auth_entries = [e for e in table.entries if e.set_auth != 0]
@@ -457,7 +457,7 @@ class TestTableCompilerIcode3:
def test_session_dispatches_icode3(self): def test_session_dispatches_icode3(self):
"""SimSession uses compile_icode3 for Icode3Tag.""" """SimSession uses compile_icode3 for Icode3Tag."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
from pm3py.sim.sim_session import SimSession from pm3py.sim.sim_session import SimSession
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -478,14 +478,14 @@ class TestTableCompilerIcode3:
class TestTableCompilerDna: class TestTableCompilerDna:
def test_compile_icode_dna_exists(self): def test_compile_icode_dna_exists(self):
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
table = TableCompiler.compile_icode_dna(tag) table = TableCompiler.compile_icode_dna(tag)
assert len(table.entries) > 0 assert len(table.entries) > 0
def test_compile_includes_base_entries(self): def test_compile_includes_base_entries(self):
"""DNA table includes GET_RANDOM, READ_SIGNATURE from NXP base.""" """DNA table includes GET_RANDOM, READ_SIGNATURE from NXP base."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
table = TableCompiler.compile_icode_dna(tag) table = TableCompiler.compile_icode_dna(tag)
assert table.lookup(bytes([0x02, 0xB2])) is not None # GET_RANDOM assert table.lookup(bytes([0x02, 0xB2])) is not None # GET_RANDOM
@@ -493,7 +493,7 @@ class TestTableCompilerDna:
def test_compile_has_dna_feature_flags(self): def test_compile_has_dna_feature_flags(self):
"""GET NXP SYSTEM INFO has DNA-specific feature flags.""" """GET NXP SYSTEM INFO has DNA-specific feature flags."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
table = TableCompiler.compile_icode_dna(tag) table = TableCompiler.compile_icode_dna(tag)
entry = table.lookup(bytes([0x02, 0xAB])) entry = table.lookup(bytes([0x02, 0xAB]))
@@ -507,7 +507,7 @@ class TestTableCompilerDna:
def test_compile_has_eas(self): def test_compile_has_eas(self):
"""DNA table includes EAS entries.""" """DNA table includes EAS entries."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
tag = IcodeDnaTag() tag = IcodeDnaTag()
tag.set_eas(True) tag.set_eas(True)
table = TableCompiler.compile_icode_dna(tag) table = TableCompiler.compile_icode_dna(tag)
@@ -516,7 +516,7 @@ class TestTableCompilerDna:
def test_session_dispatches_dna(self): def test_session_dispatches_dna(self):
"""SimSession uses compile_icode_dna for IcodeDnaTag.""" """SimSession uses compile_icode_dna for IcodeDnaTag."""
from pm3py.sim.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.sim.sim_session import SimSession from pm3py.sim.sim_session import SimSession
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -538,7 +538,7 @@ class TestTableCompilerDna:
class TestSimSessionTableCompile: class TestSimSessionTableCompile:
def test_start_15693_compiles_nxp_table(self): def test_start_15693_compiles_nxp_table(self):
"""SimSession uses compile_icode3 for Icode3Tag.""" """SimSession uses compile_icode3 for Icode3Tag."""
from pm3py.sim.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
from pm3py.sim.sim_session import SimSession from pm3py.sim.sim_session import SimSession
from unittest.mock import MagicMock from unittest.mock import MagicMock

View File

@@ -2,7 +2,9 @@
import os import os
import pytest import pytest
from pm3py.sim.trace_fmt import decode_15693, decode_15693_nxp, decode_14443a, TraceFormatter from pm3py.trace.decode_iso15 import decode_15693, decode_15693_nxp
from pm3py.trace.decode_iso14a import decode_14443a
from pm3py.trace.format import TraceFormatter
class TestDecode15693Request: class TestDecode15693Request:
@@ -391,7 +393,7 @@ class TestTraceFormatterWrapping:
def test_long_annotation_wraps_to_next_line(self): def test_long_annotation_wraps_to_next_line(self):
cc = bytes([0xE1, 0x40, 0x0D, 0x01]) cc = bytes([0xE1, 0x40, 0x0D, 0x01])
from pm3py.sim.type5 import ndef_text from pm3py.transponders.hf.iso15693.type5 import ndef_text
msg = ndef_text("A" * 30) msg = ndef_text("A" * 30)
tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE]) tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
payload = bytes([0x00]) + cc + tlv payload = bytes([0x00]) + cc + tlv
@@ -490,44 +492,44 @@ class TestTransponderDecodeTrace:
assert hasattr(Transponder, 'decode_trace') assert hasattr(Transponder, 'decode_trace')
def test_tag15693_decodes_standard(self): def test_tag15693_decodes_standard(self):
from pm3py.sim.iso15693 import Tag15693 from pm3py.transponders.hf.iso15693.base import Tag15693
tag = Tag15693(uid=bytes(8)) tag = Tag15693(uid=bytes(8))
result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00])) result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00]))
assert result == "INVENTORY" assert result == "INVENTORY"
def test_tag15693_decodes_nxp_names(self): def test_tag15693_decodes_nxp_names(self):
"""Base Tag15693 decodes NXP command names (via standard decoder).""" """Base Tag15693 decodes NXP command names (via standard decoder)."""
from pm3py.sim.iso15693 import Tag15693 from pm3py.transponders.hf.iso15693.base import Tag15693
tag = Tag15693(uid=bytes(8)) tag = Tag15693(uid=bytes(8))
result = tag.decode_trace(0, bytes([0x22, 0xA1, 0x04, 0x02])) result = tag.decode_trace(0, bytes([0x22, 0xA1, 0x04, 0x02]))
assert result == "NXP FAST INVENTORY READ" assert result == "NXP FAST INVENTORY READ"
def test_nxp_icode_decodes_nxp(self): def test_nxp_icode_decodes_nxp(self):
from pm3py.sim.nxp_icode import NxpIcodeTag from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
tag = NxpIcodeTag(uid=b"\xE0\x04" + bytes(6)) tag = NxpIcodeTag(uid=b"\xE0\x04" + bytes(6))
result = tag.decode_trace(0, bytes([0x22, 0xA1, 0x04, 0x02])) result = tag.decode_trace(0, bytes([0x22, 0xA1, 0x04, 0x02]))
assert result == "NXP FAST INVENTORY READ" assert result == "NXP FAST INVENTORY READ"
def test_nxp_icode_falls_back_to_standard(self): def test_nxp_icode_falls_back_to_standard(self):
from pm3py.sim.nxp_icode import NxpIcodeTag from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
tag = NxpIcodeTag(uid=b"\xE0\x04" + bytes(6)) tag = NxpIcodeTag(uid=b"\xE0\x04" + bytes(6))
result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00])) result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00]))
assert result == "INVENTORY" assert result == "INVENTORY"
def test_icode_slix2_inherits_nxp(self): def test_icode_slix2_inherits_nxp(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5)) tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
result = tag.decode_trace(0, bytes([0x22, 0xB2, 0x04])) result = tag.decode_trace(0, bytes([0x22, 0xB2, 0x04]))
assert result == "NXP GET RANDOM" assert result == "NXP GET RANDOM"
def test_tag14443a_decodes(self): def test_tag14443a_decodes(self):
from pm3py.sim.iso14443a import Tag14443A from pm3py.transponders.hf.iso14443a.base import Tag14443A
tag = Tag14443A(uid=bytes(4)) tag = Tag14443A(uid=bytes(4))
result = tag.decode_trace(0, bytes([0x26])) result = tag.decode_trace(0, bytes([0x26]))
assert result == "REQA" assert result == "REQA"
def test_tag14443a_no_15693(self): def test_tag14443a_no_15693(self):
from pm3py.sim.iso14443a import Tag14443A from pm3py.transponders.hf.iso14443a.base import Tag14443A
tag = Tag14443A(uid=bytes(4)) tag = Tag14443A(uid=bytes(4))
result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00])) result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00]))
# 14443a decoder sees 0x26 as REQA (single byte check happens first) # 14443a decoder sees 0x26 as REQA (single byte check happens first)

327
tests/test_sniff_session.py Normal file
View File

@@ -0,0 +1,327 @@
"""Tests for SniffSession live streaming mode."""
import struct
import time
import pytest
from pm3py.core.protocol import (
Cmd,
SNIFF_FLAG_STREAMING, SNIFF_FLAG_BUTTON_TOGGLE,
SNIFF_STATE_STARTED, SNIFF_STATE_PAUSED,
SNIFF_STATE_RESUMED, SNIFF_STATE_STOPPED,
RESP_PREAMBLE_MAGIC, RESP_POSTAMBLE_NOCRC,
)
from pm3py.core.transport import encode_ng_frame
from pm3py.sniff.session import SniffSession
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class FakeSerial:
"""Mock serial port that feeds pre-built response frames."""
def __init__(self, frames: list[bytes] | None = None):
self._buf = bytearray()
if frames:
for f in frames:
self._buf.extend(f)
self._pos = 0
self._closed = False
self._written: list[bytes] = []
@property
def in_waiting(self):
return len(self._buf) - self._pos
def read(self, size=1):
if self._closed or self._pos >= len(self._buf):
time.sleep(0.01)
return b""
end = min(self._pos + size, len(self._buf))
data = bytes(self._buf[self._pos:end])
self._pos = end
return data
def write(self, data):
self._written.append(bytes(data))
return len(data)
def reset_input_buffer(self):
pass
def reset_output_buffer(self):
pass
def close(self):
self._closed = True
def feed(self, data: bytes):
"""Append more data to the read buffer."""
self._buf.extend(data)
def _make_resp_frame(cmd: int, status: int, data: bytes) -> bytes:
"""Build a raw NG response frame (USB, no CRC)."""
payload_len = len(data)
preamble = struct.pack("<IHBBH",
RESP_PREAMBLE_MAGIC,
payload_len | 0x8000, # NG bit
status & 0xFF,
0, # reason
cmd,
)
postamble = struct.pack("<H", RESP_POSTAMBLE_NOCRC)
return preamble + data + postamble
def _make_sniff_stream_frame(protocol, direction, duration, timestamp, data):
"""Build a CMD_HF_SNIFF_STREAM response frame."""
payload = struct.pack("<BBHI", protocol, direction, duration, timestamp) + data
return _make_resp_frame(Cmd.HF_SNIFF_STREAM.value, 0, payload)
def _make_status_frame(state, count=0):
"""Build a CMD_HF_SNIFF_STATUS response frame."""
payload = struct.pack("<BH", state, count)
return _make_resp_frame(Cmd.HF_SNIFF_STATUS.value, 0, payload)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestSniffSession:
def test_start_15693_sends_streaming_flags(self):
"""start_15693(live=True) sends payload with idle_timeout_ms=400, flags=0x03."""
ser = FakeSerial()
session = SniffSession(ser)
# Feed a status frame so the reader thread has something to consume
ser.feed(_make_status_frame(SNIFF_STATE_STARTED))
session.start_15693(live=True, idle_timeout_ms=400)
time.sleep(0.05)
session._active = False
time.sleep(0.05)
# Check the written command frame
assert len(ser._written) >= 1
frame = ser._written[0]
# Decode the command frame: magic(4) + length_ng(2) + cmd(2) + payload + postamble(2)
magic, length_ng, cmd = struct.unpack_from("<IHH", frame, 0)
payload = frame[8:-2]
assert cmd == Cmd.HF_ISO15693_SNIFF
idle_timeout, flags = struct.unpack_from("<HB", payload)
assert idle_timeout == 400
assert flags == (SNIFF_FLAG_STREAMING | SNIFF_FLAG_BUTTON_TOGGLE)
assert flags == 0x03
def test_start_15693_legacy_sends_zero_flags(self):
"""start_15693(live=False) sends flags=0."""
ser = FakeSerial()
session = SniffSession(ser)
# Feed a final response so legacy path unblocks
ser.feed(_make_resp_frame(Cmd.HF_ISO15693_SNIFF.value, 0, b""))
session.start_15693(live=False, idle_timeout_ms=400)
assert len(ser._written) >= 1
frame = ser._written[0]
payload = frame[8:-2]
idle_timeout, flags = struct.unpack_from("<HB", payload)
assert idle_timeout == 400
assert flags == 0
def test_receives_trace_entries(self):
"""Feed CMD_HF_SNIFF_STREAM frames, verify parsed entries."""
frames = [
_make_status_frame(SNIFF_STATE_STARTED),
_make_sniff_stream_frame(
protocol=0, direction=0, duration=100,
timestamp=1000, data=bytes([0x26, 0x01, 0x00])),
_make_sniff_stream_frame(
protocol=0, direction=1, duration=200,
timestamp=2000, data=bytes([0x00, 0x0F, 0xAB, 0xCD])),
]
ser = FakeSerial(frames)
session = SniffSession(ser)
session._formatter = None # suppress printing
session._active = True
session._state = "sniffing"
session._reader_thread = __import__("threading").Thread(
target=session._stream_reader, daemon=True)
session._reader_thread.start()
time.sleep(0.15)
session._active = False
session._reader_thread.join(timeout=1.0)
entries = session.entries
assert len(entries) == 2
assert entries[0]["direction"] == 0
assert entries[0]["duration"] == 100
assert entries[0]["timestamp"] == 1000
assert entries[0]["data"] == bytes([0x26, 0x01, 0x00])
assert entries[1]["direction"] == 1
assert entries[1]["duration"] == 200
assert entries[1]["timestamp"] == 2000
assert entries[1]["data"] == bytes([0x00, 0x0F, 0xAB, 0xCD])
def test_status_updates(self):
"""Feed STATUS frames, verify session.state transitions."""
frames = [
_make_status_frame(SNIFF_STATE_STARTED),
_make_status_frame(SNIFF_STATE_PAUSED, count=5),
_make_status_frame(SNIFF_STATE_RESUMED),
_make_status_frame(SNIFF_STATE_STOPPED, count=10),
]
ser = FakeSerial(frames)
session = SniffSession(ser)
session._formatter = None
session._active = True
session._state = "starting"
session._reader_thread = __import__("threading").Thread(
target=session._stream_reader, daemon=True)
session._reader_thread.start()
time.sleep(0.15)
session._reader_thread.join(timeout=1.0)
# After STOPPED, _active should be False and state should be "stopped"
assert session._active is False
assert session.state == "stopped"
def test_stop_sends_break_loop(self):
"""stop() sends a BREAK_LOOP command."""
ser = FakeSerial()
session = SniffSession(ser)
session._active = True
# No reader thread — just test the command sending
session._reader_thread = None
session.stop()
assert len(ser._written) == 1
frame = ser._written[0]
_, _, cmd = struct.unpack_from("<IHH", frame, 0)
assert cmd == Cmd.BREAK_LOOP
assert session._active is False
assert session.state == "stopped"
def test_entries_accumulate_across_cycles(self):
"""Entries persist across pause/resume cycles."""
frames = [
_make_status_frame(SNIFF_STATE_STARTED),
_make_sniff_stream_frame(0, 0, 50, 100, b"\x26"),
_make_status_frame(SNIFF_STATE_PAUSED, count=1),
_make_status_frame(SNIFF_STATE_RESUMED),
_make_sniff_stream_frame(0, 1, 60, 200, b"\x00\x0F"),
_make_status_frame(SNIFF_STATE_STOPPED, count=2),
]
ser = FakeSerial(frames)
session = SniffSession(ser)
session._formatter = None
session._active = True
session._state = "starting"
session._reader_thread = __import__("threading").Thread(
target=session._stream_reader, daemon=True)
session._reader_thread.start()
time.sleep(0.15)
session._reader_thread.join(timeout=1.0)
entries = session.entries
assert len(entries) == 2
assert entries[0]["data"] == b"\x26"
assert entries[1]["data"] == b"\x00\x0F"
def test_clear_entries(self):
"""clear() resets entries list."""
ser = FakeSerial()
session = SniffSession(ser)
session._entries.append({"direction": 0, "data": b"\x26"})
session._entries.append({"direction": 1, "data": b"\x00"})
assert len(session.entries) == 2
session.clear()
assert len(session.entries) == 0
def test_full_workflow(self):
"""Integration: started -> stream -> pause -> resume -> more -> stopped."""
frames = [
_make_status_frame(SNIFF_STATE_STARTED),
# Reader command
_make_sniff_stream_frame(0, 0, 100, 1000, b"\x26\x01\x00"),
# Tag response
_make_sniff_stream_frame(0, 1, 200, 1500, b"\x00\x0F\xAA\xBB"),
# Another reader command
_make_sniff_stream_frame(0, 0, 100, 3000, b"\x20\x01"),
_make_status_frame(SNIFF_STATE_PAUSED, count=3),
_make_status_frame(SNIFF_STATE_RESUMED),
# More frames after resume
_make_sniff_stream_frame(0, 0, 100, 5000, b"\x26\x01\x00"),
_make_sniff_stream_frame(0, 1, 200, 5500, b"\x00\x0F\xCC\xDD"),
_make_status_frame(SNIFF_STATE_STOPPED, count=5),
]
ser = FakeSerial(frames)
session = SniffSession(ser)
session._formatter = None
session._active = True
session._state = "starting"
session._reader_thread = __import__("threading").Thread(
target=session._stream_reader, daemon=True)
session._reader_thread.start()
time.sleep(0.2)
session._reader_thread.join(timeout=1.0)
entries = session.entries
assert len(entries) == 5
# Verify timestamps are monotonically increasing
timestamps = [e["timestamp"] for e in entries]
assert timestamps == sorted(timestamps)
assert timestamps == [1000, 1500, 3000, 5000, 5500]
# Verify directions alternate for first pair
assert entries[0]["direction"] == 0 # reader
assert entries[1]["direction"] == 1 # tag
# Final state
assert session.state == "stopped"
assert session._active is False
def test_close_cleans_up(self):
"""close() stops session and closes serial."""
ser = FakeSerial()
session = SniffSession(ser)
session._active = True
session._reader_thread = None
session.close()
assert session._active is False
assert session.state == "stopped"
assert ser._closed is True
def test_entries_returns_copy(self):
"""entries property returns a copy, not the internal list."""
ser = FakeSerial()
session = SniffSession(ser)
session._entries.append({"direction": 0, "data": b"\x26"})
entries = session.entries
entries.append({"direction": 1, "data": b"\x00"})
assert len(session.entries) == 1 # internal list unchanged