Browse Source

[FL-3357] Magic Gen2 support (#143)

Co-authored-by: あく <alleteam@gmail.com>
Astra 1 year ago
parent
commit
666d3a4157
44 changed files with 3127 additions and 68 deletions
  1. 3 0
      .catalog/changelog.md
  2. BIN
      .catalog/screenshots/0.png
  3. BIN
      .catalog/screenshots/1.png
  4. BIN
      .catalog/screenshots/2.png
  5. 1 6
      application.fam
  6. BIN
      assets/DolphinSuccess_91x55.png
  7. BIN
      assets/WarningDolphinFlip_45x42.png
  8. 26 12
      magic/nfc_magic_scanner.c
  9. 0 0
      magic/nfc_magic_scanner.h
  10. 0 0
      magic/protocols/gen1a/gen1a_poller.c
  11. 0 0
      magic/protocols/gen1a/gen1a_poller.h
  12. 0 0
      magic/protocols/gen1a/gen1a_poller_i.c
  13. 0 0
      magic/protocols/gen1a/gen1a_poller_i.h
  14. 178 0
      magic/protocols/gen2/crypto1.c
  15. 45 0
      magic/protocols/gen2/crypto1.h
  16. 594 0
      magic/protocols/gen2/gen2_poller.c
  17. 97 0
      magic/protocols/gen2/gen2_poller.h
  18. 629 0
      magic/protocols/gen2/gen2_poller_i.c
  19. 139 0
      magic/protocols/gen2/gen2_poller_i.h
  20. 1 0
      magic/protocols/gen4/gen4_poller.c
  21. 0 0
      magic/protocols/gen4/gen4_poller.h
  22. 0 0
      magic/protocols/gen4/gen4_poller_i.c
  23. 0 0
      magic/protocols/gen4/gen4_poller_i.h
  24. 4 2
      magic/protocols/nfc_magic_protocols.c
  25. 2 0
      magic/protocols/nfc_magic_protocols.h
  26. 30 2
      nfc_magic_app.c
  27. 52 4
      nfc_magic_app_i.h
  28. 5 0
      scenes/nfc_magic_scene_config.h
  29. 15 1
      scenes/nfc_magic_scene_file_select.c
  30. 55 0
      scenes/nfc_magic_scene_gen2_menu.c
  31. 130 0
      scenes/nfc_magic_scene_gen2_write_check.c
  32. 23 12
      scenes/nfc_magic_scene_magic_info.c
  33. 311 0
      scenes/nfc_magic_scene_mf_classic_dict_attack.c
  34. 65 0
      scenes/nfc_magic_scene_mf_classic_menu.c
  35. 124 0
      scenes/nfc_magic_scene_mf_classic_write_check.c
  36. 2 2
      scenes/nfc_magic_scene_success.c
  37. 48 6
      scenes/nfc_magic_scene_wipe.c
  38. 5 2
      scenes/nfc_magic_scene_wipe_fail.c
  39. 50 5
      scenes/nfc_magic_scene_write.c
  40. 6 14
      scenes/nfc_magic_scene_write_fail.c
  41. 245 0
      views/dict_attack.c
  42. 50 0
      views/dict_attack.h
  43. 160 0
      views/write_problems.c
  44. 32 0
      views/write_problems.h

+ 3 - 0
.catalog/changelog.md

@@ -1,3 +1,6 @@
+## 1.7
+ - Gen2/CUID write support
+
 ## 1.6
  - Rework with new bit lib API
 

BIN
.catalog/screenshots/0.png


BIN
.catalog/screenshots/1.png


BIN
.catalog/screenshots/2.png


+ 1 - 6
application.fam

@@ -10,13 +10,8 @@ App(
     ],
     stack_size=4 * 1024,
     fap_description="Application for writing to NFC tags with modifiable sector 0",
-    fap_version="1.6",
+    fap_version="1.7",
     fap_icon="assets/125_10px.png",
     fap_category="NFC",
-    fap_private_libs=[
-        Lib(
-            name="magic",
-        ),
-    ],
     fap_icon_assets="assets",
 )

BIN
assets/DolphinSuccess_91x55.png


BIN
assets/WarningDolphinFlip_45x42.png


+ 26 - 12
lib/magic/nfc_magic_scanner.c → magic/nfc_magic_scanner.c

@@ -1,6 +1,7 @@
 #include "nfc_magic_scanner.h"
 
 #include "protocols/gen1a/gen1a_poller.h"
+#include "protocols/gen2/gen2_poller.h"
 #include "protocols/gen4/gen4_poller.h"
 #include <nfc/nfc_poller.h>
 
@@ -65,20 +66,33 @@ static int32_t nfc_magic_scanner_worker(void* context) {
     furi_assert(instance->session_state == NfcMagicScannerSessionStateActive);
 
     while(instance->session_state == NfcMagicScannerSessionStateActive) {
-        if(instance->current_protocol == NfcMagicProtocolGen1) {
-            instance->magic_protocol_detected = gen1a_poller_detect(instance->nfc);
-        } else if(instance->current_protocol == NfcMagicProtocolGen4) {
-            Gen4PollerError error = gen4_poller_detect(instance->nfc, instance->gen4_password);
-            if(error == Gen4PollerErrorProtocol) {
-                NfcMagicScannerEvent event = {
-                    .type = NfcMagicScannerEventTypeDetectedNotMagic,
-                };
-                instance->callback(event, instance->context);
-                break;
-            } else {
+        do {
+            if(instance->current_protocol == NfcMagicProtocolGen1) {
+                instance->magic_protocol_detected = gen1a_poller_detect(instance->nfc);
+                if(instance->magic_protocol_detected) {
+                    break;
+                }
+            } else if(instance->current_protocol == NfcMagicProtocolGen4) {
+                Gen4PollerError error = gen4_poller_detect(instance->nfc, instance->gen4_password);
                 instance->magic_protocol_detected = (error == Gen4PollerErrorNone);
+                if(instance->magic_protocol_detected) {
+                    break;
+                }
+            } else if(instance->current_protocol == NfcMagicProtocolGen2) {
+                Gen2PollerError error = gen2_poller_detect(instance->nfc);
+                instance->magic_protocol_detected = (error == Gen2PollerErrorNone);
+                if(instance->magic_protocol_detected) {
+                    break;
+                }
+            } else if(instance->current_protocol == NfcMagicProtocolClassic) {
+                NfcPoller* poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfClassic);
+                instance->magic_protocol_detected = nfc_poller_detect(poller);
+                nfc_poller_free(poller);
+                if(instance->magic_protocol_detected) {
+                    break;
+                }
             }
-        }
+        } while(false);
 
         if(instance->magic_protocol_detected) {
             NfcMagicScannerEvent event = {

+ 0 - 0
lib/magic/nfc_magic_scanner.h → magic/nfc_magic_scanner.h


+ 0 - 0
lib/magic/protocols/gen1a/gen1a_poller.c → magic/protocols/gen1a/gen1a_poller.c


+ 0 - 0
lib/magic/protocols/gen1a/gen1a_poller.h → magic/protocols/gen1a/gen1a_poller.h


+ 0 - 0
lib/magic/protocols/gen1a/gen1a_poller_i.c → magic/protocols/gen1a/gen1a_poller_i.c


+ 0 - 0
lib/magic/protocols/gen1a/gen1a_poller_i.h → magic/protocols/gen1a/gen1a_poller_i.h


+ 178 - 0
magic/protocols/gen2/crypto1.c

@@ -0,0 +1,178 @@
+#include "crypto1.h"
+
+#include <nfc/helpers/nfc_util.h>
+#include <bit_lib/bit_lib.h>
+#include <furi.h>
+
+// Algorithm from https://github.com/RfidResearchGroup/proxmark3.git
+
+#define SWAPENDIAN(x) \
+    ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16)
+#define LF_POLY_ODD (0x29CE5C)
+#define LF_POLY_EVEN (0x870804)
+
+#define BEBIT(x, n) FURI_BIT(x, (n) ^ 24)
+
+Crypto1* crypto1_alloc() {
+    Crypto1* instance = malloc(sizeof(Crypto1));
+
+    return instance;
+}
+
+void crypto1_free(Crypto1* instance) {
+    furi_assert(instance);
+
+    free(instance);
+}
+
+void crypto1_reset(Crypto1* crypto1) {
+    furi_assert(crypto1);
+    crypto1->even = 0;
+    crypto1->odd = 0;
+}
+
+void crypto1_init(Crypto1* crypto1, uint64_t key) {
+    furi_assert(crypto1);
+    crypto1->even = 0;
+    crypto1->odd = 0;
+    for(int8_t i = 47; i > 0; i -= 2) {
+        crypto1->odd = crypto1->odd << 1 | FURI_BIT(key, (i - 1) ^ 7);
+        crypto1->even = crypto1->even << 1 | FURI_BIT(key, i ^ 7);
+    }
+}
+
+static uint32_t crypto1_filter(uint32_t in) {
+    uint32_t out = 0;
+    out = 0xf22c0 >> (in & 0xf) & 16;
+    out |= 0x6c9c0 >> (in >> 4 & 0xf) & 8;
+    out |= 0x3c8b0 >> (in >> 8 & 0xf) & 4;
+    out |= 0x1e458 >> (in >> 12 & 0xf) & 2;
+    out |= 0x0d938 >> (in >> 16 & 0xf) & 1;
+    return FURI_BIT(0xEC57E80A, out);
+}
+
+uint8_t crypto1_bit(Crypto1* crypto1, uint8_t in, int is_encrypted) {
+    furi_assert(crypto1);
+    uint8_t out = crypto1_filter(crypto1->odd);
+    uint32_t feed = out & (!!is_encrypted);
+    feed ^= !!in;
+    feed ^= LF_POLY_ODD & crypto1->odd;
+    feed ^= LF_POLY_EVEN & crypto1->even;
+    crypto1->even = crypto1->even << 1 | (nfc_util_even_parity32(feed));
+
+    FURI_SWAP(crypto1->odd, crypto1->even);
+    return out;
+}
+
+uint8_t crypto1_byte(Crypto1* crypto1, uint8_t in, int is_encrypted) {
+    furi_assert(crypto1);
+    uint8_t out = 0;
+    for(uint8_t i = 0; i < 8; i++) {
+        out |= crypto1_bit(crypto1, FURI_BIT(in, i), is_encrypted) << i;
+    }
+    return out;
+}
+
+uint32_t crypto1_word(Crypto1* crypto1, uint32_t in, int is_encrypted) {
+    furi_assert(crypto1);
+    uint32_t out = 0;
+    for(uint8_t i = 0; i < 32; i++) {
+        out |= (uint32_t)crypto1_bit(crypto1, BEBIT(in, i), is_encrypted) << (24 ^ i);
+    }
+    return out;
+}
+
+uint32_t prng_successor(uint32_t x, uint32_t n) {
+    SWAPENDIAN(x);
+    while(n--) x = x >> 1 | (x >> 16 ^ x >> 18 ^ x >> 19 ^ x >> 21) << 31;
+
+    return SWAPENDIAN(x);
+}
+
+void crypto1_decrypt(Crypto1* crypto, const BitBuffer* buff, BitBuffer* out) {
+    furi_assert(crypto);
+    furi_assert(buff);
+    furi_assert(out);
+
+    size_t bits = bit_buffer_get_size(buff);
+    bit_buffer_set_size(out, bits);
+    const uint8_t* encrypted_data = bit_buffer_get_data(buff);
+    if(bits < 8) {
+        uint8_t decrypted_byte = 0;
+        uint8_t encrypted_byte = encrypted_data[0];
+        decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_byte, 0)) << 0;
+        decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_byte, 1)) << 1;
+        decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_byte, 2)) << 2;
+        decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_byte, 3)) << 3;
+        bit_buffer_set_byte(out, 0, decrypted_byte);
+    } else {
+        for(size_t i = 0; i < bits / 8; i++) {
+            uint8_t decrypted_byte = crypto1_byte(crypto, 0, 0) ^ encrypted_data[i];
+            bit_buffer_set_byte(out, i, decrypted_byte);
+        }
+    }
+}
+
+void crypto1_encrypt(Crypto1* crypto, uint8_t* keystream, const BitBuffer* buff, BitBuffer* out) {
+    furi_assert(crypto);
+    furi_assert(buff);
+    furi_assert(out);
+
+    size_t bits = bit_buffer_get_size(buff);
+    bit_buffer_set_size(out, bits);
+    const uint8_t* plain_data = bit_buffer_get_data(buff);
+    if(bits < 8) {
+        uint8_t encrypted_byte = 0;
+        for(size_t i = 0; i < bits; i++) {
+            encrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(plain_data[0], i)) << i;
+        }
+        bit_buffer_set_byte(out, 0, encrypted_byte);
+    } else {
+        for(size_t i = 0; i < bits / 8; i++) {
+            uint8_t encrypted_byte = crypto1_byte(crypto, keystream ? keystream[i] : 0, 0) ^
+                                     plain_data[i];
+            bool parity_bit =
+                ((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_data[i])) & 0x01);
+            bit_buffer_set_byte_with_parity(out, i, encrypted_byte, parity_bit);
+        }
+    }
+}
+
+void crypto1_encrypt_reader_nonce(
+    Crypto1* crypto,
+    uint64_t key,
+    uint32_t cuid,
+    uint8_t* nt,
+    uint8_t* nr,
+    BitBuffer* out,
+    bool is_nested) {
+    furi_assert(crypto);
+    furi_assert(nt);
+    furi_assert(nr);
+    furi_assert(out);
+
+    bit_buffer_set_size_bytes(out, 8);
+    uint32_t nt_num = bit_lib_bytes_to_num_be(nt, sizeof(uint32_t));
+
+    crypto1_init(crypto, key);
+    if(is_nested) {
+        nt_num = crypto1_word(crypto, nt_num ^ cuid, 1) ^ nt_num;
+    } else {
+        crypto1_word(crypto, nt_num ^ cuid, 0);
+    }
+
+    for(size_t i = 0; i < 4; i++) {
+        uint8_t byte = crypto1_byte(crypto, nr[i], 0) ^ nr[i];
+        bool parity_bit = ((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(nr[i])) & 0x01);
+        bit_buffer_set_byte_with_parity(out, i, byte, parity_bit);
+        nr[i] = byte;
+    }
+
+    nt_num = prng_successor(nt_num, 32);
+    for(size_t i = 4; i < 8; i++) {
+        nt_num = prng_successor(nt_num, 8);
+        uint8_t byte = crypto1_byte(crypto, 0, 0) ^ (uint8_t)(nt_num);
+        bool parity_bit = ((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(nt_num)) & 0x01);
+        bit_buffer_set_byte_with_parity(out, i, byte, parity_bit);
+    }
+}

+ 45 - 0
magic/protocols/gen2/crypto1.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <toolbox/bit_buffer.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+    uint32_t odd;
+    uint32_t even;
+} Crypto1;
+
+Crypto1* crypto1_alloc();
+
+void crypto1_free(Crypto1* instance);
+
+void crypto1_reset(Crypto1* crypto1);
+
+void crypto1_init(Crypto1* crypto1, uint64_t key);
+
+uint8_t crypto1_bit(Crypto1* crypto1, uint8_t in, int is_encrypted);
+
+uint8_t crypto1_byte(Crypto1* crypto1, uint8_t in, int is_encrypted);
+
+uint32_t crypto1_word(Crypto1* crypto1, uint32_t in, int is_encrypted);
+
+void crypto1_decrypt(Crypto1* crypto, const BitBuffer* buff, BitBuffer* out);
+
+void crypto1_encrypt(Crypto1* crypto, uint8_t* keystream, const BitBuffer* buff, BitBuffer* out);
+
+void crypto1_encrypt_reader_nonce(
+    Crypto1* crypto,
+    uint64_t key,
+    uint32_t cuid,
+    uint8_t* nt,
+    uint8_t* nr,
+    BitBuffer* out,
+    bool is_nested);
+
+uint32_t prng_successor(uint32_t x, uint32_t n);
+
+#ifdef __cplusplus
+}
+#endif

+ 594 - 0
magic/protocols/gen2/gen2_poller.c

