Переглянути джерело

Merge mfkey from https://github.com/noproto/flipperzero-good-faps

Willy-JL 1 рік тому
батько
коміт
000fc3aced
7 змінених файлів з 235 додано та 179 видалено
  1. 4 4
      mfkey/.catalog/README.md
  2. 2 0
      mfkey/.catalog/changelog.md
  3. 1 1
      mfkey/application.fam
  4. 49 0
      mfkey/crypto1.h
  5. 72 129
      mfkey/init_plugin.c
  6. 103 44
      mfkey/mfkey.c
  7. 4 1
      mfkey/mfkey.h

+ 4 - 4
mfkey/.catalog/README.md

@@ -1,12 +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.
+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 Extract MF Keys 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.
+After collecting nonces using the Extract MF Keys 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
+Developers: noproto, AG, Flipper Devices
+Thanks: AloneLiberty, Foxushka, bettse, Equip

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

@@ -1,3 +1,5 @@
+## 3.0
+ - Added Static Encrypted Nested key recovery, added NFC app support, dropped FlipperNested support
 ## 2.7
  - Mfkey32 recovery is 30% faster, fix UI and slowdown bugs
 ## 2.6

+ 1 - 1
mfkey/application.fam

@@ -15,7 +15,7 @@ App(
     fap_icon_assets="images",
     fap_weburl="https://github.com/noproto/FlipperMfkey",
     fap_description="MIFARE Classic key recovery tool",
-    fap_version="2.7",
+    fap_version="3.0",
 )
 
 App(

+ 49 - 0
mfkey/crypto1.h

@@ -3,6 +3,7 @@
 
 #include <inttypes.h>
 #include "mfkey.h"
+#include <nfc/helpers/nfc_util.h>
 #include <nfc/protocols/mf_classic/mf_classic.h>
 
 #define LF_POLY_ODD  (0x29CE5C)
@@ -20,6 +21,12 @@ 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 uint32_t crypt_word_par(
+    struct Crypto1State* s,
+    uint32_t in,
+    int is_encrypted,
+    uint32_t nt_plain,
+    uint8_t* parity_keystream_bits);
 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);
@@ -131,6 +138,48 @@ static inline uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x
     return ret;
 }
 
+static uint8_t get_nth_byte(uint32_t value, int n) {
+    if(n < 0 || n > 3) {
+        // Handle invalid input
+        return 0;
+    }
+    return (value >> (8 * (3 - n))) & 0xFF;
+}
+
+static uint8_t crypt_bit(struct Crypto1State* s, uint8_t in, int is_encrypted) {
+    uint32_t feedin, t;
+    uint8_t ret = filter(s->odd);
+    feedin = ret & !!is_encrypted;
+    feedin ^= !!in;
+    feedin ^= LF_POLY_ODD & s->odd;
+    feedin ^= LF_POLY_EVEN & s->even;
+    s->even = s->even << 1 | evenparity32(feedin);
+    t = s->odd, s->odd = s->even, s->even = t;
+    return ret;
+}
+
+static inline uint32_t crypt_word_par(
+    struct Crypto1State* s,
+    uint32_t in,
+    int is_encrypted,
+    uint32_t nt_plain,
+    uint8_t* parity_keystream_bits) {
+    uint32_t ret = 0;
+    *parity_keystream_bits = 0; // Reset parity keystream bits
+
+    for(int i = 0; i < 32; i++) {
+        uint8_t bit = crypt_bit(s, BEBIT(in, i), is_encrypted);
+        ret |= bit << (24 ^ i);
+        // Save keystream parity bit
+        if((i + 1) % 8 == 0) {
+            *parity_keystream_bits |=
+                (filter(s->odd) ^ nfc_util_even_parity8(get_nth_byte(nt_plain, i / 8)))
+                << (3 - (i / 8));
+        }
+    }
+    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;

+ 72 - 129
mfkey/init_plugin.c

@@ -10,10 +10,8 @@
 #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 MF_CLASSIC_NESTED_NONCE_PATH EXT_PATH("nfc/.nested.log")
 #define TAG                          "MFKey"
 #define MAX_NAME_LEN                 32
 #define MAX_PATH_LEN                 64
@@ -30,6 +28,7 @@
     ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16)
 
 bool key_already_found_for_nonce_in_dict(KeysDict* dict, MfClassicNonce* nonce) {
+    // This function must not be passed the CUID dictionary
     bool found = false;
     uint8_t key_bytes[sizeof(MfClassicKey)];
     keys_dict_rewind(dict);
@@ -47,7 +46,7 @@ bool key_already_found_for_nonce_in_dict(KeysDict* dict, MfClassicNonce* nonce)
                 found = true;
                 break;
             }
