Commit Graph
23334 Commits
Author SHA1 Message Date
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
iceman1001andClaude Opus 5 c0e5706b04 hf mfdes sim: ReadSignature
The simulation answers ReadSignature, handing back the originality signature the
card image carries, with the 0x90 status this command uses in place of the usual
0x00 and the response CMAC taken over that byte.

It is gated on the image's generation rather than on whether bytes happen to be
stored, because an originality signature is not an EV1 feature. M134034 has no
mention of command 0x3C and no mention of signatures at all, `hf mfdes info`
only asks EV2, EV2_XL, Light, EV3, NTAG413DNA and DuoX for one, and a genuine
MF3ICD81 answers 91 1C. Measured on UID 04 26 85 12 A2 56 80, which answered
GetVersion normally in the same session:

    90 3C 00 00 01 00 00  ->  91 1C
    90 60 00 00 00        ->  04010101001A05 91 AF

So a D40 or EV1 image answers 91 1C whatever it holds, and an EV2 or later image
serves its signature. Generation comes from versionhw[3], which is 0x01 for EV1
and 0x12, 0x22 or 0x42 for EV2.

A later card whose dump never read a signature still answers 91 1C rather than
56 zero bytes, which a reader could not tell from a real answer.

Tested against the simulation both ways: an EV1 image refuses the command with
the same status the real card gives, and an image with the hardware version
bytes of an EV2 returns its 56 bytes followed by 91 90.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 21:46:55 +02:00
iceman1001andClaude Opus 5 6df7b4d07b hf mfdes sim: GetKeyVersion
The simulation answers GetKeyVersion, returning the version of the named key in
the selected application, or of the PICC master key at card level where only key
number 0 is valid (M134034 9.3.7).

No authentication is required, which is the point of the command: a key version
is readable without knowing the key. The card image has always tracked the two
apart for that reason -- a version that was read and a key value that was
recovered are different facts -- so a version the dump never read comes back as
0, the same as a default key carries.

Tested against the simulation by changing a key with a version of 0x5A and
reading it back, which also confirms ChangeKey stores the AES key version byte
it is handed. A key number the application does not have is refused.

Co-Authored-By: Claude Opus 5
2026-09-15 21:38:10 +02:00
iceman1001andClaude Opus 5 55fb665380 hf mfdes sim: ChangeKey
A reader can rekey the simulated card now, at PICC level and inside an
application, and the new keys are in what `hf mfdes esave` writes out.

The reader enciphers the key data under the session key, and its shape depends
on whether the key being changed is the one the session was opened with. Change
that key and the frame is just the new key; change any other and the new key
arrives XORed with the current one, with a second CRC32 over the new key alone
so the card can tell the XOR came apart correctly. The card holds both keys in
that case, so it can undo the XOR -- and a key the image only knows the version
of is refused rather than guessed at. The shape being fixed means the plaintext
length is known rather than searched for: the key, an AES version byte, the
CRC32 over command, key number and that lot, and the second CRC32 when it
applies.

Which key has to be authenticated comes from the key settings, not from the
frame. The master key changes only with the master key and only while bit 0
still says it is changeable; a change-key nibble of 0x0F freezes every other
key; 0x0E lets a key be changed by whoever authenticated with it; any other
value names the one key that may change the others (M134034 9.3.4, 9.3.6).

At PICC level the top two bits of the key number choose the algorithm the new
master key is to be, since there is no application creation to fix it. Inside an
application the key type cannot change, so those bits are ignored there.

"After a successful change of the key used to reach the current authentication
status, this authentication is invalidated" -- so the session is dropped after
changing the key it was opened with, but only after the MACed answer has been
built with it, since the reader still has to verify that.

Tested against the simulation on an AES application and on the PICC master key:
changing the authenticated key leaves the old key unable to authenticate and the
new one able to, changing a different key does the same to that key while the
session carries on, and changing a key while authenticated with one that the key
settings do not name is refused with the key left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:28:31 +02:00
iceman1001andClaude Opus 5 31d05aef53 hf mfdes: ChangeFileSettings, and an esave option to keep what was deleted
ChangeFileSettings is answered by the simulation now. Which form it takes is
decided by the file's own "change access rights" right, not by the reader:
never refuses the command outright, free means the settings arrive as plain text
with no security mechanism at all, and anything else names the key that has to
be authenticated and the settings arrive enciphered under it (M134034 9.5.4).

That enciphered form is the first command whose parameters the reader secured
rather than the card, so it shares the unwrapping the writes use: the file
number stays in the clear, the rest is one CBC run under the session key, and
the CRC32 behind the plaintext is checked before anything is changed. A command
that unwraps its own parameters also has to be kept away from the blanket
command CMAC, or the IV moves twice.

Tested against the simulation: a file whose change right is free goes from
rights eeee to 1234 with no session at all, a file whose change right is key 0
goes from 1200 to 3210 authenticated and enciphered, and a file whose change
right has been set to F refuses every later change and keeps its settings, which
is the point of that value.

