Commit Graph
7350 Commits
Author SHA1 Message Date
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
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 60e71e1d3d hf 14b dump: run the MyKey parser on a card read, not just a file 2026-09-16 11:33:42 +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 6e25742606 fix build, removing unused var 2026-09-16 08:49:32 +02:00
Antiklesys 1dc2ec25e9 Update frame_data2.c 2026-09-16 12:04:13 +08: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 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 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 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
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
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 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
Travis Weathers cce14d43e3 fix(client): stop the Indala demod printing a debug line at INFO level
detectindala() reports the bit count it gave up on through
PrintAndLogEx(INFO, ...), so every failed Indala demodulation prints

  [=] DEBUG: detectindala | 56

in normal output. It is the failure path: found_size < 64 means the
demodulator did not find a valid Indala (64 or 224 bits) and is returning
-5. Any noisy LF read hits it, so `lf search` with no card on the antenna
shows a developer's debug line to every user.

Every sibling in the same function already uses DEBUG, including the one
nine lines above it at "DEBUG: Warning - Indala had to invert bits", so
this is an oversight rather than a deliberate level. ui.c drops DEBUG when
g_debugMode is 0, which is the default, so the line stays available under
`data setdebugmode -1` and is simply no longer in the way.

Checked the whole client for the same class: the only other "DEBUG"
strings printed above DEBUG level are cmddata.c FSKToNRZ, and both of
those are already wrapped in `if (g_debugMode > 1)`, so they are correct
and are left alone.

Before, `lf search` over a reader field with no card:

  [=] Checking for known tags...
  [=] DEBUG: detectindala | 56
  [-] No known 125/134 kHz tags found!

After:

  [=] Checking for known tags...
  [-] No known 125/134 kHz tags found!

