فهرست منبع

Merge picopass from https://github.com/flipperdevices/flipperzero-good-faps

Willy-JL 2 سال پیش
والد
کامیت
6e4c0f3b84

+ 11 - 5
picopass/.catalog/README.md

@@ -3,19 +3,17 @@
 
 This application allows you to read, write, save, and emulate legacy HID iClass cards and fobs (based on the picopass chipset).  Also supports saving the credential to the Flipper Zero LFRFID data format, changing the keys on the card, performing dictionary attack, and performing the 'online' part of the loclass attack.
 
-NOTE: Does not support iClass SE
-
 # Loclass
 
-The loclass attack emulates specific CSN and collects responses from the reader which can be used to calculate the elite or custom key configured for that reader.  This key is then used to read data on the cards used with that reader.
+The loclass attack emulates specific CSN and collects responses from the reader which can be used to calculate the elite or (some) custom key configured for that reader.  This key is then used to read data on the cards used with that reader.
 
 ## Online part
 
 1. Run _loclass_ from the picopass main menu
-2. Present the flipper to the reader.
+2. Present the flipper to the reader.  Holding flipper directly to reader may not work, vary distance by a few inches.
 3. Collect responses until the progress bar is full.
 
-NOTE: If the screen says “Got std key” AND stays on 0/18, the reader is not elite or custom keyed.
+NOTE: If the screen says “Got std key” AND stays on 0/18, then loclass isn't needed.
 
 ## Offline part
 
@@ -24,3 +22,11 @@ NOTE: If the screen says “Got std key” AND stays on 0/18, the reader is not
 3. Copy the key to _iclass_elite_dict_user.txt_ and place in _sdcard/apps_data/picopass/_
 4. Run _Elite Dict. Attack_ from the picopass main menu
 5. Present card to the back of the Flipper Zero.
+
+## Failure
+
+There are some situations when the offline loclass may not find a key, such as:
+ * iClass SE
+ * Readers configured with Standard-2 keyset
+ * Custom keyed readers using Standard KDF
+ * Custom keyed readers using SE KDF

+ 5 - 0
picopass/.catalog/changelog.md

@@ -1,3 +1,8 @@
+## 1.9
+ - Fix bug (#77) with loclass
+ - Better loclass notes
+ - Read card using nr-mac
+ - Save as Seader format
 ## 1.8
  - Minimal changes for recent API updates
 ## 1.7

+ 1 - 1
picopass/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/picopass_device.c

@@ -36,6 +36,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;
@@ -153,11 +206,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");
@@ -173,6 +228,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/picopass_device.h

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

+ 1 - 0
picopass/picopass_i.h

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

+ 77 - 13
picopass/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
picopass/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
picopass/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
picopass/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
picopass/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
picopass/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
picopass/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
picopass/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
picopass/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
picopass/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, "");