Ver código fonte

Add mfc_editor from https://github.com/TollyH/flipper-apps

git-subtree-dir: mfc_editor
git-subtree-mainline: 4382659af8549e6cf31a9c1f6791f29264b5967f
git-subtree-split: 2d0b14415421e15dd397e22abd6f8cd33a25b17c
Willy-JL 1 ano atrás
pai
commit
cbe637eeb2

+ 3 - 0
mfc_editor/.catalog/CHANGELOG.md

@@ -0,0 +1,3 @@
+## 1.0
+
+- Initial release

+ 19 - 0
mfc_editor/.catalog/README.md

@@ -0,0 +1,19 @@
+# MIFARE Classic Editor
+
+An application for viewing and editing MIFARE Classic .nfc files without access to an external device. Supports all card types supported by the Flipper: 0.3K (Mini), 1K, and 4K - with both 4 and 7 byte UIDs.
+
+## Features
+
+- Separation of card data by sector then by block
+- Editing the raw block contents of a card
+- Separate editing for special parts of the card (such as the UID and keys)
+- A block-by-block access condition decoder and editor
+- A check for the validity of the stored BCC byte and an option to correct it if it does not match the expected value (applies to 4 byte UID cards only)
+
+## Notes
+
+- If a card has been modified during emulation, thereby giving it a corresponding .shd file, the application will give you an option of which to edit. While a .shd file exists, that is what will be read by the main NFC application.
+- If the access condition check bits for a block are invalid, the application will display as such when viewing them, and correct them when they are edited.
+  - The values in brackets in the access bit viewer correspond to the expected value of each bit according to the check bits. They should be equal to the bit value that they follow.
+  - Keep in mind that if the check bits for a block are incorrect, it will disable the **entire sector** on a real card. This is not fully reflected by the decoded view, as it treats each block individually.
+- If Key B is readable in the sector trailer (this is the case in the default configuration), then it cannot be used to authenticate anything in the sector no matter what the access bits of each block are. This is not reflected in the decoded view.

BIN
mfc_editor/.catalog/screenshots/1.png


BIN
mfc_editor/.catalog/screenshots/2.png


BIN
mfc_editor/.catalog/screenshots/3.png


BIN
mfc_editor/.catalog/screenshots/4.png


BIN
mfc_editor/.catalog/screenshots/5.png


BIN
mfc_editor/.catalog/screenshots/6.png


+ 1 - 0
mfc_editor/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/TollyH/flipper-apps main mfc-editor

+ 19 - 0
mfc_editor/application.fam

@@ -0,0 +1,19 @@
+App(
+    appid="mfc_editor",
+    name="MIFARE Classic Editor",
+    apptype=FlipperAppType.EXTERNAL,
+    targets=["f7"],
+    entry_point="mfc_editor_app",
+    requires=[
+        "storage",
+        "gui",
+    ],
+    stack_size=4 * 1024,
+    fap_description="Application for viewing and editing MIFARE Classic .nfc files",
+    fap_version="1.0",
+    fap_icon="assets/app_icon_10px.png",
+    fap_category="NFC",
+    fap_icon_assets="assets",
+    fap_author="TollyH",
+    fap_weburl="https://github.com/TollyH/flipper-apps/tree/main/mfc-editor",
+)

BIN
mfc_editor/assets/DolphinSaved_92x58.png


BIN
mfc_editor/assets/Nfc_10px.png


BIN
mfc_editor/assets/app_icon_10px.png


+ 312 - 0
mfc_editor/mfc_editor_app.c

