From 4cc5b089ff2e10dc28a23cbe36cd10a4029e6f14 Mon Sep 17 00:00:00 2001 From: mxcdoam <72457810+mxcdoam@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:15:35 +0300 Subject: [PATCH 1/3] applicatin.fam update --- applications/main/nfc/application.fam | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 275da7417..9e5caf8df 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -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, From f75a1c69aa1fb26e394c15460e5f951e8e8afec5 Mon Sep 17 00:00:00 2001 From: mxcdoam <72457810+mxcdoam@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:22:20 +0300 Subject: [PATCH 2/3] updated parsers --- .../nfc/plugins/supported_cards/plantain.c | 839 +++++++++++++----- .../nfc/plugins/supported_cards/sevppk_tk.c | 399 +++++++++ .../main/nfc/plugins/supported_cards/sk_tk.c | 406 +++++++++ .../nfc/plugins/supported_cards/szppk_so.c | 447 ++++++++++ .../nfc/plugins/supported_cards/two_cities.c | 2 +- .../main/nfc/resources/nfc/assets/sev_id.nfc | 124 +++ .../main/nfc/resources/nfc/assets/sk_id.nfc | 85 ++ .../main/nfc/resources/nfc/assets/sz_id.nfc | 151 ++++ 8 files changed, 2211 insertions(+), 242 deletions(-) create mode 100644 applications/main/nfc/plugins/supported_cards/sevppk_tk.c create mode 100644 applications/main/nfc/plugins/supported_cards/sk_tk.c create mode 100644 applications/main/nfc/plugins/supported_cards/szppk_so.c create mode 100644 applications/main/nfc/resources/nfc/assets/sev_id.nfc create mode 100644 applications/main/nfc/resources/nfc/assets/sk_id.nfc create mode 100644 applications/main/nfc/resources/nfc/assets/sz_id.nfc diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index add7ab560..53bacfbf2 100644 --- a/applications/main/nfc/plugins/supported_cards/plantain.c +++ b/applications/main/nfc/plugins/supported_cards/plantain.c @@ -1,23 +1,21 @@ +//Based on parsers written by Leptoptilos and Assasinfil + #include "nfc_supported_card_plugin.h" - #include - #include #include #include #include - -#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 +#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; } diff --git a/applications/main/nfc/plugins/supported_cards/sevppk_tk.c b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c new file mode 100644 index 000000000..ba8eef6a2 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c @@ -0,0 +1,399 @@ +//Based on parsers written by Leptoptilos and Assasinfil + +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/applications/main/nfc/plugins/supported_cards/sk_tk.c b/applications/main/nfc/plugins/supported_cards/sk_tk.c new file mode 100644 index 000000000..42d4e7e96 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/sk_tk.c @@ -0,0 +1,406 @@ +//Based on parsers written by Leptoptilos and Assasinfil + +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/applications/main/nfc/plugins/supported_cards/szppk_so.c b/applications/main/nfc/plugins/supported_cards/szppk_so.c new file mode 100644 index 000000000..2683f0105 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/szppk_so.c @@ -0,0 +1,447 @@ +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/applications/main/nfc/plugins/supported_cards/two_cities.c b/applications/main/nfc/plugins/supported_cards/two_cities.c index 240c6c585..1295b1c65 100644 --- a/applications/main/nfc/plugins/supported_cards/two_cities.c +++ b/applications/main/nfc/plugins/supported_cards/two_cities.c @@ -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); diff --git a/applications/main/nfc/resources/nfc/assets/sev_id.nfc b/applications/main/nfc/resources/nfc/assets/sev_id.nfc new file mode 100644 index 000000000..35a2bc234 --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/sev_id.nfc @@ -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 \ No newline at end of file diff --git a/applications/main/nfc/resources/nfc/assets/sk_id.nfc b/applications/main/nfc/resources/nfc/assets/sk_id.nfc new file mode 100644 index 000000000..db840ad21 --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/sk_id.nfc @@ -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 diff --git a/applications/main/nfc/resources/nfc/assets/sz_id.nfc b/applications/main/nfc/resources/nfc/assets/sz_id.nfc new file mode 100644 index 000000000..98c6f287b --- /dev/null +++ b/applications/main/nfc/resources/nfc/assets/sz_id.nfc @@ -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 \ No newline at end of file From bdff6c7bc8bbe49cf3a1d5a2eb05afb1055b5353 Mon Sep 17 00:00:00 2001 From: mxcdoam <72457810+mxcdoam@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:03:17 +0300 Subject: [PATCH 3/3] Credits --- applications/main/nfc/plugins/supported_cards/plantain.c | 2 +- applications/main/nfc/plugins/supported_cards/sevppk_tk.c | 2 +- applications/main/nfc/plugins/supported_cards/sk_tk.c | 2 +- applications/main/nfc/plugins/supported_cards/szppk_so.c | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index 53bacfbf2..a9cf2c946 100644 --- a/applications/main/nfc/plugins/supported_cards/plantain.c +++ b/applications/main/nfc/plugins/supported_cards/plantain.c @@ -1,4 +1,4 @@ -//Based on parsers written by Leptoptilos and Assasinfil +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! #include "nfc_supported_card_plugin.h" #include diff --git a/applications/main/nfc/plugins/supported_cards/sevppk_tk.c b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c index ba8eef6a2..e069c8c9a 100644 --- a/applications/main/nfc/plugins/supported_cards/sevppk_tk.c +++ b/applications/main/nfc/plugins/supported_cards/sevppk_tk.c @@ -1,4 +1,4 @@ -//Based on parsers written by Leptoptilos and Assasinfil +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! #include "nfc_supported_card_plugin.h" #include diff --git a/applications/main/nfc/plugins/supported_cards/sk_tk.c b/applications/main/nfc/plugins/supported_cards/sk_tk.c index 42d4e7e96..a294ed45d 100644 --- a/applications/main/nfc/plugins/supported_cards/sk_tk.c +++ b/applications/main/nfc/plugins/supported_cards/sk_tk.c @@ -1,4 +1,4 @@ -//Based on parsers written by Leptoptilos and Assasinfil +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! #include "nfc_supported_card_plugin.h" #include diff --git a/applications/main/nfc/plugins/supported_cards/szppk_so.c b/applications/main/nfc/plugins/supported_cards/szppk_so.c index 2683f0105..16bbbd807 100644 --- a/applications/main/nfc/plugins/supported_cards/szppk_so.c +++ b/applications/main/nfc/plugins/supported_cards/szppk_so.c @@ -1,3 +1,5 @@ +//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL () for help! + #include "nfc_supported_card_plugin.h" #include #include