[NFC] Trim MIFARE Plus generator & admin-key mapping (in-place cleanup) (#1035)

* [NFC] Data-drive MIFARE Plus manual-add generators

Replace the 18 near-identical MIFARE Plus generator thunks and their
handler-table entries with a data-driven mf_plus_generator_configs[]
table (uid_len / type / size / ATS per variant), dispatched via a small
range predicate. The Ultralight/NTAG and Classic types keep their
bespoke handlers, so the handler table now stops before the MF Plus
range.

Also drop mf_plus_ats_hist_ev, which was byte-identical to
mf_plus_ats_hist_s (EV1/EV2 are identified by GetVersion, not the ATS).

Pure refactor: the Add-Manually menu entries, the generated cards and the
public SDK API are all unchanged. Trims firmware flash by removing the
per-variant functions and the duplicate blob; a _Static_assert pins the
config table to the enum range so a future type can't desync it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: single source for admin-key addresses

Fold the three copies of the admin-key address<->type mapping
(0x9000 CardMaster, 0x9001 CardConfig, 0x9003 L3Switch, 0x9004
SL1CardAuth) into one mf_plus_admin_key_addresses[] table with a forward
helper (mf_plus_get_admin_key_address, replaces the poller's local id
array) and a reverse helper (mf_plus_admin_key_type_from_address,
replaces two identical switch statements in the listener's key-resolve
and write-store paths).

Behavior-preserving: both listener default fall-throughs are kept (resolve
returns false; store falls through to data/config block handling), the
poller authenticates the same addresses, and a _Static_assert pins the
table to MfPlusAdminKeyNum. Internal helpers only; no SDK API change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Changelog: MIFARE Plus generator/admin-map cleanup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mykhailo Shevchuk
2026-07-07 23:08:54 +03:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 45c3762c06
commit e4e8a8ef2b
6 changed files with 111 additions and 209 deletions
+1
View File
@@ -22,6 +22,7 @@
* Fix BLE sync, fix possible delay related issues
* Disabled debug and trace logs in the FW binary (apps .fap's are not affected) to free up some flash space for new features
* NFC: Fix typo in SLIX poller (by @WillyJL)
* NFC: Internal MIFARE Plus cleanup - data-drive the "Add Manually" generator variants and unify the admin-key address mapping into one source of truth; small internal-flash saving, no functional change (by @mishamyte | PR #1035)
<br><br>
----
+61 -164
View File
@@ -446,15 +446,13 @@ static void nfc_generate_mf_classic_4k_7b_uid(NfcDevice* nfc_device) {
// detection (see mf_plus_type_from_ats): S/X mask their size with 2F 2F and differ by the caps
// byte's SVC bit; SE reveals 21 30. The trailing CRC16 floats between configs -- the SE value is a
// real capture (77 C1), the S/X values are AN10833. EV1/EV2 identify via GetVersion, not the ATS, so
// they reuse the generic Plus block.
// they reuse the byte-identical S block.
static const uint8_t mf_plus_ats_hist_se[MF_PLUS_ATS_HIST_LEN] =
{0xC1, 0x05, 0x21, 0x30, 0x00, 0x77, 0xC1};
static const uint8_t mf_plus_ats_hist_s[MF_PLUS_ATS_HIST_LEN] =
{0xC1, 0x05, 0x2F, 0x2F, 0x00, 0x35, 0xC7};
static const uint8_t mf_plus_ats_hist_x[MF_PLUS_ATS_HIST_LEN] =
{0xC1, 0x05, 0x2F, 0x2F, 0x01, 0xBC, 0xD6};
static const uint8_t mf_plus_ats_hist_ev[MF_PLUS_ATS_HIST_LEN] =
{0xC1, 0x05, 0x2F, 0x2F, 0x00, 0x35, 0xC7};
// EV1/EV2 answer GetVersion; fill a plausible response. hw_type low nibble 0x02 = Plus, hw_major
// 0x11/0x22 = EV1/EV2, hw_storage 0x16/0x18 = 2K/4K (matches mf_plus_get_type_from_version). The
@@ -616,79 +614,52 @@ static void nfc_generate_mf_plus(
mf_plus_free(data);
}
static void nfc_generate_mf_plus_se_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeSE, MfPlusSize1K, mf_plus_ats_hist_se);
// The 18 MIFARE Plus variants differ only by parameters (UID length, product type, memory size and
// ATS historical bytes), so they are data-driven from this table rather than a thunk each. Indexed
// by (type - NfcDataGeneratorTypeMfPlusSE_4b), so the row order must track the enum. EV1/EV2 reuse
// the S ATS block (byte-identical; those products are identified by GetVersion, not the ATS).
typedef struct {
const char* name;
uint8_t uid_len;
uint8_t type; // MfPlusType
uint8_t size; // MfPlusSize
const uint8_t* ats_hist;
} MfPlusGeneratorConfig;
static const MfPlusGeneratorConfig mf_plus_generator_configs[] = {
{"Mifare Plus SE 4byte UID", 4, MfPlusTypeSE, MfPlusSize1K, mf_plus_ats_hist_se},
{"Mifare Plus SE 7byte UID", 7, MfPlusTypeSE, MfPlusSize1K, mf_plus_ats_hist_se},
{"Mifare Plus S 2K 4byte UID", 4, MfPlusTypeS, MfPlusSize2K, mf_plus_ats_hist_s},
{"Mifare Plus S 2K 7byte UID", 7, MfPlusTypeS, MfPlusSize2K, mf_plus_ats_hist_s},
{"Mifare Plus S 4K 4byte UID", 4, MfPlusTypeS, MfPlusSize4K, mf_plus_ats_hist_s},
{"Mifare Plus S 4K 7byte UID", 7, MfPlusTypeS, MfPlusSize4K, mf_plus_ats_hist_s},
{"Mifare Plus X 2K 4byte UID", 4, MfPlusTypeX, MfPlusSize2K, mf_plus_ats_hist_x},
{"Mifare Plus X 2K 7byte UID", 7, MfPlusTypeX, MfPlusSize2K, mf_plus_ats_hist_x},
{"Mifare Plus X 4K 4byte UID", 4, MfPlusTypeX, MfPlusSize4K, mf_plus_ats_hist_x},
{"Mifare Plus X 4K 7byte UID", 7, MfPlusTypeX, MfPlusSize4K, mf_plus_ats_hist_x},
{"Mifare Plus EV1 2K 4byte UID", 4, MfPlusTypeEV1, MfPlusSize2K, mf_plus_ats_hist_s},
{"Mifare Plus EV1 2K 7byte UID", 7, MfPlusTypeEV1, MfPlusSize2K, mf_plus_ats_hist_s},
{"Mifare Plus EV1 4K 4byte UID", 4, MfPlusTypeEV1, MfPlusSize4K, mf_plus_ats_hist_s},
{"Mifare Plus EV1 4K 7byte UID", 7, MfPlusTypeEV1, MfPlusSize4K, mf_plus_ats_hist_s},
{"Mifare Plus EV2 2K 4byte UID", 4, MfPlusTypeEV2, MfPlusSize2K, mf_plus_ats_hist_s},
{"Mifare Plus EV2 2K 7byte UID", 7, MfPlusTypeEV2, MfPlusSize2K, mf_plus_ats_hist_s},
{"Mifare Plus EV2 4K 4byte UID", 4, MfPlusTypeEV2, MfPlusSize4K, mf_plus_ats_hist_s},
{"Mifare Plus EV2 4K 7byte UID", 7, MfPlusTypeEV2, MfPlusSize4K, mf_plus_ats_hist_s},
};
_Static_assert(
COUNT_OF(mf_plus_generator_configs) ==
(size_t)(NfcDataGeneratorTypeMfPlusEV2_4k_7b - NfcDataGeneratorTypeMfPlusSE_4b + 1),
"mf_plus_generator_configs must cover every MIFARE Plus generator type");
static bool nfc_data_generator_type_is_mf_plus(NfcDataGeneratorType type) {
return type >= NfcDataGeneratorTypeMfPlusSE_4b && type <= NfcDataGeneratorTypeMfPlusEV2_4k_7b;
}
static void nfc_generate_mf_plus_se_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeSE, MfPlusSize1K, mf_plus_ats_hist_se);
}
static void nfc_generate_mf_plus_s2k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeS, MfPlusSize2K, mf_plus_ats_hist_s);
}
static void nfc_generate_mf_plus_s2k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeS, MfPlusSize2K, mf_plus_ats_hist_s);
}
static void nfc_generate_mf_plus_s4k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeS, MfPlusSize4K, mf_plus_ats_hist_s);
}
static void nfc_generate_mf_plus_s4k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeS, MfPlusSize4K, mf_plus_ats_hist_s);
}
static void nfc_generate_mf_plus_x2k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeX, MfPlusSize2K, mf_plus_ats_hist_x);
}
static void nfc_generate_mf_plus_x2k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeX, MfPlusSize2K, mf_plus_ats_hist_x);
}
static void nfc_generate_mf_plus_x4k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeX, MfPlusSize4K, mf_plus_ats_hist_x);
}
static void nfc_generate_mf_plus_x4k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeX, MfPlusSize4K, mf_plus_ats_hist_x);
}
static void nfc_generate_mf_plus_ev1_2k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeEV1, MfPlusSize2K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev1_2k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeEV1, MfPlusSize2K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev1_4k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeEV1, MfPlusSize4K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev1_4k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeEV1, MfPlusSize4K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev2_2k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeEV2, MfPlusSize2K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev2_2k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeEV2, MfPlusSize2K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev2_4k_4b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 4, MfPlusTypeEV2, MfPlusSize4K, mf_plus_ats_hist_ev);
}
static void nfc_generate_mf_plus_ev2_4k_7b(NfcDevice* nfc_device) {
nfc_generate_mf_plus(nfc_device, 7, MfPlusTypeEV2, MfPlusSize4K, mf_plus_ats_hist_ev);
}
static const NfcDataGenerator nfc_data_generator[NfcDataGeneratorTypeNum] = {
// Handler-based table for the Ultralight/NTAG and Classic types, whose per-variant layouts are
// bespoke. The MIFARE Plus types are parametric and dispatched from mf_plus_generator_configs, so
// this table intentionally stops before them.
static const NfcDataGenerator nfc_data_generator[NfcDataGeneratorTypeMfPlusSE_4b] = {
[NfcDataGeneratorTypeMfUltralight] =
{
.name = "Mifare Ultralight",
@@ -779,101 +750,15 @@ static const NfcDataGenerator nfc_data_generator[NfcDataGeneratorTypeNum] = {
.name = "Mifare Classic 4k 7byte UID",
.handler = nfc_generate_mf_classic_4k_7b_uid,
},
[NfcDataGeneratorTypeMfPlusSE_4b] =
{
.name = "Mifare Plus SE 4byte UID",
.handler = nfc_generate_mf_plus_se_4b,
},
[NfcDataGeneratorTypeMfPlusSE_7b] =
{
.name = "Mifare Plus SE 7byte UID",
.handler = nfc_generate_mf_plus_se_7b,
},
[NfcDataGeneratorTypeMfPlusS2k_4b] =
{
.name = "Mifare Plus S 2K 4byte UID",
.handler = nfc_generate_mf_plus_s2k_4b,
},
[NfcDataGeneratorTypeMfPlusS2k_7b] =
{
.name = "Mifare Plus S 2K 7byte UID",
.handler = nfc_generate_mf_plus_s2k_7b,
},
[NfcDataGeneratorTypeMfPlusS4k_4b] =
{
.name = "Mifare Plus S 4K 4byte UID",
.handler = nfc_generate_mf_plus_s4k_4b,
},
[NfcDataGeneratorTypeMfPlusS4k_7b] =
{
.name = "Mifare Plus S 4K 7byte UID",
.handler = nfc_generate_mf_plus_s4k_7b,
},
[NfcDataGeneratorTypeMfPlusX2k_4b] =
{
.name = "Mifare Plus X 2K 4byte UID",
.handler = nfc_generate_mf_plus_x2k_4b,
},
[NfcDataGeneratorTypeMfPlusX2k_7b] =
{
.name = "Mifare Plus X 2K 7byte UID",
.handler = nfc_generate_mf_plus_x2k_7b,
},
[NfcDataGeneratorTypeMfPlusX4k_4b] =
{
.name = "Mifare Plus X 4K 4byte UID",
.handler = nfc_generate_mf_plus_x4k_4b,
},
[NfcDataGeneratorTypeMfPlusX4k_7b] =
{
.name = "Mifare Plus X 4K 7byte UID",
.handler = nfc_generate_mf_plus_x4k_7b,
},
[NfcDataGeneratorTypeMfPlusEV1_2k_4b] =
{
.name = "Mifare Plus EV1 2K 4byte UID",
.handler = nfc_generate_mf_plus_ev1_2k_4b,
},
[NfcDataGeneratorTypeMfPlusEV1_2k_7b] =
{
.name = "Mifare Plus EV1 2K 7byte UID",
.handler = nfc_generate_mf_plus_ev1_2k_7b,
},
[NfcDataGeneratorTypeMfPlusEV1_4k_4b] =
{
.name = "Mifare Plus EV1 4K 4byte UID",
.handler = nfc_generate_mf_plus_ev1_4k_4b,
},
[NfcDataGeneratorTypeMfPlusEV1_4k_7b] =
{
.name = "Mifare Plus EV1 4K 7byte UID",
.handler = nfc_generate_mf_plus_ev1_4k_7b,
},
[NfcDataGeneratorTypeMfPlusEV2_2k_4b] =
{
.name = "Mifare Plus EV2 2K 4byte UID",
.handler = nfc_generate_mf_plus_ev2_2k_4b,
},
[NfcDataGeneratorTypeMfPlusEV2_2k_7b] =
{
.name = "Mifare Plus EV2 2K 7byte UID",
.handler = nfc_generate_mf_plus_ev2_2k_7b,
},
[NfcDataGeneratorTypeMfPlusEV2_4k_4b] =
{
.name = "Mifare Plus EV2 4K 4byte UID",
.handler = nfc_generate_mf_plus_ev2_4k_4b,
},
[NfcDataGeneratorTypeMfPlusEV2_4k_7b] =
{
.name = "Mifare Plus EV2 4K 7byte UID",
.handler = nfc_generate_mf_plus_ev2_4k_7b,
},
};
const char* nfc_data_generator_get_name(NfcDataGeneratorType type) {
furi_check(type < NfcDataGeneratorTypeNum);
if(nfc_data_generator_type_is_mf_plus(type)) {
return mf_plus_generator_configs[type - NfcDataGeneratorTypeMfPlusSE_4b].name;
}
return nfc_data_generator[type].name;
}
@@ -881,5 +766,17 @@ void nfc_data_generator_fill_data(NfcDataGeneratorType type, NfcDevice* nfc_devi
furi_check(type < NfcDataGeneratorTypeNum);
furi_check(nfc_device);
if(nfc_data_generator_type_is_mf_plus(type)) {
const MfPlusGeneratorConfig* config =
&mf_plus_generator_configs[type - NfcDataGeneratorTypeMfPlusSE_4b];
nfc_generate_mf_plus(
nfc_device,
config->uid_len,
(MfPlusType)config->type,
(MfPlusSize)config->size,
config->ats_hist);
return;
}
nfc_data_generator[type].handler(nfc_device);
}
+38
View File
@@ -444,6 +444,44 @@ const char* mf_plus_get_admin_key_name(MfPlusAdminKeyType type) {
return mf_plus_admin_key_names[type];
}
// Single source of truth for the admin keys' 0x90xx auth addresses. The enum skips 0x9002 (the
// SL2->SL3 key we never recover), so this is a table rather than 0x9000 + index. Both the poller
// (which address to authenticate) and the listener (which key a reader is addressing) derive from it.
typedef struct {
uint16_t address;
MfPlusAdminKeyType type;
} MfPlusAdminKeyAddress;
static const MfPlusAdminKeyAddress mf_plus_admin_key_addresses[] = {
{0x9000, MfPlusAdminKeyCardMaster},
{0x9001, MfPlusAdminKeyCardConfig},
{0x9003, MfPlusAdminKeyL3Switch},
{0x9004, MfPlusAdminKeySL1CardAuth},
};
_Static_assert(
COUNT_OF(mf_plus_admin_key_addresses) == MfPlusAdminKeyNum,
"mf_plus_admin_key_addresses must cover every admin key type");
uint16_t mf_plus_get_admin_key_address(MfPlusAdminKeyType type) {
for(size_t i = 0; i < COUNT_OF(mf_plus_admin_key_addresses); i++) {
if(mf_plus_admin_key_addresses[i].type == type) {
return mf_plus_admin_key_addresses[i].address;
}
}
furi_crash("Invalid MfPlusAdminKeyType");
}
bool mf_plus_admin_key_type_from_address(uint16_t address, MfPlusAdminKeyType* type) {
for(size_t i = 0; i < COUNT_OF(mf_plus_admin_key_addresses); i++) {
if(mf_plus_admin_key_addresses[i].address == address) {
*type = mf_plus_admin_key_addresses[i].type;
return true;
}
}
return false;
}
// Lock the mask-fits-domain invariants at compile time.
_Static_assert(MfPlusAdminKeyNum <= 8, "admin_key_mask (uint8_t) too small");
_Static_assert(MF_PLUS_CONFIG_BLOCK_NUM <= 8, "config_read_mask (uint8_t) too small");
+5
View File
@@ -50,6 +50,11 @@ void mf_plus_set_admin_key_found(MfPlusData* data, MfPlusAdminKeyType type, cons
void mf_plus_set_config_block_read(MfPlusData* data, uint8_t index, const MfPlusBlock* block);
// Map an admin key <-> its 0x90xx auth address (single source: mf_plus_admin_key_addresses).
uint16_t mf_plus_get_admin_key_address(MfPlusAdminKeyType type);
bool mf_plus_admin_key_type_from_address(uint16_t address, MfPlusAdminKeyType* type);
// Serialize / parse the SL3 payload (data format version, signature, blocks, keys, config).
// Save writes the payload after the identity metadata; load tolerates legacy dumps that have
// no "Data format version" key (returns true, leaving the SL3 fields zeroed).
+2 -35
View File
@@ -59,22 +59,7 @@ static bool
}
MfPlusAdminKeyType admin_type;
switch(key_id) {
case 0x9000:
admin_type = MfPlusAdminKeyCardMaster;
break;
case 0x9001:
admin_type = MfPlusAdminKeyCardConfig;
break;
case 0x9003:
admin_type = MfPlusAdminKeyL3Switch;
break;
case 0x9004:
admin_type = MfPlusAdminKeySL1CardAuth;
break;
default:
return false;
}
if(!mf_plus_admin_key_type_from_address(key_id, &admin_type)) return false;
if(!mf_plus_is_admin_key_found(data, admin_type)) return false;
*key = data->admin_key[admin_type];
return true;
@@ -291,25 +276,7 @@ static bool mf_plus_listener_store_write(
}
MfPlusAdminKeyType admin_type;
bool is_admin = true;
switch(block_addr) {
case 0x9000:
admin_type = MfPlusAdminKeyCardMaster;
break;
case 0x9001:
admin_type = MfPlusAdminKeyCardConfig;
break;
case 0x9003:
admin_type = MfPlusAdminKeyL3Switch;
break;
case 0x9004:
admin_type = MfPlusAdminKeySL1CardAuth;
break;
default:
is_admin = false;
break;
}
if(is_admin) {
if(mf_plus_admin_key_type_from_address(block_addr, &admin_type)) {
MfPlusKey key;
memcpy(key.data, plain, MF_PLUS_KEY_SIZE);
mf_plus_set_admin_key_found(data, admin_type, &key);
+4 -10
View File
@@ -151,15 +151,6 @@ static NfcCommand mf_plus_poller_handler_read_success(MfPlusPoller* instance) {
return command;
}
// Auth addresses for the admin keys we recover, indexed by MfPlusAdminKeyType. The enum skips
// 0x9002 (SL2 switch), so this is not simply 0x9000 + index.
static const uint16_t mf_plus_admin_key_id[MfPlusAdminKeyNum] = {
[MfPlusAdminKeyCardMaster] = 0x9000,
[MfPlusAdminKeyCardConfig] = 0x9001,
[MfPlusAdminKeyL3Switch] = 0x9003,
[MfPlusAdminKeySL1CardAuth] = 0x9004,
};
#define MF_PLUS_CONFIG_BLOCK_HIGH (0xB0) // config blocks live at 0xB000..0xB003
// Advance the read scan to the next (sector, key type): key A -> key B for the same sector, then
@@ -438,7 +429,10 @@ static NfcCommand mf_plus_poller_handler_auth_admin_key(MfPlusPoller* instance)
const MfPlusAdminKeyType admin_type = (MfPlusAdminKeyType)instance->current_admin;
MfPlusError error = mf_plus_poller_authenticate_key_id(
instance, mf_plus_admin_key_id[admin_type], &instance->current_key, &instance->session);
instance,
mf_plus_get_admin_key_address(admin_type),
&instance->current_key,
&instance->session);
if(error == MfPlusErrorNone) {
mf_plus_set_admin_key_found(instance->data, admin_type, &instance->current_key);