From f71071eb94887b5ea630f2787cca2cf482f5f59e Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sat, 3 Jan 2026 00:00:29 +0200 Subject: [PATCH 01/13] Initial commit for HRT parser --- client/CMakeLists.txt | 1 + client/Makefile | 1 + client/experimental_lib/CMakeLists.txt | 1 + client/src/cmdhfmfdes.c | 75 +++ client/src/cmdparsehrt.c | 788 +++++++++++++++++++++++++ client/src/cmdparsehrt.h | 240 ++++++++ 6 files changed, 1106 insertions(+) create mode 100644 client/src/cmdparsehrt.c create mode 100644 client/src/cmdparsehrt.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0d5bb38ce..a08ee88ec 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -468,6 +468,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdmain.c ${PM3_ROOT}/client/src/cmdmqtt.c ${PM3_ROOT}/client/src/cmdnfc.c + ${PM3_ROOT}/client/src/cmdparsehrt.c ${PM3_ROOT}/client/src/cmdparser.c ${PM3_ROOT}/client/src/cmdpiv.c ${PM3_ROOT}/client/src/cmdscript.c diff --git a/client/Makefile b/client/Makefile index 9b90a0f1c..f511239ca 100644 --- a/client/Makefile +++ b/client/Makefile @@ -809,6 +809,7 @@ SRCS = mifare/aiddesfire.c \ cmdmain.c \ cmdmqtt.c \ cmdnfc.c \ + cmdparsehrt.c \ cmdparser.c \ cmdpiv.c \ cmdscript.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index a5966ac57..171204011 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -388,6 +388,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdmain.c ${PM3_ROOT}/client/src/cmdmqtt.c ${PM3_ROOT}/client/src/cmdnfc.c + ${PM3_ROOT}/client/src/cmdparsehrt.c ${PM3_ROOT}/client/src/cmdparser.c ${PM3_ROOT}/client/src/cmdpiv.c ${PM3_ROOT}/client/src/cmdscript.c diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 4f0659871..be1985ed7 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -24,8 +24,11 @@ #include #include "jansson.h" #include "commonutil.h" // ARRAYLEN +#include "cmdparsehrt.h" #include "cmdparser.h" // command_t #include "comms.h" +#include "mifare/desfirecrypto.h" +#include "pm3_cmd.h" #include "ui.h" #include "cmdhf14a.h" #include "aes.h" @@ -417,6 +420,76 @@ static const char *getProductTypeStr(const uint8_t *versionhw) { return "UNKNOWN PROD"; } +static bool hf_mfdes_try_parse_hrt(DesfireContext_t *dctx) { + DesfireSetCommMode(dctx, DCMPlain); + DesfireSetCommandSet(dctx, DCCNative); + + // TODO: Add support for older travelcard (AID EF2011) + + if (DesfireSelectAIDHex(dctx, 0xEF2014, false, 0) == PM3_SUCCESS) { + uint8_t buf[APDU_RES_LEN] = {0}; + size_t len = 0; + + hrt_travel_card_t card; + hrt_travelcard_init_empty(&card, 2); + + // File 0x08: Application info (11 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x08, 0, 11, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_application_info(&card, buf, len); + } + + // Check that application info parses before reading other files + const char *application_id = hrt_travelcard_get_application_instance_id(&card); + if (application_id == NULL && application_id[0] == '\0') { + hrt_travelcard_free(&card); + return false; + } + PrintAndLogEx(SUCCESS, "HRT travel card detected (AID EF2014)"); + + // File 0x00: Control info (10 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x00, 0, 10, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_control_info(&card, buf, len); + } + + // File 0x01: Period pass (35 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x01, 0, 35, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_period_pass(&card, buf, len); + } + + // File 0x02: Stored value (13 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x02, 0, 13, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_stored_value(&card, buf, len); + + // Demo: Read card stored value + int stored_value = hrt_travelcard_get_stored_value_counter(&card); + char price_buf[32] = {0}; + hrt_price_to_string(stored_value, price_buf, sizeof(price_buf)); + PrintAndLogEx(SUCCESS, "HRT stored value: %s", price_buf); + } + + // File 0x03: eTicket (45 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x03, 0, 45, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_eticket(&card, buf, len); + } + + // File 0x04: History records (all) + len = 0; + if (DesfireReadRecords(dctx, 0x04, 0, 0, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_history(&card, buf, len); + } + + hrt_travelcard_free(&card); + return true; + } + + return false; +} + int mfdes_get_info(mfdes_info_res_t *info) { PacketResponseNG resp; @@ -1129,6 +1202,8 @@ static int CmdHF14ADesInfo(const char *Cmd) { */ + (void)hf_mfdes_try_parse_hrt(&dctx); + DropField(); return PM3_SUCCESS; } diff --git a/client/src/cmdparsehrt.c b/client/src/cmdparsehrt.c new file mode 100644 index 000000000..3b9f611fa --- /dev/null +++ b/client/src/cmdparsehrt.c @@ -0,0 +1,788 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// Parse Helsinki Region Transport (HRT) travel cards +//----------------------------------------------------------------------------- + +// Parser built based on com.bonwal.omamatkakortti mobile application + +#include "cmdparser.h" +#include "cmdparsehrt.h" +#include "comms.h" +#include "common.h" +#include +#include +#include +#include +#include +#include +#include + +static const int64_t EN1545_ZERO_DATE_MS = 852076800000LL; +static const int64_t DAY_IN_MS = 86400000LL; +static const int64_t MINUTE_IN_MS = 60000LL; + +// Helper Functions +static bytearray_t bytearray_alloc(size_t length) { + bytearray_t arr; + arr.data = calloc(length, 1); + arr.length = length; + return arr; +} + +static void bytearray_free(bytearray_t *arr) { + if (!arr) return; + + SAFE_FREE(arr->data); + arr->length = 0; +} + +int hrt_price_to_string(int cents, char *out, size_t out_len) { + if (!out || out_len == 0) return 0; + int euros = cents / 100; + int rem = cents % 100; + if (rem < 0) rem = -rem; + + return snprintf(out, out_len, "%d.%02d€", euros, rem); +} + +char *convert_get_hex_string(const uint8_t *data, size_t length) { + if (!data || length == 0) return NULL; + + size_t out_len = (length * 2) + 1; + char *hex = malloc(out_len); + if (!hex) return NULL; + + char *result_ptr = hex; + + for (size_t i = 0; i < length; ++i) { + snprintf(result_ptr, 3, "%02x", data[i]); + result_ptr += 2; + } + + hex[out_len - 1] = '\0'; + return hex; +} + +int convert_get_byte_value( + const uint8_t *data, + size_t data_len, + int bit_offset, + int bit_length +) { + if (!data || bit_length <= 0) return 0; + if (bit_length > 8) bit_length = 8; + + int byte_idx = bit_offset / 8; + int bit_idx = bit_offset % 8; + + if ((size_t)(byte_idx + 1) >= data_len) return 0; + + uint16_t value = + ((uint16_t)data[byte_idx] << 8) | + ((uint16_t)data[byte_idx + 1]); + + uint16_t mask = (uint16_t)((1U << bit_length) - 1U); + + int shift = (16 - bit_idx) - bit_length; + + return (int)((value >> shift) & mask); +} + +int convert_get_short_value( + const uint8_t *data, + size_t data_len, + int bit_offset, + int bit_length +) { + if (!data || bit_length <= 0) return 0; + + if (bit_length > 16) bit_length = 16; + + int byte_idx = bit_offset / 8; + int bit_idx = bit_offset % 8; + + if ((size_t)(byte_idx + 3) >= data_len) return 0; + + uint32_t value = + ((uint32_t)data[byte_idx] << 24) | + ((uint32_t)data[byte_idx + 1] << 16) | + ((uint32_t)data[byte_idx + 2] << 8) | + ((uint32_t)data[byte_idx + 3]); + + uint32_t mask = (1U << bit_length) - 1U; + int shift = (32 - bit_idx) - bit_length; + + return (int)((value >> shift) & mask); +} + +static time_t hrt_apply_local_offset(time_t time) { + struct tm local_time = {0}; + struct tm utc_time = {0}; + + if (localtime_r(&time, &local_time) == NULL) return time; + if (gmtime_r(&time, &utc_time) == NULL) return time; + + time_t local_as_time = mktime(&local_time); + time_t utc_as_local = mktime(&utc_time); + + if (local_as_time == (time_t) - 1 || utc_as_local == (time_t) - 1) return time; + + time_t offset = local_as_time - utc_as_local; + return time - offset; +} + +time_t en5145_date_to_time(int days) { + int64_t ms = (int64_t)days * DAY_IN_MS + EN1545_ZERO_DATE_MS; + time_t time = (time_t)(ms / 1000); + return hrt_apply_local_offset(time); +} + +time_t en5145_datetime_to_time(int days, int minutes) { + int64_t ms = + (int64_t)days * DAY_IN_MS + + (int64_t)minutes * MINUTE_IN_MS + + EN1545_ZERO_DATE_MS; + + time_t time = (time_t)(ms / 1000); + return hrt_apply_local_offset(time); +} + +void hrt_travelcard_free(hrt_travel_card_t *card) { + if (!card) return; + + bytearray_free(&card->application_information_data); + bytearray_free(&card->control_information_data); + bytearray_free(&card->period_pass_data); + bytearray_free(&card->stored_value_data); + bytearray_free(&card->eticket_data); + bytearray_free(&card->history_data); + + bytearray_free(&card->application_information_data_v2); + bytearray_free(&card->control_information_data_v2); + bytearray_free(&card->period_pass_data_v2); + bytearray_free(&card->stored_value_data_v2); + bytearray_free(&card->eticket_data_v2); + bytearray_free(&card->history_data_v2); + + SAFE_FREE(card->application_instance_id); + SAFE_FREE(card->history_fields); + + // TODO: Implement value tickets + // if (card->valueticket) { + // eticket_free(card->value_ticket); + // card->value_ticket = NULL; + // } + + memset(card, 0, sizeof(*card)); +} + +void hrt_travelcard_init_empty(hrt_travel_card_t *card, int version) { + if (!card) return; + + memset(card, 0, sizeof(*card)); + + card->application_information_data = bytearray_alloc(11); + card->control_information_data = bytearray_alloc(6); + card->period_pass_data = bytearray_alloc(32); + card->stored_value_data = bytearray_alloc(12); + card->eticket_data = bytearray_alloc(26); + card->history_data = bytearray_alloc(96); + + card->history_fields = calloc(8, sizeof(hrt_history_t)); + + card->application_information_data_v2 = bytearray_alloc(11); + card->control_information_data_v2 = bytearray_alloc(10); + card->period_pass_data_v2 = bytearray_alloc(35); + card->stored_value_data_v2 = bytearray_alloc(13); + card->eticket_data_v2 = bytearray_alloc(45); + card->history_data_v2 = bytearray_alloc(96); + + card->version = version; + card->error_status = 0; +} + +void hrt_travelcard_init_from_buffers( + hrt_travel_card_t *card, + const uint8_t *bArr, + const uint8_t *bArr2, + const uint8_t *bArr3, + const uint8_t *bArr4, + const uint8_t *bArr5, + const uint8_t *bArr6, + size_t history_len, + int version +) { + + hrt_travelcard_init_empty(card, version); + + if (version == 2) { + hrt_travelcard_set_application_info(card, bArr, 11); + hrt_travelcard_set_control_info(card, bArr2, 10); + hrt_travelcard_set_period_pass(card, bArr3, 35); + hrt_travelcard_set_stored_value(card, bArr4, 13); + hrt_travelcard_set_eticket(card, bArr5, 45); + hrt_travelcard_set_history(card, bArr6, history_len); + return; + } + hrt_travelcard_set_application_info(card, bArr, 11); + hrt_travelcard_set_control_info(card, bArr2, 6); + hrt_travelcard_set_period_pass(card, bArr3, 32); + hrt_travelcard_set_stored_value(card, bArr4, 12); + hrt_travelcard_set_eticket(card, bArr5, 26); + hrt_travelcard_set_history(card, bArr6, history_len); +} + + +void hrt_travelcard_init_with_error(hrt_travel_card_t *card, int error_status) +{ + hrt_travelcard_init_empty(card, 0); + card->error_status = error_status; +} + +static bool hrt_copy_and_parse(bytearray_t *dst, + const uint8_t *src, + size_t src_len, + size_t expected_len, + void (*parse_fn)(hrt_travel_card_t *, bytearray_t), + hrt_travel_card_t *card) { + if (!card || !dst || !dst->data || !src) return false; + if (expected_len == 0 || src_len < expected_len || dst->length < expected_len) return false; + + memcpy(dst->data, src, expected_len); + if (parse_fn) { + parse_fn(card, *dst); + } + return true; +} + +// Parsing Functions +void hrt_read_application_info(hrt_travel_card_t *card, bytearray_t data) { + if (!card || !data.data || data.length != 11) return; + + uint8_t first_byte = data.data[0]; + card->application_version = first_byte & 0xF0; + card->application_key_version = first_byte & 0x0F; + + uint8_t tmp[9]; + memcpy(tmp, &data.data[1], sizeof(tmp)); + + // Free previous value if exists + if (card->application_instance_id) { + free(card->application_instance_id); + card->application_instance_id = NULL; + } + + card->application_instance_id = convert_get_hex_string(tmp, sizeof(tmp)); + + uint8_t last_byte = data.data[10]; + card->platform_type = last_byte & 0xE0; + card->security_level = last_byte & 0x10; +} + +void hrt_read_control_info(hrt_travel_card_t *card, bytearray_t data) { + if (!card || !data.data) return; + card->app_status = convert_get_byte_value(data.data, data.length, 14, 1); +} + +void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { + // TODO: This function contains lots of magic numbers + // and bad variable names. Refactoring is recommended. + if (!card || !data.data || data.length < 32) return; + + const uint8_t *bytes = data.data; + + // ---- Product 1 ---- + // TODO: Better var name for i + uint16_t i = ((uint16_t)bytes[0]) << 6; + uint8_t byte1 = bytes[1]; + + card->product_code1 = (int16_t)(i | ((byte1 & 0xFC) >> 2)); + card->validity_area_type1 = (byte1 & 0x02) >> 1; + + uint8_t byte2 = bytes[2]; + card->validity_area1 = + (uint8_t)(((byte1 & 0x01) << 3) | ((byte2 & 0xE0) >> 5)); + + // TODO: Better var name for s + int16_t s = + (int16_t)(((byte2 & 0x1F) << 9) | + ((uint16_t)bytes[3] << 1) | + (MSB(bytes[4]) >> 7)); + + card->period_start_date1 = en5145_date_to_time(s); + + // TODO: Better var name for s2 + int16_t s2 = + (int16_t)((LSB7(bytes[4]) << 7) | + ((bytes[5] & 0xFE) >> 1)); + + card->period_end_date1 = en5145_date_to_time(s2); + card->period_end_date1 += 86399; // + 23:59:59 + card->period_length1 = (s2 - s) + 1; + + // ---- Product 2 ---- + uint16_t i2 = ((uint16_t)bytes[6]) << 8; + uint8_t b3 = bytes[7]; + + card->product_code2 = (int16_t)((i2 | (b3 & 0xFC)) >> 2); + card->validity_area_type2 = b3 & 0x02; + + uint8_t b4 = bytes[8]; + card->validity_area2 = + (uint8_t)(((b3 & 0x01) << 3) | ((b4 & 0xE0) >> 5)); + + int16_t s3 = + (int16_t)(((b4 & 0x1F) << 9) | + ((uint16_t)bytes[9] << 1) | + ((bytes[10] & 0x80) >> 7)); + + card->period_start_date2 = en5145_date_to_time(s3); + + int16_t s4 = + (int16_t)((LSB7(bytes[10]) << 7) | + ((bytes[11] & 0xFE) >> 1)); + + card->period_end_date2 = en5145_date_to_time(s4); + card->period_end_date2 += 86399; // + 23:59:59 + card->period_length2 = (s4 - s3) + 1; + + // ---- Loaded period ---- + uint16_t i4 = ((uint16_t)bytes[12]) << 6; + uint8_t b5 = bytes[13]; + + card->loaded_period_product = (int16_t)(i4 | ((b5 & 0xFC) >> 2)); + + uint16_t i5 = + ((b5 & 0x03) << 12) | + ((uint16_t)bytes[14] << 4); + + uint8_t b6 = bytes[15]; + + card->period_loading_date = en5145_datetime_to_time( + (int16_t)(i5 | ((b6 & 0xF0) >> 4)), + (int16_t)(((bytes[16] & 0xFE) >> 1) | + ((b6 & 0x0F) << 7)) + ); + + card->loaded_period_length = + (int16_t)(((bytes[16] & 0x01) << 8) | bytes[17]); + + uint32_t i6 = + ((uint32_t)bytes[18] << 12) | + ((uint32_t)bytes[19] << 4); + + uint8_t b7 = bytes[20]; + card->loaded_period_price = i6 | ((b7 & 0xF0) >> 4); + + uint8_t i7 = + ((b7 & 0x0F) << 10) | + ((uint16_t)bytes[21] << 2); + + uint8_t b8 = bytes[22]; + + card->period_loading_organization = + (int16_t)(i7 | ((b8 & 0xC0) >> 6)); + + card->period_loading_device_number = + (int16_t)(((b8 & 0x3F) << 8) | bytes[23]); + + // ---- Boarding ---- + uint16_t i8 = ((uint16_t)bytes[24]) << 6; + uint8_t b9 = bytes[25]; + + card->boarding_date = + en5145_datetime_to_time( + (int16_t)(i8 | ((b9 & 0xFC) >> 2)), + (int16_t)(((b9 & 0x03) << 9) | + ((uint16_t)bytes[26] << 1) | + ((bytes[27] & 0x80) >> 7)) + ); + + uint16_t i9 = LSB7(bytes[27]) << 7; + uint8_t b10 = bytes[28]; + + card->boarding_vehicle = + (int16_t)(i9 | ((b10 & 0xFE) >> 1)); + + uint8_t b11 = bytes[29]; + card->boarding_location_num_type = + (uint8_t)(((b10 & 0x01) << 1) | ((b11 & 0x80) >> 7)); + + uint16_t i11 = LSB7(b11) << 7; + uint8_t b12 = bytes[30]; + + card->boarding_location_num = + (int16_t)(i11 | ((b12 & 0xFE) >> 1)); + + card->boarding_direction = b12 & 0x01; + card->boarding_area = (bytes[31] & 0xF0) >> 4; +} + +void hrt_read_period_pass_v2(hrt_travel_card_t *card, bytearray_t data) { + if (!card || !data.data) return; + + const uint8_t *bytes = data.data; + + // ---- Product 1 ---- + card->product_code_type1 = convert_get_byte_value(bytes, data.length, 0, 1); + card->product_code1 = convert_get_short_value(bytes, data.length, 1, 14); + card->validity_area_type1 = convert_get_byte_value(bytes, data.length, 15, 2); + card->validity_area1 = convert_get_short_value(bytes, data.length, 17, 6); + + int start1 = convert_get_short_value(bytes, data.length, 23, 14); + int end1 = convert_get_short_value(bytes, data.length, 37, 14); + + card->period_start_date1 = en5145_date_to_time(start1); + card->period_end_date1 = en5145_date_to_time(end1); + card->period_length1 = (end1 - start1) + 1; + + // ---- Product 2 ---- + // In the original decompiled code, product_code_type1 is reassigned from bit offset 56. + // This should probably be product_code_type2 instead as below + card->product_code_type2 = convert_get_byte_value(bytes, data.length, 56, 1); + card->product_code2 = convert_get_short_value(bytes, data.length, 57, 14); + card->validity_area_type2 = convert_get_byte_value(bytes, data.length, 71, 2); + card->validity_area2 = convert_get_short_value(bytes, data.length, 73, 6); + + int start2 = convert_get_short_value(bytes, data.length, 79, 14); + int end2 = convert_get_short_value(bytes, data.length, 93, 14); + + card->period_start_date2 = en5145_date_to_time(start2); + card->period_end_date2 = en5145_date_to_time(end2); + card->period_length2 = (end2 - start2) + 1; + + // ---- Loaded period ---- + card->loaded_period_product_type = convert_get_byte_value(bytes, data.length, 112, 1); + card->loaded_period_product = convert_get_short_value(bytes, data.length, 113, 14); + card->period_loading_date = + en5145_datetime_to_time( + convert_get_short_value(bytes, data.length, 127, 14), + convert_get_short_value(bytes, data.length, 141, 11) + ); + card->loaded_period_length = convert_get_short_value(bytes, data.length, 152, 9); + card->loaded_period_price = convert_get_short_value(bytes, data.length, 161, 20); + card->period_loading_organization = convert_get_short_value(bytes, data.length, 181, 14); + card->period_loading_device_number = convert_get_short_value(bytes, data.length, 195, 13); + + // ---- Boarding ---- + card->boarding_date = + en5145_datetime_to_time( + convert_get_short_value(bytes, data.length, 208, 14), + convert_get_short_value(bytes, data.length, 222, 11) + ); + card->boarding_vehicle = convert_get_short_value(bytes, data.length, 233, 14); + card->boarding_location_num_type = convert_get_short_value(bytes, data.length, 247, 2); + card->boarding_location_num = convert_get_short_value(bytes, data.length, 249, 14); + card->boarding_direction = convert_get_byte_value(bytes, data.length, 263, 1); + card->boarding_area_type = convert_get_byte_value(bytes, data.length, 264, 2); + card->boarding_area = convert_get_byte_value(bytes, data.length, 266, 6); +} + +void hrt_read_stored_value(hrt_travel_card_t *card, bytearray_t data) { + if (!card || !data.data || data.length < 3) return; + + const uint8_t *bytes = data.data; + + card->stored_value_counter = + ((uint32_t)(bytes[2] & 0xF0) >> 4) | + ((uint32_t)bytes[0] << 12) | + ((uint32_t)bytes[1] << 4); +} + +// void readHistory(bytearray_t data, size_t length); +// void readHistory_v2(bytearray_t data, size_t length); + +void hrt_history_init(hrt_history_t *history) { + if (!history) return; + + history->group_size = 0; + history->price = 0; + history->transaction_d_time = (time_t)0; + history->transaction_type = 0; + history->transfer_end_date = (time_t)0; +} + +void hrt_history_set_transaction_datetime(hrt_history_t *history, time_t datetime) { + if (history) history->transaction_d_time = datetime; +} + +void hrt_history_set_transaction_type(hrt_history_t *history, int type) { + if (history) history->transaction_type = type; +} + +void hrt_history_set_group_size(hrt_history_t *history, int group_size) { + if (history) history->group_size = group_size; +} + +void hrt_history_set_price(hrt_history_t *history, int price) { + if (history) history->price = price; +} + +void hrt_history_set_transfer_end_date(hrt_history_t *history, time_t datetime) { + if (history) history->transfer_end_date = datetime; +} + +// TODO: Implement value tickets +// HRT_eTicket *eticket_create(bytearray_t data, int is_encrypted, int version); + +// Getter methods for API simplicity +int hrt_travelcard_get_application_version(const hrt_travel_card_t *card) { + return card ? card->application_version : 0; +} + +int hrt_travelcard_get_application_key_version(const hrt_travel_card_t *card) { + return card ? card->application_key_version : 0; +} + +const char *hrt_travelcard_get_application_instance_id(const hrt_travel_card_t *card) { + return card ? card->application_instance_id : NULL; +} + +int hrt_travelcard_get_platform_type(const hrt_travel_card_t *card) { + return card ? card->platform_type : 0; +} + +int hrt_travelcard_get_security_level(const hrt_travel_card_t *card) { + return card ? card->security_level : 0; +} + +int hrt_travelcard_get_product_code_type1(const hrt_travel_card_t *card) { + return card ? card->product_code_type1 : 0; +} + +int hrt_travelcard_get_product_code1(const hrt_travel_card_t *card) { + return card ? card->product_code1 : 0; +} + +int hrt_travelcard_get_validity_area_type1(const hrt_travel_card_t *card) { + return card ? card->validity_area_type1 : 0; +} + +int hrt_travelcard_get_validity_area1(const hrt_travel_card_t *card) { + return card ? card->validity_area1 : 0; +} + +time_t hrt_travelcard_get_period_start_date1(const hrt_travel_card_t *card) { + return card ? card->period_start_date1 : (time_t)0; +} + +time_t hrt_travelcard_get_period_end_date1(const hrt_travel_card_t *card) { + return card ? card->period_end_date1 : (time_t)0; +} + +int hrt_travelcard_get_period_length1(const hrt_travel_card_t *card) { + return card ? card->period_length1 : 0; +} + +int hrt_travelcard_get_product_code_type2(const hrt_travel_card_t *card) { + return card ? card->product_code_type2 : 0; +} + +int hrt_travelcard_get_product_code2(const hrt_travel_card_t *card) { + return card ? card->product_code2 : 0; +} + +int hrt_travelcard_get_validity_area_type2(const hrt_travel_card_t *card) { + return card ? card->validity_area_type2 : 0; +} + +int hrt_travelcard_get_validity_area2(const hrt_travel_card_t *card) { + return card ? card->validity_area2 : 0; +} + +time_t hrt_travelcard_get_period_start_date2(const hrt_travel_card_t *card) { + return card ? card->period_start_date2 : (time_t)0; +} + +time_t hrt_travelcard_get_period_end_date2(const hrt_travel_card_t *card) { + return card ? card->period_end_date2 : (time_t)0; +} + +int hrt_travelcard_get_period_length2(const hrt_travel_card_t *card) { + return card ? card->period_length2 : 0; +} + +int hrt_travelcard_get_stored_value_counter(const hrt_travel_card_t *card) { + return card ? card->stored_value_counter : 0; +} + +const hrt_eticket_t *hrt_travelcard_get_value_ticket(const hrt_travel_card_t *card) { + return card ? card->value_ticket : NULL; +} + +const hrt_history_t *hrt_travelcard_get_history(const hrt_travel_card_t *card) { + return card ? card->history_fields : NULL; +} + +int hrt_travelcard_get_history_len(const hrt_travel_card_t *card) { + return card ? card->history_len : 0; +} + +int hrt_travelcard_get_boarding_area(const hrt_travel_card_t *card) { + return card ? card->boarding_area : 0; +} + +int hrt_travelcard_get_boarding_area_type(const hrt_travel_card_t *card) { + return card ? card->boarding_area_type : 0; +} + +time_t hrt_travelcard_get_boarding_date(const hrt_travel_card_t *card) { + return card ? card->boarding_date : (time_t)0; +} + +int hrt_travelcard_get_boarding_vehicle(const hrt_travel_card_t *card) { + return card ? card->boarding_vehicle : 0; +} + +int hrt_travelcard_get_boarding_location_num_type(const hrt_travel_card_t *card) { + return card ? card->boarding_location_num_type : 0; +} + +int hrt_travelcard_get_boarding_location_num(const hrt_travel_card_t *card) { + return card ? card->boarding_location_num : 0; +} + +int hrt_travelcard_get_boarding_direction(const hrt_travel_card_t *card) { + return card ? card->boarding_direction : 0; +} + +int hrt_travelcard_get_loaded_period_product_type(const hrt_travel_card_t *card) { + return card ? card->loaded_period_product_type : 0; +} + +int hrt_travelcard_get_loaded_period_product(const hrt_travel_card_t *card) { + return card ? card->loaded_period_product : 0; +} + +time_t hrt_travelcard_get_period_loading_date(const hrt_travel_card_t *card) { + return card ? card->period_loading_date : (time_t)0; +} + +int hrt_travelcard_get_loaded_period_length(const hrt_travel_card_t *card) { + return card ? card->loaded_period_length : 0; +} + +int hrt_travelcard_get_loaded_period_price(const hrt_travel_card_t *card) { + return card ? card->loaded_period_price : 0; +} + +int hrt_travelcard_get_period_loading_organization(const hrt_travel_card_t *card) { + return card ? card->period_loading_organization : 0; +} + +int hrt_travelcard_get_period_loading_device_number(const hrt_travel_card_t *card) { + return card ? card->period_loading_device_number : 0; +} + +int hrt_travelcard_get_app_status(const hrt_travel_card_t *card) { + return card ? card->app_status : 0; +} + +int hrt_travelcard_get_version(const hrt_travel_card_t *card) { + return card ? card->version : 0; +} + +bool hrt_travelcard_set_application_info(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { + if (!card || !buf || len == 0) return false; + + if (card->version == 2) { + return hrt_copy_and_parse(&card->application_information_data_v2, buf, len, 11, + hrt_read_application_info, card); + } + return hrt_copy_and_parse(&card->application_information_data, buf, len, 11, + hrt_read_application_info, card); +} + +bool hrt_travelcard_set_control_info(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { + if (!card || !buf || len == 0) return false; + + if (card->version == 2) { + return hrt_copy_and_parse(&card->control_information_data_v2, buf, len, 10, + hrt_read_control_info, card); + } + return hrt_copy_and_parse(&card->control_information_data, buf, len, 6, + hrt_read_control_info, card); +} + +bool hrt_travelcard_set_period_pass(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { + if (!card || !buf || len == 0) return false; + + if (card->version == 2) { + return hrt_copy_and_parse(&card->period_pass_data_v2, buf, len, 35, + hrt_read_period_pass_v2, card); + } + return hrt_copy_and_parse(&card->period_pass_data, buf, len, 32, + hrt_read_period_pass, card); +} + +bool hrt_travelcard_set_stored_value(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { + if (!card || !buf || len == 0) return false; + + if (card->version == 2) { + return hrt_copy_and_parse(&card->stored_value_data_v2, buf, len, 13, + hrt_read_stored_value, card); + } + return hrt_copy_and_parse(&card->stored_value_data, buf, len, 12, + hrt_read_stored_value, card); +} + +bool hrt_travelcard_set_eticket(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { + if (!card || !buf || len == 0) return false; + + if (card->version == 2) { + if (len < 45 || card->eticket_data_v2.length < 45) return false; + memcpy(card->eticket_data_v2.data, buf, 45); + return true; + } + + if (len < 26 || card->eticket_data.length < 26) return false; + memcpy(card->eticket_data.data, buf, 26); + return true; +} + +bool hrt_travelcard_set_history(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { + if (!card || !buf || len == 0) return false; + + if (card->version == 2) { + if (len > card->history_data_v2.length) len = card->history_data_v2.length; + memcpy(card->history_data_v2.data, buf, len); + // TODO: readHistory_v2(card->historyData_v2, len); + return true; + } + + if (len > card->history_data.length) len = card->history_data.length; + memcpy(card->history_data.data, buf, len); + // TODO: readHistory(card->historyData, len); + return true; +} + +static int CmdHelp(const char *Cmd); + +static command_t CommandTable[] = { + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} + +int CmdParseHRT(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} diff --git a/client/src/cmdparsehrt.h b/client/src/cmdparsehrt.h new file mode 100644 index 000000000..cbd07439e --- /dev/null +++ b/client/src/cmdparsehrt.h @@ -0,0 +1,240 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// Parse Helsinki Region Transport (HRT) travel cards +//----------------------------------------------------------------------------- + +#ifndef CMDParseHRT_H__ +#define CMDParseHRT_H__ + +#include "common.h" +#include +#include + +#define HRT_TRAVELCARD_OK_STATUS 0 +#define HRT_TRAVELCARD_NO_HSL_CARD 1 +#define HRT_TRAVELCARD_HSL_CARD_DATA_FAILURE 2 +#define HRT_TRAVELCARD_CARD_READ_FAILURE 3 +#define HRT_TRAVELCARD_HSL_CARDNUMBER_FAILURE 4 + +#define MSB(b) ((b) & 0x80) +#define LSB7(b) ((b) & 0x7F) +#define SAFE_FREE(ptr) do { free(ptr); (ptr) = NULL; } while (0) + +// TODO: Implement +typedef struct hrt_eticket_t hrt_eticket_t; + +typedef struct { + uint8_t *data; + size_t length; +} PACKED bytearray_t; + +typedef struct { + int group_size; + int price; + time_t transaction_d_time; + int transaction_type; + time_t transfer_end_date; +} PACKED hrt_history_t; + +typedef struct { + // ---- Status ---- + int app_status; + int error_status; + + // ---- Application Information ---- + bytearray_t application_information_data; + bytearray_t application_information_data_v2; + char* application_instance_id; + int application_key_version; + int application_version; + int version; + int security_level; + int platform_type; + + // ---- Boarding Information ---- + int boarding_area; + int boarding_area_type; + time_t boarding_date; + int boarding_direction; + int boarding_location_num; + int boarding_location_num_type; + int boarding_vehicle; + time_t last_boarding_date_time; + + // ---- Control / Ticket data ---- + bytearray_t control_information_data; + bytearray_t control_information_data_v2; + bytearray_t eticket_data; + bytearray_t eticket_data_v2; + + // ---- History ---- + bytearray_t history_data; + bytearray_t history_data_v2; + hrt_history_t *history_fields; + int history_len; + + // ---- Period Pass data ---- + bytearray_t period_pass_data; + bytearray_t period_pass_data_v2; + + time_t period_loading_date; + int period_loading_device_number; + int period_loading_organization; + + time_t period_start_date1; + time_t period_end_date1; + int period_length1; + int product_code1; + int product_code_type1; + int validity_area1; + int validity_area_type1; + + time_t period_start_date2; + time_t period_end_date2; + int period_length2; + int product_code2; + int product_code_type2; + int validity_area2; + int validity_area_type2; + + int loaded_period_length; + int loaded_period_price; + int loaded_period_product; + int loaded_period_product_type; + + // ---- Stored Value ---- + bytearray_t stored_value_data; + bytearray_t stored_value_data_v2; + int stored_value_counter; + + // ---- Value Ticket ---- + hrt_eticket_t *value_ticket; +} PACKED hrt_travel_card_t; + +int hrt_price_to_string(int cents, char *out, size_t out_len); +char *convert_get_hex_string(const uint8_t *data, size_t length); +int convert_get_byte_value( + const uint8_t *data, + size_t data_len, + int bit_offset, + int bit_length +); +int convert_get_short_value( + const uint8_t *data, + size_t data_len, + int bit_offset, + int bit_length +); + +time_t en5145_date_to_time(int days); +time_t en5145_datetime_to_time(int days, int minutes); + +void hrt_travelcard_init_from_buffers( + hrt_travel_card_t *card, + const uint8_t *bArr, + const uint8_t *bArr2, + const uint8_t *bArr3, + const uint8_t *bArr4, + const uint8_t *bArr5, + const uint8_t *bArr6, + size_t history_len, + int version +); + +void hrt_travelcard_init_with_error( + hrt_travel_card_t *card, + int error_status +); + +void hrt_travelcard_free(hrt_travel_card_t *card); + +void hrt_history_init(hrt_history_t *history); +void hrt_history_set_transaction_datetime(hrt_history_t *history, time_t datetime); +void hrt_history_set_transaction_type(hrt_history_t *history, int type); +void hrt_history_set_group_size(hrt_history_t *history, int group_size); +void hrt_history_set_price(hrt_history_t *history, int price); +void hrt_history_set_transfer_end_date(hrt_history_t *history, time_t datetime); + +// Parsing Functions +void hrt_read_application_info(hrt_travel_card_t *card, bytearray_t data); +void hrt_read_control_info(hrt_travel_card_t *card, bytearray_t data); +void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data); +void hrt_read_period_pass_v2(hrt_travel_card_t *card, bytearray_t data); +void hrt_read_stored_value(hrt_travel_card_t *card, bytearray_t data); +void hrt_read_history(bytearray_t data, size_t length); +void hrt_read_history_v2(bytearray_t data, size_t length); + +// Getter Functions +int hrt_travelcard_get_application_version(const hrt_travel_card_t *card); +int hrt_travelcard_get_application_key_version(const hrt_travel_card_t *card); +const char *hrt_travelcard_get_application_instance_id(const hrt_travel_card_t *card); +int hrt_travelcard_get_platform_type(const hrt_travel_card_t *card); +int hrt_travelcard_get_security_level(const hrt_travel_card_t *card); + +int hrt_travelcard_get_product_code_type1(const hrt_travel_card_t *card); +int hrt_travelcard_get_product_code1(const hrt_travel_card_t *card); +int hrt_travelcard_get_validity_area_type1(const hrt_travel_card_t *card); +int hrt_travelcard_get_validity_area1(const hrt_travel_card_t *card); +time_t hrt_travelcard_get_period_start_date1(const hrt_travel_card_t *card); +time_t hrt_travelcard_get_period_end_date1(const hrt_travel_card_t *card); +int hrt_travelcard_get_period_length1(const hrt_travel_card_t *card); + +int hrt_travelcard_get_product_code_type2(const hrt_travel_card_t *card); +int hrt_travelcard_get_product_code2(const hrt_travel_card_t *card); +int hrt_travelcard_get_validity_area_type2(const hrt_travel_card_t *card); +int hrt_travelcard_get_validity_area2(const hrt_travel_card_t *card); +time_t hrt_travelcard_get_period_start_date2(const hrt_travel_card_t *card); +time_t hrt_travelcard_get_period_end_date2(const hrt_travel_card_t *card); +int hrt_travelcard_get_period_length2(const hrt_travel_card_t *card); + +int hrt_travelcard_get_stored_value_counter(const hrt_travel_card_t *card); + +const hrt_eticket_t *hrt_travelcard_get_value_ticket(const hrt_travel_card_t *card); + +const hrt_history_t *hrt_travelcard_get_history(const hrt_travel_card_t *card); +int hrt_travelcard_get_history_len(const hrt_travel_card_t *card); + +int hrt_travelcard_get_boarding_area(const hrt_travel_card_t *card); +int hrt_travelcard_get_boarding_area_type(const hrt_travel_card_t *card); +time_t hrt_travelcard_get_boarding_date(const hrt_travel_card_t *card); +int hrt_travelcard_get_boarding_vehicle(const hrt_travel_card_t *card); +int hrt_travelcard_get_boarding_location_num_type(const hrt_travel_card_t *card); +int hrt_travelcard_get_boarding_location_num(const hrt_travel_card_t *card); +int hrt_travelcard_get_boarding_direction(const hrt_travel_card_t *card); + +int hrt_travelcard_get_loaded_period_product_type(const hrt_travel_card_t *card); +int hrt_travelcard_get_loaded_period_product(const hrt_travel_card_t *card); +time_t hrt_travelcard_get_period_loading_date(const hrt_travel_card_t *card); +int hrt_travelcard_get_loaded_period_length(const hrt_travel_card_t *card); +int hrt_travelcard_get_loaded_period_price(const hrt_travel_card_t *card); +int hrt_travelcard_get_period_loading_organization(const hrt_travel_card_t *card); +int hrt_travelcard_get_period_loading_device_number(const hrt_travel_card_t *card); + +int hrt_travelcard_get_app_status(const hrt_travel_card_t *card); +int hrt_travelcard_get_version(const hrt_travel_card_t *card); + +int CmdParseHRT(const char *Cmd); + +// Setter Functions +void hrt_travelcard_init_empty(hrt_travel_card_t *card, int version); +bool hrt_travelcard_set_application_info(hrt_travel_card_t *card, const uint8_t *buf, size_t len); +bool hrt_travelcard_set_control_info(hrt_travel_card_t *card, const uint8_t *buf, size_t len); +bool hrt_travelcard_set_period_pass(hrt_travel_card_t *card, const uint8_t *buf, size_t len); +bool hrt_travelcard_set_stored_value(hrt_travel_card_t *card, const uint8_t *buf, size_t len); +bool hrt_travelcard_set_eticket(hrt_travel_card_t *card, const uint8_t *buf, size_t len); +bool hrt_travelcard_set_history(hrt_travel_card_t *card, const uint8_t *buf, size_t len); + +#endif From f216a7d5fb7aa42f9118f7b7ea4f3745163a5920 Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sat, 3 Jan 2026 00:17:16 +0200 Subject: [PATCH 02/13] Enforced styling with 'make style' --- client/src/cmdparsehrt.c | 27 +++++++++++++-------------- client/src/cmdparsehrt.h | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/client/src/cmdparsehrt.c b/client/src/cmdparsehrt.c index 3b9f611fa..3ea2e2a78 100644 --- a/client/src/cmdparsehrt.c +++ b/client/src/cmdparsehrt.c @@ -246,8 +246,7 @@ void hrt_travelcard_init_from_buffers( } -void hrt_travelcard_init_with_error(hrt_travel_card_t *card, int error_status) -{ +void hrt_travelcard_init_with_error(hrt_travel_card_t *card, int error_status) { hrt_travelcard_init_empty(card, 0); card->error_status = error_status; } @@ -319,15 +318,15 @@ void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { // TODO: Better var name for s int16_t s = (int16_t)(((byte2 & 0x1F) << 9) | - ((uint16_t)bytes[3] << 1) | - (MSB(bytes[4]) >> 7)); + ((uint16_t)bytes[3] << 1) | + (MSB(bytes[4]) >> 7)); card->period_start_date1 = en5145_date_to_time(s); // TODO: Better var name for s2 int16_t s2 = (int16_t)((LSB7(bytes[4]) << 7) | - ((bytes[5] & 0xFE) >> 1)); + ((bytes[5] & 0xFE) >> 1)); card->period_end_date1 = en5145_date_to_time(s2); card->period_end_date1 += 86399; // + 23:59:59 @@ -346,14 +345,14 @@ void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { int16_t s3 = (int16_t)(((b4 & 0x1F) << 9) | - ((uint16_t)bytes[9] << 1) | - ((bytes[10] & 0x80) >> 7)); + ((uint16_t)bytes[9] << 1) | + ((bytes[10] & 0x80) >> 7)); card->period_start_date2 = en5145_date_to_time(s3); int16_t s4 = (int16_t)((LSB7(bytes[10]) << 7) | - ((bytes[11] & 0xFE) >> 1)); + ((bytes[11] & 0xFE) >> 1)); card->period_end_date2 = en5145_date_to_time(s4); card->period_end_date2 += 86399; // + 23:59:59 @@ -372,10 +371,10 @@ void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { uint8_t b6 = bytes[15]; card->period_loading_date = en5145_datetime_to_time( - (int16_t)(i5 | ((b6 & 0xF0) >> 4)), - (int16_t)(((bytes[16] & 0xFE) >> 1) | - ((b6 & 0x0F) << 7)) - ); + (int16_t)(i5 | ((b6 & 0xF0) >> 4)), + (int16_t)(((bytes[16] & 0xFE) >> 1) | + ((b6 & 0x0F) << 7)) + ); card->loaded_period_length = (int16_t)(((bytes[16] & 0x01) << 8) | bytes[17]); @@ -407,8 +406,8 @@ void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { en5145_datetime_to_time( (int16_t)(i8 | ((b9 & 0xFC) >> 2)), (int16_t)(((b9 & 0x03) << 9) | - ((uint16_t)bytes[26] << 1) | - ((bytes[27] & 0x80) >> 7)) + ((uint16_t)bytes[26] << 1) | + ((bytes[27] & 0x80) >> 7)) ); uint16_t i9 = LSB7(bytes[27]) << 7; diff --git a/client/src/cmdparsehrt.h b/client/src/cmdparsehrt.h index cbd07439e..b6b684a86 100644 --- a/client/src/cmdparsehrt.h +++ b/client/src/cmdparsehrt.h @@ -57,7 +57,7 @@ typedef struct { // ---- Application Information ---- bytearray_t application_information_data; bytearray_t application_information_data_v2; - char* application_instance_id; + char *application_instance_id; int application_key_version; int application_version; int version; From fa70d88c739fac99219eabfef48249136697a5fd Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Wed, 17 Jun 2026 21:28:07 +0300 Subject: [PATCH 03/13] Fixed windows compatibility problems --- client/src/cmdparsehrt.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/src/cmdparsehrt.c b/client/src/cmdparsehrt.c index 3ea2e2a78..c36d22ada 100644 --- a/client/src/cmdparsehrt.c +++ b/client/src/cmdparsehrt.c @@ -18,6 +18,10 @@ // Parser built based on com.bonwal.omamatkakortti mobile application +#if !defined(_WIN32) +#define _POSIX_C_SOURCE 200112L +#endif + #include "cmdparser.h" #include "cmdparsehrt.h" #include "comms.h" @@ -55,7 +59,7 @@ int hrt_price_to_string(int cents, char *out, size_t out_len) { int rem = cents % 100; if (rem < 0) rem = -rem; - return snprintf(out, out_len, "%d.%02d€", euros, rem); + return snprintf(out, out_len, "%d.%02d EUR", euros, rem); } char *convert_get_hex_string(const uint8_t *data, size_t length) { @@ -132,8 +136,13 @@ static time_t hrt_apply_local_offset(time_t time) { struct tm local_time = {0}; struct tm utc_time = {0}; +#if defined(_WIN32) + if (localtime_s(&local_time, &time) != 0) return time; + if (gmtime_s(&utc_time, &time) != 0) return time; +#else if (localtime_r(&time, &local_time) == NULL) return time; if (gmtime_r(&time, &utc_time) == NULL) return time; +#endif time_t local_as_time = mktime(&local_time); time_t utc_as_local = mktime(&utc_time); From fed60112388d93c543c6ccc3d71e8a9c78a06f97 Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Thu, 18 Jun 2026 20:20:55 +0300 Subject: [PATCH 04/13] Fixed a null/empty check bug in hf_mfdes_try_parse_hrt --- client/src/cmdhfmfdes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index be1985ed7..2ded59c41 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -441,7 +441,7 @@ static bool hf_mfdes_try_parse_hrt(DesfireContext_t *dctx) { // Check that application info parses before reading other files const char *application_id = hrt_travelcard_get_application_instance_id(&card); - if (application_id == NULL && application_id[0] == '\0') { + if (application_id == NULL || application_id[0] == '\0') { hrt_travelcard_free(&card); return false; } From 3cc5b1ead70dd37cc61075d253bf2d6804236493 Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Thu, 18 Jun 2026 20:22:29 +0300 Subject: [PATCH 05/13] Converted from custom bytearray_t type to uint8_t *data + size_t len --- client/src/cmdparsehrt.c | 280 +++++++++++++++++++++------------------ client/src/cmdparsehrt.h | 57 ++++---- 2 files changed, 177 insertions(+), 160 deletions(-) diff --git a/client/src/cmdparsehrt.c b/client/src/cmdparsehrt.c index c36d22ada..135fe019e 100644 --- a/client/src/cmdparsehrt.c +++ b/client/src/cmdparsehrt.c @@ -26,6 +26,7 @@ #include "cmdparsehrt.h" #include "comms.h" #include "common.h" +#include "ui.h" #include #include #include @@ -38,20 +39,17 @@ static const int64_t EN1545_ZERO_DATE_MS = 852076800000LL; static const int64_t DAY_IN_MS = 86400000LL; static const int64_t MINUTE_IN_MS = 60000LL; -// Helper Functions -static bytearray_t bytearray_alloc(size_t length) { - bytearray_t arr; - arr.data = calloc(length, 1); - arr.length = length; - return arr; -} - -static void bytearray_free(bytearray_t *arr) { - if (!arr) return; - - SAFE_FREE(arr->data); - arr->length = 0; -} +#define HRT_APPLICATION_INFORMATION_LEN 11 +#define HRT_CONTROL_INFORMATION_LEN 6 +#define HRT_CONTROL_INFORMATION_V2_LEN 10 +#define HRT_PERIOD_PASS_LEN 32 +#define HRT_PERIOD_PASS_V2_LEN 35 +#define HRT_STORED_VALUE_LEN 12 +#define HRT_STORED_VALUE_V2_LEN 13 +#define HRT_ETICKET_LEN 26 +#define HRT_ETICKET_V2_LEN 45 +#define HRT_HISTORY_LEN 96 +#define HRT_HISTORY_RECORDS 8 int hrt_price_to_string(int cents, char *out, size_t out_len) { if (!out || out_len == 0) return 0; @@ -66,8 +64,11 @@ char *convert_get_hex_string(const uint8_t *data, size_t length) { if (!data || length == 0) return NULL; size_t out_len = (length * 2) + 1; - char *hex = malloc(out_len); - if (!hex) return NULL; + char *hex = calloc(out_len, sizeof(char)); + if (!hex) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return NULL; + } char *result_ptr = hex; @@ -172,25 +173,25 @@ time_t en5145_datetime_to_time(int days, int minutes) { void hrt_travelcard_free(hrt_travel_card_t *card) { if (!card) return; - bytearray_free(&card->application_information_data); - bytearray_free(&card->control_information_data); - bytearray_free(&card->period_pass_data); - bytearray_free(&card->stored_value_data); - bytearray_free(&card->eticket_data); - bytearray_free(&card->history_data); + free(card->application_information_data); + free(card->control_information_data); + free(card->period_pass_data); + free(card->stored_value_data); + free(card->eticket_data); + free(card->history_data); - bytearray_free(&card->application_information_data_v2); - bytearray_free(&card->control_information_data_v2); - bytearray_free(&card->period_pass_data_v2); - bytearray_free(&card->stored_value_data_v2); - bytearray_free(&card->eticket_data_v2); - bytearray_free(&card->history_data_v2); + free(card->application_information_data_v2); + free(card->control_information_data_v2); + free(card->period_pass_data_v2); + free(card->stored_value_data_v2); + free(card->eticket_data_v2); + free(card->history_data_v2); - SAFE_FREE(card->application_instance_id); - SAFE_FREE(card->history_fields); + free(card->application_instance_id); + free(card->history_fields); // TODO: Implement value tickets - // if (card->valueticket) { + // if (card->value_ticket) { // eticket_free(card->value_ticket); // card->value_ticket = NULL; // } @@ -203,21 +204,42 @@ void hrt_travelcard_init_empty(hrt_travel_card_t *card, int version) { memset(card, 0, sizeof(*card)); - card->application_information_data = bytearray_alloc(11); - card->control_information_data = bytearray_alloc(6); - card->period_pass_data = bytearray_alloc(32); - card->stored_value_data = bytearray_alloc(12); - card->eticket_data = bytearray_alloc(26); - card->history_data = bytearray_alloc(96); + card->application_information_data = calloc(HRT_APPLICATION_INFORMATION_LEN, sizeof(uint8_t)); + card->control_information_data = calloc(HRT_CONTROL_INFORMATION_LEN, sizeof(uint8_t)); + card->period_pass_data = calloc(HRT_PERIOD_PASS_LEN, sizeof(uint8_t)); + card->stored_value_data = calloc(HRT_STORED_VALUE_LEN, sizeof(uint8_t)); + card->eticket_data = calloc(HRT_ETICKET_LEN, sizeof(uint8_t)); + card->history_data = calloc(HRT_HISTORY_LEN, sizeof(uint8_t)); - card->history_fields = calloc(8, sizeof(hrt_history_t)); + card->history_fields = calloc(HRT_HISTORY_RECORDS, sizeof(hrt_history_t)); - card->application_information_data_v2 = bytearray_alloc(11); - card->control_information_data_v2 = bytearray_alloc(10); - card->period_pass_data_v2 = bytearray_alloc(35); - card->stored_value_data_v2 = bytearray_alloc(13); - card->eticket_data_v2 = bytearray_alloc(45); - card->history_data_v2 = bytearray_alloc(96); + card->application_information_data_v2 = calloc(HRT_APPLICATION_INFORMATION_LEN, sizeof(uint8_t)); + card->control_information_data_v2 = calloc(HRT_CONTROL_INFORMATION_V2_LEN, sizeof(uint8_t)); + card->period_pass_data_v2 = calloc(HRT_PERIOD_PASS_V2_LEN, sizeof(uint8_t)); + card->stored_value_data_v2 = calloc(HRT_STORED_VALUE_V2_LEN, sizeof(uint8_t)); + card->eticket_data_v2 = calloc(HRT_ETICKET_V2_LEN, sizeof(uint8_t)); + card->history_data_v2 = calloc(HRT_HISTORY_LEN, sizeof(uint8_t)); + + if ( + !card->application_information_data || + !card->control_information_data || + !card->period_pass_data || + !card->stored_value_data || + !card->eticket_data || + !card->history_data || + !card->history_fields || + !card->application_information_data_v2 || + !card->control_information_data_v2 || + !card->period_pass_data_v2 || + !card->stored_value_data_v2 || + !card->eticket_data_v2 || + !card->history_data_v2 + ) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + hrt_travelcard_free(card); + card->error_status = HRT_TRAVELCARD_HSL_CARD_DATA_FAILURE; + return; + } card->version = version; card->error_status = 0; @@ -236,21 +258,22 @@ void hrt_travelcard_init_from_buffers( ) { hrt_travelcard_init_empty(card, version); + if (!card || card->error_status != HRT_TRAVELCARD_OK_STATUS) return; if (version == 2) { - hrt_travelcard_set_application_info(card, bArr, 11); - hrt_travelcard_set_control_info(card, bArr2, 10); - hrt_travelcard_set_period_pass(card, bArr3, 35); - hrt_travelcard_set_stored_value(card, bArr4, 13); - hrt_travelcard_set_eticket(card, bArr5, 45); + hrt_travelcard_set_application_info(card, bArr, HRT_APPLICATION_INFORMATION_LEN); + hrt_travelcard_set_control_info(card, bArr2, HRT_CONTROL_INFORMATION_V2_LEN); + hrt_travelcard_set_period_pass(card, bArr3, HRT_PERIOD_PASS_V2_LEN); + hrt_travelcard_set_stored_value(card, bArr4, HRT_STORED_VALUE_V2_LEN); + hrt_travelcard_set_eticket(card, bArr5, HRT_ETICKET_V2_LEN); hrt_travelcard_set_history(card, bArr6, history_len); return; } - hrt_travelcard_set_application_info(card, bArr, 11); - hrt_travelcard_set_control_info(card, bArr2, 6); - hrt_travelcard_set_period_pass(card, bArr3, 32); - hrt_travelcard_set_stored_value(card, bArr4, 12); - hrt_travelcard_set_eticket(card, bArr5, 26); + hrt_travelcard_set_application_info(card, bArr, HRT_APPLICATION_INFORMATION_LEN); + hrt_travelcard_set_control_info(card, bArr2, HRT_CONTROL_INFORMATION_LEN); + hrt_travelcard_set_period_pass(card, bArr3, HRT_PERIOD_PASS_LEN); + hrt_travelcard_set_stored_value(card, bArr4, HRT_STORED_VALUE_LEN); + hrt_travelcard_set_eticket(card, bArr5, HRT_ETICKET_LEN); hrt_travelcard_set_history(card, bArr6, history_len); } @@ -260,32 +283,32 @@ void hrt_travelcard_init_with_error(hrt_travel_card_t *card, int error_status) { card->error_status = error_status; } -static bool hrt_copy_and_parse(bytearray_t *dst, +static bool hrt_copy_and_parse(uint8_t *dst, const uint8_t *src, size_t src_len, size_t expected_len, - void (*parse_fn)(hrt_travel_card_t *, bytearray_t), + void (*parse_fn)(hrt_travel_card_t *, const uint8_t *, size_t), hrt_travel_card_t *card) { - if (!card || !dst || !dst->data || !src) return false; - if (expected_len == 0 || src_len < expected_len || dst->length < expected_len) return false; + if (!card || !dst || !src) return false; + if (expected_len == 0 || src_len < expected_len) return false; - memcpy(dst->data, src, expected_len); + memcpy(dst, src, expected_len); if (parse_fn) { - parse_fn(card, *dst); + parse_fn(card, dst, expected_len); } return true; } // Parsing Functions -void hrt_read_application_info(hrt_travel_card_t *card, bytearray_t data) { - if (!card || !data.data || data.length != 11) return; +void hrt_read_application_info(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || data_len != HRT_APPLICATION_INFORMATION_LEN) return; - uint8_t first_byte = data.data[0]; + uint8_t first_byte = data[0]; card->application_version = first_byte & 0xF0; card->application_key_version = first_byte & 0x0F; uint8_t tmp[9]; - memcpy(tmp, &data.data[1], sizeof(tmp)); + memcpy(tmp, &data[1], sizeof(tmp)); // Free previous value if exists if (card->application_instance_id) { @@ -295,22 +318,22 @@ void hrt_read_application_info(hrt_travel_card_t *card, bytearray_t data) { card->application_instance_id = convert_get_hex_string(tmp, sizeof(tmp)); - uint8_t last_byte = data.data[10]; + uint8_t last_byte = data[10]; card->platform_type = last_byte & 0xE0; card->security_level = last_byte & 0x10; } -void hrt_read_control_info(hrt_travel_card_t *card, bytearray_t data) { - if (!card || !data.data) return; - card->app_status = convert_get_byte_value(data.data, data.length, 14, 1); +void hrt_read_control_info(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data) return; + card->app_status = convert_get_byte_value(data, data_len, 14, 1); } -void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { +void hrt_read_period_pass(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { // TODO: This function contains lots of magic numbers // and bad variable names. Refactoring is recommended. - if (!card || !data.data || data.length < 32) return; + if (!card || !data || data_len < HRT_PERIOD_PASS_LEN) return; - const uint8_t *bytes = data.data; + const uint8_t *bytes = data; // ---- Product 1 ---- // TODO: Better var name for i @@ -439,19 +462,17 @@ void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data) { card->boarding_area = (bytes[31] & 0xF0) >> 4; } -void hrt_read_period_pass_v2(hrt_travel_card_t *card, bytearray_t data) { - if (!card || !data.data) return; - - const uint8_t *bytes = data.data; +void hrt_read_period_pass_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || data_len < HRT_PERIOD_PASS_V2_LEN) return; // ---- Product 1 ---- - card->product_code_type1 = convert_get_byte_value(bytes, data.length, 0, 1); - card->product_code1 = convert_get_short_value(bytes, data.length, 1, 14); - card->validity_area_type1 = convert_get_byte_value(bytes, data.length, 15, 2); - card->validity_area1 = convert_get_short_value(bytes, data.length, 17, 6); + card->product_code_type1 = convert_get_byte_value(data, data_len, 0, 1); + card->product_code1 = convert_get_short_value(data, data_len, 1, 14); + card->validity_area_type1 = convert_get_byte_value(data, data_len, 15, 2); + card->validity_area1 = convert_get_short_value(data, data_len, 17, 6); - int start1 = convert_get_short_value(bytes, data.length, 23, 14); - int end1 = convert_get_short_value(bytes, data.length, 37, 14); + int start1 = convert_get_short_value(data, data_len, 23, 14); + int end1 = convert_get_short_value(data, data_len, 37, 14); card->period_start_date1 = en5145_date_to_time(start1); card->period_end_date1 = en5145_date_to_time(end1); @@ -460,58 +481,56 @@ void hrt_read_period_pass_v2(hrt_travel_card_t *card, bytearray_t data) { // ---- Product 2 ---- // In the original decompiled code, product_code_type1 is reassigned from bit offset 56. // This should probably be product_code_type2 instead as below - card->product_code_type2 = convert_get_byte_value(bytes, data.length, 56, 1); - card->product_code2 = convert_get_short_value(bytes, data.length, 57, 14); - card->validity_area_type2 = convert_get_byte_value(bytes, data.length, 71, 2); - card->validity_area2 = convert_get_short_value(bytes, data.length, 73, 6); + card->product_code_type2 = convert_get_byte_value(data, data_len, 56, 1); + card->product_code2 = convert_get_short_value(data, data_len, 57, 14); + card->validity_area_type2 = convert_get_byte_value(data, data_len, 71, 2); + card->validity_area2 = convert_get_short_value(data, data_len, 73, 6); - int start2 = convert_get_short_value(bytes, data.length, 79, 14); - int end2 = convert_get_short_value(bytes, data.length, 93, 14); + int start2 = convert_get_short_value(data, data_len, 79, 14); + int end2 = convert_get_short_value(data, data_len, 93, 14); card->period_start_date2 = en5145_date_to_time(start2); card->period_end_date2 = en5145_date_to_time(end2); card->period_length2 = (end2 - start2) + 1; // ---- Loaded period ---- - card->loaded_period_product_type = convert_get_byte_value(bytes, data.length, 112, 1); - card->loaded_period_product = convert_get_short_value(bytes, data.length, 113, 14); + card->loaded_period_product_type = convert_get_byte_value(data, data_len, 112, 1); + card->loaded_period_product = convert_get_short_value(data, data_len, 113, 14); card->period_loading_date = en5145_datetime_to_time( - convert_get_short_value(bytes, data.length, 127, 14), - convert_get_short_value(bytes, data.length, 141, 11) + convert_get_short_value(data, data_len, 127, 14), + convert_get_short_value(data, data_len, 141, 11) ); - card->loaded_period_length = convert_get_short_value(bytes, data.length, 152, 9); - card->loaded_period_price = convert_get_short_value(bytes, data.length, 161, 20); - card->period_loading_organization = convert_get_short_value(bytes, data.length, 181, 14); - card->period_loading_device_number = convert_get_short_value(bytes, data.length, 195, 13); + card->loaded_period_length = convert_get_short_value(data, data_len, 152, 9); + card->loaded_period_price = convert_get_short_value(data, data_len, 161, 20); + card->period_loading_organization = convert_get_short_value(data, data_len, 181, 14); + card->period_loading_device_number = convert_get_short_value(data, data_len, 195, 13); // ---- Boarding ---- card->boarding_date = en5145_datetime_to_time( - convert_get_short_value(bytes, data.length, 208, 14), - convert_get_short_value(bytes, data.length, 222, 11) + convert_get_short_value(data, data_len, 208, 14), + convert_get_short_value(data, data_len, 222, 11) ); - card->boarding_vehicle = convert_get_short_value(bytes, data.length, 233, 14); - card->boarding_location_num_type = convert_get_short_value(bytes, data.length, 247, 2); - card->boarding_location_num = convert_get_short_value(bytes, data.length, 249, 14); - card->boarding_direction = convert_get_byte_value(bytes, data.length, 263, 1); - card->boarding_area_type = convert_get_byte_value(bytes, data.length, 264, 2); - card->boarding_area = convert_get_byte_value(bytes, data.length, 266, 6); + card->boarding_vehicle = convert_get_short_value(data, data_len, 233, 14); + card->boarding_location_num_type = convert_get_short_value(data, data_len, 247, 2); + card->boarding_location_num = convert_get_short_value(data, data_len, 249, 14); + card->boarding_direction = convert_get_byte_value(data, data_len, 263, 1); + card->boarding_area_type = convert_get_byte_value(data, data_len, 264, 2); + card->boarding_area = convert_get_byte_value(data, data_len, 266, 6); } -void hrt_read_stored_value(hrt_travel_card_t *card, bytearray_t data) { - if (!card || !data.data || data.length < 3) return; - - const uint8_t *bytes = data.data; +void hrt_read_stored_value(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || data_len < 3) return; card->stored_value_counter = - ((uint32_t)(bytes[2] & 0xF0) >> 4) | - ((uint32_t)bytes[0] << 12) | - ((uint32_t)bytes[1] << 4); + ((uint32_t)(data[2] & 0xF0) >> 4) | + ((uint32_t)data[0] << 12) | + ((uint32_t)data[1] << 4); } -// void readHistory(bytearray_t data, size_t length); -// void readHistory_v2(bytearray_t data, size_t length); +// void readHistory(const uint8_t *data, size_t length); +// void readHistory_v2(const uint8_t *data, size_t length); void hrt_history_init(hrt_history_t *history) { if (!history) return; @@ -544,7 +563,7 @@ void hrt_history_set_transfer_end_date(hrt_history_t *history, time_t datetime) } // TODO: Implement value tickets -// HRT_eTicket *eticket_create(bytearray_t data, int is_encrypted, int version); +// HRT_eTicket *eticket_create(const uint8_t *data, size_t data_len, int is_encrypted, int version); // Getter methods for API simplicity int hrt_travelcard_get_application_version(const hrt_travel_card_t *card) { @@ -703,14 +722,15 @@ int hrt_travelcard_get_version(const hrt_travel_card_t *card) { return card ? card->version : 0; } +// Setter methods for API simplicity bool hrt_travelcard_set_application_info(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { if (!card || !buf || len == 0) return false; if (card->version == 2) { - return hrt_copy_and_parse(&card->application_information_data_v2, buf, len, 11, + return hrt_copy_and_parse(card->application_information_data_v2, buf, len, HRT_APPLICATION_INFORMATION_LEN, hrt_read_application_info, card); } - return hrt_copy_and_parse(&card->application_information_data, buf, len, 11, + return hrt_copy_and_parse(card->application_information_data, buf, len, HRT_APPLICATION_INFORMATION_LEN, hrt_read_application_info, card); } @@ -718,10 +738,10 @@ bool hrt_travelcard_set_control_info(hrt_travel_card_t *card, const uint8_t *buf if (!card || !buf || len == 0) return false; if (card->version == 2) { - return hrt_copy_and_parse(&card->control_information_data_v2, buf, len, 10, + return hrt_copy_and_parse(card->control_information_data_v2, buf, len, HRT_CONTROL_INFORMATION_V2_LEN, hrt_read_control_info, card); } - return hrt_copy_and_parse(&card->control_information_data, buf, len, 6, + return hrt_copy_and_parse(card->control_information_data, buf, len, HRT_CONTROL_INFORMATION_LEN, hrt_read_control_info, card); } @@ -729,10 +749,10 @@ bool hrt_travelcard_set_period_pass(hrt_travel_card_t *card, const uint8_t *buf, if (!card || !buf || len == 0) return false; if (card->version == 2) { - return hrt_copy_and_parse(&card->period_pass_data_v2, buf, len, 35, + return hrt_copy_and_parse(card->period_pass_data_v2, buf, len, HRT_PERIOD_PASS_V2_LEN, hrt_read_period_pass_v2, card); } - return hrt_copy_and_parse(&card->period_pass_data, buf, len, 32, + return hrt_copy_and_parse(card->period_pass_data, buf, len, HRT_PERIOD_PASS_LEN, hrt_read_period_pass, card); } @@ -740,10 +760,10 @@ bool hrt_travelcard_set_stored_value(hrt_travel_card_t *card, const uint8_t *buf if (!card || !buf || len == 0) return false; if (card->version == 2) { - return hrt_copy_and_parse(&card->stored_value_data_v2, buf, len, 13, + return hrt_copy_and_parse(card->stored_value_data_v2, buf, len, HRT_STORED_VALUE_V2_LEN, hrt_read_stored_value, card); } - return hrt_copy_and_parse(&card->stored_value_data, buf, len, 12, + return hrt_copy_and_parse(card->stored_value_data, buf, len, HRT_STORED_VALUE_LEN, hrt_read_stored_value, card); } @@ -751,13 +771,13 @@ bool hrt_travelcard_set_eticket(hrt_travel_card_t *card, const uint8_t *buf, siz if (!card || !buf || len == 0) return false; if (card->version == 2) { - if (len < 45 || card->eticket_data_v2.length < 45) return false; - memcpy(card->eticket_data_v2.data, buf, 45); + if (!card->eticket_data_v2 || len < HRT_ETICKET_V2_LEN) return false; + memcpy(card->eticket_data_v2, buf, HRT_ETICKET_V2_LEN); return true; } - if (len < 26 || card->eticket_data.length < 26) return false; - memcpy(card->eticket_data.data, buf, 26); + if (!card->eticket_data || len < HRT_ETICKET_LEN) return false; + memcpy(card->eticket_data, buf, HRT_ETICKET_LEN); return true; } @@ -765,14 +785,16 @@ bool hrt_travelcard_set_history(hrt_travel_card_t *card, const uint8_t *buf, siz if (!card || !buf || len == 0) return false; if (card->version == 2) { - if (len > card->history_data_v2.length) len = card->history_data_v2.length; - memcpy(card->history_data_v2.data, buf, len); + if (!card->history_data_v2) return false; + if (len > HRT_HISTORY_LEN) len = HRT_HISTORY_LEN; + memcpy(card->history_data_v2, buf, len); // TODO: readHistory_v2(card->historyData_v2, len); return true; } - if (len > card->history_data.length) len = card->history_data.length; - memcpy(card->history_data.data, buf, len); + if (!card->history_data) return false; + if (len > HRT_HISTORY_LEN) len = HRT_HISTORY_LEN; + memcpy(card->history_data, buf, len); // TODO: readHistory(card->historyData, len); return true; } diff --git a/client/src/cmdparsehrt.h b/client/src/cmdparsehrt.h index b6b684a86..cffdab461 100644 --- a/client/src/cmdparsehrt.h +++ b/client/src/cmdparsehrt.h @@ -20,6 +20,7 @@ #define CMDParseHRT_H__ #include "common.h" +#include #include #include @@ -31,16 +32,10 @@ #define MSB(b) ((b) & 0x80) #define LSB7(b) ((b) & 0x7F) -#define SAFE_FREE(ptr) do { free(ptr); (ptr) = NULL; } while (0) // TODO: Implement typedef struct hrt_eticket_t hrt_eticket_t; -typedef struct { - uint8_t *data; - size_t length; -} PACKED bytearray_t; - typedef struct { int group_size; int price; @@ -55,14 +50,14 @@ typedef struct { int error_status; // ---- Application Information ---- - bytearray_t application_information_data; - bytearray_t application_information_data_v2; - char *application_instance_id; - int application_key_version; - int application_version; - int version; - int security_level; - int platform_type; + uint8_t *application_information_data; + uint8_t *application_information_data_v2; + char *application_instance_id; + int application_key_version; + int application_version; + int version; + int security_level; + int platform_type; // ---- Boarding Information ---- int boarding_area; @@ -75,20 +70,20 @@ typedef struct { time_t last_boarding_date_time; // ---- Control / Ticket data ---- - bytearray_t control_information_data; - bytearray_t control_information_data_v2; - bytearray_t eticket_data; - bytearray_t eticket_data_v2; + uint8_t *control_information_data; + uint8_t *control_information_data_v2; + uint8_t *eticket_data; + uint8_t *eticket_data_v2; // ---- History ---- - bytearray_t history_data; - bytearray_t history_data_v2; + uint8_t *history_data; + uint8_t *history_data_v2; hrt_history_t *history_fields; int history_len; // ---- Period Pass data ---- - bytearray_t period_pass_data; - bytearray_t period_pass_data_v2; + uint8_t *period_pass_data; + uint8_t *period_pass_data_v2; time_t period_loading_date; int period_loading_device_number; @@ -116,8 +111,8 @@ typedef struct { int loaded_period_product_type; // ---- Stored Value ---- - bytearray_t stored_value_data; - bytearray_t stored_value_data_v2; + uint8_t *stored_value_data; + uint8_t *stored_value_data_v2; int stored_value_counter; // ---- Value Ticket ---- @@ -169,13 +164,13 @@ void hrt_history_set_price(hrt_history_t *history, int price); void hrt_history_set_transfer_end_date(hrt_history_t *history, time_t datetime); // Parsing Functions -void hrt_read_application_info(hrt_travel_card_t *card, bytearray_t data); -void hrt_read_control_info(hrt_travel_card_t *card, bytearray_t data); -void hrt_read_period_pass(hrt_travel_card_t *card, bytearray_t data); -void hrt_read_period_pass_v2(hrt_travel_card_t *card, bytearray_t data); -void hrt_read_stored_value(hrt_travel_card_t *card, bytearray_t data); -void hrt_read_history(bytearray_t data, size_t length); -void hrt_read_history_v2(bytearray_t data, size_t length); +void hrt_read_application_info(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_control_info(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_period_pass(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_period_pass_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_stored_value(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_history(const uint8_t *data, size_t length); +void hrt_read_history_v2(const uint8_t *data, size_t length); // Getter Functions int hrt_travelcard_get_application_version(const hrt_travel_card_t *card); From 4ff1a5028833af0dc12a930d9052054c2ef19b33 Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sat, 20 Jun 2026 23:53:13 +0300 Subject: [PATCH 06/13] Moved HRT parser logic under client/src/parsers + Restructured parser calling code to its own file that is hooked from `hf mfdes info` command --- client/CMakeLists.txt | 3 +- client/Makefile | 3 +- client/experimental_lib/CMakeLists.txt | 3 +- client/src/cmdhfmfdes.c | 76 +--------- .../hrtparser/hrtparser.c} | 4 +- .../hrtparser/hrtparser.h} | 6 +- client/src/parsers/parsehrt.c | 135 ++++++++++++++++++ client/src/parsers/parsehrt.h | 11 ++ 8 files changed, 161 insertions(+), 80 deletions(-) rename client/src/{cmdparsehrt.c => parsers/hrtparser/hrtparser.c} (99%) rename client/src/{cmdparsehrt.h => parsers/hrtparser/hrtparser.h} (99%) create mode 100644 client/src/parsers/parsehrt.c create mode 100644 client/src/parsers/parsehrt.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a08ee88ec..409a0cf2e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -468,7 +468,6 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdmain.c ${PM3_ROOT}/client/src/cmdmqtt.c ${PM3_ROOT}/client/src/cmdnfc.c - ${PM3_ROOT}/client/src/cmdparsehrt.c ${PM3_ROOT}/client/src/cmdparser.c ${PM3_ROOT}/client/src/cmdpiv.c ${PM3_ROOT}/client/src/cmdscript.c @@ -484,6 +483,8 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/iso4217.c ${PM3_ROOT}/client/src/jansson_path.c ${PM3_ROOT}/client/src/lua_bitlib.c + ${PM3_ROOT}/client/src/parsers/parsehrt.c + ${PM3_ROOT}/client/src/parsers/hrtparser/hrtparser.c ${PM3_ROOT}/client/src/pla.c ${PM3_ROOT}/client/src/preferences.c ${PM3_ROOT}/client/src/pm3.c diff --git a/client/Makefile b/client/Makefile index f511239ca..48e291fcd 100644 --- a/client/Makefile +++ b/client/Makefile @@ -809,7 +809,6 @@ SRCS = mifare/aiddesfire.c \ cmdmain.c \ cmdmqtt.c \ cmdnfc.c \ - cmdparsehrt.c \ cmdparser.c \ cmdpiv.c \ cmdscript.c \ @@ -883,6 +882,8 @@ SRCS = mifare/aiddesfire.c \ mifare/prime.c \ mifare/gen4.c \ nfc/ndef.c \ + parsers/parsehrt.c \ + parsers/hrtparser/hrtparser.c \ pm3.c \ pm3_binlib.c \ pm3_bitlib.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 171204011..f3cda3e7b 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -388,7 +388,6 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdmain.c ${PM3_ROOT}/client/src/cmdmqtt.c ${PM3_ROOT}/client/src/cmdnfc.c - ${PM3_ROOT}/client/src/cmdparsehrt.c ${PM3_ROOT}/client/src/cmdparser.c ${PM3_ROOT}/client/src/cmdpiv.c ${PM3_ROOT}/client/src/cmdscript.c @@ -404,6 +403,8 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/iso4217.c ${PM3_ROOT}/client/src/jansson_path.c ${PM3_ROOT}/client/src/lua_bitlib.c + ${PM3_ROOT}/client/src/parsers/parsehrt.c + ${PM3_ROOT}/client/src/parsers/hrtparser/hrtparser.c ${PM3_ROOT}/client/src/pla.c ${PM3_ROOT}/client/src/preferences.c ${PM3_ROOT}/client/src/pm3.c diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 2ded59c41..3d96e192f 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -24,7 +24,6 @@ #include #include "jansson.h" #include "commonutil.h" // ARRAYLEN -#include "cmdparsehrt.h" #include "cmdparser.h" // command_t #include "comms.h" #include "mifare/desfirecrypto.h" @@ -35,6 +34,7 @@ #include "crypto/libpcrypto.h" #include "crypto/duoxcrypto.h" #include "protocols.h" +#include "parsers/parsehrt.h" #include "cmdtrace.h" #include "cliparser.h" #include "iso7816/apduinfo.h" // APDU manipulation / errorcodes @@ -420,76 +420,6 @@ static const char *getProductTypeStr(const uint8_t *versionhw) { return "UNKNOWN PROD"; } -static bool hf_mfdes_try_parse_hrt(DesfireContext_t *dctx) { - DesfireSetCommMode(dctx, DCMPlain); - DesfireSetCommandSet(dctx, DCCNative); - - // TODO: Add support for older travelcard (AID EF2011) - - if (DesfireSelectAIDHex(dctx, 0xEF2014, false, 0) == PM3_SUCCESS) { - uint8_t buf[APDU_RES_LEN] = {0}; - size_t len = 0; - - hrt_travel_card_t card; - hrt_travelcard_init_empty(&card, 2); - - // File 0x08: Application info (11 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x08, 0, 11, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_application_info(&card, buf, len); - } - - // Check that application info parses before reading other files - const char *application_id = hrt_travelcard_get_application_instance_id(&card); - if (application_id == NULL || application_id[0] == '\0') { - hrt_travelcard_free(&card); - return false; - } - PrintAndLogEx(SUCCESS, "HRT travel card detected (AID EF2014)"); - - // File 0x00: Control info (10 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x00, 0, 10, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_control_info(&card, buf, len); - } - - // File 0x01: Period pass (35 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x01, 0, 35, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_period_pass(&card, buf, len); - } - - // File 0x02: Stored value (13 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x02, 0, 13, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_stored_value(&card, buf, len); - - // Demo: Read card stored value - int stored_value = hrt_travelcard_get_stored_value_counter(&card); - char price_buf[32] = {0}; - hrt_price_to_string(stored_value, price_buf, sizeof(price_buf)); - PrintAndLogEx(SUCCESS, "HRT stored value: %s", price_buf); - } - - // File 0x03: eTicket (45 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x03, 0, 45, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_eticket(&card, buf, len); - } - - // File 0x04: History records (all) - len = 0; - if (DesfireReadRecords(dctx, 0x04, 0, 0, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_history(&card, buf, len); - } - - hrt_travelcard_free(&card); - return true; - } - - return false; -} - int mfdes_get_info(mfdes_info_res_t *info) { PacketResponseNG resp; @@ -1202,7 +1132,9 @@ static int CmdHF14ADesInfo(const char *Cmd) { */ - (void)hf_mfdes_try_parse_hrt(&dctx); + if (is_valid_hrt_card(&dctx, aidbuf, aidbuflen)) { + (void)hrt_parser_parse(&dctx); + } DropField(); return PM3_SUCCESS; diff --git a/client/src/cmdparsehrt.c b/client/src/parsers/hrtparser/hrtparser.c similarity index 99% rename from client/src/cmdparsehrt.c rename to client/src/parsers/hrtparser/hrtparser.c index 135fe019e..7161f40da 100644 --- a/client/src/cmdparsehrt.c +++ b/client/src/parsers/hrtparser/hrtparser.c @@ -23,8 +23,8 @@ #endif #include "cmdparser.h" -#include "cmdparsehrt.h" #include "comms.h" +#include "hrtparser.h" #include "common.h" #include "ui.h" #include @@ -812,7 +812,7 @@ static int CmdHelp(const char *Cmd) { return PM3_SUCCESS; } -int CmdParseHRT(const char *Cmd) { +int HRTParser(const char *Cmd) { clearCommandBuffer(); return CmdsParse(CommandTable, Cmd); } diff --git a/client/src/cmdparsehrt.h b/client/src/parsers/hrtparser/hrtparser.h similarity index 99% rename from client/src/cmdparsehrt.h rename to client/src/parsers/hrtparser/hrtparser.h index cffdab461..89b489d26 100644 --- a/client/src/cmdparsehrt.h +++ b/client/src/parsers/hrtparser/hrtparser.h @@ -16,8 +16,8 @@ // Parse Helsinki Region Transport (HRT) travel cards //----------------------------------------------------------------------------- -#ifndef CMDParseHRT_H__ -#define CMDParseHRT_H__ +#ifndef HRTPARSER_H__ +#define HRTPARSER_H__ #include "common.h" #include @@ -221,7 +221,7 @@ int hrt_travelcard_get_period_loading_device_number(const hrt_travel_card int hrt_travelcard_get_app_status(const hrt_travel_card_t *card); int hrt_travelcard_get_version(const hrt_travel_card_t *card); -int CmdParseHRT(const char *Cmd); +int HRTParser(const char *Cmd); // Setter Functions void hrt_travelcard_init_empty(hrt_travel_card_t *card, int version); diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c new file mode 100644 index 000000000..56388db23 --- /dev/null +++ b/client/src/parsers/parsehrt.c @@ -0,0 +1,135 @@ +#include "hrtparser/hrtparser.h" +#include "iso7816/iso7816core.h" +#include "mifare/desfirecore.h" +#include "mifare/desfirecrypto.h" +#include "parsehrt.h" +#include "pm3_cmd.h" +#include "ui.h" + +#define HRT_NEW_AID 0xEF2014 +#define HRT_OLD_AID 0xEF2011 +#define HRT_APPLICATION_INFO_FILE_ID 0x08 +#define HRT_APPLICATION_INFO_LEN 11 + +static bool hrt_aid_list_contains(const uint8_t *aidbuf, size_t aidbuflen) { + if (aidbuf == NULL || aidbuflen < 3) return false; + + for (size_t i = 0; i + 2 < aidbuflen; i += 3) { + // TODO: Also add HRT_OLD_AID check. Currently it is not + // checked since parsing the older cards is not yet implemented. + if (DesfireAIDByteToUint(&aidbuf[i]) == HRT_NEW_AID) { + return true; + } + } + + return false; +} + +static bool hrt_read_and_parse_application_info(DesfireContext_t *dctx, hrt_travel_card_t *card) { + if (dctx == NULL || card == NULL) return false; + + uint8_t buf[APDU_RES_LEN] = {0}; + size_t len = 0; + + if (DesfireReadFile(dctx, HRT_APPLICATION_INFO_FILE_ID, 0, HRT_APPLICATION_INFO_LEN, buf, &len) != PM3_SUCCESS) { + return false; + } + + if (hrt_travelcard_set_application_info(card, buf, len) == false) { + return false; + } + + const char *application_id = hrt_travelcard_get_application_instance_id(card); + return application_id != NULL && application_id[0] != '\0'; +} + +bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen) { + if (dctx == NULL) return false; + + if (hrt_aid_list_contains(aidbuf, aidbuflen) == false) { + return false; + } + + DesfireSetCommMode(dctx, DCMPlain); + DesfireSetCommandSet(dctx, DCCNative); + + // TODO: Add support for older travelcard (AID EF2011) + + if (DesfireSelectAIDHex(dctx, HRT_NEW_AID, false, 0) != PM3_SUCCESS) { + return false; + } + + hrt_travel_card_t card; + hrt_travelcard_init_empty(&card, 2); + + bool is_valid = hrt_read_and_parse_application_info(dctx, &card); + + hrt_travelcard_free(&card); + return is_valid; +} + +bool hrt_parser_parse(DesfireContext_t *dctx) { + if (dctx == NULL) return false; + + DesfireSetCommMode(dctx, DCMPlain); + DesfireSetCommandSet(dctx, DCCNative); + + // TODO: Add support for older travelcard (AID EF2011) + + if (DesfireSelectAIDHex(dctx, HRT_NEW_AID, false, 0) == PM3_SUCCESS) { + uint8_t buf[APDU_RES_LEN] = {0}; + size_t len = 0; + + hrt_travel_card_t card; + hrt_travelcard_init_empty(&card, 2); + + // File 0x08: Application info (11 bytes) + // Check that application info parses before reading other files + if (hrt_read_and_parse_application_info(dctx, &card) == false) { + hrt_travelcard_free(&card); + return false; + } + PrintAndLogEx(SUCCESS, "HRT travel card detected (AID %06X)", HRT_NEW_AID); + + // File 0x00: Control info (10 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x00, 0, 10, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_control_info(&card, buf, len); + } + + // File 0x01: Period pass (35 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x01, 0, 35, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_period_pass(&card, buf, len); + } + + // File 0x02: Stored value (13 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x02, 0, 13, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_stored_value(&card, buf, len); + + // Demo: Read card stored value + int stored_value = hrt_travelcard_get_stored_value_counter(&card); + char price_buf[32] = {0}; + hrt_price_to_string(stored_value, price_buf, sizeof(price_buf)); + PrintAndLogEx(SUCCESS, "HRT stored value: %s", price_buf); + } + + // File 0x03: eTicket (45 bytes) + len = 0; + if (DesfireReadFile(dctx, 0x03, 0, 45, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_eticket(&card, buf, len); + } + + // File 0x04: History records (all) + len = 0; + if (DesfireReadRecords(dctx, 0x04, 0, 0, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_history(&card, buf, len); + } + + hrt_travelcard_free(&card); + return true; + } + + return false; +} diff --git a/client/src/parsers/parsehrt.h b/client/src/parsers/parsehrt.h new file mode 100644 index 000000000..09b8d75a6 --- /dev/null +++ b/client/src/parsers/parsehrt.h @@ -0,0 +1,11 @@ +#ifndef PARSEHRT_H__ +#define PARSEHRT_H__ + +#include "mifare/desfirecore.h" +#include +#include + +bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen); +bool hrt_parser_parse(DesfireContext_t *dctx); + +#endif From 1afa1028e429fabf957f7cff850a87daa977e829 Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 11:20:01 +0300 Subject: [PATCH 07/13] Added pretty-printing HRT card information --- client/src/parsers/parsehrt.c | 187 ++++++++++++++++++++++++++++++++-- 1 file changed, 180 insertions(+), 7 deletions(-) diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c index 56388db23..cdb543875 100644 --- a/client/src/parsers/parsehrt.c +++ b/client/src/parsers/parsehrt.c @@ -1,3 +1,4 @@ +#include "commonutil.h" #include "hrtparser/hrtparser.h" #include "iso7816/iso7816core.h" #include "mifare/desfirecore.h" @@ -5,12 +6,122 @@ #include "parsehrt.h" #include "pm3_cmd.h" #include "ui.h" +#include +#include #define HRT_NEW_AID 0xEF2014 #define HRT_OLD_AID 0xEF2011 #define HRT_APPLICATION_INFO_FILE_ID 0x08 #define HRT_APPLICATION_INFO_LEN 11 +typedef enum { + HRT_AREA_TYPE_CITY = 0, + HRT_AREA_TYPE_VEHICLE = 1, + HRT_AREA_TYPE_ZONE_RANGE = 2, +} hrt_area_type_t; + +typedef struct { + int code; + const char *name; + const char *arc_zone; +} hrt_area_mapping_t; + +static const hrt_area_mapping_t hrt_city_areas[] = { + {0, "Not defined", NULL}, + {1, "Helsinki", "AB"}, + {2, "Espoo", "BC"}, + {4, "Vantaa", "BC"}, + {5, "Region", "ABC"}, + {6, "Kirkkonummi-Siuntio", "D"}, + {7, "Vihti", NULL}, + {8, "Nurmijarvi", NULL}, + {9, "Kerava-Sipoo-Tuusula", "D"}, + {10, "Sipoo", NULL}, + {14, "Region zone 2", "BCD"}, + {15, "Region zone 3", "ABCD"}, +}; + +static const hrt_area_mapping_t hrt_vehicle_areas[] = { + {0, "Not defined", NULL}, + {1, "Bus", NULL}, + {5, "Tram", NULL}, + {6, "Metro", NULL}, + {7, "Train", NULL}, + {8, "Ferry", NULL}, + {9, "U line", NULL}, +}; + +static const char *hrt_lookup_product_name(int product_code) { + if (product_code < 800) return "Not defined"; + return NULL; +} + +static const hrt_area_mapping_t *hrt_lookup_area_mapping(const hrt_area_mapping_t *mappings, size_t mappings_len, int area) { + for (size_t i = 0; i < mappings_len; i++) { + if (mappings[i].code == area) { + return &mappings[i]; + } + } + return NULL; +} + +static bool hrt_format_zone_range(int area, char *out, size_t out_len) { + static const char zones[] = "ABCDEFGH"; + if (out == NULL || out_len == 0) return false; + + int end = area & 0x07; + int start = (area >> 3) & 0x07; + + if (start > end || start >= (int)strlen(zones) || end >= (int)strlen(zones)) return false; + + size_t pos = 0; + for (int i = start; i <= end; i++) { + if (pos + 1 >= out_len) return false; + out[pos++] = zones[i]; + } + + out[pos] = '\0'; + return true; +} + +static bool hrt_format_area(int area_type, int area, char *out, size_t out_len) { + if (out == NULL || out_len == 0) return false; + + const hrt_area_mapping_t *mapping = NULL; + switch ((hrt_area_type_t)area_type) { + case HRT_AREA_TYPE_CITY: + mapping = hrt_lookup_area_mapping(hrt_city_areas, ARRAYLEN(hrt_city_areas), area); + if (mapping == NULL) return false; + if (mapping->arc_zone != NULL) { + return snprintf(out, out_len, "%s (%s)", mapping->name, mapping->arc_zone) > 0; + } + return snprintf(out, out_len, "%s", mapping->name) > 0; + case HRT_AREA_TYPE_VEHICLE: + mapping = hrt_lookup_area_mapping(hrt_vehicle_areas, ARRAYLEN(hrt_vehicle_areas), area); + if (mapping == NULL) return false; + return snprintf(out, out_len, "%s", mapping->name) > 0; + case HRT_AREA_TYPE_ZONE_RANGE: + return hrt_format_zone_range(area, out, out_len); + default: + return false; + } +} + +static bool hrt_format_card_number(const char *card_number, char *out, size_t out_len) { + if (card_number == NULL || out == NULL || out_len == 0) return false; + + // Card number is printed on the physical card in groups of 6 + 4 + 4 + 4. + if (strlen(card_number) != 18 || out_len < 22) { + return false; + } + + int written = snprintf(out, out_len, "%.6s %.4s %.4s %.4s", + card_number, card_number + 6, + card_number + 10, card_number + 14); + + return written > 0 && (size_t)written < out_len; +} + static bool hrt_aid_list_contains(const uint8_t *aidbuf, size_t aidbuflen) { if (aidbuf == NULL || aidbuflen < 3) return false; @@ -43,6 +154,73 @@ static bool hrt_read_and_parse_application_info(DesfireContext_t *dctx, hrt_trav return application_id != NULL && application_id[0] != '\0'; } +static void hrt_print_time(const char *label, time_t value) { + if (value == (time_t)0) return; + + char buf[32] = {0}; + struct tm tm_value = {0}; + +#if defined(_WIN32) + if (localtime_s(&tm_value, &value) != 0) return; +#else + if (localtime_r(&value, &tm_value) == NULL) return; +#endif + + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value); + PrintAndLogEx(SUCCESS, "%s " _GREEN_("%s"), label, buf); +} + +static void hrt_print_card(const hrt_travel_card_t *card) { + if (card == NULL) return; + + const char *application_id = hrt_travelcard_get_application_instance_id(card); + char price_buf[32] = {0}; + char area_buf[64] = {0}; + int product_code = hrt_travelcard_get_product_code1(card); + const char *product_name = hrt_lookup_product_name(product_code); + int area_type = hrt_travelcard_get_validity_area_type1(card); + int area = hrt_travelcard_get_validity_area1(card); + + hrt_price_to_string( + hrt_travelcard_get_stored_value_counter(card), + price_buf, + sizeof(price_buf) + ); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "--- " _CYAN_("HRT travel card")); + + if (application_id != NULL && application_id[0] != '\0') { + char card_number[22] = {0}; + if (hrt_format_card_number(application_id, card_number, sizeof(card_number))) { + PrintAndLogEx(SUCCESS, "Card number......... " _GREEN_("%s"), card_number); + } else { + PrintAndLogEx(SUCCESS, "Card number......... " _GREEN_("%s"), application_id); + } + } + + PrintAndLogEx(SUCCESS, "Value on the card... " _GREEN_("%s"), price_buf); + + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "Season ticket information"); + + if (product_name != NULL) { + PrintAndLogEx(SUCCESS, " Product code........... " _GREEN_("%d (%s)"), product_code, product_name); + } else { + PrintAndLogEx(SUCCESS, " Product code........... " _GREEN_("%d (unknown)"), product_code); + } + if (hrt_format_area(area_type, area, area_buf, sizeof(area_buf))) { + PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%s"), area_buf); + } else { + PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%d (type %d)"), area, area_type); + } + hrt_print_time(" Start date.............", hrt_travelcard_get_period_start_date1(card)); + hrt_print_time(" End date...............", hrt_travelcard_get_period_end_date1(card)); + PrintAndLogEx(SUCCESS, " Season ticket length... " _GREEN_("%d days"), hrt_travelcard_get_period_length1(card)); + + // TODO: Print value ticket information +} + bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen) { if (dctx == NULL) return false; @@ -89,7 +267,6 @@ bool hrt_parser_parse(DesfireContext_t *dctx) { hrt_travelcard_free(&card); return false; } - PrintAndLogEx(SUCCESS, "HRT travel card detected (AID %06X)", HRT_NEW_AID); // File 0x00: Control info (10 bytes) len = 0; @@ -107,12 +284,6 @@ bool hrt_parser_parse(DesfireContext_t *dctx) { len = 0; if (DesfireReadFile(dctx, 0x02, 0, 13, buf, &len) == PM3_SUCCESS) { hrt_travelcard_set_stored_value(&card, buf, len); - - // Demo: Read card stored value - int stored_value = hrt_travelcard_get_stored_value_counter(&card); - char price_buf[32] = {0}; - hrt_price_to_string(stored_value, price_buf, sizeof(price_buf)); - PrintAndLogEx(SUCCESS, "HRT stored value: %s", price_buf); } // File 0x03: eTicket (45 bytes) @@ -127,6 +298,8 @@ bool hrt_parser_parse(DesfireContext_t *dctx) { hrt_travelcard_set_history(&card, buf, len); } + hrt_print_card(&card); + hrt_travelcard_free(&card); return true; } From cb1c68e610522488fde86343f449cb70707e3dda Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 13:00:06 +0300 Subject: [PATCH 08/13] Refactored hrtparser.c with better variable names to improve readability --- client/src/parsers/hrtparser/hrtparser.c | 111 +++++++++++------------ 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/client/src/parsers/hrtparser/hrtparser.c b/client/src/parsers/hrtparser/hrtparser.c index 7161f40da..d75f3e8b5 100644 --- a/client/src/parsers/hrtparser/hrtparser.c +++ b/client/src/parsers/hrtparser/hrtparser.c @@ -329,136 +329,131 @@ void hrt_read_control_info(hrt_travel_card_t *card, const uint8_t *data, size_t } void hrt_read_period_pass(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { - // TODO: This function contains lots of magic numbers - // and bad variable names. Refactoring is recommended. if (!card || !data || data_len < HRT_PERIOD_PASS_LEN) return; const uint8_t *bytes = data; // ---- Product 1 ---- - // TODO: Better var name for i - uint16_t i = ((uint16_t)bytes[0]) << 6; - uint8_t byte1 = bytes[1]; + uint16_t product1_prefix = ((uint16_t)bytes[0]) << 6; + uint8_t product1_area_byte = bytes[1]; - card->product_code1 = (int16_t)(i | ((byte1 & 0xFC) >> 2)); - card->validity_area_type1 = (byte1 & 0x02) >> 1; + card->product_code1 = (int16_t)(product1_prefix | ((product1_area_byte & 0xFC) >> 2)); + card->validity_area_type1 = (product1_area_byte & 0x02) >> 1; - uint8_t byte2 = bytes[2]; + uint8_t product1_start_date_byte = bytes[2]; card->validity_area1 = - (uint8_t)(((byte1 & 0x01) << 3) | ((byte2 & 0xE0) >> 5)); + (uint8_t)(((product1_area_byte & 0x01) << 3) | ((product1_start_date_byte & 0xE0) >> 5)); - // TODO: Better var name for s - int16_t s = - (int16_t)(((byte2 & 0x1F) << 9) | + int16_t period1_start_days = + (int16_t)(((product1_start_date_byte & 0x1F) << 9) | ((uint16_t)bytes[3] << 1) | (MSB(bytes[4]) >> 7)); - card->period_start_date1 = en5145_date_to_time(s); + card->period_start_date1 = en5145_date_to_time(period1_start_days); - // TODO: Better var name for s2 - int16_t s2 = + int16_t period1_end_days = (int16_t)((LSB7(bytes[4]) << 7) | ((bytes[5] & 0xFE) >> 1)); - card->period_end_date1 = en5145_date_to_time(s2); + card->period_end_date1 = en5145_date_to_time(period1_end_days); card->period_end_date1 += 86399; // + 23:59:59 - card->period_length1 = (s2 - s) + 1; + card->period_length1 = (period1_end_days - period1_start_days) + 1; // ---- Product 2 ---- - uint16_t i2 = ((uint16_t)bytes[6]) << 8; - uint8_t b3 = bytes[7]; + uint16_t product2_prefix = ((uint16_t)bytes[6]) << 8; + uint8_t product2_area_byte = bytes[7]; - card->product_code2 = (int16_t)((i2 | (b3 & 0xFC)) >> 2); - card->validity_area_type2 = b3 & 0x02; + card->product_code2 = (int16_t)((product2_prefix | (product2_area_byte & 0xFC)) >> 2); + card->validity_area_type2 = product2_area_byte & 0x02; - uint8_t b4 = bytes[8]; + uint8_t product2_start_date_byte = bytes[8]; card->validity_area2 = - (uint8_t)(((b3 & 0x01) << 3) | ((b4 & 0xE0) >> 5)); + (uint8_t)(((product2_area_byte & 0x01) << 3) | ((product2_start_date_byte & 0xE0) >> 5)); - int16_t s3 = - (int16_t)(((b4 & 0x1F) << 9) | + int16_t period2_start_days = + (int16_t)(((product2_start_date_byte & 0x1F) << 9) | ((uint16_t)bytes[9] << 1) | ((bytes[10] & 0x80) >> 7)); - card->period_start_date2 = en5145_date_to_time(s3); + card->period_start_date2 = en5145_date_to_time(period2_start_days); - int16_t s4 = + int16_t period2_end_days = (int16_t)((LSB7(bytes[10]) << 7) | ((bytes[11] & 0xFE) >> 1)); - card->period_end_date2 = en5145_date_to_time(s4); + card->period_end_date2 = en5145_date_to_time(period2_end_days); card->period_end_date2 += 86399; // + 23:59:59 - card->period_length2 = (s4 - s3) + 1; + card->period_length2 = (period2_end_days - period2_start_days) + 1; // ---- Loaded period ---- - uint16_t i4 = ((uint16_t)bytes[12]) << 6; - uint8_t b5 = bytes[13]; + uint16_t loaded_product_prefix = ((uint16_t)bytes[12]) << 6; + uint8_t loaded_product_date_byte = bytes[13]; - card->loaded_period_product = (int16_t)(i4 | ((b5 & 0xFC) >> 2)); + card->loaded_period_product = (int16_t)(loaded_product_prefix | ((loaded_product_date_byte & 0xFC) >> 2)); - uint16_t i5 = - ((b5 & 0x03) << 12) | + uint16_t loading_date_prefix = + ((loaded_product_date_byte & 0x03) << 12) | ((uint16_t)bytes[14] << 4); - uint8_t b6 = bytes[15]; + uint8_t loading_date_time_byte = bytes[15]; card->period_loading_date = en5145_datetime_to_time( - (int16_t)(i5 | ((b6 & 0xF0) >> 4)), + (int16_t)(loading_date_prefix | ((loading_date_time_byte & 0xF0) >> 4)), (int16_t)(((bytes[16] & 0xFE) >> 1) | - ((b6 & 0x0F) << 7)) + ((loading_date_time_byte & 0x0F) << 7)) ); card->loaded_period_length = (int16_t)(((bytes[16] & 0x01) << 8) | bytes[17]); - uint32_t i6 = + uint32_t loaded_price_prefix = ((uint32_t)bytes[18] << 12) | ((uint32_t)bytes[19] << 4); - uint8_t b7 = bytes[20]; - card->loaded_period_price = i6 | ((b7 & 0xF0) >> 4); + uint8_t loaded_price_org_byte = bytes[20]; + card->loaded_period_price = loaded_price_prefix | ((loaded_price_org_byte & 0xF0) >> 4); - uint8_t i7 = - ((b7 & 0x0F) << 10) | + uint8_t loading_org_prefix = + ((loaded_price_org_byte & 0x0F) << 10) | ((uint16_t)bytes[21] << 2); - uint8_t b8 = bytes[22]; + uint8_t loading_org_device_byte = bytes[22]; card->period_loading_organization = - (int16_t)(i7 | ((b8 & 0xC0) >> 6)); + (int16_t)(loading_org_prefix | ((loading_org_device_byte & 0xC0) >> 6)); card->period_loading_device_number = - (int16_t)(((b8 & 0x3F) << 8) | bytes[23]); + (int16_t)(((loading_org_device_byte & 0x3F) << 8) | bytes[23]); // ---- Boarding ---- - uint16_t i8 = ((uint16_t)bytes[24]) << 6; - uint8_t b9 = bytes[25]; + uint16_t boarding_date_prefix = ((uint16_t)bytes[24]) << 6; + uint8_t boarding_date_time_byte = bytes[25]; card->boarding_date = en5145_datetime_to_time( - (int16_t)(i8 | ((b9 & 0xFC) >> 2)), - (int16_t)(((b9 & 0x03) << 9) | + (int16_t)(boarding_date_prefix | ((boarding_date_time_byte & 0xFC) >> 2)), + (int16_t)(((boarding_date_time_byte & 0x03) << 9) | ((uint16_t)bytes[26] << 1) | ((bytes[27] & 0x80) >> 7)) ); - uint16_t i9 = LSB7(bytes[27]) << 7; - uint8_t b10 = bytes[28]; + uint16_t boarding_vehicle_prefix = LSB7(bytes[27]) << 7; + uint8_t boarding_vehicle_location_type_byte = bytes[28]; card->boarding_vehicle = - (int16_t)(i9 | ((b10 & 0xFE) >> 1)); + (int16_t)(boarding_vehicle_prefix | ((boarding_vehicle_location_type_byte & 0xFE) >> 1)); - uint8_t b11 = bytes[29]; + uint8_t boarding_location_type_number_byte = bytes[29]; card->boarding_location_num_type = - (uint8_t)(((b10 & 0x01) << 1) | ((b11 & 0x80) >> 7)); + (uint8_t)(((boarding_vehicle_location_type_byte & 0x01) << 1) | ((boarding_location_type_number_byte & 0x80) >> 7)); - uint16_t i11 = LSB7(b11) << 7; - uint8_t b12 = bytes[30]; + uint16_t boarding_location_prefix = LSB7(boarding_location_type_number_byte) << 7; + uint8_t boarding_location_direction_byte = bytes[30]; card->boarding_location_num = - (int16_t)(i11 | ((b12 & 0xFE) >> 1)); + (int16_t)(boarding_location_prefix | ((boarding_location_direction_byte & 0xFE) >> 1)); - card->boarding_direction = b12 & 0x01; + card->boarding_direction = boarding_location_direction_byte & 0x01; card->boarding_area = (bytes[31] & 0xF0) >> 4; } From 25f19a02ce176b224775ed49bced3d9441aae4db Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 21:26:20 +0300 Subject: [PATCH 09/13] Added support for parsing and printing value tickets --- client/src/parsers/hrtparser/hrtparser.c | 279 ++++++++++++++++++++++- client/src/parsers/hrtparser/hrtparser.h | 69 +++++- client/src/parsers/parsehrt.c | 34 ++- 3 files changed, 369 insertions(+), 13 deletions(-) diff --git a/client/src/parsers/hrtparser/hrtparser.c b/client/src/parsers/hrtparser/hrtparser.c index d75f3e8b5..d754c5a38 100644 --- a/client/src/parsers/hrtparser/hrtparser.c +++ b/client/src/parsers/hrtparser/hrtparser.c @@ -145,6 +145,10 @@ static time_t hrt_apply_local_offset(time_t time) { if (gmtime_r(&time, &utc_time) == NULL) return time; #endif + // EN1545 stores local wall-clock date/time. Match Java TimeZone.getOffset() + // by using the DST state that applies at the represented timestamp. + utc_time.tm_isdst = local_time.tm_isdst; + time_t local_as_time = mktime(&local_time); time_t utc_as_local = mktime(&utc_time); @@ -170,6 +174,125 @@ time_t en5145_datetime_to_time(int days, int minutes) { return hrt_apply_local_offset(time); } +static void hrt_eticket_parse_v1(hrt_eticket_t *ticket, const uint8_t *data, size_t data_len, bool encrypted) { + if (!ticket || !data || data_len < (encrypted ? HRT_ETICKET_LEN + 6 : HRT_ETICKET_LEN)) return; + + ticket->product_code = ((uint16_t)data[0] << 6) | ((data[1] & 0xFC) >> 2); + ticket->child = (data[1] & 0x02) >> 1; + ticket->language_code = ((data[1] & 0x01) << 1) | ((data[2] & 0x80) >> 7); + ticket->validity_length_type = (data[2] & 0x60) >> 5; + ticket->validity_length = ((data[2] & 0x1F) << 3) | ((data[3] & 0xE0) >> 5); + ticket->validity_area_type = (data[3] & 0x10) >> 4; + ticket->validity_area = data[3] & 0x0F; + ticket->sale_date = en5145_date_to_time(((uint16_t)data[4] << 6) | ((data[5] & 0xFC) >> 2)); + ticket->sale_time = ((data[5] & 0x03) << 3) | ((data[6] & 0xE0) >> 5); + ticket->ticket_fare = convert_get_short_value(data, data_len, 68, 14); + ticket->group_size = (data[10] & 0x3E) >> 1; + ticket->sale_status = data[10] & 0x01; + + size_t offset = encrypted ? 6 : 0; + + ticket->validity_start_date = en5145_datetime_to_time( + ((uint16_t)data[offset + 11] << 6) | ((data[offset + 12] & 0xFC) >> 2), + ((data[offset + 12] & 0x03) << 9) | + ((uint16_t)data[offset + 13] << 1) | + ((data[offset + 14] & 0x80) >> 7) + ); + + ticket->validity_end_date = en5145_datetime_to_time( + ((data[offset + 14] & 0x7F) << 7) | ((data[offset + 15] & 0xFF) >> 1), + ((data[offset + 15] & 0x01) << 10) | + ((uint16_t)data[offset + 16] << 2) | + ((data[offset + 17] & 0xC0) >> 6) + ); + + ticket->validity_status = data[offset + 17] & 0x01; + + ticket->boarding_date = en5145_datetime_to_time( + ((uint16_t)data[offset + 18] << 6) | ((data[offset + 19] & 0xFC) >> 2), + ((data[offset + 19] & 0x03) << 9) | + ((uint16_t)data[offset + 20] << 1) | + ((data[offset + 21] & 0x80) >> 7) + ); + + ticket->boarding_vehicle = ((data[offset + 21] & 0x7F) << 7) | ((data[offset + 22] & 0xFE) >> 1); + ticket->boarding_location_num_type = ((data[offset + 22] & 0x01) << 1) | ((data[offset + 23] & 0x80) >> 7); + ticket->boarding_location_num = ((data[offset + 23] & 0x7F) << 7) | ((data[offset + 24] & 0xFE) >> 1); + ticket->boarding_direction = data[offset + 24] & 0x01; + ticket->boarding_area = (data[offset + 25] & 0xF0) >> 4; +} + +static void hrt_eticket_parse_v2(hrt_eticket_t *ticket, const uint8_t *data, size_t data_len) { + if (!ticket || !data || data_len < HRT_ETICKET_V2_LEN) return; + + ticket->product_code = convert_get_short_value(data, data_len, 1, 14); + ticket->product_code_group = convert_get_short_value(data, data_len, 15, 14); + ticket->language_code = convert_get_byte_value(data, data_len, 39, 2); + ticket->validity_length_type = convert_get_byte_value(data, data_len, 41, 2); + ticket->validity_length = convert_get_short_value(data, data_len, 43, 8); + ticket->validity_length_type_group = convert_get_byte_value(data, data_len, 51, 2); + ticket->validity_length_group = convert_get_short_value(data, data_len, 53, 8); + ticket->validity_area_type = convert_get_byte_value(data, data_len, 61, 2); + ticket->validity_area = convert_get_byte_value(data, data_len, 63, 6); + ticket->sale_date = en5145_date_to_time(convert_get_short_value(data, data_len, 69, 14)); + ticket->sale_time = convert_get_byte_value(data, data_len, 83, 5); + ticket->ticket_fare = convert_get_short_value(data, data_len, 105, 14); + ticket->ticket_fare_group = convert_get_short_value(data, data_len, 119, 14); + ticket->group_size = convert_get_byte_value(data, data_len, 133, 6); + ticket->extra_zone = convert_get_byte_value(data, data_len, 139, 1); + ticket->ext_period_pass_validity_area = convert_get_byte_value(data, data_len, 140, 6); + ticket->ext_product_code = convert_get_short_value(data, data_len, 146, 14); + ticket->ext1_validity_area = convert_get_byte_value(data, data_len, 160, 6); + ticket->ext1_fare = convert_get_short_value(data, data_len, 166, 14); + ticket->ext2_validity_area = convert_get_byte_value(data, data_len, 180, 6); + ticket->ext2_fare = convert_get_short_value(data, data_len, 186, 14); + ticket->sale_status = convert_get_byte_value(data, data_len, 200, 1); + ticket->validity_start_date = en5145_datetime_to_time( + convert_get_short_value(data, data_len, 205, 14), + convert_get_short_value(data, data_len, 219, 11) + ); + ticket->validity_end_date = en5145_datetime_to_time( + convert_get_short_value(data, data_len, 230, 14), + convert_get_short_value(data, data_len, 244, 11) + ); + + int group_end_days = convert_get_short_value(data, data_len, 255, 14); + int group_end_minutes = convert_get_short_value(data, data_len, 269, 11); + if (group_end_days > 0 && group_end_minutes > 0) { + ticket->validity_end_date_group = en5145_datetime_to_time(group_end_days, group_end_minutes); + ticket->has_validity_end_date_group = true; + } + + ticket->validity_status = convert_get_byte_value(data, data_len, 285, 1); + ticket->boarding_date = en5145_datetime_to_time( + convert_get_short_value(data, data_len, 286, 14), + convert_get_short_value(data, data_len, 300, 11) + ); + ticket->boarding_vehicle = convert_get_short_value(data, data_len, 311, 14); + ticket->boarding_location_num_type = convert_get_byte_value(data, data_len, 325, 2); + ticket->boarding_location_num = convert_get_short_value(data, data_len, 327, 14); + ticket->boarding_direction = convert_get_byte_value(data, data_len, 341, 1); + ticket->boarding_area = convert_get_byte_value(data, data_len, 344, 6); +} + +static hrt_eticket_t *hrt_eticket_create(const uint8_t *data, size_t data_len, bool encrypted, int version) { + if (!data) return NULL; + + hrt_eticket_t *ticket = calloc(1, sizeof(hrt_eticket_t)); + if (!ticket) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return NULL; + } + + if (version == 2) { + hrt_eticket_parse_v2(ticket, data, data_len); + } else { + hrt_eticket_parse_v1(ticket, data, data_len, encrypted); + } + + return ticket; +} + void hrt_travelcard_free(hrt_travel_card_t *card) { if (!card) return; @@ -190,12 +313,7 @@ void hrt_travelcard_free(hrt_travel_card_t *card) { free(card->application_instance_id); free(card->history_fields); - // TODO: Implement value tickets - // if (card->value_ticket) { - // eticket_free(card->value_ticket); - // card->value_ticket = NULL; - // } - + free(card->value_ticket); memset(card, 0, sizeof(*card)); } @@ -557,8 +675,143 @@ void hrt_history_set_transfer_end_date(hrt_history_t *history, time_t datetime) if (history) history->transfer_end_date = datetime; } -// TODO: Implement value tickets -// HRT_eTicket *eticket_create(const uint8_t *data, size_t data_len, int is_encrypted, int version); +bool hrt_eticket_is_defined(const hrt_eticket_t *ticket) { + return ticket && + (ticket->product_code > 0 || + ticket->product_code_group > 0 || + ticket->ticket_fare > 0 || + ticket->ticket_fare_group > 0 || + ticket->group_size > 0 || + ticket->boarding_vehicle > 0); +} + +int hrt_eticket_get_product_code(const hrt_eticket_t *ticket) { + if (!ticket) return 0; + return ticket->product_code_group > 0 ? ticket->product_code_group : ticket->product_code; +} + +int hrt_eticket_get_validity_length_type(const hrt_eticket_t *ticket) { + if (!ticket) return 0; + return ticket->validity_length_type_group > 0 ? ticket->validity_length_type_group : ticket->validity_length_type; +} + +int hrt_eticket_get_validity_length(const hrt_eticket_t *ticket) { + if (!ticket) return 0; + return ticket->validity_length_group > 0 ? ticket->validity_length_group : ticket->validity_length; +} + +int hrt_eticket_get_validity_area_type(const hrt_eticket_t *ticket) { + return ticket ? ticket->validity_area_type : 0; +} + +int hrt_eticket_get_validity_area(const hrt_eticket_t *ticket) { + return ticket ? ticket->validity_area : 0; +} + +time_t hrt_eticket_get_sale_date(const hrt_eticket_t *ticket) { + return ticket ? ticket->sale_date : (time_t)0; +} + +int hrt_eticket_get_sale_time(const hrt_eticket_t *ticket) { + return ticket ? ticket->sale_time : 0; +} + +int hrt_eticket_get_ticket_fare(const hrt_eticket_t *ticket) { + if (!ticket) return 0; + if (ticket->ticket_fare > 0) return ticket->ticket_fare; + return ticket->ticket_fare_group > 0 ? ticket->ticket_fare_group : 0; +} + +int hrt_eticket_get_right_fare(const hrt_eticket_t *ticket) { + if (!ticket) return 0; + + int fare = ticket->ticket_fare; + int group_size = ticket->group_size; + if ((ticket->extra_zone == 1 || fare > 0) && group_size > 1) { + group_size--; + } + return fare + (ticket->ticket_fare_group * group_size); +} + +int hrt_eticket_get_total_fare(const hrt_eticket_t *ticket) { + if (!ticket) return 0; + + int fare = hrt_eticket_get_right_fare(ticket); + return ticket->extra_zone == 1 ? fare + ticket->ext1_fare + ticket->ext2_fare : fare; +} + +int hrt_eticket_get_group_size(const hrt_eticket_t *ticket) { + return ticket ? ticket->group_size : 0; +} + +int hrt_eticket_get_sale_status(const hrt_eticket_t *ticket) { + return ticket ? ticket->sale_status : 0; +} + +time_t hrt_eticket_get_validity_start_date(const hrt_eticket_t *ticket) { + return ticket ? ticket->validity_start_date : (time_t)0; +} + +time_t hrt_eticket_get_validity_end_date(const hrt_eticket_t *ticket) { + if (!ticket) return (time_t)0; + return ticket->has_validity_end_date_group ? ticket->validity_end_date_group : ticket->validity_end_date; +} + +int hrt_eticket_get_validity_status(const hrt_eticket_t *ticket) { + return ticket ? ticket->validity_status : 0; +} + +time_t hrt_eticket_get_boarding_date(const hrt_eticket_t *ticket) { + return ticket ? ticket->boarding_date : (time_t)0; +} + +int hrt_eticket_get_boarding_vehicle(const hrt_eticket_t *ticket) { + return ticket ? ticket->boarding_vehicle : 0; +} + +int hrt_eticket_get_boarding_location_num_type(const hrt_eticket_t *ticket) { + return ticket ? ticket->boarding_location_num_type : 0; +} + +int hrt_eticket_get_boarding_location_num(const hrt_eticket_t *ticket) { + return ticket ? ticket->boarding_location_num : 0; +} + +int hrt_eticket_get_boarding_direction(const hrt_eticket_t *ticket) { + return ticket ? ticket->boarding_direction : 0; +} + +int hrt_eticket_get_boarding_area(const hrt_eticket_t *ticket) { + return ticket ? ticket->boarding_area : 0; +} + +int hrt_eticket_get_extra_zone(const hrt_eticket_t *ticket) { + return ticket ? ticket->extra_zone : 0; +} + +int hrt_eticket_get_ext_period_pass_validity_area(const hrt_eticket_t *ticket) { + return ticket ? ticket->ext_period_pass_validity_area : 0; +} + +int hrt_eticket_get_ext_product_code(const hrt_eticket_t *ticket) { + return ticket ? ticket->ext_product_code : 0; +} + +int hrt_eticket_get_ext1_validity_area(const hrt_eticket_t *ticket) { + return ticket ? ticket->ext1_validity_area : 0; +} + +int hrt_eticket_get_ext1_fare(const hrt_eticket_t *ticket) { + return ticket ? ticket->ext1_fare : 0; +} + +int hrt_eticket_get_ext2_validity_area(const hrt_eticket_t *ticket) { + return ticket ? ticket->ext2_validity_area : 0; +} + +int hrt_eticket_get_ext2_fare(const hrt_eticket_t *ticket) { + return ticket ? ticket->ext2_fare : 0; +} // Getter methods for API simplicity int hrt_travelcard_get_application_version(const hrt_travel_card_t *card) { @@ -768,12 +1021,18 @@ bool hrt_travelcard_set_eticket(hrt_travel_card_t *card, const uint8_t *buf, siz if (card->version == 2) { if (!card->eticket_data_v2 || len < HRT_ETICKET_V2_LEN) return false; memcpy(card->eticket_data_v2, buf, HRT_ETICKET_V2_LEN); - return true; + + free(card->value_ticket); + card->value_ticket = hrt_eticket_create(card->eticket_data_v2, HRT_ETICKET_V2_LEN, false, 2); + return card->value_ticket != NULL; } if (!card->eticket_data || len < HRT_ETICKET_LEN) return false; memcpy(card->eticket_data, buf, HRT_ETICKET_LEN); - return true; + + free(card->value_ticket); + card->value_ticket = hrt_eticket_create(card->eticket_data, HRT_ETICKET_LEN, false, 1); + return card->value_ticket != NULL; } bool hrt_travelcard_set_history(hrt_travel_card_t *card, const uint8_t *buf, size_t len) { diff --git a/client/src/parsers/hrtparser/hrtparser.h b/client/src/parsers/hrtparser/hrtparser.h index 89b489d26..3285723d1 100644 --- a/client/src/parsers/hrtparser/hrtparser.h +++ b/client/src/parsers/hrtparser/hrtparser.h @@ -33,8 +33,42 @@ #define MSB(b) ((b) & 0x80) #define LSB7(b) ((b) & 0x7F) -// TODO: Implement -typedef struct hrt_eticket_t hrt_eticket_t; +typedef struct { + int boarding_area; + time_t boarding_date; + int boarding_direction; + int boarding_location_num; + int boarding_location_num_type; + int boarding_vehicle; + int child; + int ext1_fare; + int ext1_validity_area; + int ext2_fare; + int ext2_validity_area; + int ext_period_pass_validity_area; + int ext_product_code; + int extra_zone; + int group_size; + int language_code; + int product_code; + int product_code_group; + time_t sale_date; + int sale_status; + int sale_time; + int ticket_fare; + int ticket_fare_group; + int validity_area; + int validity_area_type; + time_t validity_end_date; + time_t validity_end_date_group; + bool has_validity_end_date_group; + int validity_length; + int validity_length_group; + int validity_length_type; + int validity_length_type_group; + time_t validity_start_date; + int validity_status; +} hrt_eticket_t; typedef struct { int group_size; @@ -163,6 +197,37 @@ void hrt_history_set_group_size(hrt_history_t *history, int group_size); void hrt_history_set_price(hrt_history_t *history, int price); void hrt_history_set_transfer_end_date(hrt_history_t *history, time_t datetime); +// E-ticket Functions +bool hrt_eticket_is_defined(const hrt_eticket_t *ticket); +int hrt_eticket_get_product_code(const hrt_eticket_t *ticket); +int hrt_eticket_get_validity_length_type(const hrt_eticket_t *ticket); +int hrt_eticket_get_validity_length(const hrt_eticket_t *ticket); +int hrt_eticket_get_validity_area_type(const hrt_eticket_t *ticket); +int hrt_eticket_get_validity_area(const hrt_eticket_t *ticket); +time_t hrt_eticket_get_sale_date(const hrt_eticket_t *ticket); +int hrt_eticket_get_sale_time(const hrt_eticket_t *ticket); +int hrt_eticket_get_ticket_fare(const hrt_eticket_t *ticket); +int hrt_eticket_get_right_fare(const hrt_eticket_t *ticket); +int hrt_eticket_get_total_fare(const hrt_eticket_t *ticket); +int hrt_eticket_get_group_size(const hrt_eticket_t *ticket); +int hrt_eticket_get_sale_status(const hrt_eticket_t *ticket); +time_t hrt_eticket_get_validity_start_date(const hrt_eticket_t *ticket); +time_t hrt_eticket_get_validity_end_date(const hrt_eticket_t *ticket); +int hrt_eticket_get_validity_status(const hrt_eticket_t *ticket); +time_t hrt_eticket_get_boarding_date(const hrt_eticket_t *ticket); +int hrt_eticket_get_boarding_vehicle(const hrt_eticket_t *ticket); +int hrt_eticket_get_boarding_location_num_type(const hrt_eticket_t *ticket); +int hrt_eticket_get_boarding_location_num(const hrt_eticket_t *ticket); +int hrt_eticket_get_boarding_direction(const hrt_eticket_t *ticket); +int hrt_eticket_get_boarding_area(const hrt_eticket_t *ticket); +int hrt_eticket_get_extra_zone(const hrt_eticket_t *ticket); +int hrt_eticket_get_ext_period_pass_validity_area(const hrt_eticket_t *ticket); +int hrt_eticket_get_ext_product_code(const hrt_eticket_t *ticket); +int hrt_eticket_get_ext1_validity_area(const hrt_eticket_t *ticket); +int hrt_eticket_get_ext1_fare(const hrt_eticket_t *ticket); +int hrt_eticket_get_ext2_validity_area(const hrt_eticket_t *ticket); +int hrt_eticket_get_ext2_fare(const hrt_eticket_t *ticket); + // Parsing Functions void hrt_read_application_info(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); void hrt_read_control_info(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c index cdb543875..7ff79a897 100644 --- a/client/src/parsers/parsehrt.c +++ b/client/src/parsers/parsehrt.c @@ -180,6 +180,7 @@ static void hrt_print_card(const hrt_travel_card_t *card) { const char *product_name = hrt_lookup_product_name(product_code); int area_type = hrt_travelcard_get_validity_area_type1(card); int area = hrt_travelcard_get_validity_area1(card); + const hrt_eticket_t *value_ticket = hrt_travelcard_get_value_ticket(card); hrt_price_to_string( hrt_travelcard_get_stored_value_counter(card), @@ -218,7 +219,38 @@ static void hrt_print_card(const hrt_travel_card_t *card) { hrt_print_time(" End date...............", hrt_travelcard_get_period_end_date1(card)); PrintAndLogEx(SUCCESS, " Season ticket length... " _GREEN_("%d days"), hrt_travelcard_get_period_length1(card)); - // TODO: Print value ticket information + if (hrt_eticket_is_defined(value_ticket)) { + char fare_buf[32] = {0}; + char value_area_buf[64] = {0}; + int value_product_code = hrt_eticket_get_product_code(value_ticket); + const char *value_product_name = hrt_lookup_product_name(value_product_code); + int value_area_type = hrt_eticket_get_validity_area_type(value_ticket); + int value_area = hrt_eticket_get_validity_area(value_ticket); + + hrt_price_to_string(hrt_eticket_get_total_fare(value_ticket), fare_buf, sizeof(fare_buf)); + + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "Value ticket information"); + + if (value_product_name != NULL) { + PrintAndLogEx(SUCCESS, " Product code........... " _GREEN_("%d (%s)"), value_product_code, value_product_name); + } else { + PrintAndLogEx(SUCCESS, " Product code........... " _GREEN_("%d (unknown)"), value_product_code); + } + + if (hrt_format_area(value_area_type, value_area, value_area_buf, sizeof(value_area_buf))) { + PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%s"), value_area_buf); + } else { + PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%d (type %d)"), value_area, value_area_type); + } + + PrintAndLogEx(SUCCESS, " Fare................... " _GREEN_("%s"), fare_buf); + PrintAndLogEx(SUCCESS, " Group size............. " _GREEN_("%d"), hrt_eticket_get_group_size(value_ticket)); + hrt_print_time(" Valid from.............", hrt_eticket_get_validity_start_date(value_ticket)); + hrt_print_time(" Valid until............", hrt_eticket_get_validity_end_date(value_ticket)); + hrt_print_time(" Boarding time..........", hrt_eticket_get_boarding_date(value_ticket)); + PrintAndLogEx(SUCCESS, " Boarding vehicle....... " _GREEN_("%d"), hrt_eticket_get_boarding_vehicle(value_ticket)); + } } bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen) { From 36f55d303e5bb8d76be2a2b6f0b69a0d29afdbc0 Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 21:53:52 +0300 Subject: [PATCH 10/13] Print more information about loaded season ticket --- client/src/parsers/hrtparser/hrtparser.c | 29 +++++++++++++++++++++++- client/src/parsers/hrtparser/hrtparser.h | 6 +++++ client/src/parsers/parsehrt.c | 18 ++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/client/src/parsers/hrtparser/hrtparser.c b/client/src/parsers/hrtparser/hrtparser.c index d754c5a38..eca9816b3 100644 --- a/client/src/parsers/hrtparser/hrtparser.c +++ b/client/src/parsers/hrtparser/hrtparser.c @@ -133,6 +133,33 @@ int convert_get_short_value( return (int)((value >> shift) & mask); } +int convert_get_int_value( + const uint8_t *data, + size_t data_len, + int bit_offset, + int bit_length +) { + if (!data || bit_length <= 0) return 0; + + if (bit_length > 25) bit_length = 25; + + int byte_idx = bit_offset / 8; + int bit_idx = bit_offset % 8; + + if ((size_t)(byte_idx + 3) >= data_len) return 0; + + uint32_t value = + ((uint32_t)data[byte_idx] << 24) | + ((uint32_t)data[byte_idx + 1] << 16) | + ((uint32_t)data[byte_idx + 2] << 8) | + ((uint32_t)data[byte_idx + 3]); + + uint32_t mask = (1U << bit_length) - 1U; + int shift = (32 - bit_idx) - bit_length; + + return (int)((value >> shift) & mask); +} + static time_t hrt_apply_local_offset(time_t time) { struct tm local_time = {0}; struct tm utc_time = {0}; @@ -615,7 +642,7 @@ void hrt_read_period_pass_v2(hrt_travel_card_t *card, const uint8_t *data, size_ convert_get_short_value(data, data_len, 141, 11) ); card->loaded_period_length = convert_get_short_value(data, data_len, 152, 9); - card->loaded_period_price = convert_get_short_value(data, data_len, 161, 20); + card->loaded_period_price = convert_get_int_value(data, data_len, 161, 20); card->period_loading_organization = convert_get_short_value(data, data_len, 181, 14); card->period_loading_device_number = convert_get_short_value(data, data_len, 195, 13); diff --git a/client/src/parsers/hrtparser/hrtparser.h b/client/src/parsers/hrtparser/hrtparser.h index 3285723d1..c3925713f 100644 --- a/client/src/parsers/hrtparser/hrtparser.h +++ b/client/src/parsers/hrtparser/hrtparser.h @@ -161,6 +161,12 @@ int convert_get_byte_value( int bit_offset, int bit_length ); +int convert_get_int_value( + const uint8_t *data, + size_t data_len, + int bit_offset, + int bit_length +); int convert_get_short_value( const uint8_t *data, size_t data_len, diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c index 7ff79a897..c92afcc7b 100644 --- a/client/src/parsers/parsehrt.c +++ b/client/src/parsers/parsehrt.c @@ -170,6 +170,17 @@ static void hrt_print_time(const char *label, time_t value) { PrintAndLogEx(SUCCESS, "%s " _GREEN_("%s"), label, buf); } +static bool hrt_has_loaded_period_info(const hrt_travel_card_t *card) { + if (card == NULL) return false; + + return hrt_travelcard_get_loaded_period_product(card) > 0 || + hrt_travelcard_get_loaded_period_length(card) > 0 || + hrt_travelcard_get_loaded_period_price(card) > 0 || + hrt_travelcard_get_period_loading_date(card) != (time_t)0 || + hrt_travelcard_get_period_loading_organization(card) > 0 || + hrt_travelcard_get_period_loading_device_number(card) > 0; +} + static void hrt_print_card(const hrt_travel_card_t *card) { if (card == NULL) return; @@ -217,7 +228,12 @@ static void hrt_print_card(const hrt_travel_card_t *card) { } hrt_print_time(" Start date.............", hrt_travelcard_get_period_start_date1(card)); hrt_print_time(" End date...............", hrt_travelcard_get_period_end_date1(card)); - PrintAndLogEx(SUCCESS, " Season ticket length... " _GREEN_("%d days"), hrt_travelcard_get_period_length1(card)); + PrintAndLogEx(SUCCESS, " Length................. " _GREEN_("%d days"), hrt_travelcard_get_period_length1(card)); + + if (hrt_has_loaded_period_info(card)) { + PrintAndLogEx(SUCCESS, " Organization........... " _GREEN_("%d"), hrt_travelcard_get_period_loading_organization(card)); + PrintAndLogEx(SUCCESS, " Device number.......... " _GREEN_("%d"), hrt_travelcard_get_period_loading_device_number(card)); + } if (hrt_eticket_is_defined(value_ticket)) { char fare_buf[32] = {0}; From d747d5cfac274b10f9670f2c295fca1780e2568c Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 22:33:18 +0300 Subject: [PATCH 11/13] Added support for parsing and printing history records --- client/src/parsers/hrtparser/hrtparser.c | 96 +++++++++++++++++- client/src/parsers/hrtparser/hrtparser.h | 4 +- client/src/parsers/parsehrt.c | 118 +++++++++++++++++++++-- 3 files changed, 202 insertions(+), 16 deletions(-) diff --git a/client/src/parsers/hrtparser/hrtparser.c b/client/src/parsers/hrtparser/hrtparser.c index eca9816b3..9597ed157 100644 --- a/client/src/parsers/hrtparser/hrtparser.c +++ b/client/src/parsers/hrtparser/hrtparser.c @@ -49,6 +49,7 @@ static const int64_t MINUTE_IN_MS = 60000LL; #define HRT_ETICKET_LEN 26 #define HRT_ETICKET_V2_LEN 45 #define HRT_HISTORY_LEN 96 +#define HRT_HISTORY_RECORD_LEN 12 #define HRT_HISTORY_RECORDS 8 int hrt_price_to_string(int cents, char *out, size_t out_len) { @@ -669,8 +670,95 @@ void hrt_read_stored_value(hrt_travel_card_t *card, const uint8_t *data, size_t ((uint32_t)data[1] << 4); } -// void readHistory(const uint8_t *data, size_t length); -// void readHistory_v2(const uint8_t *data, size_t length); +void hrt_read_history(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || !card->history_fields) return; + + size_t record_count = data_len / HRT_HISTORY_RECORD_LEN; + if (record_count > HRT_HISTORY_RECORDS) { + record_count = HRT_HISTORY_RECORDS; + } + + memset(card->history_fields, 0, HRT_HISTORY_RECORDS * sizeof(hrt_history_t)); + card->history_len = 0; + + for (size_t i = 0; i < record_count; i++) { + size_t offset = i * HRT_HISTORY_RECORD_LEN; + + if (data[offset + 1] == 0 && + data[offset + 2] == 0 && + data[offset + 3] == 0 && + data[offset + 4] == 0) { + continue; + } + + hrt_history_t *history = &card->history_fields[card->history_len]; + hrt_history_init(history); + + history->transaction_type = (data[offset] & 0x80) >> 7; + + uint8_t date_time_byte = data[offset + 3]; + int days = ((date_time_byte & 0x3F) << 8) | data[offset + 4]; + int minutes = ((data[offset + 1] & 0x01) << 10) | + ((uint16_t)data[offset + 2] << 2) | + ((date_time_byte & 0xC0) >> 6); + + int transfer_end_minutes = ((uint16_t)data[offset + 5] << 3) | + ((data[offset + 6] & 0xE0) >> 5); + + if (transfer_end_minutes < minutes) { + days--; + } + + history->transaction_d_time = en5145_datetime_to_time(days, minutes); + history->price = ((uint16_t)data[offset + 7] << 1) | + ((data[offset + 6] & 0x1F) << 9) | + ((data[offset + 8] & 0x80) >> 7); + history->group_size = (data[offset + 8] & 0x7C) >> 2; + + card->history_len++; + } +} + +void hrt_read_history_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len) { + if (!card || !data || !card->history_fields) return; + + size_t record_count = data_len / HRT_HISTORY_RECORD_LEN; + if (record_count > HRT_HISTORY_RECORDS) { + record_count = HRT_HISTORY_RECORDS; + } + + memset(card->history_fields, 0, HRT_HISTORY_RECORDS * sizeof(hrt_history_t)); + card->history_len = 0; + + for (size_t i = 0; i < record_count; i++) { + size_t byte_offset = i * HRT_HISTORY_RECORD_LEN; + + if (data[byte_offset + 1] == 0 && + data[byte_offset + 2] == 0 && + data[byte_offset + 3] == 0 && + data[byte_offset + 4] == 0) { + continue; + } + + int bit_offset = (int)(i * 96); + hrt_history_t *history = &card->history_fields[card->history_len]; + hrt_history_init(history); + + history->transaction_type = convert_get_byte_value(data, data_len, bit_offset, 1); + history->transaction_d_time = en5145_datetime_to_time( + convert_get_short_value(data, data_len, bit_offset + 1, 14), + convert_get_short_value(data, data_len, bit_offset + 15, 11) + ); + history->transfer_end_date = en5145_datetime_to_time( + convert_get_short_value(data, data_len, bit_offset + 26, 14), + convert_get_short_value(data, data_len, bit_offset + 40, 11) + ); + history->price = convert_get_short_value(data, data_len, bit_offset + 51, 14); + history->group_size = convert_get_byte_value(data, data_len, bit_offset + 65, 6); + + card->history_len++; + } +} void hrt_history_init(hrt_history_t *history) { if (!history) return; @@ -1069,14 +1157,14 @@ bool hrt_travelcard_set_history(hrt_travel_card_t *card, const uint8_t *buf, siz if (!card->history_data_v2) return false; if (len > HRT_HISTORY_LEN) len = HRT_HISTORY_LEN; memcpy(card->history_data_v2, buf, len); - // TODO: readHistory_v2(card->historyData_v2, len); + hrt_read_history_v2(card, card->history_data_v2, len); return true; } if (!card->history_data) return false; if (len > HRT_HISTORY_LEN) len = HRT_HISTORY_LEN; memcpy(card->history_data, buf, len); - // TODO: readHistory(card->historyData, len); + hrt_read_history(card, card->history_data, len); return true; } diff --git a/client/src/parsers/hrtparser/hrtparser.h b/client/src/parsers/hrtparser/hrtparser.h index c3925713f..ce56b4cbc 100644 --- a/client/src/parsers/hrtparser/hrtparser.h +++ b/client/src/parsers/hrtparser/hrtparser.h @@ -240,8 +240,8 @@ void hrt_read_control_info(hrt_travel_card_t *card, const uint8_t *data, size_t void hrt_read_period_pass(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); void hrt_read_period_pass_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); void hrt_read_stored_value(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); -void hrt_read_history(const uint8_t *data, size_t length); -void hrt_read_history_v2(const uint8_t *data, size_t length); +void hrt_read_history(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); +void hrt_read_history_v2(hrt_travel_card_t *card, const uint8_t *data, size_t data_len); // Getter Functions int hrt_travelcard_get_application_version(const hrt_travel_card_t *card); diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c index c92afcc7b..4f54367df 100644 --- a/client/src/parsers/parsehrt.c +++ b/client/src/parsers/parsehrt.c @@ -154,19 +154,26 @@ static bool hrt_read_and_parse_application_info(DesfireContext_t *dctx, hrt_trav return application_id != NULL && application_id[0] != '\0'; } -static void hrt_print_time(const char *label, time_t value) { - if (value == (time_t)0) return; +static bool hrt_format_time(time_t value, char *out, size_t out_len) { + if (value == (time_t)0 || out == NULL || out_len == 0) return false; char buf[32] = {0}; struct tm tm_value = {0}; #if defined(_WIN32) - if (localtime_s(&tm_value, &value) != 0) return; + if (localtime_s(&tm_value, &value) != 0) return false; #else - if (localtime_r(&value, &tm_value) == NULL) return; + if (localtime_r(&value, &tm_value) == NULL) return false; #endif - strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value); + if (strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value) == 0) return false; + return snprintf(out, out_len, "%s", buf) > 0; +} + +static void hrt_print_time(const char *label, time_t value) { + char buf[32] = {0}; + if (hrt_format_time(value, buf, sizeof(buf)) == false) return; + PrintAndLogEx(SUCCESS, "%s " _GREEN_("%s"), label, buf); } @@ -181,6 +188,85 @@ static bool hrt_has_loaded_period_info(const hrt_travel_card_t *card) { hrt_travelcard_get_period_loading_device_number(card) > 0; } +static void hrt_print_boarding_location(int location_type, int location_num) { + if (location_num <= 0) return; + + if (location_type == 1) { + PrintAndLogEx(SUCCESS, " Boarding line.......... " _GREEN_("%d"), location_num); + } else if (location_type == 2) { + PrintAndLogEx(SUCCESS, " Boarding train......... " _GREEN_("%d"), location_num); + } +} + +static bool hrt_format_value_ticket_zone(const hrt_eticket_t *ticket, char *out, size_t out_len) { + if (ticket == NULL || out == NULL || out_len == 0) return false; + + if (hrt_eticket_get_extra_zone(ticket) == 1 && hrt_eticket_get_group_size(ticket) <= 1) { + char ext1[32] = {0}; + char ext2[32] = {0}; + bool has_ext1 = false; + bool has_ext2 = false; + int ext1_area = hrt_eticket_get_ext1_validity_area(ticket); + int ext2_area = hrt_eticket_get_ext2_validity_area(ticket); + + if (ext1_area != 56) { + has_ext1 = hrt_format_area(HRT_AREA_TYPE_ZONE_RANGE, ext1_area, ext1, sizeof(ext1)); + } + if (ext2_area != 56) { + has_ext2 = hrt_format_area(HRT_AREA_TYPE_ZONE_RANGE, ext2_area, ext2, sizeof(ext2)); + } + + if (has_ext1 && has_ext2) { + return snprintf(out, out_len, "%s+%s", ext1, ext2) > 0; + } + if (has_ext1) return snprintf(out, out_len, "%s", ext1) > 0; + if (has_ext2) return snprintf(out, out_len, "%s", ext2) > 0; + } + + return hrt_format_area(hrt_eticket_get_validity_area_type(ticket), + hrt_eticket_get_validity_area(ticket), out, out_len); +} + +static const char *hrt_history_transaction_type_name(int transaction_type) { + return transaction_type == 0 ? "Season ticket" : "Value ticket"; +} + +static void hrt_print_history(const hrt_travel_card_t *card) { + if (card == NULL) return; + + const hrt_history_t *history = hrt_travelcard_get_history(card); + int history_len = hrt_travelcard_get_history_len(card); + if (history == NULL || history_len <= 0) return; + + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "History records"); + + for (int i = history_len - 1; i >= 0; i--) { + char transaction_time[32] = {0}; + char price_buf[32] = {0}; + + if (hrt_format_time(history[i].transaction_d_time, transaction_time, sizeof(transaction_time)) == false) { + continue; + } + + PrintAndLogEx(SUCCESS, " %s " _GREEN_("%s"), transaction_time, + hrt_history_transaction_type_name(history[i].transaction_type)); + + if (history[i].transaction_type != 0 && + (history[i].group_size > 1 || history[i].price > 0)) { + if (history[i].group_size > 1) { + PrintAndLogEx(SUCCESS, " Group size.......... " _GREEN_("%d"), history[i].group_size); + } + if (history[i].price > 0) { + hrt_price_to_string(history[i].price, price_buf, sizeof(price_buf)); + PrintAndLogEx(SUCCESS, " Price............... " _GREEN_("%s"), price_buf); + } + } + + hrt_print_time(" Transfer ends.......", history[i].transfer_end_date); + } +} + static void hrt_print_card(const hrt_travel_card_t *card) { if (card == NULL) return; @@ -191,6 +277,7 @@ static void hrt_print_card(const hrt_travel_card_t *card) { const char *product_name = hrt_lookup_product_name(product_code); int area_type = hrt_travelcard_get_validity_area_type1(card); int area = hrt_travelcard_get_validity_area1(card); + char boarding_area_buf[64] = {0}; const hrt_eticket_t *value_ticket = hrt_travelcard_get_value_ticket(card); hrt_price_to_string( @@ -229,6 +316,15 @@ static void hrt_print_card(const hrt_travel_card_t *card) { hrt_print_time(" Start date.............", hrt_travelcard_get_period_start_date1(card)); hrt_print_time(" End date...............", hrt_travelcard_get_period_end_date1(card)); PrintAndLogEx(SUCCESS, " Length................. " _GREEN_("%d days"), hrt_travelcard_get_period_length1(card)); + hrt_print_time(" Boarding time..........", hrt_travelcard_get_boarding_date(card)); + hrt_print_boarding_location(hrt_travelcard_get_boarding_location_num_type(card), + hrt_travelcard_get_boarding_location_num(card)); + + if (hrt_format_area(hrt_travelcard_get_boarding_area_type(card), + hrt_travelcard_get_boarding_area(card), + boarding_area_buf, sizeof(boarding_area_buf))) { + PrintAndLogEx(SUCCESS, " Boarding zone.......... " _GREEN_("%s"), boarding_area_buf); + } if (hrt_has_loaded_period_info(card)) { PrintAndLogEx(SUCCESS, " Organization........... " _GREEN_("%d"), hrt_travelcard_get_period_loading_organization(card)); @@ -240,8 +336,6 @@ static void hrt_print_card(const hrt_travel_card_t *card) { char value_area_buf[64] = {0}; int value_product_code = hrt_eticket_get_product_code(value_ticket); const char *value_product_name = hrt_lookup_product_name(value_product_code); - int value_area_type = hrt_eticket_get_validity_area_type(value_ticket); - int value_area = hrt_eticket_get_validity_area(value_ticket); hrt_price_to_string(hrt_eticket_get_total_fare(value_ticket), fare_buf, sizeof(fare_buf)); @@ -254,10 +348,10 @@ static void hrt_print_card(const hrt_travel_card_t *card) { PrintAndLogEx(SUCCESS, " Product code........... " _GREEN_("%d (unknown)"), value_product_code); } - if (hrt_format_area(value_area_type, value_area, value_area_buf, sizeof(value_area_buf))) { - PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%s"), value_area_buf); + if (hrt_format_value_ticket_zone(value_ticket, value_area_buf, sizeof(value_area_buf))) { + PrintAndLogEx(SUCCESS, " Zone................... " _GREEN_("%s"), value_area_buf); } else { - PrintAndLogEx(SUCCESS, " Area................... " _GREEN_("%d (type %d)"), value_area, value_area_type); + PrintAndLogEx(SUCCESS, " Zone................... " _GREEN_("unknown")); } PrintAndLogEx(SUCCESS, " Fare................... " _GREEN_("%s"), fare_buf); @@ -266,7 +360,11 @@ static void hrt_print_card(const hrt_travel_card_t *card) { hrt_print_time(" Valid until............", hrt_eticket_get_validity_end_date(value_ticket)); hrt_print_time(" Boarding time..........", hrt_eticket_get_boarding_date(value_ticket)); PrintAndLogEx(SUCCESS, " Boarding vehicle....... " _GREEN_("%d"), hrt_eticket_get_boarding_vehicle(value_ticket)); + hrt_print_boarding_location(hrt_eticket_get_boarding_location_num_type(value_ticket), + hrt_eticket_get_boarding_location_num(value_ticket)); } + + hrt_print_history(card); } bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen) { From 379d461bb337a5a89cdb05230c4bb4f5e64a72fe Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sun, 21 Jun 2026 23:03:02 +0300 Subject: [PATCH 12/13] Added support for older card (EF2011) --- client/src/parsers/parsehrt.c | 173 +++++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 53 deletions(-) diff --git a/client/src/parsers/parsehrt.c b/client/src/parsers/parsehrt.c index 4f54367df..974665253 100644 --- a/client/src/parsers/parsehrt.c +++ b/client/src/parsers/parsehrt.c @@ -9,10 +9,28 @@ #include #include -#define HRT_NEW_AID 0xEF2014 -#define HRT_OLD_AID 0xEF2011 -#define HRT_APPLICATION_INFO_FILE_ID 0x08 -#define HRT_APPLICATION_INFO_LEN 11 +#define HRT_NEW_AID 0xEF2014 +#define HRT_OLD_AID 0xEF2011 +#define HRT_CONTROL_INFO_FILE_ID 0x00 +#define HRT_PERIOD_PASS_FILE_ID 0x01 +#define HRT_STORED_VALUE_FILE_ID 0x02 +#define HRT_ETICKET_FILE_ID 0x03 +#define HRT_HISTORY_FILE_ID 0x04 +#define HRT_APPLICATION_INFO_FILE_ID 0x08 +#define HRT_APPLICATION_INFO_LEN 11 +#define HRT_CONTROL_INFO_LEN 6 +#define HRT_CONTROL_INFO_V2_LEN 10 +#define HRT_PERIOD_PASS_LEN 32 +#define HRT_PERIOD_PASS_V2_LEN 35 +#define HRT_STORED_VALUE_LEN 12 +#define HRT_STORED_VALUE_V2_LEN 13 +#define HRT_ETICKET_LEN 26 +#define HRT_ETICKET_V2_LEN 45 + +typedef struct { + uint32_t aid; + int version; +} hrt_application_t; typedef enum { HRT_AREA_TYPE_CITY = 0, @@ -122,13 +140,25 @@ static bool hrt_format_card_number(const char *card_number, char *out, size_t ou return written > 0 && (size_t)written < out_len; } -static bool hrt_aid_list_contains(const uint8_t *aidbuf, size_t aidbuflen) { +static bool hrt_get_application_from_aid_list(const uint8_t *aidbuf, size_t aidbuflen, hrt_application_t *application) { if (aidbuf == NULL || aidbuflen < 3) return false; for (size_t i = 0; i + 2 < aidbuflen; i += 3) { - // TODO: Also add HRT_OLD_AID check. Currently it is not - // checked since parsing the older cards is not yet implemented. - if (DesfireAIDByteToUint(&aidbuf[i]) == HRT_NEW_AID) { + uint32_t aid = DesfireAIDByteToUint(&aidbuf[i]); + + if (aid == HRT_OLD_AID) { + if (application != NULL) { + application->aid = HRT_OLD_AID; + application->version = 1; + } + return true; + } + + if (aid == HRT_NEW_AID) { + if (application != NULL) { + application->aid = HRT_NEW_AID; + application->version = 2; + } return true; } } @@ -136,6 +166,22 @@ static bool hrt_aid_list_contains(const uint8_t *aidbuf, size_t aidbuflen) { return false; } +static size_t hrt_control_info_len(int version) { + return version == 2 ? HRT_CONTROL_INFO_V2_LEN : HRT_CONTROL_INFO_LEN; +} + +static size_t hrt_period_pass_len(int version) { + return version == 2 ? HRT_PERIOD_PASS_V2_LEN : HRT_PERIOD_PASS_LEN; +} + +static size_t hrt_stored_value_len(int version) { + return version == 2 ? HRT_STORED_VALUE_V2_LEN : HRT_STORED_VALUE_LEN; +} + +static size_t hrt_eticket_len(int version) { + return version == 2 ? HRT_ETICKET_V2_LEN : HRT_ETICKET_LEN; +} + static bool hrt_read_and_parse_application_info(DesfireContext_t *dctx, hrt_travel_card_t *card) { if (dctx == NULL || card == NULL) return false; @@ -367,24 +413,75 @@ static void hrt_print_card(const hrt_travel_card_t *card) { hrt_print_history(card); } +static bool hrt_select_application(DesfireContext_t *dctx, const hrt_application_t *application) { + if (dctx == NULL || application == NULL) return false; + + return DesfireSelectAIDHex(dctx, application->aid, false, 0) == PM3_SUCCESS; +} + +static bool hrt_read_card(DesfireContext_t *dctx, const hrt_application_t *application, hrt_travel_card_t *card) { + if (dctx == NULL || application == NULL || card == NULL) return false; + + uint8_t buf[APDU_RES_LEN] = {0}; + size_t len = 0; + + hrt_travelcard_init_empty(card, application->version); + + // File 0x08: Application info + if (hrt_read_and_parse_application_info(dctx, card) == false) { + return false; + } + + // File 0x00: Control info + len = 0; + if (DesfireReadFile(dctx, HRT_CONTROL_INFO_FILE_ID, 0, hrt_control_info_len(application->version), buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_control_info(card, buf, len); + } + + // File 0x01: Period pass + len = 0; + if (DesfireReadFile(dctx, HRT_PERIOD_PASS_FILE_ID, 0, hrt_period_pass_len(application->version), buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_period_pass(card, buf, len); + } + + // File 0x02: Stored value + len = 0; + if (DesfireReadFile(dctx, HRT_STORED_VALUE_FILE_ID, 0, hrt_stored_value_len(application->version), buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_stored_value(card, buf, len); + } + + // File 0x03: eTicket + len = 0; + if (DesfireReadFile(dctx, HRT_ETICKET_FILE_ID, 0, hrt_eticket_len(application->version), buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_eticket(card, buf, len); + } + + // File 0x04: History records + len = 0; + if (DesfireReadRecords(dctx, HRT_HISTORY_FILE_ID, 0, 0, buf, &len) == PM3_SUCCESS) { + hrt_travelcard_set_history(card, buf, len); + } + + return true; +} + bool is_valid_hrt_card(DesfireContext_t *dctx, const uint8_t *aidbuf, size_t aidbuflen) { if (dctx == NULL) return false; - if (hrt_aid_list_contains(aidbuf, aidbuflen) == false) { + hrt_application_t application = {0}; + if (hrt_get_application_from_aid_list(aidbuf, aidbuflen, &application) == false) { return false; } DesfireSetCommMode(dctx, DCMPlain); DesfireSetCommandSet(dctx, DCCNative); - // TODO: Add support for older travelcard (AID EF2011) - - if (DesfireSelectAIDHex(dctx, HRT_NEW_AID, false, 0) != PM3_SUCCESS) { + if (hrt_select_application(dctx, &application) == false) { return false; } hrt_travel_card_t card; - hrt_travelcard_init_empty(&card, 2); + hrt_travelcard_init_empty(&card, application.version); bool is_valid = hrt_read_and_parse_application_info(dctx, &card); @@ -398,50 +495,20 @@ bool hrt_parser_parse(DesfireContext_t *dctx) { DesfireSetCommMode(dctx, DCMPlain); DesfireSetCommandSet(dctx, DCCNative); - // TODO: Add support for older travelcard (AID EF2011) + hrt_application_t applications[] = { + {HRT_OLD_AID, 1}, + {HRT_NEW_AID, 2}, + }; - if (DesfireSelectAIDHex(dctx, HRT_NEW_AID, false, 0) == PM3_SUCCESS) { - uint8_t buf[APDU_RES_LEN] = {0}; - size_t len = 0; + for (size_t i = 0; i < ARRAYLEN(applications); i++) { + if (hrt_select_application(dctx, &applications[i]) == false) { + continue; + } hrt_travel_card_t card; - hrt_travelcard_init_empty(&card, 2); - - // File 0x08: Application info (11 bytes) - // Check that application info parses before reading other files - if (hrt_read_and_parse_application_info(dctx, &card) == false) { + if (hrt_read_card(dctx, &applications[i], &card) == false) { hrt_travelcard_free(&card); - return false; - } - - // File 0x00: Control info (10 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x00, 0, 10, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_control_info(&card, buf, len); - } - - // File 0x01: Period pass (35 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x01, 0, 35, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_period_pass(&card, buf, len); - } - - // File 0x02: Stored value (13 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x02, 0, 13, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_stored_value(&card, buf, len); - } - - // File 0x03: eTicket (45 bytes) - len = 0; - if (DesfireReadFile(dctx, 0x03, 0, 45, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_eticket(&card, buf, len); - } - - // File 0x04: History records (all) - len = 0; - if (DesfireReadRecords(dctx, 0x04, 0, 0, buf, &len) == PM3_SUCCESS) { - hrt_travelcard_set_history(&card, buf, len); + continue; } hrt_print_card(&card); From 2f563297cd6485382c6d9028b725942e4a831b4c Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Wed, 24 Jun 2026 19:18:18 +0300 Subject: [PATCH 13/13] Add support for parsing Finnish Helsinki Regional Transport (HRT) travel cards --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd70e4e0a..d05fcf658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Add support for parsing Finnish Helsinki Regional Transport (HRT) travel cards (@sanduuz) - Added standalone mode `HF_DOEGOX_COMMIT`: DESFire suspended commit without relay (@doegox) - Added support for Innovatron protocol detection to `hf 14b info` (@kormax) - Improved `lf cotag reader` and `lf cotag demod`: Reimplementation and enhancement of proxmark3 COTAG support