Bläddra i källkod

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

git-subtree-dir: mfkey
git-subtree-mainline: fe34b19786600ecad232770c14a084ed77cdcce3
git-subtree-split: ddec2059406316aeab76a8aa160e15236c8d7fa2
Willy-JL 1 år sedan
förälder
incheckning
7f56b18dba

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

@@ -0,0 +1,12 @@
+# Flipper Zero MFKey
+
+This application allows you to calculate the keys of MIFARE Classic cards using the Mfkey32 and Nested algorithms 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 MFKey app.
+
+## Usage
+
+After collecting nonces using the Detect Reader option, press the Start button in the MFKey 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

+ 16 - 0
mfkey/.catalog/changelog.md

@@ -0,0 +1,16 @@
+## 2.7
+ - Mfkey32 recovery is 30% faster, fix UI and slowdown bugs
+## 2.6
+ - Version bump for catalog build system
+## 2.5
+ - Plugin path fixed
+## 2.4
+ - Update API for app rename
+## 2.3
+ - Update API v65.0
+## 2.0
+ - Added Nested key recovery, use new KeysDict API, fix crashes, more efficient RAM utilization, faster
+## 1.1
+ - Rework application with new NFC API
+## 1.0
+ - Initial release

BIN
mfkey/.catalog/screenshots/1.png


BIN
mfkey/.catalog/screenshots/2.png


BIN
mfkey/.catalog/screenshots/3.png


+ 3 - 0
mfkey/.gitsubtree

@@ -0,0 +1,3 @@
+https://github.com/flipperdevices/flipperzero-good-faps dev mfkey
+https://github.com/xMasterX/all-the-plugins dev base_pack/mfkey
+https://github.com/noproto/FlipperMfkey master fap_plugin

+ 28 - 0
mfkey/application.fam

@@ -0,0 +1,28 @@
+App(
+    appid="mfkey",
+    name="MFKey",
+    apptype=FlipperAppType.EXTERNAL,
+    targets=["f7"],
+    entry_point="mfkey_main",
+    requires=[
+        "gui",
+        "storage",
+    ],
+    stack_size=1 * 1024,
+    fap_icon="mfkey.png",
+    fap_category="NFC",
+    fap_author="@noproto",
+    fap_icon_assets="images",
+    fap_weburl="https://github.com/noproto/FlipperMfkey",
+    fap_description="MIFARE Classic key recovery tool",
+    fap_version="2.7",
+)
+
+App(
+    appid="mfkey_init_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="init_plugin_ep",
+    requires=["mfkey"],
+    sources=["init_plugin.c"],
+    fal_embedded=True,
+)

+ 22 - 0
mfkey/crypto1.c

@@ -0,0 +1,22 @@
+#pragma GCC optimize("O3")
+#pragma GCC optimize("-funroll-all-loops")
+
+#include <inttypes.h>
+#include "crypto1.h"
+#include "mfkey.h"
+
+#define BIT(x, n) ((x) >> (n) & 1)
+
+void crypto1_get_lfsr(struct Crypto1State* state, MfClassicKey* lfsr) {
+    int i;
+    uint64_t lfsr_value = 0;
+    for(i = 23; i >= 0; --i) {
+        lfsr_value = lfsr_value << 1 | BIT(state->odd, i ^ 3);
+        lfsr_value = lfsr_value << 1 | BIT(state->even, i ^ 3);
+    }
+
+    // Assign the key value to the MfClassicKey struct
+    for(i = 0; i < 6; ++i) {
+        lfsr->data[i] = (lfsr_value >> ((5 - i) * 8)) & 0xFF;
+    }
+}

+ 207 - 0
mfkey/crypto1.h

