Prechádzať zdrojové kódy

smartrider, and key cache updates

Luu 1 rok pred
rodič
commit
c5d04a6eee

+ 212 - 0
api/nfc/mf_classic_key_cache.c

@@ -0,0 +1,212 @@
+#include "mf_classic_key_cache.h"
+
+#include <furi/furi.h>
+#include <storage/storage.h>
+
+#define NFC_APP_KEYS_EXTENSION   ".keys"
+#define NFC_APP_KEY_CACHE_FOLDER "/ext/nfc/.cache"
+
+static const char* mf_classic_key_cache_file_header = "Flipper NFC keys";
+static const uint32_t mf_classic_key_cache_file_version = 1;
+
+struct MfClassicKeyCache {
+    MfClassicDeviceKeys keys;
+    MfClassicKeyType current_key_type;
+    uint8_t current_sector;
+};
+
+static void nfc_get_key_cache_file_path(const uint8_t* uid, size_t uid_len, FuriString* path) {
+    furi_string_printf(path, "%s/", NFC_APP_KEY_CACHE_FOLDER);
+    for(size_t i = 0; i < uid_len; i++) {
+        furi_string_cat_printf(path, "%02X", uid[i]);
+    }
+    furi_string_cat_printf(path, "%s", NFC_APP_KEYS_EXTENSION);
+}
+
+MfClassicKeyCache* mf_classic_key_cache_alloc(void) {
+    MfClassicKeyCache* instance = malloc(sizeof(MfClassicKeyCache));
+
+    return instance;
+}
+
+void mf_classic_key_cache_free(MfClassicKeyCache* instance) {
+    furi_assert(instance);
+
+    free(instance);
+}
+
+bool mf_classic_key_cache_save(MfClassicKeyCache* instance, const MfClassicData* data) {
+    UNUSED(instance);
+    furi_assert(data);
+
+    size_t uid_len = 0;
+    const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
+    FuriString* file_path = furi_string_alloc();
+    nfc_get_key_cache_file_path(uid, uid_len, file_path);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+
+    FuriString* temp_str = furi_string_alloc();
+    bool save_success = false;
+    do {
+        if(!storage_simply_mkdir(storage, NFC_APP_KEY_CACHE_FOLDER)) break;
+        if(!storage_simply_remove(storage, furi_string_get_cstr(file_path))) break;
+        if(!flipper_format_buffered_file_open_always(ff, furi_string_get_cstr(file_path))) break;
+
+        if(!flipper_format_write_header_cstr(
+               ff, mf_classic_key_cache_file_header, mf_classic_key_cache_file_version))
+            break;
+        if(!flipper_format_write_string_cstr(
+               ff, "Mifare Classic type", mf_classic_get_device_name(data, NfcDeviceNameTypeShort)))
+            break;
+        if(!flipper_format_write_hex_uint64(ff, "Key A map", &data->key_a_mask, 1)) break;
+        if(!flipper_format_write_hex_uint64(ff, "Key B map", &data->key_b_mask, 1)) break;
+
+        uint8_t sector_num = mf_classic_get_total_sectors_num(data->type);
+        bool key_save_success = true;
+        for(size_t i = 0; (i < sector_num) && (key_save_success); i++) {
+            MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, i);
+            if(FURI_BIT(data->key_a_mask, i)) {
+                furi_string_printf(temp_str, "Key A sector %d", i);
+                key_save_success = flipper_format_write_hex(
+                    ff, furi_string_get_cstr(temp_str), sec_tr->key_a.data, sizeof(MfClassicKey));
+            }
+            if(!key_save_success) break;
+            if(FURI_BIT(data->key_b_mask, i)) {
+                furi_string_printf(temp_str, "Key B sector %d", i);
+                key_save_success = flipper_format_write_hex(
+                    ff, furi_string_get_cstr(temp_str), sec_tr->key_b.data, sizeof(MfClassicKey));
+            }
+        }
+        save_success = key_save_success;
+    } while(false);
+
+    flipper_format_free(ff);
+    furi_string_free(temp_str);
+    furi_string_free(file_path);
+    furi_record_close(RECORD_STORAGE);
+
+    return save_success;
+}
+
+bool mf_classic_key_cache_load(MfClassicKeyCache* instance, const uint8_t* uid, size_t uid_len) {
+    furi_assert(instance);
+    furi_assert(uid);
+
+    mf_classic_key_cache_reset(instance);
+
+    FuriString* file_path = furi_string_alloc();
+    nfc_get_key_cache_file_path(uid, uid_len, file_path);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+
+    FuriString* temp_str = furi_string_alloc();
+    bool load_success = false;
+    do {
+        if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(file_path))) break;
+
+        uint32_t version = 0;
+        if(!flipper_format_read_header(ff, temp_str, &version)) break;
+        if(furi_string_cmp_str(temp_str, mf_classic_key_cache_file_header)) break;
+        if(version != mf_classic_key_cache_file_version) break;
+
+        if(!flipper_format_read_hex_uint64(ff, "Key A map", &instance->keys.key_a_mask, 1)) break;
+        if(!flipper_format_read_hex_uint64(ff, "Key B map", &instance->keys.key_b_mask, 1)) break;
+
+        bool key_read_success = true;
+        for(size_t i = 0; (i < MF_CLASSIC_TOTAL_SECTORS_MAX) && (key_read_success); i++) {
+            if(FURI_BIT(instance->keys.key_a_mask, i)) {
+                furi_string_printf(temp_str, "Key A sector %d", i);
+                key_read_success = flipper_format_read_hex(
+                    ff,
+                    furi_string_get_cstr(temp_str),
+                    instance->keys.key_a[i].data,
+                    sizeof(MfClassicKey));
+            }
+            if(!key_read_success) break;
+            if(FURI_BIT(instance->keys.key_b_mask, i)) {
+                furi_string_printf(temp_str, "Key B sector %d", i);
+                key_read_success = flipper_format_read_hex(
+                    ff,
+                    furi_string_get_cstr(temp_str),
+                    instance->keys.key_b[i].data,
+                    sizeof(MfClassicKey));
+            }
+        }
+        load_success = key_read_success;
+    } while(false);
+
+    flipper_format_buffered_file_close(ff);
+    flipper_format_free(ff);
+    furi_string_free(temp_str);
+    furi_string_free(file_path);
+    furi_record_close(RECORD_STORAGE);
+
+    return load_success;
+}
+
+void mf_classic_key_cache_load_from_data(MfClassicKeyCache* instance, const MfClassicData* data) {
+    furi_assert(instance);
+    furi_assert(data);
+
+    mf_classic_key_cache_reset(instance);
+    instance->keys.key_a_mask = data->key_a_mask;
+    instance->keys.key_b_mask = data->key_b_mask;
+    for(size_t i = 0; i < MF_CLASSIC_TOTAL_SECTORS_MAX; i++) {
+        MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, i);
+
+        if(FURI_BIT(data->key_a_mask, i)) {
+            instance->keys.key_a[i] = sec_tr->key_a;
+        }
+        if(FURI_BIT(data->key_b_mask, i)) {
+            instance->keys.key_b[i] = sec_tr->key_b;
+        }
+    }
+}
+
+bool mf_classic_key_cache_get_next_key(
+    MfClassicKeyCache* instance,
+    uint8_t* sector_num,
+    MfClassicKey* key,
+    MfClassicKeyType* key_type) {
+    furi_assert(instance);
+    furi_assert(sector_num);
+    furi_assert(key);
+    furi_assert(key_type);
+
+    bool next_key_found = false;
+    for(uint8_t i = instance->current_sector; i < MF_CLASSIC_TOTAL_SECTORS_MAX; i++) {
+        if(FURI_BIT(instance->keys.key_a_mask, i)) {
+            FURI_BIT_CLEAR(instance->keys.key_a_mask, i);
+            *key = instance->keys.key_a[i];
+            *key_type = MfClassicKeyTypeA;
+            *sector_num = i;
+
+            next_key_found = true;
+            break;
+        }
+        if(FURI_BIT(instance->keys.key_b_mask, i)) {
+            FURI_BIT_CLEAR(instance->keys.key_b_mask, i);
+            *key = instance->keys.key_b[i];
+            *key_type = MfClassicKeyTypeB;
+            *sector_num = i;
+
+            next_key_found = true;
+            instance->current_sector = i;
+            break;
+        }
+    }
+
+    return next_key_found;
+}
+
+void mf_classic_key_cache_reset(MfClassicKeyCache* instance) {
+    furi_assert(instance);
+
+    instance->current_key_type = MfClassicKeyTypeA;
+    instance->current_sector = 0;
+    instance->keys.key_a_mask = 0;
+    instance->keys.key_b_mask = 0;
+}

