Table of Contents
cVEND NFC Reader
cVEND is the NFC reader on the bottom half of the PM3
The associated serial device appears to be at /dev/ttymxc3 @ 115200 8n1 in Linux.
cVEND protocol notes
Boot-up / initialization logic analyzer trace (open with the Saleae app):
IPP packet structure
| offset | length | name | description |
|---|---|---|---|
| 0 | 1 | always 0xBC | |
| 1 | 1 | seq | monotonically increasing from 1; skips 0 when rolling over |
| 2 | 1 | replyTo | seq of the message that this is replying to, or 0 if not a reply |
| 3 | 1 | msgType | |
| 4 | 2 | msgLen | length of msgData |
| 6 | 1 | hdrCRC | CRC-8-Dallas/Maxim over previous 6 bytes |
| 7 | msgLen | msgData | message payload, structure depends on msgType |
7+msgLen | 4 | msgCRC | little endian bit inverted IEEE CRC-32/JAMCRC over msgData, only present when msgType >= 0x80 |
IPP message types
| msgType | dir | name | description |
|---|---|---|---|
| 0x02 | → | Version | |
| 0x03 | ← | VersionReply | (device ID from rockchip otp, firmware version, cwd) as length-prefixed strings e.g. “\x18\x00\xdd\xf5\x14cS01.05.46-49.00-2-2\x0502.35\n/home/app0” |
| 0x04 | → | Status | |
| 0x05 | ← | StatusReply | e.g. “\x00\x00” |
| 0x07 | ← | Heartbeat | sent periodically by reader |
| 0x0f | ← | Startup | sent by reader after startup, approx 1 minute after power on, e.g. “\x00” |
| 0x10 | → | Reset | first byte selects subcommand (2 = restart application, 3 = reboot, 4 = poweroff, 33-36 = reset sam 1-4, others = Error: Reset Code not implemented yet. [IppBaseTelegram.cpp:157]) |
| 0x11 | ← | ResetReply | e.g. “\x00\x00” |
| 0x20 | → | LEDs | sets LED status, u32 bitmap; the only two externally visible LEDs on the PM3 are “\x00\x00\x00\x40” and “\x00\x00\x00\x80” |
| 0x22 | → | Buzzer | makes the cvend beep; u16 frequency, u16 duration. e.g. “\x06\x00\x01\x00” |
| 0x32 | → | CardRelease | registered in ProxCardCtrl::ProxCardCtrl(RFIDReader&) |
| 0x46 | → | AbortCardHandling | registered in ProxCardCtrl::ProxCardCtrl(RFIDReader&) |
| 0x96 | → | PutFile | |
| 0x97 | ← | PutFileReply | |
| 0x98 | → | GetFile | |
| 0x99 | ← | GetFileReply | |
| 0x9a | → | DeleteFile | |
| 0x9b | ← | DeleteFileReply | |
| 0x9c | → | FileInfo | |
| 0x9d | ← | FileInfoReply | |
| 0xa4 | → | FileList | Seems to list files on the cVEND. First 2 bytes are the path length, remaining bytes are the path to list |
| 0xa5 | ← | FileListReply | First 4 bytes are reply length, remaining bytes seem to be null seperated modification timestamps and file/folder names |
| 0xaa | → | SetTime | |
| 0xab | ← | SetTimeReply | |
| 0xac | → | ITSOData | conditionally registered in ItsoIppHandler::_handleItsoCtrlReq(CommBuffer&, IppHeader const&, unsigned char const*) |
| 0xad | ← | ITSODataReply | |
| 0xae | → | ITSOCtrl | registered in ItsoIppHandler::ItsoIppHandler(ProxCardItso&) |
| 0xaf | ← | ITSOCtrlReply | |
| 0xb1 | ← | ISORead | sent by reader when ISO14443A card presented, after enabling Iso with 0xe4 card UID at offset 2 |
| 0xb3 | ← | ISOCardReleased | sent by reader after ISO1443A card released with 0x32 or 0x46 |
| 0xb4 | → | APDUProx | DeviceSelect(0x00), CLA, INS, P1, P2, Lc (2 bytes), Data, [magic], Le (2 bytes), I don't know what magic is, but put 0x04 if you have data or 0x01 if you don't |
| 0xb5 | ← | APDUProxReply | DeviceSelect(0x00), Echo (?), Status (0x00=success, 0x6c=no session, 0x70=no card, 0x76=retry), SW1, SW2, Data |
| 0xb6 | → | SAMCtrl | registered in SamCtrl::SamCtrl() |
| 0xb7 | ← | SAMCtrlReply | |
| 0xb9 | ← | DESFireRead | sent by reader when DESFire card presented, after enabling DESFire with 0xe4 |
| 0xba | → | unknown, registered in ProxCardDesfire::ProxCardDesfire(RFIDReader&) | |
| 0xbb | ← | DESFireCardRemoved | sent by reader when DESFire card removed from field |
| 0xbc | → | DESFireCommand | sends desfire command, documented in m075031_desfire.pdf |
| 0xbd | ← | DESFireCommandReply | response to command, documented above |
| 0xbe | ← | UnhandledCard | sent by reader when a card is presented that is not supported by any enabled ProxCardFunction, containing UID, historical bytes, and other data. First two bytes seem to be card type |
| 0xce | → | unknown, registered in IppHandling::IppHandling() | |
| 0xd0 | → | EMV | first byte selects subcommand (0 = load config, 1 = preprocess, 2 = toggle polling) * load config followed by null terminated filename, which is read and passed to fetfpd emvco_ep_configure* preprocess following bytes passed to fetfpd emvco_ep_preprocess* toggle polling following byte (0 = start polling, 1 = stop polling) |
| 0xd1 | ← | EMVStatus | sent by reader after startup and certain nfc state changes, format and semantics not yet understood |
| 0xd4 | → | UltralightCommand | Sends a subcommand to the UltralightC/NTAG card in the field. First byte is subcmd. See table below. |
| 0xd5 | ← | UltralightReply | Unsolicited card event (replyTo = 0), or reply to a 0xD4 command (replyTo = seq of the 0xD4). See tables below. |
| 0xe4 | → | ProxCardFunction | first 2 bytes specify function (4=VdvKa, 5=MifareClassic, 6=Iso, 7=Desfire, 8=Girogo, 9=Itso, 10=UltralightC), next byte must be 1, next byte (0=disable, 1=enable)00070101 sent by PM3 to enable DESFire reading at startup |
| 0xe5 | ← | ProxCardFunctionReply | Returns 4 byte payload: 0x00, the function number (as above), 0x00, 0x00 |
| 0xe6 | → | unknown, registered in GirogoIppHandler::GirogoIppHandler(RFIDReader&, ProxCardGirogo*) | |
| 0xe8 | → | SecurityServices | first byte selects subcommand (0=GetVersionOfKey, 1=RemoveX509Cert, 2=RemoveKeyBlock, 3=ImportX509Cert, 4=ExportX509Cert, 5=ImportKeyBlock, 6=ExportKeyBlock, 7=GenerateKSKPair), remaining bytes unknown |
| 0xe9 | ← | SecurityServicesReply | |
| 0xea | → | unknown, registered in ProxCardMifareClassic::ProxCardMifareClassic(RFIDReader&) | |
| 0xed | ← | Log | first byte specifies log level (1=INFO, 2=WARNING, 3=ERROR) followed by null-terminated human-readable log message |
→ - Host to Reader
← - Reader to Host
0xD5 UltralightReply — unsolicited card events
replyTo = 0 for both forms.
| byte 0 | meaning | payload |
|---|---|---|
| 0x00 | Card entered field | byte 1 = 0x00 (purpose unknown); bytes 2–8 = 7-byte UID (cascade / double UID format, e.g. 04 21 B9 01 10 04 03 for NTAG 215) |
| 0x01 | Card left field | byte 1 = 0x00 |
0xD4 UltralightCommand / 0xD5 UltralightReply — subcommands
Host sends 0xD4 with msgData[0] = subcmd. Reader replies with 0xD5 where replyTo = seq of the 0xD4.
Reply format: [subcmd_echo, status, payload…]
Status codes: 0x00 = success, 0x01 = operation error (e.g. no card in field), 0x02 = invalid parameters.
| subcmd | name | request payload (after subcmd byte) | reply payload (after subcmd + status) |
|---|---|---|---|
| 0x02 | Read | [start_page: u8, num_pages: u8] — exactly 3 bytes total | page data, 4 bytes per page (e.g. 135 pages × 4 = 540 bytes for NTAG 215) |
| 0x03 | Write | [page_addr: u8, data: 4 bytes] — 5 bytes total | empty |
| 0x04 | Authenticate (raw key) | [key: 16 bytes] — 17 bytes total | empty |
| 0x05 | Authenticate (key index) | [key_idx: BE u16] — 3 bytes total | empty |
| 0x06 | Authenticate (index + key) | [key_idx: BE u16, key: 16 bytes] — 19 bytes total | empty |
| 0x07 | Abort | none — exactly 1 byte total | empty |
| 0x00–0x01, 0x08+ | (invalid) | — | no reply frame; device logs “Unknown subcommand” |
Notes:
- Subcmds 0x02–0x07 are registered in
ProxCardUltralightC::ProxCardUltralightC(RFIDReader&)(confirmed by disassembly offeigARM binary from V0200.C72 firmware). - If no card is in the field, the device echoes
[subcmd, 0x01]immediately without attempting the operation. - To read all pages of an NTAG 215: send subcmd 0x02 with
start_page=0x00,num_pages=135.
Sample reader flow
- Host enables desired card type with
ProxCardFunction. - Reader acknowledges with
ProxCardFunctionReply. - Reader waits for card, seems like it eventually goes to sleep without any stimuli. Might have to keep it awake by sending packets occasionally (e.g.
Status). - When card is scanned, reader sends the corresponding read packet if the card type is enabled (e.g.
DESFireReadfor DESFire). If type is not enabled, sendsUnhandledCard. - Card data can then be queried by sending the equivalent command packets. DESFire commands are documented in m075031_desfire.pdf. Example for reading a page off a DESFire ( all of the following packets are of type
DESFireCommandandDESFireCommandReply): - Host sets DESFire application (e.g. packet type
DESFireCommandwith body0x5AF210E0for application ID0xE010F2). NOTE: The application ID will vary between issuer/agency. For example, the stock software uses application IDF9C32Bwhile Portland's TriMet uses the one in the example. You can check this for your card with an NFC reader or app. - Reader responds with a status code in accordance with the documentation. This comes in the form of a
DESFireCommandReplypacket. - Host sends read command. To read the full contents of file 0x00, the body is
0xBD00000000000000. - Reader responds with the status code and file data.
Reader -> Host
packet type 0xED seems to be a log message:
\xBC\xB9\0\xED\0h\xB6\x0306:48:13 Error: feclr_transceive() failed: error: 0, status: 0x00000008
0xB9 - card read?
Host -> reader
0xBC - appears after every read (ack of some sort?)
Card read flow
R: BCB600B9000BBB0C000000048A6EF29B149079CB8CF0 H: BC2200BC000158602A714F60 # DESFire GetVersion (60) R: BCB722BD001E3D00000401013300160504010103001605048A6EF29B149020487330305022C98C7E9B H: BC2300BC0004AA5A2BC3F911A8A56A # DESFire SelectApplication (5A 2BC3F9) R: BCB823BD0002E301FDE0523164
Another one (same payloads as previous but with different seq/hdrCRC):
R: BCBF00B9000B480C000000048A6EF29B149079CB8CF0 H: BC2400BC0001C4602A714F60 R: BCC024BD001E0800000401013300160504010103001605048A6EF29B149020487330305022C98C7E9B H: BC2500BC0004365A2BC3F911A8A56A R: BCC125BD00027401FDE0523164
Sample log message:
BCB000ED0068450330363A34383A3038204572726F723A206665636C725F7472616E7363656976652829206661696C65643A206572726F723A20302C207374617475733A2030783030303030303038200A205B524649445265616465724D756C74694170702E6370703A3135395D0002CD1533BCB100ED0046B40130363A34383A3038207472616E7363656976652065
00000000 bc b0 00 ed 00 68 45 03 30 36 3a 34 38 3a 30 38 |¼°.í.hE.06:48:08| 00000010 20 45 72 72 6f 72 3a 20 66 65 63 6c 72 5f 74 72 | Error: feclr_tr| 00000020 61 6e 73 63 65 69 76 65 28 29 20 66 61 69 6c 65 |ansceive() faile| 00000030 64 3a 20 65 72 72 6f 72 3a 20 30 2c 20 73 74 61 |d: error: 0, sta| 00000040 74 75 73 3a 20 30 78 30 30 30 30 30 30 30 38 20 |tus: 0x00000008 | 00000050 0a 20 5b 52 46 49 44 52 65 61 64 65 72 4d 75 6c |. [RFIDReaderMul| 00000060 74 69 41 70 70 2e 63 70 70 3a 31 35 39 5d 00 02 |tiApp.cpp:159]..| 00000070 cd 15 33 bc b1 00 ed 00 46 b4 01 30 36 3a 34 38 |Í.3¼±.í.F´.06:48| 00000080 3a 30 38 20 74 72 61 6e 73 63 65 69 76 65 20 65 |:08 transceive e|
There are 4 bytes appended at the end of some messages (presumably those with ID LSB=1, if the Rust struct description is accurate?) which is not a CRC32 with any polynomial I recognize, but a CRC32 of (message + CRC) is always 0xFFFFFFFF. These 4 bytes are NOT included in the length.
The checksum is _appended_, as can be seen by it following the log message (after the final \0).
is it a _little endian_, _bit negated_ CRC32. Which is weird because the length is big endian?..
* Host only sends two types of messages after initialization - both of type 0xBC. One is length 1 and the body is always 0x60 (+ negated CRC 2A714F60), the other is of length 4 and the body is always 5A2BC3F9 (+ negated-CRC 11A8A56A)