@@ -0,0 +1,312 @@
+#include "mfc_editor_app_i.h"
+
+const char* access_data_block_labels[8] = {
+    // C3, C2, C1
+    "Key A: Read, Write, Inc, Dec\nKey B: Read, Write, Inc, Dec", // 000
+    "Key A: Read\nKey B: Read, Write", // 001
+    "Key A: Read\nKey B: Read", // 010
+    "Key A: Read, Dec\nKey B: Read, Write, Inc, Dec", // 011
+    "Key A: Read, Dec\nKey B: Read, Dec", // 100
+    "Key A: No Access\nKey B: Read", // 101
+    "Key A: No Access\nKey B: Read, Write", // 110
+    "Key A: No Access\nKey B: No Access", // 111
+};
+
+const char* access_sector_trailer_labels[8] = {
+    // C3, C2, C1
+    "Key A: KA-W, AB-R, KB-RW\nKey B: No Access", // 000
+    "Key A: AB-R\nKey B: KA+KB-W, AB-R", // 001
+    "Key A: AB+KB-R\nKey B: No Access", // 010
+    "Key A: AB-R\nKey B: AB-R", // 011
+    "Key A: KA-W, AB+KB-RW\nKey B: No Access", // 100
+    "Key A: AB-R\nKey B: AB-RW", // 101
+    "Key A: AB-R\nKey B: KA+KB-W, AB-RW", // 110
+    "Key A: AB-R\nKey B: AB-R", // 111
+};
+
+bool mfc_editor_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    MfcEditorApp* instance = context;
+    return scene_manager_handle_custom_event(instance->scene_manager, event);
+}
+
+bool mfc_editor_app_back_event_callback(void* context) {
+    furi_assert(context);
+    MfcEditorApp* instance = context;
+    return scene_manager_handle_back_event(instance->scene_manager);
+}
+
+void mfc_editor_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    MfcEditorApp* instance = context;
+    scene_manager_handle_tick_event(instance->scene_manager);
+}
+
+MfcEditorApp* mfc_editor_app_alloc() {
+    MfcEditorApp* instance = malloc(sizeof(MfcEditorApp));
+
+    instance->view_dispatcher = view_dispatcher_alloc();
+    instance->scene_manager = scene_manager_alloc(&mfc_editor_scene_handlers, instance);
+    view_dispatcher_enable_queue(instance->view_dispatcher);
+    view_dispatcher_set_event_callback_context(instance->view_dispatcher, instance);
+    view_dispatcher_set_custom_event_callback(
+        instance->view_dispatcher, mfc_editor_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        instance->view_dispatcher, mfc_editor_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        instance->view_dispatcher, mfc_editor_app_tick_event_callback, 100);
+
+    instance->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(
+        instance->view_dispatcher, instance->gui, ViewDispatcherTypeFullscreen);
+
+    instance->storage = furi_record_open(RECORD_STORAGE);
+
+    instance->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    instance->mf_classic_data = mf_classic_alloc();
+    instance->file_path = furi_string_alloc_set(NFC_APP_FOLDER);
+
+    instance->data_view_header = furi_string_alloc();
+    instance->data_view_text = furi_string_alloc();
+
+    instance->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        instance->view_dispatcher, MfcEditorAppViewSubmenu, submenu_get_view(instance->submenu));
+
+    instance->popup = popup_alloc();
+    view_dispatcher_add_view(
+        instance->view_dispatcher, MfcEditorAppViewPopup, popup_get_view(instance->popup));
+
+    instance->dialog_ex = dialog_ex_alloc();
+    view_dispatcher_add_view(
+        instance->view_dispatcher,
+        MfcEditorAppViewDialogEx,
+        dialog_ex_get_view(instance->dialog_ex));
+
+    instance->byte_input = byte_input_alloc();
+    view_dispatcher_add_view(
+        instance->view_dispatcher,
+        MfcEditorAppViewByteInput,
+        byte_input_get_view(instance->byte_input));
+
+    return instance;
+}
+
+void mfc_editor_app_free(MfcEditorApp* instance) {
+    furi_assert(instance);
+
+    view_dispatcher_remove_view(instance->view_dispatcher, MfcEditorAppViewSubmenu);
+    submenu_free(instance->submenu);
+
+    view_dispatcher_remove_view(instance->view_dispatcher, MfcEditorAppViewPopup);
+    popup_free(instance->popup);
+
+    view_dispatcher_remove_view(instance->view_dispatcher, MfcEditorAppViewDialogEx);
+    dialog_ex_free(instance->dialog_ex);
+
+    view_dispatcher_remove_view(instance->view_dispatcher, MfcEditorAppViewByteInput);
+    byte_input_free(instance->byte_input);
+
+    view_dispatcher_free(instance->view_dispatcher);
+    scene_manager_free(instance->scene_manager);
+
+    furi_record_close(RECORD_GUI);
+    instance->gui = NULL;
+
+    furi_record_close(RECORD_STORAGE);
+    instance->storage = NULL;
+
+    furi_record_close(RECORD_DIALOGS);
+    instance->dialogs = NULL;
+
+    mf_classic_free(instance->mf_classic_data);
+    furi_string_free(instance->file_path);
+
+    furi_string_free(instance->data_view_header);
+    furi_string_free(instance->data_view_text);
+
+    free(instance);
+}
+
+MfcEditorPromptResponse mfc_editor_load_file(MfcEditorApp* instance, FuriString* file_path) {
+    furi_assert(instance);
+    furi_assert(file_path);
+
+    MfcEditorPromptResponse result = MfcEditorPromptResponseSuccess;
+
+    NfcDevice* nfc_device = nfc_device_alloc();
+
+    if(!nfc_device_load(nfc_device, furi_string_get_cstr(file_path))) {
+        result = MfcEditorPromptResponseFailure;
+        dialog_message_show_storage_error(instance->dialogs, "Cannot load\nkey file");
+    } else {
+        if(nfc_device_get_protocol(nfc_device) == NfcProtocolMfClassic) {
+            const MfClassicData* mf_classic_data =
+                nfc_device_get_data(nfc_device, NfcProtocolMfClassic);
+            mf_classic_copy(instance->mf_classic_data, mf_classic_data);
+            instance->is_unsaved_changes = false;
+        } else {
+            result = MfcEditorPromptResponseNotMfClassic;
+        }
+    }
+
+    nfc_device_free(nfc_device);
+
+    return result;
+}
+
+bool mfc_editor_save_file(MfcEditorApp* instance) {
+    furi_assert(instance);
+    furi_assert(instance->file_path);
+    furi_assert(instance->mf_classic_data);
+
+    NfcDevice* nfc_device = nfc_device_alloc();
+
+    nfc_device_set_data(nfc_device, NfcProtocolMfClassic, instance->mf_classic_data);
+
+    bool result = nfc_device_save(nfc_device, furi_string_get_cstr(instance->file_path));
+    if(!result) {
+        dialog_message_show_storage_error(instance->dialogs, "Cannot save\nkey file");
+    }
+
+    nfc_device_free(nfc_device);
+
+    return result;
+}
+
+static DialogMessageButton mfc_editor_prompt_should_load_shadow(MfcEditorApp* instance) {
+    DialogMessage* message = dialog_message_alloc();
+    dialog_message_set_header(message, "File has modifications", 63, 3, AlignCenter, AlignTop);
+    dialog_message_set_text(
+        message,
+        "Would you like to edit the\nmodified file (recommended)\nor the original file?",
+        63,
+        31,
+        AlignCenter,
+        AlignCenter);
+    dialog_message_set_buttons(message, "Original", NULL, "Modified");
+
+    DialogMessageButton message_button = dialog_message_show(instance->dialogs, message);
+
+    dialog_message_free(message);
+
+    return message_button;
+}
+
+static void mfc_editor_get_shadow_file_path(FuriString* file_path, FuriString* shadow_file_path) {
+    furi_assert(file_path);
+    furi_assert(shadow_file_path);
+
+    // Remove NFC extension from end of string then append shadow extension
+    furi_string_set_n(shadow_file_path, file_path, 0, furi_string_size(file_path) - 4);
+    furi_string_cat_printf(shadow_file_path, "%s", NFC_APP_SHADOW_EXTENSION);
+}
+
+static bool mfc_editor_file_has_shadow_file(MfcEditorApp* instance, FuriString* file_path) {
+    furi_assert(instance);
+    furi_assert(file_path);
+
+    FuriString* shadow_file_path = furi_string_alloc();
+    mfc_editor_get_shadow_file_path(file_path, shadow_file_path);
+    bool has_shadow_file =
+        storage_common_exists(instance->storage, furi_string_get_cstr(shadow_file_path));
+
+    furi_string_free(shadow_file_path);
+
+    return has_shadow_file;
+}
+
+MfcEditorPromptResponse mfc_editor_prompt_load_file(MfcEditorApp* instance) {
+    furi_assert(instance);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, NFC_APP_EXTENSION, &I_Nfc_10px);
+    browser_options.base_path = NFC_APP_FOLDER;
+    browser_options.hide_dot_files = true;
+
+    MfcEditorPromptResponse result = MfcEditorPromptResponseSuccess;
+    if(!dialog_file_browser_show(
+           instance->dialogs, instance->file_path, instance->file_path, &browser_options)) {
+        result = MfcEditorPromptResponseExitedFile;
+    } else {
+        if(mfc_editor_file_has_shadow_file(instance, instance->file_path)) {
+            DialogMessageButton message_button = mfc_editor_prompt_should_load_shadow(instance);
+
+            if(message_button == DialogMessageButtonRight) {
+                // User selected to use shadow file, so replace selected path with that path
+                FuriString* shadow_file_path = furi_string_alloc();
+                mfc_editor_get_shadow_file_path(instance->file_path, shadow_file_path);
+                furi_string_set(instance->file_path, shadow_file_path);
+                furi_string_free(shadow_file_path);
+            } else if(message_button == DialogMessageButtonBack) {
+                result = MfcEditorPromptResponseExitedShadow;
+            }
+        }
+
+        // Don't load the file if user was prompted for shadow file use but went back
+        if(result == MfcEditorPromptResponseSuccess) {
+            result = mfc_editor_load_file(instance, instance->file_path);
+        }
+    }
+
+    return result;
+}
+
+bool mfc_editor_warn_risky_operation(MfcEditorApp* instance) {
+    DialogMessage* message = dialog_message_alloc();
+    dialog_message_set_header(message, "Risky operation", 63, 3, AlignCenter, AlignTop);
+    dialog_message_set_text(
+        message,
+        "Changing this data may\ninhibit writing to the card\nor could brick the card.",
+        63,
+        31,
+        AlignCenter,
+        AlignCenter);
+    dialog_message_set_buttons(message, "Back", "Continue", NULL);
+
+    DialogMessageButton message_button = dialog_message_show(instance->dialogs, message);
+
+    dialog_message_free(message);
+
+    return message_button == DialogMessageButtonCenter;
+}
+
+MfcEditorSaveResponse mfc_editor_warn_unsaved_changes(MfcEditorApp* instance) {
+    DialogMessage* message = dialog_message_alloc();
+    dialog_message_set_header(message, "Unsaved changes", 63, 3, AlignCenter, AlignTop);
+    dialog_message_set_text(
+        message,
+        "Would you like to save?\nDiscarding your\nchanges is permanent.",
+        63,
+        31,
+        AlignCenter,
+        AlignCenter);
+    dialog_message_set_buttons(message, "Discrd", "Save", "Cancel");
+
+    DialogMessageButton message_button = dialog_message_show(instance->dialogs, message);
+
+    dialog_message_free(message);
+
+    if(message_button == DialogMessageButtonCenter) {
+        return MfcEditorSaveResponseSave;
+    } else if(message_button == DialogMessageButtonLeft) {
+        return MfcEditorSaveResponseDiscard;
+    } else {
+        return MfcEditorSaveResponseCancel;
+    }
+}
+
+int32_t mfc_editor_app(void* p) {
+    UNUSED(p);
+
+    MfcEditorApp* instance = mfc_editor_app_alloc();
+
+    scene_manager_next_scene(instance->scene_manager, MfcEditorSceneFileSelect);
+
+    view_dispatcher_run(instance->view_dispatcher);
+
+    mfc_editor_app_free(instance);
+
+    return 0;
+}

+ 3 - 0
mfc_editor/mfc_editor_app.h

@@ -0,0 +1,3 @@
+#pragma once
+
+typedef struct MfcEditorApp MfcEditorApp;

+ 146 - 0
mfc_editor/mfc_editor_app_i.h