@@ -0,0 +1,594 @@
+#include "gen2_poller_i.h"
+#include <nfc/helpers/nfc_data_generator.h>
+
+#include <furi/furi.h>
+
+#define GEN2_POLLER_THREAD_FLAG_DETECTED (1U << 0)
+
+#define TAG "GEN2"
+
+typedef NfcCommand (*Gen2PollerStateHandler)(Gen2Poller* instance);
+
+typedef struct {
+    NfcPoller* poller;
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+    FuriThreadId thread_id;
+    bool detected;
+    Gen2PollerError error;
+} Gen2PollerDetectContext;
+
+// Array of known Gen2 ATS responses
+// 0978009102DABC1910F005 - flavour 2
+// 0978009102DABC1910F005 - flavour 4
+// 0D780071028849A13020150608563D - flavour 6
+// Other flavours can't be detected other than by just trying to write to block 0
+const uint8_t GEN2_ATS[3][16] = {
+    {0x09, 0x78, 0x00, 0x91, 0x02, 0xDA, 0xBC, 0x19, 0x10, 0xF0, 0x05},
+    {0x09, 0x78, 0x00, 0x91, 0x02, 0xDA, 0xBC, 0x19, 0x10, 0xF0, 0x05},
+    {0x0D, 0x78, 0x00, 0x71, 0x02, 0x88, 0x49, 0xA1, 0x30, 0x20, 0x15, 0x06, 0x08, 0x56, 0x3D}};
+
+static const MfClassicBlock gen2_poller_default_block_0 = {
+    .data =
+        {0x00,
+         0x01,
+         0x02,
+         0x03,
+         0x00, // BCC - IMPORTANT
+         0x08, // SAK
+         0x04, // ATQA0
+         0x00, // ATQA1
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00},
+};
+
+static const MfClassicBlock gen2_poller_default_empty_block = {
+    .data =
+        {0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00,
+         0x00},
+};
+
+static MfClassicBlock gen2_poller_default_sector_trailer_block = {
+    .data =
+        {0xFF,
+         0xFF,
+         0xFF,
+         0xFF,
+         0xFF,
+         0xFF,
+         0xFF,
+         0x07,
+         0x80,
+         0x69,
+         0xFF,
+         0xFF,
+         0xFF,
+         0xFF,
+         0xFF,
+         0xFF},
+};
+
+const char* const gen2_problem_strings[] = {
+    "UID may be non-\nrewritable. Check data after writing",
+    "No data in selected file",
+    "Some sectors are locked",
+    "Can't find keys to some sectors",
+    "The selected file is incomplete",
+};
+
+Gen2Poller* gen2_poller_alloc(Nfc* nfc) {
+    Gen2Poller* instance = malloc(sizeof(Gen2Poller));
+    instance->poller = nfc_poller_alloc(nfc, NfcProtocolIso14443_3a);
+    instance->data = mf_classic_alloc();
+    instance->crypto = crypto1_alloc();
+    instance->tx_plain_buffer = bit_buffer_alloc(GEN2_POLLER_MAX_BUFFER_SIZE);
+    instance->tx_encrypted_buffer = bit_buffer_alloc(GEN2_POLLER_MAX_BUFFER_SIZE);
+    instance->rx_plain_buffer = bit_buffer_alloc(GEN2_POLLER_MAX_BUFFER_SIZE);
+    instance->rx_encrypted_buffer = bit_buffer_alloc(GEN2_POLLER_MAX_BUFFER_SIZE);
+    instance->card_state = Gen2CardStateLost;
+
+    instance->gen2_event.data = &instance->gen2_event_data;
+
+    instance->mode_ctx.write_ctx.mfc_data_source = malloc(sizeof(MfClassicData));
+    instance->mode_ctx.write_ctx.mfc_data_target = malloc(sizeof(MfClassicData));
+
+    instance->mode_ctx.write_ctx.need_halt_before_write = true;
+
+    return instance;
+}
+
+void gen2_poller_free(Gen2Poller* instance) {
+    furi_assert(instance);
+    furi_assert(instance->data);
+    furi_assert(instance->crypto);
+    furi_assert(instance->tx_plain_buffer);
+    furi_assert(instance->rx_plain_buffer);
+    furi_assert(instance->tx_encrypted_buffer);
+    furi_assert(instance->rx_encrypted_buffer);
+
+    nfc_poller_free(instance->poller);
+    mf_classic_free(instance->data);
+    crypto1_free(instance->crypto);
+    bit_buffer_free(instance->tx_plain_buffer);
+    bit_buffer_free(instance->rx_plain_buffer);
+    bit_buffer_free(instance->tx_encrypted_buffer);
+    bit_buffer_free(instance->rx_encrypted_buffer);
+
+    free(instance->mode_ctx.write_ctx.mfc_data_source);
+    free(instance->mode_ctx.write_ctx.mfc_data_target);
+
+    free(instance);
+}
+
+NfcCommand gen2_poller_detect_callback(NfcGenericEvent event, void* context) {
+    furi_assert(context);
+    furi_assert(event.protocol == NfcProtocolIso14443_3a);
+    furi_assert(event.event_data);
+    furi_assert(event.instance);
+
+    NfcCommand command = NfcCommandStop;
+    Gen2PollerDetectContext* detect_ctx = context;
+    Iso14443_3aPoller* iso3_poller = event.instance;
+    Iso14443_3aPollerEvent* iso3_event = event.event_data;
+    detect_ctx->error = Gen2PollerErrorTimeout;
+
+    bit_buffer_reset(detect_ctx->tx_buffer);
+    bit_buffer_append_byte(detect_ctx->tx_buffer, GEN2_CMD_READ_ATS);
+    bit_buffer_append_byte(detect_ctx->tx_buffer, GEN2_FSDI_256 << 4);
+
+    if(iso3_event->type == Iso14443_3aPollerEventTypeReady) {
+        do {
+            const Iso14443_3aError iso14443_3a_error = iso14443_3a_poller_send_standard_frame(
+                iso3_poller, detect_ctx->tx_buffer, detect_ctx->rx_buffer, GEN2_POLLER_MAX_FWT);
+
+            if(iso14443_3a_error != Iso14443_3aErrorNone &&
+               iso14443_3a_error != Iso14443_3aErrorWrongCrc) {
+                FURI_LOG_E(TAG, "ATS request failed");
+                detect_ctx->error = Gen2PollerErrorProtocol;
+                break;
+
+            } else {
+                FURI_LOG_D(TAG, "ATS request succeeded:");
+                // Check against known ATS responses
+                for(size_t i = 0; i < COUNT_OF(GEN2_ATS); i++) {
+                    if(memcmp(
+                           bit_buffer_get_data(detect_ctx->rx_buffer),
+                           GEN2_ATS[i],
+                           sizeof(GEN2_ATS[i])) == 0) {
+                        detect_ctx->error = Gen2PollerErrorNone;
+                        break;
+                    }
+                }
+            }
+        } while(false);
+    } else if(iso3_event->type == Iso14443_3aPollerEventTypeError) {
+        detect_ctx->error = Gen2PollerErrorTimeout;
+    }
+    furi_thread_flags_set(detect_ctx->thread_id, GEN2_POLLER_THREAD_FLAG_DETECTED);
+
+    return command;
+}
+
+Gen2PollerError gen2_poller_detect(Nfc* nfc) {
+    furi_assert(nfc);
+
+    Gen2PollerDetectContext detect_ctx = {
+        .poller = nfc_poller_alloc(nfc, NfcProtocolIso14443_3a),
+        .tx_buffer = bit_buffer_alloc(GEN2_POLLER_MAX_BUFFER_SIZE),
+        .rx_buffer = bit_buffer_alloc(GEN2_POLLER_MAX_BUFFER_SIZE),
+        .thread_id = furi_thread_get_current_id(),
+        .detected = false,
+        .error = Gen2PollerErrorNone,
+    };
+
+    nfc_poller_start(detect_ctx.poller, gen2_poller_detect_callback, &detect_ctx);
+    uint32_t flags =
+        furi_thread_flags_wait(GEN2_POLLER_THREAD_FLAG_DETECTED, FuriFlagWaitAny, FuriWaitForever);
+    if(flags & GEN2_POLLER_THREAD_FLAG_DETECTED) {
+        furi_thread_flags_clear(GEN2_POLLER_THREAD_FLAG_DETECTED);
+    }
+    nfc_poller_stop(detect_ctx.poller);
+
+    bit_buffer_free(detect_ctx.tx_buffer);
+    bit_buffer_free(detect_ctx.rx_buffer);
+    nfc_poller_free(detect_ctx.poller);
+
+    return detect_ctx.error;
+}
+
+NfcCommand gen2_poller_idle_handler(Gen2Poller* instance) {
+    furi_assert(instance);
+
+    NfcCommand command = NfcCommandContinue;
+
+    instance->mode_ctx.write_ctx.current_block = 0;
+    instance->gen2_event.type = Gen2PollerEventTypeDetected;
+    command = instance->callback(instance->gen2_event, instance->context);
+    instance->state = Gen2PollerStateRequestMode;
+
+    return command;
+}
+
+NfcCommand gen2_poller_request_mode_handler(Gen2Poller* instance) {
+    furi_assert(instance);
+
+    NfcCommand command = NfcCommandContinue;
+
+    instance->gen2_event.type = Gen2PollerEventTypeRequestMode;
+    command = instance->callback(instance->gen2_event, instance->context);
+    instance->mode = instance->gen2_event_data.poller_mode.mode;
+    if(instance->gen2_event_data.poller_mode.mode == Gen2PollerModeWipe) {
+        instance->state = Gen2PollerStateWriteTargetDataRequest;
+    } else {
+        instance->state = Gen2PollerStateWriteSourceDataRequest;
+    }
+
+    return command;
+}
+
+NfcCommand gen2_poller_write_source_data_request_handler(Gen2Poller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    instance->gen2_event.type = Gen2PollerEventTypeRequestDataToWrite;
+    command = instance->callback(instance->gen2_event, instance->context);
+    memcpy(
+        instance->mode_ctx.write_ctx.mfc_data_source,
+        instance->gen2_event_data.data_to_write.mfc_data,
+        sizeof(MfClassicData));
+    instance->state = Gen2PollerStateWriteTargetDataRequest;
+
+    return command;
+}
+
+NfcCommand gen2_poller_write_target_data_request_handler(Gen2Poller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    instance->gen2_event.type = Gen2PollerEventTypeRequestTargetData;
+    command = instance->callback(instance->gen2_event, instance->context);
+    memcpy(
+        instance->mode_ctx.write_ctx.mfc_data_target,
+        instance->gen2_event_data.target_data.mfc_data,
+        sizeof(MfClassicData));
+    if(instance->mode == Gen2PollerModeWipe) {
+        instance->state = Gen2PollerStateWipe;
+    } else {
+        instance->state = Gen2PollerStateWrite;
+    }
+
+    return command;
+}
+
+Gen2PollerError gen2_poller_write_block_handler(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    const MfClassicBlock* block) {
+    furi_assert(instance);
+
+    Gen2PollerError error = Gen2PollerErrorNone;
+    Gen2PollerWriteContext* write_ctx = &instance->mode_ctx.write_ctx;
+    MfClassicKey auth_key = write_ctx->auth_key;
+
+    do {
+        // Compare the target and source data
+        if(memcmp(block->data, write_ctx->mfc_data_target->block[block_num].data, 16) == 0) {
+            FURI_LOG_D(TAG, "Block %d is the same, skipping", block_num);
+            break;
+        }
+
+        // Reauth if necessary
+        if(write_ctx->need_halt_before_write) {
+            FURI_LOG_D(TAG, "Auth before writing block %d", write_ctx->current_block);
+            error = gen2_poller_auth(
+                instance, write_ctx->current_block, &auth_key, write_ctx->write_key, NULL);
+            if(error != Gen2PollerErrorNone) {
+                FURI_LOG_D(
+                    TAG, "Failed to auth to block %d for writing", write_ctx->current_block);
+                break;
+            }
+        }
+
+        // Write the block
+        error = gen2_poller_write_block(instance, write_ctx->current_block, block);
+        if(error != Gen2PollerErrorNone) {
+            FURI_LOG_D(TAG, "Failed to write block %d", write_ctx->current_block);
+            break;
+        }
+    } while(false);
+    FURI_LOG_D(TAG, "Block %d finished, halting", write_ctx->current_block);
+    gen2_poller_halt(instance);
+    return error;
+}
+
+NfcCommand gen2_poller_wipe_handler(Gen2Poller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    Gen2PollerError error = Gen2PollerErrorNone;
+    Gen2PollerWriteContext* write_ctx = &instance->mode_ctx.write_ctx;
+    uint8_t block_num = write_ctx->current_block;
+
+    do {
+        // Check whether the ACs for that block are known in target data
+        if(!mf_classic_is_block_read(
+               write_ctx->mfc_data_target,
+               mf_classic_get_sector_trailer_num_by_block(block_num))) {
+            FURI_LOG_E(TAG, "Sector trailer for block %d not present in target data", block_num);
+            break;
+        }
+
+        // Check whether ACs need to be reset and whether they can be reset
+        if(!gen2_poller_can_write_block(write_ctx->mfc_data_target, block_num)) {
+            if(!gen2_can_reset_access_conditions(write_ctx->mfc_data_target, block_num)) {
+                FURI_LOG_E(TAG, "Block %d cannot be written", block_num);
+                break;
+            } else {
+                FURI_LOG_D(TAG, "Resetting ACs for block %d", block_num);
+                // Generate a block with old keys and default ACs (0xFF, 0x07, 0x80)
+                MfClassicBlock block;
+                memset(&block, 0, sizeof(block));
+                memcpy(block.data, write_ctx->mfc_data_target->block[block_num].data, 16);
+                memcpy(block.data + 6, "\xFF\x07\x80", 3);
+
+                error = gen2_poller_write_block_handler(instance, block_num, &block);
+                if(error != Gen2PollerErrorNone) {
+                    FURI_LOG_E(TAG, "Failed to reset ACs for block %d", block_num);
+                    break;
+                } else {
+                    FURI_LOG_D(TAG, "ACs for block %d reset", block_num);
+                    memcpy(write_ctx->mfc_data_target->block[block_num].data, block.data, 16);
+                }
+            }
+        }
+
+        // Figure out which key to use for writing
+        write_ctx->write_key =
+            gen2_poller_get_key_type_to_write(write_ctx->mfc_data_target, block_num);
+
+        // Get the key to use for writing from the target data
+        MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(
+            write_ctx->mfc_data_target, mf_classic_get_sector_by_block(block_num));
+        if(write_ctx->write_key == MfClassicKeyTypeA) {
+            write_ctx->auth_key = sec_tr->key_a;
+        } else {
+            write_ctx->auth_key = sec_tr->key_b;
+        }
+
+        // Write the default block depending on the block type
+        if(block_num == 0) {
+            error =
+                gen2_poller_write_block_handler(instance, block_num, &gen2_poller_default_block_0);
+        } else if(mf_classic_is_sector_trailer(block_num)) {
+            error = gen2_poller_write_block_handler(
+                instance, block_num, &gen2_poller_default_sector_trailer_block);
+        } else {
+            error = gen2_poller_write_block_handler(
+                instance, block_num, &gen2_poller_default_empty_block);
+        }
+        if(error != Gen2PollerErrorNone) {
+            FURI_LOG_E(TAG, "Couldn't write block %d", block_num);
+        }
+    } while(false);
+
+    write_ctx->current_block++;
+
+    if(error != Gen2PollerErrorNone) {
+        FURI_LOG_D(TAG, "Error occurred: %d", error);
+    }
+
+    if(write_ctx->current_block ==
+       mf_classic_get_total_block_num(write_ctx->mfc_data_target->type)) {
+        instance->state = Gen2PollerStateSuccess;
+    }
+
+    return command;
+}
+
+NfcCommand gen2_poller_write_handler(Gen2Poller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    Gen2PollerError error = Gen2PollerErrorNone;
+    Gen2PollerWriteContext* write_ctx = &instance->mode_ctx.write_ctx;
+    uint8_t block_num = write_ctx->current_block;
+
+    do {
+        // Check whether the block is present in the source data
+        if(!mf_classic_is_block_read(write_ctx->mfc_data_source, block_num)) {
+            // FURI_LOG_E(TAG, "Block %d not present in source data", block_num);
+            break;
+        }
+
+        // Check whether the ACs for that block are known in target data
+        if(!mf_classic_is_block_read(
+               write_ctx->mfc_data_target,
+               mf_classic_get_sector_trailer_num_by_block(block_num))) {
+            FURI_LOG_E(TAG, "Sector trailer for block %d not present in target data", block_num);
+            break;
+        }
+
+        // Check whether ACs need to be reset and whether they can be reset
+        if(!gen2_poller_can_write_block(write_ctx->mfc_data_target, block_num)) {
+            if(!gen2_can_reset_access_conditions(write_ctx->mfc_data_target, block_num)) {
+                FURI_LOG_E(TAG, "Block %d cannot be written", block_num);
+                break;
+            } else {
+                FURI_LOG_D(TAG, "Resetting ACs for block %d", block_num);
+                // Generate a block with old keys and default ACs (0xFF, 0x07, 0x80)
+                MfClassicBlock block;
+                memset(&block, 0, sizeof(block));
+                memcpy(block.data, write_ctx->mfc_data_target->block[block_num].data, 16);
+                memcpy(block.data + 6, "\xFF\x07\x80", 3);
+
+                error = gen2_poller_write_block_handler(instance, block_num, &block);
+                if(error != Gen2PollerErrorNone) {
+                    FURI_LOG_E(TAG, "Failed to reset ACs for block %d", block_num);
+                    break;
+                } else {
+                    FURI_LOG_D(TAG, "ACs for block %d reset", block_num);
+                    memcpy(write_ctx->mfc_data_target->block[block_num].data, block.data, 16);
+                }
+            }
+        }
+
+        // Figure out which key to use for writing
+        write_ctx->write_key =
+            gen2_poller_get_key_type_to_write(write_ctx->mfc_data_target, block_num);
+
+        // Get the key to use for writing from the target data
+        MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(
+            write_ctx->mfc_data_target, mf_classic_get_sector_by_block(block_num));
+        if(write_ctx->write_key == MfClassicKeyTypeA) {
+            write_ctx->auth_key = sec_tr->key_a;
+        } else {
+            write_ctx->auth_key = sec_tr->key_b;
+        }
+
+        // Write the block
+        error = gen2_poller_write_block_handler(
+            instance, block_num, &write_ctx->mfc_data_source->block[block_num]);
+        if(error != Gen2PollerErrorNone) {
+            FURI_LOG_E(TAG, "Couldn't write block %d", block_num);
+        }
+    } while(false);
+    write_ctx->current_block++;
+
+    if(error != Gen2PollerErrorNone) {
+        FURI_LOG_D(TAG, "Error occurred: %d", error);
+    } else if(
+        write_ctx->current_block ==
+        mf_classic_get_total_block_num(write_ctx->mfc_data_source->type)) {
+        instance->state = Gen2PollerStateSuccess;
+    }
+
+    return command;
+}
+
+NfcCommand gen2_poller_success_handler(Gen2Poller* instance) {
+    furi_assert(instance);
+
+    NfcCommand command = NfcCommandContinue;
+
+    instance->gen2_event.type = Gen2PollerEventTypeSuccess;
+    command = instance->callback(instance->gen2_event, instance->context);
+    instance->state = Gen2PollerStateIdle;
+
+    return command;
+}
+
+NfcCommand gen2_poller_fail_handler(Gen2Poller* instance) {
+    furi_assert(instance);
+
+    NfcCommand command = NfcCommandContinue;
+
+    instance->gen2_event.type = Gen2PollerEventTypeFail;
+    command = instance->callback(instance->gen2_event, instance->context);
+    instance->state = Gen2PollerStateIdle;
+
+    return command;
+}
+
+static const Gen2PollerStateHandler gen2_poller_state_handlers[Gen2PollerStateNum] = {
+    [Gen2PollerStateIdle] = gen2_poller_idle_handler,
+    [Gen2PollerStateRequestMode] = gen2_poller_request_mode_handler,
+    [Gen2PollerStateWipe] = gen2_poller_wipe_handler,
+    [Gen2PollerStateWriteSourceDataRequest] = gen2_poller_write_source_data_request_handler,
+    [Gen2PollerStateWriteTargetDataRequest] = gen2_poller_write_target_data_request_handler,
+    [Gen2PollerStateWrite] = gen2_poller_write_handler,
+    [Gen2PollerStateSuccess] = gen2_poller_success_handler,
+    [Gen2PollerStateFail] = gen2_poller_fail_handler,
+};
+
+NfcCommand gen2_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(context);
+    furi_assert(event.protocol == NfcProtocolIso14443_3a);
+    furi_assert(event.event_data);
+    furi_assert(event.instance);
+
+    NfcCommand command = NfcCommandContinue;
+    Gen2Poller* instance = context;
+    instance->iso3_poller = event.instance;
+    Iso14443_3aPollerEvent* iso3_event = event.event_data;
+
+    if(iso3_event->type == Iso14443_3aPollerEventTypeReady) {
+        command = gen2_poller_state_handlers[instance->state](instance);
+    }
+
+    return command;
+}
+
+void gen2_poller_start(Gen2Poller* instance, Gen2PollerCallback callback, void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+
+    instance->callback = callback;
+    instance->context = context;
+
+    nfc_poller_start(instance->poller, gen2_poller_callback, instance);
+    return;
+}
+
+void gen2_poller_stop(Gen2Poller* instance) {
+    furi_assert(instance);
+
+    FURI_LOG_D(TAG, "Stopping Gen2 poller");
+    nfc_poller_stop(instance->poller);
+    return;
+}
+
+Gen2PollerWriteProblems gen2_poller_check_target_problems(NfcDevice* target_dev) {
+    furi_assert(target_dev);
+
+    Gen2PollerWriteProblems problems = {0};
+    const MfClassicData* mfc_data = nfc_device_get_data(target_dev, NfcProtocolMfClassic);
+
+    if(mfc_data) {
+        uint16_t total_block_num = mf_classic_get_total_block_num(mfc_data->type);
+        for(uint16_t i = 0; i < total_block_num; i++) {
+            if(mf_classic_is_sector_trailer(i)) {
+                problems.all_problems |=
+                    gen2_poller_can_write_sector_trailer(mfc_data, i).all_problems;
+            } else {
+                problems.all_problems |=
+                    gen2_poller_can_write_data_block(mfc_data, i).all_problems;
+            }
+        }
+    } else {
+        problems.no_data = true;
+    }
+
+    return problems;
+}
+
+Gen2PollerWriteProblems gen2_poller_check_source_problems(NfcDevice* source_dev) {
+    furi_assert(source_dev);
+
+    Gen2PollerWriteProblems problems = {0};
+    const MfClassicData* mfc_data = nfc_device_get_data(source_dev, NfcProtocolMfClassic);
+
+    if(mfc_data) {
+        uint16_t total_block_num = mf_classic_get_total_block_num(mfc_data->type);
+        for(uint16_t i = 0; i < total_block_num; i++) {
+            if(!mf_classic_is_block_read(mfc_data, i)) {
+                problems.missing_source_data = true;
+            }
+        }
+    }
+
+    return problems;
+}