Separately, `hf mfdes esave` grew a `--keep` flag. A deleted application or file
stays in the card image as a tombstone, because the memory it held stays spent
until a FormatPICC, and esave was writing those out as though they still
existed. They are now left out by default, so a dump is the card as a reader
sees it, and `--keep` puts them back for when what a reader did is the
interesting part rather than the result it left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:15:41 +02:00
iceman1001andClaude Opus 5 734500bdf6 hf mfdes sim: CreateApplication and DeleteApplication
A reader can add and remove applications on the simulated card now.

The three tables sit head to tail -- applications, then files, then keys -- so a
new application entry goes in where the file table currently starts and
everything above it moves up by one entry, with the application's keys added to
the end of the key table. They come up all zero, which is what a card gives a
new application. KeySettings2 is decoded as the spec defines it: the key count
in bits 0-3, the ISO file id flag in bit 5, and the cipher for the whole
application in bits 6-7.

CreateApplication requires that AID 0x000000 is selected (M134034 9.4.1) and,
when bit 2 of the PICC key settings is clear, a PICC master key session.
DeleteApplication takes either the PICC master key, or -- when that bit leaves
create and delete free -- the application's own master key, which means the
application being deleted has to be the selected and authenticated one.

Delete leaves a tombstone rather than compacting. That is not a shortcut: a
genuine card does not hand the memory back either, and only FormatPICC reclaims
it. Measured against the simulation, starting from 6400 bytes free: two
applications of five AES keys each cost 160 bytes apiece and leave 6080,
deleting one of them still leaves 6080, and FormatPICC then returns the card to
8000.

The refusals were checked by sending the raw APDUs with no session, since the
client always authenticates first: a duplicate AID answers 91 DE, creating while
an application is selected answers 91 9D, deleting with no session answers
91 AE, and AID 0x000000 answers 91 9E because it is reserved as the reference to
the PICC itself.

Note that AIDs travel little endian, so AID 010203 is 03 02 01 on the wire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:56:13 +02:00
iceman1001andClaude Opus 5 480934bfd5 hf mfdes sim: FormatPICC, and stop esave dropping the ATS
FormatPICC is answered now. It releases every application and every file and
hands the memory back, which is what makes it different from DeleteFile and
DeleteApplication: those leave a tombstone because a real card does not give the
memory back either, and this is the one command that reclaims. Here that means
rebuilding the tables around the surviving PICC entry and putting both frontiers
back where an empty card has them.

The PICC master key, its settings and the card identity are explicitly untouched
(M134034 9.4.6), so what is left is the same card with nothing on it. The same
section says the command always requires a preceding authentication with the
PICC master key, and unlike create and delete that is not relaxed by the free
create/delete key setting, so the check is on the session rather than on the
application's key settings. Measured against the simulation: 3 applications and
6400 bytes free become 0 applications and 8000 bytes free, and a bare
`90 FC 00 00 00` with no session is refused with 91 AE.

Separately, `hf mfdes esave` wrote the ATS as zeros. desfire_em_unpack() bounded
the copy with

    MIN(hdr->atslen, (uint8_t)sizeof(dump->card_info.ats))

and card_info.ats is 256 bytes, so the cast wrapped to 0 and the memcpy copied
nothing -- while ats_len was still set from the image, so the saved file carried
a correct length and eight zero bytes. Both copies here are now bounded by the
image's own array, which is the smaller of the two and the one a corrupt length
could run past.

This was invisible in simulation because SimulateIso14443aInit() falls back to
the tag type's built-in DESFire ATS when the image supplies none, and for this
card the two are identical. A reader saw a correct ATS from a card image that no
longer had one, and only a load-save round trip showed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:49:01 +02:00
iceman1001 7e0ce399f0 text 2026-09-15 20:41:22 +02:00
iceman1001andClaude Opus 5 4dc412e403 hf mfdes sim: authentication, secure messaging, reads, writes and transactions
The DESFire simulation now plays the EV1 protocol rather than just the
activation and a handful of unauthenticated queries. A reader authenticates,
enumerates, reads and writes files, and commits or aborts transactions against
the card image `hf mfdes eload` put in emulator memory.

Authentication covers 0x0A, 0x1A and 0xAA with DES, 2TDEA, 3K3DES and AES keys
taken from the image. A key the image only holds a version for still refuses to
authenticate rather than authenticating with zeros.

Secure messaging follows the file's communication mode, with the rule that the
mode only applies when a key right matching the authenticated key granted the
operation -- access granted by the free-access right runs plain whatever the
file settings say. Responses are MACed or enciphered accordingly, and the
session CMAC is taken over every command and response in order so the two sides'
IVs stay together even where neither puts a MAC on the wire.

Reads cover ReadData, ReadRecords, GetValue and GetCardUID. Writes cover
WriteData, WriteRecord, UpdateRecord, Credit, Debit and LimitedCredit, plus
CommitTransaction, AbortTransaction and ClearRecordFile. Backup data, value and
record files write into their shadow region and only move across on commit, so
an abort really does discard.

Three fixes were needed to interoperate with the client, and all three share a
shape worth naming: the authentication handshake still succeeded, because it
runs on the original key, and only the traffic afterwards was wrong.

