Commit Graph
23349 Commits
Author SHA1 Message Date
歐歪 dfa3255fd6 fix: refine hint messages for V3 tag finalization status 2026-09-22 16:30:43 +08:00
歐歪andClaude Opus 5 f0a502b279 refactor: move ISO15693 magic tag handling to the device side
Identification, writing and failure handling for magic Gen1 and V3 tags
now runs in a single field session on the device, which reports
PM3_SUCCESS only when the UID reads back as requested.

armsrc/iso15693.c:
- SetTag15693Uid() probes 0x38/0x39/0x3E/0x3F and refuses to write when
  any of them answers with data, verifies the UID afterwards and blanks
  the four blocks again when it did not change.
- SetTag15693Uid_v3() and FinalizeTag15693_v3() are new, both check the
  0x14/0x15 configuration signature before writing and verify after.

client/src/cmdhf15.c:
- csetuid and cfinalize now only select a command and print the status.
  The V3 block helpers and constants move to armsrc, which restores the
  original read helper with its CALLOC message and the
  ISO15_ERROR_HANDLING_* macros.

Behaviour change: hf 15 cfinalize without -y no longer energizes the tag
and no longer reports configuration mode before asking for confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:04:16 +08:00
歐歪andClaude Sonnet 5 e74bb0f2e5 fix: 15693 csetuid v1 probe, single session write and safer rollback
- probe: only no answer / block not available count as unreadable, other read errors abort
- 69960000 is only accepted in 0x3F, the other blocks must be blank
- write 0x3E/0x3F/0x38/0x39 in one RF session
- rollback only when the tag still shows its original UID, message lists all four blocks

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:25:47 +08:00
歐歪 cd24d8b0b0 fix: rewind if 0x38 0x39 write fails 2026-09-21 18:27:45 +08:00
歐歪 f7a9682a9e fix: silent v1 pre-check and refine the commands 2026-09-21 18:06:42 +08:00
歐歪 473dcb0aa5 fix: v1 tag probing fail message clear 2026-09-21 17:34:37 +08:00
歐歪 e1e023ea61 feat: 15693 csetuid v1 v3 check before write 2026-09-21 17:11:23 +08:00
Philippe Teuwen 90c41c6de5 use arraylen 2026-09-17 00:23:18 +02:00
Mistial DeveloperandGPT-Daybreak Blue 8ed285af90 feat(desfire): add DFC credential interoperability
Add bidirectional conversion between DFC v4 credentials and mfdes v1 JSON, with documentation and round-trip coverage. Include H10301 FC69 CN420 factory and field fixtures and an external HID reader test target.

Co-Authored-By: GPT-Daybreak Blue <noreply@openai.com>
2026-09-17 00:08:06 +02:00
Mistial DeveloperandGPT-Daybreak Blue 957896a926 build(desfire): make emulator test hook optional
Exclude the USB test dispatcher and queued-random support from normal firmware builds. Enable them with ENABLE_HFMFDESETEST=1 when running emulator vectors.

Co-Authored-By: GPT-Daybreak Blue <noreply@openai.com>
2026-09-17 00:08:06 +02:00
Mistial DeveloperandGPT-Daybreak Blue 50f6b76469 feat(desfire): handle ISO 7816 selection in simulator
Recognize interindustry SELECT APDUs by DF name or file identifier and return card-compatible status words.

Co-Authored-By: GPT-Daybreak Blue <noreply@openai.com>
2026-09-17 00:08:06 +02:00
Mistial DeveloperandGPT-Daybreak Blue a8e347ddb9 fix(desfire): stabilize simulator RF exchanges
Constrain activation to supported bit rates, ignore invalid polling and frames, and follow independent ISO-DEP block sequences during recovery.

