Explorar el Código

Add mfkey32 from https://github.com/flipperdevices/flipperzero-good-faps

git-subtree-dir: mfkey32
git-subtree-mainline: 3fc56975ff72adac933b0ec56e4314467b2e2843
git-subtree-split: 5568d73741189dfe59d826adfd61d63dda428d5e
Willy-JL hace 2 años
padre
commit
56c342a4a1

+ 12 - 0
mfkey32/.catalog/README.md

@@ -0,0 +1,12 @@
+# Flipper Zero MFKey32
+
+This application allows you to calculate the keys to Mifare Classic cards from the nonces using the MFkey32 algorithm directly on your Flipper Zero. After collecting the nonces using the Detect Reader feature of the NFC app, they can be used to calculate the keys to the card in the MFKey32 app.
+
+## Usage
+
+After collecting nonces using the Detect Reader option, press the Start button in the MFKey32 app and wait for it to finish. The calculation can take more than 10 minutes, so you'll have to be patient. After the calculation is complete, the keys will be saved to the user key dictionary.
+
+## Credits
+
+Developers: noproto, AG
+Thanks: bettse

BIN
mfkey32/.catalog/screenshots/1.png


BIN
mfkey32/.catalog/screenshots/2.png


BIN
mfkey32/.catalog/screenshots/3.png


+ 1 - 0
mfkey32/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/flipperdevices/flipperzero-good-faps dev mfkey32

+ 19 - 0
mfkey32/application.fam

@@ -0,0 +1,19 @@
+App(
+    appid="mfkey32",
+    name="Mfkey32",
+    apptype=FlipperAppType.EXTERNAL,
+    targets=["f7"],
+    entry_point="mfkey32_main",
+    requires=[
+        "gui",
+        "storage",
+    ],
+    stack_size=1 * 1024,
+    fap_description="Mf Classic key finder",
+    fap_version="1.0",
+    fap_icon="mfkey.png",
+    fap_category="NFC",
+    fap_author="@noproto",
+    fap_icon_assets="images",
+    fap_weburl="https://github.com/noproto/FlipperMfkey",
+)

BIN
mfkey32/images/mfkey.png


BIN
mfkey32/mfkey.png


+ 1349 - 0
mfkey32/mfkey32.c

