Просмотр исходного кода

Add new card parsers (#1503)

* Add the "Two cities" parser
* Add plantain and plantain4k parsers
* Add new parsers to the supported list
* United card PoC
* Fix nfc device not sleeping
* Completely read the 4K troika variants
* Correct naming
* Update to reflect upstream changes
* Add support for MfUl info
* Fix parsers
* Card type detection fixes
* Remove debug info
* Fixes for the verification of cards
* nfc: fix verification for supported cards
* nfc: remove unused vars
* Improve card reading reliability and fix plantain
* plantain: change log level

Co-authored-by: gornekich <n.gorbadey@gmail.com>
Astra 3 лет назад
Родитель
Сommit
9f3b80e606

+ 3 - 1
applications/main/nfc/scenes/nfc_scene_device_info.c

@@ -47,7 +47,9 @@ void nfc_scene_device_info_on_enter(void* context) {
             }
             string_clear(country_name);
         }
-    } else if(dev_data->protocol == NfcDeviceProtocolMifareClassic) {
+    } else if(
+        dev_data->protocol == NfcDeviceProtocolMifareClassic ||
+        dev_data->protocol == NfcDeviceProtocolMifareUl) {
         string_set(temp_str, nfc->dev->dev_data.parsed_data);
     }
 

+ 13 - 9
applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c

@@ -34,15 +34,19 @@ void nfc_scene_mf_ultralight_read_success_on_enter(void* context) {
         nfc);
 
     string_t temp_str;