@@ -0,0 +1,146 @@
+#pragma once
+
+#include <furi.h>
+
+#include <dialogs/dialogs.h>
+
+#include <gui/gui.h>
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+
+#include <gui/modules/byte_input.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/submenu.h>
+
+#include <nfc/nfc.h>
+#include <nfc/nfc_device.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+
+#include <storage/storage.h>
+
+#include "mfc_editor_app.h"
+#include "mfc_editor_icons.h"
+#include "scenes/mfc_editor_scene.h"
+
+#define TAG "MFCEditor"
+
+#define NFC_APP_FOLDER ANY_PATH("nfc")
+#define NFC_APP_EXTENSION ".nfc"
+#define NFC_APP_SHADOW_EXTENSION ".shd"
+
+enum MfcEditorCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    MfcEditorCustomEventReserved = 100,
+
+    MfcEditorCustomEventViewExit,
+    MfcEditorCustomEventSave,
+};
+
+typedef struct {
+    uint8_t bits : 3;
+    uint8_t check_bits : 3;
+} MfcEditorAccessBits;
+
+struct MfcEditorApp {
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+
+    Gui* gui;
+    Storage* storage;
+    DialogsApp* dialogs;
+
+    Submenu* submenu;
+    Popup* popup;
+    DialogEx* dialog_ex;
+    ByteInput* byte_input;
+
+    MfClassicData* mf_classic_data;
+
+    FuriString* file_path;
+
+    bool is_unsaved_changes;
+
+    uint8_t current_sector;
+    uint8_t current_block;
+
+    // DialogEx doesn't copy the strings given to it, so we need these
+    FuriString* data_view_header;
+    FuriString* data_view_text;
+
+    uint8_t* edit_buffer;
+    MfcEditorAccessBits access_bits_edit;
+};
+
+typedef enum {
+    MfcEditorAppViewSubmenu,
+    MfcEditorAppViewPopup,
+    MfcEditorAppViewDialogEx,
+    MfcEditorAppViewByteInput,
+} MfcEditorAppView;
+
+typedef enum {
+    // Generic
+    MfcEditorPromptResponseSuccess,
+    MfcEditorPromptResponseFailure,
+
+    MfcEditorPromptResponseNotMfClassic,
+
+    // Backed out of a prompt
+    MfcEditorPromptResponseExitedFile,
+    MfcEditorPromptResponseExitedShadow,
+} MfcEditorPromptResponse;
+
+typedef enum {
+    MfcEditorSaveResponseSave,
+    MfcEditorSaveResponseDiscard,
+    MfcEditorSaveResponseCancel,
+} MfcEditorSaveResponse;
+
+typedef enum {
+    MfcEditorBlockViewNormal,
+
+    // Special options - Sector 0 only
+    MfcEditorBlockViewUID,
+    MfcEditorBlockViewBCC,
+    MfcEditorBlockViewManufacturerBytes,
+
+    // Special options - All sectors
+    MfcEditorBlockViewKeyA,
+    MfcEditorBlockViewKeyB,
+    MfcEditorBlockViewAccessBits,
+    MfcEditorBlockViewUserByte,
+} MfcEditorBlockView;
+
+// Main loading methods
+
+MfcEditorPromptResponse mfc_editor_prompt_load_file(MfcEditorApp* instance);
+
+MfcEditorPromptResponse mfc_editor_load_file(MfcEditorApp* instance, FuriString* file_path);
+
+bool mfc_editor_save_file(MfcEditorApp* instance);
+
+// Warning dialogs
+
+bool mfc_editor_warn_risky_operation(MfcEditorApp* instance);
+
+MfcEditorSaveResponse mfc_editor_warn_unsaved_changes(MfcEditorApp* instance);
+
+// Helper methods
+
+uint8_t mfc_editor_calculate_uid_bcc(uint8_t* uid, uint8_t uid_len);
+
+MfcEditorAccessBits mfc_editor_get_block_access_bits(const MfClassicData* data, uint8_t block_num);
+
+void mfc_editor_set_block_access_bits(
+    MfClassicData* data,
+    uint8_t block_num,
+    const MfcEditorAccessBits* access_bits);
+
+void mfc_editor_furi_string_render_bytes(FuriString* string, const uint8_t* data, uint8_t length);
+
+// Strings
+
+extern const char* access_data_block_labels[8];
+
+extern const char* access_sector_trailer_labels[8];

+ 102 - 0
mfc_editor/mfc_editor_helpers.c

@@ -0,0 +1,102 @@
+#include "mfc_editor_app_i.h"
+
+uint8_t mfc_editor_calculate_uid_bcc(uint8_t* uid, uint8_t uid_len) {
+    furi_check(uid_len > 0);
+
+    uint8_t bcc = uid[0];
+    for(int i = 1; i < uid_len; i++) {
+        bcc ^= uid[i];
+    }
+
+    return bcc;
+}
+
+MfcEditorAccessBits
+    mfc_editor_get_block_access_bits(const MfClassicData* data, uint8_t block_num) {
+    MfcEditorAccessBits result;
+
+    uint8_t sector_num = mf_classic_get_sector_by_block(block_num);
+    MfClassicSectorTrailer* trailer = mf_classic_get_sector_trailer_by_sector(data, sector_num);
+    uint8_t relative_block_num = block_num - mf_classic_get_first_block_num_of_sector(sector_num);
+    if(sector_num >= 32) {
+        // 4K large sector - access bits affect range of blocks
+        relative_block_num /= 5;
+    }
+
+    uint8_t access_byte_1 = trailer->access_bits.data[0];
+    uint8_t access_byte_2 = trailer->access_bits.data[1];
+    uint8_t access_byte_3 = trailer->access_bits.data[2];
+
+    result.bits = (FURI_BIT(access_byte_3, 4 + relative_block_num) << 2) |
+                  (FURI_BIT(access_byte_3, relative_block_num) << 1) |
+                  (FURI_BIT(access_byte_2, 4 + relative_block_num));
+    result.check_bits = (FURI_BIT(access_byte_2, relative_block_num) << 2) |
+                        (FURI_BIT(access_byte_1, 4 + relative_block_num) << 1) |
+                        (FURI_BIT(access_byte_1, relative_block_num));
+    // Check bits are inverted in storage, flip them to match actual bits
+    result.check_bits ^= 0b111;
+
+    return result;
+}
+
+void mfc_editor_set_block_access_bits(
+    MfClassicData* data,
+    uint8_t block_num,
+    const MfcEditorAccessBits* access_bits) {
+    uint8_t sector_num = mf_classic_get_sector_by_block(block_num);
+    MfClassicSectorTrailer* trailer = mf_classic_get_sector_trailer_by_sector(data, sector_num);
+    uint8_t relative_block_num = block_num - mf_classic_get_first_block_num_of_sector(sector_num);
+    if(sector_num >= 32) {
+        // 4K large sector - access bits affect range of blocks
+        relative_block_num /= 5;
+    }
+
+    uint8_t* access_byte_1 = &trailer->access_bits.data[0];
+    uint8_t* access_byte_2 = &trailer->access_bits.data[1];
+    uint8_t* access_byte_3 = &trailer->access_bits.data[2];
+
+    if(FURI_BIT(access_bits->bits, 0)) {
+        FURI_BIT_SET(*access_byte_2, 4 + relative_block_num);
+    } else {
+        FURI_BIT_CLEAR(*access_byte_2, 4 + relative_block_num);
+    }
+
+    if(FURI_BIT(access_bits->bits, 1)) {
+        FURI_BIT_SET(*access_byte_3, relative_block_num);
+    } else {
+        FURI_BIT_CLEAR(*access_byte_3, relative_block_num);
+    }
+
+    if(FURI_BIT(access_bits->bits, 2)) {
+        FURI_BIT_SET(*access_byte_3, 4 + relative_block_num);
+    } else {
+        FURI_BIT_CLEAR(*access_byte_3, 4 + relative_block_num);
+    }
+
+    // Check bits are the inverse of the the actual bits
+    if(FURI_BIT(access_bits->check_bits, 0)) {
+        FURI_BIT_CLEAR(*access_byte_1, relative_block_num);
+    } else {
+        FURI_BIT_SET(*access_byte_1, relative_block_num);
+    }
+
+    if(FURI_BIT(access_bits->check_bits, 1)) {
+        FURI_BIT_CLEAR(*access_byte_1, 4 + relative_block_num);
+    } else {
+        FURI_BIT_SET(*access_byte_1, 4 + relative_block_num);
+    }
+
+    if(FURI_BIT(access_bits->check_bits, 2)) {
+        FURI_BIT_CLEAR(*access_byte_2, relative_block_num);
+    } else {
+        FURI_BIT_SET(*access_byte_2, relative_block_num);
+    }
+}
+
+void mfc_editor_furi_string_render_bytes(FuriString* string, const uint8_t* data, uint8_t length) {
+    for(uint8_t i = 0; i < length - 1; i++) {
+        furi_string_cat_printf(string, "%02X ", data[i]);
+    }
+    // Don't add a trailing space
+    furi_string_cat_printf(string, "%02X", data[length - 1]);
+}