@@ -0,0 +1,207 @@
+#ifndef CRYPTO1_H
+#define CRYPTO1_H
+
+#include <inttypes.h>
+#include "mfkey.h"
+#include <nfc/protocols/mf_classic/mf_classic.h>
+
+#define LF_POLY_ODD  (0x29CE5C)
+#define LF_POLY_EVEN (0x870804)
+#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)
+
+static inline uint32_t prng_successor(uint32_t x, uint32_t n);
+static inline int filter(uint32_t const x);
+static inline uint8_t evenparity32(uint32_t x);
+static inline void update_contribution(unsigned int data[], int item, int mask1, int mask2);
+void crypto1_get_lfsr(struct Crypto1State* state, MfClassicKey* lfsr);
+static inline uint32_t crypt_word(struct Crypto1State* s);
+static inline void crypt_word_noret(struct Crypto1State* s, uint32_t in, int x);
+static inline uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x);
+static inline void rollback_word_noret(struct Crypto1State* s, uint32_t in, int x);
+static inline uint8_t napi_lfsr_rollback_bit(struct Crypto1State* s, uint32_t in, int fb);
+static inline uint32_t napi_lfsr_rollback_word(struct Crypto1State* s, uint32_t in, int fb);
+
+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};
+
+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);
+}
+
+#ifndef __ARM_ARCH_7EM__
+static inline uint8_t evenparity32(uint32_t x) {
+    return __builtin_parity(x);
+}
+#endif
+
+#ifdef __ARM_ARCH_7EM__
+static inline uint8_t evenparity32(uint32_t x) {
+    uint32_t result;
+    __asm__ volatile("eor r1, %[x], %[x], lsr #16  \n\t" // r1 = x ^ (x >> 16)
+                     "eor r1, r1, r1, lsr #8       \n\t" // r1 = r1 ^ (r1 >> 8)
+                     "eor r1, r1, r1, lsr #4       \n\t" // r1 = r1 ^ (r1 >> 4)
+                     "eor r1, r1, r1, lsr #2       \n\t" // r1 = r1 ^ (r1 >> 2)
+                     "eor r1, r1, r1, lsr #1       \n\t" // r1 = r1 ^ (r1 >> 1)
+                     "and %[result], r1, #1        \n\t" // result = r1 & 1
+                     : [result] "=r"(result)
+                     : [x] "r"(x)
+                     : "r1");
+    return result;
+}
+#endif
+
+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);
+}
+
+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 uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x) {
+    uint32_t ret = 0;
+    uint32_t feedin, t, next_in;
+    uint8_t next_ret;
+    for(int i = 0; i <= 31; i++) {
+        next_in = BEBIT(in, i);
+        next_ret = filter(s->odd);
+        feedin = next_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;
+        ret |= next_ret << (24 ^ i);
+    }
+    return ret;
+}
+
+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;
+}
+
+// TODO:
+/*
+uint32_t rollback_word(struct Crypto1State *s, uint32_t in, int x) {
+    uint32_t res_ret = 0;
+    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;
+        res_ret |= (ret << (24 ^ i));
+    }
+    return res_ret;
+}
+*/
+
+uint8_t napi_lfsr_rollback_bit(struct Crypto1State* s, uint32_t in, int fb) {
+    int out;
+    uint8_t ret;
+    uint32_t t;
+    s->odd &= 0xffffff;
+    t = s->odd, s->odd = s->even, s->even = t;
+
+    out = s->even & 1;
+    out ^= LF_POLY_EVEN & (s->even >>= 1);
+    out ^= LF_POLY_ODD & s->odd;
+    out ^= !!in;
+    out ^= (ret = filter(s->odd)) & !!fb;
+
+    s->even |= evenparity32(out) << 23;
+    return ret;
+}
+
+uint32_t napi_lfsr_rollback_word(struct Crypto1State* s, uint32_t in, int fb) {
+    int i;
+    uint32_t ret = 0;
+    for(i = 31; i >= 0; --i)
+        ret |= napi_lfsr_rollback_bit(s, BEBIT(in, i), fb) << (i ^ 24);
+    return ret;
+}
+
+static inline 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);
+}
+
+#endif // CRYPTO1_H

BIN
mfkey/images/mfkey.png


+ 412 - 0
mfkey/init_plugin.c

