this commit fixes #2244 #2246 #1596 #2101. Its kind of a big refactoring and I most likely broke something. With that said. Now: HF 15 commands now uses NG packets, hf 15 raw support -k keepfield on and -s select, hf 15 dump/rdbl/rdmulti should handle blocksizes of 4 or 8, the error messages are unified and error handling the same. Some understanding how add_option impacts response message from card. A more clear separation between PM3 flags and ISO15693 protocol flags.

This commit is contained in:
iceman1001 2024-01-14 14:23:51 +01:00
parent 224da75e7a
commit 8d0b41a911
11 changed files with 824 additions and 544 deletions

View file

@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
## [unreleased][unreleased] ## [unreleased][unreleased]
- Changed `hf 15 findafi` - improved the params (@iceman1001)
- Changed `hf 15 rdbl/rdmulti/dump` - should handle 4 vs 8 bytes block sizes in cards (@iceman1001)
- Changed `hf 15 *` - all 15 commands now uses NG packets (@iceman1001)
- Changed `hf 15 raw` - now supports "-k" keep field on and "-s" select (@iceman1001)
- Changed `prefs set client.debug` - now also toggles the client side APDU logging (@iceman1001) - Changed `prefs set client.debug` - now also toggles the client side APDU logging (@iceman1001)
- Changed `hf 14b sriwrbl` - now supports --force to override block checks. Thanks @gentilkiwi for the idea! (@iceman1001) - Changed `hf 14b sriwrbl` - now supports --force to override block checks. Thanks @gentilkiwi for the idea! (@iceman1001)
- Changed `hf 14b sriwrbl` - now tries to verify the write (@iceman1001) - Changed `hf 14b sriwrbl` - now tries to verify the write (@iceman1001)

View file