-    string_init_printf(temp_str, "\e#%s\n", nfc_mf_ul_type(mf_ul_data->type, true));
-    string_cat_printf(temp_str, "UID:");
-    for(size_t i = 0; i < data->uid_len; i++) {
-        string_cat_printf(temp_str, " %02X", data->uid[i]);
-    }
-    string_cat_printf(
-        temp_str, "\nPages Read: %d/%d", mf_ul_data->data_read / 4, mf_ul_data->data_size / 4);
-    if(mf_ul_data->data_read != mf_ul_data->data_size) {
-        string_cat_printf(temp_str, "\nPassword-protected pages!");
+    if(string_size(nfc->dev->dev_data.parsed_data)) {
+        string_init_set(temp_str, nfc->dev->dev_data.parsed_data);
+    } else {
+        string_init_printf(temp_str, "\e#%s\n", nfc_mf_ul_type(mf_ul_data->type, true));
+        string_cat_printf(temp_str, "UID:");
+        for(size_t i = 0; i < data->uid_len; i++) {
+            string_cat_printf(temp_str, " %02X", data->uid[i]);
+        }
+        string_cat_printf(
+            temp_str, "\nPages Read: %d/%d", mf_ul_data->data_read / 4, mf_ul_data->data_size / 4);
+        if(mf_ul_data->data_read != mf_ul_data->data_size) {
+            string_cat_printf(temp_str, "\nPassword-protected pages!");
+        }
     }
     widget_add_text_scroll_element(widget, 0, 0, 128, 52, string_get_cstr(temp_str));
     string_clear(temp_str);

+ 3 - 1
applications/main/nfc/scenes/nfc_scene_saved_menu.c

@@ -91,7 +91,9 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
             bool application_info_present = false;
             if(dev_data->protocol == NfcDeviceProtocolEMV) {
                 application_info_present = true;
-            } else if(dev_data->protocol == NfcDeviceProtocolMifareClassic) {
+            } else if(
+                dev_data->protocol == NfcDeviceProtocolMifareClassic ||
+                dev_data->protocol == NfcDeviceProtocolMifareUl) {
                 application_info_present = nfc_supported_card_verify_and_parse(dev_data);
             }
 

+ 23 - 2
lib/nfc/nfc_worker.c

@@ -123,7 +123,25 @@ static bool nfc_worker_read_mf_ultralight(NfcWorker* nfc_worker, FuriHalNfcTxRxC
     }
 
     do {
-        // Read card
+        // Try to read supported card
+        FURI_LOG_I(TAG, "Trying to read a supported card ...");
+        for(size_t i = 0; i < NfcSupportedCardTypeEnd; i++) {
+            if(nfc_supported_card[i].protocol == NfcDeviceProtocolMifareUl) {
+                if(nfc_supported_card[i].verify(nfc_worker, tx_rx)) {
+                    if(nfc_supported_card[i].read(nfc_worker, tx_rx)) {
+                        read_success = true;
+                        nfc_supported_card[i].parse(nfc_worker->dev_data);
+                        break;
+                    }
+                } else {
+                    furi_hal_nfc_sleep();
+                }
+            }
+        }
+        if(read_success) break;
+        furi_hal_nfc_sleep();
+
+        // Otherwise, try to read as usual
         if(!furi_hal_nfc_detect(&nfc_worker->dev_data->nfc_data, 200)) break;
         if(!mf_ul_read_card(tx_rx, &reader, &data)) break;
         // Copy data
@@ -149,14 +167,17 @@ static bool nfc_worker_read_mf_classic(NfcWorker* nfc_worker, FuriHalNfcTxRxCont
 
     do {
         // Try to read supported card
-        FURI_LOG_I(TAG, "Try read supported card ...");
+        FURI_LOG_I(TAG, "Trying to read a supported card ...");
         for(size_t i = 0; i < NfcSupportedCardTypeEnd; i++) {
             if(nfc_supported_card[i].protocol == NfcDeviceProtocolMifareClassic) {
                 if(nfc_supported_card[i].verify(nfc_worker, tx_rx)) {
                     if(nfc_supported_card[i].read(nfc_worker, tx_rx)) {
                         read_success = true;
                         nfc_supported_card[i].parse(nfc_worker->dev_data);
+                        break;
                     }
+                } else {
+                    furi_hal_nfc_sleep();
                 }
             }
         }

+ 113 - 0
lib/nfc/parsers/all_in_one.c

@@ -0,0 +1,113 @@
+#include "nfc_supported_card.h"
+#include "all_in_one.h"
+
+#include <gui/modules/widget.h>
+#include <nfc_worker_i.h>
+
+#include "furi_hal.h"
+
+#define ALL_IN_ONE_LAYOUT_UNKNOWN 0
+#define ALL_IN_ONE_LAYOUT_A 1
+#define ALL_IN_ONE_LAYOUT_D 2
+#define ALL_IN_ONE_LAYOUT_E2 3
+#define ALL_IN_ONE_LAYOUT_E3 4
+#define ALL_IN_ONE_LAYOUT_E5 5
+#define ALL_IN_ONE_LAYOUT_2 6
+
+uint8_t all_in_one_get_layout(NfcDeviceData* dev_data) {
+    // I absolutely hate what's about to happen here.
+
+    // Switch on the second half of the third byte of page 5
+    FURI_LOG_I("all_in_one", "Layout byte: %02x", dev_data->mf_ul_data.data[(4 * 5) + 2]);
+    FURI_LOG_I(
+        "all_in_one", "Layout half-byte: %02x", dev_data->mf_ul_data.data[(4 * 5) + 3] & 0x0F);
+    switch(dev_data->mf_ul_data.data[(4 * 5) + 2] & 0x0F) {
+    // If it is A, the layout type is a type A layout
+    case 0x0A:
+        return ALL_IN_ONE_LAYOUT_A;
+    case 0x0D:
+        return ALL_IN_ONE_LAYOUT_D;
+    case 0x02:
+        return ALL_IN_ONE_LAYOUT_2;
+    default:
+        FURI_LOG_I(
+            "all_in_one",
+            "Unknown layout type: %d",
+            dev_data->mf_ul_data.data[(4 * 5) + 2] & 0x0F);
+        return ALL_IN_ONE_LAYOUT_UNKNOWN;
+    }
+}
+
+bool all_in_one_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    UNUSED(nfc_worker);
+    // If this is a all_in_one pass, first 2 bytes of page 4 are 0x45 0xD9
+    MfUltralightReader reader = {};
+    MfUltralightData data = {};
+
+    if(!mf_ul_read_card(tx_rx, &reader, &data)) {
+        return false;
+    } else {
+        if(data.data[4 * 4] == 0x45 && data.data[4 * 4 + 1] == 0xD9) {
+            FURI_LOG_I("all_in_one", "Pass verified");
+            return true;
+        }
+    }
+    return false;
+}
+
+bool all_in_one_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    MfUltralightReader reader = {};
+    MfUltralightData data = {};
+    if(!mf_ul_read_card(tx_rx, &reader, &data)) {
+        return false;
+    } else {
+        memcpy(&nfc_worker->dev_data->mf_ul_data, &data, sizeof(data));
+        FURI_LOG_I("all_in_one", "Card read");
+        return true;
+    }
+}
+
+bool all_in_one_parser_parse(NfcDeviceData* dev_data) {
+    if(dev_data->mf_ul_data.data[4 * 4] != 0x45 || dev_data->mf_ul_data.data[4 * 4 + 1] != 0xD9) {
+        FURI_LOG_I("all_in_one", "Pass not verified");
+        return false;
+    }
+
+    // If the layout is a then the ride count is stored in the first byte of page 8
+    uint8_t ride_count = 0;
+    uint32_t serial = 0;
+    if(all_in_one_get_layout(dev_data) == ALL_IN_ONE_LAYOUT_A) {
+        ride_count = dev_data->mf_ul_data.data[4 * 8];
+    } else if(all_in_one_get_layout(dev_data) == ALL_IN_ONE_LAYOUT_D) {
+        // If the layout is D, the ride count is stored in the second byte of page 9
+        ride_count = dev_data->mf_ul_data.data[4 * 9 + 1];
+        // I hate this with a burning passion.
+
+        // The number starts at the second half of the third byte on page 4, and is 32 bits long
+        // So we get the second half of the third byte, then bytes 4-6, and then the first half of the 7th byte
+        // B8 17 A2 A4 BD becomes 81 7A 2A 4B
+        serial = (dev_data->mf_ul_data.data[4 * 4 + 2] & 0x0F) << 28 |
+                 dev_data->mf_ul_data.data[4 * 4 + 3] << 20 |
+                 dev_data->mf_ul_data.data[4 * 4 + 4] << 12 |
+                 dev_data->mf_ul_data.data[4 * 4 + 5] << 4 |
+                 (dev_data->mf_ul_data.data[4 * 4 + 6] >> 4);
+    } else {
+        FURI_LOG_I("all_in_one", "Unknown layout: %d", all_in_one_get_layout(dev_data));
+        ride_count = 137;
+    }
+
+    // I hate this with a burning passion.
+
+    // The number starts at the second half of the third byte on page 4, and is 32 bits long
+    // So we get the second half of the third byte, then bytes 4-6, and then the first half of the 7th byte
+    // B8 17 A2 A4 BD becomes 81 7A 2A 4B
+    serial =
+        (dev_data->mf_ul_data.data[4 * 4 + 2] & 0x0F) << 28 |
+        dev_data->mf_ul_data.data[4 * 4 + 3] << 20 | dev_data->mf_ul_data.data[4 * 4 + 4] << 12 |
+        dev_data->mf_ul_data.data[4 * 4 + 5] << 4 | (dev_data->mf_ul_data.data[4 * 4 + 6] >> 4);
+
+    // Format string for rides count
+    string_printf(
+        dev_data->parsed_data, "\e#All-In-One\nNumber: %u\nRides left: %u", serial, ride_count);
+    return true;
+}

+ 9 - 0
lib/nfc/parsers/all_in_one.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "nfc_supported_card.h"
+
+bool all_in_one_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool all_in_one_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool all_in_one_parser_parse(NfcDeviceData* dev_data);

+ 45 - 5
lib/nfc/parsers/nfc_supported_card.c

@@ -1,14 +1,54 @@
 #include "nfc_supported_card.h"
 
-#include "troyka_parser.h"
+#include "plantain_parser.h"
+#include "troika_parser.h"
+#include "plantain_4k_parser.h"
+#include "troika_4k_parser.h"
+#include "two_cities.h"
+#include "all_in_one.h"
 
 NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = {
-    [NfcSupportedCardTypeTroyka] =
+    [NfcSupportedCardTypePlantain] =
         {
             .protocol = NfcDeviceProtocolMifareClassic,
-            .verify = troyka_parser_verify,
-            .read = troyka_parser_read,
-            .parse = troyka_parser_parse,
+            .verify = plantain_parser_verify,
+            .read = plantain_parser_read,
+            .parse = plantain_parser_parse,
+        },
+    [NfcSupportedCardTypeTroika] =
+        {
+            .protocol = NfcDeviceProtocolMifareClassic,
+            .verify = troika_parser_verify,
+            .read = troika_parser_read,
+            .parse = troika_parser_parse,
+        },
+    [NfcSupportedCardTypePlantain4K] =
+        {
+            .protocol = NfcDeviceProtocolMifareClassic,
+            .verify = plantain_4k_parser_verify,
+            .read = plantain_4k_parser_read,
+            .parse = plantain_4k_parser_parse,
+        },
+    [NfcSupportedCardTypeTroika4K] =
+        {
+            .protocol = NfcDeviceProtocolMifareClassic,
+            .verify = troika_4k_parser_verify,
+            .read = troika_4k_parser_read,
+            .parse = troika_4k_parser_parse,
+        },
+    [NfcSupportedCardTypeTwoCities] =
+        {
+            .protocol = NfcDeviceProtocolMifareClassic,
+            .verify = two_cities_parser_verify,
+            .read = two_cities_parser_read,
+            .parse = two_cities_parser_parse,
+        },
+    [NfcSupportedCardTypeAllInOne] =
+        {
+            .protocol = NfcDeviceProtocolMifareUl,
+            .verify = all_in_one_parser_verify,
+            .read = all_in_one_parser_read,
+            .parse = all_in_one_parser_parse,
         },
 };
 

+ 6 - 1
lib/nfc/parsers/nfc_supported_card.h

@@ -7,7 +7,12 @@
 #include <m-string.h>
 
 typedef enum {
-    NfcSupportedCardTypeTroyka,
+    NfcSupportedCardTypePlantain,
+    NfcSupportedCardTypeTroika,
+    NfcSupportedCardTypePlantain4K,
+    NfcSupportedCardTypeTroika4K,
+    NfcSupportedCardTypeTwoCities,
+    NfcSupportedCardTypeAllInOne,
 
     NfcSupportedCardTypeEnd,
 } NfcSupportedCardType;

+ 153 - 0
lib/nfc/parsers/plantain_4k_parser.c

@@ -0,0 +1,153 @@
+#include "nfc_supported_card.h"
+#include "plantain_parser.h" // For luhn and string_push_uint64
+
+#include <gui/modules/widget.h>
+#include <nfc_worker_i.h>
+
+#include "furi_hal.h"
+
+static const MfClassicAuthContext plantain_keys_4k[] = {
+    {.sector = 0, .key_a = 0xFFFFFFFFFFFF, .key_b = 0xFFFFFFFFFFFF},
+    {.sector = 1, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 2, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 3, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 4, .key_a = 0xe56ac127dd45, .key_b = 0x19fc84a3784b},
+    {.sector = 5, .key_a = 0x77dabc9825e1, .key_b = 0x9764fec3154a},
+    {.sector = 6, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 7, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 8, .key_a = 0x26973ea74321, .key_b = 0xd27058c6e2c7},
+    {.sector = 9, .key_a = 0xeb0a8ff88ade, .key_b = 0x578a9ada41e3},
+    {.sector = 10, .key_a = 0xea0fd73cb149, .key_b = 0x29c35fa068fb},
+    {.sector = 11, .key_a = 0xc76bf71a2509, .key_b = 0x9ba241db3f56},
+    {.sector = 12, .key_a = 0xacffffffffff, .key_b = 0x71f3a315ad26},
+    {.sector = 13, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 14, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 15, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 16, .key_a = 0x72f96bdd3714, .key_b = 0x462225cd34cf},
+    {.sector = 17, .key_a = 0x044ce1872bc3, .key_b = 0x8c90c70cff4a},
+    {.sector = 18, .key_a = 0xbc2d1791dec1, .key_b = 0xca96a487de0b},
+    {.sector = 19, .key_a = 0x8791b2ccb5c4, .key_b = 0xc956c3b80da3},
+    {.sector = 20, .key_a = 0x8e26e45e7d65, .key_b = 0x8e65b3af7d22},
+    {.sector = 21, .key_a = 0x0f318130ed18, .key_b = 0x0c420a20e056},
+    {.sector = 22, .key_a = 0x045ceca15535, .key_b = 0x31bec3d9e510},
+    {.sector = 23, .key_a = 0x9d993c5d4ef4, .key_b = 0x86120e488abf},
+    {.sector = 24, .key_a = 0xc65d4eaa645b, .key_b = 0xb69d40d1a439},
+    {.sector = 25, .key_a = 0x3a8a139c20b4, .key_b = 0x8818a9c5d406},
+    {.sector = 26, .key_a = 0xbaff3053b496, .key_b = 0x4b7cb25354d3},
+    {.sector = 27, .key_a = 0x7413b599c4ea, .key_b = 0xb0a2AAF3A1BA},
+    {.sector = 28, .key_a = 0x0ce7cd2cc72b, .key_b = 0xfa1fbb3f0f1f},
+    {.sector = 29, .key_a = 0x0be5fac8b06a, .key_b = 0x6f95887a4fd3},
+    {.sector = 30, .key_a = 0x0eb23cc8110b, .key_b = 0x04dc35277635},
+    {.sector = 31, .key_a = 0xbc4580b7f20b, .key_b = 0xd0a4131fb290},
+    {.sector = 32, .key_a = 0x7a396f0d633d, .key_b = 0xad2bdc097023},
+    {.sector = 33, .key_a = 0xa3faa6daff67, .key_b = 0x7600e889adf9},
+    {.sector = 34, .key_a = 0xfd8705e721b0, .key_b = 0x296fc317a513},
+    {.sector = 35, .key_a = 0x22052b480d11, .key_b = 0xe19504c39461},
+    {.sector = 36, .key_a = 0xa7141147d430, .key_b = 0xff16014fefc7},
+    {.sector = 37, .key_a = 0x8a8d88151a00, .key_b = 0x038b5f9b5a2a},
+    {.sector = 38, .key_a = 0xb27addfb64b0, .key_b = 0x152fd0c420a7},
+    {.sector = 39, .key_a = 0x7259fa0197c6, .key_b = 0x5583698df085},
+};
+
+bool plantain_4k_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+    UNUSED(nfc_worker);
+
+    if(nfc_worker->dev_data->mf_classic_data.type != MfClassicType4k) {
+        return false;
+    }
+
+    uint8_t sector = 8;
+    uint8_t block = mf_classic_get_sector_trailer_block_num_by_sector(sector);
+    FURI_LOG_D("Plant4K", "Verifying sector %d", sector);
+    if(mf_classic_authenticate(tx_rx, block, 0x26973ea74321, MfClassicKeyA)) {
+        FURI_LOG_D("Plant4K", "Sector %d verified", sector);
+        return true;
+    }
+    return false;
+}
+
+bool plantain_4k_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+
+    MfClassicReader reader = {};
+    FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data;
+    reader.type = mf_classic_get_classic_type(nfc_data->atqa[0], nfc_data->atqa[1], nfc_data->sak);
+    for(size_t i = 0; i < COUNT_OF(plantain_keys_4k); i++) {
+        mf_classic_reader_add_sector(
+            &reader,
+            plantain_keys_4k[i].sector,
+            plantain_keys_4k[i].key_a,
+            plantain_keys_4k[i].key_b);
+        FURI_LOG_T("plant4k", "Added sector %d", plantain_keys_4k[i].sector);
+    }
+    for(int i = 0; i < 5; i++) {
+        if(mf_classic_read_card(tx_rx, &reader, &nfc_worker->dev_data->mf_classic_data) == 40) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool plantain_4k_parser_parse(NfcDeviceData* dev_data) {
+    MfClassicData* data = &dev_data->mf_classic_data;
+
+    // Verify key
+    MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 8);
+    uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6);
+    if(key != plantain_keys_4k[8].key_a) return false;
+
+    // Point to block 0 of sector 4, value 0
+    uint8_t* temp_ptr = &data->block[4 * 4].value[0];
+    // Read first 4 bytes of block 0 of sector 4 from last to first and convert them to uint32_t
+    // 38 18 00 00 becomes 00 00 18 38, and equals to 6200 decimal
+    uint32_t balance =
+        ((temp_ptr[3] << 24) | (temp_ptr[2] << 16) | (temp_ptr[1] << 8) | temp_ptr[0]) / 100;
+    // Read card number
+    // Point to block 0 of sector 0, value 0
+    temp_ptr = &data->block[0 * 4].value[0];
+    // Read first 7 bytes of block 0 of sector 0 from last to first and convert them to uint64_t
+    // 80 5C 23 8A 16 31 04 becomes 04 31 16 8A 23 5C 80, and equals to 36130104729284868 decimal
+    uint8_t card_number_arr[7];
+    for(size_t i = 0; i < 7; i++) {
+        card_number_arr[i] = temp_ptr[6 - i];
+    }
+    // Copy card number to uint64_t
+    uint64_t card_number = 0;
+    for(size_t i = 0; i < 7; i++) {
+        card_number = (card_number << 8) | card_number_arr[i];
+    }
+    // Convert card number to string
+    string_t card_number_str;
+    string_init(card_number_str);
+    // Should look like "361301047292848684"
+    // %llu doesn't work for some reason in sprintf, so we use string_push_uint64 instead
+    string_push_uint64(card_number, card_number_str);
+    // Add suffix with luhn checksum (1 digit) to the card number string
+    string_t card_number_suffix;
+    string_init(card_number_suffix);
+
+    // The number to calculate the checksum on doesn't fit into uint64_t, idk
+    //uint8_t luhn_checksum = plantain_calculate_luhn(card_number);
+
+    // // Convert luhn checksum to string
+    // string_t luhn_checksum_str;
+    // string_init(luhn_checksum_str);
+    // string_push_uint64(luhn_checksum, luhn_checksum_str);
+
+    string_cat_printf(card_number_suffix, "-");
+    // FURI_LOG_D("plant4k", "Card checksum: %d", luhn_checksum);
+    string_cat_printf(card_number_str, string_get_cstr(card_number_suffix));
+    // Free all not needed strings
+    string_clear(card_number_suffix);
+    // string_clear(luhn_checksum_str);
+
+    string_printf(
+        dev_data->parsed_data,
+        "\e#Plantain\nN:%s\nBalance:%d\n",
+        string_get_cstr(card_number_str),
+        balance);
+    string_clear(card_number_str);
+
+    return true;
+}

