Skip to content

Commit 9b156fb

Browse files
committed
Merge branch 'nfc-parsers' into dev
2 parents abcb9b2 + de9765f commit 9b156fb

File tree

4 files changed

+268
-7
lines changed

4 files changed

+268
-7
lines changed

applications/main/nfc/application.fam

+9
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ App(
137137
sources=["plugins/supported_cards/mykey.c"],
138138
)
139139

140+
App(
141+
appid="zolotaya_korona_parser",
142+
apptype=FlipperAppType.PLUGIN,
143+
entry_point="zolotaya_korona_plugin_ep",
144+
targets=["f7"],
145+
requires=["nfc"],
146+
sources=["plugins/supported_cards/zolotaya_korona.c"],
147+
)
148+
140149
App(
141150
appid="nfc_start",
142151
targets=["f7"],

applications/main/nfc/plugins/supported_cards/kazan.c

+3-4
Original file line numberDiff line numberDiff line change
@@ -250,19 +250,18 @@ static bool kazan_parse(const NfcDevice* device, FuriString* parsed_data) {
250250
last_trip.day = block_start_ptr[2];
251251
last_trip.hour = block_start_ptr[3];
252252
last_trip.minute = block_start_ptr[4];
253-
bool is_last_trip_valid = (block_start_ptr[0] | block_start_ptr[1] | block_start_ptr[0]) &&
253+
bool is_last_trip_valid = (block_start_ptr[0] | block_start_ptr[1] | block_start_ptr[2]) &&
254254
(last_trip.day < 32 && last_trip.month < 12 &&
255255
last_trip.hour < 24 && last_trip.minute < 60);
256256

257257
start_block_num = mf_classic_get_first_block_num_of_sector(balance_sector_number);
258258
block_start_ptr = &data->block[start_block_num].data[0];
259259

260-
const uint32_t trip_counter = (block_start_ptr[3] << 24) | (block_start_ptr[2] << 16) |
261-
(block_start_ptr[1] << 8) | (block_start_ptr[0]);
260+
const uint32_t trip_counter = nfc_util_bytes2num_little_endian(block_start_ptr, 4);
262261

263262
size_t uid_len = 0;
264263
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
265-
const uint32_t card_number = (uid[3] << 24) | (uid[2] << 16) | (uid[1] << 8) | (uid[0]);
264+
const uint32_t card_number = nfc_util_bytes2num_little_endian(uid, 4);
266265

267266
furi_string_cat_printf(
268267
parsed_data, "\e#Kazan transport card\nCard number: %lu\n", card_number);

applications/main/nfc/plugins/supported_cards/metromoney.c

+2-3
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,14 @@ static bool metromoney_parse(const NfcDevice* device, FuriString* parsed_data) {
148148
const uint8_t* block_start_ptr =
149149
&data->block[start_block_num + ticket_block_number].data[0];
150150

151-
uint32_t balance = (block_start_ptr[3] << 24) | (block_start_ptr[2] << 16) |
152-
(block_start_ptr[1] << 8) | (block_start_ptr[0]);
151+
uint32_t balance = nfc_util_bytes2num_little_endian(block_start_ptr, 4);
153152

154153
uint32_t balance_lari = balance / 100;
155154
uint8_t balance_tetri = balance % 100;
156155

157156
size_t uid_len = 0;
158157
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
159-
uint32_t card_number = (uid[3] << 24) | (uid[2] << 16) | (uid[1] << 8) | (uid[0]);
158+
uint32_t card_number = nfc_util_bytes2num_little_endian(uid, 4);
160159

161160
furi_string_printf(
162161
parsed_data,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)