1+ /*
2+ * Parser for Zolotaya Korona Online card (Russia).
3+ * Tariffs research by DNZ1393
4+ *
5+ * Copyright 2023 Leptoptilos <[email protected] > 6+ *
7+ * This program is free software: you can redistribute it and/or modify it
8+ * under the terms of the GNU General Public License as published by
9+ * the Free Software Foundation, either version 3 of the License, or
10+ * (at your option) any later version.
11+ *
12+ * This program is distributed in the hope that it will be useful, but
13+ * WITHOUT ANY WARRANTY; without even the implied warranty of
14+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+ * General Public License for more details.
16+ *
17+ * You should have received a copy of the GNU General Public License
18+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
19+ */
20+ #include "furi_hal_rtc.h"
21+ #include "nfc_supported_card_plugin.h"
22+
23+ #include "protocols/mf_classic/mf_classic.h"
24+ #include <flipper_application/flipper_application.h>
25+
26+ #include <nfc/nfc_device.h>
27+ #include <nfc/helpers/nfc_util.h>
28+ #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
29+
30+ #define TAG "Zolotaya Korona Online"
31+
32+ #define TRIP_SECTOR_NUM (4)
33+ #define INFO_SECTOR_NUM (15)
34+
35+ uint64_t bytes2num_bcd (const uint8_t * src , uint8_t len_bytes , bool * is_bcd ) {
36+ furi_assert (src );
37+ furi_assert (len_bytes <= 9 );
38+
39+ uint64_t result = 0 ;
40+ * is_bcd = true;
41+
42+ for (uint8_t i = 0 ; i < len_bytes ; i ++ ) {
43+ if (((src [i ] / 16 ) > 9 ) || ((src [i ] % 16 ) > 9 )) * is_bcd = false;
44+
45+ result *= 10 ;
46+ result += src [i ] / 16 ;
47+
48+ result *= 10 ;
49+ result += src [i ] % 16 ;
50+ }
51+
52+ return result ;
53+ }
54+
55+ bool parse_online_card_tariff (uint16_t tariff_num , FuriString * tariff_name ) {
56+ bool tariff_parsed = false;
57+
58+ switch (tariff_num ) {
59+ case 0x0100 :
60+ furi_string_set_str (tariff_name , "Standart (online)" );
61+ tariff_parsed = true;
62+ break ;
63+ case 0x0101 :
64+ case 0x0121 :
65+ furi_string_set_str (tariff_name , "Standart (airtag)" );
66+ tariff_parsed = true;
67+ break ;
68+ case 0x0401 :
69+ furi_string_set_str (tariff_name , "Student (50%% discount)" );
70+ tariff_parsed = true;
71+ break ;
72+ case 0x0402 :
73+ furi_string_set_str (tariff_name , "Student (travel)" );
74+ tariff_parsed = true;
75+ break ;
76+ case 0x0002 :
77+ furi_string_set_str (tariff_name , "School (50%% discount)" );
78+ tariff_parsed = true;
79+ break ;
80+ case 0x0505 :
81+ furi_string_set_str (tariff_name , "Social (large families)" );
82+ tariff_parsed = true;
83+ break ;
84+ case 0x0528 :
85+ furi_string_set_str (tariff_name , "Social (handicapped)" );
86+ tariff_parsed = true;
87+ break ;
88+ default :
89+ furi_string_set_str (tariff_name , "Unknown" );
90+ tariff_parsed = false;
91+ break ;
92+ }
93+
94+ return tariff_parsed ;
95+ }
96+
97+ static bool zolotaya_korona_online_parse (const NfcDevice * device , FuriString * parsed_data ) {
98+ furi_assert (device );
99+
100+ const MfClassicData * data = nfc_device_get_data (device , NfcProtocolMfClassic );
101+
102+ bool parsed = false;
103+
104+ do {
105+ // Verify info sector data (card number prefix)
106+ const uint8_t start_trip_block_number =
107+ mf_classic_get_first_block_num_of_sector (TRIP_SECTOR_NUM );
108+ const uint8_t start_info_block_number =
109+ mf_classic_get_first_block_num_of_sector (INFO_SECTOR_NUM );
110+ const uint8_t * block_start_ptr = & data -> block [start_info_block_number ].data [3 ];
111+
112+ // Validate card number
113+ bool is_bcd ;
114+ const uint16_t card_number_prefix = bytes2num_bcd (block_start_ptr , 2 , & is_bcd );
115+ if (!is_bcd ) break ;
116+ if (card_number_prefix != 9643 ) break ;
117+ const uint64_t card_number_postfix = bytes2num_bcd (block_start_ptr + 2 , 8 , & is_bcd ) / 10 ;
118+ if (!is_bcd ) break ;
119+
120+ // Parse data
121+ FuriString * tariff_name = furi_string_alloc ();
122+
123+ block_start_ptr = & data -> block [start_info_block_number ].data [1 ];
124+ const uint16_t tariff = nfc_util_bytes2num (block_start_ptr , 2 );
125+ parse_online_card_tariff (tariff , tariff_name );
126+
127+ block_start_ptr = & data -> block [start_trip_block_number ].data [0 ];
128+ const uint8_t region_number = nfc_util_bytes2num (block_start_ptr , 1 );
129+
130+ furi_string_cat_printf (
131+ parsed_data ,
132+ "\e#Zolotaya korona\nCard number: %u%015llu\nTariff: %02X.%02X: %s\nRegion: %u\n" ,
133+ card_number_prefix ,
134+ card_number_postfix ,
135+ tariff / 256 ,
136+ tariff % 256 ,
137+ furi_string_get_cstr (tariff_name ),
138+ region_number );
139+
140+ furi_string_free (tariff_name );
141+
142+ parsed = true;
143+ } while (false);
144+
145+ return parsed ;
146+ }
147+
148+ /* Actual implementation of app<>plugin interface */
149+ static const NfcSupportedCardsPlugin zolotaya_korona_online_plugin = {
150+ .protocol = NfcProtocolMfClassic ,
151+ .verify = NULL ,
152+ .read = NULL ,
153+ .parse = zolotaya_korona_online_parse ,
154+ };
155+
156+ /* Plugin descriptor to comply with basic plugin specification */
157+ static const FlipperAppPluginDescriptor zolotaya_korona_online_plugin_descriptor = {
158+ .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID ,
159+ .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION ,
160+ .entry_point = & zolotaya_korona_online_plugin ,
161+ };
162+
163+ /* Plugin entry point - must return a pointer to const descriptor */
164+ const FlipperAppPluginDescriptor * zolotaya_korona_online_plugin_ep () {
165+ return & zolotaya_korona_online_plugin_descriptor ;
166+ }
0 commit comments