+ 30 - 0
mfc_editor/scenes/mfc_editor_scene.c

@@ -0,0 +1,30 @@
+#include "mfc_editor_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const mfc_editor_on_enter_handlers[])(void*) = {
+#include "mfc_editor_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const mfc_editor_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "mfc_editor_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const mfc_editor_on_exit_handlers[])(void* context) = {
+#include "mfc_editor_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers mfc_editor_scene_handlers = {
+    .on_enter_handlers = mfc_editor_on_enter_handlers,
+    .on_event_handlers = mfc_editor_on_event_handlers,
+    .on_exit_handlers = mfc_editor_on_exit_handlers,
+    .scene_num = MfcEditorSceneNum,
+};

+ 29 - 0
mfc_editor/scenes/mfc_editor_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) MfcEditorScene##id,
+typedef enum {
+#include "mfc_editor_scene_config.h"
+    MfcEditorSceneNum,
+} MfcEditorScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers mfc_editor_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "mfc_editor_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "mfc_editor_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "mfc_editor_scene_config.h"
+#undef ADD_SCENE

+ 142 - 0
mfc_editor/scenes/mfc_editor_scene_block_select.c

@@ -0,0 +1,142 @@
+#include "../mfc_editor_app_i.h"
+
+enum SubmenuIndex {
+    // Reserve first indices for opening normal block
+    SubmenuIndexReserved = MF_CLASSIC_TOTAL_BLOCKS_MAX,
+
+    // Special options - Sector 0 only
+    SubmenuIndexUID,
+    SubmenuIndexBCC,
+    SubmenuIndexManufacturerBytes,
+
+    // Special options - All sectors
+    SubmenuIndexKeyA,
+    SubmenuIndexKeyB,
+    SubmenuIndexAccessBits,
+    SubmenuIndexUserByte,
+};
+
+void mfc_editor_scene_block_select_submenu_callback(void* context, uint32_t index) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, index);
+}
+
+void mfc_editor_scene_block_select_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    Submenu* submenu = instance->submenu;
+
+    uint8_t first_block = mf_classic_get_first_block_num_of_sector(instance->current_sector);
+    uint8_t block_num = mf_classic_get_blocks_num_in_sector(instance->current_sector);
+
+    FuriString* label = furi_string_alloc();
+    for(uint8_t i = 0; i < block_num; i++) {
+        uint8_t block_index = first_block + i;
+        furi_string_printf(label, "Block %u", block_index);
+        submenu_add_item(
+            submenu,
+            furi_string_get_cstr(label),
+            block_index,
+            mfc_editor_scene_block_select_submenu_callback,
+            instance);
+    }
+    furi_string_free(label);
+
+    if(instance->current_sector == 0) {
+        submenu_add_item(
+            submenu,
+            "UID",
+            SubmenuIndexUID,
+            mfc_editor_scene_block_select_submenu_callback,
+            instance);
+        if(instance->mf_classic_data->iso14443_3a_data->uid_len == 4) {
+            // 7-byte UID cards don't store a BCC byte
+            submenu_add_item(
+                submenu,
+                "BCC",
+                SubmenuIndexBCC,
+                mfc_editor_scene_block_select_submenu_callback,
+                instance);
+        }
+        submenu_add_item(
+            submenu,
+            "Manufacturer Bytes",
+            SubmenuIndexManufacturerBytes,
+            mfc_editor_scene_block_select_submenu_callback,
+            instance);
+    }
+
+    submenu_add_item(
+        submenu,
+        "Key A",
+        SubmenuIndexKeyA,
+        mfc_editor_scene_block_select_submenu_callback,
+        instance);
+    submenu_add_item(
+        submenu,
+        "Key B",
+        SubmenuIndexKeyB,
+        mfc_editor_scene_block_select_submenu_callback,
+        instance);
+    submenu_add_item(
+        submenu,
+        "Access Bits",
+        SubmenuIndexAccessBits,
+        mfc_editor_scene_block_select_submenu_callback,
+        instance);
+    submenu_add_item(
+        submenu,
+        "User Byte",
+        SubmenuIndexUserByte,
+        mfc_editor_scene_block_select_submenu_callback,
+        instance);
+
+    submenu_set_selected_item(
+        submenu,
+        scene_manager_get_scene_state(instance->scene_manager, MfcEditorSceneBlockSelect));
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewSubmenu);
+}
+
+bool mfc_editor_scene_block_select_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(
+            instance->scene_manager, MfcEditorSceneBlockSelect, event.event);
+
+        MfcEditorBlockView block_view;
+        if(event.event == SubmenuIndexUID) {
+            block_view = MfcEditorBlockViewUID;
+        } else if(event.event == SubmenuIndexBCC) {
+            block_view = MfcEditorBlockViewBCC;
+        } else if(event.event == SubmenuIndexManufacturerBytes) {
+            block_view = MfcEditorBlockViewManufacturerBytes;
+        } else if(event.event == SubmenuIndexKeyA) {
+            block_view = MfcEditorBlockViewKeyA;
+        } else if(event.event == SubmenuIndexKeyB) {
+            block_view = MfcEditorBlockViewKeyB;
+        } else if(event.event == SubmenuIndexAccessBits) {
+            block_view = MfcEditorBlockViewAccessBits;
+            instance->current_block =
+                mf_classic_get_first_block_num_of_sector(instance->current_sector);
+        } else if(event.event == SubmenuIndexUserByte) {
+            block_view = MfcEditorBlockViewUserByte;
+        } else {
+            block_view = MfcEditorBlockViewNormal;
+            instance->current_block = event.event;
+        }
+
+        scene_manager_set_scene_state(instance->scene_manager, MfcEditorSceneDataView, block_view);
+        scene_manager_next_scene(instance->scene_manager, MfcEditorSceneDataView);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_block_select_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    submenu_reset(instance->submenu);
+}

+ 8 - 0
mfc_editor/scenes/mfc_editor_scene_config.h

@@ -0,0 +1,8 @@
+ADD_SCENE(mfc_editor, file_select, FileSelect)
+ADD_SCENE(mfc_editor, invalid_file, InvalidFile)
+ADD_SCENE(mfc_editor, sector_select, SectorSelect)
+ADD_SCENE(mfc_editor, block_select, BlockSelect)
+ADD_SCENE(mfc_editor, data_view, DataView)
+ADD_SCENE(mfc_editor, data_edit, DataEdit)
+ADD_SCENE(mfc_editor, data_edit_access_bits, DataEditAccessBits)
+ADD_SCENE(mfc_editor, save_success, SaveSuccess)

+ 205 - 0
mfc_editor/scenes/mfc_editor_scene_data_edit.c