+ 97 - 0
magic/protocols/gen2/gen2_poller.h

@@ -0,0 +1,97 @@
+#pragma once
+
+#include <nfc/nfc.h>
+#include <nfc/protocols/nfc_generic_event.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include <nfc/protocols/iso14443_3a/iso14443_3a.h>
+#include <nfc/protocols/iso14443_3a/iso14443_3a_poller.h>
+#include <nfc/nfc_device.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    Gen2PollerErrorNone,
+    Gen2PollerErrorNotPresent,
+    Gen2PollerErrorProtocol,
+    Gen2PollerErrorAuth,
+    Gen2PollerErrorTimeout,
+    Gen2PollerErrorAccess,
+} Gen2PollerError;
+
+// Possible write problems, sorted by priority top to bottom
+typedef union {
+    uint8_t all_problems;
+    struct {
+        bool uid_locked : 1; // UID may be non-rewritable. Check data after writing
+        bool no_data : 1; // Shouldn't happen, mfc_data missing in nfc device
+        bool locked_access_bits : 1; // Access bits on the target card don't allow writing in some cases
+        bool missing_target_keys : 1; // Keys to write some sectors are not available
+        bool missing_source_data : 1; // The source dump is incomplete
+    };
+} Gen2PollerWriteProblems;
+
+#define GEN2_POLLER_WRITE_PROBLEMS_LEN (5)
+
+extern const char* const gen2_problem_strings[];
+
+typedef enum {
+    Gen2PollerEventTypeDetected,
+    Gen2PollerEventTypeRequestMode,
+    Gen2PollerEventTypeRequestDataToWrite,
+    Gen2PollerEventTypeRequestTargetData,
+
+    Gen2PollerEventTypeSuccess,
+    Gen2PollerEventTypeFail,
+} Gen2PollerEventType;
+
+typedef enum {
+    Gen2PollerModeWipe,
+    Gen2PollerModeWrite,
+} Gen2PollerMode;
+
+typedef struct {
+    Gen2PollerMode mode;
+} Gen2PollerEventDataRequestMode;
+
+typedef struct {
+    const MfClassicData* mfc_data;
+} Gen2PollerEventDataRequestDataToWrite;
+
+typedef struct {
+    const MfClassicData* mfc_data;
+} Gen2PollerEventDataRequestTargetData;
+
+typedef union {
+    Gen2PollerEventDataRequestMode poller_mode;
+    Gen2PollerEventDataRequestDataToWrite data_to_write;
+    Gen2PollerEventDataRequestTargetData target_data;
+} Gen2PollerEventData;
+
+typedef struct {
+    Gen2PollerEventType type;
+    Gen2PollerEventData* data;
+} Gen2PollerEvent;
+
+typedef NfcCommand (*Gen2PollerCallback)(Gen2PollerEvent event, void* context);
+
+typedef struct Gen2Poller Gen2Poller;
+
+Gen2PollerError gen2_poller_detect(Nfc* nfc);
+
+Gen2Poller* gen2_poller_alloc(Nfc* nfc);
+
+void gen2_poller_free(Gen2Poller* instance);
+
+void gen2_poller_start(Gen2Poller* instance, Gen2PollerCallback callback, void* context);
+
+void gen2_poller_stop(Gen2Poller* instance);
+
+Gen2PollerWriteProblems gen2_poller_check_target_problems(NfcDevice* target_dev);
+
+Gen2PollerWriteProblems gen2_poller_check_source_problems(NfcDevice* source_dev);
+
+#ifdef __cplusplus
+}
+#endif

+ 629 - 0
magic/protocols/gen2/gen2_poller_i.c

