MX 2 лет назад
Родитель
Сommit
ca2458030d

+ 1 - 1
application.fam

@@ -10,7 +10,7 @@ App(
     ],
     stack_size=4 * 1024,
     fap_description="App to communicate with NFC tags using the PicoPass(iClass) format",
-    fap_version="1.8",
+    fap_version="1.9",
     fap_icon="125_10px.png",
     fap_category="NFC",
     fap_libs=["mbedtls"],

+ 60 - 2
picopass_device.c

@@ -34,6 +34,59 @@ void picopass_device_set_name(PicopassDevice* dev, const char* name) {
     strlcpy(dev->dev_name, name, PICOPASS_DEV_NAME_MAX_LEN);
 }
 
+// For use with Seader's virtual card processing.
+static bool picopass_device_save_file_seader(
+    PicopassDevice* dev,
+    FlipperFormat* file,
+    FuriString* file_path) {
+    furi_assert(dev);
+    PicopassPacs* pacs = &dev->dev_data.pacs;
+    PicopassBlock* AA1 = dev->dev_data.AA1;
+    bool result = false;
+
+    const char* seader_file_header = "Flipper Seader Credential";
+    const uint32_t seader_file_version = 1;
+
+    do {
+        FURI_LOG_D(
+            TAG,
+            "Save %s %ld to %s",
+            seader_file_header,
+            seader_file_version,
+            furi_string_get_cstr(file_path));
+        if(!flipper_format_file_open_always(file, furi_string_get_cstr(file_path))) break;
+        if(!flipper_format_write_header_cstr(file, seader_file_header, seader_file_version)) break;
+        if(!flipper_format_write_uint32(file, "Bits", (uint32_t*)&pacs->bitLength, 1)) break;
+        if(!flipper_format_write_hex(file, "Credential", pacs->credential, PICOPASS_BLOCK_LEN))
+            break;
+
+        FURI_LOG_D(TAG, "Pre-sio");
+        // Seader only captures 64 byte SIO so I'm going to leave it at that
+        uint8_t sio[64];
+
+        // TODO: save SR vs SE more properly
+        if(pacs->sio) { // SR
+            for(uint8_t i = 0; i < 8; i++) {
+                memcpy(sio + (i * 8), AA1[10 + i].data, PICOPASS_BLOCK_LEN);
+            }
+            if(!flipper_format_write_hex(file, "SIO", sio, sizeof(sio))) break;
+        } else if(pacs->se_enabled) { //SE
+            for(uint8_t i = 0; i < 8; i++) {
+                memcpy(sio + (i * 8), AA1[6 + i].data, PICOPASS_BLOCK_LEN);
+            }
+            if(!flipper_format_write_hex(file, "SIO", sio, sizeof(sio))) break;
+        }
+        FURI_LOG_D(TAG, "post sio");
+        if(!flipper_format_write_hex(
+               file, "Diversifier", AA1[PICOPASS_CSN_BLOCK_INDEX].data, PICOPASS_BLOCK_LEN))
+            break;
+
+        result = true;
+    } while(false);
+
+    return result;
+}
+
 static bool picopass_device_save_file_lfrfid(PicopassDevice* dev, FuriString* file_path) {
     furi_assert(dev);
     PicopassPacs* pacs = &dev->dev_data.pacs;
@@ -151,11 +204,13 @@ static bool picopass_device_save_file(
                 }
             }
             if(!block_saved) break;
+            saved = true;
         } else if(dev->format == PicopassDeviceSaveFormatLF) {
             saved = picopass_device_save_file_lfrfid(dev, temp_str);
+        } else if(dev->format == PicopassDeviceSaveFormatSeader) {
+            saved = picopass_device_save_file_seader(dev, file, temp_str);
         }
-        saved = true;
-    } while(0);
+    } while(false);
 
     if(!saved) {
         dialog_message_show_storage_error(dev->dialogs, "Can not save\nfile");
@@ -171,6 +226,9 @@ bool picopass_device_save(PicopassDevice* dev, const char* dev_name) {
             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);
+    } else if(dev->format == PicopassDeviceSaveFormatSeader) {
+        return picopass_device_save_file(
+            dev, dev_name, EXT_PATH("apps_data/seader"), ".credential", true);
     }
 
     return false;