@@ -0,0 +1,205 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_data_edit_block_view_save_callback(void* context) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, MfcEditorCustomEventSave);
+}
+
+void mfc_editor_scene_data_edit_block_view_changed_callback(void* context) {
+    UNUSED(context);
+}
+
+void mfc_editor_scene_data_edit_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    ByteInput* byte_input = instance->byte_input;
+
+    MfClassicData* mf_classic_data = instance->mf_classic_data;
+    Iso14443_3aData* iso14443_3a_data = mf_classic_data->iso14443_3a_data;
+
+    MfcEditorBlockView block_view =
+        scene_manager_get_scene_state(instance->scene_manager, MfcEditorSceneDataView);
+
+    if(block_view == MfcEditorBlockViewUID) {
+        byte_input_set_header_text(byte_input, "Enter new UID");
+
+        instance->edit_buffer = malloc(iso14443_3a_data->uid_len);
+        memcpy(instance->edit_buffer, iso14443_3a_data->uid, iso14443_3a_data->uid_len);
+
+        byte_input_set_result_callback(
+            byte_input,
+            mfc_editor_scene_data_edit_block_view_save_callback,
+            mfc_editor_scene_data_edit_block_view_changed_callback,
+            instance,
+            instance->edit_buffer,
+            iso14443_3a_data->uid_len);
+    } else if(block_view == MfcEditorBlockViewManufacturerBytes) {
+        byte_input_set_header_text(byte_input, "Enter new Manufacturer Bytes");
+
+        // Skip BCC byte (not present on 7B UID cards)
+        bool skip_byte = iso14443_3a_data->uid_len == 4;
+        uint8_t byte_num = MF_CLASSIC_BLOCK_SIZE - iso14443_3a_data->uid_len - skip_byte;
+
+        instance->edit_buffer = malloc(byte_num);
+        memcpy(
+            instance->edit_buffer,
+            mf_classic_data->block[0].data + iso14443_3a_data->uid_len + skip_byte,
+            byte_num);
+
+        byte_input_set_result_callback(
+            byte_input,
+            mfc_editor_scene_data_edit_block_view_save_callback,
+            mfc_editor_scene_data_edit_block_view_changed_callback,
+            instance,
+            instance->edit_buffer,
+            byte_num);
+    } else if(block_view == MfcEditorBlockViewKeyA) {
+        byte_input_set_header_text(byte_input, "Enter new Key A");
+
+        instance->edit_buffer = malloc(MF_CLASSIC_KEY_SIZE);
+        memcpy(
+            instance->edit_buffer,
+            mf_classic_get_sector_trailer_by_sector(mf_classic_data, instance->current_sector)
+                ->key_a.data,
+            MF_CLASSIC_KEY_SIZE);
+
+        byte_input_set_result_callback(
+            byte_input,
+            mfc_editor_scene_data_edit_block_view_save_callback,
+            mfc_editor_scene_data_edit_block_view_changed_callback,
+            instance,
+            instance->edit_buffer,
+            MF_CLASSIC_KEY_SIZE);
+    } else if(block_view == MfcEditorBlockViewKeyB) {
+        byte_input_set_header_text(byte_input, "Enter new Key B");
+
+        instance->edit_buffer = malloc(MF_CLASSIC_KEY_SIZE);
+        memcpy(
+            instance->edit_buffer,
+            mf_classic_get_sector_trailer_by_sector(mf_classic_data, instance->current_sector)
+                ->key_b.data,
+            MF_CLASSIC_KEY_SIZE);
+
+        byte_input_set_result_callback(
+            byte_input,
+            mfc_editor_scene_data_edit_block_view_save_callback,
+            mfc_editor_scene_data_edit_block_view_changed_callback,
+            instance,
+            instance->edit_buffer,
+            MF_CLASSIC_KEY_SIZE);
+    } else if(block_view == MfcEditorBlockViewUserByte) {
+        byte_input_set_header_text(byte_input, "Enter new User Byte");
+
+        instance->edit_buffer = malloc(1);
+        instance->edit_buffer[0] =
+            mf_classic_get_sector_trailer_by_sector(mf_classic_data, instance->current_sector)
+                ->access_bits.data[3];
+
+        byte_input_set_result_callback(
+            byte_input,
+            mfc_editor_scene_data_edit_block_view_save_callback,
+            mfc_editor_scene_data_edit_block_view_changed_callback,
+            instance,
+            instance->edit_buffer,
+            1);
+    } else {
+        byte_input_set_header_text(byte_input, "Enter new block content");
+
+        instance->edit_buffer = malloc(MF_CLASSIC_BLOCK_SIZE);
+        memcpy(
+            instance->edit_buffer,
+            mf_classic_data->block[instance->current_block].data,
+            MF_CLASSIC_BLOCK_SIZE);
+
+        byte_input_set_result_callback(
+            byte_input,
+            mfc_editor_scene_data_edit_block_view_save_callback,
+            mfc_editor_scene_data_edit_block_view_changed_callback,
+            instance,
+            instance->edit_buffer,
+            MF_CLASSIC_BLOCK_SIZE);
+    }
+
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewByteInput);
+}
+
+bool mfc_editor_scene_data_edit_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    MfClassicData* mf_classic_data = instance->mf_classic_data;
+    Iso14443_3aData* iso14443_3a_data = mf_classic_data->iso14443_3a_data;
+
+    MfcEditorBlockView block_view =
+        scene_manager_get_scene_state(instance->scene_manager, MfcEditorSceneDataView);
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MfcEditorCustomEventSave) {
+            if(block_view == MfcEditorBlockViewNormal) {
+                memcpy(
+                    mf_classic_data->block[instance->current_block].data,
+                    instance->edit_buffer,
+                    MF_CLASSIC_BLOCK_SIZE);
+                if(instance->current_block == 0) {
+                    // UID needs to be equal to the first bytes in block 0
+                    memcpy(
+                        iso14443_3a_data->uid,
+                        mf_classic_data->block[0].data,
+                        iso14443_3a_data->uid_len);
+                }
+            } else if(block_view == MfcEditorBlockViewUID) {
+                memcpy(iso14443_3a_data->uid, instance->edit_buffer, iso14443_3a_data->uid_len);
+                // UID is also the first bytes in block 0
+                memcpy(
+                    mf_classic_data->block[0].data,
+                    iso14443_3a_data->uid,
+                    iso14443_3a_data->uid_len);
+                // 4-byte UID cards need the BCC byte set to correct value
+                if(iso14443_3a_data->uid_len == 4) {
+                    mf_classic_data->block[0].data[4] = mfc_editor_calculate_uid_bcc(
+                        iso14443_3a_data->uid, iso14443_3a_data->uid_len);
+                }
+            } else if(block_view == MfcEditorBlockViewManufacturerBytes) {
+                // Skip BCC byte (not present on 7B UID cards)
+                bool skip_byte = iso14443_3a_data->uid_len == 4;
+                uint8_t byte_num = MF_CLASSIC_BLOCK_SIZE - iso14443_3a_data->uid_len - skip_byte;
+
+                memcpy(
+                    mf_classic_data->block[0].data + iso14443_3a_data->uid_len + skip_byte,
+                    instance->edit_buffer,
+                    byte_num);
+            } else if(block_view == MfcEditorBlockViewKeyA) {
+                memcpy(
+                    mf_classic_get_sector_trailer_by_sector(
+                        mf_classic_data, instance->current_sector)
+                        ->key_a.data,
+                    instance->edit_buffer,
+                    MF_CLASSIC_KEY_SIZE);
+            } else if(block_view == MfcEditorBlockViewKeyB) {
+                memcpy(
+                    mf_classic_get_sector_trailer_by_sector(
+                        mf_classic_data, instance->current_sector)
+                        ->key_b.data,
+                    instance->edit_buffer,
+                    MF_CLASSIC_KEY_SIZE);
+            } else if(block_view == MfcEditorBlockViewUserByte) {
+                mf_classic_get_sector_trailer_by_sector(mf_classic_data, instance->current_sector)
+                    ->access_bits.data[3] = instance->edit_buffer[0];
+            }
+            instance->is_unsaved_changes = true;
+            scene_manager_previous_scene(instance->scene_manager);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_data_edit_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    if(instance->edit_buffer != NULL) {
+        free(instance->edit_buffer);
+        instance->edit_buffer = NULL;
+    }
+}

+ 117 - 0
mfc_editor/scenes/mfc_editor_scene_data_edit_access_bits.c