@@ -0,0 +1,629 @@
+#include "gen2_poller_i.h"
+#include <nfc/helpers/iso14443_crc.h>
+
+#include <bit_lib/bit_lib.h>
+#include "furi_hal_random.h"
+
+#include <furi/furi.h>
+
+#define TAG "GEN2_I"
+
+MfClassicError mf_classic_process_error(Iso14443_3aError error) {
+    MfClassicError ret = MfClassicErrorNone;
+
+    switch(error) {
+    case Iso14443_3aErrorNone:
+        ret = MfClassicErrorNone;
+        break;
+    case Iso14443_3aErrorNotPresent:
+        ret = MfClassicErrorNotPresent;
+        break;
+    case Iso14443_3aErrorColResFailed:
+    case Iso14443_3aErrorCommunication:
+    case Iso14443_3aErrorWrongCrc:
+        ret = MfClassicErrorProtocol;
+        break;
+    case Iso14443_3aErrorTimeout:
+        ret = MfClassicErrorTimeout;
+        break;
+    default:
+        ret = MfClassicErrorProtocol;
+        break;
+    }
+    return ret;
+}
+
+Gen2PollerError gen2_poller_process_iso3_error(Iso14443_3aError error) {
+    Gen2PollerError ret = Gen2PollerErrorNone;
+
+    switch(error) {
+    case Iso14443_3aErrorNone:
+        ret = Gen2PollerErrorNone;
+        break;
+    case Iso14443_3aErrorNotPresent:
+        ret = Gen2PollerErrorNotPresent;
+        break;
+    case Iso14443_3aErrorWrongCrc:
+        ret = Gen2PollerErrorProtocol;
+        break;
+    case Iso14443_3aErrorTimeout:
+        ret = Gen2PollerErrorTimeout;
+        break;
+    default:
+        ret = Gen2PollerErrorProtocol;
+        break;
+    }
+    return ret;
+}
+
+Gen2PollerError gen2_poller_process_mifare_classic_error(MfClassicError error) {
+    Gen2PollerError ret = Gen2PollerErrorNone;
+
+    switch(error) {
+    case MfClassicErrorNone:
+        ret = Gen2PollerErrorNone;
+        break;
+    case MfClassicErrorNotPresent:
+        ret = Gen2PollerErrorNotPresent;
+        break;
+    case MfClassicErrorProtocol:
+        ret = Gen2PollerErrorProtocol;
+        break;
+    case MfClassicErrorAuth:
+        ret = Gen2PollerErrorAuth;
+        break;
+    case MfClassicErrorTimeout:
+        ret = Gen2PollerErrorTimeout;
+        break;
+    default:
+        ret = Gen2PollerErrorProtocol;
+        break;
+    }
+
+    return ret;
+}
+
+static Gen2PollerError gen2_poller_get_nt_common(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicNt* nt,
+    bool is_nested) {
+    MfClassicError ret = MfClassicErrorNone;
+    Iso14443_3aError error = Iso14443_3aErrorNone;
+
+    do {
+        uint8_t auth_type = (key_type == MfClassicKeyTypeB) ? MF_CLASSIC_CMD_AUTH_KEY_B :
+                                                              MF_CLASSIC_CMD_AUTH_KEY_A;
+        uint8_t auth_cmd[2] = {auth_type, block_num};
+        bit_buffer_copy_bytes(instance->tx_plain_buffer, auth_cmd, sizeof(auth_cmd));
+
+        if(is_nested) {
+            iso14443_crc_append(Iso14443CrcTypeA, instance->tx_plain_buffer);
+            crypto1_encrypt(
+                instance->crypto, NULL, instance->tx_plain_buffer, instance->tx_encrypted_buffer);
+            error = iso14443_3a_poller_txrx_custom_parity(
+                instance->iso3_poller,
+                instance->tx_encrypted_buffer,
+                instance->rx_plain_buffer, // NT gets decrypted by mf_classic_async_auth
+                GEN2_POLLER_MAX_FWT);
+            if(error != Iso14443_3aErrorNone) {
+                ret = mf_classic_process_error(error);
+                break;
+            }
+        } else {
+            FURI_LOG_D(TAG, "Plain auth cmd");
+            error = iso14443_3a_poller_send_standard_frame(
+                instance->iso3_poller,
+                instance->tx_plain_buffer,
+                instance->rx_plain_buffer,
+                GEN2_POLLER_MAX_FWT);
+            if(error != Iso14443_3aErrorWrongCrc) {
+                ret = mf_classic_process_error(error);
+                break;
+            }
+        }
+        if(bit_buffer_get_size_bytes(instance->rx_plain_buffer) != sizeof(MfClassicNt)) {
+            ret = MfClassicErrorProtocol;
+            break;
+        }
+
+        if(nt) {
+            bit_buffer_write_bytes(instance->rx_plain_buffer, nt->data, sizeof(MfClassicNt));
+        }
+    } while(false);
+
+    return gen2_poller_process_mifare_classic_error(ret);
+}
+
+Gen2PollerError gen2_poller_get_nt(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicNt* nt) {
+    return gen2_poller_get_nt_common(instance, block_num, key_type, nt, false);
+}
+
+Gen2PollerError gen2_poller_get_nt_nested(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicNt* nt) {
+    return gen2_poller_get_nt_common(instance, block_num, key_type, nt, true);
+}
+
+static Gen2PollerError gen2_poller_auth_common(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    MfClassicKey* key,
+    MfClassicKeyType key_type,
+    MfClassicAuthContext* data,
+    bool is_nested) {
+    Gen2PollerError ret = Gen2PollerErrorNone;
+    Iso14443_3aError error = Iso14443_3aErrorNone;
+
+    do {
+        iso14443_3a_copy(instance->data->iso14443_3a_data, nfc_poller_get_data(instance->poller));
+
+        MfClassicNt nt = {};
+        if(is_nested) {
+            ret = gen2_poller_get_nt_nested(instance, block_num, key_type, &nt);
+        } else {
+            ret = gen2_poller_get_nt(instance, block_num, key_type, &nt);
+        }
+        if(ret != Gen2PollerErrorNone) break;
+        if(data) {
+            data->nt = nt;
+        }
+
+        uint32_t cuid = iso14443_3a_get_cuid(instance->data->iso14443_3a_data);
+        uint64_t key_num = bit_lib_bytes_to_num_be(key->data, sizeof(MfClassicKey));
+        MfClassicNr nr = {};
+        furi_hal_random_fill_buf(nr.data, sizeof(MfClassicNr));
+
+        crypto1_encrypt_reader_nonce(
+            instance->crypto,
+            key_num,
+            cuid,
+            nt.data,
+            nr.data,
+            instance->tx_encrypted_buffer,
+            is_nested);
+        error = iso14443_3a_poller_txrx_custom_parity(
+            instance->iso3_poller,
+            instance->tx_encrypted_buffer,
+            instance->rx_encrypted_buffer,
+            GEN2_POLLER_MAX_FWT);
+
+        if(error != Iso14443_3aErrorNone) {
+            ret = gen2_poller_process_iso3_error(error);
+            break;
+        }
+        if(bit_buffer_get_size_bytes(instance->rx_encrypted_buffer) != 4) {
+            ret = Gen2PollerErrorAuth;
+        }
+
+        crypto1_word(instance->crypto, 0, 0);
+        instance->auth_state = Gen2AuthStatePassed;
+
+        if(data) {
+            data->nr = nr;
+            const uint8_t* nr_ar = bit_buffer_get_data(instance->tx_encrypted_buffer);
+            memcpy(data->ar.data, &nr_ar[4], sizeof(MfClassicAr));
+            bit_buffer_write_bytes(
+                instance->rx_encrypted_buffer, data->at.data, sizeof(MfClassicAt));
+        }
+    } while(false);
+
+    if(ret != Gen2PollerErrorNone) {
+        iso14443_3a_poller_halt(instance->iso3_poller);
+    }
+
+    return ret;
+}
+
+Gen2PollerError gen2_poller_auth(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    MfClassicKey* key,
+    MfClassicKeyType key_type,
+    MfClassicAuthContext* data) {
+    return gen2_poller_auth_common(instance, block_num, key, key_type, data, false);
+}
+
+Gen2PollerError gen2_poller_halt(Gen2Poller* instance) {
+    Gen2PollerError ret = Gen2PollerErrorNone;
+    Iso14443_3aError error = Iso14443_3aErrorNone;
+
+    do {
+        uint8_t halt_cmd[2] = {MF_CLASSIC_CMD_HALT_MSB, MF_CLASSIC_CMD_HALT_LSB};
+        bit_buffer_copy_bytes(instance->tx_plain_buffer, halt_cmd, sizeof(halt_cmd));
+        iso14443_crc_append(Iso14443CrcTypeA, instance->tx_plain_buffer);
+
+        if(instance->auth_state == Gen2AuthStatePassed) {
+            // Send an encrypted halt command
+            crypto1_encrypt(
+                instance->crypto, NULL, instance->tx_plain_buffer, instance->tx_encrypted_buffer);
+            FURI_LOG_D(TAG, "Send enc halt");
+            error = iso14443_3a_poller_txrx_custom_parity(
+                instance->iso3_poller,
+                instance->tx_encrypted_buffer,
+                instance->rx_encrypted_buffer,
+                GEN2_POLLER_MAX_FWT);
+        }
+
+        if(error != Iso14443_3aErrorNone) {
+            FURI_LOG_D(TAG, "Enc halt error");
+            // Do not break because we still need to halt the iso3 poller
+        }
+
+        // Send a regular halt command to halt the iso3 poller
+        FURI_LOG_D(TAG, "Send reg halt");
+        error = iso14443_3a_poller_halt(instance->iso3_poller);
+
+        if(error != Iso14443_3aErrorTimeout) {
+            FURI_LOG_D(TAG, "Reg halt error");
+            // Do not break as well becaue the first halt command might have worked
+            // and the card didn't respond because it was already halted
+        }
+
+        crypto1_reset(instance->crypto);
+        instance->auth_state = Gen2AuthStateIdle;
+    } while(false);
+
+    return ret;
+}
+
+Gen2PollerError
+    gen2_poller_write_block(Gen2Poller* instance, uint8_t block_num, const MfClassicBlock* data) {
+    Gen2PollerError ret = Gen2PollerErrorNone;
+    Iso14443_3aError error = Iso14443_3aErrorNone;
+
+    do {
+        uint8_t write_block_cmd[2] = {MF_CLASSIC_CMD_WRITE_BLOCK, block_num};
+        bit_buffer_copy_bytes(instance->tx_plain_buffer, write_block_cmd, sizeof(write_block_cmd));
+        iso14443_crc_append(Iso14443CrcTypeA, instance->tx_plain_buffer);
+
+        crypto1_encrypt(
+            instance->crypto, NULL, instance->tx_plain_buffer, instance->tx_encrypted_buffer);
+
+        error = iso14443_3a_poller_txrx_custom_parity(
+            instance->iso3_poller,
+            instance->tx_encrypted_buffer,
+            instance->rx_encrypted_buffer,
+            GEN2_POLLER_MAX_FWT);
+        if(error != Iso14443_3aErrorNone) {
+            ret = gen2_poller_process_iso3_error(error);
+            break;
+        }
+        if(bit_buffer_get_size(instance->rx_encrypted_buffer) != 4) {
+            ret = Gen2PollerErrorProtocol;
+            break;
+        }
+
+        crypto1_decrypt(
+            instance->crypto, instance->rx_encrypted_buffer, instance->rx_plain_buffer);
+
+        if(bit_buffer_get_byte(instance->rx_plain_buffer, 0) != MF_CLASSIC_CMD_ACK) {
+            FURI_LOG_D(TAG, "NACK received");
+            ret = Gen2PollerErrorProtocol;
+            break;
+        }
+
+        bit_buffer_copy_bytes(instance->tx_plain_buffer, data->data, sizeof(MfClassicBlock));
+        iso14443_crc_append(Iso14443CrcTypeA, instance->tx_plain_buffer);
+
+        crypto1_encrypt(
+            instance->crypto, NULL, instance->tx_plain_buffer, instance->tx_encrypted_buffer);
+
+        error = iso14443_3a_poller_txrx_custom_parity(
+            instance->iso3_poller,
+            instance->tx_encrypted_buffer,
+            instance->rx_encrypted_buffer,
+            GEN2_POLLER_MAX_FWT);
+        if(error != Iso14443_3aErrorNone) {
+            ret = gen2_poller_process_iso3_error(error);
+            break;
+        }
+        if(bit_buffer_get_size(instance->rx_encrypted_buffer) != 4) {
+            ret = Gen2PollerErrorProtocol;
+            break;
+        }
+
+        crypto1_decrypt(
+            instance->crypto, instance->rx_encrypted_buffer, instance->rx_plain_buffer);
+
+        if(bit_buffer_get_byte(instance->rx_plain_buffer, 0) != MF_CLASSIC_CMD_ACK) {
+            FURI_LOG_D(TAG, "NACK received");
+            ret = Gen2PollerErrorProtocol;
+            break;
+        }
+    } while(false);
+
+    return ret;
+}
+
+bool gen2_poller_can_write_block(const MfClassicData* target_data, uint8_t block_num) {
+    furi_assert(target_data);
+
+    bool can_write = true;
+
+    if(block_num == 0 && target_data->iso14443_3a_data->uid_len == 7) {
+        // 7-byte UID gen2 cards are not supported yet, need further testing
+        can_write = false;
+    }
+
+    if(mf_classic_is_sector_trailer(block_num)) {
+        can_write = gen2_poller_can_write_sector_trailer(target_data, block_num).all_problems == 0;
+    } else {
+        can_write = gen2_poller_can_write_data_block(target_data, block_num).all_problems == 0;
+    }
+
+    return can_write;
+}
+
+Gen2PollerWriteProblems
+    gen2_poller_can_write_data_block(const MfClassicData* target_data, uint8_t block_num) {
+    // Check whether it's possible to write the block
+    furi_assert(target_data);
+
+    // Check rules:
+    // 1. Check if block is read
+    // 2. Check if we have any of the keys
+    // 3. For each key, check if we can write the block
+    // 3.1. If none of the keys can write the block, check whether access conditions can be reset to allow writing
+    // 3.2 If the above conditions are not met, return an error code
+
+    Gen2PollerWriteProblems can_write = {0};
+
+    bool has_key_a = mf_classic_is_key_found(
+        target_data, mf_classic_get_sector_by_block(block_num), MfClassicKeyTypeA);
+    bool has_key_b = mf_classic_is_key_found(
+        target_data, mf_classic_get_sector_by_block(block_num), MfClassicKeyTypeB);
+
+    if(!has_key_a && !has_key_b) {
+        can_write.missing_target_keys = true;
+    }
+    if(!gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeA, MfClassicActionDataWrite) &&
+       !gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeB, MfClassicActionDataWrite)) {
+        if(!gen2_can_reset_access_conditions(target_data, block_num)) {
+            can_write.locked_access_bits = true;
+        }
+    }
+
+    return can_write;
+}
+
+Gen2PollerWriteProblems
+    gen2_poller_can_write_sector_trailer(const MfClassicData* target_data, uint8_t block_num) {
+    // Check whether it's possible to write the sector trailer
+    furi_assert(target_data);
+
+    // Check rules:
+    // 1. Check if block is read
+    // 2. Check if we have any of the keys
+    // 3. For each key, check if we can write the block
+    // 3.1 Check that at least one of the keys can write Key A
+    // 3.1.1 If none of the keys can write Key A, check whether access conditions can be reset to allow writing
+    // 3.2 Check that at least one of the keys can write the Access Conditions
+    // 3.3 Check that at least one of the keys can write Key B
+    // 3.3.1 If none of the keys can write Key B, check whether access conditions can be reset to allow writing
+    // 3.4 If any of the above conditions are not met, return an error code
+
+    Gen2PollerWriteProblems can_write = {0};
+
+    bool has_key_a = mf_classic_is_key_found(
+        target_data, mf_classic_get_sector_by_block(block_num), MfClassicKeyTypeA);
+    bool has_key_b = mf_classic_is_key_found(
+        target_data, mf_classic_get_sector_by_block(block_num), MfClassicKeyTypeB);
+
+    if(!has_key_a && !has_key_b) {
+        can_write.missing_target_keys = true;
+    }
+    if(!gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeA, MfClassicActionKeyAWrite) &&
+       !gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeB, MfClassicActionKeyAWrite)) {
+        if(!gen2_can_reset_access_conditions(target_data, block_num)) {
+            can_write.locked_access_bits = true;
+        }
+    }
+    if(!gen2_is_allowed_access(target_data, block_num, MfClassicKeyTypeA, MfClassicActionACWrite) &&
+       !gen2_is_allowed_access(target_data, block_num, MfClassicKeyTypeB, MfClassicActionACWrite)) {
+        can_write.locked_access_bits = true;
+    }
+    if(!gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeA, MfClassicActionKeyBWrite) &&
+       !gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeB, MfClassicActionKeyBWrite)) {
+        if(!gen2_can_reset_access_conditions(target_data, block_num)) {
+            can_write.locked_access_bits = true;
+        }
+    }
+
+    return can_write;
+}
+
+bool gen2_can_reset_access_conditions(const MfClassicData* target_data, uint8_t block_num) {
+    // Check whether it's possible to reset the access conditions
+    furi_assert(target_data);
+
+    // Check rules:
+    // 1. Check if the sector trailer for this block is read
+    // 2. Check if we have any of the keys
+    // 3. For each key, check if we can write the access conditions
+    // 3.1. If none of the keys can write the access conditions, return false
+
+    bool can_reset = false;
+
+    bool has_key_a = mf_classic_is_key_found(
+        target_data, mf_classic_get_sector_by_block(block_num), MfClassicKeyTypeA);
+    bool has_key_b = mf_classic_is_key_found(
+        target_data, mf_classic_get_sector_by_block(block_num), MfClassicKeyTypeB);
+    uint8_t sector_tr_num = mf_classic_get_sector_trailer_num_by_block(block_num);
+
+    if(!mf_classic_is_block_read(target_data, sector_tr_num)) {
+        can_reset = false;
+        return can_reset;
+    }
+
+    if(!has_key_a && !has_key_b) {
+        can_reset = false;
+        return can_reset;
+    }
+    if(gen2_is_allowed_access(target_data, block_num, MfClassicKeyTypeA, MfClassicActionACWrite) ||
+       gen2_is_allowed_access(target_data, block_num, MfClassicKeyTypeB, MfClassicActionACWrite)) {
+        can_reset = true;
+    }
+
+    return can_reset;
+}
+
+MfClassicKeyType
+    gen2_poller_get_key_type_to_write(const MfClassicData* target_data, uint8_t block_num) {
+    // Get the key type to use for writing
+    // We assume that at least one of the keys can write the block
+    furi_assert(target_data);
+
+    MfClassicKeyType key_type = MfClassicKeyTypeA;
+
+    if(gen2_is_allowed_access(
+           target_data, block_num, MfClassicKeyTypeA, MfClassicActionDataWrite)) {
+        key_type = MfClassicKeyTypeA;
+    } else if(gen2_is_allowed_access(
+                  target_data, block_num, MfClassicKeyTypeB, MfClassicActionDataWrite)) {
+        key_type = MfClassicKeyTypeB;
+    }
+
+    return key_type;
+}
+
+static bool gen2_is_allowed_access_sector_trailer(
+    const MfClassicData* data,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicAction action) {
+    uint8_t sector_num = mf_classic_get_sector_by_block(block_num);
+    MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, sector_num);
+    uint8_t* access_bits_arr = sec_tr->access_bits.data;
+    uint8_t AC = ((access_bits_arr[1] >> 5) & 0x04) | ((access_bits_arr[2] >> 2) & 0x02) |
+                 ((access_bits_arr[2] >> 7) & 0x01);
+    FURI_LOG_T("NFC", "AC: %02X", AC);
+
+    switch(action) {
+    case MfClassicActionKeyARead: {
+        return false;
+    }
+    case MfClassicActionKeyAWrite:
+    case MfClassicActionKeyBWrite: {
+        return (
+            (key_type == MfClassicKeyTypeA && (AC == 0x00 || AC == 0x01)) ||
+            (key_type == MfClassicKeyTypeB &&
+             (AC == 0x00 || AC == 0x04 || AC == 0x03 || AC == 0x01)));
+    }
+    case MfClassicActionKeyBRead: {
+        return (key_type == MfClassicKeyTypeA && (AC == 0x00 || AC == 0x02 || AC == 0x01)) ||
+               (key_type == MfClassicKeyTypeB && (AC == 0x00 || AC == 0x02 || AC == 0x01));
+    }
+    case MfClassicActionACRead: {
+        return ((key_type == MfClassicKeyTypeA) || (key_type == MfClassicKeyTypeB));
+    }
+    case MfClassicActionACWrite: {
+        return (
+            (key_type == MfClassicKeyTypeA && (AC == 0x01)) ||
+            (key_type == MfClassicKeyTypeB && (AC == 0x01 || AC == 0x03 || AC == 0x05)));
+    }
+    default:
+        return false;
+    }
+    return true;
+}
+
+bool gen2_is_allowed_access_data_block(
+    MfClassicSectorTrailer* sec_tr,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicAction action) {
+    // Same as mf_classic_is_allowed_access_data_block but with sector 0 allowed
+    furi_assert(sec_tr);
+
+    uint8_t* access_bits_arr = sec_tr->access_bits.data;
+
+    uint8_t sector_block = 0;
+    if(block_num <= 128) {
+        sector_block = block_num & 0x03;
+    } else {
+        sector_block = (block_num & 0x0f) / 5;
+    }
+
+    uint8_t AC;
+    switch(sector_block) {
+    case 0x00: {
+        AC = ((access_bits_arr[1] >> 2) & 0x04) | ((access_bits_arr[2] << 1) & 0x02) |
+             ((access_bits_arr[2] >> 4) & 0x01);
+        break;
+    }
+    case 0x01: {
+        AC = ((access_bits_arr[1] >> 3) & 0x04) | ((access_bits_arr[2] >> 0) & 0x02) |
+             ((access_bits_arr[2] >> 5) & 0x01);
+        break;
+    }
+    case 0x02: {
+        AC = ((access_bits_arr[1] >> 4) & 0x04) | ((access_bits_arr[2] >> 1) & 0x02) |
+             ((access_bits_arr[2] >> 6) & 0x01);
+        break;
+    }
+    default:
+        return false;
+    }
+
+    switch(action) {
+    case MfClassicActionDataRead: {
+        return (
+            (key_type == MfClassicKeyTypeA && !(AC == 0x03 || AC == 0x05 || AC == 0x07)) ||
+            (key_type == MfClassicKeyTypeB && !(AC == 0x07)));
+    }
+    case MfClassicActionDataWrite: {
+        return (
+            (key_type == MfClassicKeyTypeA && (AC == 0x00)) ||
+            (key_type == MfClassicKeyTypeB &&
+             (AC == 0x00 || AC == 0x04 || AC == 0x06 || AC == 0x03)));
+    }
+    case MfClassicActionDataInc: {
+        return (
+            (key_type == MfClassicKeyTypeA && (AC == 0x00)) ||
+            (key_type == MfClassicKeyTypeB && (AC == 0x00 || AC == 0x06)));
+    }
+    case MfClassicActionDataDec: {
+        return (
+            (key_type == MfClassicKeyTypeA && (AC == 0x00 || AC == 0x06 || AC == 0x01)) ||
+            (key_type == MfClassicKeyTypeB && (AC == 0x00 || AC == 0x06 || AC == 0x01)));
+    }
+    default:
+        return false;
+    }
+
+    return false;
+}
+
+bool gen2_is_allowed_access(
+    const MfClassicData* data,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicAction action) {
+    // Same as mf_classic_is_allowed_access but with sector 0 allowed
+    furi_assert(data);
+
+    bool access_allowed = false;
+    if(mf_classic_is_sector_trailer(block_num)) {
+        access_allowed = gen2_is_allowed_access_sector_trailer(data, block_num, key_type, action);
+    } else {
+        uint8_t sector_num = mf_classic_get_sector_by_block(block_num);
+        MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, sector_num);
+        access_allowed = gen2_is_allowed_access_data_block(sec_tr, block_num, key_type, action);
+    }
+
+    return access_allowed;
+}

+ 139 - 0
magic/protocols/gen2/gen2_poller_i.h