+ 9 - 0
lib/nfc/parsers/plantain_4k_parser.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "nfc_supported_card.h"
+
+bool plantain_4k_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool plantain_4k_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool plantain_4k_parser_parse(NfcDeviceData* dev_data);

+ 147 - 0
lib/nfc/parsers/plantain_parser.c

@@ -0,0 +1,147 @@
+#include "nfc_supported_card.h"
+
+#include <gui/modules/widget.h>
+#include <nfc_worker_i.h>
+
+#include "furi_hal.h"
+
+static const MfClassicAuthContext plantain_keys[] = {
+    {.sector = 0, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 1, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 2, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 3, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 4, .key_a = 0xe56ac127dd45, .key_b = 0x19fc84a3784b},
+    {.sector = 5, .key_a = 0x77dabc9825e1, .key_b = 0x9764fec3154a},
+    {.sector = 6, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 7, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 8, .key_a = 0x26973ea74321, .key_b = 0xd27058c6e2c7},
+    {.sector = 9, .key_a = 0xeb0a8ff88ade, .key_b = 0x578a9ada41e3},
+    {.sector = 10, .key_a = 0xea0fd73cb149, .key_b = 0x29c35fa068fb},
+    {.sector = 11, .key_a = 0xc76bf71a2509, .key_b = 0x9ba241db3f56},
+    {.sector = 12, .key_a = 0xacffffffffff, .key_b = 0x71f3a315ad26},
+    {.sector = 13, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 14, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 15, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+};
+
+bool plantain_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+    UNUSED(nfc_worker);
+    if(nfc_worker->dev_data->mf_classic_data.type != MfClassicType1k) {
+        return false;
+    }
+
+    uint8_t sector = 8;
+    uint8_t block = mf_classic_get_sector_trailer_block_num_by_sector(sector);
+    FURI_LOG_D("Plant", "Verifying sector %d", sector);
+    if(mf_classic_authenticate(tx_rx, block, 0x26973ea74321, MfClassicKeyA)) {
+        FURI_LOG_D("Plant", "Sector %d verified", sector);
+        return true;
+    }
+    return false;
+}
+
+bool plantain_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+
+    MfClassicReader reader = {};
+    FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data;
+    reader.type = mf_classic_get_classic_type(nfc_data->atqa[0], nfc_data->atqa[1], nfc_data->sak);
+    for(size_t i = 0; i < COUNT_OF(plantain_keys); i++) {
+        mf_classic_reader_add_sector(
+            &reader, plantain_keys[i].sector, plantain_keys[i].key_a, plantain_keys[i].key_b);
+    }
+
+    return mf_classic_read_card(tx_rx, &reader, &nfc_worker->dev_data->mf_classic_data) == 16;
+}
+
+void string_push_uint64(uint64_t input, string_t output) {
+    const uint8_t base = 10;
+
+    do {
+        char c = input % base;
+        input /= base;
+
+        if(c < 10)
+            c += '0';
+        else
+            c += 'A' - 10;
+        string_push_back(output, c);
+    } while(input);
+
+    // reverse string
+    for(uint8_t i = 0; i < string_size(output) / 2; i++) {
+        char c = string_get_char(output, i);
+        string_set_char(output, i, string_get_char(output, string_size(output) - i - 1));
+        string_set_char(output, string_size(output) - i - 1, c);
+    }
+}
+
+uint8_t plantain_calculate_luhn(uint64_t number) {
+    // No.
+    UNUSED(number);
+    return 0;
+}
+
+bool plantain_parser_parse(NfcDeviceData* dev_data) {
+    MfClassicData* data = &dev_data->mf_classic_data;
+
+    // Verify key
+    MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 8);
+    uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6);
+    if(key != plantain_keys[8].key_a) return false;
+
+    // Point to block 0 of sector 4, value 0
+    uint8_t* temp_ptr = &data->block[4 * 4].value[0];
+    // Read first 4 bytes of block 0 of sector 4 from last to first and convert them to uint32_t
+    // 38 18 00 00 becomes 00 00 18 38, and equals to 6200 decimal
+    uint32_t balance =
+        ((temp_ptr[3] << 24) | (temp_ptr[2] << 16) | (temp_ptr[1] << 8) | temp_ptr[0]) / 100;
+    // Read card number
+    // Point to block 0 of sector 0, value 0
+    temp_ptr = &data->block[0 * 4].value[0];
+    // Read first 7 bytes of block 0 of sector 0 from last to first and convert them to uint64_t
+    // 80 5C 23 8A 16 31 04 becomes 04 31 16 8A 23 5C 80, and equals to 36130104729284868 decimal
+    uint8_t card_number_arr[7];
+    for(size_t i = 0; i < 7; i++) {
+        card_number_arr[i] = temp_ptr[6 - i];
+    }
+    // Copy card number to uint64_t
+    uint64_t card_number = 0;
+    for(size_t i = 0; i < 7; i++) {
+        card_number = (card_number << 8) | card_number_arr[i];
+    }
+    // Convert card number to string
+    string_t card_number_str;
+    string_init(card_number_str);
+    // Should look like "361301047292848684"
+    // %llu doesn't work for some reason in sprintf, so we use string_push_uint64 instead
+    string_push_uint64(card_number, card_number_str);
+    // Add suffix with luhn checksum (1 digit) to the card number string
+    string_t card_number_suffix;
+    string_init(card_number_suffix);
+
+    // The number to calculate the checksum on doesn't fit into uint64_t, idk
+    //uint8_t luhn_checksum = plantain_calculate_luhn(card_number);
+
+    // // Convert luhn checksum to string
+    // string_t luhn_checksum_str;
+    // string_init(luhn_checksum_str);
+    // string_push_uint64(luhn_checksum, luhn_checksum_str);
+
+    string_cat_printf(card_number_suffix, "-");
+    // FURI_LOG_D("plant4k", "Card checksum: %d", luhn_checksum);
+    string_cat_printf(card_number_str, string_get_cstr(card_number_suffix));
+    // Free all not needed strings
+    string_clear(card_number_suffix);
+    // string_clear(luhn_checksum_str);
+
+    string_printf(
+        dev_data->parsed_data,
+        "\e#Plantain\nN:%s\nBalance:%d\n",
+        string_get_cstr(card_number_str),
+        balance);
+    string_clear(card_number_str);
+
+    return true;
+}