@@ -0,0 +1,117 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_data_edit_access_bits_dialog_ex_callback(
+    DialogExResult result,
+    void* context) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, result);
+}
+
+void mfc_editor_scene_data_edit_access_bits_update_display(MfcEditorApp* instance) {
+    DialogEx* dialog_ex = instance->dialog_ex;
+
+    uint8_t sector_trailer_num =
+        mf_classic_get_sector_trailer_num_by_sector(instance->current_sector);
+
+    furi_string_printf(
+        instance->data_view_text,
+        "C1: %i, C2: %i, C3: %i\n",
+        FURI_BIT(instance->access_bits_edit.bits, 0),
+        FURI_BIT(instance->access_bits_edit.bits, 1),
+        FURI_BIT(instance->access_bits_edit.bits, 2));
+
+    if(instance->current_block == sector_trailer_num) {
+        furi_string_cat(
+            instance->data_view_text,
+            access_sector_trailer_labels[instance->access_bits_edit.bits]);
+    } else {
+        furi_string_cat(
+            instance->data_view_text, access_data_block_labels[instance->access_bits_edit.bits]);
+    }
+
+    dialog_ex_set_text(
+        dialog_ex,
+        furi_string_get_cstr(instance->data_view_text),
+        63,
+        31,
+        AlignCenter,
+        AlignCenter);
+}
+
+void mfc_editor_scene_data_edit_access_bits_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    DialogEx* dialog_ex = instance->dialog_ex;
+
+    dialog_ex_set_context(instance->dialog_ex, instance);
+
+    if(instance->current_sector >= 32 && !mf_classic_is_sector_trailer(instance->current_block)) {
+        // 4K large sector - access bits affect range of blocks
+        uint8_t sector_start_num =
+            mf_classic_get_first_block_num_of_sector(instance->current_sector);
+        // Relative to the sector start, round down to multiple of 5
+        uint8_t relative_block_num = (instance->current_block - sector_start_num) / 5 * 5;
+        furi_string_printf(
+            instance->data_view_header,
+            "Edit B. %u-%u Access",
+            sector_start_num + relative_block_num,
+            sector_start_num + relative_block_num + 4);
+    } else {
+        furi_string_printf(
+            instance->data_view_header, "Edit Block %u Access", instance->current_block);
+    }
+
+    dialog_ex_set_header(
+        dialog_ex, furi_string_get_cstr(instance->data_view_header), 63, 3, AlignCenter, AlignTop);
+
+    instance->access_bits_edit =
+        mfc_editor_get_block_access_bits(instance->mf_classic_data, instance->current_block);
+
+    if(instance->access_bits_edit.bits != instance->access_bits_edit.check_bits) {
+        // If access check bits don't match, make them match now
+        instance->access_bits_edit.check_bits = instance->access_bits_edit.bits;
+    }
+
+    mfc_editor_scene_data_edit_access_bits_update_display(instance);
+
+    dialog_ex_set_center_button_text(dialog_ex, "Save");
+    dialog_ex_set_left_button_text(dialog_ex, "Prev");
+    dialog_ex_set_right_button_text(dialog_ex, "Next");
+
+    dialog_ex_set_result_callback(
+        dialog_ex, mfc_editor_scene_data_edit_access_bits_dialog_ex_callback);
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewDialogEx);
+}
+
+bool mfc_editor_scene_data_edit_access_bits_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == DialogExResultLeft) {
+            instance->access_bits_edit.bits--;
+            instance->access_bits_edit.check_bits = instance->access_bits_edit.bits;
+            mfc_editor_scene_data_edit_access_bits_update_display(instance);
+            consumed = true;
+        } else if(event.event == DialogExResultRight) {
+            instance->access_bits_edit.bits++;
+            instance->access_bits_edit.check_bits = instance->access_bits_edit.bits;
+            mfc_editor_scene_data_edit_access_bits_update_display(instance);
+            consumed = true;
+        } else if(event.event == DialogExResultCenter) {
+            mfc_editor_set_block_access_bits(
+                instance->mf_classic_data, instance->current_block, &instance->access_bits_edit);
+            instance->is_unsaved_changes = true;
+            scene_manager_previous_scene(instance->scene_manager);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_data_edit_access_bits_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    dialog_ex_reset(instance->dialog_ex);
+}

+ 339 - 0
mfc_editor/scenes/mfc_editor_scene_data_view.c