1. A DES or 2TDEA key is stored as 16 bytes and the key itself decides which
   cipher the PICC uses -- if the second half equals the first it is a single
   DES key, and that governs session key generation too (M134034 8.1). The
   all-zero default key is the common case. Deriving a 2K3DES session key from
   it left the card MACing under a key the reader did not have. Confirmed by
   decrypting a captured GetCardUID response offline: under the session key the
   reader derives it yields the UID and a valid CRC32, under the other it does
   not.

2. Session keys are built here rather than through Desfire_session_key_new(),
   whose 3K3DES branch clears the low bit of the first eight bytes. Those bits
   are key version, which a session key does not have, and the reader keeps them.

3. The reader's 0xAF continuation is not a command, it continues one. Giving it
   its own CMAC restarted the running calculation halfway through a chained
   answer, so every chained response longer than one frame carried a wrong MAC
   while single-frame answers verified fine.

The sample card in traces/mifare is rekeyed from all zeros to the sequence
01 02 .. 10, extended to .. 18 for 3TDEA. An all-zero key has matching halves,
so it exercises only the degenerate path of fix 1 above and hides the bug;
distinct halves surface a wrong derivation on the first MACed frame.

tools/desfire_sim_test.sh loads an image, simulates it, drives the second
Proxmark3 at it and reports. It stops the simulation the way the client does, a
newline on its stdin, through a fifo held open for the run rather than a fixed
timer -- a run that outlives its timer leaves the device simulating. A command
counts as passing only if it prints something that says it worked: card errors,
client side argument rejections and MAC or CRC complaints all fail, because
several of those print a plausible result line as well and matching on the
result alone reports passes that never happened.

Tested on two RDV4s, one simulating and one reading: activation, info,
application and file enumeration, all three key types, enciphered GetCardUID,
plain and MACed reads up to 256 bytes over chained frames, and writes verified
by reading the image back out with `hf mfdes esave`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:40:32 +02:00
Iceman da3400ff04 Merge pull request #3629 from tweathers-sec/fix-bwm-data-available
Fix: poll the BWM link in data_available(), so a Proxmark5 on BLE/WiFi can be aborted
2026-09-16 00:24:24 +07:00
Iceman ae8398b7b5 Merge branch 'master' into fix-bwm-data-available
Signed-off-by: Iceman <iceman@iuse.se>
2026-09-16 00:24:10 +07:00
Iceman f6e60104f1 Merge pull request #3630 from tweathers-sec/fix-indala-debug-level
Fix: stop the Indala demod printing a debug line at INFO level
2026-09-16 00:21:26 +07:00
Iceman 7c65cc9f01 Merge branch 'master' into fix-indala-debug-level
Signed-off-by: Iceman <iceman@iuse.se>
2026-09-16 00:21:17 +07:00
iceman1001andClaude Opus 5 08e66d97f8 armsrc: word align the NG reply payload
PacketResponseNGPreamble is 10 bytes, so data[] sat two past a word
boundary while its source is word aligned. The residues differ, so the
memcpy word path could never trigger for a reply.

Offset the whole frame by two in the buffer and data[] lands aligned.
Costs two bytes of stack. The preamble, the frame length and every byte
on the wire are unchanged, so no client change is needed.

hw status transfer speed on RDV4: 630240 -> 788736 bytes/s. That is
about 1us/packet off the link floor; what remains is usb_write waiting
for the host, not device work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 18:50:57 +02:00
iceman1001andClaude Opus 5 b66bb2659e armsrc: copy words in memcpy, not just bytes
The device ships its own string.c because there is no libc linked, and
memcpy was a byte at a time loop: six instructions per byte, 17 cycles
per byte measured, 221us for a 624 byte frame. Every copy on the device
paid that.

Take words when source and destination share their offset within a word,
which is the only case ARM7TDMI can do at all, and unroll the byte tail
four ways since at -Os the loop bookkeeping otherwise costs more than the
copy. 624 bytes aligned goes 221us -> 34us, misaligned 221 -> 125.

192 bytes, was 24. The bootrom does not link string.c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 18:49:36 +02:00
iceman1001andClaude Opus 5 89305b2f8b usb: bound the wait for the host to collect a STALL
AT91F_USB_SendStall spun on STALLSENT with no exit. That bit only
arrives once the host polls the endpoint and takes the handshake, so a
host that vanishes in between wedges the device. It was the last
unbounded wait in the file without an escape.

Exit on RXSETUP (host abandoned the request and sent a new SETUP), on
ENDBUSRES, or on a spin count, reusing the 0x1FFF that usb_read already
uses. usb_check() is not usable here: it calls back into
AT91F_CDC_Enumerate, which is what calls this.

Do not test RXSUSP. Nothing writes it back to UDP_ICR, so it latches on
the first bus idle and stays set, which makes the guard fire on every
stall and stops the STALL from ever being delivered.

The second wait is bounded too, since leaving the first one early can
let the host set STALLSENT after the clear.

Also ISOERROR -> STALLSENT. Same bit 3, but only one of those names is
true on a control endpoint.

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