+ 1 - 0
picopass_device.h

@@ -70,6 +70,7 @@ typedef enum {
 typedef enum {
     PicopassDeviceSaveFormatHF,
     PicopassDeviceSaveFormatLF,
+    PicopassDeviceSaveFormatSeader,
 } PicopassDeviceSaveFormat;
 
 typedef enum {

+ 1 - 0
picopass_i.h

@@ -51,6 +51,7 @@ enum PicopassCustomEvent {
     PicopassCustomEventDictAttackUpdateView,
     PicopassCustomEventLoclassGotMac,
     PicopassCustomEventLoclassGotStandardKey,
+    PicopassCustomEventNrMacSaved,
 
     PicopassCustomEventPollerSuccess,
     PicopassCustomEventPollerFail,

+ 77 - 13
protocol/picopass_listener.c

@@ -1,3 +1,4 @@
+#include "picopass_i.h"
 #include "picopass_listener_i.h"
 #include "picopass_keys.h"
 
@@ -299,6 +300,61 @@ PicopassListenerCommand
     return command;
 }
 
+PicopassListenerCommand picopass_listener_save_mac(PicopassListener* instance, uint8_t* rx_data) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+    Picopass* picopass = instance->context;
+
+    PicopassDevice* dev = picopass->dev;
+
+    const uint8_t* csn = instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+    const uint8_t* epurse = instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data;
+
+    FuriString* temp_str = furi_string_alloc();
+    FuriString* filename = furi_string_alloc();
+    FlipperFormat* file = flipper_format_file_alloc(dev->storage);
+
+    for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+        furi_string_cat_printf(filename, "%02x", csn[i]);
+    }
+    furi_string_cat_printf(filename, "_");
+    for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+        furi_string_cat_printf(filename, "%02x", epurse[i]);
+    }
+
+    furi_string_printf(
+        temp_str, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, furi_string_get_cstr(filename), ".mac");
+    do {
+        // Open file
+        if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
+
+        if(!flipper_format_write_hex(file, "NR-MAC", rx_data + 1, PICOPASS_BLOCK_LEN)) break;
+
+        FURI_LOG_D(
+            TAG,
+            "Saved nr-mac: %02x %02x %02x %02x %02x %02x %02x %02x",
+            // Skip command byte [0]
+            rx_data[1],
+            rx_data[2],
+            rx_data[3],
+            rx_data[4],
+            rx_data[5],
+            rx_data[6],
+            rx_data[7],
+            rx_data[8]);
+
+        notification_message(picopass->notifications, &sequence_double_vibro);
+        command = PicopassListenerCommandStop;
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventNrMacSaved);
+    } while(0);
+
+    furi_string_free(temp_str);
+    furi_string_free(filename);
+    flipper_format_free(file);
+
+    return command;
+}
+
 PicopassListenerCommand
     picopass_listener_check_handler_emulation(PicopassListener* instance, BitBuffer* buf) {
     PicopassListenerCommand command = PicopassListenerCommandSilent;
@@ -310,23 +366,31 @@ PicopassListenerCommand
         // Since nr isn't const in loclass_opt_doBothMAC_2() copy buffer
         uint8_t rx_data[9] = {};
         bit_buffer_write_bytes(buf, rx_data, sizeof(rx_data));
-        loclass_opt_doBothMAC_2(instance->cipher_state, &rx_data[1], rmac, tmac, key);
+        bool no_key = picopass_is_memset(key, 0x00, PICOPASS_BLOCK_LEN);
 
-#ifndef PICOPASS_DEBUG_IGNORE_BAD_RMAC
-        if(memcmp(&rx_data[5], rmac, 4)) {
-            // Bad MAC from reader, do not send a response.
-            FURI_LOG_I(TAG, "Got bad MAC from reader");
-            // Reset the cipher state since we don't do it in READCHECK
-            picopass_listener_init_cipher_state(instance);
+        if(no_key) {
+            // We're emulating a partial dump of an iClass SE card and should capture the NR and MAC
+            command = picopass_listener_save_mac(instance, rx_data);
             break;
-        }
+        } else {
+            loclass_opt_doBothMAC_2(instance->cipher_state, &rx_data[1], rmac, tmac, key);
+
+#ifndef PICOPASS_DEBUG_IGNORE_BAD_RMAC
+            if(memcmp(&rx_data[5], rmac, PICOPASS_MAC_LEN)) {
+                // Bad MAC from reader, do not send a response.
+                FURI_LOG_I(TAG, "Got bad MAC from reader");
+                // Reset the cipher state since we don't do it in READCHECK
+                picopass_listener_init_cipher_state(instance);
+                break;
+            }
 #endif
 
-        bit_buffer_copy_bytes(instance->tx_buffer, tmac, sizeof(tmac));
-        NfcError error = nfc_listener_tx(instance->nfc, instance->tx_buffer);
-        if(error != NfcErrorNone) {
-            FURI_LOG_D(TAG, "Failed tx update response: %d", error);
-            break;
+            bit_buffer_copy_bytes(instance->tx_buffer, tmac, sizeof(tmac));
+            NfcError error = nfc_listener_tx(instance->nfc, instance->tx_buffer);
+            if(error != NfcErrorNone) {
+                FURI_LOG_D(TAG, "Failed tx update response: %d", error);
+                break;
+            }
         }
 
         command = PicopassListenerCommandProcessed;

+ 136 - 3
protocol/picopass_poller.c

@@ -1,3 +1,4 @@
+#include "picopass_i.h"
 #include "picopass_poller_i.h"
 
 #include "../loclass/optimized_cipher.h"
@@ -95,7 +96,7 @@ NfcCommand picopass_poller_pre_auth_handler(PicopassPoller* instance) {
             instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[7]);
 
         PicopassBlock block = {};
-        error = picopass_poller_read_block(instance, 1, &block);
+        error = picopass_poller_read_block(instance, PICOPASS_CONFIG_BLOCK_INDEX, &block);
         if(error != PicopassErrorNone) {
             instance->state = PicopassPollerStateFail;
             break;
@@ -116,6 +117,27 @@ NfcCommand picopass_poller_pre_auth_handler(PicopassPoller* instance) {
             instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[6],
             instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7]);
 
+        error = picopass_poller_read_block(instance, PICOPASS_SECURE_EPURSE_BLOCK_INDEX, &block);
+        if(error != PicopassErrorNone) {
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+        memcpy(
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data,
+            block.data,
+            sizeof(PicopassBlock));
+        FURI_LOG_D(
+            TAG,
+            "epurse %02x%02x%02x%02x%02x%02x%02x%02x",
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[0],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[1],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[2],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[3],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[4],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[5],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[6],
+            instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[7]);
+
         error = picopass_poller_read_block(instance, 5, &block);
         if(error != PicopassErrorNone) {
             instance->state = PicopassPollerStateFail;
@@ -165,10 +187,120 @@ NfcCommand picopass_poller_check_security(PicopassPoller* instance) {
 
     if(instance->data->pacs.se_enabled) {
         FURI_LOG_D(TAG, "SE enabled");
-        instance->state = PicopassPollerStateFail;
+        instance->state = PicopassPollerStateNrMacAuth;
     } else {
         instance->state = PicopassPollerStateAuth;
     }
+    return command;
+}
+
+NfcCommand picopass_poller_nr_mac_auth(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    Picopass* picopass = instance->context;
+    PicopassDevice* dev = picopass->dev;
+
+    uint8_t* csn = instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+    uint8_t* epurse = instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data;
+
+    FuriString* temp_str = furi_string_alloc();
+    FuriString* filename = furi_string_alloc();
+    FlipperFormat* file = flipper_format_file_alloc(dev->storage);
+    PicopassMac mac = {};
+
+    for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+        furi_string_cat_printf(filename, "%02x", csn[i]);
+    }
+    furi_string_cat_printf(filename, "_");
+    for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+        furi_string_cat_printf(filename, "%02x", epurse[i]);
+    }
+
+    furi_string_printf(
+        temp_str, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, furi_string_get_cstr(filename), ".mac");
+
+    FURI_LOG_D(TAG, "Looking for %s", furi_string_get_cstr(temp_str));
+    uint8_t nr_mac[PICOPASS_BLOCK_LEN];
+
+    // Presume failure unless all steps are successful and the state is made "read block"
+    instance->state = PicopassPollerStateFail;
+    do {
+        //check for file
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(temp_str))) break;
+        // FURI_LOG_D(TAG, "Found %s", furi_string_get_cstr(temp_str));
+
+        furi_string_printf(temp_str, "NR-MAC");
+        if(!flipper_format_read_hex(
+               file, furi_string_get_cstr(temp_str), nr_mac, PICOPASS_BLOCK_LEN))
+            break;
+        memcpy(mac.data, nr_mac + 4, PICOPASS_MAC_LEN);
+        /*
+        FURI_LOG_D(
+            TAG,
+            "Read nr-mac: %02x %02x %02x %02x %02x %02x %02x %02x",
+            nr_mac[0],
+            nr_mac[1],
+            nr_mac[2],
+            nr_mac[3],
+            nr_mac[4],
+            nr_mac[5],
+            nr_mac[6],
+            nr_mac[7]);
+        FURI_LOG_D(
+            TAG, "MAC: %02x %02x %02x %02x", mac.data[0], mac.data[1], mac.data[2], mac.data[3]);
+        */
+
+        uint8_t ccnr[12] = {};
+        PicopassReadCheckResp read_check_resp = {};
+        PicopassError error = picopass_poller_read_check(instance, &read_check_resp);
+        if(error == PicopassErrorTimeout) {
+            instance->event.type = PicopassPollerEventTypeCardLost;
+            instance->callback(instance->event, instance->context);
+            instance->state = PicopassPollerStateDetect;
+            break;
+        } else if(error != PicopassErrorNone) {
+            FURI_LOG_E(TAG, "Read check failed: %d", error);
+            break;
+        }
+        memcpy(ccnr, read_check_resp.data, sizeof(PicopassReadCheckResp)); // last 4 bytes left 0
+
+        /*
+        FURI_LOG_D(
+            TAG,
+            "CCNR: %02x %02x %02x %02x %02x %02x %02x %02x",
+            ccnr[0],
+            ccnr[1],
+            ccnr[2],
+            ccnr[3],
+            ccnr[4],
+            ccnr[5],
+            ccnr[6],
+            ccnr[7]);
+            */
+
+        //use mac
+        PicopassCheckResp check_resp = {};
+        error = picopass_poller_check(instance, nr_mac, &mac, &check_resp);
+        if(error == PicopassErrorNone) {
+            memcpy(instance->mac.data, mac.data, sizeof(PicopassMac));
+            if(instance->mode == PicopassPollerModeRead) {
+                picopass_poller_prepare_read(instance);
+                instance->state = PicopassPollerStateReadBlock;
+                // Set to non-zero keys to allow emulation
+                memset(
+                    instance->data->AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data,
+                    0xff,
+                    PICOPASS_BLOCK_LEN);
+                memset(
+                    instance->data->AA1[PICOPASS_SECURE_KC_BLOCK_INDEX].data,
+                    0xff,
+                    PICOPASS_BLOCK_LEN);
+            }
+        }
+
+    } while(false);
+    furi_string_free(temp_str);
+    furi_string_free(filename);
+    flipper_format_free(file);
 
     return command;
 }