+ 13 - 0
lib/nfc/parsers/plantain_parser.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include "nfc_supported_card.h"
+
+bool plantain_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool plantain_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool plantain_parser_parse(NfcDeviceData* dev_data);
+
+void string_push_uint64(uint64_t input, string_t output);
+
+uint8_t plantain_calculate_luhn(uint64_t number);

+ 104 - 0
lib/nfc/parsers/troika_4k_parser.c

@@ -0,0 +1,104 @@
+#include "nfc_supported_card.h"
+
+#include <gui/modules/widget.h>
+#include <nfc_worker_i.h>
+
+static const MfClassicAuthContext troika_4k_keys[] = {
+    {.sector = 0, .key_a = 0xa0a1a2a3a4a5, .key_b = 0xfbf225dc5d58},
+    {.sector = 1, .key_a = 0xa82607b01c0d, .key_b = 0x2910989b6880},
+    {.sector = 2, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 3, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 4, .key_a = 0x73068f118c13, .key_b = 0x2b7f3253fac5},
+    {.sector = 5, .key_a = 0xFBC2793D540B, .key_b = 0xd3a297dc2698},
+    {.sector = 6, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 7, .key_a = 0xae3d65a3dad4, .key_b = 0x0f1c63013dbb},
+    {.sector = 8, .key_a = 0xa73f5dc1d333, .key_b = 0xe35173494a81},
+    {.sector = 9, .key_a = 0x69a32f1c2f19, .key_b = 0x6b8bd9860763},
+    {.sector = 10, .key_a = 0x9becdf3d9273, .key_b = 0xf8493407799d},
+    {.sector = 11, .key_a = 0x08b386463229, .key_b = 0x5efbaecef46b},
+    {.sector = 12, .key_a = 0xcd4c61c26e3d, .key_b = 0x31c7610de3b0},
+    {.sector = 13, .key_a = 0xa82607b01c0d, .key_b = 0x2910989b6880},
+    {.sector = 14, .key_a = 0x0e8f64340ba4, .key_b = 0x4acec1205d75},
+    {.sector = 15, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 16, .key_a = 0x6b02733bb6ec, .key_b = 0x7038cd25c408},
+    {.sector = 17, .key_a = 0x403d706ba880, .key_b = 0xb39d19a280df},
+    {.sector = 18, .key_a = 0xc11f4597efb5, .key_b = 0x70d901648cb9},
+    {.sector = 19, .key_a = 0x0db520c78c1c, .key_b = 0x73e5b9d9d3a4},
+    {.sector = 20, .key_a = 0x3ebce0925b2f, .key_b = 0x372cc880f216},
+    {.sector = 21, .key_a = 0x16a27af45407, .key_b = 0x9868925175ba},
+    {.sector = 22, .key_a = 0xaba208516740, .key_b = 0xce26ecb95252},
+    {.sector = 23, .key_a = 0xCD64E567ABCD, .key_b = 0x8f79c4fd8a01},
+    {.sector = 24, .key_a = 0x764cd061f1e6, .key_b = 0xa74332f74994},
+    {.sector = 25, .key_a = 0x1cc219e9fec1, .key_b = 0xb90de525ceb6},
+    {.sector = 26, .key_a = 0x2fe3cb83ea43, .key_b = 0xfba88f109b32},
+    {.sector = 27, .key_a = 0x07894ffec1d6, .key_b = 0xefcb0e689db3},
+    {.sector = 28, .key_a = 0x04c297b91308, .key_b = 0xc8454c154cb5},
+    {.sector = 29, .key_a = 0x7a38e3511a38, .key_b = 0xab16584c972a},
+    {.sector = 30, .key_a = 0x7545df809202, .key_b = 0xecf751084a80},
+    {.sector = 31, .key_a = 0x5125974cd391, .key_b = 0xd3eafb5df46d},
+    {.sector = 32, .key_a = 0x7a86aa203788, .key_b = 0xe41242278ca2},
+    {.sector = 33, .key_a = 0xafcef64c9913, .key_b = 0x9db96dca4324},
+    {.sector = 34, .key_a = 0x04eaa462f70b, .key_b = 0xac17b93e2fae},
+    {.sector = 35, .key_a = 0xe734c210f27e, .key_b = 0x29ba8c3e9fda},
+    {.sector = 36, .key_a = 0xd5524f591eed, .key_b = 0x5daf42861b4d},
+    {.sector = 37, .key_a = 0xe4821a377b75, .key_b = 0xe8709e486465},
+    {.sector = 38, .key_a = 0x518dc6eea089, .key_b = 0x97c64ac98ca4},
+    {.sector = 39, .key_a = 0xbb52f8cce07f, .key_b = 0x6b6119752c70},
+};
+
+bool troika_4k_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+
+    if(nfc_worker->dev_data->mf_classic_data.type != MfClassicType4k) {
+        return false;
+    }
+
+    uint8_t sector = 11;
+    uint8_t block = mf_classic_get_sector_trailer_block_num_by_sector(sector);
+    FURI_LOG_D("Troika", "Verifying sector %d", sector);
+    if(mf_classic_authenticate(tx_rx, block, 0x08b386463229, MfClassicKeyA)) {
+        FURI_LOG_D("Troika", "Sector %d verified", sector);
+        return true;
+    }
+    return false;
+}
+
+bool troika_4k_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+
+    MfClassicReader reader = {};
+    FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data;
+    reader.type = mf_classic_get_classic_type(nfc_data->atqa[0], nfc_data->atqa[1], nfc_data->sak);
+    for(size_t i = 0; i < COUNT_OF(troika_4k_keys); i++) {
+        mf_classic_reader_add_sector(
+            &reader, troika_4k_keys[i].sector, troika_4k_keys[i].key_a, troika_4k_keys[i].key_b);
+    }
+
+    return mf_classic_read_card(tx_rx, &reader, &nfc_worker->dev_data->mf_classic_data) == 40;
+}
+
+bool troika_4k_parser_parse(NfcDeviceData* dev_data) {
+    MfClassicData* data = &dev_data->mf_classic_data;
+
+    // Verify key
+    MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 4);
+    uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6);
+    if(key != troika_4k_keys[4].key_a) return false;
+
+    // Verify card type
+    if(data->type != MfClassicType4k) return false;
+
+    uint8_t* temp_ptr = &data->block[8 * 4 + 1].value[5];
+    uint16_t balance = ((temp_ptr[0] << 8) | temp_ptr[1]) / 25;
+    temp_ptr = &data->block[8 * 4].value[3];
+    uint32_t number = 0;
+    for(size_t i = 0; i < 4; i++) {
+        number <<= 8;
+        number |= temp_ptr[i];
+    }
+    number >>= 4;
+
+    string_printf(dev_data->parsed_data, "\e#Troika\nNum: %ld\nBalance: %d rur.", number, balance);
+
+    return true;
+}

