1
+ /*
2
+ * Parser for Zolotaya Korona card (Russia).
3
+ *
4
+ * Copyright 2023 Leptoptilos <[email protected] >
5
+ *
6
+ * More info about Zolotaya Korona cards: https://github.com/metrodroid/metrodroid/wiki/Zolotaya-Korona
7
+ *
8
+ * This program is free software: you can redistribute it and/or modify it
9
+ * under the terms of the GNU General Public License as published by
10
+ * the Free Software Foundation, either version 3 of the License, or
11
+ * (at your option) any later version.
12
+ *
13
+ * This program is distributed in the hope that it will be useful, but
14
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
+ * General Public License for more details.
17
+ *
18
+ * You should have received a copy of the GNU General Public License
19
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+ */
21
+ #include "core/core_defines.h"
22
+ #include "core/string.h"
23
+ #include "furi_hal_rtc.h"
24
+ #include "nfc_supported_card_plugin.h"
25
+
26
+ #include "protocols/mf_classic/mf_classic.h"
27
+ #include <flipper_application/flipper_application.h>
28
+
29
+ #include <nfc/nfc_device.h>
30
+ #include <nfc/helpers/nfc_util.h>
31
+ #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
32
+ #include <stdbool.h>
33
+ #include <stdint.h>
34
+
35
+ #define TAG "Zolotaya Korona"
36
+
37
+ #define TRIP_SECTOR_NUM (4)
38
+ #define PURSE_SECTOR_NUM (6)
39
+ #define INFO_SECTOR_NUM (15)
40
+
41
+ typedef struct {
42
+ uint64_t a ;
43
+ uint64_t b ;
44
+ } MfClassicKeyPair ;
45
+
46
+ // Sector 15 data. Byte [11] contains the mistake. If byte [11] was 0xEF, bytes [1-18] means "ЗАО Золотая Корона"
47
+ static const uint8_t info_sector_signature [] = {0xE2 , 0x87 , 0x80 , 0x8E , 0x20 , 0x87 , 0xAE ,
48
+ 0xAB , 0xAE , 0xF2 , 0xA0 , 0xEF , 0x20 , 0x8A ,
49
+ 0xAE , 0xE0 , 0xAE , 0xAD , 0xA0 , 0x00 , 0x00 ,
50
+ 0x00 , 0x00 , 0x00 , 0x00 };
51
+
52
+ #define FURI_HAL_RTC_SECONDS_PER_MINUTE 60
53
+ #define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60)
54
+ #define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24)
55
+ #define FURI_HAL_RTC_EPOCH_START_YEAR 1970
56
+
57
+ void timestamp_to_datetime (uint32_t timestamp , FuriHalRtcDateTime * datetime ) {
58
+ uint32_t days = timestamp / FURI_HAL_RTC_SECONDS_PER_DAY ;
59
+ uint32_t seconds_in_day = timestamp % FURI_HAL_RTC_SECONDS_PER_DAY ;
60
+
61
+ datetime -> year = FURI_HAL_RTC_EPOCH_START_YEAR ;
62
+
63
+ while (days >= furi_hal_rtc_get_days_per_year (datetime -> year )) {
64
+ days -= furi_hal_rtc_get_days_per_year (datetime -> year );
65
+ (datetime -> year )++ ;
66
+ }
67
+
68
+ datetime -> month = 1 ;
69
+ while (days >= furi_hal_rtc_get_days_per_month (
70
+ furi_hal_rtc_is_leap_year (datetime -> year ), datetime -> month )) {
71
+ days -= furi_hal_rtc_get_days_per_month (
72
+ furi_hal_rtc_is_leap_year (datetime -> year ), datetime -> month );
73
+ (datetime -> month )++ ;
74
+ }
75
+
76
+ datetime -> day = days + 1 ;
77
+ datetime -> hour = seconds_in_day / FURI_HAL_RTC_SECONDS_PER_HOUR ;
78
+ datetime -> minute =
79
+ (seconds_in_day % FURI_HAL_RTC_SECONDS_PER_HOUR ) / FURI_HAL_RTC_SECONDS_PER_MINUTE ;
80
+ datetime -> second = seconds_in_day % FURI_HAL_RTC_SECONDS_PER_MINUTE ;
81
+ }
82
+
83
+ uint64_t bytes2num_bcd (const uint8_t * src , uint8_t len_bytes ) {
84
+ furi_assert (src );
85
+
86
+ uint64_t res = 0 ;
87
+
88
+ for (uint8_t i = 0 ; i < len_bytes ; i ++ ) {
89
+ res *= 10 ;
90
+ res += src [i ] / 16 ;
91
+ res *= 10 ;
92
+ res += src [i ] % 16 ;
93
+ }
94
+
95
+ return res ;
96
+ }
97
+
98
+ static bool zolotaya_korona_parse (const NfcDevice * device , FuriString * parsed_data ) {
99
+ furi_assert (device );
100
+
101
+ const MfClassicData * data = nfc_device_get_data (device , NfcProtocolMfClassic );
102
+
103
+ bool parsed = false;
104
+
105
+ do {
106
+ // Verify info sector data
107
+ const uint8_t start_info_block_number =
108
+ mf_classic_get_first_block_num_of_sector (INFO_SECTOR_NUM );
109
+ const uint8_t * block_start_ptr = & data -> block [start_info_block_number ].data [0 ];
110
+
111
+ bool verified = true;
112
+ for (uint8_t i = 0 ; i < sizeof (info_sector_signature ); i ++ ) {
113
+ if (i == 16 ) {
114
+ block_start_ptr = & data -> block [start_info_block_number + 1 ].data [0 ];
115
+ }
116
+ if (block_start_ptr [i % 16 ] != info_sector_signature [i ]) {
117
+ verified = false;
118
+ break ;
119
+ }
120
+ }
121
+
122
+ if (!verified ) break ;
123
+
124
+ // Parse data
125
+
126
+ // INFO SECTOR
127
+ // block 1
128
+ const uint8_t region_number = bytes2num_bcd (block_start_ptr + 10 , 1 );
129
+
130
+ // block 2
131
+ block_start_ptr = & data -> block [start_info_block_number + 2 ].data [4 ];
132
+ const uint64_t card_number =
133
+ bytes2num_bcd (block_start_ptr , 9 ) * 10 + bytes2num_bcd (block_start_ptr + 9 , 1 ) / 10 ;
134
+
135
+ // TRIP SECTOR
136
+ const uint8_t start_trip_block_number =
137
+ mf_classic_get_first_block_num_of_sector (TRIP_SECTOR_NUM );
138
+ // block 0
139
+ block_start_ptr = & data -> block [start_trip_block_number ].data [7 ];
140
+
141
+ const uint8_t status = block_start_ptr [0 ] % 16 ;
142
+ const uint16_t sequence_number = nfc_util_bytes2num (block_start_ptr + 1 , 2 );
143
+ const uint8_t discount_code = nfc_util_bytes2num (block_start_ptr + 3 , 1 );
144
+
145
+ // block 1: refill block
146
+ block_start_ptr = & data -> block [start_trip_block_number + 1 ].data [1 ];
147
+
148
+ const uint16_t refill_machine_id = nfc_util_bytes2num_little_endian (block_start_ptr , 2 );
149
+ const uint32_t last_refill_timestamp =
150
+ nfc_util_bytes2num_little_endian (block_start_ptr + 2 , 4 );
151
+ const uint32_t last_refill_amount =
152
+ nfc_util_bytes2num_little_endian (block_start_ptr + 6 , 4 );
153
+ const uint32_t last_refill_amount_rub = last_refill_amount / 100 ;
154
+ const uint8_t last_refill_amount_kop = last_refill_amount % 100 ;
155
+ const uint16_t refill_counter = nfc_util_bytes2num_little_endian (block_start_ptr + 10 , 2 );
156
+
157
+ FuriHalRtcDateTime last_refill_datetime = {0 };
158
+ timestamp_to_datetime (last_refill_timestamp , & last_refill_datetime );
159
+
160
+ // block 2: trip block
161
+ block_start_ptr = & data -> block [start_trip_block_number + 2 ].data [0 ];
162
+ const char validator_first_letter =
163
+ nfc_util_bytes2num_little_endian (block_start_ptr + 1 , 1 );
164
+ const uint32_t validator_id = bytes2num_bcd (block_start_ptr + 2 , 3 );
165
+ const uint32_t last_trip_timestamp =
166
+ nfc_util_bytes2num_little_endian (block_start_ptr + 6 , 4 );
167
+ const uint8_t track_number = nfc_util_bytes2num_little_endian (block_start_ptr + 10 , 1 );
168
+ const uint32_t prev_balance = nfc_util_bytes2num_little_endian (block_start_ptr + 11 , 4 );
169
+ const uint32_t prev_balance_rub = prev_balance / 100 ;
170
+ const uint8_t prev_balance_kop = prev_balance % 100 ;
171
+
172
+ FuriHalRtcDateTime last_trip_datetime = {0 };
173
+ timestamp_to_datetime (last_trip_timestamp , & last_trip_datetime );
174
+
175
+ // PARSE DATA FROM PURSE SECTOR
176
+ const uint8_t start_purse_block_number =
177
+ mf_classic_get_first_block_num_of_sector (PURSE_SECTOR_NUM );
178
+ block_start_ptr = & data -> block [start_purse_block_number ].data [0 ];
179
+
180
+ // block 0
181
+ uint32_t balance = nfc_util_bytes2num_little_endian (block_start_ptr , 4 );
182
+
183
+ uint32_t balance_rub = balance / 100 ;
184
+ uint8_t balance_kop = balance % 100 ;
185
+
186
+ furi_string_cat_printf (
187
+ parsed_data ,
188
+ "\e#Zolotaya korona\nCard number: %llu\nRegion: %u\nBalance: %lu.%02u RUR\nPrev. balance: %lu.%02u RUR" ,
189
+ card_number ,
190
+ region_number ,
191
+ balance_rub ,
192
+ balance_kop ,
193
+ prev_balance_rub ,
194
+ prev_balance_kop );
195
+
196
+ furi_string_cat_printf (
197
+ parsed_data ,
198
+ "\nLast refill amount: %lu.%02u RUR\nRefill counter: %u\nLast refill: %u.%02u.%02u %02u:%02u\nRefill machine id: %u" ,
199
+ last_refill_amount_rub ,
200
+ last_refill_amount_kop ,
201
+ refill_counter ,
202
+ last_refill_datetime .day ,
203
+ last_refill_datetime .month ,
204
+ last_refill_datetime .year ,
205
+ last_refill_datetime .hour ,
206
+ last_refill_datetime .minute ,
207
+ refill_machine_id );
208
+
209
+ furi_string_cat_printf (
210
+ parsed_data ,
211
+ "\nLast trip: %u.%02u.%02u %02u:%02u\nTrack number: %u\nValidator: %c%06lu" ,
212
+ last_trip_datetime .day ,
213
+ last_trip_datetime .month ,
214
+ last_trip_datetime .year ,
215
+ last_trip_datetime .hour ,
216
+ last_trip_datetime .minute ,
217
+ track_number ,
218
+ validator_first_letter ,
219
+ validator_id );
220
+
221
+ if (furi_hal_rtc_is_flag_set (FuriHalRtcFlagDebug )) {
222
+ furi_string_cat_printf (
223
+ parsed_data ,
224
+ "\nStatus: %u\nSequence num: %u\nDiscount code: %u" ,
225
+ status ,
226
+ sequence_number ,
227
+ discount_code );
228
+ }
229
+
230
+ parsed = true;
231
+ } while (false);
232
+
233
+ return parsed ;
234
+ }
235
+
236
+ /* Actual implementation of app<>plugin interface */
237
+ static const NfcSupportedCardsPlugin zolotaya_korona_plugin = {
238
+ .protocol = NfcProtocolMfClassic ,
239
+ .verify = NULL ,
240
+ .read = NULL ,
241
+ .parse = zolotaya_korona_parse ,
242
+ };
243
+
244
+ /* Plugin descriptor to comply with basic plugin specification */
245
+ static const FlipperAppPluginDescriptor zolotaya_korona_plugin_descriptor = {
246
+ .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID ,
247
+ .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION ,
248
+ .entry_point = & zolotaya_korona_plugin ,
249
+ };
250
+
251
+ /* Plugin entry point - must return a pointer to const descriptor */
252
+ const FlipperAppPluginDescriptor * zolotaya_korona_plugin_ep () {
253
+ return & zolotaya_korona_plugin_descriptor ;
254
+ }
0 commit comments