@@ -234,7 +366,7 @@ NfcCommand picopass_poller_auth_handler(PicopassPoller* instance) {
         loclass_opt_doReaderMAC(ccnr, div_key, mac.data);
 
         PicopassCheckResp check_resp = {};
-        error = picopass_poller_check(instance, &mac, &check_resp);
+        error = picopass_poller_check(instance, NULL, &mac, &check_resp);
         if(error == PicopassErrorNone) {
             FURI_LOG_I(TAG, "Found key");
             memcpy(instance->mac.data, mac.data, sizeof(PicopassMac));
@@ -457,6 +589,7 @@ static const PicopassPollerStateHandler picopass_poller_state_handler[PicopassPo
     [PicopassPollerStateSelect] = picopass_poller_select_handler,
     [PicopassPollerStatePreAuth] = picopass_poller_pre_auth_handler,
     [PicopassPollerStateCheckSecurity] = picopass_poller_check_security,
+    [PicopassPollerStateNrMacAuth] = picopass_poller_nr_mac_auth,
     [PicopassPollerStateAuth] = picopass_poller_auth_handler,
     [PicopassPollerStateReadBlock] = picopass_poller_read_block_handler,
     [PicopassPollerStateWriteBlock] = picopass_poller_write_block_handler,

+ 7 - 2
protocol/picopass_poller_i.c

@@ -161,15 +161,20 @@ PicopassError
 
 PicopassError picopass_poller_check(
     PicopassPoller* instance,
+    uint8_t* nr,
     PicopassMac* mac,
     PicopassCheckResp* check_resp) {
     PicopassError ret = PicopassErrorNone;
+    uint8_t null_arr[4] = {};
 
     do {
         bit_buffer_reset(instance->tx_buffer);
         bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_CHECK);
-        uint8_t null_arr[4] = {};
-        bit_buffer_append_bytes(instance->tx_buffer, null_arr, sizeof(null_arr));
+        if(nr) {
+            bit_buffer_append_bytes(instance->tx_buffer, nr, 4);
+        } else {
+            bit_buffer_append_bytes(instance->tx_buffer, null_arr, sizeof(null_arr));
+        }
         bit_buffer_append_bytes(instance->tx_buffer, mac->data, sizeof(PicopassMac));
 
         NfcError error = nfc_poller_trx(

+ 2 - 0
protocol/picopass_poller_i.h

@@ -20,6 +20,7 @@ typedef enum {
     PicopassPollerStateSelect,
     PicopassPollerStatePreAuth,
     PicopassPollerStateCheckSecurity,
+    PicopassPollerStateNrMacAuth,
     PicopassPollerStateAuth,
     PicopassPollerStateReadBlock,
     PicopassPollerStateWriteBlock,
@@ -75,6 +76,7 @@ PicopassError
 
 PicopassError picopass_poller_check(
     PicopassPoller* instance,
+    uint8_t* nr,
     PicopassMac* mac,
     PicopassCheckResp* check_resp);
 

+ 76 - 22
scenes/picopass_scene_card_menu.c

@@ -3,9 +3,11 @@
 enum SubmenuIndex {
     SubmenuIndexSave,
     SubmenuIndexSaveAsLF,
+    SubmenuIndexSaveAsSeader,
     SubmenuIndexChangeKey,
     SubmenuIndexWrite,
     SubmenuIndexEmulate,
+    SubmenuIndexSavePartial,
 };
 
 void picopass_scene_card_menu_submenu_callback(void* context, uint32_t index) {
@@ -17,29 +19,69 @@ void picopass_scene_card_menu_submenu_callback(void* context, uint32_t index) {
 void picopass_scene_card_menu_on_enter(void* context) {
     Picopass* picopass = context;
     Submenu* submenu = picopass->submenu;
+    PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
+    PicopassBlock* AA1 = picopass->dev->dev_data.AA1;
 
-    submenu_add_item(
-        submenu, "Save", SubmenuIndexSave, picopass_scene_card_menu_submenu_callback, picopass);
-    submenu_add_item(
-        submenu,
-        "Save as LFRFID",
-        SubmenuIndexSaveAsLF,
-        picopass_scene_card_menu_submenu_callback,
-        picopass);
-    submenu_add_item(
-        submenu, "Write", SubmenuIndexWrite, picopass_scene_card_menu_submenu_callback, picopass);
-    submenu_add_item(
-        submenu,
-        "Emulate",
-        SubmenuIndexEmulate,
-        picopass_scene_card_menu_submenu_callback,
-        picopass);
-    submenu_add_item(
-        submenu,
-        "Change Key",
-        SubmenuIndexChangeKey,
-        picopass_scene_card_menu_submenu_callback,
-        picopass);
+    bool sio = 0x30 == AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data[0];
+
+    if(pacs->se_enabled) {
+        if(sio) {
+            submenu_add_item(
+                submenu,
+                "Save",
+                SubmenuIndexSave,
+                picopass_scene_card_menu_submenu_callback,
+                picopass);
+            submenu_add_item(
+                submenu,
+                "Save in Seader fmt",
+                SubmenuIndexSaveAsSeader,
+                picopass_scene_card_menu_submenu_callback,
+                picopass);
+        } else {
+            submenu_add_item(
+                submenu,
+                "Save Partial",
+                SubmenuIndexSavePartial,
+                picopass_scene_card_menu_submenu_callback,
+                picopass);
+        }
+    } else {
+        submenu_add_item(
+            submenu, "Save", SubmenuIndexSave, picopass_scene_card_menu_submenu_callback, picopass);
+        submenu_add_item(
+            submenu,
+            "Save as LFRFID",
+            SubmenuIndexSaveAsLF,
+            picopass_scene_card_menu_submenu_callback,
+            picopass);
+        if(pacs->sio) { // SR
+            submenu_add_item(
+                submenu,
+                "Save in Seader fmt",
+                SubmenuIndexSaveAsSeader,
+                picopass_scene_card_menu_submenu_callback,
+                picopass);
+        }
+        submenu_add_item(
+            submenu,
+            "Write",
+            SubmenuIndexWrite,
+            picopass_scene_card_menu_submenu_callback,
+            picopass);
+        submenu_add_item(
+            submenu,
+            "Emulate",
+            SubmenuIndexEmulate,
+            picopass_scene_card_menu_submenu_callback,
+            picopass);
+        submenu_add_item(
+            submenu,
+            "Change Key",
+            SubmenuIndexChangeKey,
+            picopass_scene_card_menu_submenu_callback,
+            picopass);
+    }
 
     submenu_set_selected_item(
         picopass->submenu,
@@ -59,6 +101,18 @@ bool picopass_scene_card_menu_on_event(void* context, SceneManagerEvent event) {
             scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
             picopass->dev->format = PicopassDeviceSaveFormatHF;
             consumed = true;
+        } else if(event.event == SubmenuIndexSavePartial) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSave);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
+            picopass->dev->format = PicopassDeviceSaveFormatHF;
+            consumed = true;
+        } else if(event.event == SubmenuIndexSaveAsSeader) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneCardMenu, event.event);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
+            picopass->dev->format = PicopassDeviceSaveFormatSeader;
+            consumed = true;
         } else if(event.event == SubmenuIndexSaveAsLF) {
             scene_manager_set_scene_state(
                 picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSaveAsLF);

+ 1 - 0
scenes/picopass_scene_config.h

@@ -19,3 +19,4 @@ ADD_SCENE(picopass, elite_dict_attack, EliteDictAttack)
 ADD_SCENE(picopass, emulate, Emulate)
 ADD_SCENE(picopass, loclass, Loclass)
 ADD_SCENE(picopass, key_input, KeyInput)
+ADD_SCENE(picopass, nr_mac_saved, NrMacSaved)

+ 5 - 2
scenes/picopass_scene_device_info.c

@@ -30,8 +30,11 @@ void picopass_scene_device_info_on_enter(void* context) {
     for(uint8_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
         furi_string_cat_printf(csn_str, "%02X ", csn[i]);
     }
+    bool sio = 0x30 == AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data[0];
 
-    if(pacs->bitLength == 0 || pacs->bitLength == 255) {
+    if(sio) {
+        furi_string_cat_printf(wiegand_str, "SIO");
+    } else if(pacs->bitLength == 0 || pacs->bitLength == 255) {
         // Neither of these are valid.  Indicates the block was all 0x00 or all 0xff
         furi_string_cat_printf(wiegand_str, "Invalid PACS");
     } else {
@@ -46,7 +49,7 @@ void picopass_scene_device_info_on_enter(void* context) {
         }
         furi_string_cat_printf(wiegand_str, "%d bits", pacs->bitLength);
 
-        if(pacs->sio) {
+        if(pacs->sio) { // SR
             furi_string_cat_printf(credential_str, " +SIO");
         }
     }

+ 3 - 0
scenes/picopass_scene_emulate.c

@@ -32,6 +32,9 @@ bool picopass_scene_emulate_on_event(void* context, SceneManagerEvent event) {
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == PicopassCustomEventWorkerExit) {
             consumed = true;
+        } else if(event.event == PicopassCustomEventNrMacSaved) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneNrMacSaved);
+            consumed = true;
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         consumed = scene_manager_previous_scene(picopass->scene_manager);

+ 44 - 0
scenes/picopass_scene_nr_mac_saved.c

@@ -0,0 +1,44 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_scene_nr_mac_saved_popup_callback(void* context) {
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventViewExit);
+}
+
+void picopass_scene_nr_mac_saved_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcSave);
+
+    // Setup view
+    Popup* popup = picopass->popup;
+    popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
+    popup_set_header(popup, "NR-MAC\nSaved!", 13, 22, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, picopass);
+    popup_set_callback(popup, picopass_scene_nr_mac_saved_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+}
+
+bool picopass_scene_nr_mac_saved_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventViewExit) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneStart, 0); // Set back to "read card"
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                picopass->scene_manager, PicopassSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_nr_mac_saved_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    popup_reset(picopass->popup);
+}

+ 19 - 2
scenes/picopass_scene_read_card_success.c

@@ -43,6 +43,7 @@ void picopass_scene_read_card_success_on_enter(void* context) {
         AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data, 0x00, PICOPASS_BLOCK_LEN);
     bool empty = picopass_is_memset(
         AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data, 0xFF, PICOPASS_BLOCK_LEN);
+    bool sio = 0x30 == AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data[0];
 
     if(no_key) {
         furi_string_cat_printf(wiegand_str, "Read Failed");
@@ -50,6 +51,13 @@ void picopass_scene_read_card_success_on_enter(void* context) {
 
         if(pacs->se_enabled) {
             furi_string_cat_printf(credential_str, "SE enabled");
+
+            widget_add_button_element(
+                widget,
+                GuiButtonTypeRight,
+                "More",
+                picopass_scene_read_card_success_widget_callback,
+                picopass);
         } else if(!hid_csn) {
             furi_string_cat_printf(credential_str, "Non-HID CSN");
         }
@@ -70,8 +78,11 @@ void picopass_scene_read_card_success_on_enter(void* context) {
             picopass);
     } else if(pacs->bitLength == 0 || pacs->bitLength == 255) {
         // Neither of these are valid.  Indicates the block was all 0x00 or all 0xff
-        furi_string_cat_printf(wiegand_str, "Invalid PACS");
-
+        if(sio) {
+            furi_string_cat_printf(wiegand_str, "SIO");
+        } else {
+            furi_string_cat_printf(wiegand_str, "Invalid PACS");
+        }
         if(pacs->se_enabled) {
             furi_string_cat_printf(credential_str, "SE enabled");
         }
@@ -81,6 +92,12 @@ void picopass_scene_read_card_success_on_enter(void* context) {
             "Menu",
             picopass_scene_read_card_success_widget_callback,
             picopass);
+        widget_add_button_element(
+            widget,
+            GuiButtonTypeRight,
+            "More",
+            picopass_scene_read_card_success_widget_callback,
+            picopass);
     } else {
         size_t bytesLength = 1 + pacs->bitLength / 8;
         furi_string_set(credential_str, "");