Add quick ECP configuration aliases for hf 14a config --pla

This commit is contained in:
kormax
2025-11-01 12:25:50 +02:00
parent 6d2a9c2be9
commit bc5fabf0e4
+432 -24
View File
@@ -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 <hex> -> 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 <hex> -> 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", "<std|force|skip>", "Configure ATQA<>anticollision behavior"),
@@ -397,7 +785,7 @@ static int CmdHf14AConfig(const char *Cmd) {
arg_str0(NULL, "cl3", "<std|force|skip>", "Configure SAK<>CL3 behavior"),
arg_str0(NULL, "rats", "<std|force|skip>", "Configure RATS behavior"),
arg_str0(NULL, "mag", "<on|off>", "Configure Apple MagSafe polling"),
arg_str0(NULL, "pla", "<hex|off>", "Configure polling loop annotation"),
arg_str0(NULL, "pla", "<hex|off|ecp>", "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));
}
}