mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-05-12 11:18:11 -07:00
Merge pull request #3247 from kormax/felica-info-service-detection
Add detection of nested services present on a FeliCa system
This commit is contained in:
@@ -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",
|
||||
|
||||
+240
-3
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user