+ 31 - 0
api/nfc/mf_classic_key_cache.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <nfc/protocols/mf_classic/mf_classic.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct MfClassicKeyCache MfClassicKeyCache;
+
+MfClassicKeyCache* mf_classic_key_cache_alloc(void);
+
+void mf_classic_key_cache_free(MfClassicKeyCache* instance);
+
+bool mf_classic_key_cache_load(MfClassicKeyCache* instance, const uint8_t* uid, size_t uid_len);
+
+void mf_classic_key_cache_load_from_data(MfClassicKeyCache* instance, const MfClassicData* data);
+
+bool mf_classic_key_cache_get_next_key(
+    MfClassicKeyCache* instance,
+    uint8_t* sector_num,
+    MfClassicKey* key,
+    MfClassicKeyType* key_type);
+
+bool mf_classic_key_cache_save(MfClassicKeyCache* instance, const MfClassicData* data);
+
+void mf_classic_key_cache_reset(MfClassicKeyCache* instance);
+
+#ifdef __cplusplus
+}
+#endif

+ 87 - 0
metroflip.c

@@ -21,6 +21,9 @@ Metroflip* metroflip_alloc() {
     app->nfc = nfc_alloc();
     app->nfc_device = nfc_device_alloc();
 
+    // key cache
+    app->mfc_key_cache = mf_classic_key_cache_alloc();
+
     // notifs
     app->notifications = furi_record_open(RECORD_NOTIFICATION);
     // View Dispatcher and Scene Manager
@@ -74,6 +77,9 @@ void metroflip_free(Metroflip* app) {
     nfc_free(app->nfc);
     nfc_device_free(app->nfc_device);
 
+    // key cache
+    mf_classic_key_cache_free(app->mfc_key_cache);
+
     //notifs
     furi_record_close(RECORD_NOTIFICATION);
     app->notifications = NULL;
@@ -209,3 +215,84 @@ void dec_to_bits(char dec_representation, char* bit_representation) {
         bit_representation[i] = (decimal & (1 << i)) ? '1' : '0';
     }
 }
+
+KeyfileManager manage_keyfiles(char uid_str[]) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    File* source = storage_file_alloc(storage);
+    char source_path[64];
+
+    FURI_LOG_I("TAG", "%s", uid_str);
+    size_t source_required_size =
+        strlen("/ext/nfc/.cache/") + strlen(uid_str) + strlen(".keys") + 1;
+    snprintf(source_path, source_required_size, "/ext/nfc/.cache/%s.keys", uid_str);
+    bool cache_file = storage_file_open(source, source_path, FSAM_READ, FSOM_OPEN_EXISTING);
+    /*-----------------Open assets cache file (if exists)------------*/
+
+    File* dest = storage_file_alloc(storage);
+    char dest_path[64];
+    size_t dest_required_size =
+        strlen("/ext/nfc/assets/.") + strlen(uid_str) + strlen(".keys") + 1;
+    snprintf(dest_path, dest_required_size, "/ext/nfc/assets/.%s.keys", uid_str);
+    bool dest_cache_file = storage_file_open(dest, dest_path, FSAM_READ, FSOM_OPEN_EXISTING);
+
+    /*-----------------Check cache file------------*/
+    if(!cache_file) {
+        /*-----------------Check assets cache file------------*/
+        FURI_LOG_I("TAG", "cache dont exist, checking assets");
+
+        if(!dest_cache_file) {
+            FURI_LOG_I("TAG", "assets dont exist, prompting user to fix..");
+            storage_file_close(source);
+            storage_file_close(dest);
+            return MISSING_KEYFILE;
+        } else {
+            size_t dest_file_length = storage_file_size(dest);
+
+            FURI_LOG_I(
+                "TAG", "assets exist, but cache doesnt, proceeding to copy assets to cache");
+            // Close, then open both files
+            storage_file_close(source);
+            storage_file_close(dest);
+            storage_file_open(
+                source, source_path, FSAM_WRITE, FSOM_OPEN_ALWAYS); // create new file
+            storage_file_open(
+                dest, dest_path, FSAM_READ, FSOM_OPEN_EXISTING); // open existing assets keyfile
+            FURI_LOG_I("TAG", "creating cache file at %s from %s", source_path, dest_path);
+            /*-----Clone keyfile from assets to cache (creates temporary buffer)----*/
+            uint8_t* cloned_buffer = malloc(dest_file_length);
+            storage_file_read(dest, cloned_buffer, dest_file_length);
+            storage_file_write(source, cloned_buffer, dest_file_length);
+            free(cloned_buffer);
+            storage_file_close(source);
+            storage_file_close(dest);
+            return SUCCESSFUL;
+        }
+    } else {
+        size_t source_file_length = storage_file_size(source);
+        if(source_file_length > 1216) {
+            FURI_LOG_I("TAG", "cache exist, creating assets cache if not already exists");
+            storage_file_close(dest);
+            storage_file_close(source);
+            storage_file_open(dest, dest_path, FSAM_WRITE, FSOM_OPEN_ALWAYS);
+            storage_file_open(source, source_path, FSAM_READ, FSOM_OPEN_EXISTING);
+            FURI_LOG_I("TAG", "creating assets cache");
+            /*-----Clone keyfile from assets to cache (creates temporary buffer)----*/
+            uint8_t* cloned_buffer = malloc(source_file_length);
+            storage_file_read(source, cloned_buffer, source_file_length);
+            storage_file_write(dest, cloned_buffer, source_file_length);
+            free(cloned_buffer);
+            storage_file_close(source);
+            storage_file_close(dest);
+            return SUCCESSFUL;
+
+        } else {
+            FURI_LOG_I("TAG", "incomplete cache file, aborting.");
+            storage_file_close(source);
+            storage_file_close(dest);
+            return INCOMPLETE_KEYFILE;
+        }
+    }
+    FURI_LOG_I("TAG", "proceeding to read");
+    storage_file_close(source);
+    storage_file_close(dest);
+}

