diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 5a2d08d2a..f1565e916 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -421,6 +421,9 @@ static void SendStatus(uint32_t wait) { #endif #ifdef WITH_ISO14443a printHf14aConfig(); // HF 14a config +#endif +#ifdef WITH_ISO14443b + printHf14bConfig(); // HF 14b config #endif printConnSpeed(wait); DbpString(_CYAN_("Various")); @@ -1640,6 +1643,21 @@ static void PacketReceived(PacketCommandNG *packet) { SendRawCommand14443B(payload); break; } + case CMD_HF_ISO14443B_PRINT_CONFIG: { + printHf14bConfig(); + break; + } + case CMD_HF_ISO14443B_GET_CONFIG: { + hf14b_config_t *c = getHf14bConfig(); + reply_ng(CMD_HF_ISO14443B_GET_CONFIG, PM3_SUCCESS, (uint8_t *)c, sizeof(hf14b_config_t)); + break; + } + case CMD_HF_ISO14443B_SET_CONFIG: { + hf14b_config_t c; + memcpy(&c, packet->data.asBytes, sizeof(hf14b_config_t)); + setHf14bConfig(&c); + break; + } case CMD_HF_CRYPTORF_SIM : { // simulate_crf_tag(); break; diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 0a324ff30..54c455b38 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -199,6 +199,32 @@ static uint32_t s_iso14b_timeout = MAX_14B_TIMEOUT; static bool s_field_on = false; +/* +Default HF 14b config is set to: + polling_loop_annotation = {{0}, 0, 0, 0} (disabled) +*/ +static hf14b_config_t hf14bconfig = { {{0}, 0, 0, 0} }; + +void printHf14bConfig(void) { + DbpString(_CYAN_("HF 14b config")); + Dbprintf(" [p] Polling loop annotation.... %s %*D", + (hf14bconfig.polling_loop_annotation.frame_length <= 0) ? _YELLOW_("disabled") : _GREEN_("enabled"), + hf14bconfig.polling_loop_annotation.frame_length, + hf14bconfig.polling_loop_annotation.frame, + "" + ); +} + +void setHf14bConfig(const hf14b_config_t *hc) { + if (hc->polling_loop_annotation.frame_length >= 0) { + memcpy(&hf14bconfig.polling_loop_annotation, &hc->polling_loop_annotation, sizeof(iso14b_polling_frame_t)); + } +} + +hf14b_config_t *getHf14bConfig(void) { + return &hf14bconfig; +} + /* * ISO 14443-B communications * -------------------------- @@ -2053,9 +2079,28 @@ int iso14443b_select_card(iso14b_card_select_t *card) { uint8_t r_pupid[14] = { 0x00 }; uint8_t r_attrib[3] = { 0x00 }; - // first, wake up the tag uint32_t start_time = 0; uint32_t eof_time = 0; + + // Send polling loop annotation if configured (3 times before WUPB) + if (hf14bconfig.polling_loop_annotation.frame_length > 0) { + const iso14b_polling_frame_t *pla = &hf14bconfig.polling_loop_annotation; + + for (int i = 0; i < 3; i++) { + CodeAndTransmit14443bAsReader(pla->frame, pla->frame_length, &start_time, &eof_time, true); + + // Add extra delay if specified + if (pla->extra_delay > 0) { + SpinDelayUs(pla->extra_delay * 1000); + } + + // Reset timing for the next iteration + start_time = 0; + eof_time = 0; + } + } + + // first, wake up the tag CodeAndTransmit14443bAsReader(wupb, sizeof(wupb), &start_time, &eof_time, true); eof_time += DELAY_ISO14443B_PCD_TO_PICC_READER; diff --git a/armsrc/iso14443b.h b/armsrc/iso14443b.h index 7dadda7bd..c8b8629c1 100644 --- a/armsrc/iso14443b.h +++ b/armsrc/iso14443b.h @@ -51,6 +51,11 @@ void SniffIso14443b(void); void SendRawCommand14443B(iso14b_raw_cmd_t *p); void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_time, uint32_t *eof_time, bool framing); +// 14b config +void printHf14bConfig(void); +void setHf14bConfig(const hf14b_config_t *hc); +hf14b_config_t *getHf14bConfig(void); + // States for 14B SIM command #define SIM_POWER_OFF 0 #define SIM_IDLE 1 diff --git a/client/Makefile b/client/Makefile index 2dcaf415b..b305e9e09 100644 --- a/client/Makefile +++ b/client/Makefile @@ -629,6 +629,7 @@ SRCS = mifare/aiddesfire.c \ cmdflashmemspiffs.c \ cmdhf.c \ cmdhf14a.c \ + pla.c \ cmdhf14b.c \ cmdhf15.c \ cmdhfcryptorf.c \ diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 41a29ff8d..bb820a64e 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -47,6 +47,7 @@ #include "pm3_cmd.h" #include "mbedtls/cmac.h" #include "jansson.h" // JSON parsing +#include "pla.h" // ECP parsing static bool g_apdu_in_framing_enable = true; bool Get_apdu_in_framing(void) { @@ -330,381 +331,6 @@ int hf14a_setconfig(hf14a_config_t *config, bool verbose) { return PM3_SUCCESS; } -// Load ecplist.json file -static json_t *load_ecplist(void) { - json_error_t error; - char *path; - - int res = searchFile(&path, RESOURCES_SUBDIR, "ecplist", ".json", false); - if (res != PM3_SUCCESS) { - PrintAndLogEx(ERR, "Cannot find ecplist.json"); - return NULL; - } - - json_t *root = json_load_file(path, 0, &error); - free(path); - - if (!root) { - PrintAndLogEx(ERR, "json error on line %d: %s", error.line, error.text); - return NULL; - } - - if (!json_is_array(root)) { - PrintAndLogEx(ERR, "Invalid ecplist.json format. Root must be an array."); - json_decref(root); - return NULL; - } - - return root; -} - -// Search ecplist for an entry matching the given type, subtype and/or key -// If type is not NULL, only search entries with matching "type" field (supports string or array) -// If subtype is not NULL, searches in "subtype" field (supports string or array) -// If key is not NULL, searches in "key" field (supports string or array) -static json_t *search_ecplist_by_key(json_t *root, const char *type, const char *subtype, const char *key) { - size_t index; - json_t *entry; - - json_array_foreach(root, index, entry) { - // If type filter is specified, check if entry has matching type - if (type != NULL) { - json_t *type_obj = json_object_get(entry, "type"); - if (!type_obj) { - continue; // Skip entries without type field - } - - bool type_matched = false; - if (json_is_string(type_obj)) { - const char *type_str = json_string_value(type_obj); - if (type_str && strcmp(type_str, type) == 0) { - type_matched = true; - } - } else if (json_is_array(type_obj)) { - size_t type_index; - json_t *type_value; - json_array_foreach(type_obj, type_index, type_value) { - const char *type_str = json_string_value(type_value); - if (type_str && strcmp(type_str, type) == 0) { - type_matched = true; - break; - } - } - } - - if (!type_matched) { - continue; // Type doesn't match - } - } else { - // If no type filter, skip entries that have a type field - if (json_object_get(entry, "type")) { - continue; - } - } - - bool key_matched = (key == NULL); // If no key specified, consider it matched - bool subtype_matched = (subtype == NULL); // If no subtype specified, consider it matched - - // Check if the subtype matches the "subtype" field (string or array) - if (subtype != NULL) { - json_t *subtype_obj = json_object_get(entry, "subtype"); - if (subtype_obj) { - if (json_is_string(subtype_obj)) { - const char *subtype_str = json_string_value(subtype_obj); - if (subtype_str && strcmp(subtype_str, subtype) == 0) { - subtype_matched = true; - } - } else if (json_is_array(subtype_obj)) { - size_t subtype_index; - json_t *subtype_value; - json_array_foreach(subtype_obj, subtype_index, subtype_value) { - const char *subtype_str = json_string_value(subtype_value); - if (subtype_str && strcmp(subtype_str, subtype) == 0) { - subtype_matched = true; - break; - } - } - } - } - } - - // Check if the key matches the "key" field (string or array) - if (key != NULL) { - json_t *key_obj = json_object_get(entry, "key"); - if (key_obj) { - if (json_is_string(key_obj)) { - const char *key_str = json_string_value(key_obj); - if (key_str && strcmp(key_str, key) == 0) { - key_matched = true; - } - } else if (json_is_array(key_obj)) { - size_t key_index; - json_t *key_value; - json_array_foreach(key_obj, key_index, key_value) { - const char *key_str = json_string_value(key_value); - if (key_str && strcmp(key_str, key) == 0) { - key_matched = true; - break; - } - } - } - } - } - - // Entry must match both key and subtype criteria (if specified) - if (key_matched && subtype_matched) { - return entry; - } - } - - return NULL; // Not found -} - -// Helper function to parse ECP (Enhanced Contactless Polling) subcommands -// Returns the length of the generated frame (without CRC), or -1 on error -static int parse_ecp_subcommand(const char *cmd, uint8_t *frame, size_t frame_size) { - if (cmd == NULL || frame == NULL || frame_size < 22) { - return -1; - } - - // Make a mutable copy of the command and replace dots/colons with spaces - char *cmd_copy = strdup(cmd); - if (!cmd_copy) { - return -1; - } - - for (char *p_char = cmd_copy; *p_char != '\0'; p_char++) { - if (*p_char == '.' || *p_char == ':') { - *p_char = ' '; - } - } - - // Load ecplist.json - json_t *ecplist = load_ecplist(); - if (!ecplist) { - PrintAndLogEx(ERR, "Failed to load ecplist.json"); - free(cmd_copy); - return -1; - } - - // Skip "ecp" prefix and any whitespace - const char *p = cmd_copy; - if (strncmp(p, "ecp", 3) == 0) { - p += 3; - } - while (*p == ' ' || *p == '\t') { - p++; - } - - int result = -1; - const char *type = NULL; - const char *search_term = p; - - // Check if first term is a type ("transit" or "access") - if (strncmp(p, "transit", 7) == 0) { - type = "transit"; - p += 7; - while (*p == ' ' || *p == '\t') { - p++; - } - search_term = p; - - // If second term provided, search by key in transit entries - if (*p != '\0') { - json_t *entry = search_ecplist_by_key(ecplist, type, NULL, search_term); - if (entry) { - // Found matching entry, use its value - json_t *value_obj = json_object_get(entry, "value"); - if (value_obj) { - const char *hex_str = json_string_value(value_obj); - if (hex_str) { - size_t hex_len = strlen(hex_str); - if (hex_len % 2 == 0 && hex_len <= frame_size * 2) { - for (size_t i = 0; i < hex_len / 2; i++) { - sscanf(hex_str + i * 2, "%2hhx", &frame[i]); - } - result = hex_len / 2; - } - } - } - } - - // If not found, try interpreting as hex TCI - if (result == -1) { - char *endptr; - uint32_t tci = strtoul(search_term, &endptr, 16); - if (search_term != endptr) { - // Build frame: 6a02c801000300{tci as 3 bytes}0000000000 - frame[0] = 0x6a; - frame[1] = 0x02; - frame[2] = 0xc8; - frame[3] = 0x01; - frame[4] = 0x00; - frame[5] = (tci >> 16) & 0xff; - frame[6] = (tci >> 8) & 0xff; - frame[7] = tci & 0xff; - frame[8] = 0x00; - frame[9] = 0x00; - frame[10] = 0x00; - frame[11] = 0x00; - frame[12] = 0x00; - result = 13; - } else { - PrintAndLogEx(ERR, "Unknown transit key or invalid TCI: %s", search_term); - } - } - } else { - PrintAndLogEx(ERR, "Transit type requires a key or TCI value"); - } - - } else if (strncmp(p, "access", 6) == 0) { - type = "access"; - p += 6; - while (*p == ' ' || *p == '\t') { - p++; - } - - // Parse second term - const char *second_term = p; - - // Skip to end of second term - while (*p != '\0' && *p != ' ' && *p != '\t') { - p++; - } - - // Extract second term - size_t second_term_len = p - second_term; - char *second = NULL; - if (second_term_len > 0) { - second = str_ndup(second_term, second_term_len); - } - - // Skip whitespace - while (*p == ' ' || *p == '\t') { - p++; - } - - // Parse third term if present - const char *third_term = p; - char *third = NULL; - if (*p != '\0') { - // Skip to end of third term - while (*p != '\0' && *p != ' ' && *p != '\t') { - p++; - } - size_t third_term_len = p - third_term; - if (third_term_len > 0) { - third = str_ndup(third_term, third_term_len); - } - } - - // Default TCI is 02ffff if not provided - uint32_t tci = 0x02ffff; - - // If terms provided, try to parse them - if (second != NULL && *second != '\0') { - json_t *entry = NULL; - - if (third != NULL && *third != '\0') { - // Two terms: second is subtype, third is key - entry = search_ecplist_by_key(ecplist, type, second, third); - } else { - // One term: try as subtype first, then as key - entry = search_ecplist_by_key(ecplist, type, second, NULL); - - if (!entry) { - entry = search_ecplist_by_key(ecplist, type, NULL, second); - } - } - - if (entry) { - // Found matching entry, use its value - json_t *value_obj = json_object_get(entry, "value"); - if (value_obj) { - const char *hex_str = json_string_value(value_obj); - if (hex_str) { - size_t hex_len = strlen(hex_str); - if (hex_len % 2 == 0 && hex_len <= frame_size * 2) { - for (size_t i = 0; i < hex_len / 2; i++) { - sscanf(hex_str + i * 2, "%2hhx", &frame[i]); - } - result = hex_len / 2; - } - } - } - } - - // If not found and no third term, try interpreting second term as hex TCI - if (result == -1 && third == NULL) { - char *endptr; - tci = strtoul(second, &endptr, 16); - if (second == endptr) { - PrintAndLogEx(ERR, "Unknown access subtype/key or invalid TCI: %s", second); - free(second); - if (third) free(third); - json_decref(ecplist); - free(cmd_copy); - return -1; - } - } else if (result == -1 && third != NULL) { - PrintAndLogEx(ERR, "No matching access entry for subtype '%s' and key '%s'", second, third); - free(second); - free(third); - json_decref(ecplist); - free(cmd_copy); - return -1; - } - } - - if (second) { - free(second); - } - if (third) { - free(third); - } - - // Build frame with TCI if we didn't find a matching entry - if (result == -1) { - // Build frame: 6a02c30200{tci as 3 bytes} - frame[0] = 0x6a; - frame[1] = 0x02; - frame[2] = 0xc3; - frame[3] = 0x02; - frame[4] = 0x00; - frame[5] = (tci >> 16) & 0xff; - frame[6] = (tci >> 8) & 0xff; - frame[7] = tci & 0xff; - result = 8; - } - - } else { - // No type specified, search for entries without type field by key - json_t *entry = search_ecplist_by_key(ecplist, search_term, NULL, NULL); - if (entry) { - json_t *value_obj = json_object_get(entry, "value"); - if (value_obj) { - const char *hex_str = json_string_value(value_obj); - if (hex_str) { - size_t hex_len = strlen(hex_str); - if (hex_len % 2 == 0 && hex_len <= frame_size * 2) { - for (size_t i = 0; i < hex_len / 2; i++) { - sscanf(hex_str + i * 2, "%2hhx", &frame[i]); - } - result = hex_len / 2; - } - } - } - } else { - PrintAndLogEx(ERR, "Unknown ECP type: %s", search_term); - PrintAndLogEx(HINT, "Available types: access, transit, vasorpay, vasandpay, vasonly, payonly, gymkit, identity, aidrop"); - } - } - - json_decref(ecplist); - free(cmd_copy); - return result; -} - static int hf_14a_config_example(void) { PrintAndLogEx(NORMAL, "\nExamples to revive Gen2/DirectWrite magic cards failing at anticollision:"); PrintAndLogEx(NORMAL, _CYAN_(" MFC 1k 4b UID")":"); @@ -891,7 +517,7 @@ static int CmdHf14AConfig(const char *Cmd) { else if (strncmp((char *)value, "off", 3) == 0) pla.frame_length = 0; else if (strncmp((char *)value, "ecp", 3) == 0) { // Parse ECP subcommand - int length = parse_ecp_subcommand((char *)value, pla.frame, sizeof(pla.frame)); + int length = pla_parse_ecp_subcommand((char *)value, pla.frame, sizeof(pla.frame)); if (length < 0) { CLIParserFree(ctx); return PM3_EINVARG; diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index ea7df2569..d8086f922 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -36,6 +36,7 @@ #include "iclass_cmd.h" // picopass defines #include "cmdhf.h" // handle HF plot #include "atrs.h" // atqbToEmulatedAtr +#include "pla.h" // ECP parsing #define MAX_14B_TIMEOUT_MS (4949U) @@ -878,6 +879,120 @@ static void print_sr_blocks(uint8_t *data, size_t len, const uint8_t *uid, bool // 0200a404000ca000000063504b43532d313500 (resp 02 6a 82 [4b 4c]) // 0200a4040010a000000018300301000000000000000000 (resp 02 6a 82 [4b 4c]) +static int hf14b_setconfig(hf14b_config_t *config, bool verbose) { + if (!g_session.pm3_present) return PM3_ENOTTY; + + clearCommandBuffer(); + if (config != NULL) { + SendCommandNG(CMD_HF_ISO14443B_SET_CONFIG, (uint8_t *)config, sizeof(hf14b_config_t)); + if (verbose) { + SendCommandNG(CMD_HF_ISO14443B_PRINT_CONFIG, NULL, 0); + } + } else { + SendCommandNG(CMD_HF_ISO14443B_PRINT_CONFIG, NULL, 0); + } + + return PM3_SUCCESS; +} + +static int CmdHf14BConfig(const char *Cmd) { + if (!g_session.pm3_present) return PM3_ENOTTY; + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf 14b config", + "Configure 14b settings (use with caution)\n", + "hf 14b config -> Print current configuration\n" + "hf 14b config --std -> Reset default configuration\n" + "hf 14b config --pla -> Set polling loop annotation (max 22 bytes)\n" + "hf 14b config --pla off -> Disable polling loop annotation\n" + "hf 14b config --pla ecp.access -> Set ECP Access (default)\n" + "hf 14b config --pla ecp.transit.emv -> Set ECP Transit for EMV\n"); + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "pla", "", "Configure polling loop annotation"), + arg_lit0(NULL, "std", "Reset default configuration"), + arg_lit0("v", "verbose", "verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool defaults = arg_get_lit(ctx, 2); + bool verbose = arg_get_lit(ctx, 3); + + int vlen = 0; + char value[64]; + + // Handle polling loop annotation parameter + iso14b_polling_frame_t pla = { + // 0 signals that PLA has to be disabled, -1 signals that no change has to be made + .frame_length = defaults ? 0 : -1, + .last_byte_bits = 8, + .extra_delay = 30 + }; + + // Get main --pla value + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)value, sizeof(value), &vlen); + str_lower((char *)value); + + if (vlen > 0) { + if (strncmp((char *)value, "std", 3) == 0) pla.frame_length = 0; + else if (strncmp((char *)value, "skip", 4) == 0) pla.frame_length = 0; + else if (strncmp((char *)value, "disable", 3) == 0) pla.frame_length = 0; + else if (strncmp((char *)value, "off", 3) == 0) pla.frame_length = 0; + else if (strncmp((char *)value, "ecp", 3) == 0) { + // Parse ECP subcommand + int length = pla_parse_ecp_subcommand((char *)value, pla.frame, sizeof(pla.frame)); + if (length < 0) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + pla.frame_length = length; + + // Add CRC + uint8_t first, second; + compute_crc(CRC_14443_B, pla.frame, pla.frame_length, &first, &second); + pla.frame[pla.frame_length++] = first; + pla.frame[pla.frame_length++] = second; + PrintAndLogEx(INFO, "Set polling loop annotation to ECP: %s", sprint_hex(pla.frame, pla.frame_length)); + } else { + // Convert hex string to bytes + int length = 0; + if (param_gethex_to_eol((char *)value, 0, pla.frame, sizeof(pla.frame), &length) != 0) { + PrintAndLogEx(ERR, "Error parsing polling loop annotation bytes"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + pla.frame_length = length; + + // Validate length before adding CRC + if (pla.frame_length < 1 || pla.frame_length > 22) { + PrintAndLogEx(ERR, "Polling loop annotation length invalid: min %d; max %d", 1, 22); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + uint8_t first, second; + compute_crc(CRC_14443_B, pla.frame, pla.frame_length, &first, &second); + pla.frame[pla.frame_length++] = first; + pla.frame[pla.frame_length++] = second; + PrintAndLogEx(INFO, "Set polling loop annotation to: %s", sprint_hex(pla.frame, pla.frame_length)); + } + } + + CLIParserFree(ctx); + + // Handle empty command + if (strlen(Cmd) == 0) { + return hf14b_setconfig(NULL, verbose); + } + + // Initialize config with all parameters + hf14b_config_t config = { + .polling_loop_annotation = pla + }; + + return hf14b_setconfig(&config, verbose); +} + static int CmdHF14BList(const char *Cmd) { return CmdTraceListAlias(Cmd, "hf 14b", "14b -c"); } @@ -3161,6 +3276,7 @@ static int CmdHF14BSetUID(const char *Cmd) { static command_t CommandTable[] = { {"---------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("General") " -----------------------"}, {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"config", CmdHf14BConfig, IfPm3Iso14443b, "Configure 14b settings (use with caution)"}, {"list", CmdHF14BList, AlwaysAvailable, "List ISO-14443-B history"}, {"---------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operations") " -----------------------"}, {"apdu", CmdHF14BAPDU, IfPm3Iso14443b, "Send ISO 14443-4 APDU to tag"}, diff --git a/client/src/pla.c b/client/src/pla.c new file mode 100644 index 000000000..cda42b8eb --- /dev/null +++ b/client/src/pla.c @@ -0,0 +1,396 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Polling Loop Annotations (PLA) and Enhanced Contactless Polling (ECP) utilities +//----------------------------------------------------------------------------- + +#include "pla.h" +#include +#include +#include "ui.h" +#include "util.h" +#include "fileutils.h" + +// Load ecplist.json file +json_t *pla_load_ecplist(void) { + json_error_t error; + char *path; + + int res = searchFile(&path, RESOURCES_SUBDIR, "ecplist", ".json", false); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Cannot find ecplist.json"); + return NULL; + } + + json_t *root = json_load_file(path, 0, &error); + free(path); + + if (!root) { + PrintAndLogEx(ERR, "json error on line %d: %s", error.line, error.text); + return NULL; + } + + if (!json_is_array(root)) { + PrintAndLogEx(ERR, "Invalid ecplist.json format. Root must be an array."); + json_decref(root); + return NULL; + } + + return root; +} + +// Search ecplist for an entry matching the given type, subtype and/or key +json_t *pla_search_ecplist_by_key(json_t *root, const char *type, const char *subtype, const char *key) { + size_t index; + json_t *entry; + + json_array_foreach(root, index, entry) { + // If type filter is specified, check if entry has matching type + if (type != NULL) { + json_t *type_obj = json_object_get(entry, "type"); + if (!type_obj) { + continue; // Skip entries without type field + } + + bool type_matched = false; + if (json_is_string(type_obj)) { + const char *type_str = json_string_value(type_obj); + if (type_str && strcmp(type_str, type) == 0) { + type_matched = true; + } + } else if (json_is_array(type_obj)) { + size_t type_index; + json_t *type_value; + json_array_foreach(type_obj, type_index, type_value) { + const char *type_str = json_string_value(type_value); + if (type_str && strcmp(type_str, type) == 0) { + type_matched = true; + break; + } + } + } + + if (!type_matched) { + continue; // Type doesn't match + } + } else { + // If no type filter, skip entries that have a type field + if (json_object_get(entry, "type")) { + continue; + } + } + + bool key_matched = (key == NULL); // If no key specified, consider it matched + bool subtype_matched = (subtype == NULL); // If no subtype specified, consider it matched + + // Check if the subtype matches the "subtype" field (string or array) + if (subtype != NULL) { + json_t *subtype_obj = json_object_get(entry, "subtype"); + if (subtype_obj) { + if (json_is_string(subtype_obj)) { + const char *subtype_str = json_string_value(subtype_obj); + if (subtype_str && strcmp(subtype_str, subtype) == 0) { + subtype_matched = true; + } + } else if (json_is_array(subtype_obj)) { + size_t subtype_index; + json_t *subtype_value; + json_array_foreach(subtype_obj, subtype_index, subtype_value) { + const char *subtype_str = json_string_value(subtype_value); + if (subtype_str && strcmp(subtype_str, subtype) == 0) { + subtype_matched = true; + break; + } + } + } + } + } + + // Check if the key matches the "key" field (string or array) + if (key != NULL) { + json_t *key_obj = json_object_get(entry, "key"); + if (key_obj) { + if (json_is_string(key_obj)) { + const char *key_str = json_string_value(key_obj); + if (key_str && strcmp(key_str, key) == 0) { + key_matched = true; + } + } else if (json_is_array(key_obj)) { + size_t key_index; + json_t *key_value; + json_array_foreach(key_obj, key_index, key_value) { + const char *key_str = json_string_value(key_value); + if (key_str && strcmp(key_str, key) == 0) { + key_matched = true; + break; + } + } + } + } + } + + // Entry must match both key and subtype criteria (if specified) + if (key_matched && subtype_matched) { + return entry; + } + } + + return NULL; // Not found +} + +// Parse ECP (Enhanced Contactless Polling) subcommands +// Returns the length of the generated frame (without CRC), or -1 on error +int pla_parse_ecp_subcommand(const char *cmd, uint8_t *frame, size_t frame_size) { + if (cmd == NULL || frame == NULL || frame_size < 22) { + return -1; + } + + // Make a mutable copy of the command and replace dots/colons with spaces + char *cmd_copy = strdup(cmd); + if (!cmd_copy) { + return -1; + } + + for (char *p_char = cmd_copy; *p_char != '\0'; p_char++) { + if (*p_char == '.' || *p_char == ':') { + *p_char = ' '; + } + } + + // Load ecplist.json + json_t *ecplist = pla_load_ecplist(); + if (!ecplist) { + PrintAndLogEx(ERR, "Failed to load ecplist.json"); + free(cmd_copy); + return -1; + } + + // Skip "ecp" prefix and any whitespace + const char *p = cmd_copy; + if (strncmp(p, "ecp", 3) == 0) { + p += 3; + } + while (*p == ' ' || *p == '\t') { + p++; + } + + int result = -1; + const char *type = NULL; + const char *search_term = p; + + // Check if first term is a type ("transit" or "access") + if (strncmp(p, "transit", 7) == 0) { + type = "transit"; + p += 7; + while (*p == ' ' || *p == '\t') { + p++; + } + search_term = p; + + // If second term provided, search by key in transit entries + if (*p != '\0') { + json_t *entry = pla_search_ecplist_by_key(ecplist, type, NULL, search_term); + if (entry) { + // Found matching entry, use its value + json_t *value_obj = json_object_get(entry, "value"); + if (value_obj) { + const char *hex_str = json_string_value(value_obj); + if (hex_str) { + size_t hex_len = strlen(hex_str); + if (hex_len % 2 == 0 && hex_len <= frame_size * 2) { + for (size_t i = 0; i < hex_len / 2; i++) { + sscanf(hex_str + i * 2, "%2hhx", &frame[i]); + } + result = hex_len / 2; + } + } + } + } + + // If not found, try interpreting as hex TCI + if (result == -1) { + char *endptr; + uint32_t tci = strtoul(search_term, &endptr, 16); + if (search_term != endptr) { + // Build frame: 6a02c801000300{tci as 3 bytes}0000000000 + frame[0] = 0x6a; + frame[1] = 0x02; + frame[2] = 0xc8; + frame[3] = 0x01; + frame[4] = 0x00; + frame[5] = (tci >> 16) & 0xff; + frame[6] = (tci >> 8) & 0xff; + frame[7] = tci & 0xff; + frame[8] = 0x00; + frame[9] = 0x00; + frame[10] = 0x00; + frame[11] = 0x00; + frame[12] = 0x00; + result = 13; + } else { + PrintAndLogEx(ERR, "Unknown transit key or invalid TCI: %s", search_term); + } + } + } else { + PrintAndLogEx(ERR, "Transit type requires a key or TCI value"); + } + + } else if (strncmp(p, "access", 6) == 0) { + type = "access"; + p += 6; + while (*p == ' ' || *p == '\t') { + p++; + } + + // Parse second term + const char *second_term = p; + + // Skip to end of second term + while (*p != '\0' && *p != ' ' && *p != '\t') { + p++; + } + + // Extract second term + size_t second_term_len = p - second_term; + char *second = NULL; + if (second_term_len > 0) { + second = str_ndup(second_term, second_term_len); + } + + // Skip whitespace + while (*p == ' ' || *p == '\t') { + p++; + } + + // Parse third term if present + const char *third_term = p; + char *third = NULL; + if (*p != '\0') { + // Skip to end of third term + while (*p != '\0' && *p != ' ' && *p != '\t') { + p++; + } + size_t third_term_len = p - third_term; + if (third_term_len > 0) { + third = str_ndup(third_term, third_term_len); + } + } + + // Default TCI is 02ffff if not provided + uint32_t tci = 0x02ffff; + + // If terms provided, try to parse them + if (second != NULL && *second != '\0') { + json_t *entry = NULL; + + if (third != NULL && *third != '\0') { + // Two terms: second is subtype, third is key + entry = pla_search_ecplist_by_key(ecplist, type, second, third); + } else { + // One term: try as subtype first, then as key + entry = pla_search_ecplist_by_key(ecplist, type, second, NULL); + + if (!entry) { + entry = pla_search_ecplist_by_key(ecplist, type, NULL, second); + } + } + + if (entry) { + // Found matching entry, use its value + json_t *value_obj = json_object_get(entry, "value"); + if (value_obj) { + const char *hex_str = json_string_value(value_obj); + if (hex_str) { + size_t hex_len = strlen(hex_str); + if (hex_len % 2 == 0 && hex_len <= frame_size * 2) { + for (size_t i = 0; i < hex_len / 2; i++) { + sscanf(hex_str + i * 2, "%2hhx", &frame[i]); + } + result = hex_len / 2; + } + } + } + } + + // If not found and no third term, try interpreting second term as hex TCI + if (result == -1 && third == NULL) { + char *endptr; + tci = strtoul(second, &endptr, 16); + if (second == endptr) { + PrintAndLogEx(ERR, "Unknown access subtype/key or invalid TCI: %s", second); + free(second); + if (third) free(third); + json_decref(ecplist); + free(cmd_copy); + return -1; + } + } else if (result == -1 && third != NULL) { + PrintAndLogEx(ERR, "No matching access entry for subtype '%s' and key '%s'", second, third); + free(second); + free(third); + json_decref(ecplist); + free(cmd_copy); + return -1; + } + } + + if (second) { + free(second); + } + if (third) { + free(third); + } + + // Build frame with TCI if we didn't find a matching entry + if (result == -1) { + // Build frame: 6a02c30200{tci as 3 bytes} + frame[0] = 0x6a; + frame[1] = 0x02; + frame[2] = 0xc3; + frame[3] = 0x02; + frame[4] = 0x00; + frame[5] = (tci >> 16) & 0xff; + frame[6] = (tci >> 8) & 0xff; + frame[7] = tci & 0xff; + result = 8; + } + + } else { + // No type specified, search for entries without type field by key + json_t *entry = pla_search_ecplist_by_key(ecplist, search_term, NULL, NULL); + if (entry) { + json_t *value_obj = json_object_get(entry, "value"); + if (value_obj) { + const char *hex_str = json_string_value(value_obj); + if (hex_str) { + size_t hex_len = strlen(hex_str); + if (hex_len % 2 == 0 && hex_len <= frame_size * 2) { + for (size_t i = 0; i < hex_len / 2; i++) { + sscanf(hex_str + i * 2, "%2hhx", &frame[i]); + } + result = hex_len / 2; + } + } + } + } else { + PrintAndLogEx(ERR, "Unknown ECP type: %s", search_term); + PrintAndLogEx(HINT, "Available types: access, transit, vasorpay, vasandpay, vasonly, payonly, gymkit, identity, aidrop"); + } + } + + json_decref(ecplist); + free(cmd_copy); + return result; +} diff --git a/client/src/pla.h b/client/src/pla.h new file mode 100644 index 000000000..5d28f60e1 --- /dev/null +++ b/client/src/pla.h @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Polling Loop Annotations (PLA) and Enhanced Contactless Polling (ECP) utilities +//----------------------------------------------------------------------------- + +#ifndef PLA_H__ +#define PLA_H__ + +#include "common.h" +#include + +// Load ecplist.json file +json_t *pla_load_ecplist(void); + +// Search ecplist for an entry matching the given type, subtype and/or key +// If type is not NULL, only search entries with matching "type" field (supports string or array) +// If subtype is not NULL, searches in "subtype" field (supports string or array) +// If key is not NULL, searches in "key" field (supports string or array) +json_t *pla_search_ecplist_by_key(json_t *root, const char *type, const char *subtype, const char *key); + +// Parse ECP (Enhanced Contactless Polling) subcommands +// Returns the length of the generated frame (without CRC), or -1 on error +int pla_parse_ecp_subcommand(const char *cmd, uint8_t *frame, size_t frame_size); + +#endif diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index c1889e037..e59c4aa54 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -163,6 +163,21 @@ typedef struct { iso14a_polling_frame_t polling_loop_annotation; // Polling loop annotation } PACKED hf14a_config_t; +// Defines a frame that will be used in ISO14443B polling sequence +// Polling loop annotations are up to 20 bytes long, 24 bytes should cover future and other cases +typedef struct { + uint8_t frame[24]; + // negative values can be used to carry special info + int8_t frame_length; + uint8_t last_byte_bits; + uint16_t extra_delay; +} PACKED iso14b_polling_frame_t; + +// A struct used to send hf14b-configs over USB +typedef struct { + iso14b_polling_frame_t polling_loop_annotation; // Polling loop annotation +} PACKED hf14b_config_t; + // Tracelog Header struct typedef struct { uint32_t timestamp; @@ -726,6 +741,11 @@ typedef struct { #define CMD_HF_ISO14443A_SET_THRESHOLDS 0x03B8 +// For 14b config +#define CMD_HF_ISO14443B_PRINT_CONFIG 0x03D0 +#define CMD_HF_ISO14443B_GET_CONFIG 0x03D1 +#define CMD_HF_ISO14443B_SET_CONFIG 0x03D2 + // For measurements of the antenna tuning #define CMD_MEASURE_ANTENNA_TUNING 0x0400 #define CMD_MEASURE_ANTENNA_TUNING_HF 0x0401