mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-05-12 11:18:11 -07:00
Merge pull request #3049 from kormax/nfc-b-polling-loop-annotations
Add polling loop annotation support for iso14443b
This commit is contained in:
@@ -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;
|
||||
|
||||
+46
-1
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -629,6 +629,7 @@ SRCS = mifare/aiddesfire.c \
|
||||
cmdflashmemspiffs.c \
|
||||
cmdhf.c \
|
||||
cmdhf14a.c \
|
||||
pla.c \
|
||||
cmdhf14b.c \
|
||||
cmdhf15.c \
|
||||
cmdhfcryptorf.c \
|
||||
|
||||
+2
-376
@@ -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;
|
||||
|
||||
@@ -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 <hex> -> 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", "<hex|off>", "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"},
|
||||
|
||||
@@ -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 <string.h>
|
||||
#include <stdlib.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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 <jansson.h>
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user