Co-Authored-By: GPT-Daybreak Blue <noreply@openai.com>
2026-09-17 00:08:06 +02:00
Mistial DeveloperandClaude Fable 5.1 2b35f40c95 hf mfdes sim: legacy secure messaging after a 0x0A authentication
A session opened with 0x0A kept the EV1 secure messaging: an 8 byte CMAC
on every answer, CRC32 in enciphered transfers and ChangeKey, one CBC
chain across commands. The legacy session is different in every one of
those: nothing on a plain answer, a 4 byte DES MAC over the data alone on
MACed transfers, CRC16 over the data in enciphered transfers and in the
ChangeKey cryptogram, the reader's cryptograms undone by enciphering, and
every cryptogram from a zero IV.

The session remembers which authentication opened it and the four places
that secure or check a transfer take the legacy path when it was 0x0A:
the write gatherer's expected length, the command unwrap, the answer
wrapping, and ChangeKey. Chained answers carry the transfer's mode so a
MACed read puts its MAC on the last frame only.

Measured against a genuine DESFire EV1 4K with the pm3 as reader, all
zero and non-zero DES keys, writes and reads in all three modes and a
ChangeKey of another key: the simulation answers every frame with the
card's bytes.

Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
2026-09-16 22:14:07 +02:00
Mistial DeveloperandClaude Fable 5.1 49a19be247 hf mfdes sim: run the 0x0A legacy authentication the way a card does
The second frame of a 0x0A handshake was handled like 0x1A: the reader
token was deciphered in CBC with the IV chained from E(RndB), and RndA'
was enciphered with the IV chained from the token. A card starts each
legacy cryptogram from a zero IV and only ever enciphers, so it recovers
the token by enciphering each block and xoring the previous ciphertext
block.

The wrong IV only touches the token's first block, so RndB' still
verified and the simulation reported success with RndA off by E(RndB).
Session key and final frame were wrong from there, which is where the
D40 secure messaging captures started to disagree.

Measured against a genuine DESFire EV1 4K: the card's final frame is the
same across sessions with different RndB, which only a zero IV gives,
and the simulation's old answer matched the chained-IV arithmetic byte
for byte. Both handshake captures now replay through the simulation.

Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
2026-09-16 22:04:01 +02:00
Mistial DeveloperandClaude Fable 5.1 fee9648b53 hf mfdes etest: place the command doc entry at top level, state the draw order
The regenerated `hf mfdes etest` object had been spliced inside the
`hf mfdes esave` object in doc/commands.json, so catalog consumers did
not see it. Moved by hand to its place among the commands, matching what
`make commands` produces for that entry.

Help and how-to said a recorded session replays byte for byte. It does
only when the queue is filled in draw order: the random UID takes 3 bytes
at --scan on a random-id image, then each authentication takes its RndB.
Say so.

Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
2026-09-16 22:04:01 +02:00
Mistial DeveloperandClaude Fable 5.1 deab288804 hf mfdes etest: changelog and how-to
Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
2026-09-16 22:04:01 +02:00
Mistial DeveloperandClaude Fable 5.1 2ff6b0fecd hf mfdes etest: drive the DESFire simulation from the host
One command with one action per call: --begin, --scan, --apdu <hex>,
--random <hex>, --fieldoff, --state, --end. Text output by default,
-j gives one line of JSON per call for a script to read back.

Distinct failures are reported as such: no image in emulator memory,
not activated (--scan first), random queue full, and a firmware that
does not answer.

Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
2026-09-16 22:04:01 +02:00
Mistial DeveloperandClaude Fable 5.1 e588085c10 hf mfdes sim: host driven test hook with injectable randomness
The DESFire simulation could only be reached over the air. A conformance
suite that has recorded a genuine card's session needs to replay it byte
for byte, which takes two things the simulation did not have: a way in
over USB, and control of the card's random draws.

CMD_HF_DESFIRE_SIM_TEST carries one operation per packet into the same
state machine the RF loop drives: BEGIN, SCAN, APDU, RANDOM, FIELDOFF,
STATE, END. SCAN runs the RF-reset path (init, reset, random id, FSD) and
APDU goes through desfire_sim_apdu(), so native and ISO 7816 wrapped
commands answer exactly as they do on air. No FPGA involvement.