@@ -0,0 +1,339 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_data_view_dialog_ex_callback(DialogExResult result, void* context) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, result);
+}
+
+void mfc_editor_scene_data_view_update_display(MfcEditorApp* instance) {
+    DialogEx* dialog_ex = instance->dialog_ex;
+
+    dialog_ex_reset(instance->dialog_ex);
+    dialog_ex_set_context(instance->dialog_ex, instance);
+    dialog_ex_set_result_callback(
+        instance->dialog_ex, mfc_editor_scene_data_view_dialog_ex_callback);
+
+    MfcEditorBlockView block_view =
+        scene_manager_get_scene_state(instance->scene_manager, MfcEditorSceneDataView);
+
+    MfClassicData* mf_classic_data = instance->mf_classic_data;
+    Iso14443_3aData* iso14443_3a_data = mf_classic_data->iso14443_3a_data;
+
+    furi_string_reset(instance->data_view_text);
+
+    if(block_view == MfcEditorBlockViewUID) {
+        dialog_ex_set_header(dialog_ex, "UID", 63, 3, AlignCenter, AlignTop);
+
+        mfc_editor_furi_string_render_bytes(
+            instance->data_view_text, iso14443_3a_data->uid, iso14443_3a_data->uid_len);
+
+        if(memcmp(
+               iso14443_3a_data->uid, mf_classic_data->block[0].data, iso14443_3a_data->uid_len)) {
+            // ISO-14443 UID does not match first bytes of block 0
+            furi_string_cat_printf(instance->data_view_text, "\nBlock 0 does not match UID!\n(");
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text,
+                mf_classic_data->block[0].data,
+                iso14443_3a_data->uid_len);
+            furi_string_push_back(instance->data_view_text, ')');
+            dialog_ex_set_center_button_text(dialog_ex, "Fix");
+        }
+
+        if(mf_classic_is_block_read(mf_classic_data, 0)) {
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        }
+    } else if(block_view == MfcEditorBlockViewBCC) {
+        dialog_ex_set_header(dialog_ex, "Block Check Character", 63, 3, AlignCenter, AlignTop);
+
+        uint8_t stored_bcc = mf_classic_data->block[0].data[4];
+        uint8_t calculated_bcc =
+            mfc_editor_calculate_uid_bcc(iso14443_3a_data->uid, iso14443_3a_data->uid_len);
+
+        if(mf_classic_is_block_read(mf_classic_data, 0)) {
+            furi_string_printf(
+                instance->data_view_text,
+                "Stored BCC: %02X\nActual BCC: %02X",
+                stored_bcc,
+                calculated_bcc);
+
+            if(stored_bcc != calculated_bcc) {
+                furi_string_cat(instance->data_view_text, "\n(Mismatch!)");
+                dialog_ex_set_center_button_text(dialog_ex, "Fix");
+            }
+        } else {
+            furi_string_printf(
+                instance->data_view_text,
+                "Actual BCC: %02X\nStored BCC is unavailable\nas Block 0 has not been read.",
+                calculated_bcc);
+        }
+    } else if(block_view == MfcEditorBlockViewManufacturerBytes) {
+        dialog_ex_set_header(dialog_ex, "Manufacturer Bytes", 63, 3, AlignCenter, AlignTop);
+
+        if(mf_classic_is_block_read(mf_classic_data, 0)) {
+            // Skip BCC byte (not present on 7B UID cards)
+            bool skip_byte = iso14443_3a_data->uid_len == 4;
+            uint8_t start_index = iso14443_3a_data->uid_len + skip_byte;
+            uint8_t byte_num = MF_CLASSIC_BLOCK_SIZE - iso14443_3a_data->uid_len - skip_byte;
+            uint8_t line_len = byte_num / 2;
+
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text, mf_classic_data->block[0].data + start_index, line_len);
+            furi_string_push_back(instance->data_view_text, '\n');
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text,
+                mf_classic_data->block[0].data + start_index + line_len,
+                byte_num - line_len);
+
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        } else {
+            furi_string_set(
+                instance->data_view_text, "Data unavailable.\nBlock 0 has not been read.");
+        }
+    } else if(block_view == MfcEditorBlockViewKeyA) {
+        dialog_ex_set_header(dialog_ex, "Key A", 63, 3, AlignCenter, AlignTop);
+
+        if(mf_classic_is_key_found(mf_classic_data, instance->current_sector, MfClassicKeyTypeA)) {
+            MfClassicSectorTrailer* sector_trailer =
+                mf_classic_get_sector_trailer_by_sector(mf_classic_data, instance->current_sector);
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text, sector_trailer->key_a.data, MF_CLASSIC_KEY_SIZE);
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        } else {
+            furi_string_set(
+                instance->data_view_text, "Key A has not been found\nfor this sector.");
+        }
+    } else if(block_view == MfcEditorBlockViewKeyB) {
+        dialog_ex_set_header(dialog_ex, "Key B", 63, 3, AlignCenter, AlignTop);
+
+        if(mf_classic_is_key_found(mf_classic_data, instance->current_sector, MfClassicKeyTypeB)) {
+            MfClassicSectorTrailer* sector_trailer =
+                mf_classic_get_sector_trailer_by_sector(mf_classic_data, instance->current_sector);
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text, sector_trailer->key_b.data, MF_CLASSIC_KEY_SIZE);
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        } else {
+            furi_string_set(
+                instance->data_view_text, "Key B has not been found\nfor this sector.");
+        }
+    } else if(block_view == MfcEditorBlockViewAccessBits) {
+        uint8_t sector_trailer_num =
+            mf_classic_get_sector_trailer_num_by_sector(instance->current_sector);
+
+        if(mf_classic_is_block_read(mf_classic_data, sector_trailer_num)) {
+            furi_string_printf(
+                instance->data_view_header, "Access Bits (Block %u)", instance->current_block);
+            dialog_ex_set_header(
+                dialog_ex,
+                furi_string_get_cstr(instance->data_view_header),
+                63,
+                3,
+                AlignCenter,
+                AlignTop);
+
+            MfcEditorAccessBits access_bits =
+                mfc_editor_get_block_access_bits(mf_classic_data, instance->current_block);
+
+            furi_string_printf(
+                instance->data_view_text,
+                "C1: %i(%i), C2: %i(%i), C3: %i(%i)\n",
+                FURI_BIT(access_bits.bits, 0),
+                FURI_BIT(access_bits.check_bits, 0),
+                FURI_BIT(access_bits.bits, 1),
+                FURI_BIT(access_bits.check_bits, 1),
+                FURI_BIT(access_bits.bits, 2),
+                FURI_BIT(access_bits.check_bits, 2));
+
+            if(access_bits.bits != access_bits.check_bits) {
+                furi_string_cat(
+                    instance->data_view_text,
+                    "Access Bits are invalid.\nEntire sector inaccessible.");
+            } else if(instance->current_block == sector_trailer_num) {
+                furi_string_cat(
+                    instance->data_view_text, access_sector_trailer_labels[access_bits.bits]);
+            } else {
+                furi_string_cat(
+                    instance->data_view_text, access_data_block_labels[access_bits.bits]);
+            }
+
+            dialog_ex_set_center_button_text(dialog_ex, "Next");
+            dialog_ex_set_left_button_text(dialog_ex, "Prev");
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        } else {
+            dialog_ex_set_header(dialog_ex, "Access Bits", 63, 3, AlignCenter, AlignTop);
+            furi_string_printf(
+                instance->data_view_text,
+                "Access Bits unavailable.\nBlock %u has not been read.",
+                sector_trailer_num);
+        }
+    } else if(block_view == MfcEditorBlockViewUserByte) {
+        dialog_ex_set_header(dialog_ex, "User Byte", 63, 3, AlignCenter, AlignTop);
+
+        uint8_t sector_trailer_num =
+            mf_classic_get_sector_trailer_num_by_sector(instance->current_sector);
+
+        if(mf_classic_is_block_read(mf_classic_data, sector_trailer_num)) {
+            furi_string_printf(
+                instance->data_view_text,
+                "Free byte between\nAccess Bits and Key B:\n%02X",
+                mf_classic_data->block[sector_trailer_num].data[9]);
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        } else {
+            furi_string_printf(
+                instance->data_view_text,
+                "Data unavailable.\nBlock %u has not been read.",
+                sector_trailer_num);
+        }
+    } else {
+        uint8_t current_block = instance->current_block;
+        furi_string_printf(instance->data_view_header, "Block %u Data", current_block);
+        dialog_ex_set_header(
+            dialog_ex,
+            furi_string_get_cstr(instance->data_view_header),
+            63,
+            3,
+            AlignCenter,
+            AlignTop);
+
+        // Only display a block if it is fully read, and, if it is a sector trailer, both keys are found
+        if(mf_classic_is_block_read(mf_classic_data, current_block) &&
+           (!mf_classic_is_sector_trailer(current_block) ||
+            (mf_classic_is_key_found(
+                 mf_classic_data, instance->current_sector, MfClassicKeyTypeA) &&
+             mf_classic_is_key_found(
+                 mf_classic_data, instance->current_sector, MfClassicKeyTypeB)))) {
+            // Split block data across 2 even lines
+            const uint8_t* block_data = mf_classic_data->block[current_block].data;
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text, block_data, MF_CLASSIC_BLOCK_SIZE / 2);
+            furi_string_push_back(instance->data_view_text, '\n');
+            mfc_editor_furi_string_render_bytes(
+                instance->data_view_text,
+                block_data + MF_CLASSIC_BLOCK_SIZE / 2,
+                MF_CLASSIC_BLOCK_SIZE / 2);
+            dialog_ex_set_right_button_text(dialog_ex, "Edit");
+        } else {
+            furi_string_set(
+                instance->data_view_text, "Data unavailable.\nBlock has not been fully read.");
+        }
+    }
+
+    dialog_ex_set_text(
+        dialog_ex,
+        furi_string_get_cstr(instance->data_view_text),
+        63,
+        31,
+        AlignCenter,
+        AlignCenter);
+}
+
+void mfc_editor_scene_data_view_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    mfc_editor_scene_data_view_update_display(instance);
+
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewDialogEx);
+}
+
+bool mfc_editor_scene_data_view_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        MfcEditorBlockView block_view =
+            scene_manager_get_scene_state(instance->scene_manager, MfcEditorSceneDataView);
+        if(block_view == MfcEditorBlockViewNormal) {
+            if(event.event == DialogExResultRight) {
+                // Block 0 and sector trailer blocks are risky edits
+                bool risky_block =
+                    instance->current_block == 0 ||
+                    instance->current_block ==
+                        mf_classic_get_sector_trailer_num_by_block(instance->current_block);
+                if(!risky_block || mfc_editor_warn_risky_operation(instance)) {
+                    scene_manager_set_scene_state(
+                        instance->scene_manager, MfcEditorSceneDataEdit, block_view);
+                    scene_manager_next_scene(instance->scene_manager, MfcEditorSceneDataEdit);
+                }
+                consumed = true;
+            }
+        } else if(block_view == MfcEditorBlockViewUID) {
+            if(event.event == DialogExResultRight) {
+                if(mfc_editor_warn_risky_operation(instance)) {
+                    scene_manager_set_scene_state(
+                        instance->scene_manager, MfcEditorSceneDataEdit, block_view);
+                    scene_manager_next_scene(instance->scene_manager, MfcEditorSceneDataEdit);
+                }
+                consumed = true;
+            } else if(event.event == DialogExResultCenter) {
+                if(mfc_editor_warn_risky_operation(instance)) {
+                    memcpy(
+                        instance->mf_classic_data->block[0].data,
+                        instance->mf_classic_data->iso14443_3a_data->uid,
+                        instance->mf_classic_data->iso14443_3a_data->uid_len);
+                    instance->is_unsaved_changes = true;
+                    mfc_editor_scene_data_view_update_display(instance);
+                }
+                consumed = true;
+            }
+        } else if(
+            block_view == MfcEditorBlockViewManufacturerBytes ||
+            block_view == MfcEditorBlockViewKeyA || block_view == MfcEditorBlockViewKeyB) {
+            if(event.event == DialogExResultRight) {
+                if(mfc_editor_warn_risky_operation(instance)) {
+                    scene_manager_set_scene_state(
+                        instance->scene_manager, MfcEditorSceneDataEdit, block_view);
+                    scene_manager_next_scene(instance->scene_manager, MfcEditorSceneDataEdit);
+                }
+                consumed = true;
+            }
+        } else if(block_view == MfcEditorBlockViewBCC) {
+            if(event.event == DialogExResultCenter) {
+                if(mfc_editor_warn_risky_operation(instance)) {
+                    // Fix BCC byte by setting it to calculated one
+                    instance->mf_classic_data->block[0].data[4] = mfc_editor_calculate_uid_bcc(
+                        instance->mf_classic_data->iso14443_3a_data->uid,
+                        instance->mf_classic_data->iso14443_3a_data->uid_len);
+                    instance->is_unsaved_changes = true;
+                    mfc_editor_scene_data_view_update_display(instance);
+                }
+                consumed = true;
+            }
+        } else if(block_view == MfcEditorBlockViewAccessBits) {
+            if(event.event == DialogExResultLeft) {
+                uint8_t new_sector = mf_classic_get_sector_by_block(--instance->current_block);
+                if(new_sector != instance->current_sector) {
+                    instance->current_block =
+                        mf_classic_get_sector_trailer_num_by_sector(instance->current_sector);
+                }
+                mfc_editor_scene_data_view_update_display(instance);
+                consumed = true;
+            } else if(event.event == DialogExResultCenter) {
+                uint8_t new_sector = mf_classic_get_sector_by_block(++instance->current_block);
+                if(new_sector != instance->current_sector) {
+                    instance->current_block =
+                        mf_classic_get_first_block_num_of_sector(instance->current_sector);
+                }
+                mfc_editor_scene_data_view_update_display(instance);
+                consumed = true;
+            } else if(event.event == DialogExResultRight) {
+                if(mfc_editor_warn_risky_operation(instance)) {
+                    scene_manager_next_scene(
+                        instance->scene_manager, MfcEditorSceneDataEditAccessBits);
+                }
+                consumed = true;
+            }
+        } else if(block_view == MfcEditorBlockViewUserByte) {
+            scene_manager_set_scene_state(
+                instance->scene_manager, MfcEditorSceneDataEdit, block_view);
+            scene_manager_next_scene(instance->scene_manager, MfcEditorSceneDataEdit);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_data_view_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    dialog_ex_reset(instance->dialog_ex);
+}