@ -815,14 +815,21 @@ static void PacketReceived(PacketCommandNG *packet) {
bool off; bool off;
} PACKED; } PACKED;
struct p *payload = (struct p *)packet->data.asBytes; struct p *payload = (struct p *)packet->data.asBytes;
if (payload->on && payload->off) if (payload->on && payload->off) {
reply_ng(CMD_SET_TEAROFF, PM3_EINVARG, NULL, 0); reply_ng(CMD_SET_TEAROFF, PM3_EINVARG, NULL, 0);
if (payload->on) }
if (payload->on) {
g_tearoff_enabled = true; g_tearoff_enabled = true;
if (payload->off) }
if (payload->off) {
g_tearoff_enabled = false; g_tearoff_enabled = false;
if (payload->delay_us > 0) }
if (payload->delay_us > 0) {
g_tearoff_delay_us = payload->delay_us; g_tearoff_delay_us = payload->delay_us;
}
reply_ng(CMD_SET_TEAROFF, PM3_SUCCESS, NULL, 0); reply_ng(CMD_SET_TEAROFF, PM3_SUCCESS, NULL, 0);
break; break;
} }
@ -1269,11 +1276,16 @@ static void PacketReceived(PacketCommandNG *packet) {
break; break;
} }
case CMD_HF_ISO15693_COMMAND: { case CMD_HF_ISO15693_COMMAND: {
DirectTag15693Command(packet->oldarg[0], packet->oldarg[1], packet->oldarg[2], packet->data.asBytes); iso15_raw_cmd_t *payload = (iso15_raw_cmd_t *)packet->data.asBytes;
SendRawCommand15693(payload);
break; break;
} }
case CMD_HF_ISO15693_FINDAFI: { case CMD_HF_ISO15693_FINDAFI: {
BruteforceIso15693Afi(packet->oldarg[0]); struct p {
uint32_t flags;
} PACKED;
struct p *payload = (struct p *)packet->data.asBytes;
BruteforceIso15693Afi(payload->flags);
break; break;
} }
case CMD_HF_ISO15693_READER: { case CMD_HF_ISO15693_READER: {

View file

@ -1937,8 +1937,8 @@ int SendDataTag(uint8_t *send, int sendlen, bool init, bool speed_fast, uint8_t
*eof_time = start_time + 32 * ((8 * ts->max) - 4); // subtract the 4 padding bits after EOF *eof_time = start_time + 32 * ((8 * ts->max) - 4); // subtract the 4 padding bits after EOF
LogTrace_ISO15693(send, sendlen, (start_time * 4), (*eof_time * 4), NULL, true); LogTrace_ISO15693(send, sendlen, (start_time * 4), (*eof_time * 4), NULL, true);
if (recv != NULL) { if (recv != NULL) {
bool fsk = send[0] & ISO15_REQ_SUBCARRIER_TWO; bool fsk = ((send[0] & ISO15_REQ_SUBCARRIER_TWO) == ISO15_REQ_SUBCARRIER_TWO);
bool recv_speed = send[0] & ISO15_REQ_DATARATE_HIGH; bool recv_speed = ((send[0] & ISO15_REQ_DATARATE_HIGH) == ISO15_REQ_DATARATE_HIGH);
res = GetIso15693AnswerFromTag(recv, max_recv_len, timeout, eof_time, fsk, recv_speed, resp_len); res = GetIso15693AnswerFromTag(recv, max_recv_len, timeout, eof_time, fsk, recv_speed, resp_len);
} }
return res; return res;
@ -2087,7 +2087,7 @@ void ReaderIso15693(iso15_card_select_t *p_card) {
reply_ng(CMD_HF_ISO15693_READER, PM3_SUCCESS, uid, sizeof(uid)); reply_ng(CMD_HF_ISO15693_READER, PM3_SUCCESS, uid, sizeof(uid));
if (g_dbglevel >= DBG_EXTENDED) { if (g_dbglevel >= DBG_EXTENDED) {
Dbprintf("[+] %d octets read from IDENTIFY request:", recvlen); Dbprintf("[+] %d bytes read from IDENTIFY request:", recvlen);
DbdecodeIso15693Answer(recvlen, answer); DbdecodeIso15693Answer(recvlen, answer);
Dbhexdump(recvlen, answer, true); Dbhexdump(recvlen, answer, true);
} }
@ -2360,16 +2360,20 @@ void SimTagIso15693(uint8_t *uid, uint8_t block_size) {
// Since there is no standardized way of reading the AFI out of a tag, we will brute force it // Since there is no standardized way of reading the AFI out of a tag, we will brute force it
// (some manufactures offer a way to read the AFI, though) // (some manufactures offer a way to read the AFI, though)
void BruteforceIso15693Afi(uint32_t speed) { void BruteforceIso15693Afi(uint32_t flags) {
clear_trace();
uint8_t data[7] = {0};
uint8_t recv[ISO15693_MAX_RESPONSE_LENGTH];
Iso15693InitReader(); Iso15693InitReader();
bool speed = ((flags & ISO15_HIGH_SPEED) == ISO15_HIGH_SPEED);
// first without AFI // first without AFI
// Tags should respond without AFI and with AFI=0 even when AFI is active // Tags should respond without AFI and with AFI=0 even when AFI is active
uint8_t data[7] = {0};
uint8_t recv[ISO15693_MAX_RESPONSE_LENGTH] = {0};
data[0] = ISO15_REQ_SUBCARRIER_SINGLE | ISO15_REQ_DATARATE_HIGH | ISO15_REQ_INVENTORY | ISO15_REQINV_SLOT1; data[0] = (ISO15_REQ_SUBCARRIER_SINGLE | ISO15_REQ_DATARATE_HIGH | ISO15_REQ_INVENTORY | ISO15_REQINV_SLOT1);
data[1] = ISO15693_INVENTORY; data[1] = ISO15693_INVENTORY;
data[2] = 0; // AFI data[2] = 0; // AFI
AddCrc15(data, 3); AddCrc15(data, 3);
@ -2434,16 +2438,26 @@ void BruteforceIso15693Afi(uint32_t speed) {
// Allows to directly send commands to the tag via the client // Allows to directly send commands to the tag via the client
// OBS: doesn't turn off rf field afterwards. // OBS: doesn't turn off rf field afterwards.
void DirectTag15693Command(uint32_t datalen, uint32_t speed, uint32_t recv, uint8_t *data) { void SendRawCommand15693(iso15_raw_cmd_t *packet) {
LED_A_ON(); LED_A_ON();
uint8_t recvbuf[ISO15693_MAX_RESPONSE_LENGTH]; uint16_t timeout = ISO15693_READER_TIMEOUT;
uint16_t timeout; if ((packet->flags & ISO15_LONG_WAIT) == ISO15_LONG_WAIT) {
uint32_t eof_time = 0; timeout = ISO15693_READER_TIMEOUT_WRITE;
bool request_answer = false; }
switch (data[1]) { bool speed = ((packet->flags & ISO15_HIGH_SPEED) == ISO15_HIGH_SPEED);
bool keep_field_on = ((packet->flags & ISO15_NO_DISCONNECT) == ISO15_NO_DISCONNECT);
bool read_respone = ((packet->flags & ISO15_READ_RESPONSE) == ISO15_READ_RESPONSE);
bool init = ((packet->flags & ISO15_CONNECT) == ISO15_CONNECT);
// This isn't part of the RAW FLAGS from the client.
// This is part of ISO15693 protocol definitions where the following commands needs to request option.
bool request_answer = false;
switch (packet->raw[1]) {
case ISO15693_SET_PASSWORD:
case ISO15693_ENABLE_PRIVACY:
case ISO15693_WRITEBLOCK: case ISO15693_WRITEBLOCK:
case ISO15693_LOCKBLOCK: case ISO15693_LOCKBLOCK:
case ISO15693_WRITE_MULTI_BLOCK: case ISO15693_WRITE_MULTI_BLOCK:
@ -2453,42 +2467,50 @@ void DirectTag15693Command(uint32_t datalen, uint32_t speed, uint32_t recv, uint
case ISO15693_WRITE_PASSWORD: case ISO15693_WRITE_PASSWORD:
case ISO15693_PASSWORD_PROTECT_EAS: case ISO15693_PASSWORD_PROTECT_EAS:
case ISO15693_LOCK_DSFID: case ISO15693_LOCK_DSFID:
timeout = ISO15693_READER_TIMEOUT_WRITE; request_answer = ((packet->raw[0] & ISO15_REQ_OPTION) == ISO15_REQ_OPTION);
request_answer = data[0] & ISO15_REQ_OPTION;
break; break;
default: default:
timeout = ISO15693_READER_TIMEOUT; break;
} }
uint32_t eof_time = 0;
uint32_t start_time = 0; uint32_t start_time = 0;
uint16_t recvlen = 0; uint16_t recvlen = 0;
int res = SendDataTag(data, datalen, true, speed, (recv ? recvbuf : NULL), sizeof(recvbuf), start_time, timeout, &eof_time, &recvlen);
uint8_t buf[ISO15693_MAX_RESPONSE_LENGTH] = {0x00};
int res = SendDataTag(packet->raw, packet->rawlen, init, speed, (read_respone ? buf : NULL), sizeof(buf), start_time, timeout, &eof_time, &recvlen);
if (res == PM3_ETEAROFF) { // tearoff occurred if (res == PM3_ETEAROFF) { // tearoff occurred
reply_ng(CMD_HF_ISO15693_COMMAND, res, NULL, 0); reply_ng(CMD_HF_ISO15693_COMMAND, res, NULL, 0);
} else { } else {
bool fsk = data[0] & ISO15_REQ_SUBCARRIER_TWO; // looking at the first byte of the RAW bytes to determine Subcarrier, datarate, request option
bool recv_speed = data[0] & ISO15_REQ_DATARATE_HIGH; bool fsk = packet->raw[0] & ISO15_REQ_SUBCARRIER_TWO;
bool recv_speed = packet->raw[0] & ISO15_REQ_DATARATE_HIGH;
// send a single EOF to get the tag response // send a single EOF to get the tag response
if (request_answer) { if (request_answer) {
start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER;
res = SendDataTagEOF((recv ? recvbuf : NULL), sizeof(recvbuf), start_time, ISO15693_READER_TIMEOUT, &eof_time, fsk, recv_speed, &recvlen); res = SendDataTagEOF((read_respone ? buf : NULL), sizeof(buf), start_time, ISO15693_READER_TIMEOUT, &eof_time, fsk, recv_speed, &recvlen);
} }
if (recv) { if (read_respone) {
recvlen = MIN(recvlen, ISO15693_MAX_RESPONSE_LENGTH); recvlen = MIN(recvlen, ISO15693_MAX_RESPONSE_LENGTH);
reply_ng(CMD_HF_ISO15693_COMMAND, res, recvbuf, recvlen); reply_ng(CMD_HF_ISO15693_COMMAND, res, buf, recvlen);
} else { } else {
reply_ng(CMD_HF_ISO15693_COMMAND, PM3_SUCCESS, NULL, 0); reply_ng(CMD_HF_ISO15693_COMMAND, PM3_SUCCESS, NULL, 0);
} }
} }
// note: this prevents using hf 15 cmd with s option - which isn't implemented yet anyway // note: this prevents using hf 15 cmd with s option - which isn't implemented yet anyway
// also prevents hf 15 raw -k keep_field on ...
FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); if (keep_field_on == false) {
LED_D_OFF(); switch_off(); // disconnect raw
SpinDelay(20);
}
LED_A_OFF();
} }
/* /*

View file

@ -48,8 +48,8 @@ void AcquireRawAdcSamplesIso15693(void);
void ReaderIso15693(iso15_card_select_t *p_card); // ISO15693 reader void ReaderIso15693(iso15_card_select_t *p_card); // ISO15693 reader
void EmlClearIso15693(void); void EmlClearIso15693(void);
void SimTagIso15693(uint8_t *uid, uint8_t block_size); // simulate an ISO15693 tag void SimTagIso15693(uint8_t *uid, uint8_t block_size); // simulate an ISO15693 tag
void BruteforceIso15693Afi(uint32_t speed); // find an AFI of a tag void BruteforceIso15693Afi(uint32_t flags); // find an AFI of a tag
void DirectTag15693Command(uint32_t datalen, uint32_t speed, uint32_t recv, uint8_t *data); // send arbitrary commands from CLI void SendRawCommand15693(iso15_raw_cmd_t *packet); // send arbitrary commands from CLI
void SniffIso15693(uint8_t jam_search_len, uint8_t *jam_search_string, bool iclass); void SniffIso15693(uint8_t jam_search_len, uint8_t *jam_search_string, bool iclass);

File diff suppressed because it is too large Load diff

View file

@ -263,28 +263,32 @@ static const char *felica_model_name(uint8_t rom_type, uint8_t ic_type) {
* @param verbose prints out the response received. * @param verbose prints out the response received.
*/ */
static bool waitCmdFelica(uint8_t iSelect, PacketResponseNG *resp, bool verbose) { static bool waitCmdFelica(uint8_t iSelect, PacketResponseNG *resp, bool verbose) {
if (WaitForResponseTimeout(CMD_ACK, resp, 2000)) { if (WaitForResponseTimeout(CMD_ACK, resp, 2000) == false) {
uint16_t len = iSelect ? (resp->oldarg[1] & 0xffff) : (resp->oldarg[0] & 0xffff);
if (verbose) {
PrintAndLogEx(SUCCESS, "client received %i octets", len);
if (len == 0 || len == 1) {
PrintAndLogEx(ERR, "Could not receive data correctly!");
return false;
}
PrintAndLogEx(SUCCESS, "%s", sprint_hex(resp->data.asBytes, len));
if (!check_crc(CRC_FELICA, resp->data.asBytes + 2, len - 2)) {
PrintAndLogEx(WARNING, "wrong or no CRC bytes");
}
if (resp->data.asBytes[0] != 0xB2 && resp->data.asBytes[1] != 0x4D) {
PrintAndLogEx(ERR, "received incorrect frame format!");
return false;
}
}
return true;
} else {
PrintAndLogEx(WARNING, "timeout while waiting for reply."); PrintAndLogEx(WARNING, "timeout while waiting for reply.");
return false;
} }
return false;
uint16_t len = iSelect ? (resp->oldarg[1] & 0xffff) : (resp->oldarg[0] & 0xffff);
if (verbose) {
if (len == 0 || len == 1) {
PrintAndLogEx(ERR, "Could not receive data correctly!");
return false;
}
PrintAndLogEx(SUCCESS, "(%u) %s", len, sprint_hex(resp->data.asBytes, len));
if (check_crc(CRC_FELICA, resp->data.asBytes + 2, len - 2) == false) {
PrintAndLogEx(WARNING, "CRC ( " _RED_("fail") " )");
}
if (resp->data.asBytes[0] != 0xB2 && resp->data.asBytes[1] != 0x4D) {
PrintAndLogEx(ERR, "received incorrect frame format!");
return false;
}
}
return true;
} }

View file

@ -1690,7 +1690,7 @@ static int acquire_nonces(uint8_t blockNo, uint8_t keyType, uint8_t *key, uint8_
} }
if (acquisition_completed) { if (acquisition_completed) {
field_off = true; // switch off field with next SendCommandOLD and then finish field_off = true; // switch off field with next SendCommandMIX and then finish
} }
if (initialize == false) { if (initialize == false) {

View file

@ -44,10 +44,11 @@ static isodep_state_t isodep_state = ISODEP_INACTIVE;
void SetISODEPState(isodep_state_t state) { void SetISODEPState(isodep_state_t state) {
isodep_state = state; isodep_state = state;
if (APDULogging) { if (APDULogging) {
PrintAndLogEx(SUCCESS, "Setting ISODEP -> %s%s%s" PrintAndLogEx(SUCCESS, "Setting ISODEP -> %s%s%s%s"
, isodep_state == ISODEP_INACTIVE ? "inactive" : "" , isodep_state == ISODEP_INACTIVE ? "inactive" : ""
, isodep_state == ISODEP_NFCA ? _GREEN_("NFC-A") : "" , isodep_state == ISODEP_NFCA ? _GREEN_("NFC-A") : ""
, isodep_state == ISODEP_NFCB ? _GREEN_("NFC-B") : "" , isodep_state == ISODEP_NFCB ? _GREEN_("NFC-B") : ""
, isodep_state == ISODEP_NFCV ? _GREEN_("NFC-V") : ""
); );
} }
} }
@ -64,7 +65,6 @@ int Iso7816Connect(Iso7816CommandChannel channel) {
// select with no disconnect and set frameLength // select with no disconnect and set frameLength
int res = SelectCard14443A_4(false, false, NULL); int res = SelectCard14443A_4(false, false, NULL);
if (res == PM3_SUCCESS) { if (res == PM3_SUCCESS) {
SetISODEPState(ISODEP_NFCA);
return PM3_SUCCESS; return PM3_SUCCESS;
} }
@ -72,7 +72,6 @@ int Iso7816Connect(Iso7816CommandChannel channel) {
// If not 14a, try to 14b // If not 14a, try to 14b
res = select_card_14443b_4(false, NULL); res = select_card_14443b_4(false, NULL);
if (res == PM3_SUCCESS) { if (res == PM3_SUCCESS) {
SetISODEPState(ISODEP_NFCB);
return PM3_SUCCESS; return PM3_SUCCESS;
} }
@ -110,8 +109,9 @@ int Iso7816ExchangeEx(Iso7816CommandChannel channel, bool activate_field, bool l
return 201; return 201;
} }
if (APDULogging) if (APDULogging) {
PrintAndLogEx(SUCCESS, ">>>> %s", sprint_hex(data, datalen)); PrintAndLogEx(SUCCESS, ">>>> %s", sprint_hex(data, datalen));
}
int res = 0; int res = 0;
@ -125,6 +125,9 @@ int Iso7816ExchangeEx(Iso7816CommandChannel channel, bool activate_field, bool l
case ISODEP_NFCB: case ISODEP_NFCB:
res = exchange_14b_apdu(data, datalen, activate_field, leave_field_on, result, (int)max_result_len, (int *)result_len, 4000); res = exchange_14b_apdu(data, datalen, activate_field, leave_field_on, result, (int)max_result_len, (int *)result_len, 4000);
break; break;
case ISODEP_NFCV:
PrintAndLogEx(INFO, " To be implemented, feel free to contribute!");
break;
case ISODEP_INACTIVE: case ISODEP_INACTIVE:
if (activate_field == false) { if (activate_field == false) {
PrintAndLogEx(FAILED, "Field currently inactive, cannot send an APDU"); PrintAndLogEx(FAILED, "Field currently inactive, cannot send an APDU");
@ -133,11 +136,7 @@ int Iso7816ExchangeEx(Iso7816CommandChannel channel, bool activate_field, bool l
res = ExchangeAPDU14a(data, datalen, activate_field, leave_field_on, result, (int)max_result_len, (int *)result_len); res = ExchangeAPDU14a(data, datalen, activate_field, leave_field_on, result, (int)max_result_len, (int *)result_len);
if (res != PM3_SUCCESS) { if (res != PM3_SUCCESS) {
res = exchange_14b_apdu(data, datalen, activate_field, leave_field_on, result, (int)max_result_len, (int *)result_len, 4000); res = exchange_14b_apdu(data, datalen, activate_field, leave_field_on, result, (int)max_result_len, (int *)result_len, 4000);
if (res == PM3_SUCCESS) { PrintAndLogEx(INFO, "Testing ISO14443-B... ( %s )", (res == PM3_SUCCESS) ? _GREEN_("ok") : _RED_("fail"));
PrintAndLogEx(INFO, "Testing ISO14443-B... ( " _GREEN_("ok") " )");
} else {
PrintAndLogEx(INFO, "Testing ISO14443-B... ( " _RED_("fail") " )");
}
} }
break; break;
} }
@ -160,8 +159,9 @@ int Iso7816ExchangeEx(Iso7816CommandChannel channel, bool activate_field, bool l
} }
} }
if (APDULogging) if (APDULogging) {
PrintAndLogEx(SUCCESS, "<<<< %s", sprint_hex(result, *result_len)); PrintAndLogEx(SUCCESS, "<<<< %s", sprint_hex(result, *result_len));
}
if (*result_len < 2) { if (*result_len < 2) {
return 200; return 200;

View file

@ -30,6 +30,7 @@ typedef enum {
ISODEP_INACTIVE = 0, ISODEP_INACTIVE = 0,
ISODEP_NFCA, ISODEP_NFCA,
ISODEP_NFCB, ISODEP_NFCB,
ISODEP_NFCV,
} isodep_state_t; } isodep_state_t;
typedef enum { typedef enum {

View file

@ -202,17 +202,22 @@ void ResetSspClk(void) {
AT91C_BASE_TC2->TC_CCR = AT91C_TC_CLKEN | AT91C_TC_SWTRG; AT91C_BASE_TC2->TC_CCR = AT91C_TC_CLKEN | AT91C_TC_SWTRG;
while (AT91C_BASE_TC2->TC_CV > 0); while (AT91C_BASE_TC2->TC_CV > 0);
} }
uint32_t RAMFUNC GetCountSspClk(void) { uint32_t RAMFUNC GetCountSspClk(void) {
uint32_t tmp_count = (AT91C_BASE_TC2->TC_CV << 16) | AT91C_BASE_TC0->TC_CV; uint32_t tmp_count = (AT91C_BASE_TC2->TC_CV << 16) | AT91C_BASE_TC0->TC_CV;
if ((tmp_count & 0x0000ffff) == 0) //small chance that we may have missed an increment in TC2
// small chance that we may have missed an increment in TC2
if ((tmp_count & 0x0000ffff) == 0) {
return (AT91C_BASE_TC2->TC_CV << 16); return (AT91C_BASE_TC2->TC_CV << 16);
}
return tmp_count; return tmp_count;
} }
uint32_t RAMFUNC GetCountSspClkDelta(uint32_t start) { uint32_t RAMFUNC GetCountSspClkDelta(uint32_t start) {
uint32_t stop = GetCountSspClk(); uint32_t stop = GetCountSspClk();
if (stop >= start) if (stop >= start) {
return stop - start; return stop - start;
}
return (UINT32_MAX - start) + stop; return (UINT32_MAX - start) + stop;
} }

View file

@ -34,8 +34,14 @@ typedef enum ISO15_COMMAND {
ISO15_RAW = (1 << 2), ISO15_RAW = (1 << 2),
ISO15_APPEND_CRC = (1 << 3), ISO15_APPEND_CRC = (1 << 3),
ISO15_HIGH_SPEED = (1 << 4), ISO15_HIGH_SPEED = (1 << 4),
ISO15_READ_RESPONSE = (1 << 5) ISO15_READ_RESPONSE = (1 << 5),
ISO15_LONG_WAIT = (1 << 6),
} iso15_command_t; } iso15_command_t;
typedef struct {
uint8_t flags; // ISO15_COMMAND
uint16_t rawlen;
uint8_t raw[];
} PACKED iso15_raw_cmd_t;
#endif // _ISO15_H_ #endif // _ISO15_H_