-        } else if(nonce->attack == static_nested) {
+        } else if(nonce->attack == static_nested || nonce->attack == static_encrypted) {
             uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0);
             if(nonce->ks1_1_enc == expected_ks1) {
                 found = true;
@@ -68,59 +67,30 @@ bool napi_mf_classic_mfkey32_nonces_check_presence() {
     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);
+    Stream* stream = buffered_file_stream_alloc(storage);
+    bool nonces_present = false;
+    FuriString* line = furi_string_alloc();
 
-    if(!(storage_dir_exists(storage, MF_CLASSIC_NESTED_NONCE_PATH))) {
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
+    do {
+        if(!buffered_file_stream_open(
+               stream, MF_CLASSIC_NESTED_NONCE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
+            break;
+        }
 
-    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))) {
+        while(stream_read_line(stream, line)) {
+            if(furi_string_search_str(line, "dist 0") != FURI_STRING_FAILURE) {
                 nonces_present = true;
                 break;
             }
         }
-    }
 
-    storage_dir_close(dir);
-    storage_file_free(dir);
+    } while(false);
+
+    furi_string_free(line);
+    buffered_file_stream_close(stream);
+    stream_free(stream);
     furi_record_close(RECORD_STORAGE);
 
     return nonces_present;
@@ -247,7 +217,6 @@ bool load_mfkey32_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);
@@ -262,89 +231,70 @@ bool load_nested_nonces(
     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);
+    if(!buffered_file_stream_open(
+           nonce_array->stream, MF_CLASSIC_NESTED_NONCE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
         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;
-            }
+    FuriString* next_line = furi_string_alloc();
+    bool array_loaded = false;
 
-            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;
-                    }
+    while(stream_read_line(nonce_array->stream, next_line)) {
+        const char* line = furi_string_get_cstr(next_line);
 
-                    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++;
-                }
+        // Only process lines ending with "dist 0"
+        if(!strstr(line, "dist 0")) {
+            continue;
+        }
+
+        MfClassicNonce res = {0};
+        res.attack = static_encrypted;
+
+        int parsed = sscanf(
+            line,
+            "Sec %*d key %*c cuid %" PRIx32 " nt0 %" PRIx32 " ks0 %" PRIx32
+            " par0 %4[01] nt1 %" PRIx32 " ks1 %" 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 >= 4) { // At least one nonce is present
+            res.par_1 = binaryStringToInt(res.par_1_str);
+            res.uid_xor_nt0 = res.uid ^ res.nt0;
+
+            if(parsed == 7) { // Both nonces are present
+                res.attack = static_nested;
+                res.par_2 = binaryStringToInt(res.par_2_str);
+                res.uid_xor_nt1 = res.uid ^ res.nt1;
             }
 
-            buffered_file_stream_close(nonce_array->stream);
+            (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++;
+            array_loaded = true;
         }
     }
 
-    storage_dir_close(dir);
-    storage_file_free(dir);
-    furi_record_close(RECORD_STORAGE);
     furi_string_free(next_line);
+    buffered_file_stream_close(nonce_array->stream);
 
     //FURI_LOG_I(TAG, "Loaded %lu Static Nested nonces", nonce_array->total_nonces);
