mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-06-21 20:42:15 -07:00
@@ -305,6 +305,33 @@ App(
|
||||
sources=["plugins/supported_cards/plantain.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="szppk_so_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="szppk_so_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/szppk_so.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="sk_tk_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="sk_tk_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/sk_tk.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="sev_tk_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="sev_tk_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/sevppk_tk.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="two_cities_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
|
||||
#define TAG "Plantain"
|
||||
|
||||
void from_minutes_to_datetime(uint32_t minutes, DateTime* datetime, uint16_t start_year) {
|
||||
uint32_t timestamp = minutes * 60;
|
||||
DateTime start_datetime = {0};
|
||||
start_datetime.year = start_year - 1;
|
||||
start_datetime.month = 12;
|
||||
start_datetime.day = 31;
|
||||
timestamp += datetime_datetime_to_timestamp(&start_datetime);
|
||||
datetime_timestamp_to_datetime(timestamp, datetime);
|
||||
}
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PLANTAIN_EPOCH_START 1262304000 //2010-01-01
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 101
|
||||
#define FIRST_TICKET_VALUE_BLOCK 104
|
||||
#define SECOND_PPK_TICKET_OFFSET 102
|
||||
#define SECOND_TICKET_VALUE_BLOCK 108
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
@@ -30,79 +28,549 @@ typedef struct {
|
||||
} PlantainCardConfig;
|
||||
|
||||
static const MfClassicKeyPair plantain_1k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b},
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7},
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb},
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
|
||||
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
|
||||
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
|
||||
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
|
||||
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
|
||||
{.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26
|
||||
{.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //27
|
||||
{.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //28
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //29
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
|
||||
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
|
||||
|
||||
};
|
||||
|
||||
static const MfClassicKeyPair plantain_4k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406},
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA},
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3},
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
|
||||
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
|
||||
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
|
||||
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
|
||||
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
|
||||
{.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26
|
||||
{.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA}, //27
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //28
|
||||
{.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3}, //29
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
|
||||
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32
|
||||
{.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34
|
||||
{.a = 0x22052b480d11, .b = 0xe19504c39461}, //35
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36
|
||||
{.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38
|
||||
{.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39
|
||||
};
|
||||
|
||||
static const MfClassicKeyPair plantain_4k_keys_legacy[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x46d78e850a7e, .b = 0xa470f8130991},
|
||||
{.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, {.a = 0x0f01ceff2742, .b = 0x6fec74559ca7},
|
||||
{.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, {.a = 0x9ea3387a63c1, .b = 0x437e59f57561},
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
|
||||
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
|
||||
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
|
||||
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
|
||||
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
|
||||
{.a = 0x46d78e850a7e, .b = 0xa470f8130991}, //25
|
||||
{.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, //26
|
||||
{.a = 0x0f01ceff2742, .b = 0x6fec74559ca7}, //27
|
||||
{.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, //28
|
||||
{.a = 0x9ea3387a63c1, .b = 0x437e59f57561}, //29
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
|
||||
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32
|
||||
{.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34
|
||||
{.a = 0x22052b480d11, .b = 0xe19504c39461}, //35
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36
|
||||
{.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38
|
||||
{.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t direction;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint64_t sys_n;
|
||||
uint8_t ppk_cnt;
|
||||
} PPKData;
|
||||
|
||||
typedef struct {
|
||||
uint64_t card_number;
|
||||
FuriString* card_number_str;
|
||||
uint32_t balance;
|
||||
uint8_t trips_metro;
|
||||
uint8_t trips_ground;
|
||||
uint32_t last_trip_data;
|
||||
DateTime last_trip_time;
|
||||
uint16_t validator;
|
||||
uint16_t fare;
|
||||
uint32_t last_payment_date_data;
|
||||
DateTime last_payment_date;
|
||||
uint16_t last_payment_amount;
|
||||
uint8_t keyset;
|
||||
|
||||
} PlantainData;
|
||||
// Function to map UIC codes to station names
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, PPKData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code
|
||||
static void resolve_station_name(Storage* storage, PPKData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
// Function to extract plantain purse data from the card data
|
||||
static inline void extract_purse_data(
|
||||
const MfClassicData* data,
|
||||
PlantainData* purse,
|
||||
PPKData* ticket,
|
||||
uint8_t first_ppk_ticket_offset,
|
||||
uint8_t second_ppk_ticket_offset) {
|
||||
uint64_t card_number = 0;
|
||||
size_t uid_len = 0;
|
||||
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
|
||||
purse->card_number_str = furi_string_alloc();
|
||||
const uint8_t* temp_ptr = &uid[0];
|
||||
uint8_t card_number_tmp[uid_len];
|
||||
|
||||
if(uid_len == 4) {
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
card_number_tmp[i] = temp_ptr[3 - i];
|
||||
}
|
||||
} else if(uid_len == 7) {
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number_tmp[i] = temp_ptr[6 - i];
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < uid_len; i++) {
|
||||
card_number = (card_number << 8) | card_number_tmp[i];
|
||||
}
|
||||
FuriString* card_number_s = furi_string_alloc();
|
||||
furi_string_cat_printf(card_number_s, "%lld", card_number);
|
||||
FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 ");
|
||||
for(uint8_t i = 0; i < 24; i += 4) {
|
||||
for(uint8_t j = 0; j < 4; j++) {
|
||||
furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j));
|
||||
}
|
||||
furi_string_push_back(tmp_s, ' ');
|
||||
}
|
||||
|
||||
furi_string_set(purse->card_number_str, furi_string_get_cstr(tmp_s));
|
||||
furi_string_free(card_number_s);
|
||||
furi_string_free(tmp_s);
|
||||
|
||||
if(data->type == MfClassicType1k) {
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++)
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
balance /= 100;
|
||||
purse->balance = balance;
|
||||
purse->trips_metro = data->block[21].data[0];
|
||||
purse->trips_ground = data->block[21].data[1];
|
||||
uint32_t last_trip_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100;
|
||||
purse->fare = fare;
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_payment_date_data = (purse->last_payment_date_data << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
purse->last_payment_amount = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
last_trip_timestamp = PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE;
|
||||
const uint32_t last_payment_timestamp =
|
||||
PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time);
|
||||
datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date);
|
||||
|
||||
} else if(data->type == MfClassicType4k) {
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++)
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
balance /= 100;
|
||||
purse->balance = balance;
|
||||
purse->trips_metro = data->block[21].data[0];
|
||||
purse->trips_ground = data->block[21].data[1];
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100;
|
||||
purse->fare = fare;
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_payment_date_data = (purse->last_payment_date_data << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
purse->last_payment_amount = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
uint32_t last_trip_timestamp =
|
||||
PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE;
|
||||
const uint32_t last_payment_timestamp =
|
||||
PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time);
|
||||
datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date);
|
||||
}
|
||||
|
||||
ticket->first_ticket_marker = data->block[first_ppk_ticket_offset].data[0];
|
||||
ticket->second_ticket_marker = data->block[second_ppk_ticket_offset].data[0];
|
||||
}
|
||||
// Function to extract PPK ticket data from the card data
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
PPKData* ticket,
|
||||
bool second_ticket) {
|
||||
const uint8_t* temp_ptr = &data->block[SECOND_TICKET_VALUE_BLOCK + 2].data[0];
|
||||
uint8_t sys_n_arr[6] = {0};
|
||||
|
||||
if(second_ticket == 0) {
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
sys_n_arr[i] = temp_ptr[7 - i];
|
||||
}
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
|
||||
ticket->direction = data->block[FIRST_PPK_TICKET_OFFSET].data[14];
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[105].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else if(second_ticket == 1) {
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
sys_n_arr[i] = temp_ptr[13 - i];
|
||||
}
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
|
||||
ticket->direction = data->block[SECOND_PPK_TICKET_OFFSET].data[14];
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[109].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
uint64_t sys_n = 0;
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
sys_n = (sys_n << 8) | sys_n_arr[i];
|
||||
}
|
||||
ticket->sys_n = sys_n;
|
||||
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
// Function to format and print plantain purse data
|
||||
static void printf_plantain_data(FuriString* parsed_data, PlantainData* purse) {
|
||||
furi_string_printf(parsed_data, "\e#Plantain Card");
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nNumber: %s\nBalance: %ld RUB\nMetro Trips: %d\nGround Trips: %d\nLast Trip: %02d.%02d.%04d %02d:%02d",
|
||||
furi_string_get_cstr(purse->card_number_str),
|
||||
purse->balance,
|
||||
purse->trips_metro,
|
||||
purse->trips_ground,
|
||||
purse->last_trip_time.day,
|
||||
purse->last_trip_time.month,
|
||||
purse->last_trip_time.year,
|
||||
purse->last_trip_time.hour,
|
||||
purse->last_trip_time.minute);
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nValidator: %d\nFare: %d RUB\nRefilled on: %02d.%02d.%04d %02d:%02d\nAmount: %d RUB",
|
||||
purse->validator,
|
||||
purse->fare,
|
||||
purse->last_payment_date.day,
|
||||
purse->last_payment_date.month,
|
||||
purse->last_payment_date.year,
|
||||
purse->last_payment_date.hour,
|
||||
purse->last_payment_date.minute,
|
||||
purse->last_payment_amount);
|
||||
|
||||
if(purse->keyset == 1)
|
||||
furi_string_cat_printf(parsed_data, "\nPPK keys installed:> YES");
|
||||
else
|
||||
furi_string_cat_printf(parsed_data, "\nPPK keys installed:> NO");
|
||||
|
||||
furi_string_free(purse->card_number_str);
|
||||
}
|
||||
|
||||
// Function to format and print PPK ticket data
|
||||
static void printf_ppk_data(FuriString* parsed_data, PPKData* ticket, bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\n\n\e#PPK Ticket:\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x33:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 1 ride");
|
||||
break;
|
||||
case 0x34:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)");
|
||||
break;
|
||||
case 0x35:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)");
|
||||
break;
|
||||
case 0x36:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type:> Unknown, 0x%02X", ticket->first_ticket_marker);
|
||||
}
|
||||
} else if(ticket_number == 1) {
|
||||
furi_string_cat_printf(parsed_data, "\n\nSecond PPK Ticket:\n");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x33:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 1 ride");
|
||||
break;
|
||||
case 0x34:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)");
|
||||
break;
|
||||
case 0x35:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)");
|
||||
break;
|
||||
case 0x36:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.)");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type:> Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:> %s\nTo:> %s",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name));
|
||||
|
||||
if(ticket->valid_from.day == ticket->valid_till.day) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nValid On: %02d-%02d-%04d",
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year);
|
||||
} else {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d",
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
}
|
||||
|
||||
if(ticket->direction == 1) {
|
||||
furi_string_cat_printf(parsed_data, "\nDirection: One-way ->>");
|
||||
} else if(ticket->direction == 2) {
|
||||
furi_string_cat_printf(parsed_data, "\nDirection: Round-trip <<-->>");
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "\nRides left:> %02d", ticket->value_data);
|
||||
|
||||
if(ticket->current_status == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\nStatus:> TICKET IS READY\n");
|
||||
} else if(ticket->current_status == 0x80)
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute);
|
||||
else if(ticket->current_status == 0x1E)
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute);
|
||||
else
|
||||
furi_string_cat_printf(parsed_data, "\nStatus:> UNKNOWN (%02X)\n", ticket->current_status);
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "SYS N:> %lld\nPPK CNT:> %03d", ticket->sys_n, ticket->ppk_cnt);
|
||||
}
|
||||
//Function to select a keyset based on card type
|
||||
static bool plantain_get_card_config(PlantainCardConfig* config, MfClassicType type) {
|
||||
bool success = true;
|
||||
|
||||
if(type == MfClassicType1k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = plantain_1k_keys;
|
||||
|
||||
} else if(type == MfClassicType4k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = plantain_4k_keys;
|
||||
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
@@ -114,11 +582,10 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
PlantainCardConfig cfg = {};
|
||||
PlantainCardConfig cfg = {0};
|
||||
if(!plantain_get_card_config(&cfg, type)) break;
|
||||
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
|
||||
FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
|
||||
|
||||
MfClassicKey key = {0};
|
||||
bit_lib_num_to_bytes_be(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data);
|
||||
@@ -127,7 +594,6 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -157,7 +623,7 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
|
||||
if(error != MfClassicErrorNone) break;
|
||||
|
||||
data->type = type;
|
||||
PlantainCardConfig cfg = {};
|
||||
PlantainCardConfig cfg = {0};
|
||||
if(!plantain_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
const uint8_t legacy_check_sec_num = 26;
|
||||
@@ -171,21 +637,28 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
|
||||
error = mf_classic_poller_sync_auth(
|
||||
nfc, legacy_check_block_num, &key, MfClassicKeyTypeA, NULL);
|
||||
if(error == MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Legacy keys detected");
|
||||
cfg.keys = plantain_4k_keys_legacy;
|
||||
}
|
||||
|
||||
MfClassicDeviceKeys keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
if(data->type == MfClassicType1k) {
|
||||
for(size_t i = 0; i < 32; i++) {
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
} else {
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error == MfClassicErrorNotPresent) {
|
||||
FURI_LOG_W(TAG, "Failed to read data");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -198,18 +671,22 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
//Main parsing function
|
||||
static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
size_t uid_len = 0;
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
|
||||
|
||||
PPKData ticket = {0};
|
||||
ticket.departure_name = furi_string_alloc();
|
||||
ticket.destination_name = furi_string_alloc();
|
||||
ticket.trip_start_sta_name = furi_string_alloc();
|
||||
ticket.trip_end_sta_name = furi_string_alloc();
|
||||
PlantainData purse = {0};
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Verify card type
|
||||
PlantainCardConfig cfg = {};
|
||||
PlantainCardConfig cfg = {0};
|
||||
if(!plantain_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
// Verify key
|
||||
@@ -220,167 +697,47 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
|
||||
if(key != cfg.keys[cfg.data_sector].a) break;
|
||||
|
||||
furi_string_printf(parsed_data, "\e#Plantain card\n");
|
||||
if(data->block[107].data[10] == 0x02)
|
||||
purse.keyset = 0;
|
||||
else
|
||||
purse.keyset = 1;
|
||||
|
||||
const uint8_t* temp_ptr = &uid[0];
|
||||
|
||||
// UID is read from last to first byte
|
||||
uint8_t card_number_tmp[uid_len];
|
||||
|
||||
if(uid_len == 4) {
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
card_number_tmp[i] = temp_ptr[3 - i];
|
||||
}
|
||||
} else if(uid_len == 7) {
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number_tmp[i] = temp_ptr[6 - i];
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
// Extract plantain purse data and fill PPK tickets markers
|
||||
extract_purse_data(
|
||||
data, &purse, &ticket, FIRST_PPK_TICKET_OFFSET, SECOND_PPK_TICKET_OFFSET);
|
||||
// Print plantain purse data
|
||||
printf_plantain_data(parsed_data, &purse);
|
||||
if(purse.card_number_str) furi_string_free(purse.card_number_str);
|
||||
// Extract and print PPK ticket data if present
|
||||
if(ticket.first_ticket_marker != 0) {
|
||||
extract_ppk_data(storage, data, &ticket, 0);
|
||||
printf_ppk_data(parsed_data, &ticket, false);
|
||||
}
|
||||
//UID is converted to a card number
|
||||
uint64_t card_number = 0;
|
||||
for(size_t i = 0; i < uid_len; i++) {
|
||||
card_number = (card_number << 8) | card_number_tmp[i];
|
||||
// Extract and print second PPK ticket data if present
|
||||
if(ticket.second_ticket_marker != 0) {
|
||||
PPKData second_ticket = {0};
|
||||
second_ticket.departure_name = furi_string_alloc();
|
||||
second_ticket.destination_name = furi_string_alloc();
|
||||
second_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
second_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &second_ticket, 1);
|
||||
|
||||
printf_ppk_data(parsed_data, &second_ticket, true);
|
||||
|
||||
furi_string_free(second_ticket.departure_name);
|
||||
furi_string_free(second_ticket.destination_name);
|
||||
furi_string_free(second_ticket.trip_start_sta_name);
|
||||
furi_string_free(second_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
// Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards.
|
||||
furi_string_cat_printf(parsed_data, "Number: ");
|
||||
FuriString* card_number_s = furi_string_alloc();
|
||||
furi_string_cat_printf(card_number_s, "%lld", card_number);
|
||||
FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 ");
|
||||
for(uint8_t i = 0; i < 24; i += 4) {
|
||||
for(uint8_t j = 0; j < 4; j++) {
|
||||
furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j));
|
||||
}
|
||||
furi_string_push_back(tmp_s, ' ');
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s));
|
||||
// this works for 2K Plantain
|
||||
if(data->type == MfClassicType1k) {
|
||||
//balance
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
|
||||
|
||||
//trips
|
||||
uint8_t trips_metro = data->block[21].data[0];
|
||||
uint8_t trips_ground = data->block[21].data[1];
|
||||
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
|
||||
//trip time
|
||||
uint32_t last_trip_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
DateTime last_trip = {0};
|
||||
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Trip start: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_trip.day,
|
||||
last_trip.month,
|
||||
last_trip.year,
|
||||
last_trip.hour,
|
||||
last_trip.minute);
|
||||
//validator
|
||||
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
|
||||
//tariff
|
||||
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
|
||||
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
|
||||
//trips in metro
|
||||
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
|
||||
//trips on ground
|
||||
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
|
||||
//last payment
|
||||
uint32_t last_payment_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_payment_timestamp = (last_payment_timestamp << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
DateTime last_payment_date = {0};
|
||||
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Last pay: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_payment_date.day,
|
||||
last_payment_date.month,
|
||||
last_payment_date.year,
|
||||
last_payment_date.hour,
|
||||
last_payment_date.minute);
|
||||
//Last payment amount.
|
||||
uint16_t last_payment = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment);
|
||||
furi_string_free(card_number_s);
|
||||
furi_string_free(tmp_s);
|
||||
//This is for 4K Plantains.
|
||||
} else if(data->type == MfClassicType4k) {
|
||||
//balance
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
|
||||
|
||||
//trips
|
||||
uint8_t trips_metro = data->block[21].data[0];
|
||||
uint8_t trips_ground = data->block[21].data[1];
|
||||
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
|
||||
//trip time
|
||||
uint32_t last_trip_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
DateTime last_trip = {0};
|
||||
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Trip start: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_trip.day,
|
||||
last_trip.month,
|
||||
last_trip.year,
|
||||
last_trip.hour,
|
||||
last_trip.minute);
|
||||
//validator
|
||||
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
|
||||
//tariff
|
||||
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
|
||||
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
|
||||
//trips in metro
|
||||
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
|
||||
//trips on ground
|
||||
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
|
||||
//last payment
|
||||
uint32_t last_payment_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_payment_timestamp = (last_payment_timestamp << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
DateTime last_payment_date = {0};
|
||||
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Last pay: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_payment_date.day,
|
||||
last_payment_date.month,
|
||||
last_payment_date.year,
|
||||
last_payment_date.hour,
|
||||
last_payment_date.minute);
|
||||
//Last payment amount
|
||||
uint16_t last_payment = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment);
|
||||
furi_string_free(card_number_s);
|
||||
furi_string_free(tmp_s);
|
||||
}
|
||||
parsed = true;
|
||||
} while(false);
|
||||
furi_string_free(ticket.departure_name);
|
||||
furi_string_free(ticket.destination_name);
|
||||
furi_string_free(ticket.trip_start_sta_name);
|
||||
furi_string_free(ticket.trip_end_sta_name);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 76
|
||||
#define FIRST_TICKET_VALUE_BLOCK 77
|
||||
#define SECOND_PPK_TICKET_OFFSET 88
|
||||
#define SECOND_TICKET_VALUE_BLOCK 89
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint8_t ppk_cnt;
|
||||
} TicketData;
|
||||
|
||||
static const MfClassicKeyPair t_card_2k[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //0
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //1
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //2
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //3
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //4
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //5
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //6
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //7
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //8
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //9
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //10
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //11
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //12
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //13
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //14
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //15
|
||||
|
||||
{.a = 0x061029A4A6A4, .b = 0xCF43E52FC23D}, //16
|
||||
{.a = 0xE09F2EC8229C, .b = 0x044D78B70F92}, //17
|
||||
{.a = 0x66843B4FF86E, .b = 0x4474EB131577}, //18
|
||||
{.a = 0x9AC9A9B5E809, .b = 0x558B8E45E568}, //19
|
||||
{.a = 0xC114DE1AD90F, .b = 0xA0A9033E2A09}, //20
|
||||
{.a = 0xF696EFFDD838, .b = 0xA38333EC3F72}, //21
|
||||
{.a = 0xAB50BDE9CD04, .b = 0x67991D678AED}, //22
|
||||
{.a = 0x9041DD7B236C, .b = 0x8A527C8CA237}, //23
|
||||
{.a = 0x7D40A5AA63D0, .b = 0x4A61C563DD8A}, //24
|
||||
{.a = 0x05D0520EC52B, .b = 0xB40BD14E6CB4}, //25
|
||||
{.a = 0x9354F1BFF80F, .b = 0xF99D40CBBA63}, //26
|
||||
{.a = 0x84132D9FD1AF, .b = 0x1BAA2563C14D}, //27
|
||||
{.a = 0x27A8E6436B01, .b = 0x4DB883715A5C}, //28
|
||||
{.a = 0x58B173568D26, .b = 0x2E4ADD66E35E}, //29
|
||||
{.a = 0x1BE71601E73C, .b = 0xB855F30C54FB}, //30
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //31
|
||||
|
||||
};
|
||||
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
|
||||
static void resolve_station_name(Storage* storage, TicketData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sev_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
TicketData* ticket,
|
||||
bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else {
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
|
||||
static void
|
||||
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SevPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\e#SevPPK Transport Card\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->first_ticket_marker);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat_printf(parsed_data, "\e#Second Ticket:");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name),
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
|
||||
if(ticket->value_data > 0) {
|
||||
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
|
||||
}
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(parsed_data, "Status:> NOT USED\n");
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1F:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
|
||||
ticket->current_status,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool sev_tk_verify(Nfc* nfc) {
|
||||
const uint8_t verify_sector = 19;
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
|
||||
MfClassicKey key = {};
|
||||
bit_lib_num_to_bytes_be(t_card_2k[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
|
||||
return error == MfClassicErrorNone;
|
||||
}
|
||||
|
||||
static bool sev_tk_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
bool is_read = false;
|
||||
MfClassicType type = MfClassicType1k;
|
||||
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error == MfClassicErrorNone) {
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
|
||||
for(size_t i = 0; i < 32; i++) {
|
||||
bit_lib_num_to_bytes_be(t_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(t_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNotPresent) {
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
is_read = (error == MfClassicErrorNone);
|
||||
}
|
||||
}
|
||||
|
||||
mf_classic_free(data);
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool sev_tk_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
|
||||
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
|
||||
if(key != t_card_2k[19].a) break;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
TicketData primary_ticket = {0};
|
||||
primary_ticket.departure_name = furi_string_alloc();
|
||||
primary_ticket.destination_name = furi_string_alloc();
|
||||
primary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
primary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
extract_ppk_data(storage, data, &primary_ticket, 0);
|
||||
|
||||
printf_transport_card(parsed_data, &primary_ticket, false);
|
||||
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
|
||||
if(primary_ticket.second_ticket_marker != 0) {
|
||||
TicketData secondary_ticket = {0};
|
||||
secondary_ticket.departure_name = furi_string_alloc();
|
||||
secondary_ticket.destination_name = furi_string_alloc();
|
||||
secondary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
secondary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &secondary_ticket, 1);
|
||||
printf_transport_card(parsed_data, &secondary_ticket, true);
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static const NfcSupportedCardsPlugin sev_tk_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = sev_tk_verify,
|
||||
.read = sev_tk_read,
|
||||
.parse = sev_tk_parse,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor sev_tk_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &sev_tk_plugin,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* sev_tk_plugin_ep(void) {
|
||||
return &sev_tk_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 76
|
||||
#define FIRST_TICKET_VALUE_BLOCK 77
|
||||
#define SECOND_PPK_TICKET_OFFSET 88
|
||||
#define SECOND_TICKET_VALUE_BLOCK 89
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint8_t ppk_cnt;
|
||||
} TicketData;
|
||||
|
||||
static const MfClassicKeyPair t_card_4k[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B1B3B6}, //0
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B5B2B4B9B0}, //1
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B6B1B3}, //2
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B3B8B5B7}, //3
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B2B8}, //4
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B4B5B3B8}, //5
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B8B1B7B8}, //6
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B5B1B8B1}, //7
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B8B7}, //8
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B9B2B4}, //9
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B8B0B6}, //10
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B9B5B5}, //11
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B7B1B0B0}, //12
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B8B3B2}, //13
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B1B0B4B6}, //14
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B5B4B8}, //15
|
||||
{.a = 0x684CE8377AB8, .b = 0x91888D728D9E}, //16
|
||||
{.a = 0xF462ED255B44, .b = 0x0D40620AC610}, //17
|
||||
{.a = 0xF1E8FD5B5C9F, .b = 0xABB5763550B0}, //18
|
||||
{.a = 0xE9E3265B45B1, .b = 0x11D848B26034}, //19
|
||||
{.a = 0xAEA3755DFA82, .b = 0x36D1BAC1395E}, //20
|
||||
{.a = 0x83F58B205854, .b = 0x967DAFCF2674}, //21
|
||||
{.a = 0x5CDF18E68A75, .b = 0x1689C175B14E}, //22
|
||||
{.a = 0x126DC25C5D53, .b = 0x346B03AF1FF3}, //23
|
||||
{.a = 0xF1B013C4495C, .b = 0x74CE14DBC71F}, //24
|
||||
{.a = 0xA5FE4FAD0269, .b = 0x0025FEA845E5}, //25
|
||||
{.a = 0x43080428049C, .b = 0x2E91BB6F511E}, //26
|
||||
{.a = 0x4F44A08C51BC, .b = 0xE44CC58DF833}, //27
|
||||
{.a = 0x3FEC92F652BE, .b = 0x942039006B83}, //28
|
||||
{.a = 0x3A3BE5B635FA, .b = 0xFC564425A9BA}, //29
|
||||
{.a = 0x78C9E1C688BB, .b = 0xA29E362B22F3}, //30
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //32
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //33
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //34
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //35
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //36
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //37
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //38
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //39
|
||||
};
|
||||
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
|
||||
static void resolve_station_name(Storage* storage, TicketData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sk_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
TicketData* ticket,
|
||||
bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else {
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
|
||||
static void
|
||||
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SKPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\e#SKPPK Transport Card\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name),
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
|
||||
if(ticket->value_data > 0) {
|
||||
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
|
||||
}
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(parsed_data, "Status:> NOT USED\n");
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1F:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
|
||||
ticket->current_status,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool sk_tk_verify(Nfc* nfc) {
|
||||
const uint8_t verify_sector = 19;
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
|
||||
MfClassicKey key = {};
|
||||
bit_lib_num_to_bytes_be(t_card_4k[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
|
||||
return error == MfClassicErrorNone;
|
||||
}
|
||||
|
||||
static bool sk_tk_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
bool is_read = false;
|
||||
MfClassicType type = MfClassicType4k;
|
||||
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error == MfClassicErrorNone) {
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(t_card_4k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(t_card_4k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNotPresent) {
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
is_read = (error == MfClassicErrorNone);
|
||||
}
|
||||
}
|
||||
|
||||
mf_classic_free(data);
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool sk_tk_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
|
||||
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
|
||||
if(key != t_card_4k[19].a) break;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
TicketData primary_ticket = {0};
|
||||
primary_ticket.departure_name = furi_string_alloc();
|
||||
primary_ticket.destination_name = furi_string_alloc();
|
||||
primary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
primary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
extract_ppk_data(storage, data, &primary_ticket, 0);
|
||||
|
||||
printf_transport_card(parsed_data, &primary_ticket, false);
|
||||
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
|
||||
if(primary_ticket.second_ticket_marker != 0) {
|
||||
TicketData secondary_ticket = {0};
|
||||
secondary_ticket.departure_name = furi_string_alloc();
|
||||
secondary_ticket.destination_name = furi_string_alloc();
|
||||
secondary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
secondary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &secondary_ticket, 1);
|
||||
printf_transport_card(parsed_data, &secondary_ticket, true);
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static const NfcSupportedCardsPlugin sk_tk_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = sk_tk_verify,
|
||||
.read = sk_tk_read,
|
||||
.parse = sk_tk_parse,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor sk_tk_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &sk_tk_plugin,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* sk_tk_plugin_ep(void) {
|
||||
return &sk_tk_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //1388530800 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 76
|
||||
#define FIRST_TICKET_VALUE_BLOCK 77
|
||||
#define SECOND_PPK_TICKET_OFFSET 88
|
||||
#define SECOND_TICKET_VALUE_BLOCK 89
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint8_t ppk_cnt;
|
||||
} TicketData;
|
||||
|
||||
static const MfClassicKeyPair so_card_2k[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B5B8B4B5}, //0
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B2B9B8B7}, //1
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B4B4B4B0}, //2
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B5B2B2B2}, //3
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B8B0B9}, //4
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B7B9B8B6}, //5
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B5B5B5B6}, //6
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B0B0B2B0}, //7
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B2B8B0}, //8
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B1B8B3B2}, //9
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B0B3B1B8}, //10
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B6B7B5B2}, //11
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B0B2B0}, //12
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B8B7B3}, //13
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B4B9B2B0}, //14
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B2B0B0B9}, //15
|
||||
{.a = 0xA94CAB611187, .b = 0x389109BD1D82}, //16
|
||||
{.a = 0xBF4280329F11, .b = 0x28D9EDD2096D}, //17
|
||||
{.a = 0xDE6BD90BD6B0, .b = 0x94866C16E9A4}, //18
|
||||
{.a = 0x2EA9493CAA7C, .b = 0x5068BCE2BC1C}, //19
|
||||
{.a = 0x15A41BA53F6C, .b = 0x3BD3CF43571C}, //20
|
||||
{.a = 0x1290FFD80DB5, .b = 0xD821B7916B7E}, //21
|
||||
{.a = 0x68C1A07E96A9, .b = 0x2B3323E75750}, //22
|
||||
{.a = 0xC699831AB307, .b = 0xCD7F7E9111F1}, //23
|
||||
{.a = 0x4E5884BF23E9, .b = 0x2287812A6AEE}, //24
|
||||
{.a = 0xC55212F716DC, .b = 0x594E368CCEFF}, //25
|
||||
{.a = 0x6EF127E674B1, .b = 0xDD21C8D3E0B9}, //26
|
||||
{.a = 0xFB79FAF4B55C, .b = 0xFE52B3B2A93B}, //27
|
||||
{.a = 0x6CF85CDFF647, .b = 0xCCAD7C41FC8A}, //28
|
||||
{.a = 0x591F6C130F91, .b = 0x2D2B734ECF91}, //29
|
||||
{.a = 0xEEB83529B79B, .b = 0xCB14E70EBA38}, //30
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31
|
||||
};
|
||||
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code
|
||||
static void resolve_station_name(Storage* storage, TicketData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
TicketData* ticket,
|
||||
bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else {
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
|
||||
static inline bool is_accompany_card(uint16_t departure_uic, uint16_t destination_uic) {
|
||||
return departure_uic == destination_uic;
|
||||
}
|
||||
|
||||
static void printf_accompany_card(TicketData* ticket, FuriString* parsed_data) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#SZPPK Accompany Card\nValid on: %02d-%02d-%04d\nStation: > %s\n",
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
furi_string_get_cstr(ticket->departure_name));
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> ENTERED STATION\nChecked in at:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1E:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> EXITED STATION\nChecked out at:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Status:> UNKNOWN\nPPK CNT: %03d\n", ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\e#SZPPK Transport Card\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name),
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
|
||||
if(ticket->value_data > 0) {
|
||||
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
|
||||
}
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1E:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
|
||||
ticket->current_status,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool szppk_so_verify(Nfc* nfc) {
|
||||
const uint8_t verify_sector = 19;
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
|
||||
MfClassicKey key = {};
|
||||
bit_lib_num_to_bytes_be(so_card_2k[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
|
||||
return error == MfClassicErrorNone;
|
||||
}
|
||||
|
||||
static bool szppk_so_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
bool is_read = false;
|
||||
MfClassicType type = MfClassicType1k;
|
||||
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error == MfClassicErrorNone) {
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
|
||||
for(size_t i = 0; i < 32; i++) {
|
||||
bit_lib_num_to_bytes_be(so_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(so_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNotPresent) {
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
is_read = (error == MfClassicErrorNone);
|
||||
}
|
||||
}
|
||||
|
||||
mf_classic_free(data);
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool szppk_so_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
|
||||
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
|
||||
if(key != so_card_2k[19].a) break;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
TicketData primary_ticket = {0};
|
||||
primary_ticket.departure_name = furi_string_alloc();
|
||||
primary_ticket.destination_name = furi_string_alloc();
|
||||
primary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
primary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
extract_ppk_data(storage, data, &primary_ticket, 0);
|
||||
if(is_accompany_card(primary_ticket.departure_uic, primary_ticket.destination_uic)) {
|
||||
printf_accompany_card(&primary_ticket, parsed_data);
|
||||
} else {
|
||||
printf_transport_card(parsed_data, &primary_ticket, false);
|
||||
}
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
|
||||
if(primary_ticket.second_ticket_marker != 0) {
|
||||
TicketData secondary_ticket = {0};
|
||||
secondary_ticket.departure_name = furi_string_alloc();
|
||||
secondary_ticket.destination_name = furi_string_alloc();
|
||||
secondary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
secondary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &secondary_ticket, 1);
|
||||
printf_transport_card(parsed_data, &secondary_ticket, true);
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
parsed = true;
|
||||
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static const NfcSupportedCardsPlugin szppk_so_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = szppk_so_verify,
|
||||
.read = szppk_so_read,
|
||||
.parse = szppk_so_parse,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor szppk_so_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &szppk_so_plugin,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* szppk_so_plugin_ep(void) {
|
||||
return &szppk_so_plugin_descriptor;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ bool two_cities_verify(Nfc* nfc) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
const uint8_t verify_sector = 4;
|
||||
const uint8_t verify_sector = 9;
|
||||
uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
FURI_LOG_D(TAG, "Verifying sector %u", verify_sector);
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
Filetype: Flipper NFC resources
|
||||
Version: 1
|
||||
# staID: Name
|
||||
9468: KULISHKI
|
||||
9469: KRASNOE
|
||||
968C: UGLICH
|
||||
97A7: 40 KM
|
||||
9C61: 29 KM
|
||||
AB91: YAROSLAVL-GL.
|
||||
AB92: YAROSLAVL-PASS
|
||||
AB93: PRIVOLZHYE
|
||||
AB94: FILINO
|
||||
ABD5: 300 KM
|
||||
ABFF: 4 KM
|
||||
AC4D: SKALINO
|
||||
AC4F: MARFINO
|
||||
AC50: PRECHISTOE
|
||||
AC51: MAKAROVO
|
||||
AC53: PURSHEVO
|
||||
AC54: RODIONOVO
|
||||
AC55: MASLOVO
|
||||
AC56: NEKOUZ
|
||||
AC57: SHESTIKHINO
|
||||
AC59: VOLGA
|
||||
AC5A: KOBOSTOVO
|
||||
AC5B: TIKHMENEVO
|
||||
AC5C: LOM
|
||||
AC5D: VAULOVO
|
||||
AC5E: CHEBAKOVO
|
||||
AC64: LYUTOVO
|
||||
AC65: TOSCHIKHA
|
||||
AC66: BURMAKINO
|
||||
AC68: PANTELEEVO
|
||||
AC69: PUTYATINO
|
||||
AC6A: PUCHKOVSKIY
|
||||
AC6B: UTKINO
|
||||
AC6F: KOZMODEMNSK
|
||||
AC70: KOROMYSLOVO
|
||||
AC71: SEMIBRATOVO
|
||||
AC72: ROSTOV-YARSLV.
|
||||
AC73: PETROVSK
|
||||
AC74: SIL'NITSY
|
||||
AC75: ITLAR'
|
||||
AC77: BEKLEMISHEVO
|
||||
AC78: RYAZANTSEVO
|
||||
AC79: SHUSHKOVO
|
||||
AC7A: BERENDEEVO
|
||||
AC8A: RYBINSK-PASS.
|
||||
ACA6: SEKSHA
|
||||
ACA7: LYUBIM
|
||||
ACA9: ZHAROK
|
||||
ACAA: SOT'
|
||||
ACAB: LUNKA
|
||||
ACD4: DANOLOV
|
||||
ACE9: DUNAIKA
|
||||
ACEA: LIPOVAYA GORA
|
||||
AD03: 155 KM
|
||||
AD04: 147 KM
|
||||
AD06: 187 KM
|
||||
AD07: 231 KM
|
||||
AD08: 259 KM
|
||||
AD09: 268 KM
|
||||
AD0A: 274 KM
|
||||
AD0B: DEPO
|
||||
AD0C: 310 KM
|
||||
AD0D: 322 KM
|
||||
AD0E: 331 KM
|
||||
AD0F: 343 KM
|
||||
AD10: 351 KM
|
||||
AD45: 361 KM
|
||||
AD46: 370 KM
|
||||
AD47: 378 KM
|
||||
AD48: 388 KM
|
||||
AD4A: 399 KM
|
||||
AD4B: 409 KM
|
||||
AD4C: 419 KM
|
||||
AEB1: KOCHENYATINO
|
||||
AEB3: BAGRIMOVO
|
||||
AEB4: REKA
|
||||
AEB5: SOSNOVTSY
|
||||
AEB9: DOGADTSEVO
|
||||
AEBD: KUDRYAVTSEVO
|
||||
AEBF: PROSVET
|
||||
AEC1: GAVROLOV YAM
|
||||
AEC3: LEVTSOVO
|
||||
AEC7: HOZHAEVO
|
||||
AECA: TSYBRINO
|
||||
AECB: ROKSHA
|
||||
AEF4: 69 KM
|
||||
AEF5: 56 KM
|
||||
AEF6: 323 KM
|
||||
AEFA: 287 KM
|
||||
AEFB: 296 KM
|
||||
AEFC: 305 KM
|
||||
AEFD: 310 KM
|
||||
AEFE: 318 KM
|
||||
AF0E: 265 KM
|
||||
AF0F: 285 KM
|
||||
B057: MAKAROVSKAYA
|
||||
B05A: USHAKOVO
|
||||
B05F: VAREGOVO
|
||||
B07A: 306 KM
|
||||
B07B: 296 KM
|
||||
B07C: YURINSKIY
|
||||
B07D: TOROPOVO
|
||||
B07E: PINYAGI
|
||||
B07F: CHIZHOVO
|
||||
B080: KLINTSEVO
|
||||
B081: TENINO
|
||||
B082: MOLOT
|
||||
B083: TELISCHEVO
|
||||
B084: SUHAREZH
|
||||
B086: 324 KM
|
||||
B087: POLYANKI
|
||||
B08A: DEBOLOVSKAYA
|
||||
B08B: 2 KM
|
||||
B090: RUSHA
|
||||
B0AC: KOTOROSL'
|
||||
B0D1: 403 KM
|
||||
B0D2: SHOLOSOVO
|
||||
B0D3: MURINSKOE
|
||||
B0D4: ONAN'JINO
|
||||
B0D5: L'NOZAVOD
|
||||
B0F3: 359 KM
|
||||
@@ -0,0 +1,85 @@
|
||||
Filetype: Flipper NFC resources
|
||||
Version: 1
|
||||
# staID: Name
|
||||
8377: OP 1725 KM
|
||||
8350: STEKOL'NYI
|
||||
834F: SELHOZTECHNIKA
|
||||
834D: KANGLY
|
||||
834B: OP 1861 KM
|
||||
834A: OP 1856 KM
|
||||
8349: OP 1842 KM
|
||||
8348: OP 1832 KM
|
||||
8347: OP 1806 KM
|
||||
8344: OP 44 KM
|
||||
8343: OP 129 KM
|
||||
8288: SUVOROVSKAYA
|
||||
8188: NESKUCHNYI
|
||||
8183: OP 1741 KM
|
||||
8182: KOCHUBEEVSKII
|
||||
817E: SURKUL
|
||||
817D: OP 1823 KM
|
||||
8177: TERSKII
|
||||
8176: KRASNAYA
|
||||
8175: TEPLAYA RECHKA
|
||||
8165: DVORTSOVYI
|
||||
8164: PIONER
|
||||
8161: ORBEL'YANOVO
|
||||
815F: DZHEMUHA
|
||||
815D: ETOKA
|
||||
8158: OBIL'NYI
|
||||
8157: NINY
|
||||
813C: STAVROPOL
|
||||
8104: KARMALINOVSKII
|
||||
8084: KRASN.DEREVNIA
|
||||
807E: MIHAILOVSKAIA
|
||||
7FF8: ZMEIKA
|
||||
7FF6: MASHUK
|
||||
7FF5: LERMONTOVSKII
|
||||
7FF4: NOVOPIATIGORSK
|
||||
7FF3: SKACHKI
|
||||
7FF2: ZOLOTUSHKA
|
||||
7FF1: BELYI UGOL'
|
||||
7FF0: PODKUMOK
|
||||
7FEF: MINUTKA
|
||||
7FBA: PLAKSEIKA
|
||||
7FB9: MASLOV KUT
|
||||
7FB8: ZELENOKUMSK
|
||||
7FB7: KUMA
|
||||
7FB6: GEORGIEVSK
|
||||
7FA6: VIAZNIKI
|
||||
7FA4: YAGODKA
|
||||
7FA1: MAIAK
|
||||
7F9B: BEDENNOVSK
|
||||
7F6A: PALAGIADA
|
||||
7F69: RYZDVIANAIA
|
||||
7F68: PEREDOVAIA
|
||||
7F65: GRIGOROPOLISSK.
|
||||
7F5C: NEVINNOMYSSK.
|
||||
7F2B: INOZEMTSEVO
|
||||
7F2A: MIN.VODY
|
||||
7F1B: ZHELEZNOVODSK
|
||||
7F17: BESHTAU
|
||||
7EFD: RASSHEVATKA
|
||||
7EBC: PIATIGORSK
|
||||
7EB2: KISLOVODSK
|
||||
7EA5: ZOLSKII
|
||||
7EA4: VINOGRADNAIA
|
||||
7EA2: KUMAGORSK
|
||||
7E9F: NAGUTSKAIA
|
||||
7E9D: KURSAVKA
|
||||
7E9B: KIAN
|
||||
7E9A: ZELENCHUK
|
||||
7E96: BOGLOVSKAIA
|
||||
7E95: OVECHKA
|
||||
8162: KURSHAVA
|
||||
7F97: CHUKOTSKII
|
||||
83AE: DEBRI
|
||||
8306: DEGTIAREVSKI
|
||||
8185: NIVA
|
||||
8160: OP 1814 KM
|
||||
815B: CHISTAIA
|
||||
7FA5: BERMEDSKII
|
||||
7F93: IZOBILNAIA
|
||||
7F64: KRASNOKUBANSK.
|
||||
7ED0: ESSENTUKI
|
||||
7EA6: APOLLONSKAIA
|
||||
@@ -0,0 +1,151 @@
|
||||
Filetype: Flipper NFC resources
|
||||
Version: 1
|
||||
# staID: Name
|
||||
9426: LADOZH.VOKZ.
|
||||
9421: MOSKOV.VOKZ.
|
||||
9423: VITEBS.VOKZ
|
||||
9424: FINLND.VOKZ
|
||||
9425: BALT.VOKZ
|
||||
948B: IM.MOROZOVA
|
||||
94B4: TOSNO
|
||||
94B5: SABLINO
|
||||
94BE: GORY
|
||||
94C0: IZHORY
|
||||
94C1: RYBATSKOE
|
||||
24C2: OBUKHOVO
|
||||
94C3: SLAVYANKA
|
||||
94C4: KOLPINO
|
||||
94C9: MSHINSKAYA
|
||||
94CC: STROGANOVO
|
||||
94CD: SUI'DA
|
||||
94CE: GATCHINA BLT.
|
||||
94D4: VYRITSA
|
||||
94D5: PAVLOVSK
|
||||
94D6: TSARSKOE SELO
|
||||
94E2: KALISHCHE
|
||||
94E3: LEBYAZHYE
|
||||
94E4: BOLSH.IZHORA
|
||||
9507: BELOOSTROV
|
||||
9508: ZELENOGORSK
|
||||
9511: RUCH'I
|
||||
9512: TOKSOVO
|
||||
9513: OSEL'KI
|
||||
9514: PERI
|
||||
9515: GRUZINO
|
||||
9516: VASKELOVO
|
||||
9517: OREKHOVO
|
||||
9518: PETYAJARVI
|
||||
9519: LOSEVO
|
||||
951A: GROMOVO
|
||||
951F: KUZNECHNOE
|
||||
95C3: KAPITOLOVO
|
||||
969A: MGA
|
||||
969C: LUGA
|
||||
969D: TOLMACHEVO
|
||||
969E: SIVERSKAYA
|
||||
969F: GATCHINA VRSH.
|
||||
96A1: OREDEZH
|
||||
96A3: SLANTSY
|
||||
96A5: ORANIENBAUM 1
|
||||
96A8: VOLOSOVO
|
||||
96BD: TIKHVIN
|
||||
96C0: VOLKHOVSTROY 1
|
||||
96C1: VOLKHOVSTROY 2
|
||||
96CA: VYBORG
|
||||
96CB: SOSNOVO
|
||||
96CC: PRIOZYORSK
|
||||
970A: BOLOTISTOE
|
||||
971D: STAR.DEREVNYA
|
||||
9721: 54 KM
|
||||
9723: KOLOSKOVO(79KM)
|
||||
972C: PARAVOZNY MUZEI
|
||||
9741: BOROVAYA
|
||||
97BB: DETSKOSELSKAYA
|
||||
9809: KUPCHINO
|
||||
9810: PETROKREPOST'
|
||||
9811: ALEKSANDROVSK.F
|
||||
9817: BERNGARDOVKA
|
||||
981C: PROSPEKT SLAVY
|
||||
9826: SOSNOVAYA POL.
|
||||
9829: PUDOST'
|
||||
982B: LENINSKI' PR.
|
||||
982D: PAVLOVO-NA-NEVE
|
||||
9835: LANSKAYA
|
||||
9838: KANNELYARVI
|
||||
983A: MELN.RUCHEI
|
||||
983F: PISKARYOVKA
|
||||
9849: UDEL'NAYA
|
||||
9852: BRONKA
|
||||
9853: VSEVOLOZHSK.
|
||||
985D: FARFOROVSKAYA
|
||||
9867: RZHEVKA
|
||||
9868: KOBRALOVO
|
||||
986B: AEROPORT
|
||||
9871: BRONEVAYA
|
||||
987B: SESTRORETSK
|
||||
9880: TATIANINO
|
||||
988A: KUSHELEVKA
|
||||
9890: RAKH'YA
|
||||
9894: UL'YANKA
|
||||
9899: PESOCHNAYA
|
||||
989A: NAVALOCHNAYA
|
||||
989E: STREL'NA
|
||||
98A3: ST.PETERGOF
|
||||
98A5: KOLTUSHI
|
||||
98A8: KUZ'MOLOVO
|
||||
98AD: KRASN. SELO
|
||||
98AE: SHUSHARY
|
||||
98BC: GORELOVO
|
||||
98C1: ROSHCHINO
|
||||
98CB: LISII' NOS
|
||||
98D5: DACHNOYE
|
||||
98D6: PREDPORTOVAYA
|
||||
98E4: NOVAYA OKHTA
|
||||
98E9: REPINO
|
||||
98F3: LEVASHOVO
|
||||
98FD: NOV.DEREVNYA
|
||||
98FE: IZHORSK.ZAVOD
|
||||
9908: VAGANOVO
|
||||
990C: SHUVALOVO
|
||||
9911: DUNAI'
|
||||
9916: IRINOVKA
|
||||
991C: TARKHOVKA
|
||||
9923: PUPYSHEVO
|
||||
9925: TAI'TSY
|
||||
9926: LAVRIKI
|
||||
992A: LADOZHSK.OZ.
|
||||
992D: ROMANOVKA
|
||||
9936: RAZLIV
|
||||
9938: KIRILLOVSKOE
|
||||
9939: OL'GINO
|
||||
993D: METALLOSTROY
|
||||
993E: KAVGOLOVO
|
||||
9943: VOZD.PARK
|
||||
9944: POST KOVALEVO
|
||||
9949: LAPPELOVO
|
||||
994D: KORNEVO
|
||||
9952: SOLNECHNOYE
|
||||
9956: MYAGLOVO
|
||||
995B: OZERKI
|
||||
995C: KOMAROVO
|
||||
9969: PROBA
|
||||
996A: KIRPICH.ZAVOD
|
||||
9970: KURORT
|
||||
9975: 67 KM
|
||||
9976: USHKOVO
|
||||
9981: DUDERGOF
|
||||
9989: UNIVERSITETSK.
|
||||
9992: YAKHTENNAYA
|
||||
9996: KOVALEVO
|
||||
9997: DIBUNY
|
||||
99AF: MANUSHKINO
|
||||
99CF: PARGOLOVO
|
||||
99D4: LIGOVO
|
||||
9C09: DEVYATKINO
|
||||
9C58: NEV.DUBROVKA
|
||||
9C5A: ALEKSANDROVSK.
|
||||
9C5B: GORSKAYA
|
||||
9D1C: NOV.PETERGOF
|
||||
9FDC: BORISOVA GRIVA
|
||||
98B7: SERGIEVO
|
||||
9957: LAKHTA
|
||||
Reference in New Issue
Block a user