-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
268 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
254 changes: 254 additions & 0 deletions
254
applications/main/nfc/plugins/supported_cards/zolotaya_korona.c
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
/* | ||
* Parser for Zolotaya Korona card (Russia). | ||
* | ||
* Copyright 2023 Leptoptilos <[email protected]> | ||
* | ||
* More info about Zolotaya Korona cards: https://github.com/metrodroid/metrodroid/wiki/Zolotaya-Korona | ||
* | ||
* 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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
#include "core/core_defines.h" | ||
#include "core/string.h" | ||
#include "furi_hal_rtc.h" | ||
#include "nfc_supported_card_plugin.h" | ||
|
||
#include "protocols/mf_classic/mf_classic.h" | ||
#include <flipper_application/flipper_application.h> | ||
|
||
#include <nfc/nfc_device.h> | ||
#include <nfc/helpers/nfc_util.h> | ||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h> | ||
#include <stdbool.h> | ||
#include <stdint.h> | ||
|
||
#define TAG "Zolotaya Korona" | ||
|
||
#define TRIP_SECTOR_NUM (4) | ||
#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, | ||
0xAE, 0xE0, 0xAE, 0xAD, 0xA0, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00}; | ||
|
||
#define FURI_HAL_RTC_SECONDS_PER_MINUTE 60 | ||
#define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60) | ||
#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) | ||
#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 | ||
|
||
void timestamp_to_datetime(uint32_t timestamp, FuriHalRtcDateTime* datetime) { | ||
uint32_t days = timestamp / FURI_HAL_RTC_SECONDS_PER_DAY; | ||
uint32_t seconds_in_day = timestamp % FURI_HAL_RTC_SECONDS_PER_DAY; | ||
|
||
datetime->year = FURI_HAL_RTC_EPOCH_START_YEAR; | ||
|
||
while(days >= furi_hal_rtc_get_days_per_year(datetime->year)) { | ||
days -= furi_hal_rtc_get_days_per_year(datetime->year); | ||
(datetime->year)++; | ||
} | ||
|
||
datetime->month = 1; | ||
while(days >= furi_hal_rtc_get_days_per_month( | ||
furi_hal_rtc_is_leap_year(datetime->year), datetime->month)) { | ||
days -= furi_hal_rtc_get_days_per_month( | ||
furi_hal_rtc_is_leap_year(datetime->year), datetime->month); | ||
(datetime->month)++; | ||
} | ||
|
||
datetime->day = days + 1; | ||
datetime->hour = seconds_in_day / FURI_HAL_RTC_SECONDS_PER_HOUR; | ||
datetime->minute = | ||
(seconds_in_day % FURI_HAL_RTC_SECONDS_PER_HOUR) / FURI_HAL_RTC_SECONDS_PER_MINUTE; | ||
datetime->second = seconds_in_day % FURI_HAL_RTC_SECONDS_PER_MINUTE; | ||
} | ||
|
||
uint64_t bytes2num_bcd(const uint8_t* src, uint8_t len_bytes) { | ||
furi_assert(src); | ||
|
||
uint64_t res = 0; | ||
|
||
for(uint8_t i = 0; i < len_bytes; i++) { | ||
res *= 10; | ||
res += src[i] / 16; | ||
res *= 10; | ||
res += src[i] % 16; | ||
} | ||
|
||
return res; | ||
} | ||
|
||
static bool zolotaya_korona_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 | ||
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[0]; | ||
|
||
bool verified = true; | ||
for(uint8_t i = 0; i < sizeof(info_sector_signature); i++) { | ||
if(i == 16) { | ||
block_start_ptr = &data->block[start_info_block_number + 1].data[0]; | ||
} | ||
if(block_start_ptr[i % 16] != info_sector_signature[i]) { | ||
verified = false; | ||
break; | ||
} | ||
} | ||
|
||
if(!verified) break; | ||
|
||
// Parse data | ||
|
||
// INFO SECTOR | ||
// block 1 | ||
const uint8_t region_number = bytes2num_bcd(block_start_ptr + 10, 1); | ||
|
||
// 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; | ||
|
||
// TRIP SECTOR | ||
const uint8_t start_trip_block_number = | ||
mf_classic_get_first_block_num_of_sector(TRIP_SECTOR_NUM); | ||
// block 0 | ||
block_start_ptr = &data->block[start_trip_block_number].data[7]; | ||
|
||
const uint8_t status = block_start_ptr[0] % 16; | ||
const uint16_t sequence_number = nfc_util_bytes2num(block_start_ptr + 1, 2); | ||
const uint8_t discount_code = nfc_util_bytes2num(block_start_ptr + 3, 1); | ||
|
||
// block 1: refill block | ||
block_start_ptr = &data->block[start_trip_block_number + 1].data[1]; | ||
|
||
const uint16_t refill_machine_id = nfc_util_bytes2num_little_endian(block_start_ptr, 2); | ||
const uint32_t last_refill_timestamp = | ||
nfc_util_bytes2num_little_endian(block_start_ptr + 2, 4); | ||
const uint32_t last_refill_amount = | ||
nfc_util_bytes2num_little_endian(block_start_ptr + 6, 4); | ||
const uint32_t last_refill_amount_rub = last_refill_amount / 100; | ||
const uint8_t last_refill_amount_kop = last_refill_amount % 100; | ||
const uint16_t refill_counter = nfc_util_bytes2num_little_endian(block_start_ptr + 10, 2); | ||
|
||
FuriHalRtcDateTime last_refill_datetime = {0}; | ||
timestamp_to_datetime(last_refill_timestamp, &last_refill_datetime); | ||
|
||
// block 2: trip block | ||
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 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); | ||
const uint32_t prev_balance = nfc_util_bytes2num_little_endian(block_start_ptr + 11, 4); | ||
const uint32_t prev_balance_rub = prev_balance / 100; | ||
const uint8_t prev_balance_kop = prev_balance % 100; | ||
|
||
FuriHalRtcDateTime last_trip_datetime = {0}; | ||
timestamp_to_datetime(last_trip_timestamp, &last_trip_datetime); | ||
|
||
// PARSE DATA FROM PURSE SECTOR | ||
const uint8_t start_purse_block_number = | ||
mf_classic_get_first_block_num_of_sector(PURSE_SECTOR_NUM); | ||
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); | ||
|
||
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, | ||
region_number, | ||
balance_rub, | ||
balance_kop, | ||
prev_balance_rub, | ||
prev_balance_kop); | ||
|
||
furi_string_cat_printf( | ||
parsed_data, | ||
"\nLast refill amount: %lu.%02u RUR\nRefill counter: %u\nLast refill: %u.%02u.%02u %02u:%02u\nRefill machine id: %u", | ||
last_refill_amount_rub, | ||
last_refill_amount_kop, | ||
refill_counter, | ||
last_refill_datetime.day, | ||
last_refill_datetime.month, | ||
last_refill_datetime.year, | ||
last_refill_datetime.hour, | ||
last_refill_datetime.minute, | ||
refill_machine_id); | ||
|
||
furi_string_cat_printf( | ||
parsed_data, | ||
"\nLast trip: %u.%02u.%02u %02u:%02u\nTrack number: %u\nValidator: %c%06lu", | ||
last_trip_datetime.day, | ||
last_trip_datetime.month, | ||
last_trip_datetime.year, | ||
last_trip_datetime.hour, | ||
last_trip_datetime.minute, | ||
track_number, | ||
validator_first_letter, | ||
validator_id); | ||
|
||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { | ||
furi_string_cat_printf( | ||
parsed_data, | ||
"\nStatus: %u\nSequence num: %u\nDiscount code: %u", | ||
status, | ||
sequence_number, | ||
discount_code); | ||
} | ||
|
||
parsed = true; | ||
} while(false); | ||
|
||
return parsed; | ||
} | ||
|
||
/* Actual implementation of app<>plugin interface */ | ||
static const NfcSupportedCardsPlugin zolotaya_korona_plugin = { | ||
.protocol = NfcProtocolMfClassic, | ||
.verify = NULL, | ||
.read = NULL, | ||
.parse = zolotaya_korona_parse, | ||
}; | ||
|
||
/* Plugin descriptor to comply with basic plugin specification */ | ||
static const FlipperAppPluginDescriptor zolotaya_korona_plugin_descriptor = { | ||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, | ||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, | ||
.entry_point = &zolotaya_korona_plugin, | ||
}; | ||
|
||
/* Plugin entry point - must return a pointer to const descriptor */ | ||
const FlipperAppPluginDescriptor* zolotaya_korona_plugin_ep() { | ||
return &zolotaya_korona_plugin_descriptor; | ||
} |