@@ -0,0 +1,139 @@
+#pragma once
+
+#include "gen2_poller.h"
+#include <nfc/protocols/nfc_generic_event.h>
+#include "crypto1.h" // TODO: Move to a better home
+#include <nfc/protocols/iso14443_3a/iso14443_3a_poller.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define GEN2_CMD_READ_ATS (0xE0)
+#define GEN2_FSDI_256 (0x8U)
+
+#define GEN2_POLLER_BLOCK_SIZE (16)
+
+#define GEN2_POLLER_MAX_BUFFER_SIZE (64U)
+#define GEN2_POLLER_MAX_FWT (150000U)
+
+typedef enum {
+    Gen2PollerStateIdle,
+    Gen2PollerStateRequestMode,
+    Gen2PollerStateWipe,
+    Gen2PollerStateWriteSourceDataRequest,
+    Gen2PollerStateWriteTargetDataRequest,
+    Gen2PollerStateWrite,
+    Gen2PollerStateSuccess,
+    Gen2PollerStateFail,
+
+    Gen2PollerStateNum,
+} Gen2PollerState;
+
+typedef enum {
+    Gen2AuthStateIdle,
+    Gen2AuthStatePassed,
+} Gen2AuthState;
+
+typedef enum {
+    Gen2CardStateDetected,
+    Gen2CardStateLost,
+} Gen2CardState;
+
+typedef struct {
+    MfClassicData* mfc_data_source;
+    MfClassicData* mfc_data_target;
+    MfClassicKey auth_key;
+    MfClassicKeyType read_key;
+    MfClassicKeyType write_key;
+    uint16_t current_block;
+    bool need_halt_before_write;
+} Gen2PollerWriteContext;
+
+typedef union {
+    Gen2PollerWriteContext write_ctx;
+} Gen2PollerModeContext;
+
+struct Gen2Poller {
+    Nfc* nfc;
+    Gen2PollerState state;
+
+    NfcPoller* poller;
+    Iso14443_3aPoller* iso3_poller;
+
+    Gen2AuthState auth_state;
+    Gen2CardState card_state;
+
+    Gen2PollerModeContext mode_ctx;
+    Gen2PollerMode mode;
+
+    Crypto1* crypto;
+    BitBuffer* tx_plain_buffer;
+    BitBuffer* tx_encrypted_buffer;
+    BitBuffer* rx_plain_buffer;
+    BitBuffer* rx_encrypted_buffer;
+    MfClassicData* data;
+
+    Gen2PollerEvent gen2_event;
+    Gen2PollerEventData gen2_event_data;
+
+    Gen2PollerCallback callback;
+    void* context;
+};
+
+typedef struct {
+    uint8_t block;
+    MfClassicKeyType key_type;
+    MfClassicNt nt;
+} Gen2CollectNtContext;
+
+typedef struct {
+    uint8_t block_num;
+    MfClassicKey key;
+    MfClassicKeyType key_type;
+    MfClassicBlock block;
+} Gen2ReadBlockContext;
+
+typedef struct {
+    uint8_t block_num;
+    MfClassicKey key;
+    MfClassicKeyType key_type;
+    MfClassicBlock block;
+} Gen2WriteBlockContext;
+
+Gen2PollerError gen2_poller_write(Gen2Poller* instance);
+
+Gen2PollerError gen2_poller_auth(
+    Gen2Poller* instance,
+    uint8_t block_num,
+    MfClassicKey* key,
+    MfClassicKeyType key_type,
+    MfClassicAuthContext* data);
+
+Gen2PollerError gen2_poller_halt(Gen2Poller* instance);
+
+Gen2PollerError
+    gen2_poller_write_block(Gen2Poller* instance, uint8_t block_num, const MfClassicBlock* data);
+
+MfClassicKeyType
+    gen2_poller_get_key_type_to_write(const MfClassicData* mfc_data, uint8_t block_num);
+
+bool gen2_poller_can_write_block(const MfClassicData* mfc_data, uint8_t block_num);
+
+bool gen2_can_reset_access_conditions(const MfClassicData* mfc_data, uint8_t block_num);
+
+Gen2PollerWriteProblems
+    gen2_poller_can_write_data_block(const MfClassicData* mfc_data, uint8_t block_num);
+
+Gen2PollerWriteProblems
+    gen2_poller_can_write_sector_trailer(const MfClassicData* mfc_data, uint8_t block_num);
+
+bool gen2_is_allowed_access(
+    const MfClassicData* data,
+    uint8_t block_num,
+    MfClassicKeyType key_type,
+    MfClassicAction action);
+
+#ifdef __cplusplus
+}
+#endif

+ 1 - 0
lib/magic/protocols/gen4/gen4_poller.c → magic/protocols/gen4/gen4_poller.c

@@ -3,6 +3,7 @@
 #include <nfc/protocols/iso14443_3a/iso14443_3a.h>
 #include <nfc/protocols/iso14443_3a/iso14443_3a_poller.h>
 #include <nfc/helpers/nfc_util.h>
+#include <bit_lib/bit_lib.h>
 #include <nfc/nfc_poller.h>
 
 #include <furi/furi.h>

+ 0 - 0
lib/magic/protocols/gen4/gen4_poller.h → magic/protocols/gen4/gen4_poller.h


+ 0 - 0
lib/magic/protocols/gen4/gen4_poller_i.c → magic/protocols/gen4/gen4_poller_i.c


+ 0 - 0
lib/magic/protocols/gen4/gen4_poller_i.h → magic/protocols/gen4/gen4_poller_i.h


+ 4 - 2
lib/magic/protocols/nfc_magic_protocols.c → magic/protocols/nfc_magic_protocols.c

@@ -3,8 +3,10 @@
 #include <furi/furi.h>
 
 static const char* nfc_magic_protocol_names[NfcMagicProtocolNum] = {
-    [NfcMagicProtocolGen1] = "Classic Gen 1A/B",
-    [NfcMagicProtocolGen4] = "Gen 4 GTU",
+    [NfcMagicProtocolGen1] = "Gen1A/B",
+    [NfcMagicProtocolGen2] = "Gen2",
+    [NfcMagicProtocolClassic] = "MIFARE Classic",
+    [NfcMagicProtocolGen4] = "Gen4 GTU",
 };
 
 const char* nfc_magic_protocols_get_name(NfcMagicProtocol protocol) {

+ 2 - 0
lib/magic/protocols/nfc_magic_protocols.h → magic/protocols/nfc_magic_protocols.h

@@ -6,7 +6,9 @@ extern "C" {
 
 typedef enum {
     NfcMagicProtocolGen1,
+    NfcMagicProtocolGen2,
     NfcMagicProtocolGen4,
+    NfcMagicProtocolClassic, // Last to give priority to the others
 
     NfcMagicProtocolNum,
     NfcMagicProtocolInvalid,

+ 30 - 2
nfc_magic_app.c

@@ -48,13 +48,16 @@ NfcMagicApp* nfc_magic_app_alloc() {
     view_dispatcher_set_tick_event_callback(
         instance->view_dispatcher, nfc_magic_app_tick_event_callback, 100);
 
-    // Nfc device
+    // NFC source device (file)
     instance->source_dev = nfc_device_alloc();
     nfc_device_set_loading_callback(
         instance->source_dev, nfc_magic_app_show_loading_popup, instance);
     instance->file_path = furi_string_alloc_set(NFC_APP_FOLDER);
     instance->file_name = furi_string_alloc();
 
+    // NFC target device (tag)
+    instance->target_dev = nfc_device_alloc();
+
     // Open GUI record
     instance->gui = furi_record_open(RECORD_GUI);
     view_dispatcher_attach_to_gui(
@@ -103,6 +106,20 @@ NfcMagicApp* nfc_magic_app_alloc() {
     view_dispatcher_add_view(
         instance->view_dispatcher, NfcMagicAppViewWidget, widget_get_view(instance->widget));
 
+    // Dict attack
+    instance->dict_attack = dict_attack_alloc();
+    view_dispatcher_add_view(
+        instance->view_dispatcher,
+        NfcMagicAppViewDictAttack,
+        dict_attack_get_view(instance->dict_attack));
+
+    // Write problems
+    instance->write_problems = write_problems_alloc();
+    view_dispatcher_add_view(
+        instance->view_dispatcher,
+        NfcMagicAppViewWriteProblems,
+        write_problems_get_view(instance->write_problems));
+
     instance->nfc = nfc_alloc();
     instance->scanner = nfc_magic_scanner_alloc(instance->nfc);
 
@@ -112,11 +129,14 @@ NfcMagicApp* nfc_magic_app_alloc() {
 void nfc_magic_app_free(NfcMagicApp* instance) {
     furi_assert(instance);
 
-    // Nfc device
+    // Nfc source device
     nfc_device_free(instance->source_dev);
     furi_string_free(instance->file_name);
     furi_string_free(instance->file_path);
 
+    // Nfc target device
+    nfc_device_free(instance->target_dev);
+
     // Submenu
     view_dispatcher_remove_view(instance->view_dispatcher, NfcMagicAppViewMenu);
     submenu_free(instance->submenu);
@@ -141,6 +161,14 @@ void nfc_magic_app_free(NfcMagicApp* instance) {
     view_dispatcher_remove_view(instance->view_dispatcher, NfcMagicAppViewWidget);
     widget_free(instance->widget);
 
+    // Dict attack
+    view_dispatcher_remove_view(instance->view_dispatcher, NfcMagicAppViewDictAttack);
+    dict_attack_free(instance->dict_attack);
+
+    // Write problems
+    view_dispatcher_remove_view(instance->view_dispatcher, NfcMagicAppViewWriteProblems);
+    write_problems_free(instance->write_problems);
+
     // View Dispatcher
     view_dispatcher_free(instance->view_dispatcher);
 

+ 52 - 4
nfc_magic_app_i.h

@@ -15,6 +15,8 @@
 #include <gui/modules/text_input.h>
 #include <gui/modules/byte_input.h>
 #include <gui/modules/widget.h>
+#include <views/dict_attack.h>
+#include <views/write_problems.h>
 
 #include <input/input.h>
 
@@ -28,16 +30,24 @@
 
 #include <nfc/nfc.h>
 #include <nfc/nfc_device.h>
+#include <nfc/nfc_poller.h>
+#include <toolbox/keys_dict.h>
 
-#include "lib/magic/nfc_magic_scanner.h"
-#include "lib/magic/protocols/nfc_magic_protocols.h"
-#include "lib/magic/protocols/gen1a/gen1a_poller.h"
-#include "lib/magic/protocols/gen4/gen4_poller.h"
+#include "magic/nfc_magic_scanner.h"
+#include "magic/protocols/nfc_magic_protocols.h"
+#include "magic/protocols/gen1a/gen1a_poller.h"
+#include "magic/protocols/gen2/gen2_poller.h"
+#include "magic/protocols/gen4/gen4_poller.h"
+
+#include "lib/nfc/protocols/mf_classic/mf_classic_poller.h"
 
 #define NFC_APP_FOLDER ANY_PATH("nfc")
 #define NFC_APP_EXTENSION ".nfc"
 #define NFC_APP_SHADOW_EXTENSION ".shd"
 
+#define NFC_APP_MF_CLASSIC_DICT_USER_PATH (NFC_APP_FOLDER "/assets/mf_classic_dict_user.nfc")
+#define NFC_APP_MF_CLASSIC_DICT_SYSTEM_PATH (NFC_APP_FOLDER "/assets/mf_classic_dict.nfc")
+
 #define NFC_MAGIC_APP_BYTE_INPUT_STORE_SIZE (4)
 
 enum NfcMagicAppCustomEvent {
@@ -48,8 +58,33 @@ enum NfcMagicAppCustomEvent {
     NfcMagicAppCustomEventWorkerExit,
     NfcMagicAppCustomEventByteInputDone,
     NfcMagicAppCustomEventTextInputDone,
+    NfcMagicAppCustomEventCardDetected,
+    NfcMagicAppCustomEventCardLost,
+    NfcMagicAppCustomEventDictAttackDataUpdate,
+    NfcMagicAppCustomEventDictAttackComplete,
+    NfcMagicAppCustomEventDictAttackSkip,
 };
 
+typedef struct {
+    KeysDict* dict;
+    uint8_t sectors_total;
+    uint8_t sectors_read;
+    uint8_t current_sector;
+    uint8_t keys_found;
+    size_t dict_keys_total;
+    size_t dict_keys_current;
+    bool is_key_attack;
+    uint8_t key_attack_current_sector;
+    bool is_card_present;
+} NfcMagicAppMfClassicDictAttackContext;
+
+typedef struct {
+    uint8_t problem_index;
+    uint8_t problem_index_abs;
+    uint8_t problems_total;
+    Gen2PollerWriteProblems problems;
+} NfcMagicAppWriteProblemsContext;
+
 struct NfcMagicApp {
     ViewDispatcher* view_dispatcher;
     Gui* gui;
@@ -59,15 +94,26 @@ struct NfcMagicApp {
 
     SceneManager* scene_manager;
     NfcDevice* source_dev;
+    NfcDevice* target_dev;
     FuriString* file_name;
     FuriString* file_path;
 
     Nfc* nfc;
     NfcMagicProtocol protocol;
     NfcMagicScanner* scanner;
+    NfcPoller* poller;
     Gen1aPoller* gen1a_poller;
+
+    Gen2Poller* gen2_poller;
+    bool gen2_poller_is_wipe_mode;
+
     Gen4Poller* gen4_poller;
 
+    NfcMagicAppMfClassicDictAttackContext nfc_dict_context;
+    DictAttack* dict_attack;
+    NfcMagicAppWriteProblemsContext write_problems_context;
+    WriteProblems* write_problems;
+
     uint32_t gen4_password;
     uint32_t gen4_password_new;
 
@@ -93,6 +139,8 @@ typedef enum {
     NfcMagicAppViewTextInput,
     NfcMagicAppViewByteInput,
     NfcMagicAppViewWidget,
+    NfcMagicAppViewDictAttack,
+    NfcMagicAppViewWriteProblems,
 } NfcMagicAppView;
 
 void nfc_magic_app_blink_start(NfcMagicApp* nfc_magic);

+ 5 - 0
scenes/nfc_magic_scene_config.h

@@ -22,3 +22,8 @@ ADD_SCENE(nfc_magic, change_key, ChangeKey)
 ADD_SCENE(nfc_magic, change_key_fail, ChangeKeyFail)
 ADD_SCENE(nfc_magic, wrong_card, WrongCard)
 ADD_SCENE(nfc_magic, not_magic, NotMagic)
+ADD_SCENE(nfc_magic, gen2_menu, Gen2Menu)
+ADD_SCENE(nfc_magic, mf_classic_menu, MfClassicMenu)
+ADD_SCENE(nfc_magic, mf_classic_dict_attack, MfClassicDictAttack)
+ADD_SCENE(nfc_magic, gen2_write_check, Gen2WriteCheck)
+ADD_SCENE(nfc_magic, mf_classic_write_check, MfClassicWriteCheck)

+ 15 - 1
scenes/nfc_magic_scene_file_select.c

@@ -30,6 +30,14 @@ static bool nfc_magic_scene_file_select_is_file_suitable(NfcMagicApp* instance)
                            (mfu_type != MfUltralightTypeNTAGI2CPlus2K);
             }
         }
+    } else if(instance->protocol == NfcMagicProtocolGen2) {
+        if(protocol == NfcProtocolMfClassic) {
+            suitable = true;
+        }
+    } else if(instance->protocol == NfcMagicProtocolClassic) {
+        if(protocol == NfcProtocolMfClassic) {
+            suitable = true;
+        }
     }
 
     return suitable;
@@ -40,7 +48,13 @@ void nfc_magic_scene_file_select_on_enter(void* context) {
 
     if(nfc_magic_load_from_file_select(instance)) {
         if(nfc_magic_scene_file_select_is_file_suitable(instance)) {
-            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWriteConfirm);
+            if(instance->protocol == NfcMagicProtocolClassic ||
+               instance->protocol == NfcMagicProtocolGen2) {
+                scene_manager_next_scene(
+                    instance->scene_manager, NfcMagicSceneMfClassicDictAttack);
+            } else {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWriteConfirm);
+            }
         } else {
             scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWrongCard);
         }

+ 55 - 0
scenes/nfc_magic_scene_gen2_menu.c

@@ -0,0 +1,55 @@
+#include "../nfc_magic_app_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexWrite,
+    SubmenuIndexWipe,
+};
+
+void nfc_magic_scene_gen2_menu_submenu_callback(void* context, uint32_t index) {
+    NfcMagicApp* instance = context;
+
+    view_dispatcher_send_custom_event(instance->view_dispatcher, index);
+}
+
+void nfc_magic_scene_gen2_menu_on_enter(void* context) {
+    NfcMagicApp* instance = context;
+
+    Submenu* submenu = instance->submenu;
+    submenu_add_item(
+        submenu, "Write", SubmenuIndexWrite, nfc_magic_scene_gen2_menu_submenu_callback, instance);
+    submenu_add_item(
+        submenu, "Wipe", SubmenuIndexWipe, nfc_magic_scene_gen2_menu_submenu_callback, instance);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(instance->scene_manager, NfcMagicSceneGen2Menu));
+    view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewMenu);
+}
+
+bool nfc_magic_scene_gen2_menu_on_event(void* context, SceneManagerEvent event) {
+    NfcMagicApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexWrite) {
+            instance->gen2_poller_is_wipe_mode = false;
+            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneFileSelect);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWipe) {
+            instance->gen2_poller_is_wipe_mode = true;
+            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneMfClassicDictAttack);
+            consumed = true;
+        }
+        scene_manager_set_scene_state(instance->scene_manager, NfcMagicSceneGen2Menu, event.event);
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_search_and_switch_to_previous_scene(
+            instance->scene_manager, NfcMagicSceneStart);
+    }
+
+    return consumed;
+}
+
+void nfc_magic_scene_gen2_menu_on_exit(void* context) {
+    NfcMagicApp* instance = context;
+
+    submenu_reset(instance->submenu);
+}

+ 130 - 0
scenes/nfc_magic_scene_gen2_write_check.c

@@ -0,0 +1,130 @@
+#include "../nfc_magic_app_i.h"
+
+void nfc_magic_scene_gen2_write_check_view_callback(WriteProblemsEvent event, void* context) {
+    NfcMagicApp* instance = context;
+    NfcMagicAppWriteProblemsContext* problems_context = &instance->write_problems_context;
+
+    if(event == WriteProblemsEventCenterPressed) {
+        if(problems_context->problem_index == problems_context->problems_total - 1) {
+            // Continue to the next scene
+            if(instance->gen2_poller_is_wipe_mode) {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWipe);
+            } else {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWrite);
+            }
+        } else {
+            // Move to the next problem
+            problems_context->problem_index++;
+            problems_context->problem_index_abs++;
+            write_problems_set_problem_index(
+                instance->write_problems, problems_context->problem_index);
+
+            for(uint8_t i = problems_context->problem_index_abs;
+                i < GEN2_POLLER_WRITE_PROBLEMS_LEN;
+                i++) {
+                if(problems_context->problems.all_problems & (1 << i)) {
+                    write_problems_set_content(instance->write_problems, gen2_problem_strings[i]);
+                    problems_context->problem_index_abs = i;
+                    break;
+                }
+            }
+        }
+    } else if(event == WriteProblemsEventLeftPressed) {
+        if(problems_context->problem_index == 0) {
+            // Exit to the previous scene
+            scene_manager_search_and_switch_to_previous_scene(
+                instance->scene_manager, NfcMagicSceneMfClassicMenu);
+        } else {
+            // Move to the previous problem
+            problems_context->problem_index--;
+            problems_context->problem_index_abs--;
+            write_problems_set_problem_index(
+                instance->write_problems, problems_context->problem_index);
+
+            for(uint8_t i = problems_context->problem_index_abs;
+                i < GEN2_POLLER_WRITE_PROBLEMS_LEN;
+                i--) {
+                if(problems_context->problems.all_problems & (1 << i)) {
+                    write_problems_set_content(instance->write_problems, gen2_problem_strings[i]);
+                    problems_context->problem_index_abs = i;
+                    break;
+                }
+            }
+        }
+    }
+}
+
+void nfc_magic_scene_gen2_write_check_on_enter(void* context) {
+    NfcMagicApp* instance = context;
+
+    Gen2PollerWriteProblems problems = gen2_poller_check_target_problems(instance->target_dev);
+    if(!instance->gen2_poller_is_wipe_mode) {
+        problems.all_problems |=
+            gen2_poller_check_source_problems(instance->source_dev).all_problems;
+    }
+
+    WriteProblems* write_problems = instance->write_problems;
+    uint8_t problems_count = 0;
+    uint8_t current_problem = 0;
+    furi_assert(!problems.no_data, "No MFC data in nfc device");
+
+    if(problems.all_problems == 0) {
+        if(instance->gen2_poller_is_wipe_mode) {
+            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWipe);
+            return;
+        } else {
+            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWrite);
+            return;
+        }
+    }
+
+    // Count the total number of problems
+    for(uint8_t i = 0; i < GEN2_POLLER_WRITE_PROBLEMS_LEN; i++) {
+        if(problems.all_problems & (1 << i)) {
+            problems_count++;
+        }
+    }
+
+    // Init the view
+    write_problems_set_callback(
+        write_problems, nfc_magic_scene_gen2_write_check_view_callback, instance);
+    write_problems_set_problems_total(write_problems, problems_count);
+    write_problems_set_problem_index(write_problems, current_problem);
+
+    // Set the initial content to the first problem
+    for(uint8_t i = 0; i < GEN2_POLLER_WRITE_PROBLEMS_LEN; i++) {
+        if(problems.all_problems & (1 << i)) {
+            write_problems_set_content(write_problems, gen2_problem_strings[i]);
+            current_problem = i;
+            break;
+        }
+    }
+
+    // Save the context
+    instance->write_problems_context.problem_index = current_problem;
+    instance->write_problems_context.problems_total = problems_count;
+    instance->write_problems_context.problems = problems;
+
+    // Setup and start worker
+    view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewWriteProblems);
+}
+
+bool nfc_magic_scene_gen2_write_check_on_event(void* context, SceneManagerEvent event) {
+    NfcMagicApp* instance = context;
+    UNUSED(event);
+    UNUSED(context);
+    UNUSED(instance);
+    bool consumed = false;
+
+    return consumed;
+}
+
+void nfc_magic_scene_gen2_write_check_on_exit(void* context) {
+    NfcMagicApp* instance = context;
+
+    instance->write_problems_context.problem_index = 0;
+    instance->write_problems_context.problems_total = 0;
+    instance->write_problems_context.problems.all_problems = 0;
+
+    write_problems_reset(instance->write_problems);
+}