@@ -0,0 +1,412 @@
+#include <furi_hal.h>
+#include <inttypes.h>
+#include <toolbox/keys_dict.h>
+#include <bit_lib/bit_lib.h>
+#include <toolbox/stream/buffered_file_stream.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include "mfkey.h"
+#include "crypto1.h"
+#include "plugin_interface.h"
+#include <flipper_application/flipper_application.h>
+
+// TODO: Remove defines that are not needed
+#define KEYS_DICT_SYSTEM_PATH        EXT_PATH("nfc/assets/mf_classic_dict.nfc")
+#define KEYS_DICT_USER_PATH          EXT_PATH("nfc/assets/mf_classic_dict_user.nfc")
+#define MF_CLASSIC_NONCE_PATH        EXT_PATH("nfc/.mfkey32.log")
+#define MF_CLASSIC_NESTED_NONCE_PATH EXT_PATH("nfc/.nested")
+#define TAG                          "MFKey"
+#define MAX_NAME_LEN                 32
+#define MAX_PATH_LEN                 64
+
+#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)
+
+bool key_already_found_for_nonce_in_dict(KeysDict* dict, MfClassicNonce* nonce) {
+    bool found = false;
+    uint8_t key_bytes[sizeof(MfClassicKey)];
+    keys_dict_rewind(dict);
+    while(keys_dict_get_next_key(dict, key_bytes, sizeof(MfClassicKey))) {
+        uint64_t k = bit_lib_bytes_to_num_be(key_bytes, sizeof(MfClassicKey));
+        struct Crypto1State temp = {0, 0};
+        for(int i = 0; i < 24; i++) {
+            (&temp)->odd |= (BIT(k, 2 * i + 1) << (i ^ 3));
+            (&temp)->even |= (BIT(k, 2 * i) << (i ^ 3));
+        }
+        if(nonce->attack == mfkey32) {
+            crypt_word_noret(&temp, nonce->uid_xor_nt1, 0);
+            crypt_word_noret(&temp, nonce->nr1_enc, 1);
+            if(nonce->ar1_enc == (crypt_word(&temp) ^ nonce->p64b)) {
+                found = true;
+                break;
+            }
+        } else if(nonce->attack == static_nested) {
+            uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0);
+            if(nonce->ks1_1_enc == expected_ks1) {
+                found = true;
+                break;
+            }
+        }
+    }
+    return found;
+}
+
+bool napi_mf_classic_mfkey32_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;
+}
+
+bool distance_in_nonces_file(const char* file_path, const char* file_name) {
+    char full_path[MAX_PATH_LEN];
+    snprintf(full_path, sizeof(full_path), "%s/%s", file_path, file_name);
+    bool distance_present = false;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    Stream* file_stream = buffered_file_stream_alloc(storage);
+    FuriString* line_str;
+    line_str = furi_string_alloc();
+
+    if(buffered_file_stream_open(file_stream, full_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        while(true) {
+            if(!stream_read_line(file_stream, line_str)) break;
+            if(furi_string_search_str(line_str, "distance") != FURI_STRING_FAILURE) {
+                distance_present = true;
+                break;
+            }
+        }
+    }
+
+    buffered_file_stream_close(file_stream);
+    stream_free(file_stream);
+    furi_string_free(line_str);
+    furi_record_close(RECORD_STORAGE);
+
+    return distance_present;
+}
+
+bool napi_mf_classic_nested_nonces_check_presence() {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    if(!(storage_dir_exists(storage, MF_CLASSIC_NESTED_NONCE_PATH))) {
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    bool nonces_present = false;
+    File* dir = storage_file_alloc(storage);
+    char filename_buffer[MAX_NAME_LEN];
+    FileInfo file_info;
+
+    if(storage_dir_open(dir, MF_CLASSIC_NESTED_NONCE_PATH)) {
+        while(storage_dir_read(dir, &file_info, filename_buffer, MAX_NAME_LEN)) {
+            // We only care about Static Nested files
+            if(!(file_info.flags & FSF_DIRECTORY) && strstr(filename_buffer, ".nonces") &&
+               !(distance_in_nonces_file(MF_CLASSIC_NESTED_NONCE_PATH, filename_buffer))) {
+                nonces_present = true;
+                break;
+            }
+        }
+    }
+
+    storage_dir_close(dir);
+    storage_file_free(dir);
+    furi_record_close(RECORD_STORAGE);
+
+    return nonces_present;
+}
+
+int binaryStringToInt(const char* binStr) {
+    int result = 0;
+    while(*binStr) {
+        result <<= 1;
+        if(*binStr == '1') {
+            result |= 1;
+        }
+        binStr++;
+    }
+    return result;
+}
+
+bool load_mfkey32_nonces(
+    MfClassicNonceArray* nonce_array,
+    ProgramState* program_state,
+    KeysDict* system_dict,
+    bool system_dict_exists,
+    KeysDict* user_dict) {
+    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};
+            res.attack = mfkey32;
+            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;
+            }
+            res.p64 = prng_successor(res.nt0, 64);
+            res.p64b = prng_successor(res.nt1, 64);
+            res.uid_xor_nt0 = res.uid ^ res.nt0;
+            res.uid_xor_nt1 = res.uid ^ res.nt1;
+
+            (program_state->total)++;
+            if((system_dict_exists && key_already_found_for_nonce_in_dict(system_dict, &res)) ||
+               (key_already_found_for_nonce_in_dict(user_dict, &res))) {
+                (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);
+        //stream_free(nonce_array->stream);
+
+        array_loaded = true;
+        //FURI_LOG_I(TAG, "Loaded %lu Mfkey32 nonces", nonce_array->total_nonces);
+    } while(false);
+
+    return array_loaded;
+}
+
+bool load_nested_nonces(
+    MfClassicNonceArray* nonce_array,
+    ProgramState* program_state,
+    KeysDict* system_dict,
+    bool system_dict_exists,
+    KeysDict* user_dict) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    File* dir = storage_file_alloc(storage);
+    char filename_buffer[MAX_NAME_LEN];
+    FileInfo file_info;
+    FuriString* next_line = furi_string_alloc();
+
+    if(!storage_dir_open(dir, MF_CLASSIC_NESTED_NONCE_PATH)) {
+        storage_dir_close(dir);
+        storage_file_free(dir);
+        furi_record_close(RECORD_STORAGE);
+        furi_string_free(next_line);
+        return false;
+    }
+
+    while(storage_dir_read(dir, &file_info, filename_buffer, MAX_NAME_LEN)) {
+        if(!(file_info.flags & FSF_DIRECTORY) && strstr(filename_buffer, ".nonces") &&
+           !(distance_in_nonces_file(MF_CLASSIC_NESTED_NONCE_PATH, filename_buffer))) {
+            char full_path[MAX_PATH_LEN];
+            snprintf(
+                full_path,
+                sizeof(full_path),
+                "%s/%s",
+                MF_CLASSIC_NESTED_NONCE_PATH,
+                filename_buffer);
+
+            // TODO: We should only need READ_WRITE here if we plan on adding a newline to the end of the file if has none
+            if(!buffered_file_stream_open(
+                   nonce_array->stream, full_path, FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) {
+                buffered_file_stream_close(nonce_array->stream);
+                continue;
+            }
+
+            while(stream_read_line(nonce_array->stream, next_line)) {
+                if(furi_string_search_str(next_line, "Nested:") != FURI_STRING_FAILURE) {
+                    MfClassicNonce res = {0};
+                    res.attack = static_nested;
+                    int parsed = sscanf(
+                        furi_string_get_cstr(next_line),
+                        "Nested: %*s %*s cuid 0x%" PRIx32 " nt0 0x%" PRIx32 " ks0 0x%" PRIx32
+                        " par0 %4[01] nt1 0x%" PRIx32 " ks1 0x%" PRIx32 " par1 %4[01]",
+                        &res.uid,
+                        &res.nt0,
+                        &res.ks1_1_enc,
+                        res.par_1_str,
+                        &res.nt1,
+                        &res.ks1_2_enc,
+                        res.par_2_str);
+
+                    if(parsed != 7) continue;
+                    res.par_1 = binaryStringToInt(res.par_1_str);
+                    res.par_2 = binaryStringToInt(res.par_2_str);
+                    res.uid_xor_nt0 = res.uid ^ res.nt0;
+                    res.uid_xor_nt1 = res.uid ^ res.nt1;
+
+                    (program_state->total)++;
+                    if((system_dict_exists &&
+                        key_already_found_for_nonce_in_dict(system_dict, &res)) ||
+                       (key_already_found_for_nonce_in_dict(user_dict, &res))) {
+                        (program_state->cracked)++;
+                        (program_state->num_completed)++;
+                        continue;
+                    }
+
+                    nonce_array->remaining_nonce_array = realloc(
+                        nonce_array->remaining_nonce_array,
+                        sizeof(MfClassicNonce) * (nonce_array->remaining_nonces + 1));
+                    nonce_array->remaining_nonce_array[nonce_array->remaining_nonces] = res;
+                    nonce_array->remaining_nonces++;
+                    nonce_array->total_nonces++;
+                }
+            }
+
+            buffered_file_stream_close(nonce_array->stream);
+        }
+    }
+
+    storage_dir_close(dir);
+    storage_file_free(dir);
+    furi_record_close(RECORD_STORAGE);
+    furi_string_free(next_line);
+
+    //FURI_LOG_I(TAG, "Loaded %lu Static Nested nonces", nonce_array->total_nonces);
+    return true;
+}
+
+MfClassicNonceArray* napi_mf_classic_nonce_array_alloc(
+    KeysDict* system_dict,
+    bool system_dict_exists,
+    KeysDict* 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;
+
+    if(program_state->mfkey32_present) {
+        array_loaded = load_mfkey32_nonces(
+            nonce_array, program_state, system_dict, system_dict_exists, user_dict);
+    }
+
+    if(program_state->nested_present) {
+        array_loaded |= load_nested_nonces(
+            nonce_array, program_state, system_dict, system_dict_exists, user_dict);
+    }
+
+    if(!array_loaded) {
+        free(nonce_array);
+        nonce_array = NULL;
+    }
+
+    return nonce_array;
+}
+
+void napi_mf_classic_nonce_array_free(MfClassicNonceArray* nonce_array) {
+    // TODO: Track free state at the time this is called to ensure double free does not happen
+    furi_assert(nonce_array);
+    furi_assert(nonce_array->stream);
+
+    buffered_file_stream_close(nonce_array->stream);
+    stream_free(nonce_array->stream);
+    free(nonce_array);
+}
+
+/* Actual implementation of app<>plugin interface */
+static const MfkeyPlugin init_plugin = {
+    .name = "Initialization Plugin",
+    .napi_mf_classic_mfkey32_nonces_check_presence =
+        &napi_mf_classic_mfkey32_nonces_check_presence,
+    .napi_mf_classic_nested_nonces_check_presence = &napi_mf_classic_nested_nonces_check_presence,
+    .napi_mf_classic_nonce_array_alloc = &napi_mf_classic_nonce_array_alloc,
+    .napi_mf_classic_nonce_array_free = &napi_mf_classic_nonce_array_free,
+};
+
+/* Plugin descriptor to comply with basic plugin specification */
+static const FlipperAppPluginDescriptor init_plugin_descriptor = {
+    .appid = PLUGIN_APP_ID,
+    .ep_api_version = PLUGIN_API_VERSION,
+    .entry_point = &init_plugin,
+};
+
+/* Plugin entry point - must return a pointer to const descriptor  */
+const FlipperAppPluginDescriptor* init_plugin_ep() {
+    return &init_plugin_descriptor;
+}

