From f71071eb94887b5ea630f2787cca2cf482f5f59e Mon Sep 17 00:00:00 2001 From: Sanduuz Date: Sat, 3 Jan 2026 00:00:29 +0200 Subject: [PATCH] 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