diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 4ea2e6928..d74447844 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -146,6 +146,15 @@ App( sources=["plugins/supported_cards/zolotaya_korona.c"], ) +App( + appid="zolotaya_korona_online_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="zolotaya_korona_online_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/zolotaya_korona_online.c"], +) + App( appid="hid_parser", apptype=FlipperAppType.PLUGIN, diff --git a/applications/main/nfc/plugins/supported_cards/kazan.c b/applications/main/nfc/plugins/supported_cards/kazan.c index f14c11369..a1bcbb1f8 100644 --- a/applications/main/nfc/plugins/supported_cards/kazan.c +++ b/applications/main/nfc/plugins/supported_cards/kazan.c @@ -328,6 +328,8 @@ static bool kazan_parse(const NfcDevice* device, FuriString* parsed_data) { last_trip.minute); } + furi_string_free(tariff_name); + parsed = true; } while(false); diff --git a/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c b/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c index 2e4f51598..1778cd38e 100644 --- a/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c +++ b/applications/main/nfc/plugins/supported_cards/zolotaya_korona.c @@ -34,11 +34,6 @@ #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, @@ -76,19 +71,24 @@ void timestamp_to_datetime(uint32_t timestamp, FuriHalRtcDateTime* datetime) { datetime->second = seconds_in_day % FURI_HAL_RTC_SECONDS_PER_MINUTE; } -uint64_t bytes2num_bcd(const uint8_t* src, uint8_t len_bytes) { +uint64_t bytes2num_bcd(const uint8_t* src, uint8_t len_bytes, bool* is_bcd) { furi_assert(src); + furi_assert(len_bytes <= 9); - uint64_t res = 0; + uint64_t result = 0; + *is_bcd = true; for(uint8_t i = 0; i < len_bytes; i++) { - res *= 10; - res += src[i] / 16; - res *= 10; - res += src[i] % 16; + if(((src[i] / 16) > 9) || ((src[i] % 16) > 9)) *is_bcd = false; + + result *= 10; + result += src[i] / 16; + + result *= 10; + result += src[i] % 16; } - return res; + return result; } static bool zolotaya_korona_parse(const NfcDevice* device, FuriString* parsed_data) { @@ -121,12 +121,12 @@ static bool zolotaya_korona_parse(const NfcDevice* device, FuriString* parsed_da // INFO SECTOR // block 1 - const uint8_t region_number = bytes2num_bcd(block_start_ptr + 10, 1); + const uint8_t region_number = bytes2num_bcd(block_start_ptr + 10, 1, &verified); // 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; + const uint16_t card_number_prefix = bytes2num_bcd(block_start_ptr, 2, &verified); + const uint64_t card_number_postfix = bytes2num_bcd(block_start_ptr + 2, 8, &verified) / 10; // TRIP SECTOR const uint8_t start_trip_block_number = @@ -157,7 +157,7 @@ static bool zolotaya_korona_parse(const NfcDevice* device, FuriString* parsed_da 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 validator_id = bytes2num_bcd(block_start_ptr + 2, 3, &verified); 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); @@ -174,15 +174,16 @@ static bool zolotaya_korona_parse(const NfcDevice* device, FuriString* parsed_da 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); + const 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, + "\e#Zolotaya korona\nCard number: %u%015llu\nRegion: %u\nBalance: %lu.%02u RUR\nPrev. balance: %lu.%02u RUR", + card_number_prefix, + card_number_postfix, region_number, balance_rub, balance_kop, diff --git a/applications/main/nfc/plugins/supported_cards/zolotaya_korona_online.c b/applications/main/nfc/plugins/supported_cards/zolotaya_korona_online.c new file mode 100644 index 000000000..eb992763e --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/zolotaya_korona_online.c @@ -0,0 +1,166 @@ +/* + * Parser for Zolotaya Korona Online card (Russia). + * Tariffs research by DNZ1393 + * + * 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 "furi_hal_rtc.h" +#include "nfc_supported_card_plugin.h" + +#include "protocols/mf_classic/mf_classic.h" +#include + +#include +#include +#include + +#define TAG "Zolotaya Korona Online" + +#define TRIP_SECTOR_NUM (4) +#define INFO_SECTOR_NUM (15) + +uint64_t bytes2num_bcd(const uint8_t* src, uint8_t len_bytes, bool* is_bcd) { + furi_assert(src); + furi_assert(len_bytes <= 9); + + uint64_t result = 0; + *is_bcd = true; + + for(uint8_t i = 0; i < len_bytes; i++) { + if(((src[i] / 16) > 9) || ((src[i] % 16) > 9)) *is_bcd = false; + + result *= 10; + result += src[i] / 16; + + result *= 10; + result += src[i] % 16; + } + + return result; +} + +bool parse_online_card_tariff(uint16_t tariff_num, FuriString* tariff_name) { + bool tariff_parsed = false; + + switch(tariff_num) { + case 0x0100: + furi_string_set_str(tariff_name, "Standart (online)"); + tariff_parsed = true; + break; + case 0x0101: + case 0x0121: + furi_string_set_str(tariff_name, "Standart (airtag)"); + tariff_parsed = true; + break; + case 0x0401: + furi_string_set_str(tariff_name, "Student (50%% discount)"); + tariff_parsed = true; + break; + case 0x0402: + furi_string_set_str(tariff_name, "Student (travel)"); + tariff_parsed = true; + break; + case 0x0002: + furi_string_set_str(tariff_name, "School (50%% discount)"); + tariff_parsed = true; + break; + case 0x0505: + furi_string_set_str(tariff_name, "Social (large families)"); + tariff_parsed = true; + break; + case 0x0528: + furi_string_set_str(tariff_name, "Social (handicapped)"); + tariff_parsed = true; + break; + default: + furi_string_set_str(tariff_name, "Unknown"); + tariff_parsed = false; + break; + } + + return tariff_parsed; +} + +static bool zolotaya_korona_online_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 (card number prefix) + const uint8_t start_trip_block_number = + mf_classic_get_first_block_num_of_sector(TRIP_SECTOR_NUM); + 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[3]; + + // Validate card number + bool is_bcd; + const uint16_t card_number_prefix = bytes2num_bcd(block_start_ptr, 2, &is_bcd); + if(!is_bcd) break; + if(card_number_prefix != 9643) break; + const uint64_t card_number_postfix = bytes2num_bcd(block_start_ptr + 2, 8, &is_bcd) / 10; + if(!is_bcd) break; + + // Parse data + FuriString* tariff_name = furi_string_alloc(); + + block_start_ptr = &data->block[start_info_block_number].data[1]; + const uint16_t tariff = nfc_util_bytes2num(block_start_ptr, 2); + parse_online_card_tariff(tariff, tariff_name); + + block_start_ptr = &data->block[start_trip_block_number].data[0]; + const uint8_t region_number = nfc_util_bytes2num(block_start_ptr, 1); + + furi_string_cat_printf( + parsed_data, + "\e#Zolotaya korona\nCard number: %u%015llu\nTariff: %02X.%02X: %s\nRegion: %u\n", + card_number_prefix, + card_number_postfix, + tariff / 256, + tariff % 256, + furi_string_get_cstr(tariff_name), + region_number); + + furi_string_free(tariff_name); + + parsed = true; + } while(false); + + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin zolotaya_korona_online_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = NULL, + .read = NULL, + .parse = zolotaya_korona_online_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor zolotaya_korona_online_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &zolotaya_korona_online_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* zolotaya_korona_online_plugin_ep() { + return &zolotaya_korona_online_plugin_descriptor; +} \ No newline at end of file