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>
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>
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>
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>
uint32_t plus three uint8_t pads to 8, so sizeof(line) was 8 where CDC
defines 7. Hosts asking for more than 7 got a byte of uninitialised
static padding. The commented out SET_LINE_CODING loop already hardcodes
i < 7, so the wire format was understood; the struct just did not match.
No observable change for cdc_acm or usbser.sys, which both ask for
exactly 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AT91F_USB_SendData only took length, and every caller passed
MIN(sizeof(x), wLength), so the function could not tell it had
truncated. A control IN transfer ends on wLength bytes or on a short
packet; returning fewer than wLength in whole 8 byte packets is neither,
and the host polls until it times out.
Take wLength too, do the MIN inside, and send a ZLP when the reply is
shorter than asked and lands on a packet boundary. The length > 0 guard
avoids a second termination, since the send loop already emits an empty
packet for an empty payload.
This is what the serial number descriptor's '(size % 8) == 0 OS bug
workaround' padding was dodging. Not an OS bug.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StrMS_OSDescriptor advertises MSFT100 and vendor code 0x1C, but the
handler for that request was commented out, so it fell through to the
standard switch and stalled. Windows caches that under
usbflags\<VID><PID><bcdDevice> and stops asking, and bcdDevice never
changes, so no reflash could recover it.
Enable both feature descriptors and answer 0x1C in AT91F_CDC_Enumerate.
The compatible ID is left empty so usbser.sys keeps binding; WINUSB
there would take interface 0 away from CDC and kill the COM port.
DeviceInterfaceGUID was GUID_DEVCLASS_PORTS, a setup class GUID in an
interface GUID field - replaced with a fixed project GUID.
AT32 is unchanged; both descriptors are behind #ifndef PM5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
data_available() and data_available_fast() are how the device notices that
the host has asked a running command to stop. They branch on USB and on
WITH_FPC_USART_HOST, and never learned about the Proxmark5 BWM.
A PM5 with the wireless module builds PLATFORM=PM5 PLATFORM_EXTRAS=BWM,
which defines WITH_BWM_FORWARD. BTADDON errors out on PM5 and BWM errors
out on anything else (common_arm/Makefile.hal), so WITH_FPC_USART_HOST is
never defined on that build and both functions compile down to a USB-only
check.
Commands still arrive, because receive_ng() does poll the module
(armsrc/cmd.c). What was blind was the abort path: CMD_BREAK_LOOP is never
observed over BLE or WiFi, so every loop whose only exit is this check runs
until the button is pressed. That is 75 call sites across 26 files in
armsrc/, including the sniff, simulate and reader loops, so in practice a
wireless session cannot be interrupted at all. ReaderHitag() is the worst
case: with no tag on the antenna it has no other exit, so `lf search` alone
leaves the device answering nothing.
This is reachable with upstream tooling only: client/src/uart/ble_posix.c
is the native BLE transport, selected with `-p ble:<address-or-name>`, and
doc/md/PM5_Start_Here/PM5-BWM-USAGE.md documents both that and
`-p tcp:<host>:<port>` over the module's WiFi. Both share the same inbound
de-framer, so both are affected.
Folded into the existing preprocessor chain as an #elif, mirroring
receive_ng(), since the two transports are mutually exclusive by build.
USB is still tested first, so the module is only polled when USB has
nothing, exactly as the FPC branch behaves.
bwm_fwd_rxdata_available() is the same call receive_ng() already uses. When
the de-frame FIFO is empty it costs a flag test and one DMA counter read
(bwm_uart_rx_available()); it only walks bytes when bytes have actually
arrived, and those bytes are the abort. That mirrors the FPC branch, where
usart_rxdata_available() already calls usart_rx_poll() from these same two
functions. data_available_fast()'s only caller is the ReadLF_realtime()
sampling loop, which reaches it once per 64 saved samples; it is included
here so the two functions cannot drift apart again, which is how this bug
arose.
Object code is byte-identical on every platform that does not define
WITH_BWM_FORWARD. Built PLATFORM=PM3RDV4, PM3RDV4 with BTADDON, PM5 without
BWM and PM5 with BWM; only the last one changes:
PM3RDV4 74f7f7a1... -> 74f7f7a1... identical
PM3RDV4 BTADDON f94c31de... -> f94c31de... identical
PM5 e3f0a203... -> e3f0a203... identical
PM5 BWM e3f0a203... -> efbba2f0... changed
Passes `make style` unmodified.
Verified on a Proxmark5 with the BWM fitted, over BLE, with the stock
client. Before, one command left the device deaf for good:
[fpc|tcp] pm5 --> hw ping
[+] Ping response received in 111 ms and content ( ok )
[fpc|tcp] pm5 --> lf hitag read --ht2 --pwd -k 4D494B52
[!] timeout while waiting for reply
[fpc|tcp] pm5 --> hw ping
[!] Ping response timeout
[fpc|tcp] pm5 --> hw ping
[!] Ping response timeout
After, the same sequence on the same device:
[fpc|tcp] pm5 --> hw ping
[+] Ping response received in 107 ms and content ( ok )
[fpc|tcp] pm5 --> lf hitag read --ht2 --pwd -k 4D494B52
[!] timeout while waiting for reply
[fpc|tcp] pm5 --> hw ping
[+] Ping response received in 102 ms and content ( ok )
[fpc|tcp] pm5 --> hw ping
[+] Ping response received in 85 ms and content ( ok )
A USB command recovers a device stuck this way, which is why the bug is
easy to miss on a bench with a cable attached: after the failing sequence
above, `hw ping` over USB answered in 1 ms and the wireless link resumed.
Both were unimplemented, so they fell through to ILLEGAL_COMMAND_CODE and a
reader walking the simulated card saw every ISO file id and DF name blank.
GetDFNames (0x6D) is PICC level and chains, one application per frame: AID
LSB first, ISO file id little endian, then the DF name. Applications without a
DF name are not sent back at all, which the spec is explicit about (M134034
9.4.4, "If the DC [sic, DF] has no DF name it is not sent back within this
command"), so the walk steps past them and only the last record carries
OPERATION_OK. Verified on the wire against a reader, and the frames come back
the same shape a genuine EV1 8K produces:
90 6D -> 03 02 01 | 34 12 | "test1" 91 AF
90 AF -> 33 22 11 | 45 23 | "test2" 91 AF
90 AF -> CC BB AA | 56 34 | "test3" 91 00
GetISOFileIDs (0x61) is application level and lists the ISO file ids of the
files that have one. Value files and transaction MAC files never carry one, so
they are skipped -- that matters because the client maps the returned ids onto
files positionally, and including them would shift every id after onto the
wrong file. When no file in the application has an ISO file id the answer is
FILE_NOT_FOUND, which is what 9.5.2 means by "If there is no ISO File EF, only
an error code can be returned", and is already what the client reads as an
application created without ISO file ids.
With these in place `hf mfdes lsapp` and `hf mfdes lsfiles` report the
simulated card completely: ISO ids 0x1234 / 0x2345 / 0x3456, DF names test1 /
test2 / test3, and per file ids 0001, 0002, 0011, 0033, 0055 with the value
file correctly showing n/a.
Not done: 0x61 is answered in a single frame. The spec allows it to chain, 27
file ids in the first frame and up to 5 more after, so an application holding
more ISO file ids than fit one frame would need the chained path that 0x6D
already has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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)
PLATFORM=PM3ICOPYX has not compiled at all -- it dies on obj/start.o,
before anything else is built:
common_arm/gpio/gpio_hw_at91.h:77: error: 'GPIO_FPGA_ON' undeclared
(first use in this function); did you mean 'GPIO_FPGA_DONE'?
PA26 has two different jobs depending on the FPGA, per
config_gpio_proxmark3.h:
#if defined XC3
#define GPIO_FPGA_SWITCH AT91C_PIO_PA26
#else
#define GPIO_FPGA_ON AT91C_PIO_PA26
#endif
ICopyX has no FPGA power rail to switch; that pin selects the FPGA
instead. But Gpio_FPGA_ON_High(), Gpio_FPGA_ON_Low() and
gpio_fpga_on_setup() all referenced GPIO_FPGA_ON unconditionally, so the
symbol simply does not exist on XC3.
'This board has no FPGA power switch' is already a supported case: the
AT32 port stubs both accessors with a bare '// Unsupported' comment, and
PM5 depends on that, while Gpio_FPGA_SWITCH_High()/Low() two functions
further down in this same header are guarded with #ifdef for the mirror
case. The AT91 header just never grew the XC3 arm. All three now carry
the same guard.
fpga_loader.c calls Gpio_FPGA_ON_High() from shared code; on ICopyX that
compiles to nothing, which is correct -- the rail is always on.
Verified: fullimage builds for PM3ICOPYX (356832 bytes, with
PLATFORM_EXTRAS=FLASH), and for PM3RDV4, PM3GENERIC, PM3ULTIMATE and
PM5. For the boards that do have the pin the guard costs nothing -- the
.text section is byte-identical before and after, 289464 bytes on
PM3RDV4 and 243584 on PM3GENERIC; only the embedded build timestamp and
source hash move in the full ELF.
Co-Authored-By: Claude Opus 5 (1M context)