+ 23 - 12
scenes/nfc_magic_scene_magic_info.c

@@ -17,22 +17,27 @@ void nfc_magic_scene_magic_info_on_enter(void* context) {
 
     notification_message(instance->notifications, &sequence_success);
 
-    widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48);
-    widget_add_string_element(
-        widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "Magic card detected");
-    widget_add_string_element(
-        widget,
-        3,
-        17,
-        AlignLeft,
-        AlignTop,
-        FontSecondary,
-        nfc_magic_protocols_get_name(instance->protocol));
+    FuriString* message = furi_string_alloc();
+
+    if(instance->protocol == NfcMagicProtocolClassic) {
+        widget_add_string_element(
+            widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "It Might Be a Magic Card");
+        furi_string_printf(message, "You can make sure the card is\nmagic by writing to it\n");
+    } else {
+        widget_add_string_element(
+            widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "Magic card detected!");
+    }
+    furi_string_cat_printf(
+        message, "Magic Type: %s", nfc_magic_protocols_get_name(instance->protocol));
+    widget_add_text_box_element(
+        widget, 0, 10, 128, 54, AlignLeft, AlignTop, furi_string_get_cstr(message), false);
     widget_add_button_element(
         widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_magic_info_widget_callback, instance);
     widget_add_button_element(
         widget, GuiButtonTypeRight, "More", nfc_magic_scene_magic_info_widget_callback, instance);
 
+    furi_string_free(message);
+
     view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewWidget);
 }
 
@@ -47,9 +52,15 @@ bool nfc_magic_scene_magic_info_on_event(void* context, SceneManagerEvent event)
             if(instance->protocol == NfcMagicProtocolGen1) {
                 scene_manager_next_scene(instance->scene_manager, NfcMagicSceneGen1Menu);
                 consumed = true;
-            } else {
+            } else if(instance->protocol == NfcMagicProtocolGen4) {
                 scene_manager_next_scene(instance->scene_manager, NfcMagicSceneGen4Menu);
                 consumed = true;
+            } else if(instance->protocol == NfcMagicProtocolGen2) {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneGen2Menu);
+                consumed = true;
+            } else if(instance->protocol == NfcMagicProtocolClassic) {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneMfClassicMenu);
+                consumed = true;
             }
         }
     }

+ 311 - 0
scenes/nfc_magic_scene_mf_classic_dict_attack.c

@@ -0,0 +1,311 @@
+#include "../nfc_magic_app_i.h"
+
+#include <dolphin/dolphin.h>
+#include <lib/nfc/protocols/mf_classic/mf_classic_poller.h>
+
+#include "views/dict_attack.h"
+
+#define TAG "NfcMagicMfClassicDictAttack"
+
+typedef enum {
+    DictAttackStateUserDictInProgress,
+    DictAttackStateSystemDictInProgress,
+} DictAttackState;
+
+NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context) {
+    furi_assert(context);
+    furi_assert(event.event_data);
+    furi_assert(event.instance);
+    furi_assert(event.protocol == NfcProtocolMfClassic);
+
+    NfcCommand command = NfcCommandContinue;
+    MfClassicPollerEvent* mfc_event = event.event_data;
+
+    NfcMagicApp* instance = context;
+    if(mfc_event->type == MfClassicPollerEventTypeCardDetected) {
+        instance->nfc_dict_context.is_card_present = true;
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventCardDetected);
+    } else if(mfc_event->type == MfClassicPollerEventTypeCardLost) {
+        instance->nfc_dict_context.is_card_present = false;
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventCardLost);
+    } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) {
+        const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller);
+        nfc_device_set_data(instance->target_dev, NfcProtocolMfClassic, mfc_data);
+        FURI_LOG_D(TAG, "MFC type: %d", mfc_data->type);
+        mfc_event->data->poller_mode.mode = MfClassicPollerModeDictAttack;
+        mfc_event->data->poller_mode.data = mfc_data;
+        instance->nfc_dict_context.sectors_total =
+            mf_classic_get_total_sectors_num(mfc_data->type);
+        FURI_LOG_D(TAG, "Total sectors: %d", mf_classic_get_total_sectors_num(mfc_data->type));
+        mf_classic_get_read_sectors_and_keys(
+            mfc_data,
+            &instance->nfc_dict_context.sectors_read,
+            &instance->nfc_dict_context.keys_found);
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeRequestKey) {
+        MfClassicKey key = {};
+        if(keys_dict_get_next_key(
+               instance->nfc_dict_context.dict, key.data, sizeof(MfClassicKey))) {
+            mfc_event->data->key_request_data.key = key;
+            mfc_event->data->key_request_data.key_provided = true;
+            instance->nfc_dict_context.dict_keys_current++;
+            if(instance->nfc_dict_context.dict_keys_current % 10 == 0) {
+                view_dispatcher_send_custom_event(
+                    instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+            }
+        } else {
+            mfc_event->data->key_request_data.key_provided = false;
+        }
+    } else if(mfc_event->type == MfClassicPollerEventTypeDataUpdate) {
+        MfClassicPollerEventDataUpdate* data_update = &mfc_event->data->data_update;
+        instance->nfc_dict_context.sectors_read = data_update->sectors_read;
+        instance->nfc_dict_context.keys_found = data_update->keys_found;
+        instance->nfc_dict_context.current_sector = data_update->current_sector;
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeNextSector) {
+        keys_dict_rewind(instance->nfc_dict_context.dict);
+        instance->nfc_dict_context.dict_keys_current = 0;
+        instance->nfc_dict_context.current_sector =
+            mfc_event->data->next_sector_data.current_sector;
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeFoundKeyA) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeFoundKeyB) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeKeyAttackStart) {
+        instance->nfc_dict_context.key_attack_current_sector =
+            mfc_event->data->key_attack_data.current_sector;
+        instance->nfc_dict_context.is_key_attack = true;
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeKeyAttackStop) {
+        keys_dict_rewind(instance->nfc_dict_context.dict);
+        instance->nfc_dict_context.is_key_attack = false;
+        instance->nfc_dict_context.dict_keys_current = 0;
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackDataUpdate);
+    } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) {
+        const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller);
+        nfc_device_set_data(instance->target_dev, NfcProtocolMfClassic, mfc_data);
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackComplete);
+        command = NfcCommandStop;
+    }
+
+    return command;
+}
+
+void nfc_dict_attack_dict_attack_result_callback(DictAttackEvent event, void* context) {
+    furi_assert(context);
+    NfcMagicApp* instance = context;
+
+    if(event == DictAttackEventSkipPressed) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicAppCustomEventDictAttackSkip);
+    }
+}
+
+static void nfc_magic_scene_mf_classic_dict_attack_update_view(NfcMagicApp* instance) {
+    NfcMagicAppMfClassicDictAttackContext* mfc_dict = &instance->nfc_dict_context;
+
+    if(mfc_dict->is_key_attack) {
+        dict_attack_set_key_attack(instance->dict_attack, mfc_dict->key_attack_current_sector);
+    } else {
+        dict_attack_reset_key_attack(instance->dict_attack);
+        dict_attack_set_sectors_total(instance->dict_attack, mfc_dict->sectors_total);
+        dict_attack_set_sectors_read(instance->dict_attack, mfc_dict->sectors_read);
+        dict_attack_set_keys_found(instance->dict_attack, mfc_dict->keys_found);
+        dict_attack_set_current_dict_key(instance->dict_attack, mfc_dict->dict_keys_current);
+        dict_attack_set_current_sector(instance->dict_attack, mfc_dict->current_sector);
+    }
+}
+
+static void nfc_magic_scene_mf_classic_dict_attack_prepare_view(NfcMagicApp* instance) {
+    uint32_t state =
+        scene_manager_get_scene_state(instance->scene_manager, NfcMagicSceneMfClassicDictAttack);
+    if(state == DictAttackStateUserDictInProgress) {
+        do {
+            if(!keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_USER_PATH)) {
+                state = DictAttackStateSystemDictInProgress;
+                break;
+            }
+
+            instance->nfc_dict_context.dict = keys_dict_alloc(
+                NFC_APP_MF_CLASSIC_DICT_USER_PATH, KeysDictModeOpenAlways, sizeof(MfClassicKey));
+            if(keys_dict_get_total_keys(instance->nfc_dict_context.dict) == 0) {
+                keys_dict_free(instance->nfc_dict_context.dict);
+                state = DictAttackStateSystemDictInProgress;
+                break;
+            }
+
+            dict_attack_set_header(instance->dict_attack, "MF Classic User Dictionary");
+        } while(false);
+    }
+    if(state == DictAttackStateSystemDictInProgress) {
+        instance->nfc_dict_context.dict = keys_dict_alloc(
+            NFC_APP_MF_CLASSIC_DICT_SYSTEM_PATH, KeysDictModeOpenExisting, sizeof(MfClassicKey));
+        dict_attack_set_header(instance->dict_attack, "MF Classic System Dictionary");
+    }
+
+    instance->nfc_dict_context.dict_keys_total =
+        keys_dict_get_total_keys(instance->nfc_dict_context.dict);
+    dict_attack_set_total_dict_keys(
+        instance->dict_attack, instance->nfc_dict_context.dict_keys_total);
+    instance->nfc_dict_context.dict_keys_current = 0;
+
+    dict_attack_set_callback(
+        instance->dict_attack, nfc_dict_attack_dict_attack_result_callback, instance);
+    nfc_magic_scene_mf_classic_dict_attack_update_view(instance);
+
+    scene_manager_set_scene_state(
+        instance->scene_manager, NfcMagicSceneMfClassicDictAttack, state);
+}
+
+void nfc_magic_scene_mf_classic_dict_attack_on_enter(void* context) {
+    NfcMagicApp* instance = context;
+
+    scene_manager_set_scene_state(
+        instance->scene_manager,
+        NfcMagicSceneMfClassicDictAttack,
+        DictAttackStateUserDictInProgress);
+    nfc_magic_scene_mf_classic_dict_attack_prepare_view(instance);
+    dict_attack_set_card_state(instance->dict_attack, true);
+    view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewDictAttack);
+    nfc_magic_app_blink_start(instance);
+    notification_message(instance->notifications, &sequence_display_backlight_enforce_on);
+
+    instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfClassic);
+    nfc_poller_start(instance->poller, nfc_dict_attack_worker_callback, instance);
+}
+
+static void nfc_magic_scene_mf_classic_dict_attack_notify_read(NfcMagicApp* instance) {
+    const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller);
+    bool is_card_fully_read = mf_classic_is_card_read(mfc_data);
+    if(is_card_fully_read) {
+        notification_message(instance->notifications, &sequence_success);
+    } else {
+        notification_message(instance->notifications, &sequence_semi_success);
+    }
+}
+
+bool nfc_magic_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent event) {
+    NfcMagicApp* instance = context;
+    bool consumed = false;
+
+    uint32_t state =
+        scene_manager_get_scene_state(instance->scene_manager, NfcMagicSceneMfClassicDictAttack);
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == NfcMagicAppCustomEventDictAttackComplete) {
+            if(state == DictAttackStateUserDictInProgress) {
+                nfc_poller_stop(instance->poller);
+                nfc_poller_free(instance->poller);
+                keys_dict_free(instance->nfc_dict_context.dict);
+                scene_manager_set_scene_state(
+                    instance->scene_manager,
+                    NfcMagicSceneMfClassicDictAttack,
+                    DictAttackStateSystemDictInProgress);
+                nfc_magic_scene_mf_classic_dict_attack_prepare_view(instance);
+                instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfClassic);
+                nfc_poller_start(instance->poller, nfc_dict_attack_worker_callback, instance);
+                consumed = true;
+            } else {
+                nfc_magic_scene_mf_classic_dict_attack_notify_read(instance);
+                if(instance->protocol == NfcMagicProtocolGen2) {
+                    scene_manager_next_scene(instance->scene_manager, NfcMagicSceneGen2WriteCheck);
+                } else {
+                    scene_manager_next_scene(
+                        instance->scene_manager, NfcMagicSceneMfClassicWriteCheck);
+                }
+                dolphin_deed(DolphinDeedNfcReadSuccess);
+                consumed = true;
+            }
+        } else if(event.event == NfcMagicAppCustomEventCardDetected) {
+            dict_attack_set_card_state(instance->dict_attack, true);
+            consumed = true;
+        } else if(event.event == NfcMagicAppCustomEventCardLost) {
+            dict_attack_set_card_state(instance->dict_attack, false);
+            consumed = true;
+        } else if(event.event == NfcMagicAppCustomEventDictAttackDataUpdate) {
+            nfc_magic_scene_mf_classic_dict_attack_update_view(instance);
+        } else if(event.event == NfcMagicAppCustomEventDictAttackSkip) {
+            const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller);
+            nfc_device_set_data(instance->target_dev, NfcProtocolMfClassic, mfc_data);
+            if(state == DictAttackStateUserDictInProgress) {
+                if(instance->nfc_dict_context.is_card_present) {
+                    nfc_poller_stop(instance->poller);
+                    nfc_poller_free(instance->poller);
+                    keys_dict_free(instance->nfc_dict_context.dict);
+                    scene_manager_set_scene_state(
+                        instance->scene_manager,
+                        NfcMagicSceneMfClassicDictAttack,
+                        DictAttackStateSystemDictInProgress);
+                    nfc_magic_scene_mf_classic_dict_attack_prepare_view(instance);
+                    instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfClassic);
+                    nfc_poller_start(instance->poller, nfc_dict_attack_worker_callback, instance);
+                } else {
+                    nfc_magic_scene_mf_classic_dict_attack_notify_read(instance);
+                    if(instance->protocol == NfcMagicProtocolGen2) {
+                        scene_manager_next_scene(
+                            instance->scene_manager, NfcMagicSceneGen2WriteCheck);
+                    } else {
+                        scene_manager_next_scene(
+                            instance->scene_manager, NfcMagicSceneMfClassicWriteCheck);
+                    }
+                    dolphin_deed(DolphinDeedNfcReadSuccess);
+                }
+                consumed = true;
+            } else if(state == DictAttackStateSystemDictInProgress) {
+                nfc_magic_scene_mf_classic_dict_attack_notify_read(instance);
+                if(instance->protocol == NfcMagicProtocolGen2) {
+                    scene_manager_next_scene(instance->scene_manager, NfcMagicSceneGen2WriteCheck);
+                } else {
+                    scene_manager_next_scene(
+                        instance->scene_manager, NfcMagicSceneMfClassicWriteCheck);
+                }
+                dolphin_deed(DolphinDeedNfcReadSuccess);
+                consumed = true;
+            }
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_previous_scene(instance->scene_manager);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void nfc_magic_scene_mf_classic_dict_attack_on_exit(void* context) {
+    NfcMagicApp* instance = context;
+    const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller);
+    nfc_device_set_data(instance->target_dev, NfcProtocolMfClassic, mfc_data);
+
+    nfc_poller_stop(instance->poller);
+    nfc_poller_free(instance->poller);
+
+    dict_attack_reset(instance->dict_attack);
+    scene_manager_set_scene_state(
+        instance->scene_manager,
+        NfcMagicSceneMfClassicDictAttack,
+        DictAttackStateUserDictInProgress);
+
+    keys_dict_free(instance->nfc_dict_context.dict);
+
+    instance->nfc_dict_context.current_sector = 0;
+    instance->nfc_dict_context.sectors_total = 0;
+    instance->nfc_dict_context.sectors_read = 0;
+    instance->nfc_dict_context.keys_found = 0;
+    instance->nfc_dict_context.dict_keys_total = 0;
+    instance->nfc_dict_context.dict_keys_current = 0;
+    instance->nfc_dict_context.is_key_attack = false;
+    instance->nfc_dict_context.key_attack_current_sector = 0;
+    instance->nfc_dict_context.is_card_present = false;
+
+    nfc_magic_app_blink_stop(instance);
+    notification_message(instance->notifications, &sequence_display_backlight_enforce_auto);
+}