+ 9 - 0
lib/nfc/parsers/troika_4k_parser.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "nfc_supported_card.h"
+
+bool troika_4k_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool troika_4k_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool troika_4k_parser_parse(NfcDeviceData* dev_data);

+ 25 - 17
lib/nfc/parsers/troyka_parser.c → lib/nfc/parsers/troika_parser.c

@@ -3,7 +3,7 @@
 #include <gui/modules/widget.h>
 #include <nfc_worker_i.h>
 
-static const MfClassicAuthContext troyka_keys[] = {
+static const MfClassicAuthContext troika_keys[] = {
     {.sector = 0, .key_a = 0xa0a1a2a3a4a5, .key_b = 0xfbf225dc5d58},
     {.sector = 1, .key_a = 0xa82607b01c0d, .key_b = 0x2910989b6880},
     {.sector = 2, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
@@ -22,42 +22,50 @@ static const MfClassicAuthContext troyka_keys[] = {
     {.sector = 15, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
 };
 
-bool troyka_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+bool troika_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
     furi_assert(nfc_worker);
     UNUSED(nfc_worker);
+    if(nfc_worker->dev_data->mf_classic_data.type != MfClassicType1k) {
+        return false;
+    }
 
-    MfClassicAuthContext auth_ctx = {
-        .key_a = MF_CLASSIC_NO_KEY,
-        .key_b = MF_CLASSIC_NO_KEY,
-        .sector = 8,
-    };
-    return mf_classic_auth_attempt(tx_rx, &auth_ctx, 0xa73f5dc1d333);
+    uint8_t sector = 11;
+    uint8_t block = mf_classic_get_sector_trailer_block_num_by_sector(sector);
+    FURI_LOG_D("Troika", "Verifying sector %d", sector);
+    if(mf_classic_authenticate(tx_rx, block, 0x08b386463229, MfClassicKeyA)) {
+        FURI_LOG_D("Troika", "Sector %d verified", sector);
+        return true;
+    }
+    return false;
 }
 
-bool troyka_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+bool troika_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
     furi_assert(nfc_worker);
 
     MfClassicReader reader = {};
     FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data;
     reader.type = mf_classic_get_classic_type(nfc_data->atqa[0], nfc_data->atqa[1], nfc_data->sak);
 
-    for(size_t i = 0; i < COUNT_OF(troyka_keys); i++) {
+    for(size_t i = 0; i < COUNT_OF(troika_keys); i++) {
         mf_classic_reader_add_sector(
-            &reader, troyka_keys[i].sector, troyka_keys[i].key_a, troyka_keys[i].key_b);
+            &reader, troika_keys[i].sector, troika_keys[i].key_a, troika_keys[i].key_b);
     }
 
     return mf_classic_read_card(tx_rx, &reader, &nfc_worker->dev_data->mf_classic_data) == 16;
 }
 
-bool troyka_parser_parse(NfcDeviceData* dev_data) {
+bool troika_parser_parse(NfcDeviceData* dev_data) {
     MfClassicData* data = &dev_data->mf_classic_data;
-    bool troyka_parsed = false;
+    bool troika_parsed = false;
 
     do {
         // Verify key
         MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 8);
         uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6);
-        if(key != troyka_keys[8].key_a) break;
+        if(key != troika_keys[8].key_a) break;
+
+        // Verify card type
+        if(data->type != MfClassicType1k) break;
 
         // Parse data
         uint8_t* temp_ptr = &data->block[8 * 4 + 1].value[5];
@@ -71,9 +79,9 @@ bool troyka_parser_parse(NfcDeviceData* dev_data) {
         number >>= 4;
 
         string_printf(
-            dev_data->parsed_data, "\e#Troyka\nNum: %ld\nBalance: %d rur.", number, balance);
-        troyka_parsed = true;
+            dev_data->parsed_data, "\e#Troika\nNum: %ld\nBalance: %d rur.", number, balance);
+        troika_parsed = true;
     } while(false);
 
-    return troyka_parsed;
+    return troika_parsed;
 }

+ 9 - 0
lib/nfc/parsers/troika_parser.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "nfc_supported_card.h"
+
+bool troika_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool troika_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool troika_parser_parse(NfcDeviceData* dev_data);

+ 0 - 9
lib/nfc/parsers/troyka_parser.h

@@ -1,9 +0,0 @@
-#pragma once
-
-#include "nfc_supported_card.h"
-
-bool troyka_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
-
-bool troyka_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
-
-bool troyka_parser_parse(NfcDeviceData* dev_data);

+ 171 - 0
lib/nfc/parsers/two_cities.c

@@ -0,0 +1,171 @@
+#include "nfc_supported_card.h"
+#include "plantain_parser.h" // For plantain-specific stuff
+
+#include <gui/modules/widget.h>
+#include <nfc_worker_i.h>
+
+#include "furi_hal.h"
+
+static const MfClassicAuthContext two_cities_keys_4k[] = {
+    {.sector = 0, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 1, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 2, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 3, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 4, .key_a = 0xe56ac127dd45, .key_b = 0x19fc84a3784b},
+    {.sector = 5, .key_a = 0x77dabc9825e1, .key_b = 0x9764fec3154a},
+    {.sector = 6, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 7, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 8, .key_a = 0xa73f5dc1d333, .key_b = 0xe35173494a81},
+    {.sector = 9, .key_a = 0x69a32f1c2f19, .key_b = 0x6b8bd9860763},
+    {.sector = 10, .key_a = 0xea0fd73cb149, .key_b = 0x29c35fa068fb},
+    {.sector = 11, .key_a = 0xc76bf71a2509, .key_b = 0x9ba241db3f56},
+    {.sector = 12, .key_a = 0xacffffffffff, .key_b = 0x71f3a315ad26},
+    {.sector = 13, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 14, .key_a = 0xffffffffffff, .key_b = 0xffffffffffff},
+    {.sector = 15, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 16, .key_a = 0x72f96bdd3714, .key_b = 0x462225cd34cf},
+    {.sector = 17, .key_a = 0x044ce1872bc3, .key_b = 0x8c90c70cff4a},
+    {.sector = 18, .key_a = 0xbc2d1791dec1, .key_b = 0xca96a487de0b},
+    {.sector = 19, .key_a = 0x8791b2ccb5c4, .key_b = 0xc956c3b80da3},
+    {.sector = 20, .key_a = 0x8e26e45e7d65, .key_b = 0x8e65b3af7d22},
+    {.sector = 21, .key_a = 0x0f318130ed18, .key_b = 0x0c420a20e056},
+    {.sector = 22, .key_a = 0x045ceca15535, .key_b = 0x31bec3d9e510},
+    {.sector = 23, .key_a = 0x9d993c5d4ef4, .key_b = 0x86120e488abf},
+    {.sector = 24, .key_a = 0xc65d4eaa645b, .key_b = 0xb69d40d1a439},
+    {.sector = 25, .key_a = 0x3a8a139c20b4, .key_b = 0x8818a9c5d406},
+    {.sector = 26, .key_a = 0xbaff3053b496, .key_b = 0x4b7cb25354d3},
+    {.sector = 27, .key_a = 0x7413b599c4ea, .key_b = 0xb0a2AAF3A1BA},
+    {.sector = 28, .key_a = 0x0ce7cd2cc72b, .key_b = 0xfa1fbb3f0f1f},
+    {.sector = 29, .key_a = 0x0be5fac8b06a, .key_b = 0x6f95887a4fd3},
+    {.sector = 30, .key_a = 0x26973ea74321, .key_b = 0xd27058c6e2c7},
+    {.sector = 31, .key_a = 0xeb0a8ff88ade, .key_b = 0x578a9ada41e3},
+    {.sector = 32, .key_a = 0x7a396f0d633d, .key_b = 0xad2bdc097023},
+    {.sector = 33, .key_a = 0xa3faa6daff67, .key_b = 0x7600e889adf9},
+    {.sector = 34, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 35, .key_a = 0x2aa05ed1856f, .key_b = 0xeaac88e5dc99},
+    {.sector = 36, .key_a = 0xa7141147d430, .key_b = 0xff16014fefc7},
+    {.sector = 37, .key_a = 0x8a8d88151a00, .key_b = 0x038b5f9b5a2a},
+    {.sector = 38, .key_a = 0xb27addfb64b0, .key_b = 0x152fd0c420a7},
+    {.sector = 39, .key_a = 0x7259fa0197c6, .key_b = 0x5583698df085},
+};
+
+bool two_cities_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+    UNUSED(nfc_worker);
+
+    if(nfc_worker->dev_data->mf_classic_data.type != MfClassicType4k) {
+        return false;
+    }
+
+    uint8_t sector = 4;
+    uint8_t block = mf_classic_get_sector_trailer_block_num_by_sector(sector);
+    FURI_LOG_D("2cities", "Verifying sector %d", sector);
+    if(mf_classic_authenticate(tx_rx, block, 0xe56ac127dd45, MfClassicKeyA)) {
+        FURI_LOG_D("2cities", "Sector %d verified", sector);
+        return true;
+    }
+    return false;
+}
+
+bool two_cities_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
+    furi_assert(nfc_worker);
+
+    MfClassicReader reader = {};
+    FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data;
+    reader.type = mf_classic_get_classic_type(nfc_data->atqa[0], nfc_data->atqa[1], nfc_data->sak);
+    for(size_t i = 0; i < COUNT_OF(two_cities_keys_4k); i++) {
+        mf_classic_reader_add_sector(
+            &reader,
+            two_cities_keys_4k[i].sector,
+            two_cities_keys_4k[i].key_a,
+            two_cities_keys_4k[i].key_b);
+        FURI_LOG_T("2cities", "Added sector %d", two_cities_keys_4k[i].sector);
+    }
+
+    return mf_classic_read_card(tx_rx, &reader, &nfc_worker->dev_data->mf_classic_data) == 40;
+}
+
+bool two_cities_parser_parse(NfcDeviceData* dev_data) {
+    MfClassicData* data = &dev_data->mf_classic_data;
+
+    // Verify key
+    MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 4);
+    uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6);
+    if(key != two_cities_keys_4k[4].key_a) return false;
+
+    // =====
+    // PLANTAIN
+    // =====
+
+    // Point to block 0 of sector 4, value 0
+    uint8_t* temp_ptr = &data->block[4 * 4].value[0];
+    // Read first 4 bytes of block 0 of sector 4 from last to first and convert them to uint32_t
+    // 38 18 00 00 becomes 00 00 18 38, and equals to 6200 decimal
+    uint32_t balance =
+        ((temp_ptr[3] << 24) | (temp_ptr[2] << 16) | (temp_ptr[1] << 8) | temp_ptr[0]) / 100;
+    // Read card number
+    // Point to block 0 of sector 0, value 0
+    temp_ptr = &data->block[0 * 4].value[0];
+    // Read first 7 bytes of block 0 of sector 0 from last to first and convert them to uint64_t
+    // 80 5C 23 8A 16 31 04 becomes 04 31 16 8A 23 5C 80, and equals to 36130104729284868 decimal
+    uint8_t card_number_arr[7];
+    for(size_t i = 0; i < 7; i++) {
+        card_number_arr[i] = temp_ptr[6 - i];
+    }
+    // Copy card number to uint64_t
+    uint64_t card_number = 0;
+    for(size_t i = 0; i < 7; i++) {
+        card_number = (card_number << 8) | card_number_arr[i];
+    }
+    // Convert card number to string
+    string_t card_number_str;
+    string_init(card_number_str);
+    // Should look like "361301047292848684"
+    // %llu doesn't work for some reason in sprintf, so we use string_push_uint64 instead
+    string_push_uint64(card_number, card_number_str);
+    // Add suffix with luhn checksum (1 digit) to the card number string
+    string_t card_number_suffix;
+    string_init(card_number_suffix);
+
+    // The number to calculate the checksum on doesn't fit into uint64_t, idk
+    //uint8_t luhn_checksum = two_cities_calculate_luhn(card_number);
+
+    // // Convert luhn checksum to string
+    // string_t luhn_checksum_str;
+    // string_init(luhn_checksum_str);
+    // string_push_uint64(luhn_checksum, luhn_checksum_str);
+
+    string_cat_printf(card_number_suffix, "-");
+    // FURI_LOG_D("plant4k", "Card checksum: %d", luhn_checksum);
+    string_cat_printf(card_number_str, string_get_cstr(card_number_suffix));
+    // Free all not needed strings
+    string_clear(card_number_suffix);
+    // string_clear(luhn_checksum_str);
+
+    // =====
+    // --PLANTAIN--
+    // =====
+    // TROIKA
+    // =====
+
+    uint8_t* troika_temp_ptr = &data->block[8 * 4 + 1].value[5];
+    uint16_t troika_balance = ((troika_temp_ptr[0] << 8) | troika_temp_ptr[1]) / 25;
+    troika_temp_ptr = &data->block[8 * 4].value[3];
+    uint32_t troika_number = 0;
+    for(size_t i = 0; i < 4; i++) {
+        troika_number <<= 8;
+        troika_number |= troika_temp_ptr[i];
+    }
+    troika_number >>= 4;
+
+    string_printf(
+        dev_data->parsed_data,
+        "\e#Troika+Plantain\nPN: %s\nPB: %d rur.\nTN: %d\nTB: %d rur.\n",
+        string_get_cstr(card_number_str),
+        balance,
+        troika_number,
+        troika_balance);
+    string_clear(card_number_str);
+
+    return true;
+}

+ 9 - 0
lib/nfc/parsers/two_cities.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "nfc_supported_card.h"
+
+bool two_cities_parser_verify(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool two_cities_parser_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx);
+
+bool two_cities_parser_parse(NfcDeviceData* dev_data);