and with `data setdebugmode -1` the detectindala lines come back, now
carrying the [#] debug prefix like the rest of them.

Passes `make style` unmodified.
2026-09-15 10:57:02 -04:00
iceman1001andClaude Opus 5 2d70fcb4ff hf mfdes sim: self contained DESFire simulation, and stop bitstream downloads eating emulator memory
The DESFire simulation gets its own ISO 14443-A loop in armsrc/desfiresim.c
rather than hooking into SimulateIso14443aTag(). That function is complicated
enough without a DESFire state machine threaded through it, and the hook had
already got the ATQA wrong once. It still borrows the library helpers from
iso14443a.c -- the precompiled activation answers, the receive call, the send
calls -- so there is no duplicated state machine, and iso14443a.c goes back to
knowing nothing about DESFire. desfiresim.h exports one symbol.

Three fixes were needed to make it actually answer a reader.

1. ISO 7816 wrapping. A reader sends `02 90 60 00 00 00`, not `02 60`. The
   simulation read in[0] as the DESFire command and so saw 0x90, the class
   byte, answering ILLEGAL_COMMAND_CODE to everything -- and in native form
   (status || data) when the reader wanted the wrapped form (data || 91 SW).
   Both framings are handled now, including the Lc/Le distinction: five bytes
   means no data and in[4] is Le, longer means in[4] is Lc with data following.

2. A bitstream download frees and clears BigBuf to get scratch space for the
   decompressor. SimulateIso14443aTagEx() and Mifare1ksim() both guard against
   this by calling FpgaDownloadAndGo_keep_EM() before they allocate anything;
   the new loop did not, so iso14443a_setup() wiped the emulator memory holding
   the card image and the precompiled answers after they had been filled. The
   pointers survive, the bytes do not, and the tag then clocks out zeros.

3. iso14443a_setup() itself used the plain FpgaDownloadAndGo(), which does
   BigBuf_free() and BigBuf_Clear_ext() -- taking the emulator memory with it.
   Every 14a command comes through there, so a card image that `eload` had just
   put in place was destroyed by the next `hf 14a` command whenever the HF
   bitstream was not already resident. Demonstrated before and after:
   `eload` then `hf 14a reader` then `eview` used to report "No DESFire card
   image in emulator memory" and now returns the image intact. This affected
   every emulator memory user, not only DESFire.

Verified against a second Proxmark3 acting as reader, simulating a real
DESFire EV1 8K dump: activation (UID 04268512A25680, ATQA 03 44, SAK 20,
ATS 06 75 77 81 02 80), the three frame GetVersion chain over 0xAF, GetFreeMem,
GetApplicationIDs, SelectApplication, GetKeySettings and GetFileSettings.
`hf mfdes info`, `getaids`, `freemem`, `lsapp` and `lsfiles` all read correctly,
including per application key types (AES, 2TDEA, 3TDEA) and all five EV1 file
types with their real settings -- a value file holding 1000 with limits
[0..10000], a linear record 2/8 of 16 bytes, a cyclic record 1/4 of 24 bytes,
standard files of 256 and 64 bytes, and a backup file of 128 bytes in MAC mode
with keyed rights 1200.

Not implemented yet: GetDFNames (0x6D) and GetISOFileIDs (0x61), so a reader
sees empty ISO IDs and DF names. Everything else answers
ILLEGAL_COMMAND_CODE, which is what a PICC says to a command it does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:43:52 +02:00
iceman1001 d882c716af adaptations of VIGIK parsers 2026-09-15 14:39:14 +02:00
iceman1001 a528d5ac1b text 2026-09-15 11:16:15 +02:00
iceman1001 7eb4ecb332 text 2026-09-14 22:31:35 +02:00
iceman1001 c0e20a9121 text 2026-09-14 22:27:58 +02:00
iceman1001 7b16d091ac text 2026-09-14 22:27:31 +02:00
iceman1001 b706d715b9 text 2026-09-14 22:27:01 +02:00
iceman1001 59cd76905f minor fix, obey 2026-09-14 22:18:27 +02:00
iceman1001 72f3f15dd3 found a new public key! 2026-09-14 22:17:13 +02:00
iceman1001andClaude Opus 5 8225d76ccd hf mfdes: stop GetDFNames from permanently disabling DESFire EV1 cards
Sending GetDFNames (0x6D) inside an authenticated session destroys DESFire EV1
silicon. When the response chains, the card answers the first 0xAF continuation
frame with 0xC1 PICC_INTEGRITY_ERROR, "PICC will be disabled", and from that
moment every command returns 0xCD PICC_DISABLED_ERROR. The ATS historical byte
changes 0x80 -> 0x90. Three cards were destroyed establishing this.

It is reached by `hf mfdes dump`, `hf mfdes lsapp` and `hf mfdes info --files`,
all of which authenticate and then call DesfireFillAppList.

HOW IT WAS ISOLATED

One variable at a time, on a card that survived all of it. Unauthenticated, the
identical frames are answered normally:

  - 0x6D on a blank card
  - 0x6D with an application that has no DF name (empty, per 9.4.4)
  - 0x6D with one DF name, single frame, no chaining
  - 0x6D chained over two and then three DF names, sent by hand
  - the chain abandoned mid-way with the field dropped
  - a stray 0xAF with no chain open, answered 91 1C
  - the client's own chaining code with splitbysize

Authenticated, the same card died on the first continuation frame. The client
appends no MAC to 0x6D -- it is not in EV1D40TransmitMAC -- so the bytes on the
wire are `90 6D 00 00 00` then `90 AF 00 00 00` either way, byte for byte
identical to the probes that passed. Only the card's session state differs.

Ruled out along the way:

  - Failed or wrong-algorithm authentication. `hf mfdes chk` has fired hundreds
    of failed auths per card for years without harm.
  - Abandoned authentication chains. DesfireCheckAuthCmd sends only the first
    frame of an auth and then drops the field, six times per `hf mfdes info`
    run; 30 of them left the test card healthy.
  - Malformed commands. The only out-of-spec command in the original sequence
    (UpdateRecord, 18 bytes into a 16 byte record) was refused client-side and
    never reached the card.
  - Chaining itself, DF names themselves, and the record count.

THE FIX

Drop the PICC session around the call and restore it afterwards.

It is the card's session that has to go, not the context. Clearing
DesfireContext_t alone would leave the card authenticated and change nothing, so
SelectApplication is used: "each SelectApplication command invalidates the
current authentication status" (M134034 9.4.5).

The session is then re-established on whatever AID was selected when we were
called, not on 000000. The issuer info path enters DesfireFillAppList with an
application already selected, and re-authenticating the PICC with that
application's key would silently fail. A context selected by DF name has no AID
to return to, so the DF name list is skipped rather than guessed at.

No configuration forces the unsafe order. Key settings bit 1 governs whether the
directory commands need authentication, and its "needs auth" branch names only
GetApplicationIDs and GetKeySettings, not GetDFNames (M134034 p.39). A card that
refuses the plain command costs the DF name column; sending it authenticated
costs the card.

WHAT THE DATASHEETS SAY

EV1 never specifies how secure messaging applies across 0xAF frames. EV3 had to
write the rule down later, ev3.pdf 7.3.2.2: "the secure messaging is applied as
if the command or response would have been sent in a single large frame. The
0xAF command and response codes are ignored for these calculations." The exact
interaction that destroys these cards is the one EV1 left unwritten.

EV2 and EV3 then deleted the whole self-disable family: ev3.pdf contains zero
occurrences of PICC_DISABLED or PICC_INTEGRITY, which M134034 defines. EV3
renames 0xEE to MEMORY_ERROR and says the part may execute a reset rather than
disable itself.

Whether the disabled state is recoverable is SILENT in every direction. The five
codes appear once each, in one status table, with no prose anywhere, and there is
no lifecycle command in DESFire to rehabilitate a PICC. Measured on a dead card,
nothing works: FormatPICC cannot authenticate (0x1A -> 91 CD) and is refused raw
(90 FC -> 91 CD); SelectApplication is refused (91 CD) even though it needs no
authentication and no bit can gate it; every ISO 7816 command including ACTIVATE
FILE and TERMINATE DF answers 65 81 "Memory failure". A malformed GetVersion
still answers 91 7E LENGTH_ERROR, so anticollision, RATS, the command parser and
length validation all still run -- only the application layer underneath is gone.

Note M075031 is the D40 datasheet, not EV1; the EV1 document is M134034.

ALSO IN THIS CHANGE, because without them the cause was invisible

Every card error collapsed to PM3_EAPDU_FAIL before reaching a caller, and both
send paths only printed the status when -a was given. That is why the first two
cards told us nothing about how they died.

  - The five codes M134034 Table 11 footnotes as "not expected to appear during
    normal operation" (0xC1, 0xCD, 0xA1, 0xF1, 0xEE) are now reported from both
    DESFIRESendRaw and DESFIRESendApduEx whether or not -a is given. Ordinary
    refusals stay at debug level, so chk and the ISO file id probes stay quiet.
  - A chained 0xAF is attributed to the command it is continuing, so a failure
    reads "command 0x6D frame 0xAF -> 0xC1" rather than a bare 0xAF.
  - DesfireContext_t keeps lastRespCode, set where both exchange paths meet.

A command error also ends the authentication on the PICC, so the client now
drops its side of the session rather than continuing to MAC into a session the
card has thrown away. The tree already did this at one call site
(DesfireGetFileISOIDList); it is now done at the choke point every native
command passes through, plus the paths that bypass it: DesfireReadSignature,
DesfireCreateDelegatedApplication, DesfireISOSelectEx, the ISO data plane
(ReadBinary, UpdateBinary, ReadRecords, AppendRecord) and GetDelegateInfo. The
three ISO authentication wrappers are deliberately left alone, since they run
before a session exists and clearing would wipe the handshake's own state.

`hf mfdes pc` checked only the transport result and treated a card error as
success: DesfireExchangeEx returns PM3_SUCCESS with the card's status in
respcode, so PreparePC, the proximity check rounds and VerifyPC went on to parse
error responses as data. They now check respcode.

DesfireFillFileList discarded the result of DesfireFileSettingsStruct, so a file
whose settings could not be read became a zeroed entry indistinguishable from a
valid 0 byte standard data file. FileListElm_t now carries fileSettingsRead and
five consumers honour it: the ISO id mapping loop (positional, an unread file
would shift every id after it onto the wrong file), `hf mfdes chk`'s used-key
set, lsfiles, the application walk print, and `hf mfdes dump`, where settings_ok
was being set true unconditionally.

DesfireFillAppList tested a stale `res` after DesfireGetKeySettings, whose return
value was discarded; only an incidental buflen guard kept it from consuming
garbage key settings.

`hf mfdes sim` now runs through the shared ISO14443-A simulation loop as tag
type 3 rather than a duplicate 14a loop, which had got the ATQA wrong. The
DESFire logic stays in armsrc/desfiresim.c behind a small hook API.

KNOWN, NOT FIXED HERE

DesfireFillAppList reads the DF name list into uint8_t buf[250] while
DesfireCommandEx copies records * 24 bytes. The EV1 limit of 28 applications
would write 672 bytes into it. Harmless at the record counts seen here, but a
stack overflow waiting for a full card.

VERIFICATION

On a fourth card from the same lot (batch B9 0C 17 49 70, week 27 / 2017, EV1 8K,
HW 04010101001A05, SW 04010101041A05): the sequence that killed the third card
now completes, the chained 0xAF is answered 91 00, and the card is healthy
afterwards. The APDU log shows GetDFNames and its continuation carrying no MAC
while the commands either side of them do, confirming the session is dropped and
restored. A full `hf mfdes dump` of three applications (AES, 2TDEA, 3TDEA) and
all five EV1 file types completes and round-trips through the dump format.

Scope: all four cards are from one production lot. This is not established as an
EV1-wide erratum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 21:50:21 +02:00
iceman1001 52f3225661 parser adaptation 2026-09-14 21:13:52 +02:00
iceman1001 a7b1276781 phase 1 simulation and desfire fixes... 2026-09-14 21:13:10 +02:00
iceman1001 e5a44e1874 fix more parsing 2026-09-14 20:30:15 +02:00
iceman1001 e72bcdb1c0 fix identifyer 2026-09-14 20:04:18 +02:00
iceman1001andClaude Opus 5 (1M context) ee2d55fafb hf mfdes: put a DESFire card image in emulator memory
Adds the on-device card image a DESFire simulation will read, the client
side that packs a dump into it, and eload / esave / eview.

The layout is in include/desfire_em.h. Tables grow up from the header,
file data grows down from the end, and an allocation only has to leave
the two frontiers apart -- so a three-file card spends a few hundred
bytes rather than a worst case, and the space between them is what the
card has left. The PICC is application 000000 and uses the same struct
as any other application, so key settings and keys are always stated
against an AID. Delete sets a tombstone rather than compacting, which is
not a shortcut: a real card does not reclaim on delete either, measured
as 2080 bytes free with zero applications before an experiment and 2560
after FormatPICC.

Two size limits, and a reader only ever sees the first. cardsize is what
the emulated card claims to hold, so GetFreeMem answers from that and
CreateFile will refuse with OUT_OF_EEPROM when it runs out. Without it a
card impersonating a 2K part would report 7434 bytes free, which no 2K
part does. The image size is what emulator memory physically holds, is
the harder limit, and is never visible.

cardsize is anchored to the free memory the real card reported when the
dump was taken -- observed free plus what we reserve for the same content
-- so the emulation answers what its original answered. That is also a
check on the reservation rule rather than only a convenience: for the
bench card it computed 576 bytes spent, and 1984 + 576 is 2560, exactly
what that card reports when formatted.

Reservation follows what CommitTransaction covers. Backup data, value and
record files each get a shadow region because writes to them are staged
until commit; a standard data file writes through and does not. Sizes
round to a 32 byte granule, which is the granule a real card allocates in.
It deliberately does not reproduce NXP's allocator -- that is
undocumented and does not fit a simple model, a declared 1024 byte record
file costs 1088 on silicon -- so what matters is that the figure is
self-consistent and shrinks as the reader writes.

No new device command. Emulator memory is one shared region, so
CMD_HF_MIFARE_EML_MEMSET and BIG_BUF_EML already reach it, and both
inherit the bounds checking those paths gained earlier.

Verified with the client only, no simulation yet: packing a dump taken
from a DESFire EV2 and walking it back reproduces every field including
file contents byte for byte, the same round trip through the device via
eload and esave is identical, and an image too large is refused by name
rather than truncated -- 'Out of emulator memory laying out AID 112233
file 00: needs 4066 more bytes'.

Co-Authored-By: Claude Opus 5 (1M context)
2026-09-14 19:48:45 +02:00
iceman1001 669319b690 update desfire dumpformat to support verison HW,SW,PROD 2026-09-14 19:39:42 +02:00
iceman1001 95f6072e94 minor parse fix for vigik 2026-09-14 19:35:34 +02:00
iceman1001 064f8dd071 added a warning for anti clone counter measures 2026-09-14 18:32:53 +02:00
Niel Nielsen 5f641c6f75 Merge branch 'RfidResearchGroup:master' into master 2026-09-14 16:20:54 +02:00
iceman1001 d2dd2d9f32 text 2026-09-14 14:58:54 +02:00
Niel Nielsen 259f15e7df Merge branch 'RfidResearchGroup:master' into master 2026-09-14 13:11:49 +02:00
iceman1001 8f5fd048d3 improve emulator downloads with out of bounds checks 2026-09-14 12:27:06 +02:00
iceman1001 465deeda39 text 2026-09-14 12:24:31 +02:00
iceman1001 aae70014e0 - VIGIK service badge fields now show which ones the RSA signature actually covers 2026-09-14 12:21:47 +02:00
iceman1001 9caa6d6117 minor change we device now reports back EMULATOR memory size to the client. Had to bump capability version number to 11. 2026-09-14 12:16:51 +02:00
iceman1001andClaude Opus 5 (1M context) 6f4643a637 hf mf: add the MAD helpers parseproac needs, and register AID 0x4982
b82f60362 landed parseproac.c without them, so a clean checkout does not
build.

 - mad.c / mad.h: mad_find_aid() and mad_count_aid(); DetectHID() delegates
   to the first, it was already generic
 - mad.json: 0x4982 PROAC, sorted in after NORALSY's 0x4980

Co-Authored-By: Claude Opus 5 (1M context)
2026-09-14 12:13:35 +02:00
iceman1001 b82f603622 urmet parsing 2026-09-14 12:11:42 +02:00
Niel Nielsen 2cbd193d7b Update BLE function comments for clarity and consistency
Signed-off-by: Niel Nielsen <nieldk@gmail.com>
2026-09-14 11:47:32 +02:00
Niel Nielsen 5922a7112f Update BLE name resolution to use BlueZ mgmt API
Signed-off-by: Niel Nielsen <nieldk@gmail.com>
2026-09-14 11:47:08 +02:00