1
+ /*
2
+ * Parser for Kazan transport card (Kazan, Russia).
3
+ *
4
+ * Copyright 2023 Leptoptilos <[email protected] >
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify it
7
+ * under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful, but
12
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ * General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU General Public License
17
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ */
19
+ #include "core/log.h"
20
+ #include "nfc_supported_card_plugin.h"
21
+
22
+ #include "protocols/mf_classic/mf_classic.h"
23
+ #include <flipper_application/flipper_application.h>
24
+
25
+ #include <nfc/nfc_device.h>
26
+ #include <nfc/helpers/nfc_util.h>
27
+ #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
28
+ #include <stdbool.h>
29
+ #include <stdint.h>
30
+ #include <furi_hal_rtc.h>
31
+ #include "md5.h"
32
+
33
+ #define TAG "Kazan"
34
+
35
+ typedef struct {
36
+ uint64_t a ;
37
+ uint64_t b ;
38
+ } MfClassicKeyPair ;
39
+
40
+ static const MfClassicKeyPair kazan_1k_keys [] = {
41
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
42
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
43
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
44
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
45
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
46
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
47
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
48
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
49
+ {.a = 0xE954024EE754 , .b = 0x0CD464CDC100 },
50
+ {.a = 0xBC305FE2DA65 , .b = 0xCF0EC6ACF2F9 },
51
+ {.a = 0xF7A545095C49 , .b = 0x6862FD600F78 },
52
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
53
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
54
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
55
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
56
+ {.a = 0xFFFFFFFFFFFF , .b = 0xFFFFFFFFFFFF },
57
+ };
58
+
59
+ enum SubscriptionType {
60
+ SUBSCRIPTION_TYPE_UNKNOWN ,
61
+ SUBSCRIPTION_TYPE_PURSE ,
62
+ SUBSCRIPTION_TYPE_ABONNEMENT ,
63
+ };
64
+
65
+ enum SubscriptionType get_subscription_type (uint8_t value ) {
66
+ switch (value ) {
67
+ case 0 :
68
+ case 0x60 :
69
+ case 0x67 :
70
+ case 0x0F :
71
+ return SUBSCRIPTION_TYPE_ABONNEMENT ;
72
+ case 0x53 :
73
+ return SUBSCRIPTION_TYPE_PURSE ;
74
+ default :
75
+ return SUBSCRIPTION_TYPE_UNKNOWN ;
76
+ }
77
+ }
78
+
79
+ static bool kazan_verify (Nfc * nfc ) {
80
+ bool verified = false;
81
+
82
+ do {
83
+ const uint8_t ticket_sector_number = 8 ;
84
+ const uint8_t ticket_block_number =
85
+ mf_classic_get_first_block_num_of_sector (ticket_sector_number ) + 1 ;
86
+ FURI_LOG_D (TAG , "Verifying sector %u" , ticket_sector_number );
87
+
88
+ MfClassicKey key = {0 };
89
+ nfc_util_num2bytes (kazan_1k_keys [ticket_sector_number ].a , COUNT_OF (key .data ), key .data );
90
+
91
+ MfClassicAuthContext auth_context ;
92
+ MfClassicError error = mf_classic_poller_sync_auth (
93
+ nfc , ticket_block_number , & key , MfClassicKeyTypeA , & auth_context );
94
+ if (error != MfClassicErrorNone ) {
95
+ FURI_LOG_D (TAG , "Failed to read block %u: %d" , ticket_block_number , error );
96
+ break ;
97
+ }
98
+
99
+ verified = true;
100
+ } while (false);
101
+
102
+ return verified ;
103
+ }
104
+
105
+ static bool kazan_read (Nfc * nfc , NfcDevice * device ) {
106
+ furi_assert (nfc );
107
+ furi_assert (device );
108
+
109
+ bool is_read = false;
110
+
111
+ MfClassicData * data = mf_classic_alloc ();
112
+ nfc_device_copy_data (device , NfcProtocolMfClassic , data );
113
+
114
+ do {
115
+ MfClassicType type = MfClassicTypeMini ;
116
+ MfClassicError error = mf_classic_poller_sync_detect_type (nfc , & type );
117
+ if (error != MfClassicErrorNone ) break ;
118
+
119
+ data -> type = type ;
120
+ if (type != MfClassicType1k ) break ;
121
+
122
+ MfClassicDeviceKeys keys = {
123
+ .key_a_mask = 0 ,
124
+ .key_b_mask = 0 ,
125
+ };
126
+ for (size_t i = 0 ; i < mf_classic_get_total_sectors_num (data -> type ); i ++ ) {
127
+ nfc_util_num2bytes (kazan_1k_keys [i ].a , sizeof (MfClassicKey ), keys .key_a [i ].data );
128
+ FURI_BIT_SET (keys .key_a_mask , i );
129
+ nfc_util_num2bytes (kazan_1k_keys [i ].b , sizeof (MfClassicKey ), keys .key_b [i ].data );
130
+ FURI_BIT_SET (keys .key_b_mask , i );
131
+ }
132
+
133
+ error = mf_classic_poller_sync_read (nfc , & keys , data );
134
+ if (error != MfClassicErrorNone ) {
135
+ FURI_LOG_W (TAG , "Failed to read data" );
136
+ break ;
137
+ }
138
+
139
+ nfc_device_set_data (device , NfcProtocolMfClassic , data );
140
+
141
+ is_read = true;
142
+ } while (false);
143
+
144
+ mf_classic_free (data );
145
+
146
+ return is_read ;
147
+ }
148
+
149
+ static bool kazan_parse (const NfcDevice * device , FuriString * parsed_data ) {
150
+ furi_assert (device );
151
+
152
+ const MfClassicData * data = nfc_device_get_data (device , NfcProtocolMfClassic );
153
+
154
+ bool parsed = false;
155
+
156
+ do {
157
+ const uint8_t ticket_sector_number = 8 ;
158
+ const uint8_t balance_sector_number = 9 ;
159
+
160
+ // Verify keys
161
+ MfClassicKeyPair keys = {};
162
+ const MfClassicSectorTrailer * sec_tr =
163
+ mf_classic_get_sector_trailer_by_sector (data , ticket_sector_number );
164
+
165
+ keys .a = nfc_util_bytes2num (sec_tr -> key_a .data , COUNT_OF (sec_tr -> key_a .data ));
166
+ keys .b = nfc_util_bytes2num (sec_tr -> key_b .data , COUNT_OF (sec_tr -> key_b .data ));
167
+
168
+ if ((keys .a != 0xE954024EE754 ) && (keys .b != 0x0CD464CDC100 )) break ;
169
+
170
+ // Parse data
171
+ uint8_t start_block_num = mf_classic_get_first_block_num_of_sector (ticket_sector_number );
172
+
173
+ const uint8_t * block_start_ptr = & data -> block [start_block_num ].data [6 ];
174
+
175
+ enum SubscriptionType subscription_type = get_subscription_type (block_start_ptr [0 ]);
176
+
177
+ FuriHalRtcDateTime valid_from ;
178
+ valid_from .year = 2000 + block_start_ptr [1 ];
179
+ valid_from .month = block_start_ptr [2 ];
180
+ valid_from .day = block_start_ptr [3 ];
181
+
182
+ FuriHalRtcDateTime valid_to ;
183
+ valid_to .year = 2000 + block_start_ptr [4 ];
184
+ valid_to .month = block_start_ptr [5 ];
185
+ valid_to .day = block_start_ptr [6 ];
186
+
187
+ const uint8_t last_trip_block_number = 2 ;
188
+ block_start_ptr = & data -> block [start_block_num + last_trip_block_number ].data [1 ];
189
+
190
+ FuriHalRtcDateTime last_trip ;
191
+ last_trip .year = 2000 + block_start_ptr [0 ];
192
+ last_trip .month = block_start_ptr [1 ];
193
+ last_trip .day = block_start_ptr [2 ];
194
+ last_trip .hour = block_start_ptr [3 ];
195
+ last_trip .minute = block_start_ptr [4 ];
196
+ bool is_last_trip_valid = (block_start_ptr [0 ] | block_start_ptr [1 ] | block_start_ptr [0 ]) &&
197
+ (last_trip .day < 32 && last_trip .month < 12 &&
198
+ last_trip .hour < 24 && last_trip .minute < 60 );
199
+
200
+ start_block_num = mf_classic_get_first_block_num_of_sector (balance_sector_number );
201
+ block_start_ptr = & data -> block [start_block_num ].data [0 ];
202
+
203
+ const uint32_t trip_counter = (block_start_ptr [3 ] << 24 ) | (block_start_ptr [2 ] << 16 ) |
204
+ (block_start_ptr [1 ] << 8 ) | (block_start_ptr [0 ]);
205
+
206
+ size_t uid_len = 0 ;
207
+ const uint8_t * uid = mf_classic_get_uid (data , & uid_len );
208
+ const uint32_t card_number = (uid [3 ] << 24 ) | (uid [2 ] << 16 ) | (uid [1 ] << 8 ) | (uid [0 ]);
209
+
210
+ furi_string_cat_printf (
211
+ parsed_data , "\e#Kazan transport card\nCard number: %lu\n" , card_number );
212
+
213
+ if (subscription_type == SUBSCRIPTION_TYPE_PURSE ) {
214
+ furi_string_cat_printf (
215
+ parsed_data ,
216
+ "Type: purse\nBalance: %lu RUR\nBalance valid:\nfrom: %02u.%02u.%u\nto: %02u.%02u.%u" ,
217
+ trip_counter ,
218
+ valid_from .day ,
219
+ valid_from .month ,
220
+ valid_from .year ,
221
+ valid_to .day ,
222
+ valid_to .month ,
223
+ valid_to .year );
224
+ }
225
+
226
+ if (subscription_type == SUBSCRIPTION_TYPE_ABONNEMENT ) {
227
+ furi_string_cat_printf (
228
+ parsed_data ,
229
+ "Type: abonnement\nTrips left: %lu\nCard valid:\nfrom: %02u.%02u.%u\nto: %02u.%02u.%u" ,
230
+ trip_counter ,
231
+ valid_from .day ,
232
+ valid_from .month ,
233
+ valid_from .year ,
234
+ valid_to .day ,
235
+ valid_to .month ,
236
+ valid_to .year );
237
+ }
238
+
239
+ if (subscription_type == SUBSCRIPTION_TYPE_UNKNOWN ) {
240
+ furi_string_cat_printf (
241
+ parsed_data ,
242
+ "Type: unknown\nBalance: %lu RUR\nValid from: %02u.%02u.%u\nValid to: %02u.%02u.%u" ,
243
+ trip_counter ,
244
+ valid_from .day ,
245
+ valid_from .month ,
246
+ valid_from .year ,
247
+ valid_to .day ,
248
+ valid_to .month ,
249
+ valid_to .year );
250
+ }
251
+
252
+ if (is_last_trip_valid ) {
253
+ furi_string_cat_printf (
254
+ parsed_data ,
255
+ "\nLast trip: %02u.%02u.%u at %02u:%02u" ,
256
+ last_trip .day ,
257
+ last_trip .month ,
258
+ last_trip .year ,
259
+ last_trip .hour ,
260
+ last_trip .minute );
261
+ }
262
+
263
+ parsed = true;
264
+ } while (false);
265
+
266
+ return parsed ;
267
+ }
268
+
269
+ /* Actual implementation of app<>plugin interface */
270
+ static const NfcSupportedCardsPlugin kazan_plugin = {
271
+ .protocol = NfcProtocolMfClassic ,
272
+ .verify = kazan_verify ,
273
+ .read = kazan_read ,
274
+ .parse = kazan_parse ,
275
+ };
276
+
277
+ /* Plugin descriptor to comply with basic plugin specification */
278
+ static const FlipperAppPluginDescriptor kazan_plugin_descriptor = {
279
+ .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID ,
280
+ .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION ,
281
+ .entry_point = & kazan_plugin ,
282
+ };
283
+
284
+ /* Plugin entry point - must return a pointer to const descriptor */
285
+ const FlipperAppPluginDescriptor * kazan_plugin_ep () {
286
+ return & kazan_plugin_descriptor ;
287
+ }
0 commit comments