diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 1ed853e05..bae5cbb50 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -45,6 +45,7 @@ #include "preferences.h" // get/set device debug level #include "pm3_cmd.h" #include "mbedtls/cmac.h" +#include "jansson.h" // JSON parsing static bool g_apdu_in_framing_enable = true; bool Get_apdu_in_framing(void) { @@ -328,6 +329,381 @@ 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 = strndup(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 = strndup(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")":"); @@ -352,12 +728,20 @@ static int hf_14a_config_example(void) { PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --std")); PrintAndLogEx(NORMAL, "\nExamples of polling loop annotations used to enable anticollision on mobile targets:"); - PrintAndLogEx(NORMAL, _CYAN_(" ECP Express Transit EMV")":"); + PrintAndLogEx(NORMAL, _CYAN_(" ECP Express Transit TFL/EMV")":"); + PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla ecp.transit.emv")); PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla 6a02c801000300027900000000")); + PrintAndLogEx(NORMAL, _CYAN_(" ECP Express Transit Navigo")":"); + PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla ecp.transit.navigo")); + PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla 6a02c8010003095a0000000000")); PrintAndLogEx(NORMAL, _CYAN_(" ECP VAS Only")":"); + PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla ecp.vasonly")); PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla 6a01000002")); PrintAndLogEx(NORMAL, _CYAN_(" ECP Access Wildcard")":"); + PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla ecp.access:02ffff")); PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla 6a02c3020002ffff")); + PrintAndLogEx(NORMAL, _CYAN_(" ECP GymKit")":"); + PrintAndLogEx(NORMAL, _YELLOW_(" hf 14a config --pla ecp.gymkit")); return PM3_SUCCESS; } @@ -368,27 +752,31 @@ static int CmdHf14AConfig(const char *Cmd) { CLIParserInit(&ctx, "hf 14a config", "Configure 14a settings (use with caution)\n" " `-v` also prints examples for reviving Gen2 cards & configuring polling loop annotations", - "hf 14a config -> Print current configuration\n" - "hf 14a config --std -> Reset default configuration (follow standard)\n" - "hf 14a config --atqa std -> Follow standard\n" - "hf 14a config --atqa force -> Force execution of anticollision\n" - "hf 14a config --atqa skip -> Skip anticollision\n" - "hf 14a config --bcc std -> Follow standard\n" - "hf 14a config --bcc fix -> Fix bad BCC in anticollision\n" - "hf 14a config --bcc ignore -> Ignore bad BCC and use it as such\n" - "hf 14a config --cl2 std -> Follow standard\n" - "hf 14a config --cl2 force -> Execute CL2\n" - "hf 14a config --cl2 skip -> Skip CL2\n" - "hf 14a config --cl3 std -> Follow standard\n" - "hf 14a config --cl3 force -> Execute CL3\n" - "hf 14a config --cl3 skip -> Skip CL3\n" - "hf 14a config --rats std -> Follow standard\n" - "hf 14a config --rats force -> Execute RATS\n" - "hf 14a config --rats skip -> Skip RATS\n" - "hf 14a config --mag on -> Enable Apple magsafe polling\n" - "hf 14a config --mag off -> Disable Apple magsafe polling\n" - "hf 14a config --pla -> Set polling loop annotation (max 22 bytes)\n" - "hf 14a config --pla off -> Disable polling loop annotation\n"); + "hf 14a config -> Print current configuration\n" + "hf 14a config --std -> Reset default configuration (follow standard)\n" + "hf 14a config --atqa std -> Follow standard\n" + "hf 14a config --atqa force -> Force execution of anticollision\n" + "hf 14a config --atqa skip -> Skip anticollision\n" + "hf 14a config --bcc std -> Follow standard\n" + "hf 14a config --bcc fix -> Fix bad BCC in anticollision\n" + "hf 14a config --bcc ignore -> Ignore bad BCC and use it as such\n" + "hf 14a config --cl2 std -> Follow standard\n" + "hf 14a config --cl2 force -> Execute CL2\n" + "hf 14a config --cl2 skip -> Skip CL2\n" + "hf 14a config --cl3 std -> Follow standard\n" + "hf 14a config --cl3 force -> Execute CL3\n" + "hf 14a config --cl3 skip -> Skip CL3\n" + "hf 14a config --rats std -> Follow standard\n" + "hf 14a config --rats force -> Execute RATS\n" + "hf 14a config --rats skip -> Skip RATS\n" + "hf 14a config --mag on -> Enable Apple magsafe polling\n" + "hf 14a config --mag off -> Disable Apple magsafe polling\n" + "hf 14a config --pla -> Set polling loop annotation (max 22 bytes)\n" + "hf 14a config --pla off -> Disable polling loop annotation\n" + "hf 14a config --pla ecp.access.[tci] -> ECP Access (default TCI: ffff)\n" + "hf 14a config --pla ecp.transit.[tci/name] -> ECP Transit (emv/tfl/navigo or hex TCI)\n" + "hf 14a config --pla ecp.(vasorpay/vasandpay/vasonly/payonly) -> ECP VAS\n" + "hf 14a config --pla ecp.gymkit -> ECP GymKit\n"); void *argtable[] = { arg_param_begin, arg_str0(NULL, "atqa", "", "Configure ATQA<>anticollision behavior"), @@ -397,7 +785,7 @@ static int CmdHf14AConfig(const char *Cmd) { arg_str0(NULL, "cl3", "", "Configure SAK<>CL3 behavior"), arg_str0(NULL, "rats", "", "Configure RATS behavior"), arg_str0(NULL, "mag", "", "Configure Apple MagSafe polling"), - arg_str0(NULL, "pla", "", "Configure polling loop annotation"), + arg_str0(NULL, "pla", "", "Configure polling loop annotation"), arg_lit0(NULL, "std", "Reset default configuration: follow all standard"), arg_lit0("v", "verbose", "verbose output"), arg_param_end @@ -490,12 +878,32 @@ static int CmdHf14AConfig(const char *Cmd) { .last_byte_bits = 8, .extra_delay = 5 }; + + // Get main --pla value CLIParamStrToBuf(arg_get_str(ctx, 7), (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 = 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_A, 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; @@ -517,7 +925,7 @@ static int CmdHf14AConfig(const char *Cmd) { compute_crc(CRC_14443_A, pla.frame, pla.frame_length, &first, &second); pla.frame[pla.frame_length++] = first; pla.frame[pla.frame_length++] = second; - PrintAndLogEx(INFO, "Added CRC16A to polling loop annotation: %s", sprint_hex(pla.frame, pla.frame_length)); + PrintAndLogEx(INFO, "Set polling loop annotation to: %s", sprint_hex(pla.frame, pla.frame_length)); } }