From df1d6503c0e6426d20c5db0e9774ad7f94251875 Mon Sep 17 00:00:00 2001 From: Methodius Date: Wed, 22 Nov 2023 16:15:53 +0900 Subject: [PATCH 1/5] Umarsh header check rework --- applications/main/nfc/plugins/supported_cards/umarsh.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/nfc/plugins/supported_cards/umarsh.c b/applications/main/nfc/plugins/supported_cards/umarsh.c index 5e29f5c03c..c2a35ab33e 100644 --- a/applications/main/nfc/plugins/supported_cards/umarsh.c +++ b/applications/main/nfc/plugins/supported_cards/umarsh.c @@ -59,7 +59,7 @@ static bool umarsh_parse(const NfcDevice* device, FuriString* parsed_data) { uint32_t header = block_start_ptr[0] << 24 | block_start_ptr[1] << 16 | block_start_ptr[2] << 8 | block_start_ptr[3]; - if(header != 0xFFFFFF7F && header != 0xFEFFFF7F && header != 0xE3FFFF7F) break; + if((header & 0xFFFFFF) != 0xFFFF7F) break; // Data parsing from block 1 block_start_ptr = &data->block[ticket_sector_start_block_number + 1].data[0]; From 63f072a819f2514a84ccbfe8674b996fc9d7f92c Mon Sep 17 00:00:00 2001 From: Methodius Date: Wed, 22 Nov 2023 17:26:10 +0900 Subject: [PATCH 2/5] Umarsh header check rework reworked. Thanks J for idea! --- applications/main/nfc/plugins/supported_cards/umarsh.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/umarsh.c b/applications/main/nfc/plugins/supported_cards/umarsh.c index c2a35ab33e..5e5523467b 100644 --- a/applications/main/nfc/plugins/supported_cards/umarsh.c +++ b/applications/main/nfc/plugins/supported_cards/umarsh.c @@ -57,9 +57,11 @@ static bool umarsh_parse(const NfcDevice* device, FuriString* parsed_data) { //Validate specific for Umarsh ticket sector header const uint8_t* block_start_ptr = &data->block[ticket_sector_start_block_number].data[0]; - uint32_t header = block_start_ptr[0] << 24 | block_start_ptr[1] << 16 | - block_start_ptr[2] << 8 | block_start_ptr[3]; - if((header & 0xFFFFFF) != 0xFFFF7F) break; + uint32_t header_part_0 = block_start_ptr[0] << 24 | block_start_ptr[1] << 16 | + block_start_ptr[2] << 8 | block_start_ptr[3]; + uint32_t header_part_1 = block_start_ptr[4] << 24 | block_start_ptr[5] << 16 | + block_start_ptr[6] << 8 | block_start_ptr[7]; + if((header_part_0 + header_part_1) != 0xFFFFFFFF) break; // Data parsing from block 1 block_start_ptr = &data->block[ticket_sector_start_block_number + 1].data[0]; From 5f18532a59a95587920d9a8f532e92243cd7ead9 Mon Sep 17 00:00:00 2001 From: Methodius Date: Thu, 23 Nov 2023 22:11:12 +0900 Subject: [PATCH 3/5] Umarsh code cleanup --- .../main/nfc/plugins/supported_cards/umarsh.c | 88 ++++++++++++------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/umarsh.c b/applications/main/nfc/plugins/supported_cards/umarsh.c index 5e5523467b..a85c056f6b 100644 --- a/applications/main/nfc/plugins/supported_cards/umarsh.c +++ b/applications/main/nfc/plugins/supported_cards/umarsh.c @@ -38,6 +38,13 @@ #define TAG "Umarsh" +bool parse_datetime(uint16_t date, FuriHalRtcDateTime* result) { + result->year = 2000 + (date >> 9); + result->month = date >> 5 & 0x0F; + result->day = date & 0x1F; + return (date != 0); +} + static bool umarsh_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); @@ -49,65 +56,78 @@ static bool umarsh_parse(const NfcDevice* device, FuriString* parsed_data) { // Verify card type if(data->type != MfClassicType1k) break; - const uint32_t ticket_sector = 8; + const uint8_t ticket_sector = 8; const uint8_t ticket_sector_start_block_number = mf_classic_get_first_block_num_of_sector(ticket_sector); - //Validate specific for Umarsh ticket sector header + // Validate specific for Umarsh ticket sector header const uint8_t* block_start_ptr = &data->block[ticket_sector_start_block_number].data[0]; - uint32_t header_part_0 = block_start_ptr[0] << 24 | block_start_ptr[1] << 16 | - block_start_ptr[2] << 8 | block_start_ptr[3]; - uint32_t header_part_1 = block_start_ptr[4] << 24 | block_start_ptr[5] << 16 | - block_start_ptr[6] << 8 | block_start_ptr[7]; + const uint32_t header_part_0 = nfc_util_bytes2num(block_start_ptr, 4); + const uint32_t header_part_1 = nfc_util_bytes2num(block_start_ptr + 4, 4); if((header_part_0 + header_part_1) != 0xFFFFFFFF) break; // Data parsing from block 1 block_start_ptr = &data->block[ticket_sector_start_block_number + 1].data[0]; - uint8_t region_number = (((block_start_ptr[8] >> 5) & 0x07) << 4) | - (block_start_ptr[12] & 0x0F); - uint32_t card_number = (block_start_ptr[8] << 24 | block_start_ptr[9] << 16 | - block_start_ptr[10] << 8 | block_start_ptr[11]) & - 0x3FFFFFFF; - uint8_t refill_counter = (block_start_ptr[7]); + const uint16_t expiry_date = nfc_util_bytes2num(block_start_ptr + 1, 2); + const uint8_t region_number = (((block_start_ptr[8] >> 5) & 0x07) << 4) | + (block_start_ptr[12] & 0x0F); + const uint8_t refill_counter = nfc_util_bytes2num(block_start_ptr + 7, 1); + const uint32_t card_number = nfc_util_bytes2num(block_start_ptr + 8, 4) & 0x3FFFFFFF; if(card_number == 0) break; // Data parsing from block 2 block_start_ptr = &data->block[ticket_sector_start_block_number + 2].data[0]; - uint16_t expiry_date = (block_start_ptr[0] << 8 | block_start_ptr[1]); - uint32_t terminal_number = - (block_start_ptr[3] << 16 | block_start_ptr[4] << 8 | block_start_ptr[5]); - uint16_t last_refill_date = (block_start_ptr[6] << 8 | block_start_ptr[7]); - uint16_t balance_rub = (block_start_ptr[8] << 8 | block_start_ptr[9]) & 0x7FFF; - uint8_t balance_kop = block_start_ptr[10] & 0x7F; + const uint16_t valid_to = nfc_util_bytes2num(block_start_ptr, 2); + const uint32_t terminal_number = nfc_util_bytes2num(block_start_ptr + 3, 3); + const uint16_t last_refill_date = nfc_util_bytes2num(block_start_ptr + 6, 2); + const uint16_t balance_rub = (nfc_util_bytes2num(block_start_ptr + 8, 2)) & 0x7FFF; + const uint8_t balance_kop = nfc_util_bytes2num(block_start_ptr + 10, 1) & 0x7F; FuriHalRtcDateTime expiry_datetime; - expiry_datetime.year = 2000 + (expiry_date >> 9); - expiry_datetime.month = expiry_date >> 5 & 0x0F; - expiry_datetime.day = expiry_date & 0x1F; + bool is_expiry_datetime_valid = parse_datetime(expiry_date, &expiry_datetime); + + FuriHalRtcDateTime valid_to_datetime; + bool is_valid_to_datetime_valid = parse_datetime(valid_to, &valid_to_datetime); FuriHalRtcDateTime last_refill_datetime; - last_refill_datetime.year = 2000 + (last_refill_date >> 9); - last_refill_datetime.month = last_refill_date >> 5 & 0x0F; - last_refill_datetime.day = last_refill_date & 0x1F; + bool is_last_refill_datetime_valid = + parse_datetime(last_refill_date, &last_refill_datetime); - furi_string_printf( + furi_string_cat_printf( parsed_data, - "\e#Umarsh\nCard number: %lu\nRegion: %02u\nBalance: %u.%u RUR\nTerminal number: %lu\nRefill counter: %u\nLast refill: %02u.%02u.%u\nExpires: %02u.%02u.%u", + "\e#Umarsh\nCard number: %lu\nRegion: %02u\nTerminal number: %lu\nRefill counter: %u\nBalance: %u.%02u RUR", card_number, region_number, - balance_rub, - balance_kop, terminal_number, refill_counter, - last_refill_datetime.day, - last_refill_datetime.month, - last_refill_datetime.year, - expiry_datetime.day, - expiry_datetime.month, - expiry_datetime.year); + balance_rub, + balance_kop); + + if(is_expiry_datetime_valid) + furi_string_cat_printf( + parsed_data, + "\nExpires: %02u.%02u.%u", + expiry_datetime.day, + expiry_datetime.month, + expiry_datetime.year); + if(is_valid_to_datetime_valid) + furi_string_cat_printf( + parsed_data, + "\nValid to: %02u.%02u.%u", + valid_to_datetime.day, + valid_to_datetime.month, + valid_to_datetime.year); + if(is_last_refill_datetime_valid) + furi_string_cat_printf( + parsed_data, + "\nLast refill: %02u.%02u.%u", + last_refill_datetime.day, + last_refill_datetime.month, + last_refill_datetime.year); + parsed = true; } while(false); From 6c2e33263874419b04669aba0c1ae3bb50abb055 Mon Sep 17 00:00:00 2001 From: Methodius Date: Thu, 23 Nov 2023 22:19:26 +0900 Subject: [PATCH 4/5] Kazan transport cards parser added --- applications/main/nfc/application.fam | 9 + .../main/nfc/plugins/supported_cards/kazan.c | 287 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 applications/main/nfc/plugins/supported_cards/kazan.c diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 44e3d842f8..77eaef7f77 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -101,6 +101,15 @@ App( sources=["plugins/supported_cards/metromoney.c"], ) +App( + appid="kazan_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="kazan_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/kazan.c"], +) + App( appid="nfc_start", targets=["f7"], diff --git a/applications/main/nfc/plugins/supported_cards/kazan.c b/applications/main/nfc/plugins/supported_cards/kazan.c new file mode 100644 index 0000000000..8fc9e03e8a --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/kazan.c @@ -0,0 +1,287 @@ +/* + * Parser for Kazan transport card (Kazan, Russia). + * + * Copyright 2023 Leptoptilos + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "core/log.h" +#include "nfc_supported_card_plugin.h" + +#include "protocols/mf_classic/mf_classic.h" +#include + +#include +#include +#include +#include +#include +#include +#include "md5.h" + +#define TAG "Kazan" + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +static const MfClassicKeyPair kazan_1k_keys[] = { + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xE954024EE754, .b = 0x0CD464CDC100}, + {.a = 0xBC305FE2DA65, .b = 0xCF0EC6ACF2F9}, + {.a = 0xF7A545095C49, .b = 0x6862FD600F78}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, +}; + +enum SubscriptionType { + SUBSCRIPTION_TYPE_UNKNOWN, + SUBSCRIPTION_TYPE_PURSE, + SUBSCRIPTION_TYPE_UNLIMITED, +}; + +enum SubscriptionType get_subscription_type(uint8_t value) { + switch(value) { + case 0: + case 0x60: + case 0x67: + case 0x0F: + return SUBSCRIPTION_TYPE_UNLIMITED; + case 0x53: + return SUBSCRIPTION_TYPE_PURSE; + default: + return SUBSCRIPTION_TYPE_UNKNOWN; + } +} + +static bool kazan_verify(Nfc* nfc) { + bool verified = false; + + do { + const uint8_t ticket_sector_number = 8; + const uint8_t ticket_block_number = + mf_classic_get_first_block_num_of_sector(ticket_sector_number) + 1; + FURI_LOG_D(TAG, "Verifying sector %u", ticket_sector_number); + + MfClassicKey key = {0}; + nfc_util_num2bytes(kazan_1k_keys[ticket_sector_number].a, COUNT_OF(key.data), key.data); + + MfClassicAuthContext auth_context; + MfClassicError error = mf_classic_poller_sync_auth( + nfc, ticket_block_number, &key, MfClassicKeyTypeA, &auth_context); + if(error != MfClassicErrorNone) { + FURI_LOG_D(TAG, "Failed to read block %u: %d", ticket_block_number, error); + break; + } + + verified = true; + } while(false); + + return verified; +} + +static bool kazan_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + bool is_read = false; + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + do { + MfClassicType type = MfClassicTypeMini; + MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); + if(error != MfClassicErrorNone) break; + + data->type = type; + if(type != MfClassicType1k) break; + + MfClassicDeviceKeys keys = { + .key_a_mask = 0, + .key_b_mask = 0, + }; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + nfc_util_num2bytes(kazan_1k_keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data); + FURI_BIT_SET(keys.key_a_mask, i); + nfc_util_num2bytes(kazan_1k_keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data); + FURI_BIT_SET(keys.key_b_mask, i); + } + + error = mf_classic_poller_sync_read(nfc, &keys, data); + if(error != MfClassicErrorNone) { + FURI_LOG_W(TAG, "Failed to read data"); + break; + } + + nfc_device_set_data(device, NfcProtocolMfClassic, data); + + is_read = true; + } while(false); + + mf_classic_free(data); + + return is_read; +} + +static bool kazan_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + bool parsed = false; + + do { + const uint8_t ticket_sector_number = 8; + const uint8_t balance_sector_number = 9; + + // Verify keys + MfClassicKeyPair keys = {}; + const MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(data, ticket_sector_number); + + keys.a = nfc_util_bytes2num(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); + keys.b = nfc_util_bytes2num(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data)); + + if((keys.a != 0xE954024EE754) && (keys.b != 0x0CD464CDC100)) break; + + // Parse data + uint8_t start_block_num = mf_classic_get_first_block_num_of_sector(ticket_sector_number); + + const uint8_t* block_start_ptr = &data->block[start_block_num].data[6]; + + enum SubscriptionType subscription_type = get_subscription_type(block_start_ptr[0]); + + FuriHalRtcDateTime valid_from; + valid_from.year = 2000 + block_start_ptr[1]; + valid_from.month = block_start_ptr[2]; + valid_from.day = block_start_ptr[3]; + + FuriHalRtcDateTime valid_to; + valid_to.year = 2000 + block_start_ptr[4]; + valid_to.month = block_start_ptr[5]; + valid_to.day = block_start_ptr[6]; + + const uint8_t last_trip_block_number = 2; + block_start_ptr = &data->block[start_block_num + last_trip_block_number].data[1]; + + FuriHalRtcDateTime last_trip; + last_trip.year = 2000 + block_start_ptr[0]; + last_trip.month = block_start_ptr[1]; + last_trip.day = block_start_ptr[2]; + last_trip.hour = block_start_ptr[3]; + last_trip.minute = block_start_ptr[4]; + bool is_last_trip_valid = (block_start_ptr[0] | block_start_ptr[1] | block_start_ptr[0]) && + (last_trip.day < 32 && last_trip.month < 12 && + last_trip.hour < 24 && last_trip.minute < 60); + + start_block_num = mf_classic_get_first_block_num_of_sector(balance_sector_number); + block_start_ptr = &data->block[start_block_num].data[0]; + + const uint32_t trip_counter = (block_start_ptr[3] << 24) | (block_start_ptr[2] << 16) | + (block_start_ptr[1] << 8) | (block_start_ptr[0]); + + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + const uint32_t card_number = (uid[3] << 24) | (uid[2] << 16) | (uid[1] << 8) | (uid[0]); + + furi_string_cat_printf( + parsed_data, "\e#Kazan transport card\nCard number: %lu\n", card_number); + + if(subscription_type == SUBSCRIPTION_TYPE_PURSE) { + furi_string_cat_printf( + parsed_data, + "Type: purse\nBalance: %lu RUR\nBalance valid:\nfrom: %02u.%02u.%u\nto: %02u.%02u.%u", + trip_counter, + valid_from.day, + valid_from.month, + valid_from.year, + valid_to.day, + valid_to.month, + valid_to.year); + } + + if(subscription_type == SUBSCRIPTION_TYPE_UNLIMITED) { + furi_string_cat_printf( + parsed_data, + "Type: unlimited\nTrips left: %lu\nCard valid:\nfrom: %02u.%02u.%u\nto: %02u.%02u.%u", + trip_counter, + valid_from.day, + valid_from.month, + valid_from.year, + valid_to.day, + valid_to.month, + valid_to.year); + } + + if(subscription_type == SUBSCRIPTION_TYPE_UNKNOWN) { + furi_string_cat_printf( + parsed_data, + "Type: Unknown\nBalance: %lu RUR\nValid from: %02u.%02u.%u\nValid to: %02u.%02u.%u", + trip_counter, + valid_from.day, + valid_from.month, + valid_from.year, + valid_to.day, + valid_to.month, + valid_to.year); + } + + if(is_last_trip_valid) { + furi_string_cat_printf( + parsed_data, + "\nLast trip: %02u.%02u.%u at %02u:%02u", + last_trip.day, + last_trip.month, + last_trip.year, + last_trip.hour, + last_trip.minute); + } + + parsed = true; + } while(false); + + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin kazan_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = kazan_verify, + .read = kazan_read, + .parse = kazan_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor kazan_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &kazan_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* kazan_plugin_ep() { + return &kazan_plugin_descriptor; +} \ No newline at end of file From 8c5f28d6a002de98a64c3c0d414237d3fb9c0502 Mon Sep 17 00:00:00 2001 From: Methodius Date: Thu, 23 Nov 2023 23:59:06 +0900 Subject: [PATCH 5/5] Kazan parser: Type: abonnement fix --- applications/main/nfc/plugins/supported_cards/kazan.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/kazan.c b/applications/main/nfc/plugins/supported_cards/kazan.c index 8fc9e03e8a..68ef6c16f6 100644 --- a/applications/main/nfc/plugins/supported_cards/kazan.c +++ b/applications/main/nfc/plugins/supported_cards/kazan.c @@ -59,7 +59,7 @@ static const MfClassicKeyPair kazan_1k_keys[] = { enum SubscriptionType { SUBSCRIPTION_TYPE_UNKNOWN, SUBSCRIPTION_TYPE_PURSE, - SUBSCRIPTION_TYPE_UNLIMITED, + SUBSCRIPTION_TYPE_ABONNEMENT, }; enum SubscriptionType get_subscription_type(uint8_t value) { @@ -68,7 +68,7 @@ enum SubscriptionType get_subscription_type(uint8_t value) { case 0x60: case 0x67: case 0x0F: - return SUBSCRIPTION_TYPE_UNLIMITED; + return SUBSCRIPTION_TYPE_ABONNEMENT; case 0x53: return SUBSCRIPTION_TYPE_PURSE; default: @@ -223,10 +223,10 @@ static bool kazan_parse(const NfcDevice* device, FuriString* parsed_data) { valid_to.year); } - if(subscription_type == SUBSCRIPTION_TYPE_UNLIMITED) { + if(subscription_type == SUBSCRIPTION_TYPE_ABONNEMENT) { furi_string_cat_printf( parsed_data, - "Type: unlimited\nTrips left: %lu\nCard valid:\nfrom: %02u.%02u.%u\nto: %02u.%02u.%u", + "Type: abonnement\nTrips left: %lu\nCard valid:\nfrom: %02u.%02u.%u\nto: %02u.%02u.%u", trip_counter, valid_from.day, valid_from.month, @@ -239,7 +239,7 @@ static bool kazan_parse(const NfcDevice* device, FuriString* parsed_data) { if(subscription_type == SUBSCRIPTION_TYPE_UNKNOWN) { furi_string_cat_printf( parsed_data, - "Type: Unknown\nBalance: %lu RUR\nValid from: %02u.%02u.%u\nValid to: %02u.%02u.%u", + "Type: unknown\nBalance: %lu RUR\nValid from: %02u.%02u.%u\nValid to: %02u.%02u.%u", trip_counter, valid_from.day, valid_from.month,