From c53f5aa5b551b518c6cac2f02a1d8c38782a42f2 Mon Sep 17 00:00:00 2001 From: Methodius Date: Sun, 17 Dec 2023 04:58:07 +0900 Subject: [PATCH] Zolotaya Korona transport card parser added --- applications/main/nfc/application.fam | 9 + .../plugins/supported_cards/zolotaya_korona.c | 254 ++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 applications/main/nfc/plugins/supported_cards/zolotaya_korona.c diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 4a31458d8..1b999e37e 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -137,6 +137,15 @@ App( sources=["plugins/supported_cards/mykey.c"], ) +App( + appid="zolotaya_korona_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="zolotaya_korona_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/zolotaya_korona.c"], +) + App( appid="nfc_start", targets=["f7"], diff --git a/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c b/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c new file mode 100644 index 000000000..1c48f967a --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c @@ -0,0 +1,254 @@ +/* + * Parser for Zolotaya Korona card (Russia). + * + * Copyright 2023 Leptoptilos + * + * More info about Zolotaya Korona cards: https://github.com/metrodroid/metrodroid/wiki/Zolotaya-Korona + * + * 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/core_defines.h" +#include "core/string.h" +#include "furi_hal_rtc.h" +#include "nfc_supported_card_plugin.h" + +#include "protocols/mf_classic/mf_classic.h" +#include + +#include +#include +#include +#include +#include + +#define TAG "Zolotaya Korona" + +#define TRIP_SECTOR_NUM (4) +#define PURSE_SECTOR_NUM (6) +#define INFO_SECTOR_NUM (15) + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +// Sector 15 data. Byte [11] contains the mistake. If byte [11] was 0xEF, bytes [1-18] means "ЗАО Золотая Корона" +static const uint8_t info_sector_signature[] = {0xE2, 0x87, 0x80, 0x8E, 0x20, 0x87, 0xAE, + 0xAB, 0xAE, 0xF2, 0xA0, 0xEF, 0x20, 0x8A, + 0xAE, 0xE0, 0xAE, 0xAD, 0xA0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00}; + +#define FURI_HAL_RTC_SECONDS_PER_MINUTE 60 +#define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60) +#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) +#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 + +void timestamp_to_datetime(uint32_t timestamp, FuriHalRtcDateTime* datetime) { + uint32_t days = timestamp / FURI_HAL_RTC_SECONDS_PER_DAY; + uint32_t seconds_in_day = timestamp % FURI_HAL_RTC_SECONDS_PER_DAY; + + datetime->year = FURI_HAL_RTC_EPOCH_START_YEAR; + + while(days >= furi_hal_rtc_get_days_per_year(datetime->year)) { + days -= furi_hal_rtc_get_days_per_year(datetime->year); + (datetime->year)++; + } + + datetime->month = 1; + while(days >= furi_hal_rtc_get_days_per_month( + furi_hal_rtc_is_leap_year(datetime->year), datetime->month)) { + days -= furi_hal_rtc_get_days_per_month( + furi_hal_rtc_is_leap_year(datetime->year), datetime->month); + (datetime->month)++; + } + + datetime->day = days + 1; + datetime->hour = seconds_in_day / FURI_HAL_RTC_SECONDS_PER_HOUR; + datetime->minute = + (seconds_in_day % FURI_HAL_RTC_SECONDS_PER_HOUR) / FURI_HAL_RTC_SECONDS_PER_MINUTE; + datetime->second = seconds_in_day % FURI_HAL_RTC_SECONDS_PER_MINUTE; +} + +uint64_t bytes2num_bcd(const uint8_t* src, uint8_t len_bytes) { + furi_assert(src); + + uint64_t res = 0; + + for(uint8_t i = 0; i < len_bytes; i++) { + res *= 10; + res += src[i] / 16; + res *= 10; + res += src[i] % 16; + } + + return res; +} + +static bool zolotaya_korona_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + bool parsed = false; + + do { + // Verify info sector data + const uint8_t start_info_block_number = + mf_classic_get_first_block_num_of_sector(INFO_SECTOR_NUM); + const uint8_t* block_start_ptr = &data->block[start_info_block_number].data[0]; + + bool verified = true; + for(uint8_t i = 0; i < sizeof(info_sector_signature); i++) { + if(i == 16) { + block_start_ptr = &data->block[start_info_block_number + 1].data[0]; + } + if(block_start_ptr[i % 16] != info_sector_signature[i]) { + verified = false; + break; + } + } + + if(!verified) break; + + // Parse data + + // INFO SECTOR + // block 1 + const uint8_t region_number = bytes2num_bcd(block_start_ptr + 10, 1); + + // block 2 + block_start_ptr = &data->block[start_info_block_number + 2].data[4]; + const uint64_t card_number = + bytes2num_bcd(block_start_ptr, 9) * 10 + bytes2num_bcd(block_start_ptr + 9, 1) / 10; + + // TRIP SECTOR + const uint8_t start_trip_block_number = + mf_classic_get_first_block_num_of_sector(TRIP_SECTOR_NUM); + // block 0 + block_start_ptr = &data->block[start_trip_block_number].data[7]; + + const uint8_t status = block_start_ptr[0] % 16; + const uint16_t sequence_number = nfc_util_bytes2num(block_start_ptr + 1, 2); + const uint8_t discount_code = nfc_util_bytes2num(block_start_ptr + 3, 1); + + // block 1: refill block + block_start_ptr = &data->block[start_trip_block_number + 1].data[1]; + + const uint16_t refill_machine_id = nfc_util_bytes2num_little_endian(block_start_ptr, 2); + const uint32_t last_refill_timestamp = + nfc_util_bytes2num_little_endian(block_start_ptr + 2, 4); + const uint32_t last_refill_amount = + nfc_util_bytes2num_little_endian(block_start_ptr + 6, 4); + const uint32_t last_refill_amount_rub = last_refill_amount / 100; + const uint8_t last_refill_amount_kop = last_refill_amount % 100; + const uint16_t refill_counter = nfc_util_bytes2num_little_endian(block_start_ptr + 10, 2); + + FuriHalRtcDateTime last_refill_datetime = {0}; + timestamp_to_datetime(last_refill_timestamp, &last_refill_datetime); + + // block 2: trip block + block_start_ptr = &data->block[start_trip_block_number + 2].data[0]; + const char validator_first_letter = + nfc_util_bytes2num_little_endian(block_start_ptr + 1, 1); + const uint32_t validator_id = bytes2num_bcd(block_start_ptr + 2, 3); + const uint32_t last_trip_timestamp = + nfc_util_bytes2num_little_endian(block_start_ptr + 6, 4); + const uint8_t track_number = nfc_util_bytes2num_little_endian(block_start_ptr + 10, 1); + const uint32_t prev_balance = nfc_util_bytes2num_little_endian(block_start_ptr + 11, 4); + const uint32_t prev_balance_rub = prev_balance / 100; + const uint8_t prev_balance_kop = prev_balance % 100; + + FuriHalRtcDateTime last_trip_datetime = {0}; + timestamp_to_datetime(last_trip_timestamp, &last_trip_datetime); + + // PARSE DATA FROM PURSE SECTOR + const uint8_t start_purse_block_number = + mf_classic_get_first_block_num_of_sector(PURSE_SECTOR_NUM); + block_start_ptr = &data->block[start_purse_block_number].data[0]; + + // block 0 + uint32_t balance = nfc_util_bytes2num_little_endian(block_start_ptr, 4); + + uint32_t balance_rub = balance / 100; + uint8_t balance_kop = balance % 100; + + furi_string_cat_printf( + parsed_data, + "\e#Zolotaya korona\nCard number: %llu\nRegion: %u\nBalance: %lu.%02u RUR\nPrev. balance: %lu.%02u RUR", + card_number, + region_number, + balance_rub, + balance_kop, + prev_balance_rub, + prev_balance_kop); + + furi_string_cat_printf( + parsed_data, + "\nLast refill amount: %lu.%02u RUR\nRefill counter: %u\nLast refill: %u.%02u.%02u %02u:%02u\nRefill machine id: %u", + last_refill_amount_rub, + last_refill_amount_kop, + refill_counter, + last_refill_datetime.day, + last_refill_datetime.month, + last_refill_datetime.year, + last_refill_datetime.hour, + last_refill_datetime.minute, + refill_machine_id); + + furi_string_cat_printf( + parsed_data, + "\nLast trip: %u.%02u.%02u %02u:%02u\nTrack number: %u\nValidator: %c%06lu", + last_trip_datetime.day, + last_trip_datetime.month, + last_trip_datetime.year, + last_trip_datetime.hour, + last_trip_datetime.minute, + track_number, + validator_first_letter, + validator_id); + + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + furi_string_cat_printf( + parsed_data, + "\nStatus: %u\nSequence num: %u\nDiscount code: %u", + status, + sequence_number, + discount_code); + } + + parsed = true; + } while(false); + + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin zolotaya_korona_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = NULL, + .read = NULL, + .parse = zolotaya_korona_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor zolotaya_korona_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &zolotaya_korona_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* zolotaya_korona_plugin_ep() { + return &zolotaya_korona_plugin_descriptor; +} \ No newline at end of file