+ 33 - 0
mfc_editor/scenes/mfc_editor_scene_file_select.c

@@ -0,0 +1,33 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_file_select_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    // File select scene should repeat itself if the file load failed
+    // or if the user quit the shadow file prompt, not the file selector
+    MfcEditorPromptResponse prompt_response = MfcEditorPromptResponseFailure;
+    while(prompt_response == MfcEditorPromptResponseFailure ||
+          prompt_response == MfcEditorPromptResponseExitedShadow) {
+        prompt_response = mfc_editor_prompt_load_file(instance);
+    }
+
+    if(prompt_response == MfcEditorPromptResponseSuccess) {
+        scene_manager_set_scene_state(instance->scene_manager, MfcEditorSceneSectorSelect, 0);
+        scene_manager_next_scene(instance->scene_manager, MfcEditorSceneSectorSelect);
+    } else if(prompt_response == MfcEditorPromptResponseNotMfClassic) {
+        scene_manager_next_scene(instance->scene_manager, MfcEditorSceneInvalidFile);
+    } else {
+        // Exiting from file list stops the application
+        view_dispatcher_stop(instance->view_dispatcher);
+    }
+}
+
+bool mfc_editor_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void mfc_editor_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 38 - 0
mfc_editor/scenes/mfc_editor_scene_invalid_file.c

@@ -0,0 +1,38 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_invalid_file_popup_callback(void* context) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, MfcEditorCustomEventViewExit);
+}
+
+void mfc_editor_scene_invalid_file_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    Popup* popup = instance->popup;
+    popup_set_header(popup, "Invalid file", 63, 10, AlignCenter, AlignTop);
+    popup_set_text(
+        popup, "Only MIFARE Classic files\nare supported", 63, 40, AlignCenter, AlignCenter);
+    popup_set_context(popup, instance);
+    popup_set_callback(popup, mfc_editor_scene_invalid_file_popup_callback);
+
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewPopup);
+}
+
+bool mfc_editor_scene_invalid_file_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MfcEditorCustomEventViewExit) {
+            consumed = scene_manager_previous_scene(instance->scene_manager);
+        }
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_invalid_file_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    popup_reset(instance->popup);
+}

+ 40 - 0
mfc_editor/scenes/mfc_editor_scene_save_success.c

@@ -0,0 +1,40 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_save_success_popup_callback(void* context) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, MfcEditorCustomEventViewExit);
+}
+
+void mfc_editor_scene_save_success_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    Popup* popup = instance->popup;
+    popup_set_icon(popup, 36, 5, &I_DolphinSaved_92x58);
+    popup_set_header(popup, "Saved", 15, 19, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, instance);
+    popup_set_callback(popup, mfc_editor_scene_save_success_popup_callback);
+    popup_enable_timeout(popup);
+
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewPopup);
+}
+
+bool mfc_editor_scene_save_success_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MfcEditorCustomEventViewExit) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                instance->scene_manager, MfcEditorSceneFileSelect);
+        }
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_save_success_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    popup_reset(instance->popup);
+}

+ 67 - 0
mfc_editor/scenes/mfc_editor_scene_sector_select.c

@@ -0,0 +1,67 @@
+#include "../mfc_editor_app_i.h"
+
+void mfc_editor_scene_sector_select_submenu_callback(void* context, uint32_t index) {
+    MfcEditorApp* instance = context;
+    view_dispatcher_send_custom_event(instance->view_dispatcher, index);
+}
+
+void mfc_editor_scene_sector_select_on_enter(void* context) {
+    MfcEditorApp* instance = context;
+
+    Submenu* submenu = instance->submenu;
+
+    uint8_t sectors_num = mf_classic_get_total_sectors_num(instance->mf_classic_data->type);
+
+    FuriString* label = furi_string_alloc();
+    for(uint8_t i = 0; i < sectors_num; i++) {
+        furi_string_printf(label, "Sector %u", i);
+        submenu_add_item(
+            submenu,
+            furi_string_get_cstr(label),
+            i,
+            mfc_editor_scene_sector_select_submenu_callback,
+            instance);
+    }
+    furi_string_free(label);
+
+    submenu_set_selected_item(
+        submenu,
+        scene_manager_get_scene_state(instance->scene_manager, MfcEditorSceneSectorSelect));
+    view_dispatcher_switch_to_view(instance->view_dispatcher, MfcEditorAppViewSubmenu);
+}
+
+bool mfc_editor_scene_sector_select_on_event(void* context, SceneManagerEvent event) {
+    MfcEditorApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(
+            instance->scene_manager, MfcEditorSceneSectorSelect, event.event);
+        instance->current_sector = event.event;
+        scene_manager_set_scene_state(instance->scene_manager, MfcEditorSceneBlockSelect, 0);
+        scene_manager_next_scene(instance->scene_manager, MfcEditorSceneBlockSelect);
+        consumed = true;
+    } else if(event.type == SceneManagerEventTypeBack) {
+        if(instance->is_unsaved_changes) {
+            MfcEditorSaveResponse response = mfc_editor_warn_unsaved_changes(instance);
+            if(response == MfcEditorSaveResponseSave) {
+                if(mfc_editor_save_file(instance)) {
+                    scene_manager_next_scene(instance->scene_manager, MfcEditorSceneSaveSuccess);
+                }
+                // Stop the scene manager from going back to previous scene
+                consumed = true;
+            } else if(response == MfcEditorSaveResponseCancel) {
+                // Stop the scene manager from going back to previous scene
+                consumed = true;
+            }
+        }
+    }
+
+    return consumed;
+}
+
+void mfc_editor_scene_sector_select_on_exit(void* context) {
+    MfcEditorApp* instance = context;
+
+    submenu_reset(instance->submenu);
+}