Parcourir la source

Merge picopass from https://github.com/xMasterX/all-the-plugins

Willy-JL il y a 1 an
Parent
commit
babee2eae7

+ 85 - 18
picopass/picopass_device.c

@@ -7,6 +7,7 @@
 #include <assets_icons.h>
 
 #include <toolbox/protocols/protocol_dict.h>
+#include <toolbox/hex.h>
 #include <lfrfid/protocols/lfrfid_protocols.h>
 #include <lfrfid/lfrfid_dict_file.h>
 
@@ -17,13 +18,21 @@ static const uint32_t picopass_file_version = 1;
 
 const uint8_t picopass_iclass_decryptionkey[] =
     {0xb4, 0x21, 0x2c, 0xca, 0xb7, 0xed, 0x21, 0x0f, 0x7b, 0x93, 0xd4, 0x59, 0x39, 0xc7, 0xdd, 0x36};
+const char unknown_block[] = "?? ?? ?? ?? ?? ?? ?? ??";
 
 PicopassDevice* picopass_device_alloc() {
     PicopassDevice* picopass_dev = malloc(sizeof(PicopassDevice));
+    picopass_dev->dev_data.auth = PicopassDeviceAuthMethodUnset;
     picopass_dev->dev_data.pacs.legacy = false;
     picopass_dev->dev_data.pacs.se_enabled = false;
+    picopass_dev->dev_data.pacs.sio = false;
+    picopass_dev->dev_data.pacs.biometrics = false;
+    memset(picopass_dev->dev_data.pacs.key, 0, sizeof(picopass_dev->dev_data.pacs.key));
     picopass_dev->dev_data.pacs.elite_kdf = false;
     picopass_dev->dev_data.pacs.pin_length = 0;
+    picopass_dev->dev_data.pacs.bitLength = 0;
+    memset(
+        picopass_dev->dev_data.pacs.credential, 0, sizeof(picopass_dev->dev_data.pacs.credential));
     picopass_dev->storage = furi_record_open(RECORD_STORAGE);
     picopass_dev->dialogs = furi_record_open(RECORD_DIALOGS);
     picopass_dev->load_path = furi_string_alloc();
@@ -141,6 +150,7 @@ static bool picopass_device_save_file_lfrfid(PicopassDevice* dev, FuriString* fi
     FURI_LOG_D(TAG, "LFRFID Brief: %s", furi_string_get_cstr(briefStr));
     furi_string_free(briefStr);
 
+    storage_simply_mkdir(dev->storage, EXT_PATH("lfrfid"));
     result = lfrfid_dict_file_save(dict, protocol, furi_string_get_cstr(file_path));
     if(result) {
         FURI_LOG_D(TAG, "Written: %d", result);
@@ -159,7 +169,7 @@ static bool picopass_device_save_file(
     const char* extension,
     bool use_load_path) {
     furi_assert(dev);
-    FURI_LOG_D(TAG, "Save File");
+    FURI_LOG_D(TAG, "Save File %s %s %s", folder, dev_name, extension);
 
     bool saved = false;
     FlipperFormat* file = flipper_format_file_alloc(dev->storage);
@@ -171,6 +181,7 @@ static bool picopass_device_save_file(
     if(dev->format == PicopassDeviceSaveFormatPartial) {
         // Clear key that may have been set when doing key tests for legacy
         memset(card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].data, 0, PICOPASS_BLOCK_LEN);
+        card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].valid = false;
     }
 
     do {
@@ -205,13 +216,21 @@ static bool picopass_device_save_file(
                                    PICOPASS_MAX_APP_LIMIT;
             for(size_t i = 0; i < app_limit; i++) {
                 furi_string_printf(temp_str, "Block %d", i);
-                if(!flipper_format_write_hex(
-                       file,
-                       furi_string_get_cstr(temp_str),
-                       card_data[i].data,
-                       PICOPASS_BLOCK_LEN)) {
-                    block_saved = false;
-                    break;
+                if(card_data[i].valid) {
+                    if(!flipper_format_write_hex(
+                           file,
+                           furi_string_get_cstr(temp_str),
+                           card_data[i].data,
+                           PICOPASS_BLOCK_LEN)) {
+                        block_saved = false;
+                        break;
+                    }
+                } else {
+                    if(!flipper_format_write_string_cstr(
+                           file, furi_string_get_cstr(temp_str), unknown_block)) {
+                        block_saved = false;
+                        break;
+                    }
                 }
             }
             if(!block_saved) break;
@@ -236,10 +255,10 @@ bool picopass_device_save(PicopassDevice* dev, const char* dev_name) {
         return picopass_device_save_file(
             dev, dev_name, STORAGE_APP_DATA_PATH_PREFIX, PICOPASS_APP_EXTENSION, true);
     } else if(dev->format == PicopassDeviceSaveFormatLF) {
-        return picopass_device_save_file(dev, dev_name, ANY_PATH("lfrfid"), ".rfid", true);
+        return picopass_device_save_file(dev, dev_name, ANY_PATH("lfrfid"), ".rfid", false);
     } else if(dev->format == PicopassDeviceSaveFormatSeader) {
         return picopass_device_save_file(
-            dev, dev_name, EXT_PATH("apps_data/seader"), ".credential", true);
+            dev, dev_name, EXT_PATH("apps_data/seader"), ".credential", false);
     } else if(dev->format == PicopassDeviceSaveFormatPartial) {
         return picopass_device_save_file(
             dev, dev_name, STORAGE_APP_DATA_PATH_PREFIX, PICOPASS_APP_EXTENSION, true);
@@ -248,6 +267,19 @@ bool picopass_device_save(PicopassDevice* dev, const char* dev_name) {
     return false;
 }
 
+bool picopass_hex_str_to_uint8(const char* value_str, uint8_t* value) {
+    furi_check(value_str);
+    furi_check(value);
+
+    bool parse_success = false;
+    while(*value_str && value_str[1]) {
+        parse_success = hex_char_to_uint8(*value_str, value_str[1], value++);
+        if(!parse_success) break;
+        value_str += 3;
+    }
+    return parse_success;
+}
+
 static bool picopass_device_load_data(PicopassDevice* dev, FuriString* path, bool show_dialog) {
     bool parsed = false;
     FlipperFormat* file = flipper_format_file_alloc(dev->storage);
@@ -262,26 +294,39 @@ static bool picopass_device_load_data(PicopassDevice* dev, FuriString* path, boo
     }
 
     do {
+        picopass_device_data_clear(&dev->dev_data);
         if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
 
         // Read and verify file header
         uint32_t version = 0;
         if(!flipper_format_read_header(file, temp_str, &version)) break;
-        if(furi_string_cmp_str(temp_str, picopass_file_header) ||
+        if(!furi_string_equal_str(temp_str, picopass_file_header) ||
            (version != picopass_file_version)) {
             deprecated_version = true;
             break;
         }
 
+        FuriString* block_str = furi_string_alloc();
         // Parse header blocks
         bool block_read = true;
         for(size_t i = 0; i < 6; i++) {
             furi_string_printf(temp_str, "Block %d", i);
-            if(!flipper_format_read_hex(
-                   file, furi_string_get_cstr(temp_str), card_data[i].data, PICOPASS_BLOCK_LEN)) {
+            if(!flipper_format_read_string(file, furi_string_get_cstr(temp_str), block_str)) {
                 block_read = false;
                 break;
             }
+            if(furi_string_equal_str(block_str, unknown_block)) {
+                FURI_LOG_D(TAG, "Block %i: %s (unknown)", i, furi_string_get_cstr(block_str));
+                card_data[i].valid = false;
+                memset(card_data[i].data, 0, PICOPASS_BLOCK_LEN);
+            } else {
+                FURI_LOG_D(TAG, "Block %i: %s (hex)", i, furi_string_get_cstr(block_str));
+                if(!picopass_hex_str_to_uint8(furi_string_get_cstr(block_str), card_data[i].data)) {
+                    block_read = false;
+                    break;
+                }
+                card_data[i].valid = true;
+            }
         }
 
         size_t app_limit = card_data[PICOPASS_CONFIG_BLOCK_INDEX].data[0];
@@ -289,16 +334,29 @@ static bool picopass_device_load_data(PicopassDevice* dev, FuriString* path, boo
         if(app_limit > PICOPASS_MAX_APP_LIMIT) app_limit = PICOPASS_MAX_APP_LIMIT;
         for(size_t i = 6; i < app_limit; i++) {
             furi_string_printf(temp_str, "Block %d", i);
-            if(!flipper_format_read_hex(
-                   file, furi_string_get_cstr(temp_str), card_data[i].data, PICOPASS_BLOCK_LEN)) {
+            if(!flipper_format_read_string(file, furi_string_get_cstr(temp_str), block_str)) {
                 block_read = false;
                 break;
             }
+            if(furi_string_equal_str(block_str, unknown_block)) {
+                FURI_LOG_D(TAG, "Block %i: %s (unknown)", i, furi_string_get_cstr(block_str));
+                card_data[i].valid = false;
+                memset(card_data[i].data, 0, PICOPASS_BLOCK_LEN);
+            } else {
+                FURI_LOG_D(TAG, "Block %i: %s (hex)", i, furi_string_get_cstr(block_str));
+                if(!picopass_hex_str_to_uint8(furi_string_get_cstr(block_str), card_data[i].data)) {
+                    block_read = false;
+                    break;
+                }
+                card_data[i].valid = true;
+            }
         }
         if(!block_read) break;
 
-        picopass_device_parse_credential(card_data, pacs);
-        picopass_device_parse_wiegand(pacs->credential, pacs);
+        if(card_data[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].valid) {
+            picopass_device_parse_credential(card_data, pacs);
+            picopass_device_parse_wiegand(pacs);
+        }
 
         parsed = true;
     } while(false);
@@ -373,14 +431,22 @@ void picopass_device_data_clear(PicopassDeviceData* dev_data) {
         memset(dev_data->card_data[i].data, 0, sizeof(dev_data->card_data[i].data));
         dev_data->card_data[i].valid = false;
     }
+    memset(dev_data->pacs.credential, 0, sizeof(dev_data->pacs.credential));
+    dev_data->auth = PicopassDeviceAuthMethodUnset;
     dev_data->pacs.legacy = false;
     dev_data->pacs.se_enabled = false;
     dev_data->pacs.elite_kdf = false;
+    dev_data->pacs.sio = false;
     dev_data->pacs.pin_length = 0;
+    dev_data->pacs.bitLength = 0;
 }
 
 bool picopass_device_delete(PicopassDevice* dev, bool use_load_path) {
     furi_assert(dev);
+    if(dev->format != PicopassDeviceSaveFormatHF) {
+        // Never delete other formats (LF, Seader, etc)
+        return false;
+    }
 
     bool deleted = false;
     FuriString* file_path;
@@ -452,7 +518,8 @@ void picopass_device_parse_credential(PicopassBlock* card_data, PicopassPacs* pa
     pacs->sio = (card_data[10].data[0] == 0x30); // rough check
 }
 
-void picopass_device_parse_wiegand(uint8_t* credential, PicopassPacs* pacs) {
+void picopass_device_parse_wiegand(PicopassPacs* pacs) {
+    uint8_t* credential = pacs->credential;
     uint32_t* halves = (uint32_t*)credential;
     if(halves[0] == 0) {
         uint8_t leading0s = __builtin_clz(REVERSE_BYTES_U32(halves[1]));

+ 10 - 1
picopass/picopass_device.h

@@ -74,6 +74,14 @@ typedef enum {
     PicopassDeviceSaveFormatPartial,
 } PicopassDeviceSaveFormat;
 
+typedef enum {
+    PicopassDeviceAuthMethodUnset,
+    PicopassDeviceAuthMethodNone, // unsecured picopass
+    PicopassDeviceAuthMethodKey,
+    PicopassDeviceAuthMethodNrMac,
+    PicopassDeviceAuthMethodFailed,
+} PicopassDeviceAuthMethod;
+
 typedef enum {
     PicopassEmulatorStateHalt,
     PicopassEmulatorStateIdle,
@@ -105,6 +113,7 @@ typedef struct {
 typedef struct {
     PicopassBlock card_data[PICOPASS_MAX_APP_LIMIT];
     PicopassPacs pacs;
+    PicopassDeviceAuthMethod auth;
 } PicopassDeviceData;
 
 typedef struct {
@@ -150,5 +159,5 @@ void picopass_device_set_loading_callback(
     void* context);
 
 void picopass_device_parse_credential(PicopassBlock* card_data, PicopassPacs* pacs);
-void picopass_device_parse_wiegand(uint8_t* credential, PicopassPacs* pacs);
+void picopass_device_parse_wiegand(PicopassPacs* pacs);
 bool picopass_device_hid_csn(PicopassDevice* dev);

+ 1 - 1
picopass/protocol/picopass_listener.c

@@ -378,7 +378,7 @@ PicopassListenerCommand
         uint8_t rmac[4] = {};
         uint8_t tmac[4] = {};
         const uint8_t* key = instance->data->card_data[instance->key_block_num].data;
-        bool no_key = picopass_is_memset(key, 0x00, PICOPASS_BLOCK_LEN);
+        bool no_key = !instance->data->card_data[instance->key_block_num].valid;
         const uint8_t* rx_data = bit_buffer_get_data(buf);
 
         if(no_key) {

+ 6 - 1
picopass/protocol/picopass_poller.c

@@ -162,6 +162,7 @@ NfcCommand picopass_poller_check_security(PicopassPoller* instance) {
     case PICOPASS_FUSE_CRYPT0:
         FURI_LOG_D(TAG, "Non-secured page, skipping auth");
         instance->secured = false;
+        instance->data->auth = PicopassDeviceAuthMethodNone;
         picopass_poller_prepare_read(instance);
         instance->state = PicopassPollerStateReadBlock;
         return command;
@@ -193,6 +194,8 @@ NfcCommand picopass_poller_check_security(PicopassPoller* instance) {
         FURI_LOG_D(TAG, "SE enabled");
     }
 
+    // Assume failure since we must auth, correct value will be set on success
+    instance->data->auth = PicopassDeviceAuthMethodFailed;
     if(instance->mode == PicopassPollerModeRead) {
         // Always try the NR-MAC auth in case we have the file.
         instance->state = PicopassPollerStateNrMacAuth;
@@ -295,6 +298,7 @@ NfcCommand picopass_poller_nr_mac_auth(PicopassPoller* instance) {
         PicopassCheckResp check_resp = {};
         error = picopass_poller_check(instance, nr_mac, &mac, &check_resp);
         if(error == PicopassErrorNone) {
+            instance->data->auth = PicopassDeviceAuthMethodNrMac;
             memcpy(instance->mac.data, mac.data, sizeof(PicopassMac));
             if(instance->mode == PicopassPollerModeRead) {
                 picopass_poller_prepare_read(instance);
@@ -383,6 +387,7 @@ NfcCommand picopass_poller_auth_handler(PicopassPoller* instance) {
         error = picopass_poller_check(instance, NULL, &mac, &check_resp);
         if(error == PicopassErrorNone) {
             FURI_LOG_I(TAG, "Found key");
+            instance->data->auth = PicopassDeviceAuthMethodKey;
             memcpy(instance->mac.data, mac.data, sizeof(PicopassMac));
             if(instance->mode == PicopassPollerModeRead) {
                 memcpy(
@@ -463,7 +468,7 @@ NfcCommand picopass_poller_parse_credential_handler(PicopassPoller* instance) {
 NfcCommand picopass_poller_parse_wiegand_handler(PicopassPoller* instance) {
     NfcCommand command = NfcCommandContinue;
 
-    picopass_device_parse_wiegand(instance->data->pacs.credential, &instance->data->pacs);
+    picopass_device_parse_wiegand(&instance->data->pacs);
     instance->state = PicopassPollerStateSuccess;
     return command;
 }

+ 1 - 2
picopass/scenes/picopass_scene_card_menu.c

@@ -28,8 +28,7 @@ void picopass_scene_card_menu_on_enter(void* context) {
     bool zero_config = picopass_is_memset(
         card_data[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data, 0x00, PICOPASS_BLOCK_LEN);
     bool no_credential = picopass_is_memset(pacs->credential, 0x00, sizeof(pacs->credential));
-    bool no_key = picopass_is_memset(
-        card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].data, 0xFF, PICOPASS_BLOCK_LEN);
+    bool no_key = !card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].valid;
 
     if(secured && zero_config) {
         submenu_add_item(

+ 1 - 2
picopass/scenes/picopass_scene_device_info.c

@@ -102,8 +102,7 @@ bool picopass_scene_device_info_on_event(void* context, SceneManagerEvent event)
             consumed = true;
         }
     } else if(event.type == SceneManagerEventTypeBack) {
-        view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
-        consumed = true;
+        consumed = scene_manager_previous_scene(picopass->scene_manager);
     }
     return consumed;
 }

+ 1 - 0
picopass/scenes/picopass_scene_loclass.c

@@ -36,6 +36,7 @@ void picopass_scene_loclass_on_enter(void* context) {
 
     loclass_set_callback(picopass->loclass, picopass_loclass_result_callback, picopass);
     loclass_set_header(picopass->loclass, "Loclass");
+    loclass_set_subheader(picopass->loclass, "Hold To Reader");
 
     picopass_blink_emulate_start(picopass);
     view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewLoclass);

+ 6 - 2
picopass/scenes/picopass_scene_more_info.c

@@ -19,8 +19,12 @@ void picopass_scene_more_info_on_enter(void* context) {
 
     for(size_t i = 0; i < app_limit; i++) {
         for(size_t j = 0; j < PICOPASS_BLOCK_LEN; j += 2) {
-            furi_string_cat_printf(
-                str, "%02X%02X ", card_data[i].data[j], card_data[i].data[j + 1]);
+            if(card_data[i].valid) {
+                furi_string_cat_printf(
+                    str, "%02X%02X ", card_data[i].data[j], card_data[i].data[j + 1]);
+            } else {
+                furi_string_cat_printf(str, "???? ");
+            }
         }
     }
 

+ 25 - 2
picopass/scenes/picopass_scene_read_card_success.c

@@ -2,6 +2,8 @@
 #include <dolphin/dolphin.h>
 #include <picopass_keys.h>
 
+#define TAG "PicopassSceneReadCardSuccess"
+
 void picopass_scene_read_card_success_widget_callback(
     GuiButtonType result,
     InputType type,
@@ -27,6 +29,28 @@ void picopass_scene_read_card_success_on_enter(void* context) {
     // Send notification
     notification_message(picopass->notifications, &sequence_success);
 
+    // For initial testing, print auth method
+    switch(picopass->dev->dev_data.auth) {
+    case PicopassDeviceAuthMethodUnset:
+        FURI_LOG_D(TAG, "Auth: Unset");
+        break;
+    case PicopassDeviceAuthMethodNone:
+        FURI_LOG_D(TAG, "Auth: None");
+        break;
+    case PicopassDeviceAuthMethodKey:
+        FURI_LOG_D(TAG, "Auth: Key");
+        break;
+    case PicopassDeviceAuthMethodNrMac:
+        FURI_LOG_D(TAG, "Auth: NR-MAC");
+        break;
+    case PicopassDeviceAuthMethodFailed:
+        FURI_LOG_D(TAG, "Auth: Failed");
+        break;
+    default:
+        FURI_LOG_D(TAG, "Auth: Unknown");
+        break;
+    };
+
     // Setup view
     PicopassBlock* card_data = picopass->dev->dev_data.card_data;
     PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
@@ -133,8 +157,7 @@ void picopass_scene_read_card_success_on_enter(void* context) {
             furi_string_cat_printf(credential_str, " +SIO");
         }
 
-        bool no_key = picopass_is_memset(
-            card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].data, 0xFF, PICOPASS_BLOCK_LEN);
+        bool no_key = !card_data[PICOPASS_SECURE_KD_BLOCK_INDEX].valid;
 
         if(no_key) {
             furi_string_cat_printf(key_str, "No Key: used NR-MAC");

+ 24 - 0
picopass/scenes/picopass_scene_saved_menu.c

@@ -6,6 +6,7 @@ enum SubmenuIndex {
     SubmenuIndexEmulate,
     SubmenuIndexRename,
     SubmenuIndexDelete,
+    SubmenuIndexSaveAsLF,
 };
 
 void picopass_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
@@ -18,6 +19,13 @@ void picopass_scene_saved_menu_on_enter(void* context) {
     Picopass* picopass = context;
     Submenu* submenu = picopass->submenu;
 
+    PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
+    PicopassBlock* card_data = picopass->dev->dev_data.card_data;
+
+    bool secured = (card_data[PICOPASS_CONFIG_BLOCK_INDEX].data[7] & PICOPASS_FUSE_CRYPT10) !=
+                   PICOPASS_FUSE_CRYPT0;
+    bool no_credential = picopass_is_memset(pacs->credential, 0x00, sizeof(pacs->credential));
+
     submenu_add_item(
         submenu, "Info", SubmenuIndexInfo, picopass_scene_saved_menu_submenu_callback, picopass);
     submenu_add_item(
@@ -28,6 +36,16 @@ void picopass_scene_saved_menu_on_enter(void* context) {
         SubmenuIndexEmulate,
         picopass_scene_saved_menu_submenu_callback,
         picopass);
+
+    if(secured && !no_credential) {
+        submenu_add_item(
+            submenu,
+            "Save as LFRFID",
+            SubmenuIndexSaveAsLF,
+            picopass_scene_saved_menu_submenu_callback,
+            picopass);
+    }
+
     submenu_add_item(
         submenu,
         "Rename",
@@ -71,6 +89,12 @@ bool picopass_scene_saved_menu_on_event(void* context, SceneManagerEvent event)
         } else if(event.event == SubmenuIndexRename) {
             scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
             consumed = true;
+        } else if(event.event == SubmenuIndexSaveAsLF) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSaveAsLF);
+            picopass->dev->format = PicopassDeviceSaveFormatLF;
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
+            consumed = true;
         }
     }
 

+ 25 - 1
picopass/views/loclass.c

@@ -13,14 +13,16 @@ struct Loclass {
 typedef struct {
     FuriString* header;
     uint8_t num_macs;
+    FuriString* subheader;
 } LoclassViewModel;
 
 static void loclass_draw_callback(Canvas* canvas, void* model) {
     LoclassViewModel* m = model;
 
     char draw_str[32] = {};
-    canvas_set_font(canvas, FontSecondary);
+    canvas_set_font(canvas, FontPrimary);
     canvas_draw_str_aligned(canvas, 64, 0, AlignCenter, AlignTop, furi_string_get_cstr(m->header));
+    canvas_set_font(canvas, FontSecondary);
 
     if(m->num_macs == 255) {
         return;
@@ -37,6 +39,9 @@ static void loclass_draw_callback(Canvas* canvas, void* model) {
 
     elements_progress_bar_with_text(canvas, 0, 20, 128, progress, draw_str);
 
+    canvas_draw_str_aligned(
+        canvas, 64, 45, AlignCenter, AlignBottom, furi_string_get_cstr(m->subheader));
+
     elements_button_center(canvas, "Skip");
 }
 
@@ -61,6 +66,11 @@ Loclass* loclass_alloc() {
     view_set_context(loclass->view, loclass);
     with_view_model(
         loclass->view, LoclassViewModel * model, { model->header = furi_string_alloc(); }, false);
+    with_view_model(
+        loclass->view,
+        LoclassViewModel * model,
+        { model->subheader = furi_string_alloc(); },
+        false);
     return loclass;
 }
 
@@ -68,6 +78,8 @@ void loclass_free(Loclass* loclass) {
     furi_assert(loclass);
     with_view_model(
         loclass->view, LoclassViewModel * model, { furi_string_free(model->header); }, false);
+    with_view_model(
+        loclass->view, LoclassViewModel * model, { furi_string_free(model->subheader); }, false);
     view_free(loclass->view);
     free(loclass);
 }
@@ -80,6 +92,7 @@ void loclass_reset(Loclass* loclass) {
         {
             model->num_macs = 0;
             furi_string_reset(model->header);
+            furi_string_reset(model->subheader);
         },
         false);
 }
@@ -104,6 +117,17 @@ void loclass_set_header(Loclass* loclass, const char* header) {
         loclass->view, LoclassViewModel * model, { furi_string_set(model->header, header); }, true);
 }
 
+void loclass_set_subheader(Loclass* loclass, const char* subheader) {
+    furi_assert(loclass);
+    furi_assert(subheader);
+
+    with_view_model(
+        loclass->view,
+        LoclassViewModel * model,
+        { furi_string_set(model->subheader, subheader); },
+        true);
+}
+
 void loclass_set_num_macs(Loclass* loclass, uint16_t num_macs) {
     furi_assert(loclass);
     with_view_model(

+ 2 - 0
picopass/views/loclass.h

@@ -19,4 +19,6 @@ void loclass_set_callback(Loclass* loclass, LoclassCallback callback, void* cont
 
 void loclass_set_header(Loclass* loclass, const char* header);
 
+void loclass_set_subheader(Loclass* loclass, const char* subheader);
+
 void loclass_set_num_macs(Loclass* loclass, uint16_t num_macs);