From 63be5d21c12c0ee0c373774e358d1f03c0e0b73f Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:49:53 +0300 Subject: [PATCH 1/2] Add block-based service qualifiers in 'felica_system_code_list.json' --- client/resources/felica_system_code_list.json | 112 +++++++++++++++++- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/client/resources/felica_system_code_list.json b/client/resources/felica_system_code_list.json index c91ddc561..389e73f71 100644 --- a/client/resources/felica_system_code_list.json +++ b/client/resources/felica_system_code_list.json @@ -263,7 +263,29 @@ "name": "Kagoshima", "region": "Japan", "type": "transit", - "description": "Used by Kagoshima cards (RapiCa/Iwasaki IC)" + "description": "Used by transit cards in Kagoshima", + "services": [ + { + "name": "Iwasaki IC", + "type": "transit", + "description": "Transit IC card sold by the Iwasaki Group for Kagoshima Kotsu and Tanegashima-Yakushima Kotsu services", + "nodes": [ + { + "code": "81027F0C" + } + ] + }, + { + "name": "RapiCa", + "type": "transit", + "description": "Kagoshima common transit IC card issued by Kagoshima City Transportation Bureau, Nangoku Kotsu, and JR Kyushu Bus", + "nodes": [ + { + "code": "010C0021" + } + ] + } + ] }, { "code": "81A1", @@ -368,7 +390,61 @@ "name": "FeliCa Lite", "region": "Global", "type": "common", - "description": "Default system code used by FeliCa Lite/Lite-S tags" + "description": "Default system code used by FeliCa Lite/Lite-S tags", + "services": [ + { + "name": "Aime", + "type": "e-amusement", + "description": "AIME E-amusement", + "nodes": [ + { + "code": "0b00", + "data": { + "0082": "^................0078............$" + } + } + ] + }, + { + "name": "Bandai Namco Pasport", + "type": "e-amusement", + "description": "Bandai Namco Pasport E-amusement", + "nodes": [ + { + "code": "0b00", + "data": { + "0082": "^................002A............$" + } + } + ] + }, + { + "name": "Konami", + "type": "e-amusement", + "description": "Konami E-amusement", + "nodes": [ + { + "code": "0b00", + "data": { + "0082": "^................0068............$" + } + } + ] + }, + { + "name": "Enekey", + "type": "loyalty", + "description": "ENEOS Enekey loyalty program", + "nodes": [ + { + "code": "0b00", + "data": { + "0082": "^................0077............$" + } + } + ] + } + ] }, { "code": "8B43", @@ -421,10 +497,38 @@ }, { "code": "8DB6", - "name": "ASACA / DoCard", + "name": "Asahikawa", "region": "Japan", "type": "transit", - "description": "Used by Asaca/DoCard transit card services (Asahikawa and Dohoku Bus)" + "description": "Used by Asahikawa transit cards", + "services": [ + { + "name": "ASACA", + "type": "transit", + "description": "Transit card service issued by Asahikawa Denkikidou (Asahikawa Electric Railway)", + "nodes": [ + { + "code": "4B00", + "data": { + "00": "^0102............................$" + } + } + ] + }, + { + "name": "DoCARD", + "type": "transit", + "description": "Transit card service issued by Dohoku Bus", + "nodes": [ + { + "code": "4B00", + "data": { + "00": "^0103............................$" + } + } + ] + } + ] }, { "code": "8E35", From 072b82bb8ec9fbda34865bd52c9a3a39fb603899 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:50:26 +0300 Subject: [PATCH 2/2] Implement block-data-based service detection in 'hf felica info' --- client/src/cmdhffelica.c | 307 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 284 insertions(+), 23 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 2d2e14fad..654240551 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -126,6 +126,8 @@ #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 +#define FELICA_SYSTEM_SERVICE_NODE_MATCHER_MAX_COUNT 8U +#define FELICA_SYSTEM_SERVICE_BLOCK_LIST_ELEMENT_MAX_LEN 3U typedef struct { uint8_t attribute; @@ -239,9 +241,17 @@ typedef struct { size_t present_node_count; } felica_system_service_t; +typedef struct { + uint8_t block_list_element[FELICA_SYSTEM_SERVICE_BLOCK_LIST_ELEMENT_MAX_LEN]; + uint8_t block_list_element_len; + const char *data_regex; +} felica_system_service_node_matcher_t; + typedef struct { uint16_t node_code_le; size_t service_index; + size_t matcher_count; + felica_system_service_node_matcher_t matchers[FELICA_SYSTEM_SERVICE_NODE_MATCHER_MAX_COUNT]; } felica_system_service_node_t; typedef enum { @@ -256,6 +266,9 @@ static void clear_and_send_command(uint8_t flags, uint16_t datalen, uint8_t *dat static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging, PacketResponseNG *resp, const char *request_name); +static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + felica_read_without_encryption_response_t *rd_noCry_resp, + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging); static bool felica_discover_nodes_with_request_code_list(uint8_t *flags, const uint8_t *idm, uint32_t retry_count, @@ -748,6 +761,192 @@ static bool felica_parse_service_node_code(const char *code_hex, uint16_t *node_ return true; } +static bool felica_parse_block_list_element(const char *ble_hex, uint8_t *ble_out, uint8_t *ble_len_out) { + if (ble_hex == NULL || ble_out == NULL || ble_len_out == NULL) { + return false; + } + + const bool explicit_raw_ble = str_startswith(ble_hex, "ble:") || str_startswith(ble_hex, "BLE:"); + const char *value_hex = explicit_raw_ble ? (ble_hex + 4) : ble_hex; + const size_t len = strlen(value_hex); + + if (explicit_raw_ble) { + if (len != 4U && len != 6U) { + return false; + } + + uint8_t ble_bytes[FELICA_SYSTEM_SERVICE_BLOCK_LIST_ELEMENT_MAX_LEN] = {0}; + size_t ble_bytes_len = 0; + if (hexstr_to_byte_array(value_hex, ble_bytes, &ble_bytes_len) == false) { + return false; + } + + if (ble_bytes_len != 2U && ble_bytes_len != 3U) { + return false; + } + + if (ble_bytes_len == 2U) { + // For 2-byte BLE form, bit7 must be set. + ble_bytes[0] |= 0x80U; + } + + memcpy(ble_out, ble_bytes, ble_bytes_len); + *ble_len_out = (uint8_t)ble_bytes_len; + return true; + } + + // Default matcher key mode: block index in hex (for example "82" or "0082"). + if (len != 2U && len != 4U) { + return false; + } + + uint8_t index_bytes[2] = {0}; + size_t index_bytes_len = 0; + if (hexstr_to_byte_array(value_hex, index_bytes, &index_bytes_len) == false) { + return false; + } + + if (index_bytes_len != 1U && index_bytes_len != 2U) { + return false; + } + + uint16_t block_index = 0; + for (size_t i = 0; i < index_bytes_len; i++) { + block_index = (uint16_t)((block_index << 8) | index_bytes[i]); + } + + if (block_index > 0xFFU) { + return false; + } + + // Short (2-byte) block-list element: access mode/order defaults + 8-bit block index. + ble_out[0] = 0x80; + ble_out[1] = (uint8_t)block_index; + *ble_len_out = 2U; + return true; +} + +static size_t felica_parse_service_node_matchers(const json_t *node_json, + felica_system_service_node_matcher_t *matchers, + size_t matcher_capacity) { + if (node_json == NULL || matchers == NULL || matcher_capacity == 0) { + return 0; + } + + json_t *data_json = json_object_get(node_json, "data"); + if (json_is_object(data_json) == false) { + return 0; + } + + size_t matcher_count = 0; + const char *ble_hex = NULL; + json_t *regex_json = NULL; + json_object_foreach(data_json, ble_hex, regex_json) { + if (matcher_count >= matcher_capacity) { + break; + } + + if (json_is_string(regex_json) == false) { + continue; + } + + const char *data_regex = json_string_value(regex_json); + if (data_regex == NULL || data_regex[0] == '\0') { + continue; + } + + felica_system_service_node_matcher_t *matcher = &matchers[matcher_count]; + memset(matcher, 0, sizeof(*matcher)); + if (felica_parse_block_list_element(ble_hex, matcher->block_list_element, &matcher->block_list_element_len) == false) { + continue; + } + + matcher->data_regex = data_regex; + matcher_count++; + } + + return matcher_count; +} + +static int felica_read_service_block_for_node(uint8_t flags, const uint8_t *idm, + uint16_t node_code_le, + const uint8_t *block_list_element, uint8_t block_list_element_len, + uint8_t *block_data_out, size_t block_data_out_capacity, size_t *block_data_len_out) { + if (idm == NULL || block_list_element == NULL || block_data_out == NULL || block_data_len_out == NULL) { + return PM3_EINVARG; + } + + *block_data_len_out = 0; + + if (block_list_element_len != 2U && block_list_element_len != 3U) { + return PM3_EINVARG; + } + + const uint16_t datalen = (uint16_t)(1 + 1 + 8 + 1 + 2 + 1 + block_list_element_len); + uint8_t data[1 + 1 + 8 + 1 + 2 + 1 + FELICA_SYSTEM_SERVICE_BLOCK_LIST_ELEMENT_MAX_LEN] = {0}; + data[0] = (uint8_t)datalen; + data[1] = FELICA_RDBLK_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x01; + data[11] = node_code_le & 0xFF; + data[12] = (node_code_le >> 8) & 0xFF; + data[13] = 0x01; + memcpy(data + 14, block_list_element, block_list_element_len); + + felica_read_without_encryption_response_t rd_resp; + if (send_read_without_encryption_ex(flags, datalen, data, false, &rd_resp, + FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + 0, false) != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + if (rd_resp.status_flags.status_flag1[0] != 0x00 || rd_resp.status_flags.status_flag2[0] != 0x00) { + return PM3_ESOFT; + } + + if (rd_resp.number_of_block[0] == 0) { + return PM3_ESOFT; + } + + size_t copy_len = MIN((size_t)FELICA_BLK_SIZE, block_data_out_capacity); + memcpy(block_data_out, rd_resp.block_data, copy_len); + *block_data_len_out = copy_len; + return PM3_SUCCESS; +} + +static bool felica_match_service_node_data(uint8_t flags, const uint8_t *idm, uint16_t node_code_le, + const felica_system_service_node_matcher_t *matchers, size_t matcher_count) { + if (idm == NULL || matchers == NULL || matcher_count == 0) { + return true; + } + + for (size_t i = 0; i < matcher_count; i++) { + if (matchers[i].data_regex == NULL || matchers[i].data_regex[0] == '\0') { + return false; + } + + uint8_t block_data[FELICA_BLK_SIZE] = {0}; + size_t block_data_len = 0; + if (felica_read_service_block_for_node(flags, idm, node_code_le, + matchers[i].block_list_element, matchers[i].block_list_element_len, + block_data, sizeof(block_data), &block_data_len) != PM3_SUCCESS) { + return false; + } + + char block_hex[(FELICA_BLK_SIZE * 2) + 1] = {0}; + hex_to_buffer((uint8_t *)block_hex, block_data, block_data_len, sizeof(block_hex) - 1, 0, 0, true); + if (str_regex_match_case_insensitive(matchers[i].data_regex, block_hex) == false) { + return false; + } + } + + return true; +} + +static bool felica_is_lite_assumed_node(uint16_t node_code_le) { + return (node_code_le == 0x000BU || node_code_le == 0x0009U); +} + 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) { @@ -808,7 +1007,7 @@ static int felica_request_service_key_versions(uint8_t flags, const uint8_t *idm } static void felica_info_process_system_services(int level, uint8_t flags, - const uint8_t *idm, const json_t *system_entry) { + uint16_t system_code, const uint8_t *idm, const json_t *system_entry) { if (idm == NULL || system_entry == NULL) { return; } @@ -862,8 +1061,18 @@ static void felica_info_process_system_services(int level, uint8_t flags, continue; } + json_t *data_json = json_object_get(node_json, "data"); + const bool has_data_matchers = json_is_object(data_json) && (json_object_size(data_json) > 0); + const size_t matcher_count = felica_parse_service_node_matchers(node_json, + nodes[node_count].matchers, + ARRAYLEN(nodes[node_count].matchers)); + if (has_data_matchers && matcher_count == 0) { + continue; + } + nodes[node_count].node_code_le = node_code_le; nodes[node_count].service_index = this_service_index; + nodes[node_count].matcher_count = matcher_count; node_count++; service->required_node_count++; } @@ -881,33 +1090,61 @@ static void felica_info_process_system_services(int level, uint8_t flags, 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) { + if (system_code == FELICA_SYSTEM_CODE_FELICA_LITE) { + for (size_t i = 0; i < node_count; i++) { + const felica_system_service_node_t *node = &nodes[i]; + if (felica_is_lite_assumed_node(node->node_code_le) == false) { continue; } - const size_t service_index = nodes[processed_nodes + i].service_index; + + if (node->matcher_count > 0 && + felica_match_service_node_data(flags, idm, node->node_code_le, + node->matchers, node->matcher_count) == false) { + continue; + } + + const size_t service_index = node->service_index; if (service_index < service_count) { services[service_index].present_node_count++; } } + } else { + 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}; - processed_nodes += batch_count; + 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 felica_system_service_node_t *node = &nodes[processed_nodes + i]; + if (node->matcher_count > 0 && + felica_match_service_node_data(flags, idm, node->node_code_le, + node->matchers, node->matcher_count) == false) { + continue; + } + + const size_t service_index = node->service_index; + if (service_index < service_count) { + services[service_index].present_node_count++; + } + } + + processed_nodes += batch_count; + } } size_t present_services_count = 0; @@ -972,7 +1209,7 @@ static void felica_info_process_system(int level, uint8_t flags, const felica_di } if (system->has_idm) { - felica_info_process_system_services(level, flags, system->idm, entry); + felica_info_process_system_services(level, flags, system->system_code, system->idm, entry); } } @@ -2052,6 +2289,10 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging) { + if (rd_noCry_resp == NULL) { + return PM3_EINVARG; + } + PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x07, timeout_ms, retries, @@ -2063,7 +2304,27 @@ static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint return PM3_ERFTRANS; } - memcpy(rd_noCry_resp, (felica_read_without_encryption_response_t *)resp.data.asBytes, sizeof(felica_read_without_encryption_response_t)); + const size_t min_response_len = sizeof(felica_frame_response_t) + sizeof(felica_status_flags_t) + 1U; + if (resp.length < min_response_len) { + return PM3_ESOFT; + } + + memset(rd_noCry_resp, 0, sizeof(*rd_noCry_resp)); + const size_t copy_len = MIN(resp.length, sizeof(*rd_noCry_resp)); + memcpy(rd_noCry_resp, resp.data.asBytes, copy_len); + + if (rd_noCry_resp->status_flags.status_flag1[0] == 0x00 && rd_noCry_resp->status_flags.status_flag2[0] == 0x00) { + const size_t block_count = rd_noCry_resp->number_of_block[0]; + if (block_count == 0 || block_count > 15U) { + return PM3_ESOFT; + } + + const size_t expected_len = min_response_len + (block_count * FELICA_BLK_SIZE); + if (resp.length < expected_len) { + return PM3_ESOFT; + } + } + return PM3_SUCCESS; }