Compare commits
10 Commits
8e8b945246
...
4be8100cb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be8100cb6 | ||
|
|
cb5d06da14 | ||
|
|
06de521d02 | ||
|
|
83c8dd5546 | ||
|
|
e90a907630 | ||
|
|
580ab2b63f | ||
|
|
44f98f137e | ||
|
|
874aa688b9 | ||
|
|
f294aba9ef | ||
|
|
c1ab980036 |
@@ -35,6 +35,9 @@ SIM = [
|
||||
("pm3py.transponders.hf.iso14443a.ndef", "NfcType4Tag"),
|
||||
("pm3py.transponders.hf.iso15693.base", "Tag15693"),
|
||||
("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.icode_slix", "IcodeSlixTag"),
|
||||
("pm3py.transponders.hf.iso15693.nxp.icode_slix2", "IcodeSlix2Tag"),
|
||||
@@ -95,6 +98,22 @@ for module_name, attr_name in imports:
|
||||
if loaded:
|
||||
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:
|
||||
print(f"\n[pm3py:{_MODE}] Failed:")
|
||||
for name, (mod, err) in failed.items():
|
||||
|
||||
32
CLAUDE.md
32
CLAUDE.md
@@ -37,25 +37,27 @@ pm3py/
|
||||
format.py # ANSI color formatting, format_sniff_line
|
||||
sniff/ # sniff orchestration — protocol detection, session lifecycle
|
||||
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
|
||||
medium.py # Medium, SoftwareMedium (RF simulation)
|
||||
reader.py # Reader ABC, ScriptedReader, InteractiveReader
|
||||
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)
|
||||
dual_session.py # DualInterfaceSession (PM3 RF + MCU I2C)
|
||||
table_compiler.py # ResponseTable, TableCompiler
|
||||
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)
|
||||
transponders/ # tag/transponder models independent of hardware (scaffold)
|
||||
```
|
||||
|
||||
## Client API
|
||||
@@ -97,9 +99,9 @@ pm3.hf.dropfield() # drop field
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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)
|
||||
- **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
|
||||
|
||||
30
activate
30
activate
@@ -1,21 +1,19 @@
|
||||
# pm3py venv activation with mode-based auto-imports.
|
||||
# Usage: source activate — core (Proxmark3, Cmd, etc.)
|
||||
# source activate sim — + SimSession, transponder models
|
||||
# source activate sniff — + SniffSession, trace decoders
|
||||
# source activate all — everything
|
||||
# pm3py venv activation.
|
||||
# Usage: source activate
|
||||
#
|
||||
# Then: python — REPL with auto-imports
|
||||
# ipython — IPython with auto-imports (if installed)
|
||||
# Then: pm3 — REPL with core imports (Proxmark3, Cmd)
|
||||
# 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
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/.venv/bin/activate"
|
||||
source "${_PM3PY_ROOT}/.venv/bin/activate"
|
||||
|
||||
# Set mode and PYTHONSTARTUP
|
||||
export PM3PY_MODE="${1:-core}"
|
||||
export PYTHONSTARTUP="$(dirname "${BASH_SOURCE[0]}")/.pythonstartup.py"
|
||||
# pm3 command — launches REPL with mode
|
||||
pm3() {
|
||||
PM3PY_MODE="${1:-core}" python -i "${_PM3PY_ROOT}/.pythonstartup.py"
|
||||
}
|
||||
|
||||
# Update prompt to show mode
|
||||
PS1="(pm3py:${PM3PY_MODE}) ${_OLD_VIRTUAL_PS1:-\$ }"
|
||||
export PS1
|
||||
|
||||
echo "pm3py env ready (mode: ${PM3PY_MODE}). Run 'python' for REPL with auto-imports."
|
||||
echo "pm3py env ready. Commands: pm3, pm3 sim, pm3 sniff, pm3 all"
|
||||
|
||||
2
firmware
2
firmware
Submodule firmware updated: 206c0f0fbc...35db99fa58
@@ -202,6 +202,9 @@ class Cmd(IntEnum):
|
||||
HF_ISO15693_CSETUID = 0x0316
|
||||
HF_ISO15693_EML_SETMEM = 0x0331
|
||||
|
||||
HF_SNIFF_STREAM = 0x0337
|
||||
HF_SNIFF_STATUS = 0x0338
|
||||
|
||||
# Sim table commands (firmware response table)
|
||||
SIM_TABLE_UPLOAD = 0x0900
|
||||
SIM_TABLE_CLEAR = 0x0901
|
||||
@@ -265,3 +268,14 @@ class Cmd(IntEnum):
|
||||
FPGAMEM_DOWNLOADED = 0x0803
|
||||
|
||||
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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_password import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso14443a.nxp.desfire import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.lf.em.em4100 import * # noqa: F401,F403
|
||||
@@ -6,7 +6,7 @@ from typing import Iterator
|
||||
|
||||
from bitarray import bitarray
|
||||
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:
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.lf.hid.hid import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.implants import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso14443a.base import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.base import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.lf.base import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso14443a.ndef import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import * # noqa: F401,F403
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import * # noqa: F401,F403
|
||||
@@ -10,7 +10,7 @@ import serial
|
||||
|
||||
from .frame import RFFrame
|
||||
from .table_compiler import ResponseTable, TableCompiler
|
||||
from .trace_fmt import TraceFormatter
|
||||
from pm3py.trace.format import TraceFormatter
|
||||
from .transponder import Transponder
|
||||
from ..core.protocol import Cmd
|
||||
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=[])
|
||||
|
||||
# Check most specific IC first, fall back to base
|
||||
from .icode3 import Icode3Tag
|
||||
from .icode_dna import IcodeDnaTag
|
||||
from .icode_slix2 import IcodeSlix2Tag
|
||||
from .ntag5_platform import Ntag5PlatformTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
|
||||
if isinstance(tag, Ntag5PlatformTag):
|
||||
# NTAG 5 Switch/Link/Boost — use DNA compiler (same platform)
|
||||
table = TableCompiler.compile_icode_dna(tag)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.lf.atmel.t5577 import * # noqa: F401,F403
|
||||
@@ -288,7 +288,7 @@ class TableCompiler:
|
||||
first_block = 128 + (sector - 32) * 16
|
||||
|
||||
# Pre-pick a tag nonce
|
||||
from .crypto1 import Crypto1
|
||||
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
|
||||
crypto = Crypto1(key)
|
||||
nt = crypto.generate_nonce()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
"""Backward compat shim — moved to pm3py.transponders."""
|
||||
from pm3py.transponders.hf.iso15693.type5 import * # noqa: F401,F403
|
||||
@@ -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 threading
|
||||
import time
|
||||
|
||||
from ..core.protocol import Cmd, PM3_CMD_DATA_SIZE
|
||||
from ..core.transport import PM3Transport, encode_ng_frame
|
||||
import serial
|
||||
|
||||
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.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:
|
||||
"""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
|
||||
lifecycle that doesn't fit the stateless command pattern.
|
||||
For live mode (default):
|
||||
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):
|
||||
self._t = transport
|
||||
def __init__(self, ser):
|
||||
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:
|
||||
"""Start ISO 15693 sniff. Blocks until button press or timeout."""
|
||||
async with self._t._lock:
|
||||
raw = encode_ng_frame(Cmd.HF_ISO15693_SNIFF)
|
||||
await self._t.send_frame(raw)
|
||||
while True:
|
||||
resp = await self._t.read_response(timeout=timeout)
|
||||
if resp.cmd == Cmd.HF_ISO15693_SNIFF:
|
||||
return {"status": resp.status}
|
||||
@classmethod
|
||||
def open(cls, port: str = DEFAULT_PORT, baudrate: int = 115200) -> "SniffSession":
|
||||
"""Open serial connection to PM3. Auto-detects port if default."""
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = baudrate
|
||||
ser.bytesize = 8
|
||||
ser.parity = "N"
|
||||
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]:
|
||||
"""Download and parse the trace buffer from the device."""
|
||||
raw, trace_len = await self._t.download_bigbuf(
|
||||
offset=0, length=PM3_CMD_DATA_SIZE, timeout=4.0)
|
||||
ser.reset_input_buffer()
|
||||
ser.reset_output_buffer()
|
||||
time.sleep(0.5)
|
||||
ser.reset_input_buffer()
|
||||
|
||||
if trace_len == 0:
|
||||
return []
|
||||
return cls(ser)
|
||||
|
||||
if trace_len > PM3_CMD_DATA_SIZE:
|
||||
raw, trace_len = await self._t.download_bigbuf(
|
||||
offset=0, length=trace_len, timeout=10.0)
|
||||
def start_15693(self, live: bool = True, idle_timeout_ms: int = 400) -> None:
|
||||
"""Start 15693 sniff session.
|
||||
|
||||
return parse_tracelog(raw[:trace_len])
|
||||
|
||||
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.
|
||||
live=True (default): streaming with button-toggle pause/resume.
|
||||
live=False: legacy BigBuf batch mode, blocks until button exits.
|
||||
"""
|
||||
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:
|
||||
width = os.get_terminal_size().columns
|
||||
except (OSError, ValueError):
|
||||
width = 120
|
||||
is_tty = sys.stdout.isatty()
|
||||
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:
|
||||
resp = decode_response_frame(bytes(buf[:frame_len]))
|
||||
del buf[:frame_len]
|
||||
except Exception:
|
||||
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.")
|
||||
result = await self._sniff_15(timeout=timeout)
|
||||
print(f"[Snf] Sniff ended (status={result['status']})")
|
||||
|
||||
print("[Snf] Downloading trace...")
|
||||
entries = await self._download_trace()
|
||||
# Read frames until we get the sniff completion response
|
||||
buf = bytearray()
|
||||
while True:
|
||||
chunk = self._ser.read(self._ser.in_waiting or 1)
|
||||
if chunk:
|
||||
buf.extend(chunk)
|
||||
|
||||
if not entries:
|
||||
print("[Snf] No trace data captured.")
|
||||
return []
|
||||
idx = buf.find(struct.pack("<I", RESP_PREAMBLE_MAGIC))
|
||||
if idx < 0:
|
||||
continue
|
||||
if idx > 0:
|
||||
buf = buf[idx:]
|
||||
|
||||
print(f"[Snf] {len(entries)} frames captured:\n")
|
||||
for entry in entries:
|
||||
line = format_sniff_line(entry, width=width, is_tty=is_tty)
|
||||
print(line)
|
||||
if len(buf) < RESP_PREAMBLE_SIZE:
|
||||
continue
|
||||
|
||||
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()
|
||||
|
||||
@@ -5,7 +5,7 @@ from enum import IntEnum
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
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.reader import Reader
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from enum import IntEnum
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
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.reader import Reader
|
||||
|
||||
@@ -128,6 +128,46 @@ class Tag15693(Transponder):
|
||||
random_bytes = os.urandom(8 - len(prefix))
|
||||
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:
|
||||
return decode_15693(direction, payload)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
import struct
|
||||
|
||||
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
|
||||
|
||||
# NXP manufacturer code
|
||||
|
||||
Binary file not shown.
@@ -5,7 +5,7 @@ from bitarray import bitarray
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.iso14443a import (
|
||||
from pm3py.transponders.hf.iso14443a.base import (
|
||||
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A,
|
||||
State14443A, REQA, WUPA, HLTA, CL1, CL2, CL3,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
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):
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
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.relay import RelayTransponder, MitMProxy
|
||||
from pm3py.sim.replay import TraceRecorder, TraceReplayer, TraceEntry
|
||||
|
||||
@@ -15,8 +15,8 @@ def run(coro):
|
||||
class TestTam1:
|
||||
def test_challenge_readbuffer(self):
|
||||
"""Full TAM1 flow: CHALLENGE computes TResponse, READBUFFER returns it."""
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
key = b"\xAA" * 16
|
||||
tag = IcodeDnaTag(aes_keys=[key, None, None, None])
|
||||
@@ -50,8 +50,8 @@ class TestTam1:
|
||||
|
||||
def test_challenge_key1(self):
|
||||
"""TAM1 with key slot 1."""
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
key = b"\xCC" * 16
|
||||
tag = IcodeDnaTag(aes_keys=[None, key, None, None])
|
||||
@@ -75,7 +75,7 @@ class TestTam1:
|
||||
|
||||
def test_challenge_inactive_key_silent(self):
|
||||
"""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])
|
||||
# Key header NOT active (default 0x81)
|
||||
@@ -92,7 +92,7 @@ class TestTam1:
|
||||
|
||||
def test_readbuffer_without_challenge(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
@@ -103,8 +103,8 @@ class TestTam1:
|
||||
|
||||
def test_readbuffer_one_shot(self):
|
||||
"""TResponse is cleared after first READBUFFER (one-shot)."""
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
key = b"\xDD" * 16
|
||||
tag = IcodeDnaTag(aes_keys=[key, None, None, None])
|
||||
@@ -126,8 +126,8 @@ class TestTam1:
|
||||
|
||||
def test_challenge_no_key_in_slot(self):
|
||||
"""CHALLENGE with None key slot stays silent."""
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
tag = IcodeDnaTag(aes_keys=[None, None, None, None])
|
||||
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
|
||||
@@ -143,8 +143,8 @@ class TestTam1:
|
||||
|
||||
def test_challenge_invalid_key_id(self):
|
||||
"""CHALLENGE with key_id > 3 stays silent."""
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None])
|
||||
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
|
||||
@@ -156,8 +156,8 @@ class TestTam1:
|
||||
|
||||
def test_challenge_wrong_auth_method(self):
|
||||
"""CHALLENGE with AuthMethod != 0x00 (not TAM1) stays silent."""
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
tag = IcodeDnaTag(aes_keys=[b"\xAA" * 16, None, None, None])
|
||||
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
|
||||
@@ -177,8 +177,8 @@ class TestMam:
|
||||
"""Tests for MAM1/MAM2 mutual authentication via AUTHENTICATE (0x35)."""
|
||||
|
||||
def _make_tag(self, key=b"\xAA" * 16, key_slot=0):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
keys = [None] * 4
|
||||
keys[key_slot] = key
|
||||
@@ -293,7 +293,7 @@ class TestMam:
|
||||
|
||||
def test_mam1_inactive_key_error(self):
|
||||
"""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])
|
||||
run(tag.power_on())
|
||||
@@ -366,7 +366,7 @@ class TestMam:
|
||||
|
||||
def test_mam2_purpose_enable_privacy(self):
|
||||
"""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
|
||||
tag = self._make_tag(key)
|
||||
@@ -380,7 +380,7 @@ class TestMam:
|
||||
|
||||
def test_mam2_purpose_disable_privacy(self):
|
||||
"""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
|
||||
tag = self._make_tag(key)
|
||||
@@ -395,7 +395,7 @@ class TestMam:
|
||||
|
||||
def test_mam2_purpose_destroy(self):
|
||||
"""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
|
||||
tag = self._make_tag(key)
|
||||
|
||||
@@ -4,8 +4,8 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.iso14443a import _compute_bcc
|
||||
from pm3py.sim.desfire import DesfireTag, DesfireReader
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
|
||||
|
||||
|
||||
def run(coro):
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from pm3py.sim.dual_session import DualInterfaceSession
|
||||
from pm3py.sim.mcu_bridge import McuBridge
|
||||
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:
|
||||
|
||||
@@ -4,7 +4,7 @@ import struct
|
||||
import pytest
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -41,20 +41,20 @@ PWD_CONFIG = 0x20
|
||||
|
||||
class TestIcode3Basics:
|
||||
def test_import(self):
|
||||
from pm3py.sim.icode3 import Icode3Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
||||
|
||||
def test_inherits_slix2(self):
|
||||
from pm3py.sim.icode3 import Icode3Tag
|
||||
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||||
assert issubclass(Icode3Tag, IcodeSlix2Tag)
|
||||
|
||||
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))
|
||||
assert tag._num_blocks == 76
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
inv = bytes([0x26, 0x01, 0x00])
|
||||
@@ -68,14 +68,14 @@ class TestIcode3Basics:
|
||||
|
||||
class TestIcode3ConfigPassword:
|
||||
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),
|
||||
config_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
_authenticate(tag, PWD_CONFIG, 0xDEADBEEF)
|
||||
|
||||
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),
|
||||
config_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -91,7 +91,7 @@ class TestIcode3ConfigPassword:
|
||||
|
||||
class TestIcode3InheritsSlix2:
|
||||
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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
|
||||
@@ -100,7 +100,7 @@ class TestIcode3InheritsSlix2:
|
||||
assert tag.eas_enabled
|
||||
|
||||
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),
|
||||
privacy_password=0xAAAAAAAA)
|
||||
run(tag.power_on())
|
||||
@@ -108,7 +108,7 @@ class TestIcode3InheritsSlix2:
|
||||
assert tag.privacy_mode
|
||||
|
||||
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),
|
||||
destroy_password=0xBBBBBBBB)
|
||||
run(tag.power_on())
|
||||
@@ -125,7 +125,7 @@ class TestIcode3InheritsSlix2:
|
||||
|
||||
class TestIcode3Counter:
|
||||
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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -141,7 +141,7 @@ class TestIcode3Counter:
|
||||
assert counter == 1
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -155,7 +155,7 @@ class TestIcode3Counter:
|
||||
assert counter == 10
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -177,7 +177,7 @@ class TestIcode3Counter:
|
||||
assert counter == 0xFFFFFF
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -198,7 +198,7 @@ class TestIcode3Counter:
|
||||
class TestIcode3Config:
|
||||
def test_read_config(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -211,7 +211,7 @@ class TestIcode3Config:
|
||||
|
||||
def test_write_config(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -228,7 +228,7 @@ class TestIcode3Config:
|
||||
|
||||
def test_read_config_passwords_masked(self):
|
||||
"""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),
|
||||
read_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -242,7 +242,7 @@ class TestIcode3Config:
|
||||
|
||||
def test_write_config_requires_auth_when_protected(self):
|
||||
"""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),
|
||||
config_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -254,7 +254,7 @@ class TestIcode3Config:
|
||||
|
||||
def test_read_config_multiple_blocks(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -273,7 +273,7 @@ class TestIcode3Config:
|
||||
class TestIcode3TagTamper:
|
||||
def test_read_tt_closed(self):
|
||||
"""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)
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -287,7 +287,7 @@ class TestIcode3TagTamper:
|
||||
|
||||
def test_read_tt_open(self):
|
||||
"""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)
|
||||
run(tag.power_on())
|
||||
tag._tt_status_actual = 0x4F # 'O' = Open
|
||||
@@ -298,7 +298,7 @@ class TestIcode3TagTamper:
|
||||
|
||||
def test_read_tt_not_supported_without_flag(self):
|
||||
"""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)
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -314,7 +314,7 @@ class TestIcode3TagTamper:
|
||||
class TestIcode3SystemInfo:
|
||||
def test_feature_flags(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -338,7 +338,7 @@ class TestIcode3SystemInfo:
|
||||
class TestIcode3NfcMirror:
|
||||
def test_uid_mirror(self):
|
||||
"""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")
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -354,7 +354,7 @@ class TestIcode3NfcMirror:
|
||||
|
||||
def test_uid_counter_mirror(self):
|
||||
"""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")
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -380,7 +380,7 @@ class TestIcode3NfcMirror:
|
||||
|
||||
def test_mirror_disabled(self):
|
||||
"""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")
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -397,7 +397,7 @@ class TestIcode3NfcMirror:
|
||||
|
||||
def test_mirror_with_byte_offset(self):
|
||||
"""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")
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -417,7 +417,7 @@ class TestIcode3NfcMirror:
|
||||
|
||||
def test_mirror_outside_block_returns_physical(self):
|
||||
"""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")
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -438,7 +438,7 @@ class TestIcode3NfcMirror:
|
||||
class TestIcode3PrivacyMode2:
|
||||
def test_privacy_mode2_inventory_zeroed_uid(self):
|
||||
"""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",
|
||||
privacy_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -455,7 +455,7 @@ class TestIcode3PrivacyMode2:
|
||||
|
||||
def test_privacy_mode2_allows_reads(self):
|
||||
"""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",
|
||||
privacy_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -475,7 +475,7 @@ class TestIcode3PrivacyMode2:
|
||||
|
||||
def test_privacy_mode1_blocks_reads(self):
|
||||
"""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",
|
||||
privacy_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -489,7 +489,7 @@ class TestIcode3PrivacyMode2:
|
||||
|
||||
def test_pick_random_id(self):
|
||||
"""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",
|
||||
privacy_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -514,7 +514,7 @@ class TestIcode3PrivacyMode2:
|
||||
|
||||
def test_pick_random_id_fails_outside_privacy(self):
|
||||
"""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")
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -530,7 +530,7 @@ class TestIcode3PrivacyMode2:
|
||||
class TestIcode3Signature48:
|
||||
def test_default_32_byte_signature(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -542,7 +542,7 @@ class TestIcode3Signature48:
|
||||
|
||||
def test_48_byte_signature_mode(self):
|
||||
"""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))
|
||||
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48)
|
||||
run(tag.power_on())
|
||||
@@ -557,7 +557,7 @@ class TestIcode3Signature48:
|
||||
|
||||
def test_48_byte_mode_disabled_truncates(self):
|
||||
"""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))
|
||||
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48)
|
||||
run(tag.power_on())
|
||||
@@ -574,7 +574,7 @@ class TestIcode3Signature48:
|
||||
class TestIcode3CounterPreset:
|
||||
def test_value_0x000001_increments(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -590,7 +590,7 @@ class TestIcode3CounterPreset:
|
||||
|
||||
def test_value_not_0x000001_presets(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -610,7 +610,7 @@ class TestIcode3CounterPreset:
|
||||
|
||||
def test_0x000001_twice_increments_twice(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -625,7 +625,7 @@ class TestIcode3CounterPreset:
|
||||
|
||||
def test_prot_byte_written(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import struct
|
||||
import pytest
|
||||
|
||||
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_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID,
|
||||
CMD_INVENTORY_READ, CMD_FAST_INVENTORY_READ,
|
||||
@@ -45,23 +45,23 @@ PWD_EAS_AFI = 0x10
|
||||
class TestIcodeSlixBasics:
|
||||
def test_import(self):
|
||||
"""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):
|
||||
"""SLIX inherits from NxpIcodeTag."""
|
||||
from pm3py.sim.icode_slix import IcodeSlixTag
|
||||
from pm3py.sim.nxp_icode import NxpIcodeTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
|
||||
assert issubclass(IcodeSlixTag, NxpIcodeTag)
|
||||
|
||||
def test_default_28_blocks(self):
|
||||
"""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))
|
||||
assert tag._num_blocks == 28
|
||||
assert tag._block_size == 4
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
inv = bytes([0x26, 0x01, 0x00])
|
||||
@@ -69,7 +69,7 @@ class TestIcodeSlixBasics:
|
||||
assert resp is not None
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
# Write block 5
|
||||
@@ -89,7 +89,7 @@ class TestIcodeSlixBasics:
|
||||
class TestIcodeSlixPassword:
|
||||
def test_set_password_eas_afi(self):
|
||||
"""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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -97,7 +97,7 @@ class TestIcodeSlixPassword:
|
||||
|
||||
def test_set_password_wrong(self):
|
||||
"""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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -108,7 +108,7 @@ class TestIcodeSlixPassword:
|
||||
|
||||
def test_write_password(self):
|
||||
"""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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -122,7 +122,7 @@ class TestIcodeSlixPassword:
|
||||
|
||||
def test_lock_password(self):
|
||||
"""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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -139,7 +139,7 @@ class TestIcodeSlixPassword:
|
||||
|
||||
def test_no_privacy_password(self):
|
||||
"""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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -156,7 +156,7 @@ class TestIcodeSlixPassword:
|
||||
|
||||
class TestIcodeSlixEas:
|
||||
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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
|
||||
@@ -165,7 +165,7 @@ class TestIcodeSlixEas:
|
||||
assert tag.eas_enabled
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
tag.set_eas(True)
|
||||
@@ -175,7 +175,7 @@ class TestIcodeSlixEas:
|
||||
assert not tag.eas_enabled
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
tag.set_eas(True)
|
||||
@@ -186,7 +186,7 @@ class TestIcodeSlixEas:
|
||||
assert len(resp.data) >= 33
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
|
||||
@@ -194,7 +194,7 @@ class TestIcodeSlixEas:
|
||||
assert resp is None
|
||||
|
||||
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))
|
||||
run(tag.power_on())
|
||||
tag.set_eas(True)
|
||||
@@ -207,7 +207,7 @@ class TestIcodeSlixEas:
|
||||
assert resp.data[0] & 0x01
|
||||
|
||||
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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -219,7 +219,7 @@ class TestIcodeSlixEas:
|
||||
|
||||
def test_write_eas_id(self):
|
||||
"""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),
|
||||
eas_afi_password=0xAABBCCDD)
|
||||
run(tag.power_on())
|
||||
@@ -231,7 +231,7 @@ class TestIcodeSlixEas:
|
||||
|
||||
def test_slix_rejects_write_eas_id(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
|
||||
@@ -245,7 +245,7 @@ class TestIcodeSlixEas:
|
||||
|
||||
class TestIcodeSlixInventoryRead:
|
||||
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")
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x06, CMD_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
|
||||
@@ -254,7 +254,7 @@ class TestIcodeSlixInventoryRead:
|
||||
assert resp.data[0] == 0x00
|
||||
|
||||
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")
|
||||
run(tag.power_on())
|
||||
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."""
|
||||
|
||||
def test_slix2_inherits_slix(self):
|
||||
from pm3py.sim.icode_slix import IcodeSlixTag
|
||||
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||||
assert issubclass(IcodeSlix2Tag, IcodeSlixTag)
|
||||
|
||||
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),
|
||||
privacy_password=0xDEADBEEF)
|
||||
run(tag.power_on())
|
||||
@@ -284,7 +284,7 @@ class TestSlix2StillWorks:
|
||||
assert tag.privacy_mode
|
||||
|
||||
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),
|
||||
destroy_password=0xDEAD1234)
|
||||
run(tag.power_on())
|
||||
@@ -298,7 +298,7 @@ class TestSlix2StillWorks:
|
||||
|
||||
def test_slix2_eas_inherited_from_slix(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
|
||||
@@ -314,7 +314,7 @@ class TestSlix2StillWorks:
|
||||
class TestSlixLacksPrivacy:
|
||||
def test_no_enable_privacy(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, 0xBA, 0x04, 0x00, 0x00, 0x00, 0x00])
|
||||
@@ -324,7 +324,7 @@ class TestSlixLacksPrivacy:
|
||||
|
||||
def test_no_destroy(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, 0xB9, 0x04, 0x00, 0x00, 0x00, 0x00])
|
||||
@@ -333,7 +333,7 @@ class TestSlixLacksPrivacy:
|
||||
|
||||
def test_no_protect_page(self):
|
||||
"""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))
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01])
|
||||
|
||||
@@ -3,12 +3,12 @@ import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.implants import (
|
||||
from pm3py.transponders.implants import (
|
||||
xEM, xNT, xM1, FlexDF, NExT,
|
||||
MagicMifareClassicTag,
|
||||
)
|
||||
from pm3py.sim.em4100 import EM4100Reader
|
||||
from pm3py.sim.iso14443a import Reader14443A, _compute_bcc
|
||||
from pm3py.transponders.lf.em.em4100 import EM4100Reader
|
||||
from pm3py.transponders.hf.iso14443a.base import Reader14443A, _compute_bcc
|
||||
|
||||
|
||||
def run(coro):
|
||||
@@ -36,7 +36,7 @@ class TestXNT:
|
||||
assert tag._total_pages >= 45 # NTAG213+ size
|
||||
|
||||
def test_is_type2_tag(self):
|
||||
from pm3py.sim.ndef import NfcType2Tag
|
||||
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag
|
||||
tag = xNT()
|
||||
assert isinstance(tag, NfcType2Tag)
|
||||
|
||||
@@ -77,7 +77,7 @@ class TestMagicMifareClassic:
|
||||
|
||||
class TestFlexDF:
|
||||
def test_creates_desfire(self):
|
||||
from pm3py.sim.desfire import DesfireTag
|
||||
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag
|
||||
tag = FlexDF()
|
||||
assert isinstance(tag, DesfireTag)
|
||||
|
||||
@@ -92,7 +92,7 @@ class TestNExT:
|
||||
# LF side is T5577 configured as EM4100
|
||||
assert lf.blocks[0] != 0
|
||||
# HF side is NfcType2Tag (xNT)
|
||||
from pm3py.sim.ndef import NfcType2Tag
|
||||
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag
|
||||
assert isinstance(hf, NfcType2Tag)
|
||||
|
||||
def test_both_sides_work(self):
|
||||
|
||||
@@ -4,10 +4,10 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.lf_base import TagLF, ReaderLF, Modulation
|
||||
from pm3py.sim.em4100 import EM4100Tag, EM4100Reader
|
||||
from pm3py.sim.hid import HIDProxTag, HIDReader
|
||||
from pm3py.sim.t5577 import T5577Tag, T5577Reader
|
||||
from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation
|
||||
from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader
|
||||
from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
|
||||
from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader
|
||||
|
||||
|
||||
def run(coro):
|
||||
|
||||
@@ -140,14 +140,14 @@ class TestTransponderRegions:
|
||||
|
||||
class TestTag15693Regions:
|
||||
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)
|
||||
assert "user" in tag.regions
|
||||
assert tag.regions["user"].block_size == 4
|
||||
assert tag.regions["user"].eml_offset == 175
|
||||
|
||||
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._memory[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:
|
||||
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,
|
||||
read_password=0xAABBCCDD)
|
||||
user = tag.regions["user"]
|
||||
@@ -166,7 +166,7 @@ class TestIcodeSlix2AccessMap:
|
||||
assert user.access_for_block(4).read_key == 1
|
||||
|
||||
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))
|
||||
user = tag.regions["user"]
|
||||
assert user.access_for_block(0).read == "open"
|
||||
@@ -177,7 +177,7 @@ class TestIcodeSlix2AccessMap:
|
||||
|
||||
class TestCompileFromRegions:
|
||||
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
|
||||
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=4)
|
||||
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"
|
||||
|
||||
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
|
||||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2,
|
||||
read_password=0xAABBCCDD, block_size=4, num_blocks=4)
|
||||
@@ -198,7 +198,7 @@ class TestCompileFromRegions:
|
||||
assert table.lookup(bytes([0x02, 0x20, 0x02])) is None
|
||||
|
||||
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
|
||||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2,
|
||||
read_password=0xAABBCCDD, block_size=4, num_blocks=4)
|
||||
|
||||
@@ -5,9 +5,9 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.crypto1 import Crypto1
|
||||
from pm3py.sim.mifare import MifareClassicTag, MifareClassicReader
|
||||
from pm3py.sim.iso14443a import Reader14443A, REQA, CL1, NVB_SELECT, _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
|
||||
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag, MifareClassicReader
|
||||
from pm3py.transponders.hf.iso14443a.base import Reader14443A, REQA, CL1, NVB_SELECT, _compute_bcc
|
||||
|
||||
|
||||
def run(coro):
|
||||
|
||||
@@ -4,8 +4,8 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.iso14443a import Reader14443A, _compute_bcc
|
||||
from pm3py.sim.ndef import NfcType2Tag, NfcType4Tag
|
||||
from pm3py.transponders.hf.iso14443a.base import Reader14443A, _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag
|
||||
|
||||
|
||||
def run(coro):
|
||||
|
||||
@@ -15,45 +15,45 @@ def run(coro):
|
||||
|
||||
class TestIcodeDna:
|
||||
def test_inherits_slix2(self):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||||
assert issubclass(IcodeDnaTag, IcodeSlix2Tag)
|
||||
|
||||
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()
|
||||
assert tag._num_blocks == 64
|
||||
|
||||
def test_uid_prefix(self):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
tag = IcodeDnaTag()
|
||||
assert tag._uid[:4] == b"\xE0\x04\x01\x18"
|
||||
|
||||
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()
|
||||
user = tag.regions["user"]
|
||||
assert user.access_for_block(63).write_mode == "counter"
|
||||
|
||||
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()
|
||||
assert len(tag._aes_keys) == 4
|
||||
assert all(k is None for k in tag._aes_keys)
|
||||
|
||||
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
|
||||
tag = IcodeDnaTag(aes_keys=[key0, None, None, None])
|
||||
assert tag._aes_keys[0] == key0
|
||||
|
||||
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()
|
||||
assert len(tag._config_memory) == 48 * 4
|
||||
|
||||
def test_read_config(self):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
tag = IcodeDnaTag()
|
||||
run(tag.power_on())
|
||||
# Read config header block 0x08
|
||||
@@ -64,7 +64,7 @@ class TestIcodeDna:
|
||||
assert resp.data[1] == 0x81 # CONFIG_HEADER_WRITABLE default
|
||||
|
||||
def test_write_config(self):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
tag = IcodeDnaTag()
|
||||
run(tag.power_on())
|
||||
# Write CID to block 0x09
|
||||
@@ -78,7 +78,7 @@ class TestIcodeDna:
|
||||
|
||||
def test_key_storage_masked_when_active(self):
|
||||
"""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])
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -95,7 +95,7 @@ class TestIcodeDna:
|
||||
|
||||
def test_key_write_rejected_when_active(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
tag._key_headers[0] = KEY_HEADER_ACTIVE_LOCKED
|
||||
@@ -106,7 +106,7 @@ class TestIcodeDna:
|
||||
|
||||
def test_key_header_one_way(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestIcodeDna:
|
||||
|
||||
def test_gch_one_way(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -129,19 +129,19 @@ class TestIcodeDna:
|
||||
assert tag._nfc_gch == GCH_ACTIVATED
|
||||
|
||||
def test_cid_default(self):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
tag = IcodeDnaTag()
|
||||
assert tag._cid == 0xC000
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00]))))
|
||||
assert resp is not None
|
||||
|
||||
def test_inherits_eas(self):
|
||||
from pm3py.sim.icode_dna import IcodeDnaTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||
tag = IcodeDnaTag()
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, 0xA2, 0x04])
|
||||
@@ -156,12 +156,12 @@ class TestIcodeDna:
|
||||
|
||||
class TestNtag5Platform:
|
||||
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)
|
||||
assert len(tag._config_memory) >= 160 * 4
|
||||
|
||||
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)
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, 0xC0, 0x04, 0x3F, 0x00])
|
||||
@@ -171,7 +171,7 @@ class TestNtag5Platform:
|
||||
assert resp.data[1] == 0xA5 # DEV_SEC_CONFIG default
|
||||
|
||||
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)
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x02, 0xC1, 0x04, 0x57, 0x34, 0x12, 0x00, 0x00])
|
||||
@@ -189,36 +189,36 @@ class TestNtag5Platform:
|
||||
|
||||
class TestNtag5Switch:
|
||||
def test_inherits_platform(self):
|
||||
from pm3py.sim.ntag5_switch import Ntag5SwitchTag
|
||||
from pm3py.sim.ntag5_platform import Ntag5PlatformTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
|
||||
assert issubclass(Ntag5SwitchTag, Ntag5PlatformTag)
|
||||
|
||||
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()
|
||||
assert tag._num_blocks == 128
|
||||
|
||||
def test_no_aes(self):
|
||||
from pm3py.sim.ntag5_switch import Ntag5SwitchTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
|
||||
tag = Ntag5SwitchTag()
|
||||
assert not tag._aes_capable
|
||||
|
||||
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()
|
||||
user = tag.regions["user"]
|
||||
assert user.access_for_block(127).write_mode == "counter"
|
||||
assert user.access_for_block(0).write_mode == "normal"
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00]))))
|
||||
assert resp is not None
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x22, 0xBC]) + tag._uid[::-1] + bytes([0x04])
|
||||
@@ -233,33 +233,33 @@ class TestNtag5Switch:
|
||||
|
||||
class TestNtag5Link:
|
||||
def test_inherits_platform(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.sim.ntag5_platform import Ntag5PlatformTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag
|
||||
assert issubclass(Ntag5LinkTag, Ntag5PlatformTag)
|
||||
|
||||
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()
|
||||
assert tag._num_blocks == 512
|
||||
|
||||
def test_i2c_address(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
tag = Ntag5LinkTag()
|
||||
assert tag.i2c_address == 0x54
|
||||
|
||||
def test_sram(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
tag = Ntag5LinkTag()
|
||||
assert len(tag.sram) == 256
|
||||
|
||||
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.i2c_write(0x2000, b"\xAA\xBB\xCC\xDD")
|
||||
assert tag.i2c_read(0x2000, 4) == b"\xAA\xBB\xCC\xDD"
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
tag._sram_enabled = True
|
||||
@@ -270,12 +270,12 @@ class TestNtag5Link:
|
||||
assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
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)
|
||||
assert not tag._aes_capable
|
||||
|
||||
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)
|
||||
assert tag._aes_capable
|
||||
|
||||
@@ -286,33 +286,33 @@ class TestNtag5Link:
|
||||
|
||||
class TestArbitration:
|
||||
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()
|
||||
assert tag.acquire_nfc() is True
|
||||
assert tag.nfc_if_locked is True
|
||||
|
||||
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.set_i2c_locked(True)
|
||||
assert tag.acquire_nfc() is False
|
||||
assert tag.nfc_if_locked is False
|
||||
|
||||
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()
|
||||
assert tag.acquire_i2c() is True
|
||||
assert tag.i2c_if_locked is True
|
||||
|
||||
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.set_nfc_locked(True)
|
||||
assert tag.acquire_i2c() is False
|
||||
assert tag.i2c_if_locked is False
|
||||
|
||||
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.acquire_nfc()
|
||||
assert tag.nfc_if_locked is True
|
||||
@@ -325,7 +325,7 @@ class TestArbitration:
|
||||
assert tag.i2c_if_locked is False
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
tag._sram_enabled = True
|
||||
@@ -339,7 +339,7 @@ class TestArbitration:
|
||||
assert resp.data[1] == 0x0F
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
tag._sram_enabled = True
|
||||
@@ -352,21 +352,21 @@ class TestArbitration:
|
||||
assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
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.set_nfc_locked(True)
|
||||
result = tag.i2c_read(0x2000, 4)
|
||||
assert result is None
|
||||
|
||||
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.set_nfc_locked(True)
|
||||
result = tag.i2c_write(0x2000, b"\x01\x02\x03\x04")
|
||||
assert result is False
|
||||
|
||||
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()
|
||||
|
||||
# NFC acquires, writes via SRAM directly, releases
|
||||
@@ -391,27 +391,27 @@ class TestArbitration:
|
||||
|
||||
class TestNtag5Boost:
|
||||
def test_inherits_link(self):
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
assert issubclass(Ntag5BoostTag, Ntag5LinkTag)
|
||||
|
||||
def test_aes_always(self):
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
assert tag._aes_capable
|
||||
|
||||
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()
|
||||
assert tag._num_blocks == 512
|
||||
|
||||
def test_has_sram(self):
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
assert len(tag.sram) == 256
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
cmd = bytes([0x22, 0xBC]) + tag._uid[::-1] + bytes([0x04])
|
||||
@@ -428,7 +428,7 @@ class TestSessionRegisters:
|
||||
"""Session register state machine for NTAG 5 platform."""
|
||||
|
||||
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)
|
||||
run(tag.power_on())
|
||||
return tag
|
||||
@@ -553,8 +553,8 @@ class TestNtag5Aes:
|
||||
|
||||
def test_link_5332_tam1(self):
|
||||
"""TAM1 (CHALLENGE + READBUFFER) works on Link NTP5332 (aes_capable=True)."""
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
key = b"\xCC" * 16
|
||||
@@ -586,8 +586,8 @@ class TestNtag5Aes:
|
||||
|
||||
def test_link_5332_tam1_readbuffer_one_shot(self):
|
||||
"""READBUFFER is one-shot — second read returns None."""
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
|
||||
key = b"\xCC" * 16
|
||||
tag = Ntag5LinkTag(aes_capable=True)
|
||||
@@ -607,7 +607,7 @@ class TestNtag5Aes:
|
||||
|
||||
def test_switch_no_aes_challenge_rejected(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
@@ -620,7 +620,7 @@ class TestNtag5Aes:
|
||||
|
||||
def test_link_no_aes_challenge_rejected(self):
|
||||
"""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)
|
||||
run(tag.power_on())
|
||||
@@ -631,8 +631,8 @@ class TestNtag5Aes:
|
||||
|
||||
def test_boost_mam1(self):
|
||||
"""MAM1 (AUTHENTICATE with AuthMethod=0x02) works on Boost."""
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
key = b"\xDD" * 16
|
||||
@@ -663,8 +663,8 @@ class TestNtag5Aes:
|
||||
|
||||
def test_boost_mam2(self):
|
||||
"""Full MAM1 + MAM2 mutual authentication on Boost."""
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
key = b"\xEE" * 16
|
||||
@@ -708,8 +708,8 @@ class TestNtag5Aes:
|
||||
|
||||
def test_boost_mam2_wrong_key_fails(self):
|
||||
"""MAM2 with wrong key is rejected."""
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.sim.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import KEY_HEADER_ACTIVE_LOCKED
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
key = b"\xEE" * 16
|
||||
@@ -735,7 +735,7 @@ class TestNtag5Aes:
|
||||
|
||||
def test_link_no_aes_authenticate_rejected(self):
|
||||
"""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)
|
||||
run(tag.power_on())
|
||||
@@ -748,7 +748,7 @@ class TestNtag5Aes:
|
||||
|
||||
def test_boost_inherits_slix2_commands(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
@@ -772,13 +772,13 @@ class TestConfigReg:
|
||||
"""CONFIG_REG (session register block 0xA1) bit-field accessors and enforcement."""
|
||||
|
||||
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)
|
||||
run(tag.power_on())
|
||||
return tag
|
||||
|
||||
def _make_link_tag(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
tag = Ntag5LinkTag()
|
||||
run(tag.power_on())
|
||||
return tag
|
||||
@@ -869,7 +869,7 @@ class TestPassThrough:
|
||||
"""SRAM pass-through mode (arbiter_mode == 2)."""
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -946,7 +946,7 @@ class TestPassThrough:
|
||||
|
||||
def test_passthrough_not_active_in_normal_mode(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -958,7 +958,7 @@ class TestPassThrough:
|
||||
|
||||
def test_passthrough_not_active_in_mirror_mode(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -998,7 +998,7 @@ class TestSynchDataBlock:
|
||||
"""Synch data block triggers status register flags on access."""
|
||||
|
||||
def _make_tag(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
tag = Ntag5LinkTag()
|
||||
run(tag.power_on())
|
||||
return tag
|
||||
@@ -1075,7 +1075,7 @@ class TestEventDetection:
|
||||
"""ED pin callbacks and trigger logic for NTAG 5 platform."""
|
||||
|
||||
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)
|
||||
run(tag.power_on())
|
||||
return tag
|
||||
@@ -1178,7 +1178,7 @@ class TestReadSram:
|
||||
"""Direct SRAM read via NXP custom command 0xD2."""
|
||||
|
||||
def _make_tag(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
tag = Ntag5LinkTag()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -1216,7 +1216,7 @@ class TestReadSram:
|
||||
|
||||
def test_error_when_sram_disabled(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
# sram_enable defaults to False
|
||||
@@ -1248,7 +1248,7 @@ class TestWriteSram:
|
||||
"""Direct SRAM write via NXP custom command 0xD3."""
|
||||
|
||||
def _make_tag(self):
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||
tag = Ntag5LinkTag()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -1275,7 +1275,7 @@ class TestWriteSram:
|
||||
|
||||
def test_error_when_sram_disabled(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
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):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -1411,7 +1411,7 @@ class TestDualInterfaceIntegration:
|
||||
|
||||
def test_session_register_consistency(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
tag.sram_enable = True
|
||||
@@ -1473,7 +1473,7 @@ class TestDualInterfaceIntegration:
|
||||
|
||||
def test_ed_pin_fires_on_synch_data_block_access(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
|
||||
@@ -1516,8 +1516,8 @@ class TestNtag5Area1Passwords:
|
||||
"""AREA_1 password IDs (0x40/0x80) per NTP5210/NTA5332 datasheets."""
|
||||
|
||||
def test_area1_password_ids_exist_on_switch(self):
|
||||
from pm3py.sim.ntag5_switch import Ntag5SwitchTag
|
||||
from pm3py.sim.ntag5_platform import PWD_AREA1_READ, PWD_AREA1_WRITE
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import PWD_AREA1_READ, PWD_AREA1_WRITE
|
||||
tag = Ntag5SwitchTag()
|
||||
assert PWD_AREA1_READ == 0x40
|
||||
assert PWD_AREA1_WRITE == 0x80
|
||||
@@ -1532,7 +1532,7 @@ class TestNtag5PickRandomUid:
|
||||
"""PICK RANDOM UID (0xC2) per NTA5332 datasheet 8.2.4.9.15."""
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
tag._privacy_mode = True
|
||||
@@ -1546,7 +1546,7 @@ class TestNtag5PickRandomUid:
|
||||
assert tag._random_uid[:2] == b"\xE0\x04"
|
||||
|
||||
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()
|
||||
run(tag.power_on())
|
||||
assert tag._privacy_mode is False
|
||||
@@ -1559,7 +1559,7 @@ class TestNtag5PickRandomUid:
|
||||
|
||||
def test_switch_rejects_pick_random_uid(self):
|
||||
"""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()
|
||||
run(tag.power_on())
|
||||
# Test outside privacy mode — should get error via custom command dispatch
|
||||
@@ -1580,7 +1580,7 @@ class TestExtendedCommands:
|
||||
def test_extended_read_single_block(self):
|
||||
"""Read block 256 (beyond 1-byte range) using extended read single."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
# Write known data to block 256
|
||||
@@ -1597,7 +1597,7 @@ class TestExtendedCommands:
|
||||
def test_extended_read_single_block_addressed(self):
|
||||
"""Extended read single in addressed mode."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
offset = 300 * tag._block_size
|
||||
@@ -1612,7 +1612,7 @@ class TestExtendedCommands:
|
||||
def test_extended_write_single_block(self):
|
||||
"""Write to block 300 using extended write, then read back."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
block_le = struct.pack("<H", 300)
|
||||
@@ -1629,7 +1629,7 @@ class TestExtendedCommands:
|
||||
def test_extended_read_multiple_blocks(self):
|
||||
"""Read 3 blocks starting at block 256."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
bs = tag._block_size
|
||||
@@ -1649,7 +1649,7 @@ class TestExtendedCommands:
|
||||
def test_extended_read_last_block_boost(self):
|
||||
"""Block 511 (last block of Boost) is readable via extended read."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
last = tag._num_blocks - 1 # 511
|
||||
@@ -1664,7 +1664,7 @@ class TestExtendedCommands:
|
||||
def test_extended_read_out_of_range(self):
|
||||
"""Block 512 on Boost (512 blocks) returns error."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
block_le = struct.pack("<H", 512)
|
||||
@@ -1677,7 +1677,7 @@ class TestExtendedCommands:
|
||||
def test_extended_read_multiple_overflow(self):
|
||||
"""Extended read multiple that would go past end returns error."""
|
||||
import struct
|
||||
from pm3py.sim.ntag5_boost import Ntag5BoostTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||
tag = Ntag5BoostTag()
|
||||
run(tag.power_on())
|
||||
block_le = struct.pack("<H", 510)
|
||||
|
||||
@@ -5,18 +5,18 @@ import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.type5 import NfcType5Tag
|
||||
from pm3py.sim.nxp_icode import NxpIcodeTag
|
||||
from pm3py.sim.icode_slix import IcodeSlixTag
|
||||
from pm3py.sim.icode_slix2 import (
|
||||
from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import (
|
||||
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_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID, CMD_INVENTORY_READ,
|
||||
CMD_FAST_INVENTORY_READ,
|
||||
)
|
||||
from pm3py.sim.iso15693 import Reader15693
|
||||
from pm3py.transponders.hf.iso15693.base import Reader15693
|
||||
|
||||
|
||||
def run(coro):
|
||||
|
||||
@@ -5,16 +5,16 @@ import pytest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.iso14443a import Tag14443A_3, Tag14443A_4, _compute_bcc
|
||||
from pm3py.sim.mifare import MifareClassicTag
|
||||
from pm3py.sim.iso15693 import Tag15693
|
||||
from pm3py.sim.desfire import DesfireTag
|
||||
from pm3py.transponders.hf.iso14443a.base import Tag14443A_3, Tag14443A_4, _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag
|
||||
from pm3py.transponders.hf.iso15693.base import Tag15693
|
||||
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag
|
||||
from pm3py.sim.table_compiler import (
|
||||
TableEntry, ResponseTable, TableCompiler,
|
||||
EML_READ, EML_WRITE, MATCH_PREFIX,
|
||||
)
|
||||
from pm3py.sim.nxp_icode import NxpIcodeTag
|
||||
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||||
|
||||
|
||||
def run(coro):
|
||||
@@ -415,14 +415,14 @@ class TestTableCompilerEasAlarm:
|
||||
|
||||
class TestTableCompilerIcode3:
|
||||
def test_compile_icode3_exists(self):
|
||||
from pm3py.sim.icode3 import Icode3Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
||||
tag = Icode3Tag()
|
||||
table = TableCompiler.compile_icode3(tag)
|
||||
assert len(table.entries) > 0
|
||||
|
||||
def test_compile_icode3_includes_slix2_entries(self):
|
||||
"""ICODE 3 table includes SLIX2 base entries."""
|
||||
from pm3py.sim.icode3 import Icode3Tag
|
||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
||||
tag = Icode3Tag()
|
||||
table = TableCompiler.compile_icode3(tag)
|
||||
# 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):
|
||||
"""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()
|
||||
table = TableCompiler.compile_icode3(tag)
|
||||
entry = table.lookup(bytes([0x02, 0xAB]))
|
||||
@@ -444,7 +444,7 @@ class TestTableCompilerIcode3:
|
||||
|
||||
def test_compile_icode3_config_password(self):
|
||||
"""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)
|
||||
table = TableCompiler.compile_icode3(tag)
|
||||
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):
|
||||
"""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 unittest.mock import MagicMock
|
||||
|
||||
@@ -478,14 +478,14 @@ class TestTableCompilerIcode3:
|
||||
|
||||
class TestTableCompilerDna:
|
||||
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()
|
||||
table = TableCompiler.compile_icode_dna(tag)
|
||||
assert len(table.entries) > 0
|
||||
|
||||
def test_compile_includes_base_entries(self):
|
||||
"""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()
|
||||
table = TableCompiler.compile_icode_dna(tag)
|
||||
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):
|
||||
"""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()
|
||||
table = TableCompiler.compile_icode_dna(tag)
|
||||
entry = table.lookup(bytes([0x02, 0xAB]))
|
||||
@@ -507,7 +507,7 @@ class TestTableCompilerDna:
|
||||
|
||||
def test_compile_has_eas(self):
|
||||
"""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.set_eas(True)
|
||||
table = TableCompiler.compile_icode_dna(tag)
|
||||
@@ -516,7 +516,7 @@ class TestTableCompilerDna:
|
||||
|
||||
def test_session_dispatches_dna(self):
|
||||
"""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 unittest.mock import MagicMock
|
||||
|
||||
@@ -538,7 +538,7 @@ class TestTableCompilerDna:
|
||||
class TestSimSessionTableCompile:
|
||||
def test_start_15693_compiles_nxp_table(self):
|
||||
"""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 unittest.mock import MagicMock
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import os
|
||||
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:
|
||||
@@ -391,7 +393,7 @@ class TestTraceFormatterWrapping:
|
||||
|
||||
def test_long_annotation_wraps_to_next_line(self):
|
||||
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)
|
||||
tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
|
||||
payload = bytes([0x00]) + cc + tlv
|
||||
@@ -490,44 +492,44 @@ class TestTransponderDecodeTrace:
|
||||
assert hasattr(Transponder, 'decode_trace')
|
||||
|
||||
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))
|
||||
result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00]))
|
||||
assert result == "INVENTORY"
|
||||
|
||||
def test_tag15693_decodes_nxp_names(self):
|
||||
"""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))
|
||||
result = tag.decode_trace(0, bytes([0x22, 0xA1, 0x04, 0x02]))
|
||||
assert result == "NXP FAST INVENTORY READ"
|
||||
|
||||
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))
|
||||
result = tag.decode_trace(0, bytes([0x22, 0xA1, 0x04, 0x02]))
|
||||
assert result == "NXP FAST INVENTORY READ"
|
||||
|
||||
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))
|
||||
result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00]))
|
||||
assert result == "INVENTORY"
|
||||
|
||||
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))
|
||||
result = tag.decode_trace(0, bytes([0x22, 0xB2, 0x04]))
|
||||
assert result == "NXP GET RANDOM"
|
||||
|
||||
def test_tag14443a_decodes(self):
|
||||
from pm3py.sim.iso14443a import Tag14443A
|
||||
from pm3py.transponders.hf.iso14443a.base import Tag14443A
|
||||
tag = Tag14443A(uid=bytes(4))
|
||||
result = tag.decode_trace(0, bytes([0x26]))
|
||||
assert result == "REQA"
|
||||
|
||||
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))
|
||||
result = tag.decode_trace(0, bytes([0x26, 0x01, 0x00]))
|
||||
# 14443a decoder sees 0x26 as REQA (single byte check happens first)
|
||||
|
||||
327
tests/test_sniff_session.py
Normal file
327
tests/test_sniff_session.py
Normal 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
|
||||
Reference in New Issue
Block a user