-    return true;
+    return array_loaded;
 }
 
 MfClassicNonceArray* napi_mf_classic_nonce_array_alloc(
@@ -359,21 +309,13 @@ MfClassicNonceArray* napi_mf_classic_nonce_array_alloc(
     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(
+        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;
+        load_nested_nonces(nonce_array, program_state, system_dict, system_dict_exists, user_dict);
     }
 
     return nonce_array;
@@ -384,6 +326,7 @@ void napi_mf_classic_nonce_array_free(MfClassicNonceArray* nonce_array) {
     furi_assert(nonce_array);
     furi_assert(nonce_array->stream);
 
+    // TODO: Already closed?
     buffered_file_stream_close(nonce_array->stream);
     stream_free(nonce_array->stream);
     free(nonce_array);

+ 103 - 44
mfkey/mfkey.c

@@ -1,7 +1,6 @@
 #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
@@ -9,8 +8,11 @@
 // 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
+// TODO: Use seednt16 to reduce static encrypted key candidates: https://gist.github.com/noproto/8102f8f32546564cd674256e62ff76ea
+//       https://eprint.iacr.org/2024/1275.pdf section X
+// TODO: Static Encrypted: Minimum RAM for adding to keys dict (avoid crashes)
+// TODO: Static Encrypted: Optimize KeysDict or buffer keys to write in chunks
 
 #include <furi.h>
 #include <furi_hal.h>
@@ -32,13 +34,11 @@
 #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 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 TAG                   "MFKey"
+#define MAX_NAME_LEN          32
+#define MAX_PATH_LEN          64
 
 #define LF_POLY_ODD  (0x29CE5C)
 #define LF_POLY_EVEN (0x870804)
@@ -57,7 +57,8 @@ 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) {
+static inline int
+    check_state(struct Crypto1State* t, MfClassicNonce* n, ProgramState* program_state) {
     if(!(t->odd | t->even)) return 0;
     if(n->attack == mfkey32) {
         uint32_t rb = (napi_lfsr_rollback_word(t, 0, 0) ^ n->p64);
@@ -73,7 +74,6 @@ int check_state(struct Crypto1State* t, MfClassicNonce* n) {
             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);
@@ -82,7 +82,21 @@ int check_state(struct Crypto1State* t, MfClassicNonce* n) {
             crypto1_get_lfsr(&temp, &(n->key));
             return 1;
         }
-        return 0;
+    } else if(n->attack == static_encrypted) {
+        // TODO: Parity bits from rollback_word?
+        if(n->ks1_1_enc == napi_lfsr_rollback_word(t, n->uid_xor_nt0, 0)) {
+            // Reduce with parity
+            uint8_t local_parity_keystream_bits;
+            struct Crypto1State temp = {t->odd, t->even};
+            if((crypt_word_par(&temp, n->uid_xor_nt0, 0, n->nt0, &local_parity_keystream_bits) ==
+                n->ks1_1_enc) &&
+               (local_parity_keystream_bits == n->par_1)) {
+                // Found key candidate
+                crypto1_get_lfsr(t, &(n->key));
+                program_state->num_candidates++;
+                keys_dict_add_key(program_state->cuid_dict, n->key.data, sizeof(MfClassicKey));
+            }
+        }
     }
     return 0;
 }
@@ -208,7 +222,8 @@ int old_recover(
     int s,
     MfClassicNonce* n,
     unsigned int in,
-    int first_run) {
+    int first_run,
+    ProgramState* program_state) {
     int o, e, i;
     if(rem == -1) {
         for(e = e_head; e <= e_tail; ++e) {
@@ -217,7 +232,7 @@ int old_recover(
                 struct Crypto1State temp = {0, 0};
                 temp.even = odd[o];
                 temp.odd = even[e] ^ evenparity32(odd[o] & LF_POLY_ODD);
-                if(check_state(&temp, n)) {
+                if(check_state(&temp, n, program_state)) {
                     return -1;
                 }
             }
@@ -245,7 +260,20 @@ int old_recover(
             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);
+                odd,
+                o_tail--,
+                o,
+                oks,
+                even,
+                e_tail--,
+                e,
+                eks,
+                rem,
+                s,
+                n,
+                in,
+                first_run,
+                program_state);
             if(s == -1) {
                 break;
             }
@@ -381,7 +409,8 @@ int calculate_msb_tables(
             0,
             n,
             in >> 16,
-            1);
+            1,
+            program_state);
         if(res == -1) {
             return 1;
         }
@@ -436,6 +465,15 @@ bool recover(MfClassicNonce* n, int ks2, unsigned int in, ProgramState* program_
             return false;
         }
     }
+    // Adjust estimates for static encrypted attacks
+    if(n->attack == static_encrypted) {
+        eta_round_time *= 4;
+        eta_total_time *= 4;
+        if(is_full_speed()) {
+            eta_round_time *= 4;
+            eta_total_time *= 4;
+        }
+    }
     struct Msb* odd_msbs = block_pointers[0];
     struct Msb* even_msbs = block_pointers[1];
     unsigned int* temp_states_odd = block_pointers[2];
@@ -523,7 +561,8 @@ static void finished_beep() {
 }
 
 void mfkey(ProgramState* program_state) {
-    MfClassicKey found_key; // recovered key
+    uint32_t ks_enc = 0, nt_xor_uid = 0;
+    MfClassicKey found_key; // Recovered key
     size_t keyarray_size = 0;
     MfClassicKey* keyarray = malloc(sizeof(MfClassicKey) * 1);
     uint32_t i = 0, j = 0;
@@ -587,6 +626,7 @@ void mfkey(ProgramState* program_state) {
     // 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);
+    // TODO: Already closed?
     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());
@@ -601,28 +641,39 @@ void mfkey(ProgramState* program_state) {
             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;
+        FuriString* cuid_dict_path;
+        switch(next_nonce.attack) {
+        case mfkey32:
+            ks_enc = next_nonce.ar0_enc ^ next_nonce.p64;
+            nt_xor_uid = 0;
+            break;
+        case static_nested:
+            ks_enc = next_nonce.ks1_2_enc;
+            nt_xor_uid = next_nonce.uid_xor_nt1;
+            break;
+        case static_encrypted:
+            ks_enc = next_nonce.ks1_1_enc;
+            nt_xor_uid = next_nonce.uid_xor_nt0;
+            cuid_dict_path = furi_string_alloc_printf(
+                "%s/mf_classic_dict_%08lx.nfc", EXT_PATH("nfc/assets"), next_nonce.uid);
+            // May need RECORD_STORAGE?
+            program_state->cuid_dict = keys_dict_alloc(
+                furi_string_get_cstr(cuid_dict_path),
+                KeysDictModeOpenAlways,
+                sizeof(MfClassicKey));
+            break;
+        }
+
+        if(!recover(&next_nonce, ks_enc, nt_xor_uid, program_state)) {
+            if((next_nonce.attack == static_encrypted) && (program_state->cuid_dict)) {
+                keys_dict_free(program_state->cuid_dict);
             }
-        } 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;
+            if(program_state->close_thread_please) {
+                break;
             }
+            // No key found in recover() or static encrypted
+            (program_state->num_completed)++;
+            continue;
         }
         (program_state->cracked)++;
         (program_state->num_completed)++;
@@ -727,13 +778,21 @@ static void render_callback(Canvas* const canvas, void* ctx) {
         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);
+        canvas_draw_str_aligned(canvas, 64, 31, AlignCenter, 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);
+        canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignTop, draw_str);
+        if(program_state->num_candidates > 0) {
+            snprintf(
+                draw_str,
+                sizeof(draw_str),
+                "SEN key candidates: %d",
+                program_state->num_candidates);
+            canvas_draw_str_aligned(canvas, 64, 51, AlignCenter, 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");
@@ -741,10 +800,10 @@ static void render_callback(Canvas* const canvas, void* ctx) {
         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");
+        canvas_draw_str_aligned(canvas, 7, 20, AlignLeft, AlignTop, "Collect nonces by reading");
+        canvas_draw_str_aligned(canvas, 7, 30, AlignLeft, AlignTop, "tag or reader in NFC app:");
+        canvas_draw_str_aligned(canvas, 7, 40, AlignLeft, AlignTop, "https://docs.flipper.net/");
+        canvas_draw_str_aligned(canvas, 7, 50, AlignLeft, AlignTop, "nfc/mfkey32");
     } else if(program_state->mfkey_state == Error) {
         canvas_draw_str_aligned(canvas, 50, 25, AlignLeft, AlignTop, "Error");
         canvas_set_font(canvas, FontSecondary);
@@ -774,6 +833,7 @@ static void mfkey_state_init(ProgramState* program_state) {
     program_state->cracked = 0;
     program_state->unique_cracked = 0;
     program_state->num_completed = 0;
+    program_state->num_candidates = 0;
     program_state->total = 0;
     program_state->dict_count = 0;
 }
@@ -783,7 +843,6 @@ 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;

+ 4 - 1
mfkey/mfkey.h

@@ -41,6 +41,7 @@ typedef struct {
     int cracked;
     int unique_cracked;
     int num_completed;
+    int num_candidates;
     int total;
     int dict_count;
     int search;
@@ -52,11 +53,13 @@ typedef struct {
     bool is_thread_running;
     bool close_thread_please;
     FuriThread* mfkeythread;
+    KeysDict* cuid_dict;
 } ProgramState;
 
 typedef enum {
     mfkey32,
-    static_nested
+    static_nested,
+    static_encrypted
 } AttackType;
 
 typedef struct {