Randomness goes through a small host-fed byte queue: RndB and the random
id draw from it first and fall back to the existing fixed value or tick
counter when it is short, raising a sticky underflow flag. With nothing
queued the RF simulation is byte-identical to before; SimulateDesfireTag()
clears the queue so a host session cannot leak into a reader session.

Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
2026-09-16 22:04:01 +02:00
CinderSocketandgpt-daybreak-blue 71b558d6be usb: consume zero-length OUT packets and already buffered commands
A transfer whose size is an exact multiple of the endpoint size ends with
a zero-length packet. Nothing acknowledged it, so that receive bank stayed
selected and data queued in the other bank stalled. A command already
buffered by usb_read_ng was also not recognised until another packet came
in. Both AT91 and AT32 reuse their receive cleanup to acknowledge the empty
packet and rearm reception; frame parsing is unchanged.

Seen as a small packet lost now and then right behind a burst of
CMD_HF_MIFARE_EML_MEMSET chunks from `hf mfdes eload`.

(cherry picked from cindersocket/proxmark3 495bc201d, upstream PR #3628)

Co-Authored-By: gpt-daybreak-blue <noreply@openai.com>
2026-09-16 22:04:01 +02:00
Philippe Teuwen 1d887fa869 make miscchecks 2026-09-16 19:06:43 +02:00
Philippe Teuwen 634edd11fd make style 2026-09-16 19:00:50 +02:00
iceman1001 f1e71c5b69 ulaes config fix 2026-09-16 18:58:42 +02:00
iceman1001 34c68fd347 ulaes print conf correct 2026-09-16 17:36:46 +02:00
iceman1001 f76ae2b3f2 fix more stabilty issues with NULL checks and leaving values after REALLOC 2026-09-16 13:58:20 +02:00
iceman1001 5318069ccf fix potential unstability with proper NULL checks 2026-09-16 13:56:02 +02:00
iceman1001andClaude Opus 5 57b30c44e1 hf mfdes chk: check every key the app declares, and stop miscounting dictionaries
Three things behind a scan that ran against one application, printed its check
line twice, and then said nothing at all.

The dictionary loader was truncating rather than rejecting. 'loadFileDICTIONARYEx'
null terminated each line at the requested key length and only skipped lines that
came out too short, so any line holding more hex than asked for was silently cut
down and taken as a key. Reading 'mfdes_default_keys' for DES keys turned all 47
of its 32-hex AES lines and all 4 of its 48-hex 3K3DES lines into 8-byte keys that
are on no card, reporting 58 DES keys where the file holds 7. It now skips a line
whose hex run is longer than the key being asked for, which is what its sibling
'loadFileDICTIONARY_safe_ex' has always done. A trailing comment or whitespace
after the key is still fine, so the bundled dictionaries keep working. Counts for
that file go from 58/51/4 to 7/47/4, and mfc, iclass, mfp and t55xx are unchanged.

The key numbers to check came only from file access rights. An application with
three keys whose files name just key 1 had key 2 left out of the sweep entirely,
even though the card had already said how many keys it holds. Key settings carry
that count in the low nibble, so chk reads it and checks every key number the
application declares, the same sweep 'hf mfdes detect' already does. On a test
card this immediately turned up an AES key 2 still at the factory default, which
no file pointed at and which the old code could never have found. The access
rights walk stays as the fallback for when key settings are not readable, which
also means the common path no longer spends a file list round trip.

And the last round of a chunked dictionary read comes back empty. That empty list
was still handed to AuthCheckDesfire, which selected the application, read its key
settings, probed for LRP and listed its files in order to then check no keys at
all, printing a second identical check line on the way. Skipped now.

Finally, a scan that found nothing printed nothing. In non verbose mode all that
was left was a blank line, itself a leftover from the progress markers removed in
d842c7e5. It says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:12:11 +02:00
iceman1001andClaude Opus 5 00aabd7443 hf mfu chk: one dictionary check that detects UL-C or UL-AES
hf mfu cchk and hf mfu aeschk were the same command twice. Ninety lines of
dictionary loading and chunked device calls were duplicated, and the only
real difference was which key slot they targeted: aeschk took --idx, cchk
hardcoded the Ultralight C slot. Both are replaced by hf mfu chk, which
reads the tag type off the card with GetHF14AMfU_Type() and picks the slot
itself. Ultralight AES still honours --idx, 0 DataProtKey, 1 UIDRetrKey,
2 OriginalityKey. Ultralight C holds a single key, so --idx is rejected
there rather than silently ignored. Any other tag is refused with a pointer
to hf mfu info.

Without -f the dictionary is mfulc_default_keys.dic for both tag types. A
segment check keeps needing an explicit -f, because the segment dictionaries
hold four byte keys and the default one does not. aeschk used to advertise
mfulaes_default_keys.dic in its help, which has never been shipped.

Collapsing the two bodies also fixes --retries. firstChunk and lastChunk
were declared outside the retry loop and never reset, so only the first pass
was correct. From the second pass on, every chunk went out with firstchunk
clear and lastchunk set, which on the device side means no select and a
field teardown after each chunk, against a field the previous pass had
already dropped. They are now scoped to the pass.

client/pyscripts/mfulaes_mask_recovery.py called aeschk and now calls chk.
doc/commands.md and doc/commands.json are regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:40:47 +02:00
iceman1001 60eca59fee text 2026-09-16 11:43:36 +02:00
iceman1001 60e71e1d3d hf 14b dump: run the MyKey parser on a card read, not just a file 2026-09-16 11:33:42 +02:00
iceman1001 99020b0297 fix cppcheck warning 2026-09-16 11:26:16 +02:00
iceman1001 9aca3be176 silence cppwarning dead code 2026-09-16 11:00:31 +02:00
iceman1001andClaude Opus 5 e648438306 hf 14b view: decode MyKey / COGES keys on SRIX4K
The SRIX4K block scrambler in cmdhf14b.c was a three entry lookup table with
its callers commented out, so `hf 14b valid` printed hard coded values and
nothing ever decoded. The scrambler is a 4x4 transpose of the block read as
sixteen crumbs. A transpose is its own inverse and it reproduces all three
entries of the old table, so the stub is replaced by a working parser.

client/src/parsers/parsemykey.c reads the application off a dump: key id,
production date, operations counter, vendor code, lock id, current and
previous credit, and the eight slot transaction ring. Every block carries a
checksum in its top byte and the parser reports how many hold up, which
catches a wrong UID or a torn write. The credit blocks are XORed with a
session key derived from the UID, the vendor code and the count down counter
in block 6, so the file name has to carry the UID.

`hf 14b valid` is removed. Checking that the maths holds is now
`hf 14b view --selftest`, following `hf mf view --selftest`, and it runs
against traces/hf-14b-D0021F673CB26556-dump.json, a dump of a real reset key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:58:57 +02:00
iceman1001 199c3f107c fix image too large for 256kb devices by adding guards 2026-09-16 10:32:48 +02:00
iceman1001 6e25742606 fix build, removing unused var 2026-09-16 08:49:32 +02:00
Iceman ffb62a22e7 Merge pull request #3635 from Antiklesys/master
Update frame_data2.c
2026-09-16 13:46:58 +07:00
Antiklesys 1dc2ec25e9 Update frame_data2.c 2026-09-16 12:04:13 +08:00
iceman1001 f66632e288 text 2026-09-16 02:44:53 +02:00
iceman1001 5e12004028 missing files 2026-09-16 02:41:19 +02:00
iceman1001 c0a109dbc6 text 2026-09-16 02:39:42 +02:00
iceman1001 9c58ec72f9 text 2026-09-16 02:16:16 +02:00
iceman1001 592977c217 armsrc: zero the command payload when a packet arrives, not on every idle poll
The main loop cleared the whole PacketCommandNG payload before every call to
receive_ng(), so a device sitting idle memset PM3_CMD_DATA_SIZE bytes on each
pass of the loop whether anything was arriving or not. The zeroing belongs
inside receive_ng_internal(), once the preamble has been read and a packet is
known to be coming in, and that is where it is now.

It has to be before either branch fills the payload, because neither fills all
of it: an NG command writes only its own length, and an old style one only
PM3_CMD_DATA_SIZE_OLD. Whatever is not written has to start at zero.

Moving it also covers two callers that never had it. appmain.c has a second
receive_ng() that drains the buffer on a mode switch, and iclass.c has one that
picks up EML_MEMSET commands while waiting for RF. Both declare their
PacketCommandNG without initialising it, and the iclass one then reads a struct
out of the payload -- so a command shorter than that struct was reading stack
garbage beyond its length. That is fixed as a side effect.

Builds for RDV4 and PM5.

Thanks to @Msprg et al
2026-09-16 02:14:48 +02:00
iceman1001andClaude Opus 5 461c252988 armsrc: fill words in memset, not just bytes
memcpy was taken off its byte-at-a-time loop in b66bb2659 and memset was left on
one, though the device memsets constantly and the loop is the same shape. This
gives it the same treatment, and it is the simpler of the two: with no source
buffer there is no alignment to match, so once the destination is word aligned
the word fill is always available.

Align the destination, build the fill word once, then store it four at a time
and singly for the remainder, with at most three bytes left over. Unrolled four
ways for the same reason memcpy is: at -Os the loop bookkeeping otherwise costs
more than the stores.

The bulk path goes from six instructions per byte to nine per sixteen bytes,
read off the disassembly:

    old   subs / cmp / bgt / strb / adds / b          per byte
    new   str x4 / adds / b, plus subs / cmp / bgt    per 16 bytes

Wall clock is not measured here. The closest anchor is memcpy's own figure from
b66bb2659, which measured 221us to 34us for a 624 byte aligned frame, and memset
has a store where memcpy has a load and a store.

Verified against libc memset on the host before flashing: 7224 cases, every
alignment from 0 to 7, every length from 0 to 300, and fill bytes 0x00, 0xFF and
0xA5, with no mismatches. Then on hardware, where a wrong memset would show up
everywhere rather than in one place: the DESFire simulation harness passes, and
hw status, hw tearoff, mem info, lf search, hf 14a info and hf mf info all
behave.

Builds for RDV4 and PM5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:59:27 +02:00
iceman1001andClaude Opus 5 d842c7e523 hf mfdes chk: drop the stray progress characters, default the dictionary
Two things you asked about after a scan printed a lone "d" between applications.

The "d" and "p" were a progress marker saying how a key had been found, a
dictionary or a pattern, printed without a newline after every hit in
non-verbose mode. The find itself is already reported in full and
unconditionally on its own line -- "AID 0x010203, Found AES Key 00... <key>" --
so the marker added a stray character to the output and nothing else. Removed.

Each found key also repeated the AID that the "Checking aid" line above it had
just announced, so that is gone too. The only path to that print is the scan
loop, which prints the AID for every application before checking it.

The crypto algorithm is highlighted now as well, so the algorithm column can be
scanned down rather than read. `hf mfdes detect` already prints its channel, its
algorithm and its key that way, and chk was highlighting only the key.

And the command needed a dictionary spelled out. `hf mfdes detect` already falls
back to the bundled `mfdes_default_keys` when none is given, and there is no
reason for chk to differ, so it does the same now when neither a dictionary nor
a pattern was asked for. That also lets it run with no arguments at all, which
it previously refused with a help screen because its parser was told not to
allow an empty command line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:07:21 +02:00
iceman1001andClaude Opus 5 41f1433a2e hf mfdes sim: follow the spec on session lifetime and frame sizes
Four fixes, three of them from auditing the simulation against M134034 rather
than from a failing test, and one found by the audit's own new test.

An error status now ends the authenticated session. "The CMAC is not calculated
and attached, if an error code is returned. Then the application of the PICC
leaves the authenticated state and needs to be authenticated again" (7.3.4).
The simulation had 127 error returns and eight of them cleared the session, so a
reader saw an authenticated session survive errors no real card survives. It is
applied once where a command's answer leaves the dispatcher, rather than at every
error return, so a new error path cannot forget it. 0x00, 0x0C, 0x90 and 0xAF are
the four statuses that are not errors.

Answers are sized from the reader's frame size. The reader states it in its RATS
-- FSDI in the high nibble of the parameter byte -- and the simulation ignored it
in favour of a fixed 96 byte chunk. That is safe against a reader asking for 256
and wrong against anything below 128, which would be sent frames it cannot
receive. The chunk now comes from FSDI, rounded to a whole cipher block. A reader
asking for less than 32 bytes still cannot be served, because the status byte, a
CMAC and the framing cost twelve before any payload, but nothing in practice asks
for that and this card's own ATS advertises 64.

The length of a write is taken from the command instead of guessed. WriteData,
WriteRecord and UpdateRecord each carry a 3 byte length in their header, and the
header is in the clear in every communication mode, enciphered included, so the
total is known from the first frame. The simulation had been treating "this frame
looked full" as "more is coming", which is guesswork about something the protocol
states outright.

Writing that test found that a chained write never worked at all. The dispatcher
refused an 0xAF continuation unless a read was being chained or an authentication
was half done, so the second frame of any write longer than one frame came back
91 1C. Every write tested until now fitted a single frame. It is one more
condition on that check.

Tested against the simulation: 100 byte writes in plain, MACed and enciphered
mode all complete over two frames and read back byte for byte, `hf mfdes dump`
walks three applications and six files with no errors, and the regression harness
passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 00:51:33 +02:00
iceman1001 4f0aabf808 text 2026-09-15 23:59:01 +02:00
iceman1001andClaude Opus 5 f3e169652a hf mfdes sim: chain an enciphered read instead of refusing it
An enciphered read longer than one frame was answered with 91 7E, because the
CRC32 covers the whole read and the code could only build that in one piece.

It is one CBC run over the file data, a CRC32 behind it and zero padding, split
across frames with the init vector carried from one to the next -- "if the
commands are queued due to a very long data stream, the init vector for the
decipherment is always updated" (M134034 7.3.7). So each frame only has to carry
whole blocks, which the 96 byte chunk and the padded total both are, and the
reader joins the ciphertext and deciphers the lot in one go.

The read now walks a stream rather than a file extent: bytes come from the file
while they last, then the CRC32, then padding. The CRC is taken up front, over
the data and the status byte the last frame will carry, so it has to be built a
piece at a time -- the status byte is not in the card image and the data can be
a whole file, which rules out crc32_ex() and its single buffer. common/crc.h
already has an incremental CRC, the one legicrf.c uses, so that is what this
uses: crc_update() shifts LSB first, so the polynomial goes in already reflected
with neither reflect flag set. Those parameters were checked against crc32_ex()
over every length from 0 to 200 before being used.

Tested against the simulation with a 128 byte file created through the reader in
Full communication mode with all rights on key 0, so the file's mode actually
applies rather than being forced plain by a free access right. Reads of 64, 96,
112 and 128 bytes all verify, the last three over two frames, and the content
comes back as written. A wrong init vector or a wrong CRC would have failed the
client's CRC32 check rather than passed quietly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 23:48:42 +02:00
iceman1001 2025d05dca text 2026-09-15 22:57:37 +02:00
iceman1001andClaude Opus 5 9e4ee9c6ad lf read: fix realtime sample streaming over USB
A realtime `lf read` streams samples straight to the host at the LF sample rate,
around 125 kB/s. If the host stopped draining for even a moment,
async_usb_write_requestWrite() returned false, and ReadLF_realtime() treated that
as fatal: it returned PM3_EIO through a goto that also skipped
async_usb_write_stop(). The IN endpoint was left busy, every later usb_write()
then returned PM3_EIO, and the device went silent until it was physically
replugged. A USB bus reset does not clear it -- EP0 keeps working, descriptors
read fine, only bulk IN is dead. That is what a long `lf read` looked like from
the client: a transfer that stopped early, and then a device that would not
answer the next command.

A busy endpoint is back pressure, not an error. The sampling loop now waits for
the host, up to 100ms, and only gives up if it never comes back. Every exit path
closes the async write. The spins inside the USB helpers are bounded and drop the
stuck packet instead of turning into an infinite loop, which is the other half of
why the device never recovered. Measured against a reader deliberately held at
41 kB/s: before, the stream died at 212238 of 300000 bytes and the unit needed a
replug; after, 300000 of 300000 and it still answers.

The last packet of a stream was being lost as well. The host's CDC read buffer is
two max sized packets, and a bulk IN transfer only completes on a short packet or
a full buffer, so a stream ending on a full 64 byte packet was left sitting in a
half filled buffer. It showed as exact parity on the packet count:

  513 packets requested -> 512 delivered      514 -> 514
  515 packets requested -> 514 delivered      516 -> 516

async_usb_write_stop() now always sends a closing packet, the leftover partial
bytes when there are any and a zero length packet otherwise, the same way
usb_write() already did for its own transfers.

On the client side a short transfer is reported instead of being presented as a
complete read, the device is told to stop streaming on that path too, and the in
place byte counter is repeated with its final value, since the loop only samples
it every 10ms and the last line printed was stale.

Lastly `lf read` and `lf sniff` cap the sample count to the graph buffer size.
Anything past it was streamed and then discarded by getSamplesFromBufEx(), so
`-s 1500000` spent about two extra seconds collecting 220000 samples that were
thrown away, and printed "Received 1500000 / 1500000 bytes" followed by "Got
1280000 samples" with nothing to explain the gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 22:49:41 +02:00
iceman1001 2d362813f1 text 2026-09-15 22:09:24 +02:00
iceman1001andClaude Opus 5 4d4dd32354 hf mfdes sim: SetConfiguration
The simulation answers SetConfiguration. The option byte travels in the clear
and the data behind it is enciphered with a CRC32, the same shape ChangeKey uses,
and card level master key authentication is required (M134034 9.4.9).

Option 0x00, the configuration byte, is kept in the card image header. Both bits
it defines are one way on a card -- the spec says "cannot be reset" of each --
so they are only ever set, never cleared, and a reader that sends a zero byte
afterwards gets an OK and no change, which is what the silicon does.

  bit 0 disables FormatPICC, and that command now refuses from then on.

  bit 1 switches anticollision to a random id: a 4 byte id whose first byte is
  the 0x08 random tag and whose other three are the random number, with a single
  cascade level (M134034 6.5). The real UID is then only reachable through
  GetCardUID, which is the reason that command exists. A card draws the number
  at RF reset; the nearest thing here is the start of a simulation, since that is
  when the anticollision answers are built, so it is stable across activations
  within one run and different across runs -- measured 08 01 9A C9 four times in
  a row, then 08 01 BB 96 and 08 01 DC 26 and 08 01 FC 8E on three restarts.

  Simulating that needed the 4 byte case adding to the UID length the simulation
  accepts, which previously took only 7 and 10 and refused to start otherwise.

Option 0x02 replaces the ATS. It lands in the image, so it takes effect the next
time the simulation starts rather than mid-run, because the ATS is one of the
answers precompiled before the reader is listened to.

Option 0x01, the default key new applications are created with, is refused with
91 9E rather than accepted and ignored. Storing it needs two fields the card
image does not have, and adding them moves every table in it. A reader that sets
a default key and then finds new applications keyed with zeros is worse off than
one told the option is not there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:58:57 +02:00