+ 869 - 0
mfkey/mfkey.c

@@ -0,0 +1,869 @@
+#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 key_already_found_for_nonce_in_dict)
+// TODO: Selectively unroll loops to reduce binary size
+// TODO: Collect parity during Mfkey32 attacks to further optimize the attack
+// TODO: Why different sscanf between Mfkey32 and Nested?
+// TODO: "Read tag again with NFC app" message upon completion, "Complete. Keys added: <n>"
+// TODO: Separate Mfkey32 and Nested functions where possible to reduce branch statements
+// TODO: More accurate timing for Nested
+// TODO: Find ~1 KB memory leak
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include "mfkey_icons.h"
+#include <inttypes.h>
+#include <toolbox/keys_dict.h>
+#include <bit_lib/bit_lib.h>
+#include <toolbox/stream/buffered_file_stream.h>
+#include <dolphin/dolphin.h>
+#include <notification/notification_messages.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include "mfkey.h"
+#include "crypto1.h"
+#include "plugin_interface.h"
+#include <flipper_application/flipper_application.h>
+#include <loader/firmware_api/firmware_api.h>
+#include <storage/storage.h>
+
+// TODO: Remove defines that are not needed
+#define KEYS_DICT_SYSTEM_PATH        EXT_PATH("nfc/assets/mf_classic_dict.nfc")
+#define KEYS_DICT_USER_PATH          EXT_PATH("nfc/assets/mf_classic_dict_user.nfc")
+#define MF_CLASSIC_NONCE_PATH        EXT_PATH("nfc/.mfkey32.log")
+#define MF_CLASSIC_NESTED_NONCE_PATH EXT_PATH("nfc/.nested")
+#define TAG                          "MFKey"
+#define MAX_NAME_LEN                 32
+#define MAX_PATH_LEN                 64
+
+#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 = 44;
+static int eta_total_time = 705;
+// MSB_LIMIT: Chunk size (out of 256)
+static int MSB_LIMIT = 16;
+
+int check_state(struct Crypto1State* t, MfClassicNonce* n) {
+    if(!(t->odd | t->even)) return 0;
+    if(n->attack == mfkey32) {
+        uint32_t rb = (napi_lfsr_rollback_word(t, 0, 0) ^ n->p64);
+        if(rb != n->ar0_enc) {
+            return 0;
+        }
+        rollback_word_noret(t, n->nr0_enc, 1);
+        rollback_word_noret(t, n->uid_xor_nt0, 0);
+        struct Crypto1State temp = {t->odd, t->even};
+        crypt_word_noret(t, n->uid_xor_nt1, 0);
+        crypt_word_noret(t, n->nr1_enc, 1);
+        if(n->ar1_enc == (crypt_word(t) ^ n->p64b)) {
+            crypto1_get_lfsr(&temp, &(n->key));
+            return 1;
+        }
+        return 0;
+    } else if(n->attack == static_nested) {
+        struct Crypto1State temp = {t->odd, t->even};
+        rollback_word_noret(t, n->uid_xor_nt1, 0);
+        if(n->ks1_1_enc == crypt_word_ret(t, n->uid_xor_nt0, 0)) {
+            rollback_word_noret(&temp, n->uid_xor_nt1, 0);
+            crypto1_get_lfsr(&temp, &(n->key));
+            return 1;
+        }
+        return 0;
+    }
+    return 0;
+}
+
+static inline int state_loop(
+    unsigned int* states_buffer,
+    int xks,
+    int m1,
+    int m2,
+    unsigned int in,
+    uint8_t and_val) {
+    int states_tail = 0;
+    int round = 0, s = 0, xks_bit = 0, round_in = 0;
+
+    for(round = 1; round <= 12; round++) {
+        xks_bit = BIT(xks, round);
+        if(round > 4) {
+            round_in = ((in >> (2 * (round - 4))) & and_val) << 24;
+        }
+
+        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);
+                    states_buffer[s] ^= round_in;
+                }
+            } 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);
+                    states_buffer[s++] ^= round_in;
+                    update_contribution(states_buffer, s, m1, m2);
+                    states_buffer[s] ^= round_in;
+                } 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, unsigned int in) {
+    in <<= 24;
+    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);
+            data[tbl] ^= in;
+        } else if(filter(data[tbl]) == bit) {
+            data[++end] = data[tbl + 1];
+            data[tbl + 1] = data[tbl] | 1;
+            update_contribution(data, tbl, m1, m2);
+            data[tbl++] ^= in;
+            update_contribution(data, tbl, m1, m2);
+            data[tbl] ^= in;
+        } 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,
+    MfClassicNonce* n,
+    unsigned int in,
+    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) ^ (!!(in & 4));
+            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, n)) {
+                    return -1;
+                }
+            }
+        }
+        return s;
+    }
+    if(first_run == 0) {
+        for(i = 0; (i < 4) && (rem-- != 0); i++) {
+            oks >>= 1;
+            eks >>= 1;
+            in >>= 2;
+            o_tail = extend_table(
+                odd, o_head, o_tail, oks & 1, LF_POLY_EVEN << 1 | 1, LF_POLY_ODD << 1, 0);
+            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, in & 3);
+            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, n, in, 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();
+    int elapsed_time = ts - program_state->eta_timestamp;
+    if(elapsed_time < program_state->eta_round) {
+        program_state->eta_round -= elapsed_time;
+    } else {
+        program_state->eta_round = 0;
+    }
+    if(elapsed_time < program_state->eta_total) {
+        program_state->eta_total -= elapsed_time;
+    } else {
+        program_state->eta_total = 0;
+    }
+    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,
+    MfClassicNonce* n,
+    unsigned int* states_buffer,
+    struct Msb* odd_msbs,
+    struct Msb* even_msbs,
+    unsigned int* temp_states_odd,
+    unsigned int* temp_states_even,
+    unsigned int in,
+    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;
+    in = ((in >> 16 & 0xff) | (in << 16) | (in & 0xff00)) << 1;
+    // 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, 0, 0);
+
+            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, in, 3);
+
+            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,
+            n,
+            in >> 16,
+            1);
+        if(res == -1) {
+            return 1;
+        }
+        //odd_msbs[i].tail = 0;
+        //even_msbs[i].tail = 0;
+    }
+
+    return 0;
+}
+
+void** allocate_blocks(const size_t* block_sizes, int num_blocks) {
+    void** block_pointers = malloc(num_blocks * sizeof(void*));
+
+    for(int i = 0; i < num_blocks; i++) {
+        if(memmgr_heap_get_max_free_block() < block_sizes[i]) {
+            // Not enough memory, free previously allocated blocks
+            for(int j = 0; j < i; j++) {
+                free(block_pointers[j]);
+            }
+            free(block_pointers);
+            return NULL;
+        }
+
+        block_pointers[i] = malloc(block_sizes[i]);
+    }
+
+    return block_pointers;
+}
+
+bool is_full_speed() {
+    return MSB_LIMIT == 16;
+}
+
+bool recover(MfClassicNonce* n, int ks2, unsigned int in, ProgramState* program_state) {
+    bool found = false;
+    const size_t block_sizes[] = {49216, 49216, 5120, 5120, 4096};
+    const size_t reduced_block_sizes[] = {24608, 24608, 5120, 5120, 4096};
+    const int num_blocks = sizeof(block_sizes) / sizeof(block_sizes[0]);
+    void** block_pointers = allocate_blocks(block_sizes, num_blocks);
+    if(block_pointers == NULL) {
+        // System has less than the guaranteed amount of RAM (140 KB) - adjust some parameters to run anyway at half speed
+        if(is_full_speed()) {
+            //eta_round_time *= 2;
+            eta_total_time *= 2;
+            MSB_LIMIT /= 2;
+        }
+        block_pointers = allocate_blocks(reduced_block_sizes, num_blocks);
+        if(block_pointers == NULL) {
+            // System has less than 70 KB of RAM - should never happen so we don't reduce speed further
+            program_state->err = InsufficientRAM;
+            program_state->mfkey_state = Error;
+            return false;
+        }
+    }
+    struct Msb* odd_msbs = block_pointers[0];
+    struct Msb* even_msbs = block_pointers[1];
+    unsigned int* temp_states_odd = block_pointers[2];
+    unsigned int* temp_states_even = block_pointers[3];
+    unsigned int* states_buffer = block_pointers[4];
+    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,
+               n,
+               states_buffer,
+               odd_msbs,
+               even_msbs,
+               temp_states_odd,
+               temp_states_even,
+               in,
+               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 the allocated blocks
+    for(int i = 0; i < num_blocks; i++) {
+        free(block_pointers[i]);
+    }
+    free(block_pointers);
+    return found;
+}
+
+bool key_already_found_for_nonce_in_solved(
+    MfClassicKey* keyarray,
+    int keyarray_size,
+    MfClassicNonce* nonce) {
+    for(int k = 0; k < keyarray_size; k++) {
+        uint64_t key_as_int = bit_lib_bytes_to_num_be(keyarray[k].data, sizeof(MfClassicKey));
+        struct Crypto1State temp = {0, 0};
+        for(int i = 0; i < 24; i++) {
+            (&temp)->odd |= (BIT(key_as_int, 2 * i + 1) << (i ^ 3));
+            (&temp)->even |= (BIT(key_as_int, 2 * i) << (i ^ 3));
+        }
+        if(nonce->attack == mfkey32) {
+            crypt_word_noret(&temp, nonce->uid_xor_nt1, 0);
+            crypt_word_noret(&temp, nonce->nr1_enc, 1);
+            if(nonce->ar1_enc == (crypt_word(&temp) ^ nonce->p64b)) {
+                return true;
+            }
+        } else if(nonce->attack == static_nested) {
+            uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0);
+            if(nonce->ks1_1_enc == expected_ks1) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+#pragma GCC push_options
+#pragma GCC optimize("Os")
+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 mfkey(ProgramState* program_state) {
+    MfClassicKey found_key; // recovered key
+    size_t keyarray_size = 0;
+    MfClassicKey* keyarray = malloc(sizeof(MfClassicKey) * 1);
+    uint32_t i = 0, j = 0;
+    //FURI_LOG_I(TAG, "Free heap before alloc(): %zub", memmgr_get_free_heap());
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperApplication* app = flipper_application_alloc(storage, firmware_api_interface);
+    flipper_application_preload(app, APP_ASSETS_PATH("plugins/mfkey_init_plugin.fal"));
+    flipper_application_map_to_memory(app);
+    const FlipperAppPluginDescriptor* app_descriptor =
+        flipper_application_plugin_get_descriptor(app);
+    const MfkeyPlugin* init_plugin = app_descriptor->entry_point;
+    // Check for nonces
+    program_state->mfkey32_present = init_plugin->napi_mf_classic_mfkey32_nonces_check_presence();
+    program_state->nested_present = init_plugin->napi_mf_classic_nested_nonces_check_presence();
+    if(!(program_state->mfkey32_present) && !(program_state->nested_present)) {
+        program_state->err = MissingNonces;
+        program_state->mfkey_state = Error;
+        flipper_application_free(app);
+        furi_record_close(RECORD_STORAGE);
+        free(keyarray);
+        return;
+    }
+    // Read dictionaries (optional)
+    KeysDict* system_dict = {0};
+    bool system_dict_exists = keys_dict_check_presence(KEYS_DICT_SYSTEM_PATH);
+    KeysDict* user_dict = {0};
+    bool user_dict_exists = keys_dict_check_presence(KEYS_DICT_USER_PATH);
+    uint32_t total_dict_keys = 0;
+    if(system_dict_exists) {
+        system_dict =
+            keys_dict_alloc(KEYS_DICT_SYSTEM_PATH, KeysDictModeOpenExisting, sizeof(MfClassicKey));
+        total_dict_keys += keys_dict_get_total_keys(system_dict);
+    }
+    user_dict = keys_dict_alloc(KEYS_DICT_USER_PATH, KeysDictModeOpenAlways, sizeof(MfClassicKey));
+    if(user_dict_exists) {
+        total_dict_keys += keys_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 = init_plugin->napi_mf_classic_nonce_array_alloc(
+        system_dict, system_dict_exists, user_dict, program_state);
+    if(system_dict_exists) {
+        keys_dict_free(system_dict);
+    }
+    if(nonce_arr->total_nonces == 0) {
+        // Nothing to crack
+        program_state->err = ZeroNonces;
+        program_state->mfkey_state = Error;
+        init_plugin->napi_mf_classic_nonce_array_free(nonce_arr);
+        flipper_application_free(app);
+        furi_record_close(RECORD_STORAGE);
+        keys_dict_free(user_dict);
+        free(keyarray);
+        return;
+    }
+    flipper_application_free(app);
+    furi_record_close(RECORD_STORAGE);
+    // TODO: Track free state at the time this is called to ensure double free does not happen
+    furi_assert(nonce_arr);
+    furi_assert(nonce_arr->stream);
+    buffered_file_stream_close(nonce_arr->stream);
+    stream_free(nonce_arr->stream);
+    //FURI_LOG_I(TAG, "Free heap after free(): %zub", memmgr_get_free_heap());
+    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];
+        if(key_already_found_for_nonce_in_solved(keyarray, keyarray_size, &next_nonce)) {
+            nonce_arr->remaining_nonces--;
+            (program_state->cracked)++;
+            (program_state->num_completed)++;
+            continue;
+        }
+        //FURI_LOG_I(TAG, "Beginning recovery for %8lx", next_nonce.uid);
+        if(next_nonce.attack == mfkey32) {
+            if(!recover(&next_nonce, next_nonce.ar0_enc ^ next_nonce.p64, 0, program_state)) {
+                if(program_state->close_thread_please) {
+                    break;
+                }
+                // No key found in recover()
+                (program_state->num_completed)++;
+                continue;
+            }
+        } else if(next_nonce.attack == static_nested) {
+            if(!recover(
+                   &next_nonce,
+                   next_nonce.ks1_2_enc,
+                   next_nonce.nt1 ^ next_nonce.uid,
+                   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 = next_nonce.key;
+        bool already_found = false;
+        for(j = 0; j < keyarray_size; j++) {
+            if(memcmp(keyarray[j].data, found_key.data, MF_CLASSIC_KEY_SIZE) == 0) {
+                already_found = true;
+                break;
+            }
+        }
+        if(already_found == false) {
+            // New key
+            keyarray = realloc(keyarray, sizeof(MfClassicKey) * (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]);
+        keys_dict_add_key(user_dict, keyarray[i].data, sizeof(MfClassicKey));
+    }
+    if(keyarray_size > 0) {
+        dolphin_deed(DolphinDeedNfcMfcAdd);
+    }
+    free(nonce_arr);
+    keys_dict_free(user_dict);
+    free(keyarray);
+    if(program_state->mfkey_state == Error) {
+        return;
+    }
+    //FURI_LOG_I(TAG, "mfkey 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_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, "MFKey");
+    snprintf(draw_str, sizeof(draw_str), "RAM: %zub", memmgr_get_free_heap());
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 48, 5, AlignLeft, AlignTop, draw_str);
+    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(canvas, 5, 18, 118, 1);
+        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 Detect");
+        canvas_draw_str_aligned(canvas, 7, 30, AlignLeft, AlignTop, "Reader or FlipperNested.");
+        canvas_draw_str_aligned(canvas, 7, 40, AlignLeft, AlignTop, "Devs: noproto, AG, ALiberty");
+        canvas_draw_str_aligned(canvas, 7, 50, AlignLeft, AlignTop, "Thanks: bettse, Foxushka");
+    } 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 if(program_state->err == InsufficientRAM) {
+            canvas_draw_str_aligned(canvas, 35, 36, AlignLeft, AlignTop, "No free RAM");
+        } else {
+            // Unhandled error
+        }
+    } else {
+        // Unhandled program state
+    }
+    furi_mutex_release(program_state->mutex);
+}
+
+static void input_callback(InputEvent* input_event, void* event_queue) {
+    furi_assert(event_queue);
+    furi_message_queue_put((FuriMessageQueue*)event_queue, input_event, FuriWaitForever);
+}
+
+static void mfkey_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 mfkey_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 mfkey worker thread"); // DEBUG
+    mfkey(program_state);
+    program_state->is_thread_running = false;
+    return 0;
+}
+
+int32_t mfkey_main() {
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    ProgramState* program_state = malloc(sizeof(ProgramState));
+
+    mfkey_state_init(program_state);
+
+    program_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+
+    // 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, "MFKeyWorker");
+    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, mfkey_worker_thread);
+
+    InputEvent input_event;
+    for(bool main_loop = true; main_loop;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &input_event, 100);
+
+        furi_mutex_acquire(program_state->mutex, FuriWaitForever);
+
+        if(event_status == FuriStatusOk) {
+            if(input_event.type == InputTypePress) {
+                switch(input_event.key) {
+                case InputKeyRight:
+                    if(!program_state->is_thread_running && program_state->mfkey_state == Ready) {
+                        program_state->mfkey_state = Help;
+                    }
+                    break;
+                case InputKeyOk:
+                    if(!program_state->is_thread_running && program_state->mfkey_state == Ready) {
+                        furi_thread_start(program_state->mfkeythread);
+                    }
+                    break;
+                case InputKeyBack:
+                    if(!program_state->is_thread_running && program_state->mfkey_state == Help) {
+                        program_state->mfkey_state = Ready;
+                    } else {
+                        program_state->close_thread_please = true;
+                        if(program_state->is_thread_running) {
+                            // Wait until thread is finished
+                            furi_thread_join(program_state->mfkeythread);
+                        }
+                        program_state->close_thread_please = false;
+                        main_loop = false;
+                    }
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+
+        furi_mutex_release(program_state->mutex);
+        view_port_update(view_port);
+    }
+
+    furi_thread_free(program_state->mfkeythread);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(program_state->mutex);
+    free(program_state);
+
+    return 0;
+}
+#pragma GCC pop_options

+ 100 - 0
mfkey/mfkey.h

@@ -0,0 +1,100 @@
+#ifndef MFKEY_H
+#define MFKEY_H
+
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <inttypes.h>
+#include <toolbox/keys_dict.h>
+#include <toolbox/stream/buffered_file_stream.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+
+struct Crypto1State {
+    uint32_t odd, even;
+};
+struct Msb {
+    int tail;
+    uint32_t states[768];
+};
+
+typedef enum {
+    MissingNonces,
+    ZeroNonces,
+    InsufficientRAM,
+} 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 mfkey32_present;
+    bool nested_present;
+    bool is_thread_running;
+    bool close_thread_please;
+    FuriThread* mfkeythread;
+} ProgramState;
+
+typedef enum {
+    mfkey32,
+    static_nested
+} AttackType;
+
+typedef struct {
+    AttackType attack;
+    MfClassicKey key; // key
+    uint32_t uid; // serial number
+    uint32_t nt0; // tag challenge first
+    uint32_t nt1; // tag challenge second
+    uint32_t uid_xor_nt0; // uid ^ nt0
+    uint32_t uid_xor_nt1; // uid ^ nt1
+    // Mfkey32
+    uint32_t p64; // 64th successor of nt0
+    uint32_t p64b; // 64th successor of nt1
+    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
+    // Nested
+    uint32_t ks1_1_enc; // first encrypted keystream
+    uint32_t ks1_2_enc; // second encrypted keystream
+    char par_1_str[5]; // first parity bits (string representation)
+    char par_2_str[5]; // second parity bits (string representation)
+    uint8_t par_1; // first parity bits
+    uint8_t par_2; // second parity bits
+} MfClassicNonce;
+
+typedef struct {
+    Stream* stream;
+    uint32_t total_nonces;
+    MfClassicNonce* remaining_nonce_array;
+    size_t remaining_nonces;
+} MfClassicNonceArray;
+
+struct KeysDict {
+    Stream* stream;
+    size_t key_size;
+    size_t key_size_symbols;
+    size_t total_keys;
+};
+
+#endif // MFKEY_H

BIN
mfkey/mfkey.png


+ 13 - 0
mfkey/plugin_interface.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#define PLUGIN_APP_ID      "mfkey"
+#define PLUGIN_API_VERSION 1
+
+typedef struct {
+    const char* name;
+    bool (*napi_mf_classic_mfkey32_nonces_check_presence)();
+    bool (*napi_mf_classic_nested_nonces_check_presence)();
+    MfClassicNonceArray* (
+        *napi_mf_classic_nonce_array_alloc)(KeysDict*, bool, KeysDict*, ProgramState*);
+    void (*napi_mf_classic_nonce_array_free)(MfClassicNonceArray*);
+} MfkeyPlugin;