@@ -0,0 +1,1349 @@
+#pragma GCC optimize("O3")
+#pragma GCC optimize("-funroll-all-loops")
+
+// TODO: Add keys to top of the user dictionary, not the bottom
+// TODO: More efficient dictionary bruteforce by scanning through hardcoded very common keys and previously found dictionary keys first?
+//       (a cache for napi_key_already_found_for_nonce)
+
+#include <furi.h>
+#include <furi_hal.h>
+#include "time.h"
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include "mfkey32_icons.h"
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <storage/storage.h>
+#include <lib/nfc/helpers/mf_classic_dict.h>
+#include <lib/toolbox/args.h>
+#include <lib/flipper_format/flipper_format.h>
+#include <dolphin/dolphin.h>
+#include <notification/notification_messages.h>
+
+#define MF_CLASSIC_DICT_FLIPPER_PATH EXT_PATH("nfc/assets/mf_classic_dict.nfc")
+#define MF_CLASSIC_DICT_USER_PATH EXT_PATH("nfc/assets/mf_classic_dict_user.nfc")
+#define MF_CLASSIC_NONCE_PATH EXT_PATH("nfc/.mfkey32.log")
+#define TAG "Mfkey32"
+#define NFC_MF_CLASSIC_KEY_LEN (13)
+
+#define MIN_RAM 115632
+#define LF_POLY_ODD (0x29CE5C)
+#define LF_POLY_EVEN (0x870804)
+#define CONST_M1_1 (LF_POLY_EVEN << 1 | 1)
+#define CONST_M2_1 (LF_POLY_ODD << 1)
+#define CONST_M1_2 (LF_POLY_ODD)
+#define CONST_M2_2 (LF_POLY_EVEN << 1 | 1)
+#define BIT(x, n) ((x) >> (n)&1)
+#define BEBIT(x, n) BIT(x, (n) ^ 24)
+#define SWAPENDIAN(x) \
+    ((x) = ((x) >> 8 & 0xff00ff) | ((x)&0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16)
+//#define SIZEOF(arr) sizeof(arr) / sizeof(*arr)
+
+static int eta_round_time = 56;
+static int eta_total_time = 900;
+// MSB_LIMIT: Chunk size (out of 256)
+static int MSB_LIMIT = 16;
+
+struct Crypto1State {
+    uint32_t odd, even;
+};
+struct Crypto1Params {
+    uint64_t key;
+    uint32_t nr0_enc, uid_xor_nt0, uid_xor_nt1, nr1_enc, p64b, ar1_enc;
+};
+struct Msb {
+    int tail;
+    uint32_t states[768];
+};
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} PluginEvent;
+
+typedef enum {
+    MissingNonces,
+    ZeroNonces,
+} MfkeyError;
+
+typedef enum {
+    Ready,
+    Initializing,
+    DictionaryAttack,
+    MfkeyAttack,
+    Complete,
+    Error,
+    Help,
+} MfkeyState;
+
+// TODO: Can we eliminate any of the members of this struct?
+typedef struct {
+    FuriMutex* mutex;
+    MfkeyError err;
+    MfkeyState mfkey_state;
+    int cracked;
+    int unique_cracked;
+    int num_completed;
+    int total;
+    int dict_count;
+    int search;
+    int eta_timestamp;
+    int eta_total;
+    int eta_round;
+    bool is_thread_running;
+    bool close_thread_please;
+    FuriThread* mfkeythread;
+} ProgramState;
+
+// TODO: Merge this with Crypto1Params?
+typedef struct {
+    uint32_t uid; // serial number
+    uint32_t nt0; // tag challenge first
+    uint32_t nt1; // tag challenge second
+    uint32_t nr0_enc; // first encrypted reader challenge
+    uint32_t ar0_enc; // first encrypted reader response
+    uint32_t nr1_enc; // second encrypted reader challenge
+    uint32_t ar1_enc; // second encrypted reader response
+} MfClassicNonce;
+
+typedef struct {
+    Stream* stream;
+    uint32_t total_nonces;
+    MfClassicNonce* remaining_nonce_array;
+    size_t remaining_nonces;
+} MfClassicNonceArray;
+
+struct MfClassicDict {
+    Stream* stream;
+    uint32_t total_keys;
+};
+
+static const uint8_t table[256] = {
+    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3,
+    4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4,
+    4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4,
+    5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5,
+    4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2,
+    3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5,
+    5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4,
+    5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6,
+    4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8};
+static const uint8_t lookup1[256] = {
+    0, 0,  16, 16, 0,  16, 0,  0,  0, 16, 0,  0,  16, 16, 16, 16, 0, 0,  16, 16, 0,  16, 0,  0,
+    0, 16, 0,  0,  16, 16, 16, 16, 0, 0,  16, 16, 0,  16, 0,  0,  0, 16, 0,  0,  16, 16, 16, 16,
+    8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24, 8, 8,  24, 24, 8,  24, 8,  8,
+    8, 24, 8,  8,  24, 24, 24, 24, 8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24,
+    0, 0,  16, 16, 0,  16, 0,  0,  0, 16, 0,  0,  16, 16, 16, 16, 0, 0,  16, 16, 0,  16, 0,  0,
+    0, 16, 0,  0,  16, 16, 16, 16, 8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24,
+    0, 0,  16, 16, 0,  16, 0,  0,  0, 16, 0,  0,  16, 16, 16, 16, 0, 0,  16, 16, 0,  16, 0,  0,
+    0, 16, 0,  0,  16, 16, 16, 16, 8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24,
+    8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24, 0, 0,  16, 16, 0,  16, 0,  0,
+    0, 16, 0,  0,  16, 16, 16, 16, 8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24,
+    8, 8,  24, 24, 8,  24, 8,  8,  8, 24, 8,  8,  24, 24, 24, 24};
+static const uint8_t lookup2[256] = {
+    0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4,
+    4, 4, 4, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6,
+    2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, 2, 6, 6, 2, 6, 2,
+    2, 2, 6, 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4,
+    0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2,
+    2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4,
+    4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2,
+    2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2,
+    2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6};
+
+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);
+}
+
+static inline int filter(uint32_t const x) {
+    uint32_t f;
+    f = lookup1[x & 0xff] | lookup2[(x >> 8) & 0xff];
+    f |= 0x0d938 >> (x >> 16 & 0xf) & 1;
+    return BIT(0xEC57E80A, f);
+}
+
+static inline uint8_t evenparity32(uint32_t x) {
+    if((table[x & 0xff] + table[(x >> 8) & 0xff] + table[(x >> 16) & 0xff] + table[x >> 24]) % 2 ==
+       0) {
+        return 0;
+    } else {
+        return 1;
+    }
+    //return ((table[x & 0xff] + table[(x >> 8) & 0xff] + table[(x >> 16) & 0xff] + table[x >> 24]) % 2) & 0xFF;
+}
+
+static inline void update_contribution(unsigned int data[], int item, int mask1, int mask2) {
+    int p = data[item] >> 25;
+    p = p << 1 | evenparity32(data[item] & mask1);
+    p = p << 1 | evenparity32(data[item] & mask2);
+    data[item] = p << 24 | (data[item] & 0xffffff);
+}
+
+void crypto1_get_lfsr(struct Crypto1State* state, uint64_t* lfsr) {
+    int i;
+    for(*lfsr = 0, i = 23; i >= 0; --i) {
+        *lfsr = *lfsr << 1 | BIT(state->odd, i ^ 3);
+        *lfsr = *lfsr << 1 | BIT(state->even, i ^ 3);
+    }
+}
+
+static inline uint32_t crypt_word(struct Crypto1State* s) {
+    // "in" and "x" are always 0 (last iteration)
+    uint32_t res_ret = 0;
+    uint32_t feedin, t;
+    for(int i = 0; i <= 31; i++) {
+        res_ret |= (filter(s->odd) << (24 ^ i)); //-V629
+        feedin = LF_POLY_EVEN & s->even;
+        feedin ^= LF_POLY_ODD & s->odd;
+        s->even = s->even << 1 | (evenparity32(feedin));
+        t = s->odd, s->odd = s->even, s->even = t;
+    }
+    return res_ret;
+}
+
+static inline void crypt_word_noret(struct Crypto1State* s, uint32_t in, int x) {
+    uint8_t ret;
+    uint32_t feedin, t, next_in;
+    for(int i = 0; i <= 31; i++) {
+        next_in = BEBIT(in, i);
+        ret = filter(s->odd);
+        feedin = ret & (!!x);
+        feedin ^= LF_POLY_EVEN & s->even;
+        feedin ^= LF_POLY_ODD & s->odd;
+        feedin ^= !!next_in;
+        s->even = s->even << 1 | (evenparity32(feedin));
+        t = s->odd, s->odd = s->even, s->even = t;
+    }
+    return;
+}
+
+static inline void rollback_word_noret(struct Crypto1State* s, uint32_t in, int x) {
+    uint8_t ret;
+    uint32_t feedin, t, next_in;
+    for(int i = 31; i >= 0; i--) {
+        next_in = BEBIT(in, i);
+        s->odd &= 0xffffff;
+        t = s->odd, s->odd = s->even, s->even = t;
+        ret = filter(s->odd);
+        feedin = ret & (!!x);
+        feedin ^= s->even & 1;
+        feedin ^= LF_POLY_EVEN & (s->even >>= 1);
+        feedin ^= LF_POLY_ODD & s->odd;
+        feedin ^= !!next_in;
+        s->even |= (evenparity32(feedin)) << 23;
+    }
+    return;
+}
+
+int key_already_found_for_nonce(
+    uint64_t* keyarray,
+    int keyarray_size,
+    uint32_t uid_xor_nt1,
+    uint32_t nr1_enc,
+    uint32_t p64b,
+    uint32_t ar1_enc) {
+    for(int k = 0; k < keyarray_size; k++) {
+        struct Crypto1State temp = {0, 0};
+
+        for(int i = 0; i < 24; i++) {
+            (&temp)->odd |= (BIT(keyarray[k], 2 * i + 1) << (i ^ 3));
+            (&temp)->even |= (BIT(keyarray[k], 2 * i) << (i ^ 3));
+        }
+
+        crypt_word_noret(&temp, uid_xor_nt1, 0);
+        crypt_word_noret(&temp, nr1_enc, 1);
+
+        if(ar1_enc == (crypt_word(&temp) ^ p64b)) {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+int check_state(struct Crypto1State* t, struct Crypto1Params* p) {
+    if(!(t->odd | t->even)) return 0;
+    rollback_word_noret(t, 0, 0);
+    rollback_word_noret(t, p->nr0_enc, 1);
+    rollback_word_noret(t, p->uid_xor_nt0, 0);
+    struct Crypto1State temp = {t->odd, t->even};
+    crypt_word_noret(t, p->uid_xor_nt1, 0);
+    crypt_word_noret(t, p->nr1_enc, 1);
+    if(p->ar1_enc == (crypt_word(t) ^ p->p64b)) {
+        crypto1_get_lfsr(&temp, &(p->key));
+        return 1;
+    }
+    return 0;
+}
+
+static inline int state_loop(unsigned int* states_buffer, int xks, int m1, int m2) {
+    int states_tail = 0;
+    int round = 0, s = 0, xks_bit = 0;
+
+    for(round = 1; round <= 12; round++) {
+        xks_bit = BIT(xks, round);
+
+        for(s = 0; s <= states_tail; s++) {
+            states_buffer[s] <<= 1;
+
+            if((filter(states_buffer[s]) ^ filter(states_buffer[s] | 1)) != 0) {
+                states_buffer[s] |= filter(states_buffer[s]) ^ xks_bit;
+                if(round > 4) {
+                    update_contribution(states_buffer, s, m1, m2);
+                }
+            } else if(filter(states_buffer[s]) == xks_bit) {
+                // TODO: Refactor
+                if(round > 4) {
+                    states_buffer[++states_tail] = states_buffer[s + 1];
+                    states_buffer[s + 1] = states_buffer[s] | 1;
+                    update_contribution(states_buffer, s, m1, m2);
+                    s++;
+                    update_contribution(states_buffer, s, m1, m2);
+                } else {
+                    states_buffer[++states_tail] = states_buffer[++s];
+                    states_buffer[s] = states_buffer[s - 1] | 1;
+                }
+            } else {
+                states_buffer[s--] = states_buffer[states_tail--];
+            }
+        }
+    }
+
+    return states_tail;
+}
+
+int binsearch(unsigned int data[], int start, int stop) {
+    int mid, val = data[stop] & 0xff000000;
+    while(start != stop) {
+        mid = (stop - start) >> 1;
+        if((data[start + mid] ^ 0x80000000) > (val ^ 0x80000000))
+            stop = start + mid;
+        else
+            start += mid + 1;
+    }
+    return start;
+}
+void quicksort(unsigned int array[], int low, int high) {
+    //if (SIZEOF(array) == 0)
+    //    return;
+    if(low >= high) return;
+    int middle = low + (high - low) / 2;
+    unsigned int pivot = array[middle];
+    int i = low, j = high;
+    while(i <= j) {
+        while(array[i] < pivot) {
+            i++;
+        }
+        while(array[j] > pivot) {
+            j--;
+        }
+        if(i <= j) { // swap
+            int temp = array[i];
+            array[i] = array[j];
+            array[j] = temp;
+            i++;
+            j--;
+        }
+    }
+    if(low < j) {
+        quicksort(array, low, j);
+    }
+    if(high > i) {
+        quicksort(array, i, high);
+    }
+}
+int extend_table(unsigned int data[], int tbl, int end, int bit, int m1, int m2) {
+    for(data[tbl] <<= 1; tbl <= end; data[++tbl] <<= 1) {
+        if((filter(data[tbl]) ^ filter(data[tbl] | 1)) != 0) {
+            data[tbl] |= filter(data[tbl]) ^ bit;
+            update_contribution(data, tbl, m1, m2);
+        } else if(filter(data[tbl]) == bit) {
+            data[++end] = data[tbl + 1];
+            data[tbl + 1] = data[tbl] | 1;
+            update_contribution(data, tbl, m1, m2);
+            tbl++;
+            update_contribution(data, tbl, m1, m2);
+        } else {
+            data[tbl--] = data[end--];
+        }
+    }
+    return end;
+}
+
+int old_recover(
+    unsigned int odd[],
+    int o_head,
+    int o_tail,
+    int oks,
+    unsigned int even[],
+    int e_head,
+    int e_tail,
+    int eks,
+    int rem,
+    int s,
+    struct Crypto1Params* p,
+    int first_run) {
+    int o, e, i;
+    if(rem == -1) {
+        for(e = e_head; e <= e_tail; ++e) {
+            even[e] = (even[e] << 1) ^ evenparity32(even[e] & LF_POLY_EVEN);
+            for(o = o_head; o <= o_tail; ++o, ++s) {
+                struct Crypto1State temp = {0, 0};
+                temp.even = odd[o];
+                temp.odd = even[e] ^ evenparity32(odd[o] & LF_POLY_ODD);
+                if(check_state(&temp, p)) {
+                    return -1;
+                }
+            }
+        }
+        return s;
+    }
+    if(first_run == 0) {
+        for(i = 0; (i < 4) && (rem-- != 0); i++) {
+            oks >>= 1;
+            eks >>= 1;
+            o_tail = extend_table(
+                odd, o_head, o_tail, oks & 1, LF_POLY_EVEN << 1 | 1, LF_POLY_ODD << 1);
+            if(o_head > o_tail) return s;
+            e_tail =
+                extend_table(even, e_head, e_tail, eks & 1, LF_POLY_ODD, LF_POLY_EVEN << 1 | 1);
+            if(e_head > e_tail) return s;
+        }
+    }
+    first_run = 0;
+    quicksort(odd, o_head, o_tail);
+    quicksort(even, e_head, e_tail);
+    while(o_tail >= o_head && e_tail >= e_head) {
+        if(((odd[o_tail] ^ even[e_tail]) >> 24) == 0) {
+            o_tail = binsearch(odd, o_head, o = o_tail);
+            e_tail = binsearch(even, e_head, e = e_tail);
+            s = old_recover(odd, o_tail--, o, oks, even, e_tail--, e, eks, rem, s, p, first_run);
+            if(s == -1) {
+                break;
+            }
+        } else if((odd[o_tail] ^ 0x80000000) > (even[e_tail] ^ 0x80000000)) {
+            o_tail = binsearch(odd, o_head, o_tail) - 1;
+        } else {
+            e_tail = binsearch(even, e_head, e_tail) - 1;
+        }
+    }
+    return s;
+}
+
+static inline int sync_state(ProgramState* program_state) {
+    int ts = furi_hal_rtc_get_timestamp();
+    program_state->eta_round = program_state->eta_round - (ts - program_state->eta_timestamp);
+    program_state->eta_total = program_state->eta_total - (ts - program_state->eta_timestamp);
+    program_state->eta_timestamp = ts;
+    if(program_state->close_thread_please) {
+        return 1;
+    }
+    return 0;
+}
+
+int calculate_msb_tables(
+    int oks,
+    int eks,
+    int msb_round,
+    struct Crypto1Params* p,
+    unsigned int* states_buffer,
+    struct Msb* odd_msbs,
+    struct Msb* even_msbs,
+    unsigned int* temp_states_odd,
+    unsigned int* temp_states_even,
+    ProgramState* program_state) {
+    //FURI_LOG_I(TAG, "MSB GO %i", msb_iter); // DEBUG
+    unsigned int msb_head = (MSB_LIMIT * msb_round); // msb_iter ranges from 0 to (256/MSB_LIMIT)-1
+    unsigned int msb_tail = (MSB_LIMIT * (msb_round + 1));
+    int states_tail = 0, tail = 0;
+    int i = 0, j = 0, semi_state = 0, found = 0;
+    unsigned int msb = 0;
+    // TODO: Why is this necessary?
+    memset(odd_msbs, 0, MSB_LIMIT * sizeof(struct Msb));
+    memset(even_msbs, 0, MSB_LIMIT * sizeof(struct Msb));
+
+    for(semi_state = 1 << 20; semi_state >= 0; semi_state--) {
+        if(semi_state % 32768 == 0) {
+            if(sync_state(program_state) == 1) {
+                return 0;
+            }
+        }
+
+        if(filter(semi_state) == (oks & 1)) { //-V547
+            states_buffer[0] = semi_state;
+            states_tail = state_loop(states_buffer, oks, CONST_M1_1, CONST_M2_1);
+
+            for(i = states_tail; i >= 0; i--) {
+                msb = states_buffer[i] >> 24;
+                if((msb >= msb_head) && (msb < msb_tail)) {
+                    found = 0;
+                    for(j = 0; j < odd_msbs[msb - msb_head].tail - 1; j++) {
+                        if(odd_msbs[msb - msb_head].states[j] == states_buffer[i]) {
+                            found = 1;
+                            break;
+                        }
+                    }
+
+                    if(!found) {
+                        tail = odd_msbs[msb - msb_head].tail++;
+                        odd_msbs[msb - msb_head].states[tail] = states_buffer[i];
+                    }
+                }
+            }
+        }
+
+        if(filter(semi_state) == (eks & 1)) { //-V547
+            states_buffer[0] = semi_state;
+            states_tail = state_loop(states_buffer, eks, CONST_M1_2, CONST_M2_2);
+
+            for(i = 0; i <= states_tail; i++) {
+                msb = states_buffer[i] >> 24;
+                if((msb >= msb_head) && (msb < msb_tail)) {
+                    found = 0;
+
+                    for(j = 0; j < even_msbs[msb - msb_head].tail; j++) {
+                        if(even_msbs[msb - msb_head].states[j] == states_buffer[i]) {
+                            found = 1;
+                            break;
+                        }
+                    }
+
+                    if(!found) {
+                        tail = even_msbs[msb - msb_head].tail++;
+                        even_msbs[msb - msb_head].states[tail] = states_buffer[i];
+                    }
+                }
+            }
+        }
+    }
+
+    oks >>= 12;
+    eks >>= 12;
+
+    for(i = 0; i < MSB_LIMIT; i++) {
+        if(sync_state(program_state) == 1) {
+            return 0;
+        }
+        // TODO: Why is this necessary?
+        memset(temp_states_even, 0, sizeof(unsigned int) * (1280));
+        memset(temp_states_odd, 0, sizeof(unsigned int) * (1280));
+        memcpy(temp_states_odd, odd_msbs[i].states, odd_msbs[i].tail * sizeof(unsigned int));
+        memcpy(temp_states_even, even_msbs[i].states, even_msbs[i].tail * sizeof(unsigned int));
+        int res = old_recover(
+            temp_states_odd,
+            0,
+            odd_msbs[i].tail,
+            oks,
+            temp_states_even,
+            0,
+            even_msbs[i].tail,
+            eks,
+            3,
+            0,
+            p,
+            1);
+        if(res == -1) {
+            return 1;
+        }
+        //odd_msbs[i].tail = 0;
+        //even_msbs[i].tail = 0;
+    }
+
+    return 0;
+}
+
+bool recover(struct Crypto1Params* p, int ks2, ProgramState* program_state) {
+    bool found = false;
+    unsigned int* states_buffer = malloc(sizeof(unsigned int) * (2 << 9));
+    struct Msb* odd_msbs = (struct Msb*)malloc(MSB_LIMIT * sizeof(struct Msb));
+    struct Msb* even_msbs = (struct Msb*)malloc(MSB_LIMIT * sizeof(struct Msb));
+    unsigned int* temp_states_odd = malloc(sizeof(unsigned int) * (1280));
+    unsigned int* temp_states_even = malloc(sizeof(unsigned int) * (1280));
+    int oks = 0, eks = 0;
+    int i = 0, msb = 0;
+    for(i = 31; i >= 0; i -= 2) {
+        oks = oks << 1 | BEBIT(ks2, i);
+    }
+    for(i = 30; i >= 0; i -= 2) {
+        eks = eks << 1 | BEBIT(ks2, i);
+    }
+    int bench_start = furi_hal_rtc_get_timestamp();
+    program_state->eta_total = eta_total_time;
+    program_state->eta_timestamp = bench_start;
+    for(msb = 0; msb <= ((256 / MSB_LIMIT) - 1); msb++) {
+        program_state->search = msb;
+        program_state->eta_round = eta_round_time;
+        program_state->eta_total = eta_total_time - (eta_round_time * msb);
+        if(calculate_msb_tables(
+               oks,
+               eks,
+               msb,
+               p,
+               states_buffer,
+               odd_msbs,
+               even_msbs,
+               temp_states_odd,
+               temp_states_even,
+               program_state)) {
+            int bench_stop = furi_hal_rtc_get_timestamp();
+            FURI_LOG_I(TAG, "Cracked in %i seconds", bench_stop - bench_start);
+            found = true;
+            break;
+        }
+        if(program_state->close_thread_please) {
+            break;
+        }
+    }
+    free(states_buffer);
+    free(odd_msbs);
+    free(even_msbs);
+    free(temp_states_odd);
+    free(temp_states_even);
+    return found;
+}
+
+bool napi_mf_classic_dict_check_presence(MfClassicDictType dict_type) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    bool dict_present = false;
+    if(dict_type == MfClassicDictTypeSystem) {
+        dict_present = storage_common_stat(storage, MF_CLASSIC_DICT_FLIPPER_PATH, NULL) == FSE_OK;
+    } else if(dict_type == MfClassicDictTypeUser) {
+        dict_present = storage_common_stat(storage, MF_CLASSIC_DICT_USER_PATH, NULL) == FSE_OK;
+    }
+
+    furi_record_close(RECORD_STORAGE);
+
+    return dict_present;
+}
+
+MfClassicDict* napi_mf_classic_dict_alloc(MfClassicDictType dict_type) {
+    MfClassicDict* dict = malloc(sizeof(MfClassicDict));
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    dict->stream = buffered_file_stream_alloc(storage);
+    furi_record_close(RECORD_STORAGE);
+
+    bool dict_loaded = false;
+    do {
+        if(dict_type == MfClassicDictTypeSystem) {
+            if(!buffered_file_stream_open(
+                   dict->stream,
+                   MF_CLASSIC_DICT_FLIPPER_PATH,
+                   FSAM_READ_WRITE,
+                   FSOM_OPEN_EXISTING)) {
+                buffered_file_stream_close(dict->stream);
+                break;
+            }
+        } else if(dict_type == MfClassicDictTypeUser) {
+            if(!buffered_file_stream_open(
+                   dict->stream, MF_CLASSIC_DICT_USER_PATH, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS)) {
+                buffered_file_stream_close(dict->stream);
+                break;
+            }
+        }
+
+        // Check for newline ending
+        if(!stream_eof(dict->stream)) {
+            if(!stream_seek(dict->stream, -1, StreamOffsetFromEnd)) break;
+            uint8_t last_char = 0;
+            if(stream_read(dict->stream, &last_char, 1) != 1) break;
+            if(last_char != '\n') {
+                FURI_LOG_D(TAG, "Adding new line ending");
+                if(stream_write_char(dict->stream, '\n') != 1) break;
+            }
+            if(!stream_rewind(dict->stream)) break;
+        }
+
+        // Read total amount of keys
+        FuriString* next_line;
+        next_line = furi_string_alloc();
+        while(true) {
+            if(!stream_read_line(dict->stream, next_line)) {
+                FURI_LOG_T(TAG, "No keys left in dict");
+                break;
+            }
+            FURI_LOG_T(
+                TAG,
+                "Read line: %s, len: %zu",
+                furi_string_get_cstr(next_line),
+                furi_string_size(next_line));
+            if(furi_string_get_char(next_line, 0) == '#') continue;
+            if(furi_string_size(next_line) != NFC_MF_CLASSIC_KEY_LEN) continue;
+            dict->total_keys++;
+        }
+        furi_string_free(next_line);
+        stream_rewind(dict->stream);
+
+        dict_loaded = true;
+        FURI_LOG_I(TAG, "Loaded dictionary with %lu keys", dict->total_keys);
+    } while(false);
+
+    if(!dict_loaded) {
+        buffered_file_stream_close(dict->stream);
+        free(dict);
+        dict = NULL;
+    }
+
+    return dict;
+}
+
+bool napi_mf_classic_dict_add_key_str(MfClassicDict* dict, FuriString* key) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+    FURI_LOG_I(TAG, "Saving key: %s", furi_string_get_cstr(key));
+
+    furi_string_cat_printf(key, "\n");
+
+    bool key_added = false;
+    do {
+        if(!stream_seek(dict->stream, 0, StreamOffsetFromEnd)) break;
+        if(!stream_insert_string(dict->stream, key)) break;
+        dict->total_keys++;
+        key_added = true;
+    } while(false);
+
+    furi_string_left(key, 12);
+    return key_added;
+}
+
+void napi_mf_classic_dict_free(MfClassicDict* dict) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    buffered_file_stream_close(dict->stream);
+    stream_free(dict->stream);
+    free(dict);
+}
+
+static void napi_mf_classic_dict_int_to_str(uint8_t* key_int, FuriString* key_str) {
+    furi_string_reset(key_str);
+    for(size_t i = 0; i < 6; i++) {
+        furi_string_cat_printf(key_str, "%02X", key_int[i]);
+    }
+}
+
+static void napi_mf_classic_dict_str_to_int(FuriString* key_str, uint64_t* key_int) {
+    uint8_t key_byte_tmp;
+
+    *key_int = 0ULL;
+    for(uint8_t i = 0; i < 12; i += 2) {
+        args_char_to_hex(
+            furi_string_get_char(key_str, i), furi_string_get_char(key_str, i + 1), &key_byte_tmp);
+        *key_int |= (uint64_t)key_byte_tmp << (8 * (5 - i / 2));
+    }
+}
+
+uint32_t napi_mf_classic_dict_get_total_keys(MfClassicDict* dict) {
+    furi_assert(dict);
+
+    return dict->total_keys;
+}
+
+bool napi_mf_classic_dict_rewind(MfClassicDict* dict) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    return stream_rewind(dict->stream);
+}
+
+bool napi_mf_classic_dict_get_next_key_str(MfClassicDict* dict, FuriString* key) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    bool key_read = false;
+    furi_string_reset(key);
+    while(!key_read) {
+        if(!stream_read_line(dict->stream, key)) break;
+        if(furi_string_get_char(key, 0) == '#') continue;
+        if(furi_string_size(key) != NFC_MF_CLASSIC_KEY_LEN) continue;
+        furi_string_left(key, 12);
+        key_read = true;
+    }
+
+    return key_read;
+}
+
+bool napi_mf_classic_dict_get_next_key(MfClassicDict* dict, uint64_t* key) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    FuriString* temp_key;
+    temp_key = furi_string_alloc();
+    bool key_read = napi_mf_classic_dict_get_next_key_str(dict, temp_key);
+    if(key_read) {
+        napi_mf_classic_dict_str_to_int(temp_key, key);
+    }
+    furi_string_free(temp_key);
+    return key_read;
+}
+
+bool napi_mf_classic_dict_is_key_present_str(MfClassicDict* dict, FuriString* key) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    FuriString* next_line;
+    next_line = furi_string_alloc();
+
+    bool key_found = false;
+    stream_rewind(dict->stream);
+    while(!key_found) { //-V654
+        if(!stream_read_line(dict->stream, next_line)) break;
+        if(furi_string_get_char(next_line, 0) == '#') continue;
+        if(furi_string_size(next_line) != NFC_MF_CLASSIC_KEY_LEN) continue;
+        furi_string_left(next_line, 12);
+        if(!furi_string_equal(key, next_line)) continue;
+        key_found = true;
+    }
+
+    furi_string_free(next_line);
+    return key_found;
+}
+
+bool napi_mf_classic_dict_is_key_present(MfClassicDict* dict, uint8_t* key) {
+    FuriString* temp_key;
+
+    temp_key = furi_string_alloc();
+    napi_mf_classic_dict_int_to_str(key, temp_key);
+    bool key_found = napi_mf_classic_dict_is_key_present_str(dict, temp_key);
+    furi_string_free(temp_key);
+    return key_found;
+}
+
+bool napi_key_already_found_for_nonce(
+    MfClassicDict* dict,
+    uint32_t uid_xor_nt1,
+    uint32_t nr1_enc,
+    uint32_t p64b,
+    uint32_t ar1_enc) {
+    bool found = false;
+    uint64_t k = 0;
+    napi_mf_classic_dict_rewind(dict);
+    while(napi_mf_classic_dict_get_next_key(dict, &k)) {
+        struct Crypto1State temp = {0, 0};
+        int i;
+        for(i = 0; i < 24; i++) {
+            (&temp)->odd |= (BIT(k, 2 * i + 1) << (i ^ 3));
+            (&temp)->even |= (BIT(k, 2 * i) << (i ^ 3));
+        }
+        crypt_word_noret(&temp, uid_xor_nt1, 0);
+        crypt_word_noret(&temp, nr1_enc, 1);
+        if(ar1_enc == (crypt_word(&temp) ^ p64b)) {
+            found = true;
+            break;
+        }
+    }
+    return found;
+}
+
+bool napi_mf_classic_nonces_check_presence() {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    bool nonces_present = storage_common_stat(storage, MF_CLASSIC_NONCE_PATH, NULL) == FSE_OK;
+
+    furi_record_close(RECORD_STORAGE);
+
+    return nonces_present;
+}
+
+MfClassicNonceArray* napi_mf_classic_nonce_array_alloc(
+    MfClassicDict* system_dict,
+    bool system_dict_exists,
+    MfClassicDict* user_dict,
+    ProgramState* program_state) {
+    MfClassicNonceArray* nonce_array = malloc(sizeof(MfClassicNonceArray));
+    MfClassicNonce* remaining_nonce_array_init = malloc(sizeof(MfClassicNonce) * 1);
+    nonce_array->remaining_nonce_array = remaining_nonce_array_init;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    nonce_array->stream = buffered_file_stream_alloc(storage);
+    furi_record_close(RECORD_STORAGE);
+
+    bool array_loaded = false;
+    do {
+        // https://github.com/flipperdevices/flipperzero-firmware/blob/5134f44c09d39344a8747655c0d59864bb574b96/applications/services/storage/filesystem_api_defines.h#L8-L22
+        if(!buffered_file_stream_open(
+               nonce_array->stream, MF_CLASSIC_NONCE_PATH, FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) {
+            buffered_file_stream_close(nonce_array->stream);
+            break;
+        }
+
+        // Check for newline ending
+        if(!stream_eof(nonce_array->stream)) {
+            if(!stream_seek(nonce_array->stream, -1, StreamOffsetFromEnd)) break;
+            uint8_t last_char = 0;
+            if(stream_read(nonce_array->stream, &last_char, 1) != 1) break;
+            if(last_char != '\n') {
+                FURI_LOG_D(TAG, "Adding new line ending");
+                if(stream_write_char(nonce_array->stream, '\n') != 1) break;
+            }
+            if(!stream_rewind(nonce_array->stream)) break;
+        }
+
+        // Read total amount of nonces
+        FuriString* next_line;
+        next_line = furi_string_alloc();
+        while(!(program_state->close_thread_please)) {
+            if(!stream_read_line(nonce_array->stream, next_line)) {
+                FURI_LOG_T(TAG, "No nonces left");
+                break;
+            }
+            FURI_LOG_T(
+                TAG,
+                "Read line: %s, len: %zu",
+                furi_string_get_cstr(next_line),
+                furi_string_size(next_line));
+            if(!furi_string_start_with_str(next_line, "Sec")) continue;
+            const char* next_line_cstr = furi_string_get_cstr(next_line);
+            MfClassicNonce res = {0};
+            int i = 0;
+            char* endptr;
+            for(i = 0; i <= 17; i++) {
+                if(i != 0) {
+                    next_line_cstr = strchr(next_line_cstr, ' ');
+                    if(next_line_cstr) {
+                        next_line_cstr++;
+                    } else {
+                        break;
+                    }
+                }
+                unsigned long value = strtoul(next_line_cstr, &endptr, 16);
+                switch(i) {
+                case 5:
+                    res.uid = value;
+                    break;
+                case 7:
+                    res.nt0 = value;
+                    break;
+                case 9:
+                    res.nr0_enc = value;
+                    break;
+                case 11:
+                    res.ar0_enc = value;
+                    break;
+                case 13:
+                    res.nt1 = value;
+                    break;
+                case 15:
+                    res.nr1_enc = value;
+                    break;
+                case 17:
+                    res.ar1_enc = value;
+                    break;
+                default:
+                    break; // Do nothing
+                }
+                next_line_cstr = endptr;
+            }
+            (program_state->total)++;
+            uint32_t p64b = prng_successor(res.nt1, 64);
+            if((system_dict_exists &&
+                napi_key_already_found_for_nonce(
+                    system_dict, res.uid ^ res.nt1, res.nr1_enc, p64b, res.ar1_enc)) ||
+               (napi_key_already_found_for_nonce(
+                   user_dict, res.uid ^ res.nt1, res.nr1_enc, p64b, res.ar1_enc))) {
+                (program_state->cracked)++;
+                (program_state->num_completed)++;
+                continue;
+            }
+            FURI_LOG_I(TAG, "No key found for %8lx %8lx", res.uid, res.ar1_enc);
+            // TODO: Refactor
+            nonce_array->remaining_nonce_array = realloc( //-V701
+                nonce_array->remaining_nonce_array,
+                sizeof(MfClassicNonce) * ((nonce_array->remaining_nonces) + 1));
+            nonce_array->remaining_nonces++;
+            nonce_array->remaining_nonce_array[(nonce_array->remaining_nonces) - 1] = res;
+            nonce_array->total_nonces++;
+        }
+        furi_string_free(next_line);
+        buffered_file_stream_close(nonce_array->stream);
+
+        array_loaded = true;
+        FURI_LOG_I(TAG, "Loaded %lu nonces", nonce_array->total_nonces);
+    } while(false);
+
+    if(!array_loaded) {
+        free(nonce_array);
+        nonce_array = NULL;
+    }
+
+    return nonce_array;
+}
+
+void napi_mf_classic_nonce_array_free(MfClassicNonceArray* nonce_array) {
+    furi_assert(nonce_array);
+    furi_assert(nonce_array->stream);
+
+    buffered_file_stream_close(nonce_array->stream);
+    stream_free(nonce_array->stream);
+    free(nonce_array);
+}
+
+static void finished_beep() {
+    // Beep to indicate completion
+    NotificationApp* notification = furi_record_open("notification");
+    notification_message(notification, &sequence_audiovisual_alert);
+    notification_message(notification, &sequence_display_backlight_on);
+    furi_record_close("notification");
+}
+
+void mfkey32(ProgramState* program_state) {
+    uint64_t found_key; // recovered key
+    size_t keyarray_size = 0;
+    uint64_t* keyarray = malloc(sizeof(uint64_t) * 1);
+    uint32_t i = 0, j = 0;
+    // Check for nonces
+    if(!napi_mf_classic_nonces_check_presence()) {
+        program_state->err = MissingNonces;
+        program_state->mfkey_state = Error;
+        free(keyarray);
+        return;
+    }
+    // Read dictionaries (optional)
+    MfClassicDict* system_dict = {0};
+    bool system_dict_exists = napi_mf_classic_dict_check_presence(MfClassicDictTypeSystem);
+    MfClassicDict* user_dict = {0};
+    bool user_dict_exists = napi_mf_classic_dict_check_presence(MfClassicDictTypeUser);
+    uint32_t total_dict_keys = 0;
+    if(system_dict_exists) {
+        system_dict = napi_mf_classic_dict_alloc(MfClassicDictTypeSystem);
+        total_dict_keys += napi_mf_classic_dict_get_total_keys(system_dict);
+    }
+    user_dict = napi_mf_classic_dict_alloc(MfClassicDictTypeUser);
+    if(user_dict_exists) {
+        total_dict_keys += napi_mf_classic_dict_get_total_keys(user_dict);
+    }
+    user_dict_exists = true;
+    program_state->dict_count = total_dict_keys;
+    program_state->mfkey_state = DictionaryAttack;
+    // Read nonces
+    MfClassicNonceArray* nonce_arr;
+    nonce_arr = napi_mf_classic_nonce_array_alloc(
+        system_dict, system_dict_exists, user_dict, program_state);
+    if(system_dict_exists) {
+        napi_mf_classic_dict_free(system_dict);
+    }
+    if(nonce_arr->total_nonces == 0) {
+        // Nothing to crack
+        program_state->err = ZeroNonces;
+        program_state->mfkey_state = Error;
+        napi_mf_classic_nonce_array_free(nonce_arr);
+        napi_mf_classic_dict_free(user_dict);
+        free(keyarray);
+        return;
+    }
+    if(memmgr_get_free_heap() < MIN_RAM) {
+        // System has less than the guaranteed amount of RAM (140 KB) - adjust some parameters to run anyway at half speed
+        eta_round_time *= 2;
+        eta_total_time *= 2;
+        MSB_LIMIT /= 2;
+    }
+    program_state->mfkey_state = MfkeyAttack;
+    // TODO: Work backwards on this array and free memory
+    for(i = 0; i < nonce_arr->total_nonces; i++) {
+        MfClassicNonce next_nonce = nonce_arr->remaining_nonce_array[i];
+        uint32_t p64 = prng_successor(next_nonce.nt0, 64);
+        uint32_t p64b = prng_successor(next_nonce.nt1, 64);
+        if(key_already_found_for_nonce(
+               keyarray,
+               keyarray_size,
+               next_nonce.uid ^ next_nonce.nt1,
+               next_nonce.nr1_enc,
+               p64b,
+               next_nonce.ar1_enc)) {
+            nonce_arr->remaining_nonces--;
+            (program_state->cracked)++;
+            (program_state->num_completed)++;
+            continue;
+        }
+        FURI_LOG_I(TAG, "Cracking %8lx %8lx", next_nonce.uid, next_nonce.ar1_enc);
+        struct Crypto1Params p = {
+            0,
+            next_nonce.nr0_enc,
+            next_nonce.uid ^ next_nonce.nt0,
+            next_nonce.uid ^ next_nonce.nt1,
+            next_nonce.nr1_enc,
+            p64b,
+            next_nonce.ar1_enc};
+        if(!recover(&p, next_nonce.ar0_enc ^ p64, program_state)) {
+            if(program_state->close_thread_please) {
+                break;
+            }
+            // No key found in recover()
+            (program_state->num_completed)++;
+            continue;
+        }
+        (program_state->cracked)++;
+        (program_state->num_completed)++;
+        found_key = p.key;
+        bool already_found = false;
+        for(j = 0; j < keyarray_size; j++) {
+            if(keyarray[j] == found_key) {
+                already_found = true;
+                break;
+            }
+        }
+        if(already_found == false) {
+            // New key
+            keyarray = realloc(keyarray, sizeof(uint64_t) * (keyarray_size + 1)); //-V701
+            keyarray_size += 1;
+            keyarray[keyarray_size - 1] = found_key;
+            (program_state->unique_cracked)++;
+        }
+    }
+    // TODO: Update display to show all keys were found
+    // TODO: Prepend found key(s) to user dictionary file
+    //FURI_LOG_I(TAG, "Unique keys found:");
+    for(i = 0; i < keyarray_size; i++) {
+        //FURI_LOG_I(TAG, "%012" PRIx64, keyarray[i]);
+        FuriString* temp_key = furi_string_alloc();
+        furi_string_cat_printf(temp_key, "%012" PRIX64, keyarray[i]);
+        napi_mf_classic_dict_add_key_str(user_dict, temp_key);
+        furi_string_free(temp_key);
+    }
+    if(keyarray_size > 0) {
+        // TODO: Should we use DolphinDeedNfcMfcAdd?
+        dolphin_deed(DolphinDeedNfcMfcAdd);
+    }
+    napi_mf_classic_nonce_array_free(nonce_arr);
+    napi_mf_classic_dict_free(user_dict);
+    free(keyarray);
+    //FURI_LOG_I(TAG, "mfkey32 function completed normally"); // DEBUG
+    program_state->mfkey_state = Complete;
+    // No need to alert the user if they asked it to stop
+    if(!(program_state->close_thread_please)) {
+        finished_beep();
+    }
+    return;
+}
+
+// Screen is 128x64 px
+static void render_callback(Canvas* const canvas, void* ctx) {
+    furi_assert(ctx);
+    ProgramState* program_state = ctx;
+    furi_mutex_acquire(program_state->mutex, FuriWaitForever);
+    char draw_str[44] = {};
+    canvas_clear(canvas);
+    canvas_draw_frame(canvas, 0, 0, 128, 64);
+    canvas_draw_frame(canvas, 0, 15, 128, 64);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 5, 4, AlignLeft, AlignTop, "Mfkey32");
+    canvas_draw_icon(canvas, 114, 4, &I_mfkey);
+    if(program_state->is_thread_running && program_state->mfkey_state == MfkeyAttack) {
+        float eta_round = (float)1 - ((float)program_state->eta_round / (float)eta_round_time);
+        float eta_total = (float)1 - ((float)program_state->eta_total / (float)eta_total_time);
+        float progress = (float)program_state->num_completed / (float)program_state->total;
+        if(eta_round < 0) {
+            // Round ETA miscalculated
+            eta_round = 1;
+            program_state->eta_round = 0;
+        }
+        if(eta_total < 0) {
+            // Total ETA miscalculated
+            eta_total = 1;
+            program_state->eta_total = 0;
+        }
+        canvas_set_font(canvas, FontSecondary);
+        snprintf(
+            draw_str,
+            sizeof(draw_str),
+            "Cracking: %d/%d - in prog.",
+            program_state->num_completed,
+            program_state->total);
+        elements_progress_bar_with_text(canvas, 5, 18, 118, progress, draw_str);
+        snprintf(
+            draw_str,
+            sizeof(draw_str),
+            "Round: %d/%d - ETA %02d Sec",
+            (program_state->search) + 1, // Zero indexed
+            256 / MSB_LIMIT,
+            program_state->eta_round);
+        elements_progress_bar_with_text(canvas, 5, 31, 118, eta_round, draw_str);
+        snprintf(draw_str, sizeof(draw_str), "Total ETA %03d Sec", program_state->eta_total);
+        elements_progress_bar_with_text(canvas, 5, 44, 118, eta_total, draw_str);
+    } else if(program_state->is_thread_running && program_state->mfkey_state == DictionaryAttack) {
+        canvas_set_font(canvas, FontSecondary);
+        snprintf(
+            draw_str, sizeof(draw_str), "Dict solves: %d (in progress)", program_state->cracked);
+        canvas_draw_str_aligned(canvas, 10, 18, AlignLeft, AlignTop, draw_str);
+        snprintf(draw_str, sizeof(draw_str), "Keys in dict: %d", program_state->dict_count);
+        canvas_draw_str_aligned(canvas, 26, 28, AlignLeft, AlignTop, draw_str);
+    } else if(program_state->mfkey_state == Complete) {
+        // TODO: Scrollable list view to see cracked keys if user presses down
+        elements_progress_bar_with_text(canvas, 5, 18, 118, 1, draw_str);
+        canvas_set_font(canvas, FontSecondary);
+        snprintf(draw_str, sizeof(draw_str), "Complete");
+        canvas_draw_str_aligned(canvas, 40, 31, AlignLeft, AlignTop, draw_str);
+        snprintf(
+            draw_str,
+            sizeof(draw_str),
+            "Keys added to user dict: %d",
+            program_state->unique_cracked);
+        canvas_draw_str_aligned(canvas, 10, 41, AlignLeft, AlignTop, draw_str);
+    } else if(program_state->mfkey_state == Ready) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(canvas, 50, 30, AlignLeft, AlignTop, "Ready");
+        elements_button_center(canvas, "Start");
+        elements_button_right(canvas, "Help");
+    } else if(program_state->mfkey_state == Help) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(canvas, 7, 20, AlignLeft, AlignTop, "Collect nonces using");
+        canvas_draw_str_aligned(canvas, 7, 30, AlignLeft, AlignTop, "Detect Reader.");
+        canvas_draw_str_aligned(canvas, 7, 40, AlignLeft, AlignTop, "Developers: noproto, AG");
+        canvas_draw_str_aligned(canvas, 7, 50, AlignLeft, AlignTop, "Thanks: bettse");
+    } else if(program_state->mfkey_state == Error) {
+        canvas_draw_str_aligned(canvas, 50, 25, AlignLeft, AlignTop, "Error");
+        canvas_set_font(canvas, FontSecondary);
+        if(program_state->err == MissingNonces) {
+            canvas_draw_str_aligned(canvas, 25, 36, AlignLeft, AlignTop, "No nonces found");
+        } else if(program_state->err == ZeroNonces) {
+            canvas_draw_str_aligned(canvas, 15, 36, AlignLeft, AlignTop, "Nonces already cracked");
+        } else {
+            // Unhandled error
+        }
+    } else {
+        // Unhandled program state
+    }
+    furi_mutex_release(program_state->mutex);
+}
+
+static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    PluginEvent event = {.type = EventTypeKey, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void mfkey32_state_init(ProgramState* program_state) {
+    program_state->is_thread_running = false;
+    program_state->mfkey_state = Ready;
+    program_state->cracked = 0;
+    program_state->unique_cracked = 0;
+    program_state->num_completed = 0;
+    program_state->total = 0;
+    program_state->dict_count = 0;
+}
+
+// Entrypoint for worker thread
+static int32_t mfkey32_worker_thread(void* ctx) {
+    ProgramState* program_state = ctx;
+    program_state->is_thread_running = true;
+    program_state->mfkey_state = Initializing;
+    //FURI_LOG_I(TAG, "Hello from the mfkey32 worker thread"); // DEBUG
+    mfkey32(program_state);
+    program_state->is_thread_running = false;
+    return 0;
+}
+
+void start_mfkey32_thread(ProgramState* program_state) {
+    if(!program_state->is_thread_running) {
+        furi_thread_start(program_state->mfkeythread);
+    }
+}
+
+int32_t mfkey32_main() {
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
+
+    ProgramState* program_state = malloc(sizeof(ProgramState));
+
+    mfkey32_state_init(program_state);
+
+    program_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    if(!program_state->mutex) {
+        FURI_LOG_E(TAG, "cannot create mutex\r\n");
+        free(program_state);
+        return 255;
+    }
+
+    // Set system callbacks
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, render_callback, program_state);
+    view_port_input_callback_set(view_port, input_callback, event_queue);
+
+    // Open GUI and register view_port
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    program_state->mfkeythread = furi_thread_alloc();
+    furi_thread_set_name(program_state->mfkeythread, "Mfkey32 Worker");
+    furi_thread_set_stack_size(program_state->mfkeythread, 2048);
+    furi_thread_set_context(program_state->mfkeythread, program_state);
+    furi_thread_set_callback(program_state->mfkeythread, mfkey32_worker_thread);
+
+    PluginEvent event;
+    for(bool main_loop = true; main_loop;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
+
+        furi_mutex_acquire(program_state->mutex, FuriWaitForever);
+
+        if(event_status == FuriStatusOk) {
+            // press events
+            if(event.type == EventTypeKey) {
+                if(event.input.type == InputTypePress) {
+                    switch(event.input.key) {
+                    case InputKeyUp:
+                        break;
+                    case InputKeyDown:
+                        break;
+                    case InputKeyRight:
+                        if(!program_state->is_thread_running &&
+                           program_state->mfkey_state == Ready) {
+                            program_state->mfkey_state = Help;
+                            view_port_update(view_port);
+                        }
+                        break;
+                    case InputKeyLeft:
+                        break;
+                    case InputKeyOk:
+                        if(!program_state->is_thread_running &&
+                           program_state->mfkey_state == Ready) {
+                            start_mfkey32_thread(program_state);
+                            view_port_update(view_port);
+                        }
+                        break;
+                    case InputKeyBack:
+                        if(!program_state->is_thread_running &&
+                           program_state->mfkey_state == Help) {
+                            program_state->mfkey_state = Ready;
+                            view_port_update(view_port);
+                        } else {
+                            program_state->close_thread_please = true;
+                            if(program_state->is_thread_running && program_state->mfkeythread) {
+                                // Wait until thread is finished
+                                furi_thread_join(program_state->mfkeythread);
+                            }
+                            program_state->close_thread_please = false;
+                            main_loop = false;
+                        }
+                        break;
+                    default:
+                        break;
+                    }
+                }
+            }
+        }
+
+        view_port_update(view_port);
+        furi_mutex_release(program_state->mutex);
+    }
+
+    furi_thread_free(program_state->mfkeythread);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close("gui");
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(program_state->mutex);
+    free(program_state);
+
+    return 0;
+}