+ 10 - 0
metroflip_i.h

@@ -8,6 +8,7 @@
 #include <gui/modules/validators.h>
 #include <gui/view_dispatcher.h>
 #include <gui/scene_manager.h>
+#include "api/nfc/mf_classic_key_cache.h"
 #if __has_include(<assets_icons.h>)
 #include <assets_icons.h>
 #else
@@ -63,6 +64,7 @@ typedef struct {
     NfcPoller* poller;
     NfcScanner* scanner;
     NfcDevice* nfc_device;
+    MfClassicKeyCache* mfc_key_cache;
 
     // card details:
     uint32_t balance_lari;
@@ -117,6 +119,14 @@ typedef enum {
     MetroflipViewUart,
 } MetroflipView;
 
+typedef enum {
+    SUCCESSFUL,
+    INCOMPLETE_KEYFILE,
+    MISSING_KEYFILE
+} KeyfileManager;
+
+KeyfileManager manage_keyfiles(char uid_str[]);
+
 void metroflip_app_blink_start(Metroflip* metroflip);
 void metroflip_app_blink_stop(Metroflip* metroflip);
 

+ 1 - 0
scenes/metroflip_scene_config.h

@@ -4,6 +4,7 @@ ADD_SCENE(metroflip, navigo, Navigo)
 ADD_SCENE(metroflip, charliecard, CharlieCard)
 ADD_SCENE(metroflip, clipper, Clipper)
 ADD_SCENE(metroflip, metromoney, Metromoney)
+ADD_SCENE(metroflip, smartrider, Smartrider)
 ADD_SCENE(metroflip, read_success, ReadSuccess)
 ADD_SCENE(metroflip, bip, Bip)
 ADD_SCENE(metroflip, myki, Myki)

+ 444 - 0
scenes/metroflip_scene_smartrider.c

@@ -0,0 +1,444 @@
+#include "../metroflip_i.h"
+
+#include <bit_lib.h>
+#include <flipper_application.h>
+#include <furi.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller.h>
+#include <string.h>
+#include <dolphin/dolphin.h>
+#include <furi_hal.h>
+#include <nfc/nfc.h>
+#include <nfc/nfc_device.h>
+#include <nfc/nfc_listener.h>
+#include <storage/storage.h>
+
+#define MAX_TRIPS           10
+#define TAG                 "Metroflip:Scene:Smartrider"
+#define MAX_BLOCKS          64
+#define MAX_DATE_ITERATIONS 366
+
+uint8_t sector_num = 0;
+
+static const uint8_t STANDARD_KEYS[3][6] = {
+    {0x20, 0x31, 0xD1, 0xE5, 0x7A, 0x3B},
+    {0x4C, 0xA6, 0x02, 0x9F, 0x94, 0x73},
+    {0x19, 0x19, 0x53, 0x98, 0xE3, 0x2F}};
+
+void uid_to_string(const uint8_t* uid, size_t uid_len, char* uid_str, size_t max_len) {
+    size_t pos = 0;
+
+    for(size_t i = 0; i < uid_len && pos + 2 < max_len; ++i) {
+        pos += snprintf(&uid_str[pos], max_len - pos, "%02X", uid[i]);
+    }
+
+    uid_str[pos] = '\0'; // Null-terminate the string
+}
+
+typedef struct {
+    uint32_t timestamp;
+    uint16_t cost;
+    uint16_t transaction_number;
+    uint16_t journey_number;
+    char route[5];
+    uint8_t tap_on : 1;
+    uint8_t block;
+} __attribute__((packed)) TripData;
+
+typedef struct {
+    uint32_t balance;
+    uint16_t issued_days;
+    uint16_t expiry_days;
+    uint16_t purchase_cost;
+    uint16_t auto_load_threshold;
+    uint16_t auto_load_value;
+    char card_serial_number[11];
+    uint8_t token;
+    TripData trips[MAX_TRIPS];
+    uint8_t trip_count;
+} __attribute__((packed)) SmartRiderData;
+
+static const char* const CONCESSION_TYPES[] = {
+    "Pre-issue",
+    "Standard Fare",
+    "Student",
+    NULL,
+    "Tertiary",
+    NULL,
+    "Seniors",
+    "Health Care",
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    "PTA Staff",
+    "Pensioner",
+    "Free Travel"};
+
+static inline const char* get_concession_type(uint8_t token) {
+    return (token <= 0x10) ? CONCESSION_TYPES[token] : "Unknown";
+}
+
+static inline bool
+    parse_trip_data(const MfClassicBlock* block_data, TripData* trip, uint8_t block_number) {
+    trip->timestamp = bit_lib_bytes_to_num_le(block_data->data + 3, 4);
+    trip->tap_on = (block_data->data[7] & 0x10) == 0x10;
+    memcpy(trip->route, block_data->data + 8, 4);
+    trip->route[4] = '\0';
+    trip->cost = bit_lib_bytes_to_num_le(block_data->data + 13, 2);
+    trip->transaction_number = bit_lib_bytes_to_num_le(block_data->data, 2);
+    trip->journey_number = bit_lib_bytes_to_num_le(block_data->data + 2, 2);
+    trip->block = block_number;
+    return true;
+}
+
+static bool is_leap_year(uint16_t year) {
+    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
+}
+
+static void calculate_date(uint32_t timestamp, char* date_str, size_t date_str_size) {
+    uint32_t seconds_since_2000 = timestamp;
+    uint32_t days_since_2000 = seconds_since_2000 / 86400;
+    uint16_t year = 2000;
+    uint8_t month = 1;
+    uint16_t day = 1;
+
+    static const uint16_t days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+    while(days_since_2000 >= (is_leap_year(year) ? 366 : 365)) {
+        days_since_2000 -= (is_leap_year(year) ? 366 : 365);
+        year++;
+    }
+
+    for(month = 0; month < 12; month++) {
+        uint16_t dim = days_in_month[month];
+        if(month == 1 && is_leap_year(year)) {
+            dim++;
+        }
+        if(days_since_2000 < dim) {
+            break;
+        }
+        days_since_2000 -= dim;
+    }
+
+    day = days_since_2000 + 1;
+    month++; // Adjust month to 1-based
+
+    if(date_str_size > 0) {
+        size_t written = 0;
+        written += snprintf(date_str + written, date_str_size - written, "%02u", day);
+        if(written < date_str_size - 1) {
+            written += snprintf(date_str + written, date_str_size - written, "/");
+        }
+        if(written < date_str_size - 1) {
+            written += snprintf(date_str + written, date_str_size - written, "%02u", month);
+        }
+        if(written < date_str_size - 1) {
+            written += snprintf(date_str + written, date_str_size - written, "/");
+        }
+        if(written < date_str_size - 1) {
+            snprintf(date_str + written, date_str_size - written, "%02u", year % 100);
+        }
+    } else {
+        // If the buffer size is 0, do nothing
+    }
+}
+
+static bool smartrider_parse(const NfcDevice* device, FuriString* parsed_data) {
+    furi_assert(device);
+    furi_assert(parsed_data);
+    const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
+    SmartRiderData sr_data = {0};
+
+    if(data->type != MfClassicType1k) {
+        FURI_LOG_E(TAG, "Invalid card type");
+        return false;
+    }
+
+    const MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 0);
+    if(!sec_tr || memcmp(sec_tr->key_a.data, STANDARD_KEYS[0], 6) != 0) {
+        FURI_LOG_E(TAG, "Key verification failed for sector 0");
+        return false;
+    }
+
+    static const uint8_t required_blocks[] = {14, 4, 5, 1, 52, 50, 0};
+    for(size_t i = 0; i < COUNT_OF(required_blocks); i++) {
+        if(required_blocks[i] >= MAX_BLOCKS ||
+           !mf_classic_is_block_read(data, required_blocks[i])) {
+            FURI_LOG_E(TAG, "Required block %d is not read or out of range", required_blocks[i]);
+            return false;
+        }
+    }
+
+    sr_data.balance = bit_lib_bytes_to_num_le(data->block[14].data + 7, 2);
+    sr_data.issued_days = bit_lib_bytes_to_num_le(data->block[4].data + 16, 2);
+    sr_data.expiry_days = bit_lib_bytes_to_num_le(data->block[4].data + 18, 2);
+    sr_data.auto_load_threshold = bit_lib_bytes_to_num_le(data->block[4].data + 20, 2);
+    sr_data.auto_load_value = bit_lib_bytes_to_num_le(data->block[4].data + 22, 2);
+    sr_data.token = data->block[5].data[8];
+    sr_data.purchase_cost = bit_lib_bytes_to_num_le(data->block[0].data + 14, 2);
+
+    snprintf(
+        sr_data.card_serial_number,
+        sizeof(sr_data.card_serial_number),
+        "%02X%02X%02X%02X%02X",
+        data->block[1].data[6],
+        data->block[1].data[7],
+        data->block[1].data[8],
+        data->block[1].data[9],
+        data->block[1].data[10]);
+
+    for(uint8_t block_number = 40; block_number <= 52 && sr_data.trip_count < MAX_TRIPS;
+        block_number++) {
+        if((block_number != 43 && block_number != 47 && block_number != 51) &&
+           mf_classic_is_block_read(data, block_number) &&
+           parse_trip_data(
+               &data->block[block_number], &sr_data.trips[sr_data.trip_count], block_number)) {
+            sr_data.trip_count++;
+        }
+    }
+
+    // Sort trips by timestamp (descending order)
+    for(uint8_t i = 0; i < sr_data.trip_count - 1; i++) {
+        for(uint8_t j = 0; j < sr_data.trip_count - i - 1; j++) {
+            if(sr_data.trips[j].timestamp < sr_data.trips[j + 1].timestamp) {
+                TripData temp = sr_data.trips[j];
+                sr_data.trips[j] = sr_data.trips[j + 1];
+                sr_data.trips[j + 1] = temp;
+            }
+        }
+    }
+
+    furi_string_printf(
+        parsed_data,
+        "\e#SmartRider\nBalance: $%lu.%02lu\nConcession: %s\nSerial: %s%s\n"
+        "Total Cost: $%u.%02u\nAuto-Load: $%u.%02u/$%u.%02u\n\e#Tag On/Off History\n",
+        sr_data.balance / 100,
+        sr_data.balance % 100,
+        get_concession_type(sr_data.token),
+        memcmp(sr_data.card_serial_number, "00", 2) == 0 ? "SR0" : "",
+        memcmp(sr_data.card_serial_number, "00", 2) == 0 ? sr_data.card_serial_number + 2 :
+                                                           sr_data.card_serial_number,
+        sr_data.purchase_cost / 100,
+        sr_data.purchase_cost % 100,
+        sr_data.auto_load_threshold / 100,
+        sr_data.auto_load_threshold % 100,
+        sr_data.auto_load_value / 100,
+        sr_data.auto_load_value % 100);
+
+    for(uint8_t i = 0; i < sr_data.trip_count; i++) {
+        char date_str[9];
+        calculate_date(sr_data.trips[i].timestamp, date_str, sizeof(date_str));
+
+        uint32_t cost = sr_data.trips[i].cost;
+        if(cost > 0) {
+            furi_string_cat_printf(
+                parsed_data,
+                "%s %c $%lu.%02lu %s\n",
+                date_str,
+                sr_data.trips[i].tap_on ? '+' : '-',
+                cost / 100,
+                cost % 100,
+                sr_data.trips[i].route);
+        } else {
+            furi_string_cat_printf(
+                parsed_data,
+                "%s %c %s\n",
+                date_str,
+                sr_data.trips[i].tap_on ? '+' : '-',
+                sr_data.trips[i].route);
+        }
+    }
+
+    return true;
+}
+
+// made with love by jay candel <3
+
+void handle_keyfile_case(
+    Metroflip* app,
+    const char* message_title,
+    const char* log_message,
+    FuriString* parsed_data) {
+    FURI_LOG_I(TAG, log_message);
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+    furi_string_reset(parsed_data);
+
+    furi_string_printf(
+        parsed_data,
+        "\e#%s\n\n"
+        "To read a SmartRider, \nyou need to read \nit in NFC "
+        "app on \nthe flipper, and it\nneeds to show \n32/32 keys and\n"
+        "16/16 sectors read\n"
+        "Here is a guide to \nfollow to read \nMIFARE Classic:\n"
+        "https://flipper.wiki/mifareclassic/\n"
+        "Once completed, Scan again",
+        message_title);
+
+    widget_add_text_scroll_element(app->widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+    widget_add_button_element(
+        app->widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+    metroflip_app_blink_stop(app);
+}
+
+static NfcCommand
+    metroflip_scene_smartrider_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(context);
+    furi_assert(event.event_data);
+    furi_assert(event.protocol == NfcProtocolMfClassic);
+
+    NfcCommand command = NfcCommandContinue;
+    const MfClassicPollerEvent* mfc_event = event.event_data;
+    Metroflip* app = context;
+    FuriString* parsed_data = furi_string_alloc();
+    Widget* widget = app->widget;
+
+    if(mfc_event->type == MfClassicPollerEventTypeCardDetected) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected);
+        command = NfcCommandContinue;
+    } else if(mfc_event->type == MfClassicPollerEventTypeCardLost) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardLost);
+
+        command = NfcCommandStop;
+    } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) {
+        mfc_event->data->poller_mode.mode = MfClassicPollerModeRead;
+        nfc_device_set_data(
+            app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller));
+        size_t uid_len = 0;
+        const uint8_t* uid = nfc_device_get_uid(app->nfc_device, &uid_len);
+        /*-----------------All of this is to store a keyfile in a permanent way for the user to always access------------*/
+        /*-----------------Open cache file (if exists)------------*/
+
+        char uid_str[uid_len * 2 + 1];
+        uid_to_string(uid, uid_len, uid_str, sizeof(uid_str));
+        KeyfileManager manage = manage_keyfiles(uid_str);
+
+        switch(manage) {
+        case MISSING_KEYFILE:
+            handle_keyfile_case(app, "No keys found", "Missing keyfile", parsed_data);
+            command = NfcCommandStop;
+            break;
+
+        case INCOMPLETE_KEYFILE:
+            handle_keyfile_case(app, "Incomplete keyfile", "incomplete keyfile", parsed_data);
+            command = NfcCommandStop;
+            break;
+
+        case SUCCESSFUL:
+            mf_classic_key_cache_load(app->mfc_key_cache, uid, uid_len);
+            FURI_LOG_I(TAG, "success");
+            break;
+        }
+    } else if(mfc_event->type == MfClassicPollerEventTypeRequestReadSector) {
+        FURI_LOG_I(TAG, "sec_num: %d", sector_num);
+        MfClassicKey key = {};
+        MfClassicKeyType key_type = MfClassicKeyTypeA;
+        if(mf_classic_key_cache_get_next_key(app->mfc_key_cache, &sector_num, &key, &key_type)) {
+            mfc_event->data->read_sector_request_data.sector_num = sector_num;
+            mfc_event->data->read_sector_request_data.key = key;
+            mfc_event->data->read_sector_request_data.key_type = key_type;
+            mfc_event->data->read_sector_request_data.key_provided = true;
+        } else {
+            mfc_event->data->read_sector_request_data.key_provided = false;
+        }
+    } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) {
+        nfc_device_set_data(
+            app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller));
+
+        dolphin_deed(DolphinDeedNfcReadSuccess);
+        furi_string_reset(app->text_box_store);
+        if(!smartrider_parse(app->nfc_device, parsed_data)) {
+            FURI_LOG_I(TAG, "Unknown card type");
+            furi_string_printf(parsed_data, "\e#Unknown card\n");
+        }
+        widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+
+        furi_string_free(parsed_data);
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        metroflip_app_blink_stop(app);
+        UNUSED(smartrider_parse);
+        command = NfcCommandStop;
+    } else if(mfc_event->type == MfClassicPollerEventTypeFail) {
+        FURI_LOG_I(TAG, "fail");
+        command = NfcCommandStop;
+    }
+
+    return command;
+}
+
+void metroflip_scene_smartrider_on_enter(void* context) {
+    Metroflip* app = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    mf_classic_key_cache_reset(app->mfc_key_cache);
+
+    // Setup view
+    Popup* popup = app->popup;
+    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    // Start worker
+    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+    nfc_scanner_alloc(app->nfc);
+    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
+    nfc_poller_start(app->poller, metroflip_scene_smartrider_poller_callback, app);
+
+    metroflip_app_blink_start(app);
+}
+
+bool metroflip_scene_smartrider_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MetroflipCustomEventCardDetected) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventCardLost) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventWrongCard) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventPollerFail) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventPollerSuccess) {
+            scene_manager_next_scene(app->scene_manager, MetroflipSceneReadSuccess);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void metroflip_scene_smartrider_on_exit(void* context) {
+    Metroflip* app = context;
+    widget_reset(app->widget);
+
+    if(app->poller) {
+        nfc_poller_stop(app->poller);
+        nfc_poller_free(app->poller);
+    }
+
+    // Clear view
+    popup_reset(app->popup);
+
+    metroflip_app_blink_stop(app);
+}

+ 7 - 0
scenes/metroflip_scene_start.c

@@ -27,6 +27,13 @@ void metroflip_scene_start_on_enter(void* context) {
     submenu_add_item(
         submenu, "Clipper", MetroflipSceneClipper, metroflip_scene_start_submenu_callback, app);
 
+    submenu_add_item(
+        submenu,
+        "SmartRider",
+        MetroflipSceneSmartrider,
+        metroflip_scene_start_submenu_callback,
+        app);
+
     submenu_add_item(
         submenu, "myki", MetroflipSceneMyki, metroflip_scene_start_submenu_callback, app);