+ 65 - 0
scenes/nfc_magic_scene_mf_classic_menu.c

@@ -0,0 +1,65 @@
+#include "../nfc_magic_app_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexWrite,
+    SubmenuIndexWipe,
+};
+
+void nfc_magic_scene_mf_classic_menu_submenu_callback(void* context, uint32_t index) {
+    NfcMagicApp* instance = context;
+
+    view_dispatcher_send_custom_event(instance->view_dispatcher, index);
+}
+
+void nfc_magic_scene_mf_classic_menu_on_enter(void* context) {
+    NfcMagicApp* instance = context;
+
+    Submenu* submenu = instance->submenu;
+    submenu_add_item(
+        submenu,
+        "Write",
+        SubmenuIndexWrite,
+        nfc_magic_scene_mf_classic_menu_submenu_callback,
+        instance);
+    submenu_add_item(
+        submenu,
+        "Wipe",
+        SubmenuIndexWipe,
+        nfc_magic_scene_mf_classic_menu_submenu_callback,
+        instance);
+
+    submenu_set_selected_item(
+        submenu,
+        scene_manager_get_scene_state(instance->scene_manager, NfcMagicSceneMfClassicMenu));
+    view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewMenu);
+}
+
+bool nfc_magic_scene_mf_classic_menu_on_event(void* context, SceneManagerEvent event) {
+    NfcMagicApp* instance = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexWrite) {
+            instance->gen2_poller_is_wipe_mode = false;
+            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneFileSelect);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWipe) {
+            instance->gen2_poller_is_wipe_mode = true;
+            scene_manager_next_scene(instance->scene_manager, NfcMagicSceneMfClassicDictAttack);
+            consumed = true;
+        }
+        scene_manager_set_scene_state(
+            instance->scene_manager, NfcMagicSceneMfClassicMenu, event.event);
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_search_and_switch_to_previous_scene(
+            instance->scene_manager, NfcMagicSceneStart);
+    }
+
+    return consumed;
+}
+
+void nfc_magic_scene_mf_classic_menu_on_exit(void* context) {
+    NfcMagicApp* instance = context;
+
+    submenu_reset(instance->submenu);
+}

+ 124 - 0
scenes/nfc_magic_scene_mf_classic_write_check.c

@@ -0,0 +1,124 @@
+#include "../nfc_magic_app_i.h"
+
+void nfc_magic_scene_mf_classic_write_check_view_callback(WriteProblemsEvent event, void* context) {
+    NfcMagicApp* instance = context;
+    NfcMagicAppWriteProblemsContext* problems_context = &instance->write_problems_context;
+
+    if(event == WriteProblemsEventCenterPressed) {
+        if(problems_context->problem_index == problems_context->problems_total - 1) {
+            // Continue to the next scene
+            if(instance->gen2_poller_is_wipe_mode) {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWipe);
+            } else {
+                scene_manager_next_scene(instance->scene_manager, NfcMagicSceneWrite);
+            }
+        } else {
+            // Move to the next problem
+            problems_context->problem_index++;
+            problems_context->problem_index_abs++;
+            write_problems_set_problem_index(
+                instance->write_problems, problems_context->problem_index);
+
+            for(uint8_t i = problems_context->problem_index_abs;
+                i < GEN2_POLLER_WRITE_PROBLEMS_LEN;
+                i++) {
+                if(problems_context->problems.all_problems & (1 << i)) {
+                    write_problems_set_content(instance->write_problems, gen2_problem_strings[i]);
+                    problems_context->problem_index_abs = i;
+                    break;
+                }
+            }
+        }
+    } else if(event == WriteProblemsEventLeftPressed) {
+        if(problems_context->problem_index == 0) {
+            // Exit to the previous scene
+            scene_manager_search_and_switch_to_previous_scene(
+                instance->scene_manager, NfcMagicSceneMfClassicMenu);
+        } else {
+            // Move to the previous problem
+            problems_context->problem_index--;
+            problems_context->problem_index_abs--;
+            write_problems_set_problem_index(
+                instance->write_problems, problems_context->problem_index);
+
+            for(uint8_t i = problems_context->problem_index_abs;
+                i < GEN2_POLLER_WRITE_PROBLEMS_LEN;
+                i--) {
+                if(problems_context->problems.all_problems & (1 << i)) {
+                    write_problems_set_content(instance->write_problems, gen2_problem_strings[i]);
+                    problems_context->problem_index_abs = i;
+                    break;
+                }
+            }
+        }
+    }
+}
+
+void nfc_magic_scene_mf_classic_write_check_on_enter(void* context) {
+    NfcMagicApp* instance = context;
+
+    Gen2PollerWriteProblems problems = gen2_poller_check_target_problems(instance->target_dev);
+    if(!instance->gen2_poller_is_wipe_mode) {
+        problems.all_problems |=
+            gen2_poller_check_source_problems(instance->source_dev).all_problems;
+    }
+    FURI_LOG_D("GEN2", "Problems: %d", problems.all_problems);
+
+    WriteProblems* write_problems = instance->write_problems;
+    uint8_t problems_count = 0;
+    uint8_t current_problem = 0;
+    furi_assert(!problems.no_data, "No MFC data in nfc device");
+
+    // Set the uid_locked problem to true as we have a Mifare Classic card
+    problems.uid_locked = true;
+
+    // Count the number of problems
+    for(uint8_t i = 0; i < GEN2_POLLER_WRITE_PROBLEMS_LEN; i++) {
+        if(problems.all_problems & (1 << i)) {
+            problems_count++;
+        }
+    }
+
+    // Init the view
+    write_problems_set_callback(
+        write_problems, nfc_magic_scene_mf_classic_write_check_view_callback, instance);
+    write_problems_set_problems_total(write_problems, problems_count);
+    write_problems_set_problem_index(write_problems, current_problem);
+
+    // Set the first problem
+    for(uint8_t i = current_problem; i < GEN2_POLLER_WRITE_PROBLEMS_LEN; i++) {
+        if(problems.all_problems & (1 << i)) {
+            write_problems_set_content(instance->write_problems, gen2_problem_strings[i]);
+            instance->write_problems_context.problem_index_abs = i;
+            break;
+        }
+    }
+
+    // Save the problems context
+    instance->write_problems_context.problem_index = current_problem;
+    instance->write_problems_context.problems_total = problems_count;
+    instance->write_problems_context.problems = problems;
+
+    // Setup and start worker
+    view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewWriteProblems);
+}
+
+bool nfc_magic_scene_mf_classic_write_check_on_event(void* context, SceneManagerEvent event) {
+    NfcMagicApp* instance = context;
+    UNUSED(event);
+    UNUSED(context);
+    UNUSED(instance);
+    bool consumed = false;
+
+    return consumed;
+}
+
+void nfc_magic_scene_mf_classic_write_check_on_exit(void* context) {
+    NfcMagicApp* instance = context;
+
+    instance->write_problems_context.problem_index = 0;
+    instance->write_problems_context.problems_total = 0;
+    instance->write_problems_context.problems.all_problems = 0;
+
+    write_problems_reset(instance->write_problems);
+}

+ 2 - 2
scenes/nfc_magic_scene_success.c

@@ -11,8 +11,8 @@ void nfc_magic_scene_success_on_enter(void* context) {
     notification_message(instance->notifications, &sequence_success);
 
     Popup* popup = instance->popup;
-    popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
-    popup_set_header(popup, "Success!", 10, 20, AlignLeft, AlignBottom);
+    popup_set_icon(popup, 0, 9, &I_DolphinSuccess_91x55);
+    popup_set_header(popup, "Success!", 75, 12, AlignLeft, AlignCenter);
     popup_set_timeout(popup, 1500);
     popup_set_context(popup, instance);
     popup_set_callback(popup, nfc_magic_scene_success_popup_callback);

+ 48 - 6
scenes/nfc_magic_scene_wipe.c

@@ -5,7 +5,7 @@ enum {
     NfcMagicSceneWipeStateCardFound,
 };
 
-NfcCommand nfc_mafic_scene_wipe_gen1_poller_callback(Gen1aPollerEvent event, void* context) {
+NfcCommand nfc_magic_scene_wipe_gen1_poller_callback(Gen1aPollerEvent event, void* context) {
     NfcMagicApp* instance = context;
     furi_assert(event.data);
 
@@ -29,7 +29,35 @@ NfcCommand nfc_mafic_scene_wipe_gen1_poller_callback(Gen1aPollerEvent event, voi
     return command;
 }
 
-NfcCommand nfc_mafic_scene_wipe_gen4_poller_callback(Gen4PollerEvent event, void* context) {
+NfcCommand nfc_magic_scene_wipe_gen2_poller_callback(Gen2PollerEvent event, void* context) {
+    NfcMagicApp* instance = context;
+
+    NfcCommand command = NfcCommandContinue;
+
+    if(event.type == Gen2PollerEventTypeDetected) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicCustomEventCardDetected);
+    } else if(event.type == Gen2PollerEventTypeRequestMode) {
+        event.data->poller_mode.mode = Gen2PollerModeWipe;
+    } else if(event.type == Gen2PollerEventTypeRequestTargetData) {
+        const MfClassicData* mfc_data =
+            nfc_device_get_data(instance->target_dev, NfcProtocolMfClassic);
+        event.data->target_data.mfc_data = mfc_data;
+
+    } else if(event.type == Gen2PollerEventTypeSuccess) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicCustomEventWorkerSuccess);
+        command = NfcCommandStop;
+    } else if(event.type == Gen2PollerEventTypeFail) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicCustomEventWorkerFail);
+        command = NfcCommandStop;
+    }
+
+    return command;
+}
+
+NfcCommand nfc_magic_scene_wipe_gen4_poller_callback(Gen4PollerEvent event, void* context) {
     NfcMagicApp* instance = context;
 
     NfcCommand command = NfcCommandContinue;
@@ -81,12 +109,20 @@ void nfc_magic_scene_wipe_on_enter(void* context) {
     if(instance->protocol == NfcMagicProtocolGen1) {
         instance->gen1a_poller = gen1a_poller_alloc(instance->nfc);
         gen1a_poller_start(
-            instance->gen1a_poller, nfc_mafic_scene_wipe_gen1_poller_callback, instance);
-    } else {
+            instance->gen1a_poller, nfc_magic_scene_wipe_gen1_poller_callback, instance);
+    } else if(instance->protocol == NfcMagicProtocolGen2) {
+        instance->gen2_poller = gen2_poller_alloc(instance->nfc);
+        gen2_poller_start(
+            instance->gen2_poller, nfc_magic_scene_wipe_gen2_poller_callback, instance);
+    } else if(instance->protocol == NfcMagicProtocolClassic) {
+        instance->gen2_poller = gen2_poller_alloc(instance->nfc);
+        gen2_poller_start(
+            instance->gen2_poller, nfc_magic_scene_wipe_gen2_poller_callback, instance);
+    } else if(instance->protocol == NfcMagicProtocolGen4) {
         instance->gen4_poller = gen4_poller_alloc(instance->nfc);
         gen4_poller_set_password(instance->gen4_poller, instance->gen4_password);
         gen4_poller_start(
-            instance->gen4_poller, nfc_mafic_scene_wipe_gen4_poller_callback, instance);
+            instance->gen4_poller, nfc_magic_scene_wipe_gen4_poller_callback, instance);
     }
 }
 
@@ -123,7 +159,13 @@ void nfc_magic_scene_wipe_on_exit(void* context) {
     if(instance->protocol == NfcMagicProtocolGen1) {
         gen1a_poller_stop(instance->gen1a_poller);
         gen1a_poller_free(instance->gen1a_poller);
-    } else {
+    } else if(instance->protocol == NfcMagicProtocolGen2) {
+        gen2_poller_stop(instance->gen2_poller);
+        gen2_poller_free(instance->gen2_poller);
+    } else if(instance->protocol == NfcMagicProtocolClassic) {
+        gen2_poller_stop(instance->gen2_poller);
+        gen2_poller_free(instance->gen2_poller);
+    } else if(instance->protocol == NfcMagicProtocolGen4) {
         gen4_poller_stop(instance->gen4_poller);
         gen4_poller_free(instance->gen4_poller);
     }

+ 5 - 2
scenes/nfc_magic_scene_wipe_fail.c

@@ -14,8 +14,11 @@ void nfc_magic_scene_wipe_fail_on_enter(void* context) {
     Widget* widget = instance->widget;
     notification_message(instance->notifications, &sequence_error);
 
-    widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48);
-    widget_add_string_element(widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "Wipe failed");
+    widget_add_icon_element(widget, 83, 22, &I_WarningDolphinFlip_45x42);
+    widget_add_string_element(widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "Failed to Wipe");
+    widget_add_string_multiline_element(
+        widget, 0, 13, AlignLeft, AlignTop, FontSecondary, "Something went\nwrong while wiping");
+
     widget_add_button_element(
         widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_wipe_fail_widget_callback, instance);
 

+ 50 - 5
scenes/nfc_magic_scene_write.c

@@ -5,7 +5,7 @@ enum {
     NfcMagicSceneWriteStateCardFound,
 };
 
-NfcCommand nfc_mafic_scene_write_gen1_poller_callback(Gen1aPollerEvent event, void* context) {
+NfcCommand nfc_magic_scene_write_gen1_poller_callback(Gen1aPollerEvent event, void* context) {
     NfcMagicApp* instance = context;
     furi_assert(event.data);
 
@@ -33,7 +33,39 @@ NfcCommand nfc_mafic_scene_write_gen1_poller_callback(Gen1aPollerEvent event, vo
     return command;
 }
 
-NfcCommand nfc_mafic_scene_write_gen4_poller_callback(Gen4PollerEvent event, void* context) {
+NfcCommand nfc_magic_scene_write_gen2_poller_callback(Gen2PollerEvent event, void* context) {
+    NfcMagicApp* instance = context;
+    furi_assert(event.data);
+
+    NfcCommand command = NfcCommandContinue;
+
+    if(event.type == Gen2PollerEventTypeDetected) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicCustomEventCardDetected);
+    } else if(event.type == Gen2PollerEventTypeRequestMode) {
+        event.data->poller_mode.mode = Gen2PollerModeWrite;
+    } else if(event.type == Gen2PollerEventTypeRequestDataToWrite) {
+        const MfClassicData* mfc_data =
+            nfc_device_get_data(instance->source_dev, NfcProtocolMfClassic);
+        event.data->data_to_write.mfc_data = mfc_data;
+    } else if(event.type == Gen2PollerEventTypeRequestTargetData) {
+        const MfClassicData* mfc_data =
+            nfc_device_get_data(instance->target_dev, NfcProtocolMfClassic);
+        event.data->target_data.mfc_data = mfc_data;
+    } else if(event.type == Gen2PollerEventTypeSuccess) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicCustomEventWorkerSuccess);
+        command = NfcCommandStop;
+    } else if(event.type == Gen2PollerEventTypeFail) {
+        view_dispatcher_send_custom_event(
+            instance->view_dispatcher, NfcMagicCustomEventWorkerFail);
+        command = NfcCommandStop;
+    }
+
+    return command;
+}
+
+NfcCommand nfc_magic_scene_write_gen4_poller_callback(Gen4PollerEvent event, void* context) {
     NfcMagicApp* instance = context;
     furi_assert(event.data);
 
@@ -90,12 +122,20 @@ void nfc_magic_scene_write_on_enter(void* context) {
     if(instance->protocol == NfcMagicProtocolGen1) {
         instance->gen1a_poller = gen1a_poller_alloc(instance->nfc);
         gen1a_poller_start(
-            instance->gen1a_poller, nfc_mafic_scene_write_gen1_poller_callback, instance);
+            instance->gen1a_poller, nfc_magic_scene_write_gen1_poller_callback, instance);
+    } else if(instance->protocol == NfcMagicProtocolGen2) {
+        instance->gen2_poller = gen2_poller_alloc(instance->nfc);
+        gen2_poller_start(
+            instance->gen2_poller, nfc_magic_scene_write_gen2_poller_callback, instance);
+    } else if(instance->protocol == NfcMagicProtocolClassic) {
+        instance->gen2_poller = gen2_poller_alloc(instance->nfc);
+        gen2_poller_start(
+            instance->gen2_poller, nfc_magic_scene_write_gen2_poller_callback, instance);
     } else {
         instance->gen4_poller = gen4_poller_alloc(instance->nfc);
         gen4_poller_set_password(instance->gen4_poller, instance->gen4_password);
         gen4_poller_start(
-            instance->gen4_poller, nfc_mafic_scene_write_gen4_poller_callback, instance);
+            instance->gen4_poller, nfc_magic_scene_write_gen4_poller_callback, instance);
     }
 }
 
