1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-13 13:09:49 +04:00
Files
unleashed-firmware/applications/system/mfkey/mfkey.c

1380 lines
37 KiB
C
Raw Normal View History

2025-04-07 18:24:33 +03:00
#pragma GCC optimize("O3")
#pragma GCC optimize("-funroll-all-loops")
// 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: 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
#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>
#define TAG "MFKey"
// TODO: Remove defines that are not needed
#define KEYS_DICT_SYSTEM_PATH EXT_PATH("nfc/assets/mf_classic_dict.nfc")
2025-09-09 15:31:43 +03:00
#define KEYS_DICT_USER_PATH EXT_PATH("nfc/assets/mf_classic_dict_user.nfc")
#define MAX_NAME_LEN 32
#define MAX_PATH_LEN 64
#define STATIC_ENCRYPTED_RAM_THRESHOLD 4096
2025-04-07 18:24:33 +03:00
2025-09-09 15:31:43 +03:00
#define LF_POLY_ODD (0x29CE5C)
2025-04-07 18:24:33 +03:00
#define LF_POLY_EVEN (0x870804)
2025-09-09 15:31:43 +03:00
#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 SWAP(a, b) \
do \
{ \
unsigned int t = a; \
a = b; \
b = t; \
} while (0)
2025-04-07 18:24:33 +03:00
#define SWAPENDIAN(x) \
2025-09-09 15:31:43 +03:00
((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16)
// #define SIZEOF(arr) sizeof(arr) / sizeof(*arr)
2025-04-07 18:24:33 +03:00
2025-09-09 15:31:43 +03:00
// Reduced to 16-bit as these values are small and don't need 32-bit
static int16_t eta_round_time = 44;
static int16_t eta_total_time = 705;
// MSB_LIMIT: Chunk size (out of 256) - can be 8-bit as it's a small value
static uint8_t MSB_LIMIT = 16;
static inline void flush_key_buffer(ProgramState *program_state)
{
if (program_state->key_buffer && program_state->key_buffer_count > 0 && program_state->cuid_dict)
{
// Pre-allocate exact size needed: 12 hex chars + 1 newline per key
size_t total_size = program_state->key_buffer_count * 13;
//FURI_LOG_I(TAG, "Flushing key buffer: %d keys", program_state->key_buffer_count);
//FURI_LOG_I(TAG, "Total size: %d bytes", total_size);
char* batch_buffer = malloc(total_size + 1); // +1 for null terminator
char* ptr = batch_buffer;
const char hex_chars[] = "0123456789ABCDEF";
for (size_t i = 0; i < program_state->key_buffer_count; i++)
{
// Convert key to hex string directly into buffer
for (size_t j = 0; j < sizeof(MfClassicKey); j++)
{
uint8_t byte = program_state->key_buffer[i].data[j];
*ptr++ = hex_chars[byte >> 4];
*ptr++ = hex_chars[byte & 0x0F];
}
*ptr++ = '\n';
}
*ptr = '\0';
// Write all keys at once by directly accessing the stream
Stream* stream = program_state->cuid_dict->stream;
uint32_t actual_pos = stream_tell(stream);
if (stream_seek(stream, 0, StreamOffsetFromEnd) &&
stream_write(stream, (uint8_t*)batch_buffer, total_size) == total_size)
{
// Update total key count
program_state->cuid_dict->total_keys += program_state->key_buffer_count;
}
// May not be needed
stream_seek(stream, actual_pos, StreamOffsetFromStart);
free(batch_buffer);
program_state->key_buffer_count = 0;
}
}
2025-04-07 18:24:33 +03:00
static inline int
2025-09-09 15:31:43 +03:00
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);
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;
}
}
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;
}
}
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++;
// Use key buffer - buffer is guaranteed to be available for static_encrypted
program_state->key_buffer[program_state->key_buffer_count] = n->key;
program_state->key_buffer_count++;
// Flush buffer when full
if (program_state->key_buffer_count >= program_state->key_buffer_size)
{
flush_key_buffer(program_state);
}
}
}
}
return 0;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
static inline __attribute__((hot)) int state_loop(
unsigned int *states_buffer,
int xks,
int m1,
int m2,
unsigned int in,
int and_val)
{
int states_tail = 0;
int xks_bit = 0, round_in = 0;
// Unroll first 4 rounds (no round_in calculations needed)
// Hoist the filter() calls to just one iteration and reuse the results
// This avoids redundant calculations and improves performance and gives us 2000b of extra ram (11496b free on run)
// V.28/04. Aprox 3s speedup per round. Total 3 keys 7mins 17s!!
for (int round = 1; round <= 4; ++round)
{
xks_bit = BIT(xks, round);
for (int s = 0; s <= states_tail; ++s)
{
unsigned int v = states_buffer[s] << 1;
states_buffer[s] = v;
int f0 = filter(v);
int f1 = filter(v | 1);
if (__builtin_expect((f0 ^ f1) != 0, 0))
{
states_buffer[s] |= f0 ^ xks_bit;
}
else if (__builtin_expect(f0 == xks_bit, 1))
{
states_buffer[++states_tail] = states_buffer[++s];
states_buffer[s] = states_buffer[s - 1] | 1;
}
else
{
states_buffer[s--] = states_buffer[states_tail--];
}
}
}
// Round 5 (unrolled)
{
xks_bit = BIT(xks, 5);
int r5_in = ((in >> 2) & and_val) << 24; // 2*(5-4)=2
for (int s = 0; s <= states_tail; ++s)
{
unsigned int v = states_buffer[s] << 1;
states_buffer[s] = v;
int f0 = filter(v), f1 = filter(v | 1);
if (__builtin_expect((f0 ^ f1) != 0, 0))
{
states_buffer[s] |= f0 ^ xks_bit;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s] ^= r5_in;
}
else if (__builtin_expect(f0 == xks_bit, 1))
{
states_buffer[++states_tail] = states_buffer[s + 1];
states_buffer[s + 1] = v | 1;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s++] ^= r5_in;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s] ^= r5_in;
}
else
{
states_buffer[s--] = states_buffer[states_tail--];
}
}
}
// Round 6 (unrolled)
{
xks_bit = BIT(xks, 6);
int r6_in = ((in >> 4) & and_val) << 24; // 2*(6-4)=4
for (int s = 0; s <= states_tail; ++s)
{
unsigned int v = states_buffer[s] << 1;
states_buffer[s] = v;
int f0 = filter(v), f1 = filter(v | 1);
if (__builtin_expect((f0 ^ f1) != 0, 0))
{
states_buffer[s] |= f0 ^ xks_bit;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s] ^= r6_in;
}
else if (__builtin_expect(f0 == xks_bit, 1))
{
states_buffer[++states_tail] = states_buffer[s + 1];
states_buffer[s + 1] = v | 1;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s++] ^= r6_in;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s] ^= r6_in;
}
else
{
states_buffer[s--] = states_buffer[states_tail--];
}
}
}
// Loop rounds 712
for (int round = 7; round <= 12; ++round)
{
xks_bit = BIT(xks, round);
round_in = ((in >> (2 * (round - 4))) & and_val) << 24;
for (int s = 0; s <= states_tail; ++s)
{
unsigned int v = states_buffer[s] << 1;
states_buffer[s] = v;
int f0 = filter(v), f1 = filter(v | 1);
if (__builtin_expect((f0 ^ f1) != 0, 0))
{
states_buffer[s] |= f0 ^ xks_bit;
update_contribution(states_buffer, s, m1, m2);
states_buffer[s] ^= round_in;
}
else if (__builtin_expect(f0 == xks_bit, 1))
{
states_buffer[++states_tail] = states_buffer[s + 1];
states_buffer[s + 1] = v | 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[s--] = states_buffer[states_tail--];
}
}
}
return states_tail;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
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;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
void quicksort(unsigned int array[], int low, int high)
{
// Use insertion sort for small arrays (threshold determined by testing)
if (high - low < 16)
{
// Insertion sort
for (int i = low + 1; i <= high; i++)
{
unsigned int key = array[i];
int j = i - 1;
while (j >= low && array[j] > key)
{
array[j + 1] = array[j];
j--;
}
array[j + 1] = key;
}
return;
}
if (low >= high)
return;
// Median-of-three pivot selection
int middle = low + (high - low) / 2;
if (array[middle] < array[low])
SWAP(array[middle], array[low]);
if (array[high] < array[low])
SWAP(array[high], array[low]);
if (array[high] < array[middle])
SWAP(array[high], array[middle]);
unsigned int pivot = array[middle];
// Rest of quicksort with improved partitioning
int i = low, j = high;
while (i <= j)
{
while (array[i] < pivot)
i++;
while (array[j] > pivot)
j--;
if (i <= j)
{
// swap
unsigned 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);
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
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;
2025-04-07 18:24:33 +03:00
}
int old_recover(
2025-09-09 15:31:43 +03:00
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,
ProgramState *program_state)
{
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, program_state))
{
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,
program_state);
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;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
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;
2025-04-07 18:24:33 +03:00
}
int calculate_msb_tables(
2025-09-09 15:31:43 +03:00
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)
{
unsigned int msb_head = (MSB_LIMIT * msb_round);
unsigned int msb_tail = (MSB_LIMIT * (msb_round + 1));
int states_tail = 0;
int semi_state = 0;
unsigned int msb = 0;
// Preprocessed in value
in = ((in >> 16 & 0xff) | (in << 16) | (in & 0xff00)) << 1;
// Clear MSB arrays once before loop instead of inside loop
memset(odd_msbs, 0, MSB_LIMIT * sizeof(struct Msb));
memset(even_msbs, 0, MSB_LIMIT * sizeof(struct Msb));
// Bit values to check - calculate once outside the loop
int oks_bit = oks & 1;
int eks_bit = eks & 1;
// Check for stop request less frequently
int sync_check_interval = 32768 * 2; // Doubled the interval
for (semi_state = 1 << 20; semi_state >= 0; semi_state--)
{
if (semi_state % sync_check_interval == 0)
{
if (sync_state(program_state) == 1)
{
return 0;
}
}
// Process both filter conditions in one pass when possible
int filter_semi_state = filter(semi_state);
// Check oks condition
if (filter_semi_state == oks_bit)
{
states_buffer[0] = semi_state;
states_tail = state_loop(states_buffer, oks, CONST_M1_1, CONST_M2_1, 0, 0);
for (int i = states_tail; i >= 0; i--)
{
msb = states_buffer[i] >> 24;
if ((msb >= msb_head) && (msb < msb_tail))
{
// Calculate index once
int msb_idx = msb - msb_head;
// Avoid sequential scan by using a direct flag
int found = 0;
for (int j = 0; j < odd_msbs[msb_idx].tail; j++)
{
if (odd_msbs[msb_idx].states[j] == states_buffer[i])
{
found = 1;
break;
}
}
if (!found)
{
int tail = odd_msbs[msb_idx].tail++;
odd_msbs[msb_idx].states[tail] = states_buffer[i];
}
}
}
}
// Check eks condition
if (filter_semi_state == eks_bit)
{
states_buffer[0] = semi_state;
states_tail = state_loop(states_buffer, eks, CONST_M1_2, CONST_M2_2, in, 3);
for (int i = 0; i <= states_tail; i++)
{
msb = states_buffer[i] >> 24;
if ((msb >= msb_head) && (msb < msb_tail))
{
// Calculate index once
int msb_idx = msb - msb_head;
2025-04-07 18:24:33 +03:00
2025-09-09 15:31:43 +03:00
// Avoid sequential scan
int found = 0;
for (int j = 0; j < even_msbs[msb_idx].tail; j++)
{
if (even_msbs[msb_idx].states[j] == states_buffer[i])
{
found = 1;
break;
}
}
2025-04-07 18:24:33 +03:00
2025-09-09 15:31:43 +03:00
if (!found)
{
int tail = even_msbs[msb_idx].tail++;
even_msbs[msb_idx].states[tail] = states_buffer[i];
}
}
}
}
}
2025-04-07 18:24:33 +03:00
2025-09-09 15:31:43 +03:00
// Shift once outside the loop
oks >>= 12;
eks >>= 12;
2025-04-07 18:24:33 +03:00
2025-09-09 15:31:43 +03:00
// Process results
for (int i = 0; i < MSB_LIMIT; i++)
{
if ((i % 4) == 0 && sync_state(program_state) == 1)
{
return 0;
}
// Only clear buffers if they're going to be used
if (odd_msbs[i].tail > 0 || even_msbs[i].tail > 0)
{
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,
program_state);
if (res == -1)
{
return 1;
}
}
}
return 0;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
void **allocate_blocks(const size_t *block_sizes, int num_blocks)
{
void **block_pointers = malloc(num_blocks * sizeof(void *));
if (!block_pointers)
{
return NULL;
}
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]);
if (!block_pointers[i])
{
// Allocation failed
for (int j = 0; j < i; j++)
{
free(block_pointers[j]);
}
free(block_pointers);
return NULL;
}
}
return block_pointers;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
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]);
// Reset globals each nonce
eta_round_time = 44;
eta_total_time = 705;
MSB_LIMIT = 16;
// Use half speed (reduced block sizes) for static encrypted nonces so we can buffer keys
bool use_half_speed = (n->attack == static_encrypted);
if (use_half_speed)
{
//eta_round_time *= 2;
eta_total_time *= 2;
MSB_LIMIT /= 2;
}
void **block_pointers = allocate_blocks(use_half_speed ? reduced_block_sizes : block_sizes, num_blocks);
if (block_pointers == NULL) {
if (n->attack != static_encrypted)
{
// System has less than the guaranteed amount of RAM (140 KB) - adjust some parameters to run anyway at half speed
// eta_round_time *= 2;
eta_total_time *= 2;
MSB_LIMIT /= 2;
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;
}
} else
{
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];
// Allocate key buffer for static encrypted nonces
if (n->attack == static_encrypted)
{
size_t available_ram = memmgr_heap_get_max_free_block();
// Each key becomes 12 hex chars + 1 newline = 13 bytes in the batch string
// Plus original 6 bytes in buffer = 19 bytes total per key
// Add extra safety margin for string overhead and other allocations
const size_t safety_threshold = STATIC_ENCRYPTED_RAM_THRESHOLD;
const size_t bytes_per_key = sizeof(MfClassicKey) + 13; // buffer + string representation
if (available_ram > safety_threshold)
{
program_state->key_buffer_size = (available_ram - safety_threshold) / bytes_per_key;
program_state->key_buffer = malloc(program_state->key_buffer_size * sizeof(MfClassicKey));
program_state->key_buffer_count = 0;
if (!program_state->key_buffer)
{
// Free the allocated blocks before returning
for (int i = 0; i < num_blocks; i++)
{
free(block_pointers[i]);
}
free(block_pointers);
program_state->err = InsufficientRAM;
program_state->mfkey_state = Error;
return false;
}
}
else
{
// Free the allocated blocks before returning
for (int i = 0; i < num_blocks; i++)
{
free(block_pointers[i]);
}
free(block_pointers);
program_state->err = InsufficientRAM;
program_state->mfkey_state = Error;
return false;
}
}
else
{
program_state->key_buffer = NULL;
program_state->key_buffer_size = 0;
program_state->key_buffer_count = 0;
}
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;
}
}
// Final flush and cleanup for key buffer
if (n->attack == static_encrypted && program_state->key_buffer)
{
flush_key_buffer(program_state);
free(program_state->key_buffer);
program_state->key_buffer = NULL;
program_state->key_buffer_size = 0;
program_state->key_buffer_count = 0;
}
// Free the allocated blocks
for (int i = 0; i < num_blocks; i++)
{
free(block_pointers[i]);
}
free(block_pointers);
return found;
2025-04-07 18:24:33 +03:00
}
bool key_already_found_for_nonce_in_solved(
2025-09-09 15:31:43 +03:00
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;
2025-04-07 18:24:33 +03:00
}
#pragma GCC push_options
#pragma GCC optimize("Os")
2025-09-09 15:31:43 +03:00
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");
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
void mfkey(ProgramState *program_state)
{
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);
if (!keyarray)
{
program_state->err = InsufficientRAM;
program_state->mfkey_state = Error;
return;
}
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);
// 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());
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);
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));
furi_string_free(cuid_dict_path);
break;
}
if (!recover(&next_nonce, ks_enc, nt_xor_uid, program_state))
{
// Check for non-recoverable errors and break the loop
if (program_state->mfkey_state == Error)
{
if ((next_nonce.attack == static_encrypted) && (program_state->cuid_dict))
{
keys_dict_free(program_state->cuid_dict);
program_state->cuid_dict = NULL;
}
break;
}
if (program_state->close_thread_please)
{
if ((next_nonce.attack == static_encrypted) && (program_state->cuid_dict))
{
keys_dict_free(program_state->cuid_dict);
program_state->cuid_dict = NULL;
}
break;
}
// No key found in recover() or static encrypted
(program_state->num_completed)++;
// Free CUID dictionary after each static_encrypted nonce processing
if ((next_nonce.attack == static_encrypted) && (program_state->cuid_dict))
{
keys_dict_free(program_state->cuid_dict);
program_state->cuid_dict = NULL;
}
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
MfClassicKey *new_keyarray = realloc(keyarray, sizeof(MfClassicKey) * (keyarray_size + 1));
if (!new_keyarray)
{
// Realloc failed - continue with existing keyarray
FURI_LOG_E(TAG, "Failed to realloc keyarray");
}
else
{
keyarray = new_keyarray;
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(DolphinDeedNfcKeyAdd);
}
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;
2025-04-07 18:24:33 +03:00
}
// Screen is 128x64 px
2025-09-09 15:31:43 +03:00
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);
// FontSecondary by default, title is drawn at the end
snprintf(draw_str, sizeof(draw_str), "RAM: %zub", memmgr_get_free_heap());
canvas_draw_str_aligned(canvas, 48, 5, AlignLeft, AlignTop, draw_str);
canvas_draw_icon(canvas, 114, 4, &I_mfkey);
if (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 || eta_round > 1)
{
// Round ETA miscalculated
eta_round = 1;
program_state->eta_round = 0;
}
if (eta_total < 0 || eta_round > 1)
{
// Total ETA miscalculated
eta_total = 1;
program_state->eta_total = 0;
}
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->mfkey_state == DictionaryAttack)
{
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_draw_str_aligned(canvas, 64, 31, AlignCenter, AlignTop, "Complete");
snprintf(
draw_str,
sizeof(draw_str),
"Keys added to user dict: %d",
program_state->unique_cracked);
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_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_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");
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
}
// Title
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 5, 4, AlignLeft, AlignTop, "MFKey");
furi_mutex_release(program_state->mutex);
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
static void input_callback(InputEvent *input_event, void *event_queue)
{
furi_assert(event_queue);
furi_message_queue_put((FuriMessageQueue *)event_queue, input_event, FuriWaitForever);
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
static void mfkey_state_init(ProgramState *program_state)
{
program_state->mfkey_state = Ready;
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;
2025-04-07 18:24:33 +03:00
}
// Entrypoint for worker thread
2025-09-09 15:31:43 +03:00
static int32_t mfkey_worker_thread(void *ctx)
{
ProgramState *program_state = ctx;
program_state->mfkey_state = Initializing;
mfkey(program_state);
return 0;
2025-04-07 18:24:33 +03:00
}
2025-09-09 15:31:43 +03:00
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_ex("MFKeyWorker", 2048, mfkey_worker_thread, program_state);
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->mfkey_state == Ready)
{
program_state->mfkey_state = Help;
}
break;
case InputKeyOk:
if (program_state->mfkey_state == Ready)
{
furi_thread_start(program_state->mfkeythread);
}
break;
case InputKeyBack:
if (program_state->mfkey_state == Help)
{
program_state->mfkey_state = Ready;
}
else
{
program_state->close_thread_please = true;
// Wait until thread is finished
furi_thread_join(program_state->mfkeythread);
main_loop = false;
}
break;
default:
break;
}
}
}
furi_mutex_release(program_state->mutex);
view_port_update(view_port);
}
// Thread joined in back event handler
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;
2025-04-07 18:24:33 +03:00
}
#pragma GCC pop_options