From e274482f71177337d223dd04c7cc08dfa715d397 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 18 Apr 2026 18:37:25 +0300 Subject: [PATCH 1/2] Add service indicators to systems in 'felica_system_code_list.json' --- client/resources/felica_system_code_list.json | 281 +++++++++++++++++- 1 file changed, 279 insertions(+), 2 deletions(-) diff --git a/client/resources/felica_system_code_list.json b/client/resources/felica_system_code_list.json index c13ac7a53..c91ddc561 100644 --- a/client/resources/felica_system_code_list.json +++ b/client/resources/felica_system_code_list.json @@ -4,7 +4,119 @@ "name": "CJRC Standard", "region": "Japan", "type": "transit", - "description": "Used by Japanese Mutual Use transit card systems" + "description": "Used by Japanese Mutual Use transit card systems", + "services": [ + { + "name": "Hayakaken", + "type": "transit", + "description": "Fukuoka City Transportation Bureau Hayakaken mutual-use transit service", + "nodes": [ + { + "code": "40247F25" + } + ] + }, + { + "name": "ICOCA", + "type": "transit", + "description": "JR West ICOCA mutual-use transit service", + "nodes": [ + { + "code": "00963F97" + } + ] + }, + { + "name": "ICSCA", + "type": "transit", + "description": "Sendai-area ICSCA mutual-use transit service", + "nodes": [ + { + "code": "40BD7FBE" + } + ] + }, + { + "name": "Kitaca", + "type": "transit", + "description": "JR Hokkaido Kitaca mutual-use transit service", + "nodes": [ + { + "code": "8020BF21" + } + ] + }, + { + "name": "manaca", + "type": "transit", + "description": "Nagoya-area manaca mutual-use transit service", + "nodes": [ + { + "code": "8098BF99" + } + ] + }, + { + "name": "nimoca", + "type": "transit", + "description": "nimoca mutual-use transit service", + "nodes": [ + { + "code": "0820" + } + ] + }, + { + "name": "PASMO", + "type": "transit", + "description": "PASMO mutual-use transit service", + "nodes": [ + { + "code": "C01CFF1D" + } + ] + }, + { + "name": "SUGOCA", + "type": "transit", + "description": "JR Kyushu SUGOCA mutual-use transit service", + "nodes": [ + { + "code": "C021FF22" + } + ] + }, + { + "name": "Suica", + "type": "transit", + "description": "JR East Suica mutual-use transit service", + "nodes": [ + { + "code": "0818" + } + ] + }, + { + "name": "Suica Regional Collaboration", + "type": "transit", + "description": "Suica Regional Collaboration service in the CJRC mutual-use system", + "nodes": [ + { + "code": "80257F26" + } + ] + }, + { + "name": "TOICA", + "type": "transit", + "description": "JR Central TOICA mutual-use transit service", + "nodes": [ + { + "code": "001EFF1E" + } + ] + } + ] }, { "code": "0102", @@ -410,7 +522,172 @@ "name": "FeliCa Networks Common Area", "region": "Japan", "type": "common", - "description": "Common area for FeliCa Networks services" + "description": "Common area for FeliCa Networks services", + "services": [ + { + "name": "ANA", + "type": "loyalty", + "description": "ANA mileage and airline membership service", + "nodes": [ + { + "code": "C017BF18" + } + ] + }, + { + "name": "Edy", + "type": "payment", + "description": "Rakuten Edy e-money payment service", + "nodes": [ + { + "code": "0B11" + } + ] + }, + { + "name": "FeliCa Kazasu Folder", + "type": "loyalty", + "description": "FeliCa Kazasu shared area for loyalty and membership services", + "nodes": [ + { + "code": "C074FF75" + } + ] + }, + { + "name": "FeliCa Pocket", + "type": "loyalty", + "description": "FeliCa Pocket shared area for loyalty and membership services", + "nodes": [ + { + "code": "4039FF39" + } + ] + }, + { + "name": "JAL", + "type": "loyalty", + "description": "JAL airline mileage and membership service", + "nodes": [ + { + "code": "402F7F30" + } + ] + }, + { + "name": "Karuwaza Club", + "type": "loyalty", + "description": "Karuwaza Club points and membership service", + "nodes": [ + { + "code": "8026FF26" + } + ] + }, + { + "name": "Kesaka", + "type": "access", + "description": "Kesaka access control service", + "nodes": [ + { + "code": "802E3F2F" + } + ] + }, + { + "name": "Kumamon", + "type": "loyalty", + "description": "Kumamon regional campaign and points service", + "nodes": [ + { + "code": "801AFF1A" + } + ] + }, + { + "name": "MIWA", + "type": "access", + "description": "MIWA access control credential service", + "nodes": [ + { + "code": "4042BF42" + }, + { + "code": "C03B3F3C" + } + ] + }, + { + "name": "MIWA FKL", + "type": "access", + "description": "MIWA FKL access control credential service", + "nodes": [ + { + "code": "40267F26" + } + ] + }, + { + "name": "MIWA Keymo", + "type": "access", + "description": "MIWA Keymo access control credential service", + "nodes": [ + { + "code": "0B3C" + } + ] + }, + { + "name": "nanaco", + "type": "payment", + "description": "nanaco e-money payment service", + "nodes": [ + { + "code": "8055BF56" + } + ] + }, + { + "name": "QUICPay", + "type": "payment", + "description": "QUICPay contactless payment service", + "nodes": [ + { + "code": "C035BF36" + } + ] + }, + { + "name": "Rakuten Point", + "type": "loyalty", + "description": "Rakuten Point loyalty points service", + "nodes": [ + { + "code": "807BBF7B" + } + ] + }, + { + "name": "WAON", + "type": "payment", + "description": "WAON e-money payment service", + "nodes": [ + { + "code": "C067FF68" + } + ] + }, + { + "name": "Yodobashi Gold Point", + "type": "loyalty", + "description": "Yodobashi Gold Point loyalty points service", + "nodes": [ + { + "code": "802C3F2D" + } + ] + } + ] }, { "code": "FE0F", From 141c21749aff92b05fc252984c105392a734ee32 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:03:07 +0300 Subject: [PATCH 2/2] Add per-system inner service discovery to 'hf felica info' --- client/src/cmdhffelica.c | 243 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 240 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 43337f892..2d2e14fad 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -59,7 +59,7 @@ #define FELICA_OPTIONAL_CMD_TIMEOUT_MS 250U #define FELICA_OPTIONAL_CMD_RETRIES 3U // Per FeliCa spec, Polling max response at 16 timeslots is ~25ms; keep extra margin. -#define FELICA_POLL_TIMEOUT_MS 50U +#define FELICA_POLL_TIMEOUT_MS 100U #define FELICA_SEAC_POLL_TIMEOUT_MS 200U #define FELICA_SEAC_POLL_RETRY_COUNT 5U #define FELICA_SEAC_POLL_FRAME_LEN 5U @@ -124,6 +124,8 @@ #define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 32U #define FELICA_MAX_NODE_NUMBER 0x03FFU #define FELICA_PRESENCE_SERVICE_CODE_LE ((uint16_t)FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY) +#define FELICA_SYSTEM_SERVICE_MAX_COUNT 32U +#define FELICA_SYSTEM_SERVICE_NODE_MAX_COUNT 64U typedef struct { uint8_t attribute; @@ -230,6 +232,18 @@ typedef struct { felica_discovered_system_t systems[FELICA_DISCOVERED_SYSTEM_MAX_COUNT]; } felica_discovered_system_list_t; +typedef struct { + const char *name; + const char *type; + size_t required_node_count; + size_t present_node_count; +} felica_system_service_t; + +typedef struct { + uint16_t node_code_le; + size_t service_index; +} felica_system_service_node_t; + typedef enum { FELICA_IDM_RESOLVE_STANDALONE = 0, FELICA_IDM_RESOLVE_CHAINED, @@ -270,6 +284,9 @@ static bool felica_discover_nodes_with_read_without_encryption(uint8_t *flags, void *ctx, uint32_t *discovered_count, int *stop_status); +static int send_polling(uint8_t flags, uint16_t system_code, uint8_t request_code, + uint32_t timeout_ms, uint32_t retries, bool logging, + uint8_t *idm_out, uint16_t *returned_system_code); static const felica_node_discovery_method_info_t *felica_get_node_discovery_method_info(felica_node_discovery_method_t method); static const char *felica_node_discovery_method_display_name(felica_node_discovery_method_t method); static void felica_print_node_discovery_method_used(felica_node_discovery_method_t method); @@ -702,7 +719,223 @@ static void felica_print_system_code_annotation(int level, const uint8_t *system } } -static void felica_print_discovered_system_block(int level, const felica_discovered_system_t *system) { +static bool felica_parse_service_node_code(const char *code_hex, uint16_t *node_code_le_out) { + if (code_hex == NULL || node_code_le_out == NULL) { + return false; + } + + const size_t len = strlen(code_hex); + if (len != 4U && len != 8U) { + return false; + } + + uint8_t code_bytes[4] = {0}; + size_t code_bytes_len = 0; + if (hexstr_to_byte_array(code_hex, code_bytes, &code_bytes_len) == false) { + return false; + } + + if (code_bytes_len != 2U && code_bytes_len != 4U) { + return false; + } + + /* + * JSON stores node codes in network order. + * Request Service packet builder expects a LE scalar and then writes it LE on wire, + * so keep original wire byte order by storing swapped scalar here. + */ + *node_code_le_out = (uint16_t)((uint16_t)code_bytes[1] << 8) | code_bytes[0]; + return true; +} + +static int felica_request_service_key_versions(uint8_t flags, const uint8_t *idm, + const uint16_t *node_codes_le, size_t node_count, + uint16_t *key_versions_le_out, size_t *returned_nodes_out) { + if (idm == NULL || node_codes_le == NULL || key_versions_le_out == NULL) { + return PM3_EINVARG; + } + + if (node_count == 0 || node_count > FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE) { + return PM3_EINVARG; + } + + if (returned_nodes_out) { + *returned_nodes_out = 0; + } + + uint8_t data[1 + 1 + 8 + 1 + (FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE * 2)] = {0}; + const uint16_t datalen = (uint16_t)(1 + 1 + 8 + 1 + (node_count * 2)); + data[0] = (uint8_t)datalen; + data[1] = FELICA_REQSRV_REQ; + memcpy(data + 2, idm, 8); + data[10] = (uint8_t)node_count; + for (size_t i = 0; i < node_count; i++) { + data[11 + i * 2] = node_codes_le[i] & 0xFF; + data[12 + i * 2] = (node_codes_le[i] >> 8) & 0xFF; + } + + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, false, + FELICA_REQSRV_ACK, + FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + 0, false, &resp, "request service") != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + size_t offset = sizeof(felica_frame_response_t); + if (resp.length < (offset + 1U)) { + return PM3_ESOFT; + } + + size_t returned_nodes = resp.data.asBytes[offset++]; + size_t available_nodes = (resp.length > offset) ? ((resp.length - offset) / 2U) : 0; + if (returned_nodes > available_nodes) { + returned_nodes = available_nodes; + } + if (returned_nodes > node_count) { + returned_nodes = node_count; + } + + for (size_t i = 0; i < returned_nodes; i++) { + key_versions_le_out[i] = (uint16_t)resp.data.asBytes[offset + i * 2] | + ((uint16_t)resp.data.asBytes[offset + i * 2 + 1] << 8); + } + + if (returned_nodes_out) { + *returned_nodes_out = returned_nodes; + } + return PM3_SUCCESS; +} + +static void felica_info_process_system_services(int level, uint8_t flags, + const uint8_t *idm, const json_t *system_entry) { + if (idm == NULL || system_entry == NULL) { + return; + } + + json_t *services_json = json_object_get(system_entry, "services"); + if (json_is_array(services_json) == false || json_array_size(services_json) == 0) { + return; + } + + felica_system_service_t services[FELICA_SYSTEM_SERVICE_MAX_COUNT]; + memset(services, 0, sizeof(services)); + + felica_system_service_node_t nodes[FELICA_SYSTEM_SERVICE_NODE_MAX_COUNT]; + memset(nodes, 0, sizeof(nodes)); + + size_t service_count = 0; + size_t node_count = 0; + + size_t service_index_json = 0; + json_t *service_json = NULL; + json_array_foreach(services_json, service_index_json, service_json) { + if (json_is_object(service_json) == false) { + continue; + } + if (service_count >= FELICA_SYSTEM_SERVICE_MAX_COUNT) { + break; + } + + json_t *nodes_json = json_object_get(service_json, "nodes"); + if (json_is_array(nodes_json) == false || json_array_size(nodes_json) == 0) { + continue; + } + + felica_system_service_t *service = &services[service_count]; + service->name = felica_get_json_string(service_json, "name"); + service->type = felica_get_json_string(service_json, "type"); + + const size_t this_service_index = service_count; + size_t node_index_json = 0; + json_t *node_json = NULL; + json_array_foreach(nodes_json, node_index_json, node_json) { + if (json_is_object(node_json) == false) { + continue; + } + if (node_count >= FELICA_SYSTEM_SERVICE_NODE_MAX_COUNT) { + break; + } + + uint16_t node_code_le = 0; + if (felica_parse_service_node_code(felica_get_json_string(node_json, "code"), &node_code_le) == false) { + continue; + } + + nodes[node_count].node_code_le = node_code_le; + nodes[node_count].service_index = this_service_index; + node_count++; + service->required_node_count++; + } + + if (service->required_node_count > 0) { + service_count++; + } + + if (node_count >= FELICA_SYSTEM_SERVICE_NODE_MAX_COUNT) { + break; + } + } + + if (service_count == 0 || node_count == 0) { + return; + } + + size_t processed_nodes = 0; + while (processed_nodes < node_count) { + const size_t batch_count = MIN(FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE, node_count - processed_nodes); + uint16_t batch_codes[FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE] = {0}; + uint16_t key_versions[FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE] = {0}; + + for (size_t i = 0; i < batch_count; i++) { + batch_codes[i] = nodes[processed_nodes + i].node_code_le; + } + + size_t returned_nodes = 0; + if (felica_request_service_key_versions(flags, idm, batch_codes, batch_count, + key_versions, &returned_nodes) != PM3_SUCCESS) { + return; + } + + for (size_t i = 0; i < returned_nodes; i++) { + if (key_versions[i] == 0xFFFFU) { + continue; + } + const size_t service_index = nodes[processed_nodes + i].service_index; + if (service_index < service_count) { + services[service_index].present_node_count++; + } + } + + processed_nodes += batch_count; + } + + size_t present_services_count = 0; + for (size_t i = 0; i < service_count; i++) { + if (services[i].present_node_count > 0) { + present_services_count++; + } + } + + if (present_services_count == 0) { + return; + } + + PrintAndLogEx(level, " Services...... " _GREEN_("%u:"), (unsigned int)present_services_count); + for (size_t i = 0; i < service_count; i++) { + if (services[i].present_node_count == 0) { + continue; + } + const char *name = (services[i].name != NULL) ? services[i].name : "Unknown service"; + if (services[i].type) { + PrintAndLogEx(level, " - %s (" _YELLOW_("%s") ")", name, services[i].type); + } else { + PrintAndLogEx(level, " - %s", name); + } + } +} + +static void felica_info_process_system(int level, uint8_t flags, const felica_discovered_system_t *system) { if (system == NULL) { return; } @@ -737,6 +970,10 @@ static void felica_print_discovered_system_block(int level, const felica_discove } else { PrintAndLogEx(level, " IDM............ " _RED_("N/A")); } + + if (system->has_idm) { + felica_info_process_system_services(level, flags, system->idm, entry); + } } static int send_request_system_code(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, @@ -1591,7 +1828,7 @@ static int info_felica(bool verbose) { discovered_systems.count > 0) { PrintAndLogEx(INFO, "Systems........ " _GREEN_("%u:"), (unsigned int)discovered_systems.count); for (size_t i = 0; i < discovered_systems.count; i++) { - felica_print_discovered_system_block(INFO, &discovered_systems.systems[i]); + felica_info_process_system(INFO, optional_flags, &discovered_systems.systems[i]); } }