@@ -132,7 +172,12 @@ void nfc_magic_scene_write_on_exit(void* context) {
     if(instance->protocol == NfcMagicProtocolGen1) {
         gen1a_poller_stop(instance->gen1a_poller);
         gen1a_poller_free(instance->gen1a_poller);
-    } else {
+    } else if(
+        instance->protocol == NfcMagicProtocolGen2 ||
+        instance->protocol == NfcMagicProtocolClassic) {
+        gen2_poller_stop(instance->gen2_poller);
+        gen2_poller_free(instance->gen2_poller);
+    } else if(instance->protocol == NfcMagicProtocolGen4) {
         gen4_poller_stop(instance->gen4_poller);
         gen4_poller_free(instance->gen4_poller);
     }

+ 6 - 14
scenes/nfc_magic_scene_write_fail.c

@@ -16,20 +16,14 @@ void nfc_magic_scene_write_fail_on_enter(void* context) {
 
     notification_message(instance->notifications, &sequence_error);
 
-    widget_add_icon_element(widget, 72, 17, &I_DolphinCommon_56x48);
+    widget_add_icon_element(widget, 83, 22, &I_WarningDolphinFlip_45x42);
     widget_add_string_element(
-        widget, 7, 4, AlignLeft, AlignTop, FontPrimary, "Writing gone wrong!");
+        widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "Failed to Write");
     widget_add_string_multiline_element(
-        widget,
-        7,
-        17,
-        AlignLeft,
-        AlignTop,
-        FontSecondary,
-        "Not all sectors\nwere written\ncorrectly.");
+        widget, 0, 13, AlignLeft, AlignTop, FontSecondary, "Something went\nwrong while\nwriting");
 
     widget_add_button_element(
-        widget, GuiButtonTypeLeft, "Finish", nfc_magic_scene_write_fail_widget_callback, instance);
+        widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_write_fail_widget_callback, instance);
 
     // Setup and start worker
     view_dispatcher_switch_to_view(instance->view_dispatcher, NfcMagicAppViewWidget);
@@ -41,12 +35,10 @@ bool nfc_magic_scene_write_fail_on_event(void* context, SceneManagerEvent event)
 
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == GuiButtonTypeLeft) {
-            consumed = scene_manager_search_and_switch_to_previous_scene(
-                instance->scene_manager, NfcMagicSceneStart);
+            consumed = scene_manager_previous_scene(instance->scene_manager);
         }
     } else if(event.type == SceneManagerEventTypeBack) {
-        consumed = scene_manager_search_and_switch_to_previous_scene(
-            instance->scene_manager, NfcMagicSceneStart);
+        consumed = scene_manager_previous_scene(instance->scene_manager);
     }
     return consumed;
 }

+ 245 - 0
views/dict_attack.c

@@ -0,0 +1,245 @@
+#include "dict_attack.h"
+
+#include <gui/elements.h>
+
+#define NFC_CLASSIC_KEYS_PER_SECTOR 2
+
+struct DictAttack {
+    View* view;
+    DictAttackCallback callback;
+    void* context;
+};
+
+typedef struct {
+    FuriString* header;
+    bool card_detected;
+    uint8_t sectors_total;
+    uint8_t sectors_read;
+    uint8_t current_sector;
+    uint8_t keys_found;
+    size_t dict_keys_total;
+    size_t dict_keys_current;
+    bool is_key_attack;
+    uint8_t key_attack_current_sector;
+} DictAttackViewModel;
+
+static void dict_attack_draw_callback(Canvas* canvas, void* model) {
+    DictAttackViewModel* m = model;
+    if(!m->card_detected) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(
+            canvas, 64, 4, AlignCenter, AlignTop, "Hold the tag to the Flipper!");
+        canvas_set_font(canvas, FontSecondary);
+        elements_multiline_text_aligned(
+            canvas, 64, 23, AlignCenter, AlignTop, "Make sure the tag is\npositioned correctly.");
+    } else {
+        char draw_str[32] = {};
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(
+            canvas, 64, 0, AlignCenter, AlignTop, furi_string_get_cstr(m->header));
+        if(m->is_key_attack) {
+            snprintf(
+                draw_str,
+                sizeof(draw_str),
+                "Reuse key check for sector: %d",
+                m->key_attack_current_sector);
+        } else {
+            snprintf(draw_str, sizeof(draw_str), "Unlocking sector: %d", m->current_sector);
+        }
+        canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str);
+        float dict_progress = m->dict_keys_total == 0 ?
+                                  0 :
+                                  (float)(m->dict_keys_current) / (float)(m->dict_keys_total);
+        float progress = m->sectors_total == 0 ? 0 :
+                                                 ((float)(m->current_sector) + dict_progress) /
+                                                     (float)(m->sectors_total);
+        if(progress > 1.0f) {
+            progress = 1.0f;
+        }
+        if(m->dict_keys_current == 0) {
+            // Cause when people see 0 they think it's broken
+            snprintf(draw_str, sizeof(draw_str), "%d/%zu", 1, m->dict_keys_total);
+        } else {
+            snprintf(
+                draw_str, sizeof(draw_str), "%zu/%zu", m->dict_keys_current, m->dict_keys_total);
+        }
+        elements_progress_bar_with_text(canvas, 0, 20, 128, dict_progress, draw_str);
+        canvas_set_font(canvas, FontSecondary);
+        snprintf(
+            draw_str,
+            sizeof(draw_str),
+            "Keys found: %d/%d",
+            m->keys_found,
+            m->sectors_total * NFC_CLASSIC_KEYS_PER_SECTOR);
+        canvas_draw_str_aligned(canvas, 0, 33, AlignLeft, AlignTop, draw_str);
+        snprintf(
+            draw_str, sizeof(draw_str), "Sectors Read: %d/%d", m->sectors_read, m->sectors_total);
+        canvas_draw_str_aligned(canvas, 0, 43, AlignLeft, AlignTop, draw_str);
+    }
+    elements_button_center(canvas, "Skip");
+}
+
+static bool dict_attack_input_callback(InputEvent* event, void* context) {
+    DictAttack* instance = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeShort && event->key == InputKeyOk) {
+        if(instance->callback) {
+            instance->callback(DictAttackEventSkipPressed, instance->context);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+DictAttack* dict_attack_alloc() {
+    DictAttack* instance = malloc(sizeof(DictAttack));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(DictAttackViewModel));
+    view_set_draw_callback(instance->view, dict_attack_draw_callback);
+    view_set_input_callback(instance->view, dict_attack_input_callback);
+    view_set_context(instance->view, instance);
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        { model->header = furi_string_alloc(); },
+        false);
+
+    return instance;
+}
+
+void dict_attack_free(DictAttack* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, DictAttackViewModel * model, { furi_string_free(model->header); }, false);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+void dict_attack_reset(DictAttack* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        {
+            model->sectors_total = 0;
+            model->sectors_read = 0;
+            model->current_sector = 0;
+            model->keys_found = 0;
+            model->dict_keys_total = 0;
+            model->dict_keys_current = 0;
+            model->is_key_attack = false;
+            furi_string_reset(model->header);
+        },
+        false);
+}
+
+View* dict_attack_get_view(DictAttack* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}
+
+void dict_attack_set_callback(DictAttack* instance, DictAttackCallback callback, void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void dict_attack_set_header(DictAttack* instance, const char* header) {
+    furi_assert(instance);
+    furi_assert(header);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        { furi_string_set(model->header, header); },
+        true);
+}
+
+void dict_attack_set_card_state(DictAttack* instance, bool detected) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, DictAttackViewModel * model, { model->card_detected = detected; }, true);
+}
+
+void dict_attack_set_sectors_total(DictAttack* instance, uint8_t sectors_total) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        { model->sectors_total = sectors_total; },
+        true);
+}
+
+void dict_attack_set_sectors_read(DictAttack* instance, uint8_t sectors_read) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, DictAttackViewModel * model, { model->sectors_read = sectors_read; }, true);
+}
+
+void dict_attack_set_keys_found(DictAttack* instance, uint8_t keys_found) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, DictAttackViewModel * model, { model->keys_found = keys_found; }, true);
+}
+
+void dict_attack_set_current_sector(DictAttack* instance, uint8_t current_sector) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        { model->current_sector = current_sector; },
+        true);
+}
+
+void dict_attack_set_total_dict_keys(DictAttack* instance, size_t dict_keys_total) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        { model->dict_keys_total = dict_keys_total; },
+        true);
+}
+
+void dict_attack_set_current_dict_key(DictAttack* instance, size_t cur_key_num) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        { model->dict_keys_current = cur_key_num; },
+        true);
+}
+
+void dict_attack_set_key_attack(DictAttack* instance, uint8_t sector) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        DictAttackViewModel * model,
+        {
+            model->is_key_attack = true;
+            model->key_attack_current_sector = sector;
+        },
+        true);
+}
+
+void dict_attack_reset_key_attack(DictAttack* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, DictAttackViewModel * model, { model->is_key_attack = false; }, true);
+}

+ 50 - 0
views/dict_attack.h

@@ -0,0 +1,50 @@
+#pragma once
+
+#include <stdint.h>
+#include <gui/view.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct DictAttack DictAttack;
+
+typedef enum {
+    DictAttackEventSkipPressed,
+} DictAttackEvent;
+
+typedef void (*DictAttackCallback)(DictAttackEvent event, void* context);
+
+DictAttack* dict_attack_alloc();
+
+void dict_attack_free(DictAttack* instance);
+
+void dict_attack_reset(DictAttack* instance);
+
+View* dict_attack_get_view(DictAttack* instance);
+
+void dict_attack_set_callback(DictAttack* instance, DictAttackCallback callback, void* context);
+
+void dict_attack_set_header(DictAttack* instance, const char* header);
+
+void dict_attack_set_card_state(DictAttack* instance, bool detected);
+
+void dict_attack_set_sectors_total(DictAttack* instance, uint8_t sectors_total);
+
+void dict_attack_set_sectors_read(DictAttack* instance, uint8_t sectors_read);
+
+void dict_attack_set_keys_found(DictAttack* instance, uint8_t keys_found);
+
+void dict_attack_set_current_sector(DictAttack* instance, uint8_t curr_sec);
+
+void dict_attack_set_total_dict_keys(DictAttack* instance, size_t dict_keys_total);
+
+void dict_attack_set_current_dict_key(DictAttack* instance, size_t cur_key_num);
+
+void dict_attack_set_key_attack(DictAttack* instance, uint8_t sector);
+
+void dict_attack_reset_key_attack(DictAttack* instance);
+
+#ifdef __cplusplus
+}
+#endif

+ 160 - 0
views/write_problems.c

@@ -0,0 +1,160 @@
+#include "write_problems.h"
+
+#include <gui/elements.h>
+#include "nfc_magic_icons.h"
+
+struct WriteProblems {
+    View* view;
+    WriteProblemsCallback callback;
+    void* context;
+};
+
+typedef struct {
+    uint8_t problem_index;
+    uint8_t problems_total;
+    FuriString* content;
+} WriteProblemsViewModel;
+
+static void write_problems_view_draw_callback(Canvas* canvas, void* _model) {
+    WriteProblemsViewModel* model = _model;
+    FuriString* header = furi_string_alloc();
+    canvas_clear(canvas);
+
+    // Header
+    if(model->problems_total > 1) {
+        furi_string_printf(
+            header, "Warnings: %d of %d\n", model->problem_index + 1, model->problems_total);
+    } else {
+        furi_string_printf(header, "Warning!");
+    }
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64, 0, AlignCenter, AlignTop, furi_string_get_cstr(header));
+
+    // Warning message
+    canvas_set_font(canvas, FontSecondary);
+    elements_text_box(
+        canvas, 1, 13, 76, 42, AlignLeft, AlignTop, furi_string_get_cstr(model->content), false);
+
+    // Butttons
+    if(model->problem_index == model->problems_total - 1) {
+        elements_button_center(canvas, "Skip");
+        elements_button_left(canvas, "Retry");
+    } else {
+        elements_button_center(canvas, "Next");
+        elements_button_left(canvas, "Back");
+    }
+
+    // Dolphin
+    canvas_draw_icon(canvas, 83, 22, &I_WarningDolphinFlip_45x42);
+
+    furi_string_free(header);
+}
+
+static bool write_problems_input_callback(InputEvent* event, void* context) {
+    WriteProblems* instance = context;
+
+    if(event->type == InputTypeShort) {
+        if(event->key == InputKeyLeft) {
+            instance->callback(WriteProblemsEventLeftPressed, instance->context);
+            return true;
+        } else if(event->key == InputKeyOk) {
+            instance->callback(WriteProblemsEventCenterPressed, instance->context);
+            return true;
+        }
+    }
+    return false;
+}
+
+WriteProblems* write_problems_alloc() {
+    WriteProblems* instance = malloc(sizeof(WriteProblems));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(WriteProblemsViewModel));
+    view_set_draw_callback(instance->view, write_problems_view_draw_callback);
+    view_set_input_callback(instance->view, write_problems_input_callback);
+    view_set_context(instance->view, instance);
+    with_view_model(
+        instance->view,
+        WriteProblemsViewModel * model,
+        {
+            model->content = furi_string_alloc();
+            model->problem_index = 0;
+            model->problems_total = 0;
+        },
+        false);
+
+    return instance;
+}
+
+void write_problems_free(WriteProblems* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        WriteProblemsViewModel * model,
+        { furi_string_free(model->content); },
+        false);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+void write_problems_reset(WriteProblems* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        WriteProblemsViewModel * model,
+        {
+            model->problem_index = 0;
+            model->problems_total = 0;
+            furi_string_reset(model->content);
+        },
+        true);
+}
+
+View* write_problems_get_view(WriteProblems* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}
+
+void write_problems_set_callback(
+    WriteProblems* instance,
+    WriteProblemsCallback callback,
+    void* context) {
+    furi_assert(instance);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void write_problems_set_content(WriteProblems* instance, const char* content) {
+    furi_assert(instance);
+    furi_assert(content);
+
+    with_view_model(
+        instance->view,
+        WriteProblemsViewModel * model,
+        { furi_string_set(model->content, content); },
+        true);
+}
+
+void write_problems_set_problems_total(WriteProblems* instance, uint8_t problems_total) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        WriteProblemsViewModel * model,
+        { model->problems_total = problems_total; },
+        true);
+}
+
+void write_problems_set_problem_index(WriteProblems* instance, uint8_t problem_index) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view,
+        WriteProblemsViewModel * model,
+        { model->problem_index = problem_index; },
+        true);
+}

+ 32 - 0
views/write_problems.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <stdint.h>
+#include <gui/view.h>
+
+typedef struct WriteProblems WriteProblems;
+
+typedef enum {
+    WriteProblemsEventCenterPressed,
+    WriteProblemsEventLeftPressed,
+} WriteProblemsEvent;
+
+typedef void (*WriteProblemsCallback)(WriteProblemsEvent event, void* context);
+
+WriteProblems* write_problems_alloc();
+
+void write_problems_free(WriteProblems* instance);
+
+void write_problems_reset(WriteProblems* instance);
+
+View* write_problems_get_view(WriteProblems* instance);
+
+void write_problems_set_callback(
+    WriteProblems* instance,
+    WriteProblemsCallback callback,
+    void* context);
+
+void write_problems_set_content(WriteProblems* instance, const char* content);
+
+void write_problems_set_problem_index(WriteProblems* instance, uint8_t index);
+
+void write_problems_set_problems_total(WriteProblems* instance, uint8_t total);