From 9579a2f4b989f758832d39e89ba77abb170633bd Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:23:05 -0800 Subject: [PATCH 01/28] change functions using UID and Key to use structs This should have zero effect on an optimized build, as there are effectively zero changes to the instructions. Rather, this provides greater clarity as to the size of buffers being passed to various functions, which improves the long-term maintainability of code, especially when maintained by someone other than the original author. --- client/src/cmdhfsaflok.c | 101 +++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 2758eea56..a0727757f 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "cliparser.h" #include "cmdparser.h" #include "comms.h" @@ -30,7 +31,26 @@ #include "generator.h" #include "cmdhfmf.h" -#define KEY_LENGTH 6 +// MiFARE Classic encoded with SafeLok encoded data +// defining this structure makes it more clear what +// is passed around, and avoids the need to pass lengths +// to many of the functions. + +typedef struct _safelok_mfc_data_t { + uint8_t data[16]; // 8*16 == 128 bits of encoded, packed data + uint8_t checksum; +} safelok_mfc_data_t; +typedef struct _safelock_mfc_uid_t { + uint8_t uid[4]; +} safelok_mfc_uid_t; +typedef struct _safelok_mfc_key_t { + uint8_t key[6]; +} safelok_mfc_key_t; +#define KEY_LENGTH sizeof(safelok_mfc_key_t) + + + + static const uint8_t c_aDecode[256] = { 234, 13, 217, 116, 78, 40, 253, 186, 123, 152, @@ -106,26 +126,26 @@ uint8_t magic_table[192] = { 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0xDA, 0x3E, 0x3F, 0xD6, 0x49, 0xDD, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x58, 0xDD, 0xED, 0x07, 0x8E, 0x3E, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x5C, 0xD0, 0x05, 0xCF, 0xD9, 0x07, - 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x11, 0x8D, 0xD0, 0x01, 0x87, 0xD0 + 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x11, 0x8D, 0xD0, 0x01, 0x87, 0xD0, }; -static const char *level_names[] = { - "Guest Key", // Index 0 - "Connectors", // Index 1 - "Suite", // Index 2 - "Limited Use", // Index 3 - "Failsafe", // Index 4 - "Inhibit", // Index 5 - "Pool/Meeting Master", // Index 6 - "Housekeeping", // Index 7 - "Floor Key", // Index 8 - "Section Key", // Index 9 - "Rooms Master", // Index 10 - "Grand Master", // Index 11 - "Emergency", // Index 12 - "Electronic Lockout", // Index 13 +static const char *level_names[16] = { + "Guest Key", // Index 0 + "Connectors", // Index 1 + "Suite", // Index 2 + "Limited Use", // Index 3 + "Failsafe", // Index 4 + "Inhibit", // Index 5 + "Pool/Meeting Master", // Index 6 + "Housekeeping", // Index 7 + "Floor Key", // Index 8 + "Section Key", // Index 9 + "Rooms Master", // Index 10 + "Grand Master", // Index 11 + "Emergency", // Index 12 + "Electronic Lockout", // Index 13 "Secondary Programming Key (SPK)", // Index 14 - "Primary Programming Key (PPK)", // Index 15 + "Primary Programming Key (PPK)", // Index 15 }; @@ -279,21 +299,21 @@ static uint8_t saflok_checksum(unsigned char *data, int length) { return sum & 0xFF; } -static void saflok_kdf(const uint8_t *uid, uint8_t *key_out) { +static void saflok_kdf(const safelok_mfc_uid_t *uid, safelok_mfc_key_t *key_out) { - uint8_t magic_byte = (uid[3] >> 4) + (uid[2] >> 4) + (uid[0] & 0x0F); + uint8_t magic_byte = (uid->uid[3] >> 4) + (uid->uid[2] >> 4) + (uid->uid[0] & 0x0F); uint8_t magickal_index = (magic_byte & 0x0F) * 12 + 11; uint8_t carry_sum = 0; - uint8_t key[KEY_LENGTH] = {magic_byte, uid[0], uid[1], uid[2], uid[3], magic_byte}; + safelok_mfc_key_t key = {{magic_byte, uid->uid[0], uid->uid[1], uid->uid[2], uid->uid[3], magic_byte}}; for (int i = KEY_LENGTH - 1; i >= 0; i--, magickal_index--) { - uint16_t keysum = key[i] + magic_table[magickal_index]; - key[i] = (keysum & 0xFF) + carry_sum; + uint16_t keysum = key.key[i] + magic_table[magickal_index]; + key.key[i] = (keysum & 0xFF) + carry_sum; carry_sum = keysum >> 8; } - memcpy(key_out, key, KEY_LENGTH); + memcpy(key_out, &key, KEY_LENGTH); } static void saflok_decode(uint8_t *data) { @@ -826,9 +846,12 @@ static int CmdHFSaflokProvision(const char *Cmd) { return PM3_ESOFT; } - uint8_t keyA[6]; - saflok_kdf(uid, keyA); - PrintAndLogEx(INFO, "Generated UID-derived key: " _GREEN_("%s"), bytes_to_hex(keyA, 6)); + _Static_assert(ARRAYLEN(uid) >= sizeof(safelok_mfc_uid_t), "UID array too small"); + const safelok_mfc_uid_t * saflok_uid = (const safelok_mfc_uid_t *)uid; + safelok_mfc_key_t keyA = {0}; + + saflok_kdf(saflok_uid, &keyA); + PrintAndLogEx(INFO, "Generated UID-derived key: " _GREEN_("%s"), bytes_to_hex(keyA.key, ARRAYLEN(keyA.key))); uint8_t all_F[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t block1[16]; @@ -840,20 +863,21 @@ static int CmdHFSaflokProvision(const char *Cmd) { block2[3] = 0x00; block2[4] = 0x01; - bool write_success = mf_write_block(1, 0, keyA, block1) == PM3_SUCCESS && - mf_write_block(2, 0, keyA, block2) == PM3_SUCCESS; + bool write_success = mf_write_block(1, 0, keyA.key, block1) == PM3_SUCCESS && + mf_write_block(2, 0, keyA.key, block2) == PM3_SUCCESS; uint8_t trailer0[16] = {0}; uint8_t set_keys = 0; if (!write_success) { PrintAndLogEx(WARNING, "Initial write failed. Attempting to set sector 0 keys..."); - memcpy(trailer0, keyA, 6); + _Static_assert(sizeof(safelok_mfc_key_t) == 6u, "safelok_mfc_key_t size changed?"); + memcpy(trailer0, &keyA, sizeof(safelok_mfc_key_t)); trailer0[6] = 0xFF; trailer0[7] = 0x07; trailer0[8] = 0x80; trailer0[9] = 0x69; - memcpy(trailer0 + 10, all_F, 6); + memset(trailer0 + 10, 0xFF, sizeof(safelok_mfc_key_t)); if (mf_write_block(3, 1, all_F, trailer0) != PM3_SUCCESS) { PrintAndLogEx(WARNING, "Failed to set key in sector 0. Try wiping the card first."); @@ -861,8 +885,8 @@ static int CmdHFSaflokProvision(const char *Cmd) { return PM3_ESOFT; } - write_success = mf_write_block(1, 0, keyA, block1) == PM3_SUCCESS && - mf_write_block(2, 0, keyA, block2) == PM3_SUCCESS; + write_success = mf_write_block(1, 0, keyA.key, block1) == PM3_SUCCESS && + mf_write_block(2, 0, keyA.key, block2) == PM3_SUCCESS; if (!write_success) { PrintAndLogEx(WARNING, "Write still failed after setting keys."); CLIParserFree(ctx); @@ -915,11 +939,14 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { return PM3_ESOFT; } - uint8_t key[6]; - saflok_kdf(uid, key); + _Static_assert(ARRAYLEN(uid) >= sizeof(safelok_mfc_uid_t), "UID array too small"); + const safelok_mfc_uid_t * saflok_uid = (const safelok_mfc_uid_t *)uid; + + safelok_mfc_key_t key; + saflok_kdf(saflok_uid, &key); uint8_t block2[16]; - if (mf_read_block(2, 0, key, block2) != PM3_SUCCESS) { + if (mf_read_block(2, 0, key.key, block2) != PM3_SUCCESS) { PrintAndLogEx(WARNING, "Failed to read block 2 with derived key."); CLIParserFree(ctx); return PM3_ESOFT; @@ -945,7 +972,7 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { continue; } - if (mf_read_block(current_block, 0, key, buffer + total_bytes) != PM3_SUCCESS) { + if (mf_read_block(current_block, 0, key.key, buffer + total_bytes) != PM3_SUCCESS) { PrintAndLogEx(WARNING, "Failed to read block %d", current_block); break; } From 72cf97c622bec5b8d875fbf78385b6aa978806fe Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:30:23 -0800 Subject: [PATCH 02/28] prevent buffer overrun ... assert with clear message instead. --- client/src/cmdhfsaflok.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index a0727757f..4813093fd 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -246,8 +246,16 @@ static void insert_bits(uint8_t *data, size_t start_bit, size_t num_bits, uint32 } } +// Static analysis hint: reads all bytes of `data[len]` +// * NOT THREAD SAFE ... uses a static buffer for result, +// so multiple calls will overwrite previous results. +// This style of design SPAWNS BUGS. +// NOTE: apparently accepted for this legacy codebase. static char *bytes_to_hex(const uint8_t *data, size_t len) { - static char buf[256]; + static char buf[256]; // WARNING: caller must immediately use or copy the result, and it's still not thread-safe! + // BUGBUG: previously had no bounds checking on len! + assert(len < (ARRAYLEN(buf)/2)); + len = (len < (ARRAYLEN(buf)/2)) ? len : (ARRAYLEN(buf)/2) - 1; // prevent buffer overflow for (size_t i = 0; i < len; i++) { sprintf(buf + (i * 2), "%02X", data[i]); } From 5fc6f5275a8757b0cd1d93fb3dae8ac652853a16 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:45:31 -0800 Subject: [PATCH 03/28] More type safety for encrypt/decrypt --- client/src/cmdhfsaflok.c | 218 ++++++++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 83 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 4813093fd..16cd0486f 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -36,20 +36,43 @@ // is passed around, and avoids the need to pass lengths // to many of the functions. -typedef struct _safelok_mfc_data_t { - uint8_t data[16]; // 8*16 == 128 bits of encoded, packed data - uint8_t checksum; -} safelok_mfc_data_t; -typedef struct _safelock_mfc_uid_t { +// TODO: consider separate structs for encrypted vs. decrypted +// for even greater type safety. +typedef struct _saflok_mfc_data_t { + uint8_t raw[17]; +} saflok_mfc_data_t; +typedef struct _saflock_mfc_uid_t { uint8_t uid[4]; -} safelok_mfc_uid_t; -typedef struct _safelok_mfc_key_t { +} saflok_mfc_uid_t; +typedef struct _saflok_mfc_key_t { uint8_t key[6]; -} safelok_mfc_key_t; -#define KEY_LENGTH sizeof(safelok_mfc_key_t) +} saflok_mfc_key_t; +#define KEY_LENGTH sizeof(saflok_mfc_key_t) +// TODO: define inline helper functions to get/set named fields from saflok_mfc_data_t +// Example: +// inline uint32_t get_saflok_mfc_card_level(saflok_mfc_data_t *data) { +// return extract_bits(data->raw, 0, 4); +// } +// inline void set_saflok_mfc_card_level(saflok_mfc_data_t *data, uint32_t level) { +// insert_bits(data->raw, 0, 4, level); +// } +// insert_bits(data, 0, 4, card_level ); +// insert_bits(data, 4, 4, card_type ); +// insert_bits(data, 8, 8, card_id ); +// insert_bits(data, 16, 2, opening_key ); +// insert_bits(data, 18, 14, lock_id ); +// insert_bits(data, 32, 12, pass_number ); +// insert_bits(data, 44, 12, sequence_and_combination); +// insert_bits(data, 56, 1, deadbolt_override ); +// insert_bits(data, 57, 7, restricted_days ); +// insert_bits(data, 64, 24, expire_date ); +// insert_bits(data, 88, 28, card_creation_date ); +// insert_bits(data, 116, 12, property_id ); +// +// And checksum as simple byte read/write of raw[16]... static const uint8_t c_aDecode[256] = { @@ -151,77 +174,89 @@ static const char *level_names[16] = { static int CmdHelp(const char *Cmd); -static void saflok_decrypt(uint8_t *strCard, int length, uint8_t *decryptedCard) { +// unsafe without static analysis hints. +// strCard : uint8_t[length] +// decryptedCard : uint8_t[length] +// length : ALWAYS == 17 +// safelok_mfc_data_t* for both parameters? +static void saflok_decrypt(const saflok_mfc_data_t *strCard, saflok_mfc_data_t *decryptedCard) { + + static const size_t length = ARRAYLEN(strCard->raw); for (int i = 0; i < length; i++) { - int num = c_aDecode[strCard[i]] - (i + 1); + int num = c_aDecode[strCard->raw[i]] - (i + 1); if (num < 0) { num += 256; } - decryptedCard[i] = num; + decryptedCard->raw[i] = num; } int b = 0; int b2 = 0; - if (length == 17) { - b = decryptedCard[10]; + if (length == 17) { // True for saflok_mfc_data_t + b = decryptedCard->raw[10]; b2 = b & 1; } for (int num2 = length; num2 > 0; num2--) { - b = decryptedCard[num2 - 1]; + b = decryptedCard->raw[num2 - 1]; for (int num3 = 8; num3 > 0; num3--) { int num4 = num2 + num3; if (num4 > length) { num4 -= length; } - int b3 = decryptedCard[num4 - 1]; + int b3 = decryptedCard->raw[num4 - 1]; int b4 = (b3 & 0x80) >> 7; b3 = ((b3 << 1) & 0xFF) | b2; b2 = (b & 0x80) >> 7; b = ((b << 1) & 0xFF) | b4; - decryptedCard[num4 - 1] = b3; + decryptedCard->raw[num4 - 1] = b3; } - decryptedCard[num2 - 1] = b; + decryptedCard->raw[num2 - 1] = b; } } -static void saflok_encrypt(uint8_t *keyCard, int length, uint8_t *encryptedCard) { +static void saflok_encrypt(const saflok_mfc_data_t *keyCard, saflok_mfc_data_t *encryptedCard) { + static const size_t length = ARRAYLEN(keyCard->raw); int b = 0; - memcpy(encryptedCard, keyCard, length); + memcpy(encryptedCard->raw, keyCard->raw, length); for (int i = 0; i < length; i++) { - int b2 = encryptedCard[i]; + int b2 = encryptedCard->raw[i]; int num2 = i; for (int j = 0; j < 8; j++) { num2 += 1; if (num2 >= length) { num2 -= length; } - int b3 = encryptedCard[num2]; + int b3 = encryptedCard->raw[num2]; int b4 = b2 & 1; b2 = (b2 >> 1) | (b << 7); b = b3 & 1; b3 = (b3 >> 1) | (b4 << 7); - encryptedCard[num2] = b3; + encryptedCard->raw[num2] = b3; } - encryptedCard[i] = b2; + encryptedCard->raw[i] = b2; } if (length == 17) { - int b2 = encryptedCard[10]; + int b2 = encryptedCard->raw[10]; b2 |= b; - encryptedCard[10] = b2; + encryptedCard->raw[10] = b2; } for (int i = 0; i < length; i++) { - int j = encryptedCard[i] + (i + 1); + int j = encryptedCard->raw[i] + (i + 1); if (j > 255) { j -= 256; } - encryptedCard[i] = c_aEncode[j]; + encryptedCard->raw[i] = c_aEncode[j]; } } +// unsafe without static analysis hints. +// Will access data[] at all indices from: +// floor(start_bit/8) .. ceil((start_bit + num_bits - 1)/8) +// safelok_mfc_data_t* for data parameter? static uint32_t extract_bits(const uint8_t *data, size_t start_bit, size_t num_bits) { uint32_t result = 0; for (size_t i = 0; i < num_bits; i++) { @@ -234,6 +269,13 @@ static uint32_t extract_bits(const uint8_t *data, size_t start_bit, size_t num_b return result; } +// unsafe without static analysis hints. +// Will access data[] at all indices from: +// floor(start_bit/8) .. ceil((start_bit + num_bits - 1)/8) +// A maximum of 32 bits can be inserted by this function. +// reject values that exceed the size of safelok_mfc_data_t->data[] +// safelok_mfc_data_t* for data parameter? + static void insert_bits(uint8_t *data, size_t start_bit, size_t num_bits, uint32_t value) { for (size_t i = 0; i < num_bits; i++) { size_t current_bit = start_bit + i; @@ -264,6 +306,7 @@ static char *bytes_to_hex(const uint8_t *data, size_t len) { } +// safelok_mfc_data_t* for data parameter? static int pack_datetime_expr(char *exp_datetime, uint8_t *data) { int year, month, day, hour, minute; @@ -279,6 +322,7 @@ static int pack_datetime_expr(char *exp_datetime, uint8_t *data) { return 0; } +// safelok_mfc_data_t* for data parameter? static int pack_datetime(char *datetime_str, uint8_t *data) { int year, month, day, hour, minute; @@ -297,7 +341,11 @@ static int pack_datetime(char *datetime_str, uint8_t *data) { return 0; } - +// unsafe without static analysis hints. +// Will access all bytes of `data[len]` +// length is ALWAYS 16 for safelock_mfc_data_t ... +// safelok_mfc_data_t* for data parameter, and only access data[0..15]? +// BUGBUG -- first parameter should point to 'const' data? static uint8_t saflok_checksum(unsigned char *data, int length) { int sum = 0; for (int i = 0; i < length; i++) { @@ -307,13 +355,13 @@ static uint8_t saflok_checksum(unsigned char *data, int length) { return sum & 0xFF; } -static void saflok_kdf(const safelok_mfc_uid_t *uid, safelok_mfc_key_t *key_out) { +static void saflok_kdf(const saflok_mfc_uid_t *uid, saflok_mfc_key_t *key_out) { uint8_t magic_byte = (uid->uid[3] >> 4) + (uid->uid[2] >> 4) + (uid->uid[0] & 0x0F); uint8_t magickal_index = (magic_byte & 0x0F) * 12 + 11; uint8_t carry_sum = 0; - safelok_mfc_key_t key = {{magic_byte, uid->uid[0], uid->uid[1], uid->uid[2], uid->uid[3], magic_byte}}; + saflok_mfc_key_t key = {{magic_byte, uid->uid[0], uid->uid[1], uid->uid[2], uid->uid[3], magic_byte}}; for (int i = KEY_LENGTH - 1; i >= 0; i--, magickal_index--) { uint16_t keysum = key.key[i] + magic_table[magickal_index]; @@ -324,6 +372,7 @@ static void saflok_kdf(const safelok_mfc_uid_t *uid, safelok_mfc_key_t *key_out) memcpy(key_out, &key, KEY_LENGTH); } +// unsafe! data : struct containing uint8[17] static void saflok_decode(uint8_t *data) { uint32_t card_level = extract_bits(data, 0, 4); @@ -502,15 +551,16 @@ static int CmdHFSaflokRead(const char *Cmd) { return PM3_EFAILED; } - uint8_t saflokdata[17]; - saflok_read_sector(0, secdata); - saflok_decrypt(secdata + 16, 17, saflokdata); + saflok_mfc_data_t decrypted[17]; + saflok_read_sector(0, secdata); // 64 bytes + const saflok_mfc_data_t * encrypted = (const saflok_mfc_data_t *)(secdata + 16); + saflok_decrypt(encrypted, decrypted); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Card Information")); - PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(secdata + 16, 17)); + PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(encrypted->raw, sizeof(saflok_mfc_data_t))); - saflok_decode(saflokdata); + saflok_decode(decrypted->raw); return PM3_SUCCESS; } @@ -542,8 +592,8 @@ static int CmdHFSaflokEncode(const char *Cmd) { CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t rsaflokdata[17] = {0}; - uint8_t esaflokdata[17] = {0}; + saflok_mfc_data_t decrypted = {0}; + saflok_mfc_data_t encrypted = {0}; int slen = 0; char dt[100]; @@ -553,7 +603,7 @@ static int CmdHFSaflokEncode(const char *Cmd) { CLIParamStrToBuf(arg_get_str(ctx, 10), (uint8_t *)dt_e, 100, &slen); - saflok_encode(rsaflokdata, + saflok_encode(decrypted.raw, arg_get_u32_def(ctx, 1, 0), arg_get_u32_def(ctx, 2, 0), arg_get_u32_def(ctx, 3, 0), @@ -569,11 +619,11 @@ static int CmdHFSaflokEncode(const char *Cmd) { dt_e, dt); - saflok_encrypt(rsaflokdata, 17, esaflokdata); + saflok_encrypt(&decrypted, &encrypted); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Encoded Card Data")); - PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(esaflokdata, 17)); + PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(encrypted.raw, 17)); CLIParserFree(ctx); @@ -593,10 +643,10 @@ static int CmdHFSaflokDecode(const char *Cmd) { }; CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t saflokdata[17]; - uint8_t dsaflokdata[17]; + saflok_mfc_data_t encrypted; + saflok_mfc_data_t decrypted; int dlen; - CLIGetHexWithReturn(ctx, 1, saflokdata, &dlen); + CLIGetHexWithReturn(ctx, 1, encrypted.raw, &dlen); CLIParserFree(ctx); @@ -605,8 +655,8 @@ static int CmdHFSaflokDecode(const char *Cmd) { return PM3_EINVARG; } - saflok_decrypt(saflokdata, 17, dsaflokdata); - saflok_decode(dsaflokdata); + saflok_decrypt(&encrypted, &decrypted); + saflok_decode(decrypted.raw); return PM3_SUCCESS; } @@ -640,54 +690,54 @@ static int CmdHFSaflokModify(const char *Cmd) { CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t user_saflokdata[17]; - uint8_t rsaflokdata[17]; - uint8_t esaflokdata[17] = {0}; + saflok_mfc_data_t encrypted; + saflok_mfc_data_t decrypted; + saflok_mfc_data_t reencrypted = {0}; int dlen; - CLIGetHexWithReturn(ctx, 13, user_saflokdata, &dlen); + CLIGetHexWithReturn(ctx, 13, encrypted.raw, &dlen); if (dlen != 17) { PrintAndLogEx(WARNING, "block data must include 17 HEX bytes. Got %i", dlen); return PM3_EINVARG; } - saflok_decrypt(user_saflokdata, 17, rsaflokdata); + saflok_decrypt(&encrypted, &decrypted); - uint32_t card_level = extract_bits(rsaflokdata, 0, 4); + uint32_t card_level = extract_bits(decrypted.raw, 0, 4); card_level = arg_get_u32_def(ctx, 1, card_level); - uint32_t card_type = extract_bits(rsaflokdata, 4, 4); + uint32_t card_type = extract_bits(decrypted.raw, 4, 4); card_type = arg_get_u32_def(ctx, 2, card_type); - uint32_t card_id = extract_bits(rsaflokdata, 8, 8); + uint32_t card_id = extract_bits(decrypted.raw, 8, 8); card_id = arg_get_u32_def(ctx, 3, card_id); - uint32_t opening_key = extract_bits(rsaflokdata, 16, 2); + uint32_t opening_key = extract_bits(decrypted.raw, 16, 2); opening_key = arg_get_u32_def(ctx, 4, opening_key); - uint32_t lock_id = extract_bits(rsaflokdata, 18, 14); + uint32_t lock_id = extract_bits(decrypted.raw, 18, 14); lock_id = arg_get_u32_def(ctx, 5, lock_id); - uint32_t pass_number = extract_bits(rsaflokdata, 32, 12); + uint32_t pass_number = extract_bits(decrypted.raw, 32, 12); pass_number = arg_get_u32_def(ctx, 6, pass_number); - uint32_t sequence_and_combination = extract_bits(rsaflokdata, 44, 12); + uint32_t sequence_and_combination = extract_bits(decrypted.raw, 44, 12); sequence_and_combination = arg_get_u32_def(ctx, 7, sequence_and_combination); - uint32_t deadbolt_override = extract_bits(rsaflokdata, 56, 1); + uint32_t deadbolt_override = extract_bits(decrypted.raw, 56, 1); deadbolt_override = arg_get_u32_def(ctx, 8, deadbolt_override); - uint32_t restricted_days = extract_bits(rsaflokdata, 57, 7); + uint32_t restricted_days = extract_bits(decrypted.raw, 57, 7); restricted_days = arg_get_u32_def(ctx, 9, restricted_days); - uint32_t expire_date = extract_bits(rsaflokdata, 64, 24); + uint32_t expire_date = extract_bits(decrypted.raw, 64, 24); //expire_date = arg_get_u32_def(ctx, 10, expire_date); - uint32_t card_creation_date = extract_bits(rsaflokdata, 88, 28); + uint32_t card_creation_date = extract_bits(decrypted.raw, 88, 28); //card_creation_date = arg_get_u32_def(ctx, 11, card_creation_date); - uint32_t property_id = extract_bits(rsaflokdata, 116, 12); + uint32_t property_id = extract_bits(decrypted.raw, 116, 12); property_id = arg_get_u32_def(ctx, 12, property_id); int slen = 0; @@ -698,7 +748,7 @@ static int CmdHFSaflokModify(const char *Cmd) { char dt_e[100]; CLIParamStrToBuf(arg_get_str(ctx, 10), (uint8_t *)dt_e, 100, &slen2); - saflok_encode(rsaflokdata, + saflok_encode(decrypted.raw, card_level, card_type, card_id, @@ -715,11 +765,11 @@ static int CmdHFSaflokModify(const char *Cmd) { dt); - saflok_encrypt(rsaflokdata, 17, esaflokdata); + saflok_encrypt(&decrypted, &reencrypted); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Modified Card Data")); - PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(esaflokdata, 17)); + PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(reencrypted.raw, 17)); CLIParserFree(ctx); @@ -741,9 +791,10 @@ static int CmdHFSaflokEncrypt(const char *Cmd) { CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t raw[17], encrypted[17]; + saflok_mfc_data_t raw = {0}; + saflok_mfc_data_t encrypted = {0}; int len; - CLIGetHexWithReturn(ctx, 1, raw, &len); + CLIGetHexWithReturn(ctx, 1, raw.raw, &len); if (len != 17) { PrintAndLogEx(WARNING, "Expected 17 bytes. Got %d.", len); @@ -751,8 +802,8 @@ static int CmdHFSaflokEncrypt(const char *Cmd) { return PM3_EINVARG; } - saflok_encrypt(raw, 17, encrypted); - PrintAndLogEx(SUCCESS, "Encrypted: " _GREEN_("%s"), bytes_to_hex(encrypted, 17)); + saflok_encrypt(&raw, &encrypted); + PrintAndLogEx(SUCCESS, "Encrypted: " _GREEN_("%s"), bytes_to_hex(encrypted.raw, 17)); CLIParserFree(ctx); return PM3_SUCCESS; @@ -772,9 +823,10 @@ static int CmdHFSaflokDecrypt(const char *Cmd) { CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t encrypted[17], decrypted[17]; + saflok_mfc_data_t encrypted = {0}; + saflok_mfc_data_t decrypted = {0}; int len; - CLIGetHexWithReturn(ctx, 1, encrypted, &len); + CLIGetHexWithReturn(ctx, 1, encrypted.raw, &len); if (len != 17) { PrintAndLogEx(WARNING, "Expected 17 bytes. Got %d.", len); @@ -782,8 +834,8 @@ static int CmdHFSaflokDecrypt(const char *Cmd) { return PM3_EINVARG; } - saflok_decrypt(encrypted, 17, decrypted); - PrintAndLogEx(SUCCESS, "Decrypted: " _GREEN_("%s"), bytes_to_hex(decrypted, 17)); + saflok_decrypt(&encrypted, &decrypted); + PrintAndLogEx(SUCCESS, "Decrypted: " _GREEN_("%s"), bytes_to_hex(decrypted.raw, 17)); CLIParserFree(ctx); return PM3_SUCCESS; @@ -854,9 +906,9 @@ static int CmdHFSaflokProvision(const char *Cmd) { return PM3_ESOFT; } - _Static_assert(ARRAYLEN(uid) >= sizeof(safelok_mfc_uid_t), "UID array too small"); - const safelok_mfc_uid_t * saflok_uid = (const safelok_mfc_uid_t *)uid; - safelok_mfc_key_t keyA = {0}; + _Static_assert(ARRAYLEN(uid) >= sizeof(saflok_mfc_uid_t), "UID array too small"); + const saflok_mfc_uid_t * saflok_uid = (const saflok_mfc_uid_t *)uid; + saflok_mfc_key_t keyA = {0}; saflok_kdf(saflok_uid, &keyA); PrintAndLogEx(INFO, "Generated UID-derived key: " _GREEN_("%s"), bytes_to_hex(keyA.key, ARRAYLEN(keyA.key))); @@ -879,13 +931,13 @@ static int CmdHFSaflokProvision(const char *Cmd) { if (!write_success) { PrintAndLogEx(WARNING, "Initial write failed. Attempting to set sector 0 keys..."); - _Static_assert(sizeof(safelok_mfc_key_t) == 6u, "safelok_mfc_key_t size changed?"); - memcpy(trailer0, &keyA, sizeof(safelok_mfc_key_t)); + _Static_assert(sizeof(saflok_mfc_key_t) == 6u, "saflok_mfc_key_t size changed?"); + memcpy(trailer0, &keyA, sizeof(saflok_mfc_key_t)); trailer0[6] = 0xFF; trailer0[7] = 0x07; trailer0[8] = 0x80; trailer0[9] = 0x69; - memset(trailer0 + 10, 0xFF, sizeof(safelok_mfc_key_t)); + memset(trailer0 + 10, 0xFF, sizeof(saflok_mfc_key_t)); if (mf_write_block(3, 1, all_F, trailer0) != PM3_SUCCESS) { PrintAndLogEx(WARNING, "Failed to set key in sector 0. Try wiping the card first."); @@ -947,10 +999,10 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { return PM3_ESOFT; } - _Static_assert(ARRAYLEN(uid) >= sizeof(safelok_mfc_uid_t), "UID array too small"); - const safelok_mfc_uid_t * saflok_uid = (const safelok_mfc_uid_t *)uid; + _Static_assert(ARRAYLEN(uid) >= sizeof(saflok_mfc_uid_t), "UID array too small"); + const saflok_mfc_uid_t * saflok_uid = (const saflok_mfc_uid_t *)uid; - safelok_mfc_key_t key; + saflok_mfc_key_t key; saflok_kdf(saflok_uid, &key); uint8_t block2[16]; From c60c15e184064b1306aff2dc3171c2ffe4d78db6 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:59:13 -0800 Subject: [PATCH 04/28] self-document `expr` --> `expiration` --- client/src/cmdhfsaflok.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 16cd0486f..a42871a6a 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -307,10 +307,10 @@ static char *bytes_to_hex(const uint8_t *data, size_t len) { // safelok_mfc_data_t* for data parameter? -static int pack_datetime_expr(char *exp_datetime, uint8_t *data) { +static int pack_datetime_expr(char *expiration_datetime, uint8_t *data) { int year, month, day, hour, minute; - if (sscanf(exp_datetime, "%4d-%2d-%2dT%2d:%2d", + if (sscanf(expiration_datetime, "%4d-%2d-%2dT%2d:%2d", &year, &month, &day, &hour, &minute) != 5) { return -1; } From 4ce34c6c1e13c033b046a252ae55a7489e8d23c8 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:42:06 -0800 Subject: [PATCH 05/28] More type and bufferlen safety, and validation in bit manipulation routines. Again, these should have zero impact on resulting instructions. At the same time, this will enable static analysis to more accurately validate a buffer (here `saflock_mfc_data_t`) points to sufficiently large allocated memory. Additional validation for `extract_bits()` and `insert_bits()` will prevent accidental corruptions (and assert, if assertions are enabled). These changes also makes the code easier to follow. --- client/src/cmdhfsaflok.c | 204 +++++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 82 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index a42871a6a..7b89fcdc3 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -253,30 +253,59 @@ static void saflok_encrypt(const saflok_mfc_data_t *keyCard, saflok_mfc_data_t * } } -// unsafe without static analysis hints. -// Will access data[] at all indices from: -// floor(start_bit/8) .. ceil((start_bit + num_bits - 1)/8) -// safelok_mfc_data_t* for data parameter? -static uint32_t extract_bits(const uint8_t *data, size_t start_bit, size_t num_bits) { +static uint32_t extract_bits(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits) { + static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; + if (start_bit >= total_available_bits) { + // Out of bounds access + assert(!"extract_bits: Out of bounds access (start_bit)"); + return 0; + } + if (num_bits > 32) { + // Exceeds maximum supported bit extraction + assert(!"extract_bits: num_bits exceeds 32"); + return 0; + } + if (total_available_bits - start_bit < num_bits) { + // Out of bounds access + assert(!"extract_bits: Out of bounds access (num_bits)"); + return 0; + } + uint32_t result = 0; for (size_t i = 0; i < num_bits; i++) { size_t byte_index = (start_bit + i) / 8; size_t bit_index = (start_bit + i) % 8; - if (data[byte_index] & (1 << (7 - bit_index))) { + if (data->raw[byte_index] & (1 << (7 - bit_index))) { result |= (1ULL << (num_bits - 1 - i)); } } return result; } -// unsafe without static analysis hints. -// Will access data[] at all indices from: -// floor(start_bit/8) .. ceil((start_bit + num_bits - 1)/8) -// A maximum of 32 bits can be inserted by this function. -// reject values that exceed the size of safelok_mfc_data_t->data[] -// safelok_mfc_data_t* for data parameter? +static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value) { + static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; + if (start_bit >= total_available_bits) { + // Out of bounds access + assert(!"insert_bits: Out of bounds access (start_bit)"); + return; + } + if (num_bits > 32) { + // Exceeds maximum supported bit extraction + assert(!"insert_bits: num_bits exceeds 32"); + return; + } + if (total_available_bits - start_bit < num_bits) { + // Out of bounds access + assert(!"insert_bits: Out of bounds access (num_bits)"); + return; + } + if ((num_bits < 32) && ((value >> num_bits) != 0)) { + // Value exceeds the size of the specified bit field + assert(!"insert_bits: value exceeds bit field size"); + return; + } + -static void insert_bits(uint8_t *data, size_t start_bit, size_t num_bits, uint32_t value) { for (size_t i = 0; i < num_bits; i++) { size_t current_bit = start_bit + i; size_t byte_index = current_bit / 8; @@ -284,7 +313,7 @@ static void insert_bits(uint8_t *data, size_t start_bit, size_t num_bits, uint32 uint32_t bit_value = (value >> (num_bits - 1 - i)) & 1U; - data[byte_index] = (data[byte_index] & ~(1 << bit_index)) | (bit_value << bit_index); + data->raw[byte_index] = (data->raw[byte_index] & ~(1 << bit_index)) | (bit_value << bit_index); } } @@ -306,8 +335,7 @@ static char *bytes_to_hex(const uint8_t *data, size_t len) { } -// safelok_mfc_data_t* for data parameter? -static int pack_datetime_expr(char *expiration_datetime, uint8_t *data) { +static int pack_datetime_expr(char *expiration_datetime, saflok_mfc_data_t *data) { int year, month, day, hour, minute; if (sscanf(expiration_datetime, "%4d-%2d-%2dT%2d:%2d", @@ -315,15 +343,14 @@ static int pack_datetime_expr(char *expiration_datetime, uint8_t *data) { return -1; } - data[8] = ((year & 0x0F) << 4) | (month & 0x0F); - data[9] = ((day & 0x1F) << 3) | ((hour & 0x1C) >> 2); - data[10] = ((hour & 0x03) << 6) | (minute & 0x3F); + data->raw[8] = ((year & 0x0F) << 4) | (month & 0x0F); + data->raw[9] = ((day & 0x1F) << 3) | ((hour & 0x1C) >> 2); + data->raw[10] = ((hour & 0x03) << 6) | (minute & 0x3F); return 0; } -// safelok_mfc_data_t* for data parameter? -static int pack_datetime(char *datetime_str, uint8_t *data) { +static int pack_datetime(char *datetime_str, saflok_mfc_data_t *data) { int year, month, day, hour, minute; if (sscanf(datetime_str, "%4d-%2d-%2dT%2d:%2d", @@ -333,23 +360,19 @@ static int pack_datetime(char *datetime_str, uint8_t *data) { uint8_t year_offset = year - 1980; - data[11] = ((year_offset & 0x0F) << 4) | (month & 0x0F); - data[12] = ((day & 0x1F) << 3) | ((hour & 0x1C) >> 2); - data[13] = ((hour & 0x03) << 6) | (minute & 0x3F); - data[14] = (data[14] & 0x0F) | ((year_offset & 0x70) << 0); + data->raw[11] = ((year_offset & 0x0F) << 4) | (month & 0x0F); + data->raw[12] = ((day & 0x1F) << 3) | ((hour & 0x1C) >> 2); + data->raw[13] = ((hour & 0x03) << 6) | (minute & 0x3F); + data->raw[14] = (data->raw[14] & 0x0F) | ((year_offset & 0x70) << 0); return 0; } -// unsafe without static analysis hints. -// Will access all bytes of `data[len]` -// length is ALWAYS 16 for safelock_mfc_data_t ... -// safelok_mfc_data_t* for data parameter, and only access data[0..15]? -// BUGBUG -- first parameter should point to 'const' data? -static uint8_t saflok_checksum(unsigned char *data, int length) { +static uint8_t calculated_saflok_checksum(const saflok_mfc_data_t *data) { + _Static_assert(ARRAYLEN(data->raw) == 17, "saflok_mfc_data_t raw size must be 17 bytes"); int sum = 0; - for (int i = 0; i < length; i++) { - sum += data[i]; + for (int i = 0; i < 16; i++) { + sum += data->raw[i]; } sum = 255 - (sum & 0xFF); return sum & 0xFF; @@ -372,8 +395,7 @@ static void saflok_kdf(const saflok_mfc_uid_t *uid, saflok_mfc_key_t *key_out) { memcpy(key_out, &key, KEY_LENGTH); } -// unsafe! data : struct containing uint8[17] -static void saflok_decode(uint8_t *data) { +static void saflok_decode(const saflok_mfc_data_t *data) { uint32_t card_level = extract_bits(data, 0, 4); uint32_t card_type = extract_bits(data, 4, 4); @@ -390,19 +412,19 @@ static void saflok_decode(uint8_t *data) { uint32_t checksum = extract_bits(data, 128, 8); //date parsing, stolen from flipper code - uint16_t interval_year = (data[8] >> 4); - uint8_t interval_month = data[8] & 0x0F; - uint8_t interval_day = (data[9] >> 3) & 0x1F; - uint8_t interval_hour = ((data[9] & 0x07) << 2) | (data[10] >> 6); - uint8_t interval_minute = data[10] & 0x3F; + uint16_t interval_year = (data->raw[8] >> 4); + uint8_t interval_month = data->raw[8] & 0x0F; + uint8_t interval_day = (data->raw[9] >> 3) & 0x1F; + uint8_t interval_hour = ((data->raw[9] & 0x07) << 2) | (data->raw[10] >> 6); + uint8_t interval_minute = data->raw[10] & 0x3F; - uint8_t creation_year_bits = (data[14] & 0xF0); + uint8_t creation_year_bits = (data->raw[14] & 0xF0); uint16_t creation_year = - (creation_year_bits | ((data[11] & 0xF0) >> 4)) + 1980; - uint8_t creation_month = data[11] & 0x0F; - uint8_t creation_day = (data[12] >> 3) & 0x1F; - uint8_t creation_hour = ((data[12] & 0x07) << 2) | (data[13] >> 6); - uint8_t creation_minute = data[13] & 0x3F; + (creation_year_bits | ((data->raw[11] & 0xF0) >> 4)) + 1980; + uint8_t creation_month = data->raw[11] & 0x0F; + uint8_t creation_day = (data->raw[12] >> 3) & 0x1F; + uint8_t creation_hour = ((data->raw[12] & 0x07) << 2) | (data->raw[13] >> 6); + uint8_t creation_minute = data->raw[13] & 0x3F; uint16_t expire_year = creation_year + interval_year; uint8_t expire_month = creation_month + interval_month; @@ -458,16 +480,28 @@ static void saflok_decode(uint8_t *data) { expire_hour, expire_minute); PrintAndLogEx(SUCCESS, "Property ID: " _GREEN_("%u"), property_id); - PrintAndLogEx(SUCCESS, "Checksum: " _GREEN_("0x%X") " (%s)", checksum, (checksum == saflok_checksum(data, 16)) ? _GREEN_("ok") : _RED_("bad")); + PrintAndLogEx(SUCCESS, "Checksum: " _GREEN_("0x%X") " (%s)", checksum, (checksum == calculated_saflok_checksum(data)) ? _GREEN_("ok") : _RED_("bad")); PrintAndLogEx(NORMAL, ""); } -static void saflok_encode(uint8_t *data, uint32_t card_level, uint32_t card_type, uint32_t card_id, - uint32_t opening_key, uint32_t lock_id, uint32_t pass_number, - uint32_t sequence_and_combination, uint32_t deadbolt_override, - uint32_t restricted_days, uint32_t expire_date, uint32_t card_creation_date, - uint32_t property_id, char *dt_e, char *dt) { +static void saflok_encode( + saflok_mfc_data_t *data, + uint32_t card_level, + uint32_t card_type, + uint32_t card_id, + uint32_t opening_key, + uint32_t lock_id, + uint32_t pass_number, + uint32_t sequence_and_combination, + uint32_t deadbolt_override, + uint32_t restricted_days, + uint32_t expire_date, + uint32_t card_creation_date, + uint32_t property_id, + char *dt_expiration, + char *dt + ) { insert_bits(data, 0, 4, card_level); insert_bits(data, 4, 4, card_type); insert_bits(data, 8, 8, card_id); @@ -483,6 +517,7 @@ static void saflok_encode(uint8_t *data, uint32_t card_level, uint32_t card_type int year, month, day, hour, minute; + // BUGBUG -- sscanf() occurs twice: once here, and once in pack_datetime if (sscanf(dt, "%4d-%2d-%2dT%2d:%2d", &year, &month, &day, &hour, &minute) == 5) { pack_datetime(dt, data); @@ -492,20 +527,23 @@ static void saflok_encode(uint8_t *data, uint32_t card_level, uint32_t card_type //PrintAndLogEx(SUCCESS, "DT BITS INSERTED"); //} - if (sscanf(dt_e, "%4d-%2d-%2dT%2d:%2d", + // BUGBUG -- sscanf() occurs twice: once here, and once in pack_datetime_expr + if (sscanf(dt_expiration, "%4d-%2d-%2dT%2d:%2d", &year, &month, &day, &hour, &minute) == 5) { - pack_datetime_expr(dt_e, data); + pack_datetime_expr(dt_expiration, data); } //else{ //insert_bits(data, 64, 24, expire_date); //PrintAndLogEx(SUCCESS, "DTE BITS INSERTED"); //} - uint8_t checksum = saflok_checksum(data, 16); + uint8_t checksum = calculated_saflok_checksum(data); insert_bits(data, 128, 8, checksum); } +// TODO: make clearer how many bytes secdata must minimally point to, +// perhaps by creating a struct to avoid having to pass a length parameter. static int saflok_read_sector(int sector, uint8_t *secdata) { clearCommandBuffer(); SendCommandMIX(CMD_HF_ISO14443A_READER, ISO14A_CONNECT, 0, 0, NULL, 0); @@ -551,16 +589,17 @@ static int CmdHFSaflokRead(const char *Cmd) { return PM3_EFAILED; } - saflok_mfc_data_t decrypted[17]; saflok_read_sector(0, secdata); // 64 bytes + const saflok_mfc_data_t * encrypted = (const saflok_mfc_data_t *)(secdata + 16); - saflok_decrypt(encrypted, decrypted); + saflok_mfc_data_t decrypted = {0}; + saflok_decrypt(encrypted, &decrypted); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Card Information")); PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(encrypted->raw, sizeof(saflok_mfc_data_t))); - saflok_decode(decrypted->raw); + saflok_decode(&decrypted); return PM3_SUCCESS; } @@ -603,7 +642,7 @@ static int CmdHFSaflokEncode(const char *Cmd) { CLIParamStrToBuf(arg_get_str(ctx, 10), (uint8_t *)dt_e, 100, &slen); - saflok_encode(decrypted.raw, + saflok_encode(&decrypted, arg_get_u32_def(ctx, 1, 0), arg_get_u32_def(ctx, 2, 0), arg_get_u32_def(ctx, 3, 0), @@ -656,7 +695,7 @@ static int CmdHFSaflokDecode(const char *Cmd) { } saflok_decrypt(&encrypted, &decrypted); - saflok_decode(decrypted.raw); + saflok_decode(&decrypted); return PM3_SUCCESS; } @@ -704,40 +743,40 @@ static int CmdHFSaflokModify(const char *Cmd) { saflok_decrypt(&encrypted, &decrypted); - uint32_t card_level = extract_bits(decrypted.raw, 0, 4); + uint32_t card_level = extract_bits(&decrypted, 0, 4); card_level = arg_get_u32_def(ctx, 1, card_level); - uint32_t card_type = extract_bits(decrypted.raw, 4, 4); + uint32_t card_type = extract_bits(&decrypted, 4, 4); card_type = arg_get_u32_def(ctx, 2, card_type); - uint32_t card_id = extract_bits(decrypted.raw, 8, 8); + uint32_t card_id = extract_bits(&decrypted, 8, 8); card_id = arg_get_u32_def(ctx, 3, card_id); - uint32_t opening_key = extract_bits(decrypted.raw, 16, 2); + uint32_t opening_key = extract_bits(&decrypted, 16, 2); opening_key = arg_get_u32_def(ctx, 4, opening_key); - uint32_t lock_id = extract_bits(decrypted.raw, 18, 14); + uint32_t lock_id = extract_bits(&decrypted, 18, 14); lock_id = arg_get_u32_def(ctx, 5, lock_id); - uint32_t pass_number = extract_bits(decrypted.raw, 32, 12); + uint32_t pass_number = extract_bits(&decrypted, 32, 12); pass_number = arg_get_u32_def(ctx, 6, pass_number); - uint32_t sequence_and_combination = extract_bits(decrypted.raw, 44, 12); + uint32_t sequence_and_combination = extract_bits(&decrypted, 44, 12); sequence_and_combination = arg_get_u32_def(ctx, 7, sequence_and_combination); - uint32_t deadbolt_override = extract_bits(decrypted.raw, 56, 1); + uint32_t deadbolt_override = extract_bits(&decrypted, 56, 1); deadbolt_override = arg_get_u32_def(ctx, 8, deadbolt_override); - uint32_t restricted_days = extract_bits(decrypted.raw, 57, 7); + uint32_t restricted_days = extract_bits(&decrypted, 57, 7); restricted_days = arg_get_u32_def(ctx, 9, restricted_days); - uint32_t expire_date = extract_bits(decrypted.raw, 64, 24); + uint32_t expire_date = extract_bits(&decrypted, 64, 24); //expire_date = arg_get_u32_def(ctx, 10, expire_date); - uint32_t card_creation_date = extract_bits(decrypted.raw, 88, 28); + uint32_t card_creation_date = extract_bits(&decrypted, 88, 28); //card_creation_date = arg_get_u32_def(ctx, 11, card_creation_date); - uint32_t property_id = extract_bits(decrypted.raw, 116, 12); + uint32_t property_id = extract_bits(&decrypted, 116, 12); property_id = arg_get_u32_def(ctx, 12, property_id); int slen = 0; @@ -748,7 +787,7 @@ static int CmdHFSaflokModify(const char *Cmd) { char dt_e[100]; CLIParamStrToBuf(arg_get_str(ctx, 10), (uint8_t *)dt_e, 100, &slen2); - saflok_encode(decrypted.raw, + saflok_encode(&decrypted, card_level, card_type, card_id, @@ -855,9 +894,9 @@ static int CmdHFSaflokChecksum(const char *Cmd) { CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t data[17]; + saflok_mfc_data_t data = {0}; int len; - CLIGetHexWithReturn(ctx, 1, data, &len); + CLIGetHexWithReturn(ctx, 1, data.raw, &len); if (len != 16) { PrintAndLogEx(WARNING, "Expected 16 bytes. Got %d.", len); @@ -865,10 +904,10 @@ static int CmdHFSaflokChecksum(const char *Cmd) { return PM3_EINVARG; } - data[16] = saflok_checksum(data, 16); + data.raw[16] = calculated_saflok_checksum(&data); - PrintAndLogEx(SUCCESS, "Block + checksum: " _GREEN_("%s"), bytes_to_hex(data, 17)); - PrintAndLogEx(SUCCESS, "Checksum byte: " _GREEN_("0x%02X"), data[16]); + PrintAndLogEx(SUCCESS, "Block + checksum: " _GREEN_("%s"), bytes_to_hex(data.raw, 17)); + PrintAndLogEx(SUCCESS, "Checksum byte: " _GREEN_("0x%02X"), data.raw[16]); CLIParserFree(ctx); return PM3_SUCCESS; @@ -888,9 +927,9 @@ static int CmdHFSaflokProvision(const char *Cmd) { CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t data[17]; + saflok_mfc_data_t data = {0}; int len; - CLIGetHexWithReturn(ctx, 1, data, &len); + CLIGetHexWithReturn(ctx, 1, data.raw, &len); if (len != 17) { PrintAndLogEx(WARNING, "Expected 17 bytes, got %d", len); @@ -898,6 +937,7 @@ static int CmdHFSaflokProvision(const char *Cmd) { return PM3_EINVARG; } + // buffer size for UID is eight bytes ... even though saflok only uses first four bytes uint8_t uid[8]; int uid_len; if (mf_read_uid(uid, &uid_len, NULL) != PM3_SUCCESS || uid_len < 4) { @@ -916,8 +956,8 @@ static int CmdHFSaflokProvision(const char *Cmd) { uint8_t all_F[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t block1[16]; uint8_t block2[16] = {0}; - memcpy(block1, data, 16); - block2[0] = data[16]; + memcpy(block1, data.raw, 16); + block2[0] = data.raw[16]; block2[1] = 0x00; block2[2] = 0x04; block2[3] = 0x00; From 319339b4cd19f6b80841ef3310e0f652fd8eb430 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:28:18 -0800 Subject: [PATCH 06/28] UNTESTED: Add lots of helper functions to get data into / out of saflok formats * Small, testable functions * `set_` functions return boolean so they can indicate out-of-bounds values and similar failures. * const-correctness for all helper functions Note: The new functions are NOT used extensively yet ... they still need testing. But they should make the code more self-documenting / readable (if somewhat verbose). --- client/src/cmdhfsaflok.c | 599 ++++++++++++++++++++++++++++++++++----- 1 file changed, 526 insertions(+), 73 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 7b89fcdc3..2ac26b445 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include "cliparser.h" #include "cmdparser.h" #include "comms.h" @@ -35,6 +37,7 @@ // defining this structure makes it more clear what // is passed around, and avoids the need to pass lengths // to many of the functions. +static const uint8_t days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // TODO: consider separate structs for encrypted vs. decrypted // for even greater type safety. @@ -47,35 +50,514 @@ typedef struct _saflock_mfc_uid_t { typedef struct _saflok_mfc_key_t { uint8_t key[6]; } saflok_mfc_key_t; +typedef struct _saflok_mfc_datetime_t { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; +} saflok_mfc_datetime_t; +typedef struct _saflok_mfc_datetime_offset_t { + uint8_t years; + uint8_t months; + uint8_t days; + uint8_t hours; + uint8_t minutes; +} saflok_mfc_datetime_offset_t; + #define KEY_LENGTH sizeof(saflok_mfc_key_t) - -// TODO: define inline helper functions to get/set named fields from saflok_mfc_data_t -// Example: -// inline uint32_t get_saflok_mfc_card_level(saflok_mfc_data_t *data) { -// return extract_bits(data->raw, 0, 4); -// } -// inline void set_saflok_mfc_card_level(saflok_mfc_data_t *data, uint32_t level) { -// insert_bits(data->raw, 0, 4, level); -// } - -// insert_bits(data, 0, 4, card_level ); -// insert_bits(data, 4, 4, card_type ); -// insert_bits(data, 8, 8, card_id ); -// insert_bits(data, 16, 2, opening_key ); -// insert_bits(data, 18, 14, lock_id ); -// insert_bits(data, 32, 12, pass_number ); -// insert_bits(data, 44, 12, sequence_and_combination); -// insert_bits(data, 56, 1, deadbolt_override ); -// insert_bits(data, 57, 7, restricted_days ); -// insert_bits(data, 64, 24, expire_date ); -// insert_bits(data, 88, 28, card_creation_date ); -// insert_bits(data, 116, 12, property_id ); -// -// And checksum as simple byte read/write of raw[16]... +static uint32_t extract_bits(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits); +static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value); -static const uint8_t c_aDecode[256] = { + +#if 1 // getters and setters for each bitfield in saflok_mfc_data_t + // TODO: consider update to setter functions to use smaller type for data? + // avoiding for this commit, as larger change. + // + // card_level field is 4 bits + // card_type field is 4 bits + // card_id field is 8 bits + // opening_key field is 2 bits + // lock_id field is 14 bits + // pass_number field is 12 bits + // sequence_and_combination field is 12 bits + // deadbolt_override field is 1 bits + // restricted_days field is 7 bits + // raw_expire_date field is 24 bits -- special set processing needed + // raw_card_creation_date field is 28 bits -- special set processing needed + // property_id field is 12 bits + + static inline uint8_t get_saflok_mfc_card_level(const saflok_mfc_data_t *data) { + return (uint8_t)extract_bits(data, 0, 4); + } + static inline bool set_saflok_mfc_card_level(saflok_mfc_data_t *data, uint32_t card_level) { + if (card_level > 0xFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " card_level out of range (%08x)\n", card_level); + return false; + } + insert_bits(data, 0, 4, card_level); + return true; + } + static inline uint8_t get_saflok_mfc_card_type(const saflok_mfc_data_t *data) { + return (uint8_t)extract_bits(data, 4, 4); + } + static inline bool set_saflok_mfc_card_type(saflok_mfc_data_t *data, uint32_t card_type) { + if (card_type > 0xFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " card_type out of range (%08x)\n", card_type); + return false; + } + insert_bits(data, 4, 4, card_type); + return true; + } + static inline uint8_t get_saflok_mfc_card_id(const saflok_mfc_data_t *data) { + return (uint8_t)extract_bits(data, 8, 8); + } + static inline bool set_saflok_mfc_card_id(saflok_mfc_data_t *data, uint32_t card_id) { + if (card_id > 0xFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " card_id out of range (%08x)\n", card_id); + return false; + } + insert_bits(data, 8, 8, card_id); + return true; + } + static inline uint8_t get_saflok_mfc_opening_key(const saflok_mfc_data_t *data) { + return (uint8_t)extract_bits(data, 16, 2); + } + static inline bool set_saflok_mfc_opening_key(saflok_mfc_data_t *data, uint32_t opening_key) { + if (opening_key > 0x3u) { + PrintAndLogEx(ERR, _RED_("ERROR:") " opening_key out of range (%08x)\n", opening_key); + return false; + } + insert_bits(data, 16, 2, opening_key); + return true; + } + static inline uint16_t get_saflok_mfc_lock_id(const saflok_mfc_data_t *data) { + return (uint16_t)extract_bits(data, 18, 14); + } + static inline bool set_saflok_mfc_lock_id(saflok_mfc_data_t *data, uint32_t lock_id) { + if (lock_id > 0x3FFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " lock_id out of range (%08x)\n", lock_id); + return false; + } + insert_bits(data, 18, 14, lock_id); + return true; + } + static inline uint16_t get_saflok_mfc_pass_number(const saflok_mfc_data_t *data) { + return (uint16_t)extract_bits(data, 32, 12); + } + static inline bool set_saflok_mfc_pass_number(saflok_mfc_data_t *data, uint32_t pass_number) { + if (pass_number > 0xFFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " pass_number out of range (%08x)\n", pass_number); + return false; + } + insert_bits(data, 32, 12, pass_number); + return true; + } + static inline uint16_t get_saflok_mfc_sequence_and_combination(const saflok_mfc_data_t *data) { + return (uint16_t)extract_bits(data, 44, 12); + } + static inline bool set_saflok_mfc_sequence_and_combination(saflok_mfc_data_t *data, uint32_t sequence_and_combination) { + if (sequence_and_combination > 0xFFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " sequence_and_combination out of range (%08x)\n", sequence_and_combination); + return false; + } + insert_bits(data, 44, 12, sequence_and_combination); + return true; + } + static inline bool get_saflok_mfc_deadbolt_override(const saflok_mfc_data_t *data) { + uint32_t result = extract_bits(data, 56, 1); + return (result != 0); + } + static inline bool set_saflok_mfc_deadbolt_override(saflok_mfc_data_t *data, uint32_t deadbolt_override) { + insert_bits(data, 56, 1, deadbolt_override ? 0x1 : 0x0); + return true; + } + static inline uint8_t get_saflok_mfc_restricted_days(const saflok_mfc_data_t *data) { + return (uint8_t)extract_bits(data, 57, 7); + } + static inline bool set_saflok_mfc_restricted_days(saflok_mfc_data_t *data, uint32_t restricted_days) { + if (restricted_days > 0x7Fu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " restricted_days out of range (%08x)\n", restricted_days); + return false; + } + insert_bits(data, 57, 7, restricted_days); + return true; + } + static inline uint32_t get_saflok_mfc_raw_interval_date(const saflok_mfc_data_t *data) { + return extract_bits(data, 64, 24); + } + static inline bool set_saflok_mfc_raw_interval_date(saflok_mfc_data_t *data, uint32_t raw_interval_date) { + if (raw_interval_date > 0xFFFFFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval_date out of range (%08x)\n", raw_interval_date); + return false; + } + insert_bits(data, 64, 24, raw_interval_date); + return true; + } + static inline uint32_t get_saflok_mfc_raw_card_creation_date(const saflok_mfc_data_t *data) { + return extract_bits(data, 88, 28); + } + static inline bool set_saflok_mfc_raw_card_creation_date(saflok_mfc_data_t *data, uint32_t raw_card_creation_date) { + if (raw_card_creation_date > 0x0FFFFFFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " raw_card_creation_date out of range (%08x)\n", raw_card_creation_date); + return false; + } + insert_bits(data, 88, 28, raw_card_creation_date); + return true; + } + static inline uint16_t get_saflok_mfc_property_id(const saflok_mfc_data_t *data) { + return extract_bits(data, 116, 12); + } + static inline bool set_saflok_mfc_property_id(saflok_mfc_data_t *data, uint32_t property_id) { + if (property_id > 0xFFFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " property_id out of range (%08x)\n", property_id); + return false; + } + insert_bits(data, 116, 12, property_id); + return true; + } +#endif // getters and setters for each bitfield in saflok_mfc_data_t + +#if 1 // helpers for get/set_saflok_mfc_card_creation_date() ... do not call these directly as does not validate... + static inline uint16_t _get_saflok_mfc_card_creation_year_impl(const saflok_mfc_data_t *data) { + uint8_t creation_year_bits_high = (data->raw[14] & 0xF0u); + uint8_t creation_year_bits_low = (data->raw[11] & 0xF0u) >> 4; + uint8_t creation_year_bits = creation_year_bits_high | creation_year_bits_low; + uint16_t creation_year = 1980u + creation_year_bits; // automatically extends to uint16_t + return creation_year; + } + static inline bool _set_saflok_mfc_card_creation_year_impl(saflok_mfc_data_t *data, uint16_t year) { + if (year < 1980u || year > (1980u+0xFFu)) { + PrintAndLogEx(ERR, _RED_("ERROR:") " year out of range (%u)\n", year); + return false; + } + uint16_t year_bits = year - 1980u; + uint8_t creation_year_bits_high = year_bits & 0xF0u; + uint8_t creation_year_bits_low = (year_bits & 0x0Fu) << 4; + data->raw[14] = (data->raw[14] & 0x0Fu) | creation_year_bits_high; + data->raw[11] = (data->raw[11] & 0x0Fu) | creation_year_bits_low; + return true; + } + static inline uint8_t _get_saflok_mfc_card_creation_month_impl(const saflok_mfc_data_t *data) { + // should be in range [1..12] ... value of zero is invalid + return (data->raw[11] & 0x0Fu); + } + static inline bool _set_saflok_mfc_card_creation_month_impl(saflok_mfc_data_t *data, uint8_t month) { + if (month < 1 || month > 12) { + PrintAndLogEx(ERR, _RED_("ERROR:") " month out of range (%u)\n", month); + return false; + } + data->raw[11] = (data->raw[11] & 0xF0u) | (month & 0x0Fu); + return true; + } + static inline uint8_t _get_saflok_mfc_card_creation_day_impl(const saflok_mfc_data_t *data) { + return ((data->raw[12] >> 3) & 0x1Fu); + } + static inline bool _set_saflok_mfc_card_creation_day_impl(saflok_mfc_data_t *data, uint8_t day) { + data->raw[12] = (data->raw[12] & 0xE0u) | ((day & 0x1Fu) << 3); + return true; + } + static inline uint8_t _get_saflok_mfc_card_creation_hour_impl(const saflok_mfc_data_t *data) { + // low three bits from raw[12] + // high two bits from raw[13] + return (((data->raw[12] & 0x07u) << 2) | (data->raw[13] >> 6)); + } + static inline bool _set_saflok_mfc_card_creation_hour_impl(saflok_mfc_data_t *data, uint8_t hour) { + data->raw[12] = (data->raw[12] & 0xF8u) | ((hour >> 2) & 0x07u); + data->raw[13] = (data->raw[13] & 0x3Fu) | ((hour & 0x03u) << 6); + return true; + } + static inline uint8_t _get_saflok_mfc_card_creation_minute_impl(const saflok_mfc_data_t *data) { + return (data->raw[13] & 0x3Fu); + } + static inline bool _set_saflok_mfc_card_creation_minute_impl(saflok_mfc_data_t *data, uint8_t minute) { + data->raw[13] = (data->raw[13] & 0xC0u) | (minute & 0x3Fu); + return true; + } +#endif // // helpers for get/set_saflok_mfc_card_creation_date() ... do not call these directly as does not validate... +#if 1 // getters and setters for creation date + + static inline bool is_saflok_mfc_datetime_valid(const saflok_mfc_datetime_t *date) { + bool result = true; + if ((date->year < 1980) || (date->year > (1980u+0xFFu)) ) { + PrintAndLogEx(WARNING, "year out of range (%d)\n", date->year); + result = false; + } + if ((date->month < 1) || (date->month > 12)) { + PrintAndLogEx(WARNING, "month out of range (%d)\n", date->month); + result = false; + } + uint8_t max_days = days_in_month_lookup[date->month]; + // Adjust for leap years + if ((date->month == 2u) && + (date->year % 4u == 0u) && + ((date->year % 100u != 0u) || (date->year % 400u == 0u)) + ) { + max_days = 29u; + } + if ((date->day < 1u) || (date->day > max_days)) { + PrintAndLogEx(WARNING, "day out of range (%d) for month %d year %d\n", date->day, date->month, date->year); + result = false; + } + if (date->hour > 23) { + PrintAndLogEx(WARNING, "hour out of range (%d)\n", date->hour); + result = false; + } + if (date->minute > 59) { + PrintAndLogEx(WARNING, "minute out of range (%d)\n", date->minute); + result = false; + } + return result; + } + static inline saflok_mfc_datetime_t get_saflok_mfc_card_creation_datetime( + const saflok_mfc_data_t *data + ) { + saflok_mfc_datetime_t date = { + .year = _get_saflok_mfc_card_creation_year_impl(data), + .month = _get_saflok_mfc_card_creation_month_impl(data), + .day = _get_saflok_mfc_card_creation_day_impl(data), + .hour = _get_saflok_mfc_card_creation_hour_impl(data), + .minute = _get_saflok_mfc_card_creation_minute_impl(data) + }; + if (!is_saflok_mfc_datetime_valid(&date)) { + PrintAndLogEx(WARNING, "Warning: creation date appears invalid: %04d-%02d-%02dT%02d:%02d\n", + date.year, date.month, date.day, date.hour, date.minute + ); + } + return date; + } + static inline bool set_saflok_mfc_card_creation_datetime(saflok_mfc_data_t * data, const saflok_mfc_datetime_t * date) { + if (!is_saflok_mfc_datetime_valid(date)) { + PrintAndLogEx(ERR, _RED_("ERROR:") " creation date is invalid\n"); + return false; + } + bool result = + _set_saflok_mfc_card_creation_year_impl(data, date->year) && + _set_saflok_mfc_card_creation_month_impl(data, date->month) && + _set_saflok_mfc_card_creation_day_impl(data, date->day) && + _set_saflok_mfc_card_creation_hour_impl(data, date->hour) && + _set_saflok_mfc_card_creation_minute_impl(data, date->minute); + if (!result) { + PrintAndLogEx(ERR, _RED_("ERROR:") " failed to set creation date of %04d-%02d-%02dT%02d:%02d\n", + date->year, date->month, date->day, date->hour, date->minute + ); + } + return result; + } +#endif // getters and setters for creation date components +#if 1 // helpers for get/set_saflok_mfc_expiration_date() + static inline uint8_t _get_saflok_mfc_interval_years_impl(const saflok_mfc_data_t *data) { + // supports up to 15 year intervals(!) + return (data->raw[8] >> 4); + } + static inline bool _set_saflok_mfc_interval_years_impl(saflok_mfc_data_t *data, uint32_t years) { + if (years > 0xFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " interval years out of range (%u)\n", years); + return false; + } + data->raw[8] = (data->raw[8] & 0x0Fu) | ((years & 0xFFu) << 4); + return true; + } + static inline uint8_t _get_saflok_mfc_interval_months_impl(const saflok_mfc_data_t *data) { + uint8_t months = data->raw[8] & 0x0Fu; + if (months >= 12u) { + PrintAndLogEx(WARNING, "Warning: interval months appears invalid (%u)\n", months); + } + return months; + } + static inline bool _set_saflok_mfc_interval_months_impl(saflok_mfc_data_t *data, uint8_t months) { + if (months >= 12u) { + PrintAndLogEx(ERR, _RED_("ERROR:") " interval months out of range (%u)\n", months); + return false; + } + data->raw[8] = (data->raw[8] & 0xF0u) | (months & 0x0Fu); + return true; + } + static inline uint8_t _get_saflok_mfc_interval_days_impl(const saflok_mfc_data_t *data) { + uint8_t days = (data->raw[9] >> 3) & 0x1Fu; + if (days > 31u) { + PrintAndLogEx(WARNING, "Warning: interval days appears invalid (%u)\n", days); + } + return days; + } + static inline bool _set_saflok_mfc_interval_days_impl(saflok_mfc_data_t *data, uint8_t days) { + if (days > 31u) { + PrintAndLogEx(ERR, _RED_("ERROR:") " interval days out of range (%u)\n", days); + return false; + } + data->raw[9] = (data->raw[9] & 0xC7u) | ((days & 0x1Fu) << 3); + return true; + } + static inline uint8_t _get_saflok_mfc_interval_hours_impl(const saflok_mfc_data_t *data) { + // low three bits from raw[9] + // top two bits from raw[10] + uint8_t hours = ((data->raw[9] & 0x07u) << 2) | (data->raw[10] >> 6); + if (hours > 23u) { + PrintAndLogEx(WARNING, "Warning: interval hours appears invalid (%u)\n", hours); + } + return hours; + } + static inline bool _set_saflok_mfc_interval_hours_impl(saflok_mfc_data_t *data, uint8_t hours) { + if (hours > 23u) { + PrintAndLogEx(ERR, _RED_("ERROR:") " interval hours out of range (%u)\n", hours); + return false; + } + // five bit value split across two bytes: + // three MSB go into the low three bits from raw[9] + // two LSB go into the top two bits from raw[10] + data->raw[9] = (data->raw[9] & 0xF8u) | ((hours >> 2) & 0x07u); + data->raw[10] = (data->raw[10] & 0x3Fu) | ((hours & 0x03u) << 6); + return true; + } + static inline uint8_t _get_saflok_mfc_interval_minutes_impl(const saflok_mfc_data_t *data) { + uint8_t minutes = (data->raw[10] & 0x3Fu); + if (minutes > 59u) { + PrintAndLogEx(WARNING, "Warning: interval minutes appears invalid (%u)\n", minutes); + } + return minutes; + } + static inline bool _set_saflok_mfc_interval_minutes_impl(saflok_mfc_data_t *data, uint8_t minutes) { + if (minutes > 59u) { + PrintAndLogEx(ERR, _RED_("ERROR:") " interval minutes out of range (%u)\n", minutes); + return false; + } + data->raw[10] = (data->raw[10] & 0xC0u) | (minutes & 0x3Fu); + return true; + } +#endif // getters for interval (expiration) components +#if 1 // Setting the interval date is based on the creation date. + + static inline saflok_mfc_datetime_offset_t get_saflok_mfc_interval( + const saflok_mfc_data_t *data + ) { + saflok_mfc_datetime_offset_t offset = { + .years = _get_saflok_mfc_interval_years_impl(data), + .months = _get_saflok_mfc_interval_months_impl(data), + .days = _get_saflok_mfc_interval_days_impl(data), + .hours = _get_saflok_mfc_interval_hours_impl(data), + .minutes = _get_saflok_mfc_interval_minutes_impl(data) + }; + return offset; + } + static inline bool set_saflok_mfc_interval( + saflok_mfc_data_t *data, + saflok_mfc_datetime_offset_t offset + ) { + bool result = + _set_saflok_mfc_interval_years_impl(data, offset.years) && + _set_saflok_mfc_interval_months_impl(data, offset.months) && + _set_saflok_mfc_interval_days_impl(data, offset.days) && + _set_saflok_mfc_interval_hours_impl(data, offset.hours) && + _set_saflok_mfc_interval_minutes_impl(data, offset.minutes); + return result; + } + static inline saflok_mfc_datetime_t add_offset( + const saflok_mfc_datetime_t *base, + const saflok_mfc_datetime_offset_t offset + ) { + // adding dates is non-trivial. + // luckily, there's already a standard for this... + struct tm tm = { + .tm_year = base->year - 1900, + .tm_mon = base->month - 1, + .tm_mday = base->day, + .tm_hour = base->hour, + .tm_min = base->minute, + .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute + .tm_isdst = 0, // UTC has no DST + }; + // add the offsets ... and it's OK for them to be out of range + tm.tm_year += offset.years; + tm.tm_mon += offset.months; + tm.tm_mday += offset.days; + tm.tm_hour += offset.hours; + tm.tm_min += offset.minutes; + // normalize into EPOCH ... which allows out-of-bounds values + time_t fixed = timegm(&tm); + // convert EPOCH back into a tm structure + gmtime_r(&fixed, &tm); + // and finally convert back into saflok_mfc_datetime_t + saflok_mfc_datetime_t result = { + .year = tm.tm_year + 1900, + .month = tm.tm_mon + 1, + .day = tm.tm_mday, + .hour = tm.tm_hour, + .minute = tm.tm_min + }; + return result; + } + static inline saflok_mfc_datetime_offset_t get_datetime_offset( + const saflok_mfc_datetime_t *start, + const saflok_mfc_datetime_t *end + ) { + // convert both to `struct tm` + struct tm tm_start = { + .tm_year = start->year - 1900, + .tm_mon = start->month - 1, + .tm_mday = start->day, + .tm_hour = start->hour, + .tm_min = start->minute, + .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute + .tm_isdst = 0, // UTC has no DST + }; + struct tm tm_end = { + .tm_year = end->year - 1900, + .tm_mon = end->month - 1, + .tm_mday = end->day, + .tm_hour = end->hour, + .tm_min = end->minute, + .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute + .tm_isdst = 0, // UTC has no DST + }; + // convert both to EPOCH + time_t epoch_start = timegm(&tm_start); + time_t epoch_end = timegm(&tm_end); + // subtract the epoch values to get delta in seconds + if (epoch_end < epoch_start) { + PrintAndLogEx(ERR, _RED_("ERROR:") " end date is before start date\n"); + saflok_mfc_datetime_offset_t zero_offset = {0}; + return zero_offset; + } + time_t delta = epoch_end - epoch_start; + // convert delta seconds back into `struct tm` + struct tm result_tm = {0}; + gmtime_r(&delta, &result_tm); + saflok_mfc_datetime_offset_t result = { + .years = result_tm.tm_year, + .months = result_tm.tm_mon, + .days = result_tm.tm_mday, + .hours = result_tm.tm_hour, + .minutes = result_tm.tm_min + }; + return result; + } + + static inline saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime( + const saflok_mfc_data_t *data + ) { + saflok_mfc_datetime_t creation_date = get_saflok_mfc_card_creation_datetime(data); + saflok_mfc_datetime_offset_t offset = get_saflok_mfc_interval(data); + saflok_mfc_datetime_t expiration = add_offset(&creation_date, offset); + return expiration; + } + static inline bool set_saflok_mfc_card_expiration_datetime( + saflok_mfc_data_t *data, + const saflok_mfc_datetime_t * expiration + ) { + if (!is_saflok_mfc_datetime_valid(expiration)) { + PrintAndLogEx(ERR, _RED_("ERROR:") " expiration date is invalid\n"); + return false; + } + saflok_mfc_datetime_t creation_datetime = get_saflok_mfc_card_creation_datetime(data); + saflok_mfc_datetime_offset_t offset = get_datetime_offset(&creation_datetime, expiration); + bool result = set_saflok_mfc_interval(data, offset); + return result; + } +#endif // Setting the interval date is based on the creation date. + +static const uint8_t c_aDecode[] = { 234, 13, 217, 116, 78, 40, 253, 186, 123, 152, 135, 120, 221, 141, 181, 26, 14, 48, 243, 47, 106, 59, 172, 9, 185, 32, 110, 91, 43, 182, @@ -103,8 +585,9 @@ static const uint8_t c_aDecode[256] = { 12, 124, 198, 189, 249, 125, 196, 145, 39, 137, 50, 114, 51, 101, 104, 175 }; +_Static_assert(ARRAYLEN(c_aDecode) == 256, "c_aDecode must have 256 elements"); -static const uint8_t c_aEncode[256] = { +static const uint8_t c_aEncode[] = { 236, 116, 192, 99, 86, 153, 105, 100, 159, 23, 38, 198, 240, 1, 16, 77, 202, 82, 138, 75, 122, 175, 173, 32, 115, 162, 15, 194, 80, 120, @@ -132,8 +615,9 @@ static const uint8_t c_aEncode[256] = { 74, 111, 239, 18, 129, 44, 180, 56, 90, 244, 151, 64, 104, 6, 49, 103 }; +_Static_assert(ARRAYLEN(c_aEncode) == 256, "c_aEncode must have 256 elements"); -uint8_t magic_table[192] = { +uint8_t magic_table[] = { 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0xF0, 0x57, 0xB3, 0x9E, 0xE3, 0xD8, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x96, 0x9D, 0x95, 0x4A, 0xC1, 0x57, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x8F, 0x43, 0x58, 0x0D, 0x2C, 0x9D, @@ -151,8 +635,9 @@ uint8_t magic_table[192] = { 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x5C, 0xD0, 0x05, 0xCF, 0xD9, 0x07, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x11, 0x8D, 0xD0, 0x01, 0x87, 0xD0, }; +_Static_assert(ARRAYLEN(magic_table) == 192, "magic_table must have 192 elements"); -static const char *level_names[16] = { +static const char *level_names[] = { "Guest Key", // Index 0 "Connectors", // Index 1 "Suite", // Index 2 @@ -170,6 +655,7 @@ static const char *level_names[16] = { "Secondary Programming Key (SPK)", // Index 14 "Primary Programming Key (PPK)", // Index 15 }; +_Static_assert(ARRAYLEN(level_names) == 16, "level_names must have 16 elements"); static int CmdHelp(const char *Cmd); @@ -334,40 +820,6 @@ static char *bytes_to_hex(const uint8_t *data, size_t len) { return buf; } - -static int pack_datetime_expr(char *expiration_datetime, saflok_mfc_data_t *data) { - int year, month, day, hour, minute; - - if (sscanf(expiration_datetime, "%4d-%2d-%2dT%2d:%2d", - &year, &month, &day, &hour, &minute) != 5) { - return -1; - } - - data->raw[8] = ((year & 0x0F) << 4) | (month & 0x0F); - data->raw[9] = ((day & 0x1F) << 3) | ((hour & 0x1C) >> 2); - data->raw[10] = ((hour & 0x03) << 6) | (minute & 0x3F); - - return 0; -} - -static int pack_datetime(char *datetime_str, saflok_mfc_data_t *data) { - int year, month, day, hour, minute; - - if (sscanf(datetime_str, "%4d-%2d-%2dT%2d:%2d", - &year, &month, &day, &hour, &minute) != 5) { - return -1; - } - - uint8_t year_offset = year - 1980; - - data->raw[11] = ((year_offset & 0x0F) << 4) | (month & 0x0F); - data->raw[12] = ((day & 0x1F) << 3) | ((hour & 0x1C) >> 2); - data->raw[13] = ((hour & 0x03) << 6) | (minute & 0x3F); - data->raw[14] = (data->raw[14] & 0x0F) | ((year_offset & 0x70) << 0); - - return 0; -} - static uint8_t calculated_saflok_checksum(const saflok_mfc_data_t *data) { _Static_assert(ARRAYLEN(data->raw) == 17, "saflok_mfc_data_t raw size must be 17 bytes"); int sum = 0; @@ -439,9 +891,8 @@ static void saflok_decode(const saflok_mfc_data_t *data) { } // Handle day rollover - static const uint8_t days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; while (true) { - uint8_t max_days = days_in_month[expire_month - 1]; + uint8_t max_days = days_in_month_lookup[expire_month]; // Adjust for leap years if (expire_month == 2 && (expire_year % 4 == 0 && (expire_year % 100 != 0 || expire_year % 400 == 0))) { @@ -515,22 +966,24 @@ static void saflok_encode( insert_bits(data, 88, 28, card_creation_date); insert_bits(data, 116, 12, property_id); - int year, month, day, hour, minute; - // BUGBUG -- sscanf() occurs twice: once here, and once in pack_datetime - if (sscanf(dt, "%4d-%2d-%2dT%2d:%2d", - &year, &month, &day, &hour, &minute) == 5) { - pack_datetime(dt, data); + // Parsing date time string from "YYYY-MM-DDTHH:mm" into `saflok_mfc_datetime_t` + static const char * fmt = "%4" SCNu16 "-%2" SCNu8 "-%2" SCNu8 "T%2" SCNu8 ":%2" SCNu8; + + saflok_mfc_datetime_t creation_dt = {0}; + if (sscanf(dt, fmt, &creation_dt.year, &creation_dt.month, &creation_dt.day, &creation_dt.hour, &creation_dt.minute) == 5) { + // return value ignored ... function here is void(!) + set_saflok_mfc_card_creation_datetime(data, &creation_dt); } //else{ //insert_bits(data, 88, 28,card_creation_date); //PrintAndLogEx(SUCCESS, "DT BITS INSERTED"); //} - // BUGBUG -- sscanf() occurs twice: once here, and once in pack_datetime_expr - if (sscanf(dt_expiration, "%4d-%2d-%2dT%2d:%2d", - &year, &month, &day, &hour, &minute) == 5) { - pack_datetime_expr(dt_expiration, data); + saflok_mfc_datetime_t expiration_dt = {0}; + if (sscanf(dt_expiration, fmt, &expiration_dt.year, &expiration_dt.month, &expiration_dt.day, &expiration_dt.hour, &expiration_dt.minute) == 5) { + // return value ignored ... function here is void(!) + set_saflok_mfc_card_expiration_datetime(data, &expiration_dt); } //else{ //insert_bits(data, 64, 24, expire_date); From cb4ba660abb6333ac202271a11410eb9faf5a1a4 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:32:48 -0800 Subject: [PATCH 07/28] framework for saflok selftest --- client/src/cmdhfsaflok.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 2ac26b445..4f242ae19 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1333,6 +1333,29 @@ static int CmdHFSaflokDecrypt(const char *Cmd) { return PM3_SUCCESS; } +static int CmdHFSaflokSelfTest(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf saflok selftest", + "Validate internal functionality of Saflok routines", + "hf saflok selftest"); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + + int result = PM3_SUCCESS; + + PrintAndLogEx(WARNING, "NYI: Saflok self-test not yet implemented."); + + + + return result; +} + + static int CmdHFSaflokChecksum(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf saflok cksum", @@ -1569,6 +1592,7 @@ static command_t CommandTable[] = { {"decrypt", CmdHFSaflokDecrypt, AlwaysAvailable, "Decrypt 17-byte encrypted block"}, {"interrogate", CmdHFSaflokInterrogate, IfPm3NfcBarcode, "Interrogate saflok card"}, {"cksum", CmdHFSaflokChecksum, IfPm3NfcBarcode, "Generate checksum for data block"}, + {"selftest", CmdHFSaflokSelfTest, AlwaysAvailable, "Run self-test"}, {NULL, NULL, NULL, NULL} }; From a535bdb0e7c6c31084e5558c2f3ba9b0a37a621c Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:37:53 -0800 Subject: [PATCH 08/28] list of needed self-test --- client/src/cmdhfsaflok.c | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 4f242ae19..3599dcc56 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1349,6 +1349,67 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { int result = PM3_SUCCESS; PrintAndLogEx(WARNING, "NYI: Saflok self-test not yet implemented."); + // TODO: encode some sample cards using prior versions of the code. + // verify the resulting decoded data matches expectations, + // or investigate which one results in correct data. + +/* +uint8_t get_saflok_mfc_card_level +bool set_saflok_mfc_card_level +uint8_t get_saflok_mfc_card_type +bool set_saflok_mfc_card_type +uint8_t get_saflok_mfc_card_id +bool set_saflok_mfc_card_id +uint8_t get_saflok_mfc_opening_key +bool set_saflok_mfc_opening_key +uint16_t get_saflok_mfc_lock_id +bool set_saflok_mfc_lock_id +uint16_t get_saflok_mfc_pass_number +bool set_saflok_mfc_pass_number +uint16_t get_saflok_mfc_sequence_and_combination +bool set_saflok_mfc_sequence_and_combination +bool get_saflok_mfc_deadbolt_override +bool set_saflok_mfc_deadbolt_override +uint8_t get_saflok_mfc_restricted_days +bool set_saflok_mfc_restricted_days +uint32_t get_saflok_mfc_raw_interval_date +bool set_saflok_mfc_raw_interval_date +uint32_t get_saflok_mfc_raw_card_creation_date +bool set_saflok_mfc_raw_card_creation_date +uint16_t get_saflok_mfc_property_id +bool set_saflok_mfc_property_id +uint16_t _get_saflok_mfc_card_creation_year_impl +bool _set_saflok_mfc_card_creation_year_impl +uint8_t _get_saflok_mfc_card_creation_month_impl +bool _set_saflok_mfc_card_creation_month_impl +uint8_t _get_saflok_mfc_card_creation_day_impl +bool _set_saflok_mfc_card_creation_day_impl +uint8_t _get_saflok_mfc_card_creation_hour_impl +bool _set_saflok_mfc_card_creation_hour_impl +uint8_t _get_saflok_mfc_card_creation_minute_impl +bool _set_saflok_mfc_card_creation_minute_impl +bool is_saflok_mfc_datetime_valid +saflok_mfc_datetime_t get_saflok_mfc_card_creation_datetime +bool set_saflok_mfc_card_creation_datetime +uint8_t _get_saflok_mfc_interval_years_impl +bool _set_saflok_mfc_interval_years_impl +uint8_t _get_saflok_mfc_interval_months_impl +bool _set_saflok_mfc_interval_months_impl +uint8_t _get_saflok_mfc_interval_days_impl +bool _set_saflok_mfc_interval_days_impl +uint8_t _get_saflok_mfc_interval_hours_impl +bool _set_saflok_mfc_interval_hours_impl +uint8_t _get_saflok_mfc_interval_minutes_impl +bool _set_saflok_mfc_interval_minutes_impl +saflok_mfc_datetime_offset_t get_saflok_mfc_interval +bool set_saflok_mfc_interval +saflok_mfc_datetime_t add_offset +saflok_mfc_datetime_offset_t get_datetime_offset +saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime +bool set_saflok_mfc_card_expiration_datetime +*/ + + From 1cfeb7900345b7a74de728d23c2df9ecd0879843 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:06:41 -0800 Subject: [PATCH 09/28] automated from `make style` --- client/src/cmdhf14b.c | 8 +++---- client/src/cmdhfmf.c | 2 +- client/src/pm3line_vocabulary.h | 5 ++--- doc/commands.json | 40 +++++++++++++-------------------- doc/commands.md | 11 +-------- 5 files changed, 23 insertions(+), 43 deletions(-) diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 9d22777f1..ea7df2569 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -2631,15 +2631,15 @@ int CmdHF14BNdefRead(const char *Cmd) { } // take offset from response uint8_t offset = response[1]; - + // Parse CC data uint8_t cc_data[resplen - 2]; memcpy(cc_data, response, sizeof(cc_data)); uint8_t file_id[2] = {cc_data[9], cc_data[10]}; - + uint16_t max_rapdu_size = (cc_data[3] << 8 | cc_data[4]) - 2; - + max_rapdu_size = max_rapdu_size < sizeof(response) - 2 ? max_rapdu_size : sizeof(response) - 2; // --------------- NDEF file reading ---------------- uint8_t aSELECT_FILE_NDEF[30]; @@ -2739,7 +2739,7 @@ int CmdHF14BNdefRead(const char *Cmd) { PrintAndLogEx(HINT, "Hint: Try " _YELLOW_("`hf 14b ndefread -v`") " for more details"); // So far this prints absolutely nothing } - + // get total NDEF length before save. If fails, we save it all size_t n = 0; if (NDEFGetTotalLength(response + 2, resplen - 4, &n) != PM3_SUCCESS) diff --git a/client/src/cmdhfmf.c b/client/src/cmdhfmf.c index c6d6c7694..c7ffab56c 100644 --- a/client/src/cmdhfmf.c +++ b/client/src/cmdhfmf.c @@ -5008,7 +5008,7 @@ void printKeyTableEx(size_t sectorscnt, sector_t *e_sector, uint8_t start_sector _YELLOW_("H") ":Hardnested / " _YELLOW_("C") ":statiCnested / " _YELLOW_("A") ":keyA " - " )" + " )" ); if (sectorscnt == 18) { PrintAndLogEx(INFO, "( " _MAGENTA_("*") " ) These sectors used for signature. Lays outside of user memory"); diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 2c894eced..9d99a6486 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -359,7 +359,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mf fchk" }, { 1, "hf mf decrypt" }, { 0, "hf mf supercard" }, - { 1, "hf mf bambukeys" }, + { 1, "hf mf keygen" }, { 0, "hf mf auth4" }, { 1, "hf mf acl" }, { 0, "hf mf dump" }, @@ -511,6 +511,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf saflok decrypt" }, { 0, "hf saflok interrogate" }, { 0, "hf saflok cksum" }, + { 1, "hf saflok selftest" }, { 1, "hf seos help" }, { 1, "hf seos list" }, { 0, "hf seos sam" }, @@ -550,8 +551,6 @@ const static vocabulary_t vocabulary[] = { { 1, "hf vas help" }, { 0, "hf vas reader" }, { 1, "hf vas decrypt" }, - { 1, "hf waveshare help" }, - { 1, "hf waveshare load" }, { 1, "hf xerox help" }, { 1, "hf xerox list" }, { 0, "hf xerox info" }, diff --git a/doc/commands.json b/doc/commands.json index ec0c14f48..07263b760 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -3256,7 +3256,7 @@ }, "hf help": { "command": "hf help", - "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", "notes": [ "hf list --frame -> show frame delay times", "hf list -1 -> use trace buffer" @@ -7815,7 +7815,7 @@ }, "hf saflok help": { "command": "hf saflok help", - "description": "help This help encode Encode Saflok card data decode Decode Saflok card data modify Modify Saflok card data encrypt Encrypt 17-byte decrypted block decrypt Decrypt 17-byte encrypted block --------------------------------------------------------------------------------------- hf saflok read available offline: no Read Saflok card (MIFARE Classic only)", + "description": "help This help encode Encode Saflok card data decode Decode Saflok card data modify Modify Saflok card data encrypt Encrypt 17-byte decrypted block decrypt Decrypt 17-byte encrypted block selftest Run self-test --------------------------------------------------------------------------------------- hf saflok read available offline: no Read Saflok card (MIFARE Classic only)", "notes": [ "hf saflok read" ], @@ -7875,6 +7875,18 @@ ], "usage": "hf saflok provision [-h] -d data" }, + "hf saflok selftest": { + "command": "hf saflok selftest", + "description": "Validate internal functionality of Saflok routines", + "notes": [ + "hf saflok selftest" + ], + "offline": true, + "options": [ + "-h, --help This help" + ], + "usage": "hf saflok selftest [-h]" + }, "hf search": { "command": "hf search", "description": "Will try to find a HF read out of the unknown tag. Continues to search for all different HF protocols.", @@ -8438,28 +8450,6 @@ ], "usage": "hf vas reader [-h@v] [--pid ] [-f ] [--url ]" }, - "hf waveshare help": { - "command": "hf waveshare help", - "description": "help This help load Load image file to Waveshare NFC ePaper --------------------------------------------------------------------------------------- hf waveshare load available offline: yes Load image file to Waveshare NFC ePaper", - "notes": [ - "hf waveshare load -f myfile -m 0 -> 2.13 inch e-paper ( 122, 250 )", - "hf waveshare load -f myfile -m 1 -> 2.9 inch e-paper ( 296, 128 )", - "hf waveshare load -f myfile -m 2 -> 4.2 inch e-paper ( 400, 300 )", - "hf waveshare load -f myfile -m 3 -> 7.5 inch e-paper ( 800, 480 )", - "hf waveshare load -f myfile -m 4 -> 2.7 inch e-paper ( 176, 276 )", - "hf waveshare load -f myfile -m 5 -> 2.13 inch e-paper B (with red) ( 104, 212 )", - "hf waveshare load -f myfile -m 6 -> 1.54 inch e-paper B (with red) ( 200, 200 )", - "hf waveshare load -f myfile -m 7 -> 7.5 inch e-paper HD ( 880, 528 )" - ], - "offline": true, - "options": [ - "-h, --help This help", - "-m model number [0 - 7] of your tag", - "-f, --file specify image to upload to tag", - "-s, --save save paletized version in file" - ], - "usage": "hf waveshare load [-h] -m -f [-s ]" - }, "hf xerox dump": { "command": "hf xerox dump", "description": "Dump all memory from a Fuji/Xerox tag ISO/IEC 14443 type B based communications", @@ -13701,6 +13691,6 @@ "metadata": { "commands_extracted": 785, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2025-11-10T17:48:24" + "extracted_on": "2025-11-15T03:41:46" } } diff --git a/doc/commands.md b/doc/commands.md index 9bf0f776a..fde136f36 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -722,6 +722,7 @@ Check column "offline" for their availability. |`hf saflok decrypt `|Y |`Decrypt 17-byte encrypted block` |`hf saflok interrogate `|N |`Interrogate saflok card` |`hf saflok cksum `|N |`Generate checksum for data block` +|`hf saflok selftest `|Y |`Run self-test` ### hf seos @@ -819,16 +820,6 @@ Check column "offline" for their availability. |`hf vas decrypt `|Y |`Decrypt a previously captured VAS cryptogram` -### hf waveshare - - { Waveshare NFC ePaper... } - -|command |offline |description -|------- |------- |----------- -|`hf waveshare help `|Y |`This help` -|`hf waveshare load `|Y |`Load image file to Waveshare NFC ePaper` - - ### hf xerox { Fuji/Xerox cartridge RFIDs... } From 11eaa017d446dd2636e3300f23e7c31bc88f0779 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:15:56 -0800 Subject: [PATCH 10/28] Saflok: Fix datetime math helpers --- client/src/cmdhfsaflok.c | 187 +++++++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 67 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 3599dcc56..8587d2acd 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -39,6 +39,19 @@ // to many of the functions. static const uint8_t days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { + assert(month >= 1 && month <= 12); // this uses 1-based indexing! + uint8_t days = days_in_month_lookup[month]; + // Adjust for leap years + if ((month == 2u) && + (year % 4u == 0u) && + ((year % 100u != 0u) || (year % 400u == 0u)) + ) { + days = 29u; + } + return days; +} + // TODO: consider separate structs for encrypted vs. decrypted // for even greater type safety. typedef struct _saflok_mfc_data_t { @@ -51,18 +64,18 @@ typedef struct _saflok_mfc_key_t { uint8_t key[6]; } saflok_mfc_key_t; typedef struct _saflok_mfc_datetime_t { - uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; + uint16_t year; // full year, e.g., 2024 + uint8_t month; // Month in range [1..12] + uint8_t day; // day of month, max range [1..31] + uint8_t hour; // hour of day, range [0..23] + uint8_t minute; // minute of hour, range [0..59] } saflok_mfc_datetime_t; typedef struct _saflok_mfc_datetime_offset_t { - uint8_t years; - uint8_t months; - uint8_t days; - uint8_t hours; - uint8_t minutes; + uint8_t years; // range [0..15] + uint8_t months; // range [0..11] + uint8_t days; // maximum range [0..31] + uint8_t hours; // range [0..23] + uint8_t minutes; // range [0..59] } saflok_mfc_datetime_offset_t; #define KEY_LENGTH sizeof(saflok_mfc_key_t) @@ -429,7 +442,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi #endif // getters for interval (expiration) components #if 1 // Setting the interval date is based on the creation date. - static inline saflok_mfc_datetime_offset_t get_saflok_mfc_interval( + static saflok_mfc_datetime_offset_t get_saflok_mfc_interval( const saflok_mfc_data_t *data ) { saflok_mfc_datetime_offset_t offset = { @@ -441,7 +454,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi }; return offset; } - static inline bool set_saflok_mfc_interval( + static bool set_saflok_mfc_interval( saflok_mfc_data_t *data, saflok_mfc_datetime_offset_t offset ) { @@ -453,7 +466,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi _set_saflok_mfc_interval_minutes_impl(data, offset.minutes); return result; } - static inline saflok_mfc_datetime_t add_offset( + static saflok_mfc_datetime_t add_offset( const saflok_mfc_datetime_t *base, const saflok_mfc_datetime_offset_t offset ) { @@ -462,7 +475,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi struct tm tm = { .tm_year = base->year - 1900, .tm_mon = base->month - 1, - .tm_mday = base->day, + .tm_mday = base->day - 1, .tm_hour = base->hour, .tm_min = base->minute, .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute @@ -474,67 +487,105 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi tm.tm_mday += offset.days; tm.tm_hour += offset.hours; tm.tm_min += offset.minutes; - // normalize into EPOCH ... which allows out-of-bounds values - time_t fixed = timegm(&tm); - // convert EPOCH back into a tm structure - gmtime_r(&fixed, &tm); + // now deal with carrying overflows + if (tm.tm_min >= 60) { + tm.tm_hour ++; + tm.tm_min -= 60; + } + if (tm.tm_hour >= 24) { + tm.tm_mday ++; + tm.tm_hour -= 24; + } + // addition is easy... + uint8_t days_in_month = get_days_in_month(tm.tm_year + 1900, tm.tm_mon + 1); + if (tm.tm_mday >= days_in_month) { + tm.tm_mon ++; + tm.tm_mday -= days_in_month; + } + if (tm.tm_mon >= 12) { + tm.tm_year ++; + tm.tm_mon -= 12; + } // and finally convert back into saflok_mfc_datetime_t saflok_mfc_datetime_t result = { .year = tm.tm_year + 1900, .month = tm.tm_mon + 1, - .day = tm.tm_mday, + .day = tm.tm_mday + 1, .hour = tm.tm_hour, .minute = tm.tm_min }; return result; } - static inline saflok_mfc_datetime_offset_t get_datetime_offset( + static saflok_mfc_datetime_offset_t get_datetime_offset( const saflok_mfc_datetime_t *start, const saflok_mfc_datetime_t *end ) { - // convert both to `struct tm` - struct tm tm_start = { - .tm_year = start->year - 1900, - .tm_mon = start->month - 1, - .tm_mday = start->day, - .tm_hour = start->hour, - .tm_min = start->minute, - .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute - .tm_isdst = 0, // UTC has no DST - }; - struct tm tm_end = { - .tm_year = end->year - 1900, - .tm_mon = end->month - 1, - .tm_mday = end->day, - .tm_hour = end->hour, - .tm_min = end->minute, - .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute - .tm_isdst = 0, // UTC has no DST - }; - // convert both to EPOCH - time_t epoch_start = timegm(&tm_start); - time_t epoch_end = timegm(&tm_end); - // subtract the epoch values to get delta in seconds - if (epoch_end < epoch_start) { - PrintAndLogEx(ERR, _RED_("ERROR:") " end date is before start date\n"); - saflok_mfc_datetime_offset_t zero_offset = {0}; - return zero_offset; + + enum logLevel MYDBG = WARNING; + saflok_mfc_datetime_offset_t result = {0}; + + if (!is_saflok_mfc_datetime_valid(start)) { + PrintAndLogEx(ERR, _RED_("ERROR:") " start date is invalid\n"); + return result; } - time_t delta = epoch_end - epoch_start; - // convert delta seconds back into `struct tm` - struct tm result_tm = {0}; - gmtime_r(&delta, &result_tm); - saflok_mfc_datetime_offset_t result = { - .years = result_tm.tm_year, - .months = result_tm.tm_mon, - .days = result_tm.tm_mday, - .hours = result_tm.tm_hour, - .minutes = result_tm.tm_min + if (!is_saflok_mfc_datetime_valid(end)) { + PrintAndLogEx(ERR, _RED_("ERROR:") " end date is invalid\n"); + return result; + } + + PrintAndLogEx(MYDBG, "Calculating offset from %04u-%02u-%02uT%02u:%02u to %04u-%02u-%02uT%02u:%02u\n", + start->year, start->month, start->day, start->hour, start->minute, + end->year, end->month, end->day, end->hour, end->minute + ); + // use `struct tm`, which uses signed integers for each part + // and thus allows simpler checks for borrowing + struct tm diff = { + .tm_year = end->year - start->year, + .tm_mon = end->month - start->month, + .tm_mday = end->day - start->day, + .tm_hour = end->hour - start->hour, + .tm_min = end->minute - start->minute, }; + PrintAndLogEx(MYDBG, "Initial diff: %d years, %d months, %d days, %d hours, %d minutes\n", + diff.tm_year, diff.tm_mon, diff.tm_mday, diff.tm_hour, diff.tm_min + ); + if (diff.tm_min < 0) { + diff.tm_min += 60; + diff.tm_hour -= 1; + } + if (diff.tm_hour < 0) { + diff.tm_hour += 24; + diff.tm_mday -= 1; + } + if (diff.tm_mday < 0) { + // determine days in previous month + uint16_t tgt_year = end->year - (end->month == 1 ? 1 : 0); + uint8_t tgt_month = (end->month == 1 ? 12 : (end->month - 1)); + uint8_t days_in_prev_month = get_days_in_month(tgt_year, tgt_month); + PrintAndLogEx(MYDBG, "Borrowing days from month %02u of year %04u which has %02u days\n", + tgt_month, tgt_year, days_in_prev_month + ); + + // borrow from previous month + diff.tm_mon -= 1; + diff.tm_mday += days_in_prev_month; + } + if (diff.tm_mon < 0) { + diff.tm_mon += 12; + diff.tm_year -= 1; + } + + result.years = diff.tm_year; + result.months = diff.tm_mon; + result.days = diff.tm_mday; + result.hours = diff.tm_hour; + result.minutes = diff.tm_min; + PrintAndLogEx(MYDBG, "Final offset: %d years, %d months, %d days, %d hours, %d minutes\n", + result.years, result.months, result.days, result.hours, result.minutes + ); return result; } - - static inline saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime( + static saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime( const saflok_mfc_data_t *data ) { saflok_mfc_datetime_t creation_date = get_saflok_mfc_card_creation_datetime(data); @@ -542,7 +593,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi saflok_mfc_datetime_t expiration = add_offset(&creation_date, offset); return expiration; } - static inline bool set_saflok_mfc_card_expiration_datetime( + static bool set_saflok_mfc_card_expiration_datetime( saflok_mfc_data_t *data, const saflok_mfc_datetime_t * expiration ) { @@ -743,17 +794,17 @@ static uint32_t extract_bits(const saflok_mfc_data_t *data, size_t start_bit, si static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access - assert(!"extract_bits: Out of bounds access (start_bit)"); + PrintAndLogEx(ERR, "extract_bits: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); return 0; } if (num_bits > 32) { // Exceeds maximum supported bit extraction - assert(!"extract_bits: num_bits exceeds 32"); + PrintAndLogEx(ERR, "extract_bits: out of bounds access (num_bits=%zu, max=32)\n", num_bits); return 0; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access - assert(!"extract_bits: Out of bounds access (num_bits)"); + PrintAndLogEx(ERR, "extract_bits: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); return 0; } @@ -772,22 +823,22 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access - assert(!"insert_bits: Out of bounds access (start_bit)"); + PrintAndLogEx(ERR, "insert_bits: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); return; } if (num_bits > 32) { // Exceeds maximum supported bit extraction - assert(!"insert_bits: num_bits exceeds 32"); + PrintAndLogEx(ERR, "insert_bits: num_bits exceeds 32\n", num_bits); return; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access - assert(!"insert_bits: Out of bounds access (num_bits)"); + PrintAndLogEx(ERR, "insert_bits: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); return; } if ((num_bits < 32) && ((value >> num_bits) != 0)) { // Value exceeds the size of the specified bit field - assert(!"insert_bits: value exceeds bit field size"); + PrintAndLogEx(ERR, "insert_bits: value exceeds bit field size (value=%u (0x%08x), num_bits=%zu)\n", value, value, num_bits); return; } @@ -1408,6 +1459,8 @@ saflok_mfc_datetime_offset_t get_datetime_offset saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime bool set_saflok_mfc_card_expiration_datetime */ + saflok_mfc_data_t test_data = {0}; + (void)get_saflok_mfc_card_expiration_datetime(&test_data); From dcf7c2dd3337c43bcfcf2e0843dd1c3356e018b7 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:51:43 -0800 Subject: [PATCH 11/28] side-by-side check of new vs. old decoding run `hf saflok decode` twice ... and it will run old decoder one time, and new decoder the other time. Makes it easy to compare side-by-side in this interrim build. --- client/src/cmdhfsaflok.c | 265 ++++++++++++++++++++++++++++----------- 1 file changed, 190 insertions(+), 75 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 8587d2acd..47965afb0 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -231,6 +231,13 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi insert_bits(data, 116, 12, property_id); return true; } + static inline uint8_t get_saflok_mfc_checksum(const saflok_mfc_data_t *data) { + return data->raw[16]; + } + static inline bool set_saflok_mfc_checksum(saflok_mfc_data_t *data, uint8_t checksum) { + data->raw[16] = checksum; + return true; + } #endif // getters and setters for each bitfield in saflok_mfc_data_t #if 1 // helpers for get/set_saflok_mfc_card_creation_date() ... do not call these directly as does not validate... @@ -521,7 +528,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi const saflok_mfc_datetime_t *end ) { - enum logLevel MYDBG = WARNING; + enum logLevel MYDBG = DEBUG; saflok_mfc_datetime_offset_t result = {0}; if (!is_saflok_mfc_datetime_valid(start)) { @@ -717,11 +724,11 @@ static int CmdHelp(const char *Cmd); // decryptedCard : uint8_t[length] // length : ALWAYS == 17 // safelok_mfc_data_t* for both parameters? -static void saflok_decrypt(const saflok_mfc_data_t *strCard, saflok_mfc_data_t *decryptedCard) { +static void saflok_decrypt(const saflok_mfc_data_t *encryptedCard, saflok_mfc_data_t *decryptedCard) { - static const size_t length = ARRAYLEN(strCard->raw); + static const size_t length = ARRAYLEN(encryptedCard->raw); for (int i = 0; i < length; i++) { - int num = c_aDecode[strCard->raw[i]] - (i + 1); + int num = c_aDecode[encryptedCard->raw[i]] - (i + 1); if (num < 0) { num += 256; } @@ -755,10 +762,10 @@ static void saflok_decrypt(const saflok_mfc_data_t *strCard, saflok_mfc_data_t * } -static void saflok_encrypt(const saflok_mfc_data_t *keyCard, saflok_mfc_data_t *encryptedCard) { - static const size_t length = ARRAYLEN(keyCard->raw); +static void saflok_encrypt(const saflok_mfc_data_t *plaintext, saflok_mfc_data_t *encryptedCard) { + static const size_t length = ARRAYLEN(plaintext->raw); int b = 0; - memcpy(encryptedCard->raw, keyCard->raw, length); + memcpy(encryptedCard->raw, plaintext->raw, length); for (int i = 0; i < length; i++) { int b2 = encryptedCard->raw[i]; int num2 = i; @@ -898,7 +905,7 @@ static void saflok_kdf(const saflok_mfc_uid_t *uid, saflok_mfc_key_t *key_out) { memcpy(key_out, &key, KEY_LENGTH); } -static void saflok_decode(const saflok_mfc_data_t *data) { +static void saflok_decode_legacy(const saflok_mfc_data_t *data) { uint32_t card_level = extract_bits(data, 0, 4); uint32_t card_type = extract_bits(data, 4, 4); @@ -960,6 +967,7 @@ static void saflok_decode(const saflok_mfc_data_t *data) { } } + PrintAndLogEx(SUCCESS, "---- OLD routines ----" ); PrintAndLogEx(SUCCESS, "Card Level: " _GREEN_("%u (%s)"), card_level, level_names[card_level]); PrintAndLogEx(SUCCESS, "Card Type: " _GREEN_("%u"), card_type); PrintAndLogEx(SUCCESS, "Card ID: " _GREEN_("%u"), card_id); @@ -987,6 +995,58 @@ static void saflok_decode(const saflok_mfc_data_t *data) { } +static void saflok_decode_new(const saflok_mfc_data_t *data) { + + uint32_t card_level = get_saflok_mfc_card_level(data); + uint32_t card_type = get_saflok_mfc_card_type(data); + uint32_t card_id = get_saflok_mfc_card_id(data); + uint32_t opening_key = get_saflok_mfc_opening_key(data); + uint32_t lock_id = get_saflok_mfc_lock_id(data); + uint32_t pass_number = get_saflok_mfc_pass_number(data); + uint32_t sequence_and_combination = get_saflok_mfc_sequence_and_combination(data); + uint32_t deadbolt_override = get_saflok_mfc_deadbolt_override(data); + uint32_t restricted_days = get_saflok_mfc_restricted_days(data); + saflok_mfc_datetime_t creation_date = get_saflok_mfc_card_creation_datetime(data); + saflok_mfc_datetime_t expiration_date = get_saflok_mfc_card_expiration_datetime(data); + uint32_t property_id = get_saflok_mfc_property_id(data); + uint32_t checksum = get_saflok_mfc_checksum(data); + + PrintAndLogEx(SUCCESS, "---- NEW routines ----" ); + PrintAndLogEx(SUCCESS, "Card Level: " _GREEN_("%u (%s)"), card_level, level_names[card_level]); + PrintAndLogEx(SUCCESS, "Card Type: " _GREEN_("%u"), card_type); + PrintAndLogEx(SUCCESS, "Card ID: " _GREEN_("%u"), card_id); + PrintAndLogEx(SUCCESS, "Opening Key: " _GREEN_("%u"), opening_key); + PrintAndLogEx(SUCCESS, "Lock ID: " _GREEN_("%u"), lock_id); + PrintAndLogEx(SUCCESS, "Pass Number: " _GREEN_("%u"), pass_number); + PrintAndLogEx(SUCCESS, "Sequence and Combination: " _GREEN_("%u"), sequence_and_combination); + PrintAndLogEx(SUCCESS, "Deadbolt Override: " _GREEN_("%u"), deadbolt_override); + PrintAndLogEx(SUCCESS, "Restricted Days: " _GREEN_("%u"), restricted_days); + PrintAndLogEx(SUCCESS, "Card Creation Date: " _GREEN_("%u-%02d-%02d %02d:%02d"), + creation_date.year, + creation_date.month, + creation_date.day, + creation_date.hour, + creation_date.minute); + PrintAndLogEx(SUCCESS, "Expire Date: " _GREEN_("%u-%02d-%02d %02d:%02d"), + expiration_date.year, + expiration_date.month, + expiration_date.day, + expiration_date.hour, + expiration_date.minute); + PrintAndLogEx(SUCCESS, "Property ID: " _GREEN_("%u"), property_id); + PrintAndLogEx(SUCCESS, "Checksum: " _GREEN_("0x%X") " (%s)", checksum, (checksum == calculated_saflok_checksum(data)) ? _GREEN_("ok") : _RED_("bad")); + PrintAndLogEx(NORMAL, ""); + +} +static void saflok_decode(const saflok_mfc_data_t *data) { + static uint8_t which = 0; + if (which++ % 2 == 0) { + saflok_decode_legacy(data); + } else { + saflok_decode_new(data); + } +} + static void saflok_encode( saflok_mfc_data_t *data, uint32_t card_level, @@ -1147,18 +1207,18 @@ static int CmdHFSaflokEncode(const char *Cmd) { saflok_encode(&decrypted, - arg_get_u32_def(ctx, 1, 0), - arg_get_u32_def(ctx, 2, 0), - arg_get_u32_def(ctx, 3, 0), - arg_get_u32_def(ctx, 4, 0), - arg_get_u32_def(ctx, 5, 0), - arg_get_u32_def(ctx, 6, 0), - arg_get_u32_def(ctx, 7, 0), - arg_get_u32_def(ctx, 8, 0), - arg_get_u32_def(ctx, 9, 0), + arg_get_u32_def(ctx, 1, 0), // card_level + arg_get_u32_def(ctx, 2, 0), // card_type + arg_get_u32_def(ctx, 3, 0), // card_id + arg_get_u32_def(ctx, 4, 0), // opening_key + arg_get_u32_def(ctx, 5, 0), // lock_id + arg_get_u32_def(ctx, 6, 0), // pass_num + arg_get_u32_def(ctx, 7, 0), // seq_combo + arg_get_u32_def(ctx, 8, 0), // deadbolt + arg_get_u32_def(ctx, 9, 0), // days 0, 0, - arg_get_u32_def(ctx, 12, 0), + arg_get_u32_def(ctx, 12, 0), // property_id dt_e, dt); @@ -1404,67 +1464,122 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { // verify the resulting decoded data matches expectations, // or investigate which one results in correct data. -/* -uint8_t get_saflok_mfc_card_level -bool set_saflok_mfc_card_level -uint8_t get_saflok_mfc_card_type -bool set_saflok_mfc_card_type -uint8_t get_saflok_mfc_card_id -bool set_saflok_mfc_card_id -uint8_t get_saflok_mfc_opening_key -bool set_saflok_mfc_opening_key -uint16_t get_saflok_mfc_lock_id -bool set_saflok_mfc_lock_id -uint16_t get_saflok_mfc_pass_number -bool set_saflok_mfc_pass_number -uint16_t get_saflok_mfc_sequence_and_combination -bool set_saflok_mfc_sequence_and_combination -bool get_saflok_mfc_deadbolt_override -bool set_saflok_mfc_deadbolt_override -uint8_t get_saflok_mfc_restricted_days -bool set_saflok_mfc_restricted_days -uint32_t get_saflok_mfc_raw_interval_date -bool set_saflok_mfc_raw_interval_date -uint32_t get_saflok_mfc_raw_card_creation_date -bool set_saflok_mfc_raw_card_creation_date -uint16_t get_saflok_mfc_property_id -bool set_saflok_mfc_property_id -uint16_t _get_saflok_mfc_card_creation_year_impl -bool _set_saflok_mfc_card_creation_year_impl -uint8_t _get_saflok_mfc_card_creation_month_impl -bool _set_saflok_mfc_card_creation_month_impl -uint8_t _get_saflok_mfc_card_creation_day_impl -bool _set_saflok_mfc_card_creation_day_impl -uint8_t _get_saflok_mfc_card_creation_hour_impl -bool _set_saflok_mfc_card_creation_hour_impl -uint8_t _get_saflok_mfc_card_creation_minute_impl -bool _set_saflok_mfc_card_creation_minute_impl -bool is_saflok_mfc_datetime_valid -saflok_mfc_datetime_t get_saflok_mfc_card_creation_datetime -bool set_saflok_mfc_card_creation_datetime -uint8_t _get_saflok_mfc_interval_years_impl -bool _set_saflok_mfc_interval_years_impl -uint8_t _get_saflok_mfc_interval_months_impl -bool _set_saflok_mfc_interval_months_impl -uint8_t _get_saflok_mfc_interval_days_impl -bool _set_saflok_mfc_interval_days_impl -uint8_t _get_saflok_mfc_interval_hours_impl -bool _set_saflok_mfc_interval_hours_impl -uint8_t _get_saflok_mfc_interval_minutes_impl -bool _set_saflok_mfc_interval_minutes_impl -saflok_mfc_datetime_offset_t get_saflok_mfc_interval -bool set_saflok_mfc_interval -saflok_mfc_datetime_t add_offset -saflok_mfc_datetime_offset_t get_datetime_offset -saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime -bool set_saflok_mfc_card_expiration_datetime -*/ - saflok_mfc_data_t test_data = {0}; - (void)get_saflok_mfc_card_expiration_datetime(&test_data); + /* + [ ] get_saflok_mfc_card_level + [ ] set_saflok_mfc_card_level + [ ] get_saflok_mfc_card_type + [ ] set_saflok_mfc_card_type + [ ] get_saflok_mfc_card_id + [ ] set_saflok_mfc_card_id + [ ] get_saflok_mfc_opening_key + [ ] set_saflok_mfc_opening_key + [ ] get_saflok_mfc_lock_id + [ ] set_saflok_mfc_lock_id + [ ] get_saflok_mfc_pass_number + [ ] set_saflok_mfc_pass_number + [ ] get_saflok_mfc_sequence_and_combination + [ ] set_saflok_mfc_sequence_and_combination + [ ] get_saflok_mfc_deadbolt_override + [ ] set_saflok_mfc_deadbolt_override + [ ] get_saflok_mfc_restricted_days + [ ] set_saflok_mfc_restricted_days + [ ] get_saflok_mfc_raw_interval_date + [ ] set_saflok_mfc_raw_interval_date + [ ] get_saflok_mfc_raw_card_creation_date + [ ] set_saflok_mfc_raw_card_creation_date + [ ] get_saflok_mfc_property_id + [ ] set_saflok_mfc_property_id + [ ] _get_saflok_mfc_card_creation_year_impl + [ ] _set_saflok_mfc_card_creation_year_impl + [ ] _get_saflok_mfc_card_creation_month_impl + [ ] _set_saflok_mfc_card_creation_month_impl + [ ] _get_saflok_mfc_card_creation_day_impl + [ ] _set_saflok_mfc_card_creation_day_impl + [ ] _get_saflok_mfc_card_creation_hour_impl + [ ] _set_saflok_mfc_card_creation_hour_impl + [ ] _get_saflok_mfc_card_creation_minute_impl + [ ] _set_saflok_mfc_card_creation_minute_impl + [ ] is_saflok_mfc_datetime_valid + [ ] get_saflok_mfc_card_creation_datetime + [ ] set_saflok_mfc_card_creation_datetime + [ ] _get_saflok_mfc_interval_years_impl + [ ] _set_saflok_mfc_interval_years_impl + [ ] _get_saflok_mfc_interval_months_impl + [ ] _set_saflok_mfc_interval_months_impl + [ ] _get_saflok_mfc_interval_days_impl + [ ] _set_saflok_mfc_interval_days_impl + [ ] _get_saflok_mfc_interval_hours_impl + [ ] _set_saflok_mfc_interval_hours_impl + [ ] _get_saflok_mfc_interval_minutes_impl + [ ] _set_saflok_mfc_interval_minutes_impl + [ ] get_saflok_mfc_interval + [ ] set_saflok_mfc_interval + [ ] add_offset + [ ] get_datetime_offset + [ ] get_saflok_mfc_card_expiration_datetime + [ ] set_saflok_mfc_card_expiration_datetime + */ + + if (true) { // test encode+encrypt, including interval affected by leap-year + + // Equivalent to running the following commands manually: + // + // [usb] pm3 --> hf saflok encode + // --level 15 --type 7 --id 176 --open 2 --lock_id 257 --pass_num 4095 --seq_combo 2047 + // --deadbolt 1 --days 30 --expire 2020-03-01T00:00 --created 2019-02-20T00:00 --prop_id 999 + // + // [=] --- Encoded Card Data + // [+] Encrypted Data: 5A4A845610B6B1DD8F4753872BC0E91095 + // + // [usb] pm3 --> hf saflok decode -d 5A4A845610B6B1DD8F4753872BC0E91095 + // [+] Card Level: 15 (Primary Programming Key (PPK)) + // [+] Card Type: 7 + // [+] Card ID: 176 + // [+] Opening Key: 2 + // [+] Lock ID: 257 + // [+] Pass Number: 4095 + // [+] Sequence and Combination: 2047 + // [+] Deadbolt Override: 1 + // [+] Restricted Days: 30 == 0b0011110 + // [+] Card Creation Date: 2019-02-20 00:00 + // [+] Expire Date: 2020-03-01 00:00 + // [+] Property ID: 999 + // [+] Checksum: 0xC7 (ok) + saflok_mfc_data_t test_data = {0}; + set_saflok_mfc_card_level(&test_data, 15); + set_saflok_mfc_card_type(&test_data, 7); + set_saflok_mfc_card_id(&test_data, 176); + set_saflok_mfc_opening_key(&test_data, 2); + set_saflok_mfc_lock_id(&test_data, 257); + set_saflok_mfc_pass_number(&test_data, 4095); + set_saflok_mfc_sequence_and_combination(&test_data, 2047); + set_saflok_mfc_deadbolt_override(&test_data, true); + set_saflok_mfc_restricted_days(&test_data, 30); + saflok_mfc_datetime_t creation_date = {.year = 2019, .month = 2, .day = 20, .hour = 0, .minute = 0}; + saflok_mfc_datetime_t expiration_date = {.year = 2020, .month = 3, .day = 1, .hour = 0, .minute = 0}; + set_saflok_mfc_card_creation_datetime(&test_data, &creation_date); + set_saflok_mfc_card_expiration_datetime(&test_data, &expiration_date); + set_saflok_mfc_property_id(&test_data, 999); + uint8_t checksum = calculated_saflok_checksum(&test_data); + set_saflok_mfc_checksum(&test_data, checksum); + saflok_mfc_data_t encrypted = {0}; + saflok_encrypt(&test_data, &encrypted); + saflok_mfc_data_t expected = { + .raw = {0x5A, 0x4A, 0x84, 0x56, 0x10, 0xB6, 0xB1, 0xDD, + 0x8F, 0x47, 0x53, 0x87, 0x2B, 0xC0, 0xE9, 0x10, + 0x95}, + }; + if (memcmp(encrypted.raw, expected.raw, sizeof(saflok_mfc_data_t)) == 0) { + PrintAndLogEx(SUCCESS, "Self-test passed: encoded+encrypted data matches expected value."); + } else { + PrintAndLogEx(FAILED, "Self-test failed: encoded+encrypted data did not match expected value."); + result = PM3_EFAILED; + } + } return result; } From 700a109a8e79d2f7a89a97c51ef865c72ecff9af Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:01:33 -0800 Subject: [PATCH 12/28] Saflok: remove redundant CLIParserFreE(ctx) --- client/src/cmdhfsaflok.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 47965afb0..639fcd54f 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1740,7 +1740,6 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { if (mf_read_uid(uid, &uid_len, NULL) != PM3_SUCCESS || uid_len < 4) { PrintAndLogEx(WARNING, "Failed to read UID."); - CLIParserFree(ctx); return PM3_ESOFT; } @@ -1753,14 +1752,13 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { uint8_t block2[16]; if (mf_read_block(2, 0, key.key, block2) != PM3_SUCCESS) { PrintAndLogEx(WARNING, "Failed to read block 2 with derived key."); - CLIParserFree(ctx); return PM3_ESOFT; } uint8_t control_byte = block2[5]; uint8_t subblock_stop = (control_byte >> 3); if (subblock_stop == 0) { - CLIParserFree(ctx); + PrintAndLogEx(WARNING, "subblock_stop is invalid (zero)."); return PM3_EINVARG; } @@ -1806,7 +1804,6 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { cursor += 6; } - CLIParserFree(ctx); return PM3_SUCCESS; } From 3d0fe23c17c6114883edbc12b7fa6868baff8a8e Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:06:05 -0800 Subject: [PATCH 13/28] improved messaging for cards w/o variable keys --- client/src/cmdhfsaflok.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 639fcd54f..086bc9872 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1758,7 +1758,8 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { uint8_t control_byte = block2[5]; uint8_t subblock_stop = (control_byte >> 3); if (subblock_stop == 0) { - PrintAndLogEx(WARNING, "subblock_stop is invalid (zero)."); + // same message as below for total_bytes == 0 + PrintAndLogEx(SUCCESS, "Card has no variable keys"); return PM3_EINVARG; } From 444e19212ec299cc5371e08b003c4e855ea04efa Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:14:25 -0800 Subject: [PATCH 14/28] more use of type-safe routines for getting properties. --- client/src/cmdhfsaflok.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 086bc9872..cd5fdd4ca 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1307,40 +1307,40 @@ static int CmdHFSaflokModify(const char *Cmd) { saflok_decrypt(&encrypted, &decrypted); - uint32_t card_level = extract_bits(&decrypted, 0, 4); + uint32_t card_level = get_saflok_mfc_card_level(&decrypted); card_level = arg_get_u32_def(ctx, 1, card_level); - uint32_t card_type = extract_bits(&decrypted, 4, 4); + uint32_t card_type = get_saflok_mfc_card_type(&decrypted); card_type = arg_get_u32_def(ctx, 2, card_type); - uint32_t card_id = extract_bits(&decrypted, 8, 8); + uint32_t card_id = get_saflok_mfc_card_id(&decrypted); card_id = arg_get_u32_def(ctx, 3, card_id); - uint32_t opening_key = extract_bits(&decrypted, 16, 2); + uint32_t opening_key = get_saflok_mfc_opening_key(&decrypted); opening_key = arg_get_u32_def(ctx, 4, opening_key); - uint32_t lock_id = extract_bits(&decrypted, 18, 14); + uint32_t lock_id = get_saflok_mfc_lock_id(&decrypted); lock_id = arg_get_u32_def(ctx, 5, lock_id); - uint32_t pass_number = extract_bits(&decrypted, 32, 12); + uint32_t pass_number = get_saflok_mfc_pass_number(&decrypted); pass_number = arg_get_u32_def(ctx, 6, pass_number); - uint32_t sequence_and_combination = extract_bits(&decrypted, 44, 12); + uint32_t sequence_and_combination = get_saflok_mfc_sequence_and_combination(&decrypted); sequence_and_combination = arg_get_u32_def(ctx, 7, sequence_and_combination); - uint32_t deadbolt_override = extract_bits(&decrypted, 56, 1); + uint32_t deadbolt_override = get_saflok_mfc_deadbolt_override(&decrypted); deadbolt_override = arg_get_u32_def(ctx, 8, deadbolt_override); - uint32_t restricted_days = extract_bits(&decrypted, 57, 7); + uint32_t restricted_days = get_saflok_mfc_restricted_days(&decrypted); restricted_days = arg_get_u32_def(ctx, 9, restricted_days); - uint32_t expire_date = extract_bits(&decrypted, 64, 24); + uint32_t interval = get_saflok_mfc_raw_interval_date(&decrypted); //expire_date = arg_get_u32_def(ctx, 10, expire_date); - uint32_t card_creation_date = extract_bits(&decrypted, 88, 28); + uint32_t raw_card_creation_date = get_saflok_mfc_raw_card_creation_date(&decrypted); //card_creation_date = arg_get_u32_def(ctx, 11, card_creation_date); - uint32_t property_id = extract_bits(&decrypted, 116, 12); + uint32_t property_id = get_saflok_mfc_property_id(&decrypted); property_id = arg_get_u32_def(ctx, 12, property_id); int slen = 0; @@ -1361,8 +1361,8 @@ static int CmdHFSaflokModify(const char *Cmd) { sequence_and_combination, deadbolt_override, restricted_days, - expire_date, - card_creation_date, + interval, + raw_card_creation_date, property_id, dt_e, dt); From 3bf51b1fd4a45026fa48d0d23bfe9d530cec8a3a Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:22:24 -0800 Subject: [PATCH 15/28] Migrate more fields to type-safe getter / setter routines --- client/src/cmdhfsaflok.c | 142 ++++++--------------------------------- 1 file changed, 21 insertions(+), 121 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index cd5fdd4ca..4367acb63 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -198,15 +198,15 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi insert_bits(data, 57, 7, restricted_days); return true; } - static inline uint32_t get_saflok_mfc_raw_interval_date(const saflok_mfc_data_t *data) { + static inline uint32_t get_saflok_mfc_raw_interval(const saflok_mfc_data_t *data) { return extract_bits(data, 64, 24); } - static inline bool set_saflok_mfc_raw_interval_date(saflok_mfc_data_t *data, uint32_t raw_interval_date) { - if (raw_interval_date > 0xFFFFFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval_date out of range (%08x)\n", raw_interval_date); + static inline bool set_saflok_mfc_raw_interval(saflok_mfc_data_t *data, uint32_t raw_interval) { + if (raw_interval > 0xFFFFFFu) { + PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval out of range (%08x)\n", raw_interval); return false; } - insert_bits(data, 64, 24, raw_interval_date); + insert_bits(data, 64, 24, raw_interval); return true; } static inline uint32_t get_saflok_mfc_raw_card_creation_date(const saflok_mfc_data_t *data) { @@ -718,7 +718,6 @@ _Static_assert(ARRAYLEN(level_names) == 16, "level_names must have 16 elements") static int CmdHelp(const char *Cmd); - // unsafe without static analysis hints. // strCard : uint8_t[length] // decryptedCard : uint8_t[length] @@ -905,97 +904,7 @@ static void saflok_kdf(const saflok_mfc_uid_t *uid, saflok_mfc_key_t *key_out) { memcpy(key_out, &key, KEY_LENGTH); } -static void saflok_decode_legacy(const saflok_mfc_data_t *data) { - - uint32_t card_level = extract_bits(data, 0, 4); - uint32_t card_type = extract_bits(data, 4, 4); - uint32_t card_id = extract_bits(data, 8, 8); - uint32_t opening_key = extract_bits(data, 16, 2); - uint32_t lock_id = extract_bits(data, 18, 14); - uint32_t pass_number = extract_bits(data, 32, 12); - uint32_t sequence_and_combination = extract_bits(data, 44, 12); - uint32_t deadbolt_override = extract_bits(data, 56, 1); - uint32_t restricted_days = extract_bits(data, 57, 7); - //uint32_t expire_date = extract_bits(data, 64, 24); - //uint32_t card_creation_date = extract_bits(data, 88, 28); - uint32_t property_id = extract_bits(data, 116, 12); - uint32_t checksum = extract_bits(data, 128, 8); - - //date parsing, stolen from flipper code - uint16_t interval_year = (data->raw[8] >> 4); - uint8_t interval_month = data->raw[8] & 0x0F; - uint8_t interval_day = (data->raw[9] >> 3) & 0x1F; - uint8_t interval_hour = ((data->raw[9] & 0x07) << 2) | (data->raw[10] >> 6); - uint8_t interval_minute = data->raw[10] & 0x3F; - - uint8_t creation_year_bits = (data->raw[14] & 0xF0); - uint16_t creation_year = - (creation_year_bits | ((data->raw[11] & 0xF0) >> 4)) + 1980; - uint8_t creation_month = data->raw[11] & 0x0F; - uint8_t creation_day = (data->raw[12] >> 3) & 0x1F; - uint8_t creation_hour = ((data->raw[12] & 0x07) << 2) | (data->raw[13] >> 6); - uint8_t creation_minute = data->raw[13] & 0x3F; - - uint16_t expire_year = creation_year + interval_year; - uint8_t expire_month = creation_month + interval_month; - uint8_t expire_day = creation_day + interval_day; - uint8_t expire_hour = interval_hour; - uint8_t expire_minute = interval_minute; - - // Handle month rollover - while (expire_month > 12) { - expire_month -= 12; - expire_year++; - } - - // Handle day rollover - while (true) { - uint8_t max_days = days_in_month_lookup[expire_month]; - // Adjust for leap years - if (expire_month == 2 && - (expire_year % 4 == 0 && (expire_year % 100 != 0 || expire_year % 400 == 0))) { - max_days = 29; - } - if (expire_day <= max_days) { - break; - } - expire_day -= max_days; - expire_month++; - if (expire_month > 12) { - expire_month = 1; - expire_year++; - } - } - - PrintAndLogEx(SUCCESS, "---- OLD routines ----" ); - PrintAndLogEx(SUCCESS, "Card Level: " _GREEN_("%u (%s)"), card_level, level_names[card_level]); - PrintAndLogEx(SUCCESS, "Card Type: " _GREEN_("%u"), card_type); - PrintAndLogEx(SUCCESS, "Card ID: " _GREEN_("%u"), card_id); - PrintAndLogEx(SUCCESS, "Opening Key: " _GREEN_("%u"), opening_key); - PrintAndLogEx(SUCCESS, "Lock ID: " _GREEN_("%u"), lock_id); - PrintAndLogEx(SUCCESS, "Pass Number: " _GREEN_("%u"), pass_number); - PrintAndLogEx(SUCCESS, "Sequence and Combination: " _GREEN_("%u"), sequence_and_combination); - PrintAndLogEx(SUCCESS, "Deadbolt Override: " _GREEN_("%u"), deadbolt_override); - PrintAndLogEx(SUCCESS, "Restricted Days: " _GREEN_("%u"), restricted_days); - PrintAndLogEx(SUCCESS, "Card Creation Date: " _GREEN_("%u-%02d-%02d %02d:%02d"), - creation_year, - creation_month, - creation_day, - creation_hour, - creation_minute); - PrintAndLogEx(SUCCESS, "Expire Date: " _GREEN_("%u-%02d-%02d %02d:%02d"), - expire_year, - expire_month, - expire_day, - expire_hour, - expire_minute); - PrintAndLogEx(SUCCESS, "Property ID: " _GREEN_("%u"), property_id); - PrintAndLogEx(SUCCESS, "Checksum: " _GREEN_("0x%X") " (%s)", checksum, (checksum == calculated_saflok_checksum(data)) ? _GREEN_("ok") : _RED_("bad")); - PrintAndLogEx(NORMAL, ""); - -} - -static void saflok_decode_new(const saflok_mfc_data_t *data) { +static void saflok_decode(const saflok_mfc_data_t *data) { uint32_t card_level = get_saflok_mfc_card_level(data); uint32_t card_type = get_saflok_mfc_card_type(data); @@ -1038,14 +947,6 @@ static void saflok_decode_new(const saflok_mfc_data_t *data) { PrintAndLogEx(NORMAL, ""); } -static void saflok_decode(const saflok_mfc_data_t *data) { - static uint8_t which = 0; - if (which++ % 2 == 0) { - saflok_decode_legacy(data); - } else { - saflok_decode_new(data); - } -} static void saflok_encode( saflok_mfc_data_t *data, @@ -1058,25 +959,24 @@ static void saflok_encode( uint32_t sequence_and_combination, uint32_t deadbolt_override, uint32_t restricted_days, - uint32_t expire_date, - uint32_t card_creation_date, + uint32_t raw_interval, + uint32_t raw_card_creation_date, uint32_t property_id, char *dt_expiration, char *dt ) { - insert_bits(data, 0, 4, card_level); - insert_bits(data, 4, 4, card_type); - insert_bits(data, 8, 8, card_id); - insert_bits(data, 16, 2, opening_key); - insert_bits(data, 18, 14, lock_id); - insert_bits(data, 32, 12, pass_number); - insert_bits(data, 44, 12, sequence_and_combination); - insert_bits(data, 56, 1, deadbolt_override); - insert_bits(data, 57, 7, restricted_days); - insert_bits(data, 64, 24, expire_date); - insert_bits(data, 88, 28, card_creation_date); - insert_bits(data, 116, 12, property_id); - + set_saflok_mfc_card_level(data, card_level); + set_saflok_mfc_card_type(data, card_type); + set_saflok_mfc_card_id(data, card_id); + set_saflok_mfc_opening_key(data, opening_key); + set_saflok_mfc_lock_id(data, lock_id); + set_saflok_mfc_pass_number(data, pass_number); + set_saflok_mfc_sequence_and_combination(data, sequence_and_combination); + set_saflok_mfc_deadbolt_override(data, deadbolt_override); + set_saflok_mfc_restricted_days(data, restricted_days); + set_saflok_mfc_raw_interval(data, raw_interval); + set_saflok_mfc_raw_card_creation_date(data, raw_card_creation_date); + set_saflok_mfc_property_id(data, property_id); // Parsing date time string from "YYYY-MM-DDTHH:mm" into `saflok_mfc_datetime_t` static const char * fmt = "%4" SCNu16 "-%2" SCNu8 "-%2" SCNu8 "T%2" SCNu8 ":%2" SCNu8; @@ -1334,7 +1234,7 @@ static int CmdHFSaflokModify(const char *Cmd) { uint32_t restricted_days = get_saflok_mfc_restricted_days(&decrypted); restricted_days = arg_get_u32_def(ctx, 9, restricted_days); - uint32_t interval = get_saflok_mfc_raw_interval_date(&decrypted); + uint32_t interval = get_saflok_mfc_raw_interval(&decrypted); //expire_date = arg_get_u32_def(ctx, 10, expire_date); uint32_t raw_card_creation_date = get_saflok_mfc_raw_card_creation_date(&decrypted); From 02056c95155214506b8bc4c1153069d4186f245f Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:26:25 -0800 Subject: [PATCH 16/28] Saflok - creation year is limited to 7 bits --- client/src/cmdhfsaflok.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 4367acb63..4b9cfbdeb 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -242,21 +242,21 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi #if 1 // helpers for get/set_saflok_mfc_card_creation_date() ... do not call these directly as does not validate... static inline uint16_t _get_saflok_mfc_card_creation_year_impl(const saflok_mfc_data_t *data) { - uint8_t creation_year_bits_high = (data->raw[14] & 0xF0u); + uint8_t creation_year_bits_high = (data->raw[14] & 0x70u); uint8_t creation_year_bits_low = (data->raw[11] & 0xF0u) >> 4; uint8_t creation_year_bits = creation_year_bits_high | creation_year_bits_low; uint16_t creation_year = 1980u + creation_year_bits; // automatically extends to uint16_t return creation_year; } static inline bool _set_saflok_mfc_card_creation_year_impl(saflok_mfc_data_t *data, uint16_t year) { - if (year < 1980u || year > (1980u+0xFFu)) { + if (year < 1980u || year > (1980u+0x7Fu)) { PrintAndLogEx(ERR, _RED_("ERROR:") " year out of range (%u)\n", year); return false; } uint16_t year_bits = year - 1980u; - uint8_t creation_year_bits_high = year_bits & 0xF0u; + uint8_t creation_year_bits_high = year_bits & 0x70u; uint8_t creation_year_bits_low = (year_bits & 0x0Fu) << 4; - data->raw[14] = (data->raw[14] & 0x0Fu) | creation_year_bits_high; + data->raw[14] = (data->raw[14] & 0x8Fu) | creation_year_bits_high; data->raw[11] = (data->raw[11] & 0x0Fu) | creation_year_bits_low; return true; } From 87a144c46a4051d424a5696748bae70ef44e9e99 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 02:02:19 -0800 Subject: [PATCH 17/28] Comments, improved function names, use of common get_days_in_month() function. --- client/src/cmdhfsaflok.c | 253 +++++++++++++++++++-------------------- 1 file changed, 122 insertions(+), 131 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 4b9cfbdeb..3713b3261 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -37,11 +37,11 @@ // defining this structure makes it more clear what // is passed around, and avoids the need to pass lengths // to many of the functions. -static const uint8_t days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +static const uint8_t _days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { assert(month >= 1 && month <= 12); // this uses 1-based indexing! - uint8_t days = days_in_month_lookup[month]; + uint8_t days = _days_in_month_lookup[month]; // Adjust for leap years if ((month == 2u) && (year % 4u == 0u) && @@ -80,155 +80,140 @@ typedef struct _saflok_mfc_datetime_offset_t { #define KEY_LENGTH sizeof(saflok_mfc_key_t) -static uint32_t extract_bits(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits); -static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value); +static uint32_t get_bitfield(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits); +static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value); #if 1 // getters and setters for each bitfield in saflok_mfc_data_t - // TODO: consider update to setter functions to use smaller type for data? - // avoiding for this commit, as larger change. - // - // card_level field is 4 bits - // card_type field is 4 bits - // card_id field is 8 bits - // opening_key field is 2 bits - // lock_id field is 14 bits - // pass_number field is 12 bits - // sequence_and_combination field is 12 bits - // deadbolt_override field is 1 bits - // restricted_days field is 7 bits - // raw_expire_date field is 24 bits -- special set processing needed - // raw_card_creation_date field is 28 bits -- special set processing needed - // property_id field is 12 bits static inline uint8_t get_saflok_mfc_card_level(const saflok_mfc_data_t *data) { - return (uint8_t)extract_bits(data, 0, 4); + return (uint8_t)get_bitfield(data, 0, 4); } static inline bool set_saflok_mfc_card_level(saflok_mfc_data_t *data, uint32_t card_level) { if (card_level > 0xFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " card_level out of range (%08x)\n", card_level); return false; } - insert_bits(data, 0, 4, card_level); + set_bitfield(data, 0, 4, card_level); return true; } static inline uint8_t get_saflok_mfc_card_type(const saflok_mfc_data_t *data) { - return (uint8_t)extract_bits(data, 4, 4); + return (uint8_t)get_bitfield(data, 4, 4); } static inline bool set_saflok_mfc_card_type(saflok_mfc_data_t *data, uint32_t card_type) { if (card_type > 0xFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " card_type out of range (%08x)\n", card_type); return false; } - insert_bits(data, 4, 4, card_type); + set_bitfield(data, 4, 4, card_type); return true; } static inline uint8_t get_saflok_mfc_card_id(const saflok_mfc_data_t *data) { - return (uint8_t)extract_bits(data, 8, 8); + return (uint8_t)get_bitfield(data, 8, 8); } static inline bool set_saflok_mfc_card_id(saflok_mfc_data_t *data, uint32_t card_id) { if (card_id > 0xFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " card_id out of range (%08x)\n", card_id); return false; } - insert_bits(data, 8, 8, card_id); + set_bitfield(data, 8, 8, card_id); return true; } static inline uint8_t get_saflok_mfc_opening_key(const saflok_mfc_data_t *data) { - return (uint8_t)extract_bits(data, 16, 2); + return (uint8_t)get_bitfield(data, 16, 2); } static inline bool set_saflok_mfc_opening_key(saflok_mfc_data_t *data, uint32_t opening_key) { if (opening_key > 0x3u) { PrintAndLogEx(ERR, _RED_("ERROR:") " opening_key out of range (%08x)\n", opening_key); return false; } - insert_bits(data, 16, 2, opening_key); + set_bitfield(data, 16, 2, opening_key); return true; } static inline uint16_t get_saflok_mfc_lock_id(const saflok_mfc_data_t *data) { - return (uint16_t)extract_bits(data, 18, 14); + return (uint16_t)get_bitfield(data, 18, 14); } static inline bool set_saflok_mfc_lock_id(saflok_mfc_data_t *data, uint32_t lock_id) { if (lock_id > 0x3FFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " lock_id out of range (%08x)\n", lock_id); return false; } - insert_bits(data, 18, 14, lock_id); + set_bitfield(data, 18, 14, lock_id); return true; } static inline uint16_t get_saflok_mfc_pass_number(const saflok_mfc_data_t *data) { - return (uint16_t)extract_bits(data, 32, 12); + return (uint16_t)get_bitfield(data, 32, 12); } static inline bool set_saflok_mfc_pass_number(saflok_mfc_data_t *data, uint32_t pass_number) { if (pass_number > 0xFFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " pass_number out of range (%08x)\n", pass_number); return false; } - insert_bits(data, 32, 12, pass_number); + set_bitfield(data, 32, 12, pass_number); return true; } static inline uint16_t get_saflok_mfc_sequence_and_combination(const saflok_mfc_data_t *data) { - return (uint16_t)extract_bits(data, 44, 12); + return (uint16_t)get_bitfield(data, 44, 12); } static inline bool set_saflok_mfc_sequence_and_combination(saflok_mfc_data_t *data, uint32_t sequence_and_combination) { if (sequence_and_combination > 0xFFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " sequence_and_combination out of range (%08x)\n", sequence_and_combination); return false; } - insert_bits(data, 44, 12, sequence_and_combination); + set_bitfield(data, 44, 12, sequence_and_combination); return true; } static inline bool get_saflok_mfc_deadbolt_override(const saflok_mfc_data_t *data) { - uint32_t result = extract_bits(data, 56, 1); + uint32_t result = get_bitfield(data, 56, 1); return (result != 0); } static inline bool set_saflok_mfc_deadbolt_override(saflok_mfc_data_t *data, uint32_t deadbolt_override) { - insert_bits(data, 56, 1, deadbolt_override ? 0x1 : 0x0); + set_bitfield(data, 56, 1, deadbolt_override ? 0x1 : 0x0); return true; } static inline uint8_t get_saflok_mfc_restricted_days(const saflok_mfc_data_t *data) { - return (uint8_t)extract_bits(data, 57, 7); + return (uint8_t)get_bitfield(data, 57, 7); } static inline bool set_saflok_mfc_restricted_days(saflok_mfc_data_t *data, uint32_t restricted_days) { if (restricted_days > 0x7Fu) { PrintAndLogEx(ERR, _RED_("ERROR:") " restricted_days out of range (%08x)\n", restricted_days); return false; } - insert_bits(data, 57, 7, restricted_days); + set_bitfield(data, 57, 7, restricted_days); return true; } static inline uint32_t get_saflok_mfc_raw_interval(const saflok_mfc_data_t *data) { - return extract_bits(data, 64, 24); + return get_bitfield(data, 64, 24); } static inline bool set_saflok_mfc_raw_interval(saflok_mfc_data_t *data, uint32_t raw_interval) { if (raw_interval > 0xFFFFFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval out of range (%08x)\n", raw_interval); return false; } - insert_bits(data, 64, 24, raw_interval); + set_bitfield(data, 64, 24, raw_interval); return true; } static inline uint32_t get_saflok_mfc_raw_card_creation_date(const saflok_mfc_data_t *data) { - return extract_bits(data, 88, 28); + return get_bitfield(data, 88, 28); } static inline bool set_saflok_mfc_raw_card_creation_date(saflok_mfc_data_t *data, uint32_t raw_card_creation_date) { if (raw_card_creation_date > 0x0FFFFFFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " raw_card_creation_date out of range (%08x)\n", raw_card_creation_date); return false; } - insert_bits(data, 88, 28, raw_card_creation_date); + set_bitfield(data, 88, 28, raw_card_creation_date); return true; } static inline uint16_t get_saflok_mfc_property_id(const saflok_mfc_data_t *data) { - return extract_bits(data, 116, 12); + return get_bitfield(data, 116, 12); } static inline bool set_saflok_mfc_property_id(saflok_mfc_data_t *data, uint32_t property_id) { if (property_id > 0xFFFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " property_id out of range (%08x)\n", property_id); return false; } - insert_bits(data, 116, 12, property_id); + set_bitfield(data, 116, 12, property_id); return true; } static inline uint8_t get_saflok_mfc_checksum(const saflok_mfc_data_t *data) { @@ -309,14 +294,7 @@ static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bi PrintAndLogEx(WARNING, "month out of range (%d)\n", date->month); result = false; } - uint8_t max_days = days_in_month_lookup[date->month]; - // Adjust for leap years - if ((date->month == 2u) && - (date->year % 4u == 0u) && - ((date->year % 100u != 0u) || (date->year % 400u == 0u)) - ) { - max_days = 29u; - } + uint8_t max_days = get_days_in_month(date->year, date->month); if ((date->day < 1u) || (date->day > max_days)) { PrintAndLogEx(WARNING, "day out of range (%d) for month %d year %d\n", date->day, date->month, date->year); result = false; @@ -718,11 +696,6 @@ _Static_assert(ARRAYLEN(level_names) == 16, "level_names must have 16 elements") static int CmdHelp(const char *Cmd); -// unsafe without static analysis hints. -// strCard : uint8_t[length] -// decryptedCard : uint8_t[length] -// length : ALWAYS == 17 -// safelok_mfc_data_t* for both parameters? static void saflok_decrypt(const saflok_mfc_data_t *encryptedCard, saflok_mfc_data_t *decryptedCard) { static const size_t length = ARRAYLEN(encryptedCard->raw); @@ -759,8 +732,6 @@ static void saflok_decrypt(const saflok_mfc_data_t *encryptedCard, saflok_mfc_da decryptedCard->raw[num2 - 1] = b; } } - - static void saflok_encrypt(const saflok_mfc_data_t *plaintext, saflok_mfc_data_t *encryptedCard) { static const size_t length = ARRAYLEN(plaintext->raw); int b = 0; @@ -795,22 +766,21 @@ static void saflok_encrypt(const saflok_mfc_data_t *plaintext, saflok_mfc_data_t encryptedCard->raw[i] = c_aEncode[j]; } } - -static uint32_t extract_bits(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits) { +static uint32_t get_bitfield(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits) { static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access - PrintAndLogEx(ERR, "extract_bits: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); + PrintAndLogEx(ERR, "get_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); return 0; } if (num_bits > 32) { // Exceeds maximum supported bit extraction - PrintAndLogEx(ERR, "extract_bits: out of bounds access (num_bits=%zu, max=32)\n", num_bits); + PrintAndLogEx(ERR, "get_bitfield: out of bounds access (num_bits=%zu, max=32)\n", num_bits); return 0; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access - PrintAndLogEx(ERR, "extract_bits: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); + PrintAndLogEx(ERR, "get_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); return 0; } @@ -825,26 +795,26 @@ static uint32_t extract_bits(const saflok_mfc_data_t *data, size_t start_bit, si return result; } -static void insert_bits(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value) { +static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value) { static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access - PrintAndLogEx(ERR, "insert_bits: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); + PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); return; } if (num_bits > 32) { // Exceeds maximum supported bit extraction - PrintAndLogEx(ERR, "insert_bits: num_bits exceeds 32\n", num_bits); + PrintAndLogEx(ERR, "set_bitfield: num_bits exceeds 32\n", num_bits); return; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access - PrintAndLogEx(ERR, "insert_bits: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); + PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); return; } if ((num_bits < 32) && ((value >> num_bits) != 0)) { // Value exceeds the size of the specified bit field - PrintAndLogEx(ERR, "insert_bits: value exceeds bit field size (value=%u (0x%08x), num_bits=%zu)\n", value, value, num_bits); + PrintAndLogEx(ERR, "set_bitfield: value exceeds bit field size (value=%u (0x%08x), num_bits=%zu)\n", value, value, num_bits); return; } @@ -986,23 +956,15 @@ static void saflok_encode( // return value ignored ... function here is void(!) set_saflok_mfc_card_creation_datetime(data, &creation_dt); } - //else{ - //insert_bits(data, 88, 28,card_creation_date); - //PrintAndLogEx(SUCCESS, "DT BITS INSERTED"); - //} saflok_mfc_datetime_t expiration_dt = {0}; if (sscanf(dt_expiration, fmt, &expiration_dt.year, &expiration_dt.month, &expiration_dt.day, &expiration_dt.hour, &expiration_dt.minute) == 5) { // return value ignored ... function here is void(!) set_saflok_mfc_card_expiration_datetime(data, &expiration_dt); } - //else{ - //insert_bits(data, 64, 24, expire_date); - //PrintAndLogEx(SUCCESS, "DTE BITS INSERTED"); - //} uint8_t checksum = calculated_saflok_checksum(data); - insert_bits(data, 128, 8, checksum); + set_bitfield(data, 128, 8, checksum); } @@ -1359,65 +1321,94 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { int result = PM3_SUCCESS; - PrintAndLogEx(WARNING, "NYI: Saflok self-test not yet implemented."); + PrintAndLogEx(WARNING, "NYI: Saflok self-test not yet fully implemented."); // TODO: encode some sample cards using prior versions of the code. // verify the resulting decoded data matches expectations, // or investigate which one results in correct data. /* - [ ] get_saflok_mfc_card_level - [ ] set_saflok_mfc_card_level - [ ] get_saflok_mfc_card_type - [ ] set_saflok_mfc_card_type - [ ] get_saflok_mfc_card_id - [ ] set_saflok_mfc_card_id - [ ] get_saflok_mfc_opening_key - [ ] set_saflok_mfc_opening_key - [ ] get_saflok_mfc_lock_id - [ ] set_saflok_mfc_lock_id - [ ] get_saflok_mfc_pass_number - [ ] set_saflok_mfc_pass_number - [ ] get_saflok_mfc_sequence_and_combination - [ ] set_saflok_mfc_sequence_and_combination - [ ] get_saflok_mfc_deadbolt_override - [ ] set_saflok_mfc_deadbolt_override - [ ] get_saflok_mfc_restricted_days - [ ] set_saflok_mfc_restricted_days - [ ] get_saflok_mfc_raw_interval_date - [ ] set_saflok_mfc_raw_interval_date - [ ] get_saflok_mfc_raw_card_creation_date - [ ] set_saflok_mfc_raw_card_creation_date - [ ] get_saflok_mfc_property_id - [ ] set_saflok_mfc_property_id - [ ] _get_saflok_mfc_card_creation_year_impl - [ ] _set_saflok_mfc_card_creation_year_impl - [ ] _get_saflok_mfc_card_creation_month_impl - [ ] _set_saflok_mfc_card_creation_month_impl - [ ] _get_saflok_mfc_card_creation_day_impl - [ ] _set_saflok_mfc_card_creation_day_impl - [ ] _get_saflok_mfc_card_creation_hour_impl - [ ] _set_saflok_mfc_card_creation_hour_impl - [ ] _get_saflok_mfc_card_creation_minute_impl - [ ] _set_saflok_mfc_card_creation_minute_impl - [ ] is_saflok_mfc_datetime_valid - [ ] get_saflok_mfc_card_creation_datetime - [ ] set_saflok_mfc_card_creation_datetime - [ ] _get_saflok_mfc_interval_years_impl - [ ] _set_saflok_mfc_interval_years_impl - [ ] _get_saflok_mfc_interval_months_impl - [ ] _set_saflok_mfc_interval_months_impl - [ ] _get_saflok_mfc_interval_days_impl - [ ] _set_saflok_mfc_interval_days_impl - [ ] _get_saflok_mfc_interval_hours_impl - [ ] _set_saflok_mfc_interval_hours_impl - [ ] _get_saflok_mfc_interval_minutes_impl - [ ] _set_saflok_mfc_interval_minutes_impl - [ ] get_saflok_mfc_interval - [ ] set_saflok_mfc_interval - [ ] add_offset - [ ] get_datetime_offset - [ ] get_saflok_mfc_card_expiration_datetime - [ ] set_saflok_mfc_card_expiration_datetime + if (true) { + [ ] get_saflok_mfc_card_level + [ ] set_saflok_mfc_card_level + } + if (true) { + [ ] get_saflok_mfc_card_type + [ ] set_saflok_mfc_card_type + } + if (true) { + [ ] get_saflok_mfc_card_id + [ ] set_saflok_mfc_card_id + } + if (true) { + [ ] get_saflok_mfc_opening_key + [ ] set_saflok_mfc_opening_key + } + if (true) { + [ ] get_saflok_mfc_lock_id + [ ] set_saflok_mfc_lock_id + } + if (true) { + [ ] get_saflok_mfc_pass_number + [ ] set_saflok_mfc_pass_number + } + if (true) { + [ ] get_saflok_mfc_sequence_and_combination + [ ] set_saflok_mfc_sequence_and_combination + } + if (true) { + [ ] get_saflok_mfc_deadbolt_override + [ ] set_saflok_mfc_deadbolt_override + } + if (true) { + [ ] get_saflok_mfc_restricted_days + [ ] set_saflok_mfc_restricted_days + } + if (true) { + [ ] get_saflok_mfc_raw_interval_date + [ ] set_saflok_mfc_raw_interval_date + } + if (true) { + [ ] get_saflok_mfc_raw_card_creation_date + [ ] set_saflok_mfc_raw_card_creation_date + } + if (true) { + [ ] get_saflok_mfc_property_id + [ ] set_saflok_mfc_property_id + } + if (true) { + [ ] _get_saflok_mfc_card_creation_year_impl + [ ] _set_saflok_mfc_card_creation_year_impl + [ ] _get_saflok_mfc_card_creation_month_impl + [ ] _set_saflok_mfc_card_creation_month_impl + [ ] _get_saflok_mfc_card_creation_day_impl + [ ] _set_saflok_mfc_card_creation_day_impl + [ ] _get_saflok_mfc_card_creation_hour_impl + [ ] _set_saflok_mfc_card_creation_hour_impl + [ ] _get_saflok_mfc_card_creation_minute_impl + [ ] _set_saflok_mfc_card_creation_minute_impl + [ ] is_saflok_mfc_datetime_valid + [ ] get_saflok_mfc_card_creation_datetime + [ ] set_saflok_mfc_card_creation_datetime + } + if (true) { + // NOTE: these require card creation date to be set first + [ ] _get_saflok_mfc_interval_years_impl + [ ] _set_saflok_mfc_interval_years_impl + [ ] _get_saflok_mfc_interval_months_impl + [ ] _set_saflok_mfc_interval_months_impl + [ ] _get_saflok_mfc_interval_days_impl + [ ] _set_saflok_mfc_interval_days_impl + [ ] _get_saflok_mfc_interval_hours_impl + [ ] _set_saflok_mfc_interval_hours_impl + [ ] _get_saflok_mfc_interval_minutes_impl + [ ] _set_saflok_mfc_interval_minutes_impl + [ ] get_saflok_mfc_interval + [ ] set_saflok_mfc_interval + [ ] add_offset + [ ] get_datetime_offset + [ ] get_saflok_mfc_card_expiration_datetime + [ ] set_saflok_mfc_card_expiration_datetime + } */ if (true) { // test encode+encrypt, including interval affected by leap-year From c98bf9ad3ad5a165e346dd074015c71c531b6f6f Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 02:34:28 -0800 Subject: [PATCH 18/28] Fix a few bugs Discovered by github copilot. Verified actual bugs by a human. Fixed by a human. --- client/src/cmdhfsaflok.c | 55 ++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 3713b3261..1dda08a05 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -209,7 +209,7 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b return get_bitfield(data, 116, 12); } static inline bool set_saflok_mfc_property_id(saflok_mfc_data_t *data, uint32_t property_id) { - if (property_id > 0xFFFFu) { + if (property_id > 0xFFFu) { PrintAndLogEx(ERR, _RED_("ERROR:") " property_id out of range (%08x)\n", property_id); return false; } @@ -261,7 +261,7 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b return ((data->raw[12] >> 3) & 0x1Fu); } static inline bool _set_saflok_mfc_card_creation_day_impl(saflok_mfc_data_t *data, uint8_t day) { - data->raw[12] = (data->raw[12] & 0xE0u) | ((day & 0x1Fu) << 3); + data->raw[12] = (data->raw[12] & 0x07u) | ((day & 0x1Fu) << 3); return true; } static inline uint8_t _get_saflok_mfc_card_creation_hour_impl(const saflok_mfc_data_t *data) { @@ -385,7 +385,7 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " interval days out of range (%u)\n", days); return false; } - data->raw[9] = (data->raw[9] & 0xC7u) | ((days & 0x1Fu) << 3); + data->raw[9] = (data->raw[9] & 0x07u) | ((days & 0x1Fu) << 3); return true; } static inline uint8_t _get_saflok_mfc_interval_hours_impl(const saflok_mfc_data_t *data) { @@ -459,8 +459,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b // luckily, there's already a standard for this... struct tm tm = { .tm_year = base->year - 1900, - .tm_mon = base->month - 1, - .tm_mday = base->day - 1, + .tm_mon = base->month, + .tm_mday = base->day, .tm_hour = base->hour, .tm_min = base->minute, .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute @@ -483,7 +483,7 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } // addition is easy... uint8_t days_in_month = get_days_in_month(tm.tm_year + 1900, tm.tm_mon + 1); - if (tm.tm_mday >= days_in_month) { + if (tm.tm_mday > days_in_month) { tm.tm_mon ++; tm.tm_mday -= days_in_month; } @@ -494,8 +494,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b // and finally convert back into saflok_mfc_datetime_t saflok_mfc_datetime_t result = { .year = tm.tm_year + 1900, - .month = tm.tm_mon + 1, - .day = tm.tm_mday + 1, + .month = tm.tm_mon, + .day = tm.tm_mday, .hour = tm.tm_hour, .minute = tm.tm_min }; @@ -804,7 +804,7 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } if (num_bits > 32) { // Exceeds maximum supported bit extraction - PrintAndLogEx(ERR, "set_bitfield: num_bits exceeds 32\n", num_bits); + PrintAndLogEx(ERR, "set_bitfield: num_bits exceeds 32\n"); return; } if (total_available_bits - start_bit < num_bits) { @@ -949,18 +949,35 @@ static void saflok_encode( set_saflok_mfc_property_id(data, property_id); // Parsing date time string from "YYYY-MM-DDTHH:mm" into `saflok_mfc_datetime_t` - static const char * fmt = "%4" SCNu16 "-%2" SCNu8 "-%2" SCNu8 "T%2" SCNu8 ":%2" SCNu8; + static const char * fmt = "%4u-%2u-%2uT%2u:%2u"; - saflok_mfc_datetime_t creation_dt = {0}; - if (sscanf(dt, fmt, &creation_dt.year, &creation_dt.month, &creation_dt.day, &creation_dt.hour, &creation_dt.minute) == 5) { - // return value ignored ... function here is void(!) - set_saflok_mfc_card_creation_datetime(data, &creation_dt); + if (true) { + saflok_mfc_datetime_t creation_dt = {0}; + unsigned int year, month, day, hour, minute; + if (sscanf(dt, fmt, &year, &month, &day, &hour, &minute) == 5) { + if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && hour <= 23 && minute <= 59) { + creation_dt.year = year; + creation_dt.month = (uint8_t)month; + creation_dt.day = (uint8_t)day; + creation_dt.hour = (uint8_t)hour; + creation_dt.minute = (uint8_t)minute; + set_saflok_mfc_card_creation_datetime(data, &creation_dt); + } + } } - - saflok_mfc_datetime_t expiration_dt = {0}; - if (sscanf(dt_expiration, fmt, &expiration_dt.year, &expiration_dt.month, &expiration_dt.day, &expiration_dt.hour, &expiration_dt.minute) == 5) { - // return value ignored ... function here is void(!) - set_saflok_mfc_card_expiration_datetime(data, &expiration_dt); + if (true) { + saflok_mfc_datetime_t expiration_dt = {0}; + unsigned int exp_year, exp_month, exp_day, exp_hour, exp_minute; + if (sscanf(dt_expiration, fmt, &exp_year, &exp_month, &exp_day, &exp_hour, &exp_minute) == 5) { + if (exp_month >= 1 && exp_month <= 12 && exp_day >= 1 && exp_day <= 31 && exp_hour <= 23 && exp_minute <= 59) { + expiration_dt.year = (uint16_t)exp_year; + expiration_dt.month = (uint8_t)exp_month; + expiration_dt.day = (uint8_t)exp_day; + expiration_dt.hour = (uint8_t)exp_hour; + expiration_dt.minute = (uint8_t)exp_minute; + set_saflok_mfc_card_expiration_datetime(data, &expiration_dt); + } + } } uint8_t checksum = calculated_saflok_checksum(data); From c5cec7ba515c1291402303fde37c6372bf8be0b3 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 03:02:37 -0800 Subject: [PATCH 19/28] Implement significant portions of saflok selftest --- client/src/cmdhfsaflok.c | 274 +++++++++++++++++++++++++++++++++------ 1 file changed, 234 insertions(+), 40 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 1dda08a05..9eafc2ef7 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1343,43 +1343,241 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { // verify the resulting decoded data matches expectations, // or investigate which one results in correct data. + // card_level field is 4 bits + if (true) { + int result_card_level = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint8_t i = 0; i < 16; ++i) { + bool success = set_saflok_mfc_card_level(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_card_level failed for value %u", i); + result_card_level = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_card_level(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_card_level returned %u, expected %u", x, i); + result_card_level = PM3_EFAILED; + } + } + } + if (result_card_level == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_card_level passed"); + } else { + result = PM3_EFAILED; + } + + } + // card_type field is 4 bits + if (true) { + int result_card_type = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint8_t i = 0; i < 16; ++i) { + bool success = set_saflok_mfc_card_type(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_card_type failed for value %u", i); + result_card_type = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_card_type(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_card_type returned %u, expected %u", x, i); + result_card_type = PM3_EFAILED; + } + } + } + if (result_card_type == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_card_type passed"); + } else { + result = PM3_EFAILED; + } + } + // card_id field is 8 bits + if (true) { + int result_card_id = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint16_t i = 0; i < 256; ++i) { + bool success = set_saflok_mfc_card_id(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_card_type failed for value %u", i); + result_card_id = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_card_id(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_card_id returned %u, expected %u", x, i); + result_card_id = PM3_EFAILED; + } + } + } + if (result_card_id == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_card_id passed"); + } else { + result = PM3_EFAILED; + } + } + // opening_key field is 2 bits + if (true) { + int result_opening_key = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint8_t i = 0; i < 4; ++i) { + bool success = set_saflok_mfc_opening_key(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_opening_key failed for value %u", i); + result_opening_key = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_opening_key(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_opening_key returned %u, expected %u", x, i); + result_opening_key = PM3_EFAILED; + } + } + } + if (result_opening_key == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_opening_key passed"); + } else { + result = PM3_EFAILED; + } + } + // lock_id field is 14 bits + if (true) { + int result_lock_id = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint16_t i = 0; i < 16384; i += 11) { // step by prime to test various bits + bool success = set_saflok_mfc_lock_id(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_lock_id failed for value %u", i); + result_lock_id = PM3_EFAILED; + } else { + uint16_t x = get_saflok_mfc_lock_id(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_lock_id returned %u, expected %u", x, i); + result_lock_id = PM3_EFAILED; + } + } + } + if (result_lock_id == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_lock_id passed"); + } else { + result = PM3_EFAILED; + } + } + // pass_number field is 12 bits + if (true) { + int result_pass_number = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint16_t i = 0; i < 4096; i += 7) { // step by prime to test various bits + bool success = set_saflok_mfc_pass_number(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_pass_number failed for value %u", i); + result_pass_number = PM3_EFAILED; + } else { + uint16_t x = get_saflok_mfc_pass_number(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_pass_number returned %u, expected %u", x, i); + result_pass_number = PM3_EFAILED; + } + } + } + if (result_pass_number == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_pass_number passed"); + } else { + result = PM3_EFAILED; + } + } + // sequence_and_combination field is 12 bits + if (true) { + int result_seq_combo = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint16_t i = 0; i < 4096; i += 5) { // step by prime to test various bits + bool success = set_saflok_mfc_sequence_and_combination(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_sequence_and_combination failed for value %u", i); + result_seq_combo = PM3_EFAILED; + } else { + uint16_t x = get_saflok_mfc_sequence_and_combination(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_sequence_and_combination returned %u, expected %u", x, i); + result_seq_combo = PM3_EFAILED; + } + } + } + if (result_seq_combo == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_sequence_and_combination passed"); + } else { + result = PM3_EFAILED; + } + } + // deadbolt_override field is 1 bits + if (true) { + int result_deadbolt = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint8_t i = 0; i < 2; ++i) { + bool success = set_saflok_mfc_deadbolt_override(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_deadbolt_override failed for value %u", i); + result_deadbolt = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_deadbolt_override(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_deadbolt_override returned %u, expected %u", x, i); + result_deadbolt = PM3_EFAILED; + } + } + } + if (result_deadbolt == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_deadbolt_override passed"); + } else { + result = PM3_EFAILED; + } + } + // restricted_days field is 7 bits + if (true) { + int result_restricted_days = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint8_t i = 0; i < 128; ++i) { + bool success = set_saflok_mfc_restricted_days(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_restricted_days failed for value %u", i); + result_restricted_days = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_restricted_days(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_restricted_days returned %u, expected %u", x, i); + result_restricted_days = PM3_EFAILED; + } + } + } + if (result_restricted_days == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_restricted_days passed"); + } else { + result = PM3_EFAILED; + } + } + // property_id field is 12 bits + if (true) { + int result_property_id = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (uint16_t i = 0; i < 4096; i += 13) { // step by prime to test various bits + bool success = set_saflok_mfc_property_id(&testdata, i); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_property_id failed for value %u", i); + result_property_id = PM3_EFAILED; + } else { + uint16_t x = get_saflok_mfc_property_id(&testdata); + if (x != i) { + PrintAndLogEx(FAILED, "get_saflok_mfc_property_id returned %u, expected %u", x, i); + result_property_id = PM3_EFAILED; + } + } + } + if (result_property_id == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_property_id passed"); + } else { + result = PM3_EFAILED; + } + } + // raw_interval field is 20 bits -- special set processing needed + // raw_expire_date field is 24 bits -- special set processing needed + // raw_card_creation_date field is 28 bits -- special set processing needed /* - if (true) { - [ ] get_saflok_mfc_card_level - [ ] set_saflok_mfc_card_level - } - if (true) { - [ ] get_saflok_mfc_card_type - [ ] set_saflok_mfc_card_type - } - if (true) { - [ ] get_saflok_mfc_card_id - [ ] set_saflok_mfc_card_id - } - if (true) { - [ ] get_saflok_mfc_opening_key - [ ] set_saflok_mfc_opening_key - } - if (true) { - [ ] get_saflok_mfc_lock_id - [ ] set_saflok_mfc_lock_id - } - if (true) { - [ ] get_saflok_mfc_pass_number - [ ] set_saflok_mfc_pass_number - } - if (true) { - [ ] get_saflok_mfc_sequence_and_combination - [ ] set_saflok_mfc_sequence_and_combination - } - if (true) { - [ ] get_saflok_mfc_deadbolt_override - [ ] set_saflok_mfc_deadbolt_override - } - if (true) { - [ ] get_saflok_mfc_restricted_days - [ ] set_saflok_mfc_restricted_days - } if (true) { [ ] get_saflok_mfc_raw_interval_date [ ] set_saflok_mfc_raw_interval_date @@ -1388,10 +1586,6 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { [ ] get_saflok_mfc_raw_card_creation_date [ ] set_saflok_mfc_raw_card_creation_date } - if (true) { - [ ] get_saflok_mfc_property_id - [ ] set_saflok_mfc_property_id - } if (true) { [ ] _get_saflok_mfc_card_creation_year_impl [ ] _set_saflok_mfc_card_creation_year_impl From 4cb20bde5fbc4c0b41e6800537f122a98161a485 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 03:18:03 -0800 Subject: [PATCH 20/28] saflok selftest -- get_/set_bitfield --- client/src/cmdhfsaflok.c | 97 ++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 9eafc2ef7..3f3af45a3 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -81,7 +81,7 @@ typedef struct _saflok_mfc_datetime_offset_t { #define KEY_LENGTH sizeof(saflok_mfc_key_t) static uint32_t get_bitfield(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits); -static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value); +static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value); @@ -95,8 +95,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " card_level out of range (%08x)\n", card_level); return false; } - set_bitfield(data, 0, 4, card_level); - return true; + bool success = set_bitfield(data, 0, 4, card_level); + return success; } static inline uint8_t get_saflok_mfc_card_type(const saflok_mfc_data_t *data) { return (uint8_t)get_bitfield(data, 4, 4); @@ -106,8 +106,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " card_type out of range (%08x)\n", card_type); return false; } - set_bitfield(data, 4, 4, card_type); - return true; + bool success = set_bitfield(data, 4, 4, card_type); + return success; } static inline uint8_t get_saflok_mfc_card_id(const saflok_mfc_data_t *data) { return (uint8_t)get_bitfield(data, 8, 8); @@ -117,8 +117,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " card_id out of range (%08x)\n", card_id); return false; } - set_bitfield(data, 8, 8, card_id); - return true; + bool success = set_bitfield(data, 8, 8, card_id); + return success; } static inline uint8_t get_saflok_mfc_opening_key(const saflok_mfc_data_t *data) { return (uint8_t)get_bitfield(data, 16, 2); @@ -128,8 +128,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " opening_key out of range (%08x)\n", opening_key); return false; } - set_bitfield(data, 16, 2, opening_key); - return true; + bool success = set_bitfield(data, 16, 2, opening_key); + return success; } static inline uint16_t get_saflok_mfc_lock_id(const saflok_mfc_data_t *data) { return (uint16_t)get_bitfield(data, 18, 14); @@ -139,8 +139,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " lock_id out of range (%08x)\n", lock_id); return false; } - set_bitfield(data, 18, 14, lock_id); - return true; + bool success = set_bitfield(data, 18, 14, lock_id); + return success; } static inline uint16_t get_saflok_mfc_pass_number(const saflok_mfc_data_t *data) { return (uint16_t)get_bitfield(data, 32, 12); @@ -150,8 +150,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " pass_number out of range (%08x)\n", pass_number); return false; } - set_bitfield(data, 32, 12, pass_number); - return true; + bool success = set_bitfield(data, 32, 12, pass_number); + return success; } static inline uint16_t get_saflok_mfc_sequence_and_combination(const saflok_mfc_data_t *data) { return (uint16_t)get_bitfield(data, 44, 12); @@ -161,16 +161,16 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " sequence_and_combination out of range (%08x)\n", sequence_and_combination); return false; } - set_bitfield(data, 44, 12, sequence_and_combination); - return true; + bool success = set_bitfield(data, 44, 12, sequence_and_combination); + return success; } static inline bool get_saflok_mfc_deadbolt_override(const saflok_mfc_data_t *data) { uint32_t result = get_bitfield(data, 56, 1); return (result != 0); } static inline bool set_saflok_mfc_deadbolt_override(saflok_mfc_data_t *data, uint32_t deadbolt_override) { - set_bitfield(data, 56, 1, deadbolt_override ? 0x1 : 0x0); - return true; + bool success = set_bitfield(data, 56, 1, deadbolt_override ? 0x1 : 0x0); + return success; } static inline uint8_t get_saflok_mfc_restricted_days(const saflok_mfc_data_t *data) { return (uint8_t)get_bitfield(data, 57, 7); @@ -180,8 +180,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " restricted_days out of range (%08x)\n", restricted_days); return false; } - set_bitfield(data, 57, 7, restricted_days); - return true; + bool success = set_bitfield(data, 57, 7, restricted_days); + return success; } static inline uint32_t get_saflok_mfc_raw_interval(const saflok_mfc_data_t *data) { return get_bitfield(data, 64, 24); @@ -191,8 +191,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval out of range (%08x)\n", raw_interval); return false; } - set_bitfield(data, 64, 24, raw_interval); - return true; + bool success = set_bitfield(data, 64, 24, raw_interval); + return success; } static inline uint32_t get_saflok_mfc_raw_card_creation_date(const saflok_mfc_data_t *data) { return get_bitfield(data, 88, 28); @@ -202,8 +202,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " raw_card_creation_date out of range (%08x)\n", raw_card_creation_date); return false; } - set_bitfield(data, 88, 28, raw_card_creation_date); - return true; + bool success = set_bitfield(data, 88, 28, raw_card_creation_date); + return success; } static inline uint16_t get_saflok_mfc_property_id(const saflok_mfc_data_t *data) { return get_bitfield(data, 116, 12); @@ -213,8 +213,8 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b PrintAndLogEx(ERR, _RED_("ERROR:") " property_id out of range (%08x)\n", property_id); return false; } - set_bitfield(data, 116, 12, property_id); - return true; + bool success = set_bitfield(data, 116, 12, property_id); + return success; } static inline uint8_t get_saflok_mfc_checksum(const saflok_mfc_data_t *data) { return data->raw[16]; @@ -795,27 +795,27 @@ static uint32_t get_bitfield(const saflok_mfc_data_t *data, size_t start_bit, si return result; } -static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value) { +static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_bits, uint32_t value) { static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); - return; + return false; } if (num_bits > 32) { // Exceeds maximum supported bit extraction PrintAndLogEx(ERR, "set_bitfield: num_bits exceeds 32\n"); - return; + return false; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); - return; + return false; } if ((num_bits < 32) && ((value >> num_bits) != 0)) { // Value exceeds the size of the specified bit field PrintAndLogEx(ERR, "set_bitfield: value exceeds bit field size (value=%u (0x%08x), num_bits=%zu)\n", value, value, num_bits); - return; + return false; } @@ -828,6 +828,7 @@ static void set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b data->raw[byte_index] = (data->raw[byte_index] & ~(1 << bit_index)) | (bit_value << bit_index); } + return true; } // Static analysis hint: reads all bytes of `data[len]` @@ -1343,6 +1344,36 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { // verify the resulting decoded data matches expectations, // or investigate which one results in correct data. + // get/set bitfield tests + if (true) { + int result_bitfield = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (size_t start_bit = 0; start_bit < ARRAYLEN(testdata.raw) * 8; start_bit += 3) { + for (size_t num_bits = 1; num_bits <= 32 && (start_bit + num_bits) <= (ARRAYLEN(testdata.raw) * 8); ++num_bits) { + uint64_t max_test_value = (num_bits == 32) ? 0xFFFFFFFFu : ((1ULL << num_bits) - 1); + for (uint64_t iter = 0u; iter <= max_test_value; iter += (max_test_value / 1024) + 1) { + uint32_t value_set = (uint32_t)iter; + bool success = set_bitfield(&testdata, start_bit, num_bits, value_set); + if (!success) { + PrintAndLogEx(FAILED, "set_bitfield failed for start_bit %zu, num_bits %zu, test_value %llu", start_bit, num_bits, value_set); + result_bitfield = PM3_EFAILED; + } + uint32_t x = get_bitfield(&testdata, start_bit, num_bits); + if (x != value_set) { + PrintAndLogEx(FAILED, "get/set_bitfield failed for start_bit %zu, num_bits %zu, test_value %llu: got %u, expected %llu", start_bit, num_bits, value_set, x, value_set); + result_bitfield = PM3_EFAILED; + } + // Clear the field for the next test + memset(testdata.raw, 0, sizeof(testdata.raw)); + } + } + } + if (result_bitfield == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_bitfield passed"); + } else { + result = PM3_EFAILED; + } + } // card_level field is 4 bits if (true) { int result_card_level = PM3_SUCCESS; @@ -1574,10 +1605,10 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { result = PM3_EFAILED; } } - // raw_interval field is 20 bits -- special set processing needed - // raw_expire_date field is 24 bits -- special set processing needed - // raw_card_creation_date field is 28 bits -- special set processing needed /* + // raw_interval field is 20 bits -- special set processing needed + // raw_expire_date field is 24 bits -- special set processing needed + // raw_card_creation_date field is 28 bits -- special set processing needed if (true) { [ ] get_saflok_mfc_raw_interval_date [ ] set_saflok_mfc_raw_interval_date From 3560850e77d7b57d43ca8f1109b7588303a089e5 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 03:23:55 -0800 Subject: [PATCH 21/28] revert removal of waveshare --- client/src/pm3line_vocabulary.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 9d99a6486..cdfdfc8ab 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -551,6 +551,8 @@ const static vocabulary_t vocabulary[] = { { 1, "hf vas help" }, { 0, "hf vas reader" }, { 1, "hf vas decrypt" }, + { 1, "hf waveshare help" }, + { 1, "hf waveshare load" }, { 1, "hf xerox help" }, { 1, "hf xerox list" }, { 0, "hf xerox info" }, From b95a9cf8b656bc8a84be35ccd7e3a693685e0516 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 03:51:47 -0800 Subject: [PATCH 22/28] Saflok: Comment the two assert()s for why they are programmer errors that must cause client to exit. --- client/src/cmdhfsaflok.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 3f3af45a3..13ab67e07 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -40,7 +40,7 @@ static const uint8_t _days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { - assert(month >= 1 && month <= 12); // this uses 1-based indexing! + assert(month >= 1 && month <= 12); // programmer error ... this function uses 1-based indexing! Must fault here to prevent wrong data from propagating .... uint8_t days = _days_in_month_lookup[month]; // Adjust for leap years if ((month == 2u) && @@ -839,7 +839,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static char *bytes_to_hex(const uint8_t *data, size_t len) { static char buf[256]; // WARNING: caller must immediately use or copy the result, and it's still not thread-safe! // BUGBUG: previously had no bounds checking on len! - assert(len < (ARRAYLEN(buf)/2)); + assert(len < (ARRAYLEN(buf)/2)); // calling with larger length is a programming error which previously caused silent data corruption. len = (len < (ARRAYLEN(buf)/2)) ? len : (ARRAYLEN(buf)/2) - 1; // prevent buffer overflow for (size_t i = 0; i < len; i++) { sprintf(buf + (i * 2), "%02X", data[i]); From 559d63cc354430a36f40d8fb42eea38287de3afe Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:30:03 -0800 Subject: [PATCH 23/28] Fix regression in expiration date handling --- client/src/cmdhfsaflok.c | 121 ++++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 26 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 13ab67e07..6ee77015f 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -455,50 +455,103 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b const saflok_mfc_datetime_t *base, const saflok_mfc_datetime_offset_t offset ) { + + static const int MY_DEBUG_LEVEL = DEBUG; // adding dates is non-trivial. // luckily, there's already a standard for this... struct tm tm = { - .tm_year = base->year - 1900, - .tm_mon = base->month, - .tm_mday = base->day, - .tm_hour = base->hour, - .tm_min = base->minute, + .tm_year = base->year - 1900, // years since 1900 + .tm_mon = base->month - 1, // range: [0..11] + .tm_mday = base->day, // range: [1..31] + .tm_hour = base->hour, // range: [0..23] + .tm_min = base->minute, // range: [0..59] .tm_sec = 30, // selecting midpoint to avoid leapseconds changing minute .tm_isdst = 0, // UTC has no DST }; + + PrintAndLogEx(MY_DEBUG_LEVEL, "Base date: %04u-%02u-%02uT%02u:%02u\n", + base->year, base->month, base->day, + base->hour, base->minute + ); + PrintAndLogEx(MY_DEBUG_LEVEL, "As struct tm: %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); + PrintAndLogEx(MY_DEBUG_LEVEL, "Offset to add: %u years, %u months, %u days, %u hours, %u minutes\n", + offset.years, offset.months, offset.days, + offset.hours, offset.minutes + ); + // add the offsets ... and it's OK for them to be out of range tm.tm_year += offset.years; tm.tm_mon += offset.months; tm.tm_mday += offset.days; tm.tm_hour += offset.hours; tm.tm_min += offset.minutes; - // now deal with carrying overflows + + PrintAndLogEx(MY_DEBUG_LEVEL, "With Offset (pre-normalization): %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); + if (tm.tm_min >= 60) { - tm.tm_hour ++; - tm.tm_min -= 60; + tm.tm_hour += tm.tm_min / 60; + tm.tm_min = tm.tm_min % 60; + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized minutes: %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); + } if (tm.tm_hour >= 24) { - tm.tm_mday ++; - tm.tm_hour -= 24; + tm.tm_mday += tm.tm_hour / 24; + tm.tm_hour = tm.tm_hour % 24; + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized hours: %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); } - // addition is easy... - uint8_t days_in_month = get_days_in_month(tm.tm_year + 1900, tm.tm_mon + 1); - if (tm.tm_mday > days_in_month) { - tm.tm_mon ++; + // variable days per month ... loop is simplest option + for (uint8_t days_in_month = get_days_in_month(tm.tm_year + 1900, tm.tm_mon + 1); + tm.tm_mday > days_in_month; + days_in_month = get_days_in_month(tm.tm_year + 1900, tm.tm_mon + 1) + ) { + tm.tm_mon ++; tm.tm_mday -= days_in_month; + if (tm.tm_mon >= 12) { + tm.tm_year ++; + tm.tm_mon -= 12; + } + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized days: %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); } if (tm.tm_mon >= 12) { - tm.tm_year ++; - tm.tm_mon -= 12; + tm.tm_year += tm.tm_mon / 12; + tm.tm_mon = tm.tm_mon % 12; + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized months: %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); } + PrintAndLogEx(MY_DEBUG_LEVEL, "Final normalized date: %04d-%02d-%02dT%02d:%02d\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min + ); + // and finally convert back into saflok_mfc_datetime_t saflok_mfc_datetime_t result = { .year = tm.tm_year + 1900, - .month = tm.tm_mon, + .month = tm.tm_mon + 1, .day = tm.tm_mday, .hour = tm.tm_hour, .minute = tm.tm_min }; + PrintAndLogEx(MY_DEBUG_LEVEL, "Converted back to saflok_mfc_datetime_t: %04d-%02d-%02dT%02d:%02d\n", + result.year, result.month, result.day, + result.hour, result.minute + ); return result; } static saflok_mfc_datetime_offset_t get_datetime_offset( @@ -573,9 +626,22 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static saflok_mfc_datetime_t get_saflok_mfc_card_expiration_datetime( const saflok_mfc_data_t *data ) { + static const int MY_DEBUG_LEVEL = DEBUG; saflok_mfc_datetime_t creation_date = get_saflok_mfc_card_creation_datetime(data); saflok_mfc_datetime_offset_t offset = get_saflok_mfc_interval(data); + PrintAndLogEx(MY_DEBUG_LEVEL, "Card Creation Date: %04u-%02u-%02uT%02u:%02u", + creation_date.year, creation_date.month, creation_date.day, + creation_date.hour, creation_date.minute + ); + PrintAndLogEx(MY_DEBUG_LEVEL, "Card Interval Offset: %u years, %u months, %u days, %u hours, %u minutes\n", + offset.years, offset.months, offset.days, + offset.hours, offset.minutes + ); saflok_mfc_datetime_t expiration = add_offset(&creation_date, offset); + PrintAndLogEx(MY_DEBUG_LEVEL, "Calculated Expiration Date: %04u-%02u-%02uT%02u:%02u\n", + expiration.year, expiration.month, expiration.day, + expiration.hour, expiration.minute + ); return expiration; } static bool set_saflok_mfc_card_expiration_datetime( @@ -838,13 +904,14 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b // NOTE: apparently accepted for this legacy codebase. static char *bytes_to_hex(const uint8_t *data, size_t len) { static char buf[256]; // WARNING: caller must immediately use or copy the result, and it's still not thread-safe! + static const size_t maximum_data_len = (ARRAYLEN(buf) / 2) - 1; // leave room for null terminator // BUGBUG: previously had no bounds checking on len! - assert(len < (ARRAYLEN(buf)/2)); // calling with larger length is a programming error which previously caused silent data corruption. - len = (len < (ARRAYLEN(buf)/2)) ? len : (ARRAYLEN(buf)/2) - 1; // prevent buffer overflow + assert(len <= maximum_data_len); // calling with larger length is a programming error which previously caused silent data corruption. + len = (len < maximum_data_len) ? len : maximum_data_len; + memset(buf, 0, sizeof(buf)); // Clear the buffer before use for (size_t i = 0; i < len; i++) { sprintf(buf + (i * 2), "%02X", data[i]); } - buf[len * 2] = '\0'; return buf; } @@ -1339,7 +1406,7 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { int result = PM3_SUCCESS; - PrintAndLogEx(WARNING, "NYI: Saflok self-test not yet fully implemented."); + PrintAndLogEx(WARNING, "Saflok self-test is non-exhaustive, 4-byte MFC only."); // TODO: encode some sample cards using prior versions of the code. // verify the resulting decoded data matches expectations, // or investigate which one results in correct data. @@ -1355,12 +1422,12 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { uint32_t value_set = (uint32_t)iter; bool success = set_bitfield(&testdata, start_bit, num_bits, value_set); if (!success) { - PrintAndLogEx(FAILED, "set_bitfield failed for start_bit %zu, num_bits %zu, test_value %llu", start_bit, num_bits, value_set); + PrintAndLogEx(FAILED, "set_bitfield failed for start_bit %zu, num_bits %zu, test_value " PRIx32, start_bit, num_bits, value_set); result_bitfield = PM3_EFAILED; } uint32_t x = get_bitfield(&testdata, start_bit, num_bits); if (x != value_set) { - PrintAndLogEx(FAILED, "get/set_bitfield failed for start_bit %zu, num_bits %zu, test_value %llu: got %u, expected %llu", start_bit, num_bits, value_set, x, value_set); + PrintAndLogEx(FAILED, "get/set_bitfield failed for start_bit %zu, num_bits %zu, test_value " PRIx32 ": got " PRIx32", expected " PRIx32, start_bit, num_bits, value_set, x, value_set); result_bitfield = PM3_EFAILED; } // Clear the field for the next test @@ -1617,6 +1684,11 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { [ ] get_saflok_mfc_raw_card_creation_date [ ] set_saflok_mfc_raw_card_creation_date } + if (true) { + [ ] is_saflok_mfc_datetime_valid + [ ] add_offset + [ ] get_datetime_offset + } if (true) { [ ] _get_saflok_mfc_card_creation_year_impl [ ] _set_saflok_mfc_card_creation_year_impl @@ -1628,7 +1700,6 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { [ ] _set_saflok_mfc_card_creation_hour_impl [ ] _get_saflok_mfc_card_creation_minute_impl [ ] _set_saflok_mfc_card_creation_minute_impl - [ ] is_saflok_mfc_datetime_valid [ ] get_saflok_mfc_card_creation_datetime [ ] set_saflok_mfc_card_creation_datetime } @@ -1646,8 +1717,6 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { [ ] _set_saflok_mfc_interval_minutes_impl [ ] get_saflok_mfc_interval [ ] set_saflok_mfc_interval - [ ] add_offset - [ ] get_datetime_offset [ ] get_saflok_mfc_card_expiration_datetime [ ] set_saflok_mfc_card_expiration_datetime } From 4c10238707bd56da3f3797e72041e55eb151a4d7 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:35:30 -0800 Subject: [PATCH 24/28] saflok - removal of assert() per iceman request --- client/src/cmdhfsaflok.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 6ee77015f..5ff613c57 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -40,7 +40,11 @@ static const uint8_t _days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { - assert(month >= 1 && month <= 12); // programmer error ... this function uses 1-based indexing! Must fault here to prevent wrong data from propagating .... + if (month < 1 || month > 12) { // programmer error ... this function uses 1-based indexing! + PrintAndLogEx(ERR, "Invalid month lookup: %d, expected range [1..12] ... returning 0 days!\n", month); + return 0; + } + uint8_t days = _days_in_month_lookup[month]; // Adjust for leap years if ((month == 2u) && @@ -905,9 +909,11 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static char *bytes_to_hex(const uint8_t *data, size_t len) { static char buf[256]; // WARNING: caller must immediately use or copy the result, and it's still not thread-safe! static const size_t maximum_data_len = (ARRAYLEN(buf) / 2) - 1; // leave room for null terminator - // BUGBUG: previously had no bounds checking on len! - assert(len <= maximum_data_len); // calling with larger length is a programming error which previously caused silent data corruption. - len = (len < maximum_data_len) ? len : maximum_data_len; + + if (len > maximum_data_len) { + PrintAndLogEx(ERR, "saflok bytes_to_hex: input length %zu exceeds maximum supported %zu, results will be truncated!\n", len, maximum_data_len); + len = maximum_data_len; + } memset(buf, 0, sizeof(buf)); // Clear the buffer before use for (size_t i = 0; i < len; i++) { sprintf(buf + (i * 2), "%02X", data[i]); From 35afc30e2f692eda9492200a63e8ae8f93cf0b6f Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:38:30 -0800 Subject: [PATCH 25/28] saflok - remove trailing `\n` from calls to `PrintAndLogEx()` --- client/src/cmdhfsaflok.c | 116 +++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 5ff613c57..4c5143731 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -41,7 +41,7 @@ static const uint8_t _days_in_month_lookup[] = {0, 31, 28, 31, 30, 31, 30, 31, 3 static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { if (month < 1 || month > 12) { // programmer error ... this function uses 1-based indexing! - PrintAndLogEx(ERR, "Invalid month lookup: %d, expected range [1..12] ... returning 0 days!\n", month); + PrintAndLogEx(ERR, "Invalid month lookup: %d, expected range [1..12] ... returning 0 days!", month); return 0; } @@ -96,7 +96,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_card_level(saflok_mfc_data_t *data, uint32_t card_level) { if (card_level > 0xFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " card_level out of range (%08x)\n", card_level); + PrintAndLogEx(ERR, _RED_("ERROR:") " card_level out of range (%08x)", card_level); return false; } bool success = set_bitfield(data, 0, 4, card_level); @@ -107,7 +107,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_card_type(saflok_mfc_data_t *data, uint32_t card_type) { if (card_type > 0xFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " card_type out of range (%08x)\n", card_type); + PrintAndLogEx(ERR, _RED_("ERROR:") " card_type out of range (%08x)", card_type); return false; } bool success = set_bitfield(data, 4, 4, card_type); @@ -118,7 +118,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_card_id(saflok_mfc_data_t *data, uint32_t card_id) { if (card_id > 0xFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " card_id out of range (%08x)\n", card_id); + PrintAndLogEx(ERR, _RED_("ERROR:") " card_id out of range (%08x)", card_id); return false; } bool success = set_bitfield(data, 8, 8, card_id); @@ -129,7 +129,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_opening_key(saflok_mfc_data_t *data, uint32_t opening_key) { if (opening_key > 0x3u) { - PrintAndLogEx(ERR, _RED_("ERROR:") " opening_key out of range (%08x)\n", opening_key); + PrintAndLogEx(ERR, _RED_("ERROR:") " opening_key out of range (%08x)", opening_key); return false; } bool success = set_bitfield(data, 16, 2, opening_key); @@ -140,7 +140,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_lock_id(saflok_mfc_data_t *data, uint32_t lock_id) { if (lock_id > 0x3FFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " lock_id out of range (%08x)\n", lock_id); + PrintAndLogEx(ERR, _RED_("ERROR:") " lock_id out of range (%08x)", lock_id); return false; } bool success = set_bitfield(data, 18, 14, lock_id); @@ -151,7 +151,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_pass_number(saflok_mfc_data_t *data, uint32_t pass_number) { if (pass_number > 0xFFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " pass_number out of range (%08x)\n", pass_number); + PrintAndLogEx(ERR, _RED_("ERROR:") " pass_number out of range (%08x)", pass_number); return false; } bool success = set_bitfield(data, 32, 12, pass_number); @@ -162,7 +162,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_sequence_and_combination(saflok_mfc_data_t *data, uint32_t sequence_and_combination) { if (sequence_and_combination > 0xFFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " sequence_and_combination out of range (%08x)\n", sequence_and_combination); + PrintAndLogEx(ERR, _RED_("ERROR:") " sequence_and_combination out of range (%08x)", sequence_and_combination); return false; } bool success = set_bitfield(data, 44, 12, sequence_and_combination); @@ -181,7 +181,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_restricted_days(saflok_mfc_data_t *data, uint32_t restricted_days) { if (restricted_days > 0x7Fu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " restricted_days out of range (%08x)\n", restricted_days); + PrintAndLogEx(ERR, _RED_("ERROR:") " restricted_days out of range (%08x)", restricted_days); return false; } bool success = set_bitfield(data, 57, 7, restricted_days); @@ -192,7 +192,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_raw_interval(saflok_mfc_data_t *data, uint32_t raw_interval) { if (raw_interval > 0xFFFFFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval out of range (%08x)\n", raw_interval); + PrintAndLogEx(ERR, _RED_("ERROR:") " raw_interval out of range (%08x)", raw_interval); return false; } bool success = set_bitfield(data, 64, 24, raw_interval); @@ -203,7 +203,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_raw_card_creation_date(saflok_mfc_data_t *data, uint32_t raw_card_creation_date) { if (raw_card_creation_date > 0x0FFFFFFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " raw_card_creation_date out of range (%08x)\n", raw_card_creation_date); + PrintAndLogEx(ERR, _RED_("ERROR:") " raw_card_creation_date out of range (%08x)", raw_card_creation_date); return false; } bool success = set_bitfield(data, 88, 28, raw_card_creation_date); @@ -214,7 +214,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_property_id(saflok_mfc_data_t *data, uint32_t property_id) { if (property_id > 0xFFFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " property_id out of range (%08x)\n", property_id); + PrintAndLogEx(ERR, _RED_("ERROR:") " property_id out of range (%08x)", property_id); return false; } bool success = set_bitfield(data, 116, 12, property_id); @@ -239,7 +239,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool _set_saflok_mfc_card_creation_year_impl(saflok_mfc_data_t *data, uint16_t year) { if (year < 1980u || year > (1980u+0x7Fu)) { - PrintAndLogEx(ERR, _RED_("ERROR:") " year out of range (%u)\n", year); + PrintAndLogEx(ERR, _RED_("ERROR:") " year out of range (%u)", year); return false; } uint16_t year_bits = year - 1980u; @@ -255,7 +255,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool _set_saflok_mfc_card_creation_month_impl(saflok_mfc_data_t *data, uint8_t month) { if (month < 1 || month > 12) { - PrintAndLogEx(ERR, _RED_("ERROR:") " month out of range (%u)\n", month); + PrintAndLogEx(ERR, _RED_("ERROR:") " month out of range (%u)", month); return false; } data->raw[11] = (data->raw[11] & 0xF0u) | (month & 0x0Fu); @@ -291,24 +291,24 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static inline bool is_saflok_mfc_datetime_valid(const saflok_mfc_datetime_t *date) { bool result = true; if ((date->year < 1980) || (date->year > (1980u+0xFFu)) ) { - PrintAndLogEx(WARNING, "year out of range (%d)\n", date->year); + PrintAndLogEx(WARNING, "year out of range (%d)", date->year); result = false; } if ((date->month < 1) || (date->month > 12)) { - PrintAndLogEx(WARNING, "month out of range (%d)\n", date->month); + PrintAndLogEx(WARNING, "month out of range (%d)", date->month); result = false; } uint8_t max_days = get_days_in_month(date->year, date->month); if ((date->day < 1u) || (date->day > max_days)) { - PrintAndLogEx(WARNING, "day out of range (%d) for month %d year %d\n", date->day, date->month, date->year); + PrintAndLogEx(WARNING, "day out of range (%d) for month %d year %d", date->day, date->month, date->year); result = false; } if (date->hour > 23) { - PrintAndLogEx(WARNING, "hour out of range (%d)\n", date->hour); + PrintAndLogEx(WARNING, "hour out of range (%d)", date->hour); result = false; } if (date->minute > 59) { - PrintAndLogEx(WARNING, "minute out of range (%d)\n", date->minute); + PrintAndLogEx(WARNING, "minute out of range (%d)", date->minute); result = false; } return result; @@ -324,7 +324,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b .minute = _get_saflok_mfc_card_creation_minute_impl(data) }; if (!is_saflok_mfc_datetime_valid(&date)) { - PrintAndLogEx(WARNING, "Warning: creation date appears invalid: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(WARNING, "Warning: creation date appears invalid: %04d-%02d-%02dT%02d:%02d", date.year, date.month, date.day, date.hour, date.minute ); } @@ -332,7 +332,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool set_saflok_mfc_card_creation_datetime(saflok_mfc_data_t * data, const saflok_mfc_datetime_t * date) { if (!is_saflok_mfc_datetime_valid(date)) { - PrintAndLogEx(ERR, _RED_("ERROR:") " creation date is invalid\n"); + PrintAndLogEx(ERR, _RED_("ERROR:") " creation date is invalid"); return false; } bool result = @@ -342,7 +342,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b _set_saflok_mfc_card_creation_hour_impl(data, date->hour) && _set_saflok_mfc_card_creation_minute_impl(data, date->minute); if (!result) { - PrintAndLogEx(ERR, _RED_("ERROR:") " failed to set creation date of %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(ERR, _RED_("ERROR:") " failed to set creation date of %04d-%02d-%02dT%02d:%02d", date->year, date->month, date->day, date->hour, date->minute ); } @@ -356,7 +356,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b } static inline bool _set_saflok_mfc_interval_years_impl(saflok_mfc_data_t *data, uint32_t years) { if (years > 0xFu) { - PrintAndLogEx(ERR, _RED_("ERROR:") " interval years out of range (%u)\n", years); + PrintAndLogEx(ERR, _RED_("ERROR:") " interval years out of range (%u)", years); return false; } data->raw[8] = (data->raw[8] & 0x0Fu) | ((years & 0xFFu) << 4); @@ -365,13 +365,13 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static inline uint8_t _get_saflok_mfc_interval_months_impl(const saflok_mfc_data_t *data) { uint8_t months = data->raw[8] & 0x0Fu; if (months >= 12u) { - PrintAndLogEx(WARNING, "Warning: interval months appears invalid (%u)\n", months); + PrintAndLogEx(WARNING, "Warning: interval months appears invalid (%u)", months); } return months; } static inline bool _set_saflok_mfc_interval_months_impl(saflok_mfc_data_t *data, uint8_t months) { if (months >= 12u) { - PrintAndLogEx(ERR, _RED_("ERROR:") " interval months out of range (%u)\n", months); + PrintAndLogEx(ERR, _RED_("ERROR:") " interval months out of range (%u)", months); return false; } data->raw[8] = (data->raw[8] & 0xF0u) | (months & 0x0Fu); @@ -380,13 +380,13 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static inline uint8_t _get_saflok_mfc_interval_days_impl(const saflok_mfc_data_t *data) { uint8_t days = (data->raw[9] >> 3) & 0x1Fu; if (days > 31u) { - PrintAndLogEx(WARNING, "Warning: interval days appears invalid (%u)\n", days); + PrintAndLogEx(WARNING, "Warning: interval days appears invalid (%u)", days); } return days; } static inline bool _set_saflok_mfc_interval_days_impl(saflok_mfc_data_t *data, uint8_t days) { if (days > 31u) { - PrintAndLogEx(ERR, _RED_("ERROR:") " interval days out of range (%u)\n", days); + PrintAndLogEx(ERR, _RED_("ERROR:") " interval days out of range (%u)", days); return false; } data->raw[9] = (data->raw[9] & 0x07u) | ((days & 0x1Fu) << 3); @@ -397,13 +397,13 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b // top two bits from raw[10] uint8_t hours = ((data->raw[9] & 0x07u) << 2) | (data->raw[10] >> 6); if (hours > 23u) { - PrintAndLogEx(WARNING, "Warning: interval hours appears invalid (%u)\n", hours); + PrintAndLogEx(WARNING, "Warning: interval hours appears invalid (%u)", hours); } return hours; } static inline bool _set_saflok_mfc_interval_hours_impl(saflok_mfc_data_t *data, uint8_t hours) { if (hours > 23u) { - PrintAndLogEx(ERR, _RED_("ERROR:") " interval hours out of range (%u)\n", hours); + PrintAndLogEx(ERR, _RED_("ERROR:") " interval hours out of range (%u)", hours); return false; } // five bit value split across two bytes: @@ -416,13 +416,13 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static inline uint8_t _get_saflok_mfc_interval_minutes_impl(const saflok_mfc_data_t *data) { uint8_t minutes = (data->raw[10] & 0x3Fu); if (minutes > 59u) { - PrintAndLogEx(WARNING, "Warning: interval minutes appears invalid (%u)\n", minutes); + PrintAndLogEx(WARNING, "Warning: interval minutes appears invalid (%u)", minutes); } return minutes; } static inline bool _set_saflok_mfc_interval_minutes_impl(saflok_mfc_data_t *data, uint8_t minutes) { if (minutes > 59u) { - PrintAndLogEx(ERR, _RED_("ERROR:") " interval minutes out of range (%u)\n", minutes); + PrintAndLogEx(ERR, _RED_("ERROR:") " interval minutes out of range (%u)", minutes); return false; } data->raw[10] = (data->raw[10] & 0xC0u) | (minutes & 0x3Fu); @@ -473,15 +473,15 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b .tm_isdst = 0, // UTC has no DST }; - PrintAndLogEx(MY_DEBUG_LEVEL, "Base date: %04u-%02u-%02uT%02u:%02u\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Base date: %04u-%02u-%02uT%02u:%02u", base->year, base->month, base->day, base->hour, base->minute ); - PrintAndLogEx(MY_DEBUG_LEVEL, "As struct tm: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "As struct tm: %04d-%02d-%02dT%02d:%02d", tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min ); - PrintAndLogEx(MY_DEBUG_LEVEL, "Offset to add: %u years, %u months, %u days, %u hours, %u minutes\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Offset to add: %u years, %u months, %u days, %u hours, %u minutes", offset.years, offset.months, offset.days, offset.hours, offset.minutes ); @@ -493,7 +493,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b tm.tm_hour += offset.hours; tm.tm_min += offset.minutes; - PrintAndLogEx(MY_DEBUG_LEVEL, "With Offset (pre-normalization): %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "With Offset (pre-normalization): %04d-%02d-%02dT%02d:%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min ); @@ -501,7 +501,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b if (tm.tm_min >= 60) { tm.tm_hour += tm.tm_min / 60; tm.tm_min = tm.tm_min % 60; - PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized minutes: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized minutes: %04d-%02d-%02dT%02d:%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min ); @@ -510,7 +510,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b if (tm.tm_hour >= 24) { tm.tm_mday += tm.tm_hour / 24; tm.tm_hour = tm.tm_hour % 24; - PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized hours: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized hours: %04d-%02d-%02dT%02d:%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min ); @@ -526,7 +526,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b tm.tm_year ++; tm.tm_mon -= 12; } - PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized days: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized days: %04d-%02d-%02dT%02d:%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min ); @@ -534,12 +534,12 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b if (tm.tm_mon >= 12) { tm.tm_year += tm.tm_mon / 12; tm.tm_mon = tm.tm_mon % 12; - PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized months: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Normalized months: %04d-%02d-%02dT%02d:%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min ); } - PrintAndLogEx(MY_DEBUG_LEVEL, "Final normalized date: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Final normalized date: %04d-%02d-%02dT%02d:%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min ); @@ -552,7 +552,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b .hour = tm.tm_hour, .minute = tm.tm_min }; - PrintAndLogEx(MY_DEBUG_LEVEL, "Converted back to saflok_mfc_datetime_t: %04d-%02d-%02dT%02d:%02d\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Converted back to saflok_mfc_datetime_t: %04d-%02d-%02dT%02d:%02d", result.year, result.month, result.day, result.hour, result.minute ); @@ -567,15 +567,15 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b saflok_mfc_datetime_offset_t result = {0}; if (!is_saflok_mfc_datetime_valid(start)) { - PrintAndLogEx(ERR, _RED_("ERROR:") " start date is invalid\n"); + PrintAndLogEx(ERR, _RED_("ERROR:") " start date is invalid"); return result; } if (!is_saflok_mfc_datetime_valid(end)) { - PrintAndLogEx(ERR, _RED_("ERROR:") " end date is invalid\n"); + PrintAndLogEx(ERR, _RED_("ERROR:") " end date is invalid"); return result; } - PrintAndLogEx(MYDBG, "Calculating offset from %04u-%02u-%02uT%02u:%02u to %04u-%02u-%02uT%02u:%02u\n", + PrintAndLogEx(MYDBG, "Calculating offset from %04u-%02u-%02uT%02u:%02u to %04u-%02u-%02uT%02u:%02u", start->year, start->month, start->day, start->hour, start->minute, end->year, end->month, end->day, end->hour, end->minute ); @@ -588,7 +588,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b .tm_hour = end->hour - start->hour, .tm_min = end->minute - start->minute, }; - PrintAndLogEx(MYDBG, "Initial diff: %d years, %d months, %d days, %d hours, %d minutes\n", + PrintAndLogEx(MYDBG, "Initial diff: %d years, %d months, %d days, %d hours, %d minutes", diff.tm_year, diff.tm_mon, diff.tm_mday, diff.tm_hour, diff.tm_min ); if (diff.tm_min < 0) { @@ -604,7 +604,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b uint16_t tgt_year = end->year - (end->month == 1 ? 1 : 0); uint8_t tgt_month = (end->month == 1 ? 12 : (end->month - 1)); uint8_t days_in_prev_month = get_days_in_month(tgt_year, tgt_month); - PrintAndLogEx(MYDBG, "Borrowing days from month %02u of year %04u which has %02u days\n", + PrintAndLogEx(MYDBG, "Borrowing days from month %02u of year %04u which has %02u days", tgt_month, tgt_year, days_in_prev_month ); @@ -622,7 +622,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b result.days = diff.tm_mday; result.hours = diff.tm_hour; result.minutes = diff.tm_min; - PrintAndLogEx(MYDBG, "Final offset: %d years, %d months, %d days, %d hours, %d minutes\n", + PrintAndLogEx(MYDBG, "Final offset: %d years, %d months, %d days, %d hours, %d minutes", result.years, result.months, result.days, result.hours, result.minutes ); return result; @@ -637,12 +637,12 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b creation_date.year, creation_date.month, creation_date.day, creation_date.hour, creation_date.minute ); - PrintAndLogEx(MY_DEBUG_LEVEL, "Card Interval Offset: %u years, %u months, %u days, %u hours, %u minutes\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Card Interval Offset: %u years, %u months, %u days, %u hours, %u minutes", offset.years, offset.months, offset.days, offset.hours, offset.minutes ); saflok_mfc_datetime_t expiration = add_offset(&creation_date, offset); - PrintAndLogEx(MY_DEBUG_LEVEL, "Calculated Expiration Date: %04u-%02u-%02uT%02u:%02u\n", + PrintAndLogEx(MY_DEBUG_LEVEL, "Calculated Expiration Date: %04u-%02u-%02uT%02u:%02u", expiration.year, expiration.month, expiration.day, expiration.hour, expiration.minute ); @@ -653,7 +653,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b const saflok_mfc_datetime_t * expiration ) { if (!is_saflok_mfc_datetime_valid(expiration)) { - PrintAndLogEx(ERR, _RED_("ERROR:") " expiration date is invalid\n"); + PrintAndLogEx(ERR, _RED_("ERROR:") " expiration date is invalid"); return false; } saflok_mfc_datetime_t creation_datetime = get_saflok_mfc_card_creation_datetime(data); @@ -840,17 +840,17 @@ static uint32_t get_bitfield(const saflok_mfc_data_t *data, size_t start_bit, si static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access - PrintAndLogEx(ERR, "get_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); + PrintAndLogEx(ERR, "get_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)", start_bit, total_available_bits); return 0; } if (num_bits > 32) { // Exceeds maximum supported bit extraction - PrintAndLogEx(ERR, "get_bitfield: out of bounds access (num_bits=%zu, max=32)\n", num_bits); + PrintAndLogEx(ERR, "get_bitfield: out of bounds access (num_bits=%zu, max=32)", num_bits); return 0; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access - PrintAndLogEx(ERR, "get_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); + PrintAndLogEx(ERR, "get_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)", start_bit, num_bits, total_available_bits); return 0; } @@ -869,22 +869,22 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static const size_t total_available_bits = ARRAYLEN(data->raw) * 8; if (start_bit >= total_available_bits) { // Out of bounds access - PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)\n", start_bit, total_available_bits); + PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu, total_available_bits=%zu)", start_bit, total_available_bits); return false; } if (num_bits > 32) { // Exceeds maximum supported bit extraction - PrintAndLogEx(ERR, "set_bitfield: num_bits exceeds 32\n"); + PrintAndLogEx(ERR, "set_bitfield: num_bits exceeds 32"); return false; } if (total_available_bits - start_bit < num_bits) { // Out of bounds access - PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)\n", start_bit, num_bits, total_available_bits); + PrintAndLogEx(ERR, "set_bitfield: out of bounds access (start_bit=%zu + num_bits=%zu > total_available_bits=%zu)", start_bit, num_bits, total_available_bits); return false; } if ((num_bits < 32) && ((value >> num_bits) != 0)) { // Value exceeds the size of the specified bit field - PrintAndLogEx(ERR, "set_bitfield: value exceeds bit field size (value=%u (0x%08x), num_bits=%zu)\n", value, value, num_bits); + PrintAndLogEx(ERR, "set_bitfield: value exceeds bit field size (value=%u (0x%08x), num_bits=%zu)", value, value, num_bits); return false; } @@ -911,7 +911,7 @@ static char *bytes_to_hex(const uint8_t *data, size_t len) { static const size_t maximum_data_len = (ARRAYLEN(buf) / 2) - 1; // leave room for null terminator if (len > maximum_data_len) { - PrintAndLogEx(ERR, "saflok bytes_to_hex: input length %zu exceeds maximum supported %zu, results will be truncated!\n", len, maximum_data_len); + PrintAndLogEx(ERR, "saflok bytes_to_hex: input length %zu exceeds maximum supported %zu, results will be truncated!", len, maximum_data_len); len = maximum_data_len; } memset(buf, 0, sizeof(buf)); // Clear the buffer before use From af7ec87092b3a6ffebb067bb98a4e94a6b0f4459 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:42:20 -0800 Subject: [PATCH 26/28] fix typo --- client/src/cmdhfsaflok.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 4c5143731..3cb884f8b 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -61,7 +61,7 @@ static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { typedef struct _saflok_mfc_data_t { uint8_t raw[17]; } saflok_mfc_data_t; -typedef struct _saflock_mfc_uid_t { +typedef struct _saflok_mfc_uid_t { uint8_t uid[4]; } saflok_mfc_uid_t; typedef struct _saflok_mfc_key_t { From c0bc988e9164e1dab4a7a8fed3d1d9320691f99a Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:05:55 -0800 Subject: [PATCH 27/28] saflok - more self-test capabilities --- client/src/cmdhfsaflok.c | 314 ++++++++++++++++++++++++++++++--------- 1 file changed, 247 insertions(+), 67 deletions(-) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index 3cb884f8b..eb49655d9 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -44,7 +44,7 @@ static inline uint8_t get_days_in_month(uint16_t year, uint8_t month) { PrintAndLogEx(ERR, "Invalid month lookup: %d, expected range [1..12] ... returning 0 days!", month); return 0; } - + uint8_t days = _days_in_month_lookup[month]; // Adjust for leap years if ((month == 2u) && @@ -82,6 +82,88 @@ typedef struct _saflok_mfc_datetime_offset_t { uint8_t minutes; // range [0..59] } saflok_mfc_datetime_offset_t; +#if 1 // test cases for self-test +typedef struct _saflok_test_case_t { + saflok_mfc_data_t encoded; + saflok_mfc_data_t decoded; + saflok_mfc_datetime_t card_creation_datetime; + saflok_mfc_datetime_offset_t card_interval; + saflok_mfc_datetime_t card_expiration_datetime; + uint16_t lock_id; + uint16_t pass_number; + uint16_t sequence_and_combination; + uint16_t property_id; + uint8_t card_level; + uint8_t card_type; + uint8_t card_id; + uint8_t opening_key; + uint8_t restricted_days; + bool deadbolt_override; + uint8_t checksum; +} saflok_test_case_t; + +static const saflok_test_case_t saflok_test_cases[] = { + { // 5A4A845610B6B1DD8F4753872BC0E91095 / F7B08101FFF7FF9E10500072A00023E7C7 + // Tests interval that crosses a leap year using days + .encoded = {.raw = {0x5A, 0x4A, 0x84, 0x56, 0x10, 0xB6, 0xB1, 0xDD, 0x8F, 0x47, 0x53, 0x87, 0x2B, 0xC0, 0xE9, 0x10, 0x95} }, + .decoded = {.raw = {0xF7, 0xB0, 0x81, 0x01, 0xFF, 0xF7, 0xFF, 0x9E, 0x10, 0x50, 0x00, 0x72, 0xA0, 0x00, 0x23, 0xE7, 0xC7} }, + .card_expiration_datetime = { .year = 2020, .month = 3, .day = 1, .hour = 0, .minute = 0 }, + .card_creation_datetime = { .year = 2019, .month = 2, .day = 20, .hour = 0, .minute = 0 }, + .card_interval = { .years = 1, .months = 0, .days = 10, .hours = 0, .minutes = 0 }, + .lock_id = 257, + .pass_number = 4095, + .sequence_and_combination = 2047, + .property_id = 999, + .card_level = 15, + .card_type = 7, + .card_id = 176, + .opening_key = 2, + .restricted_days = 30, // 0b0011110 + .deadbolt_override = true, + .checksum = 0xC7, + }, + { // 3FB01B9F0100B55A7D423B6166C0E980B9 / B5AD8101FF57EB9400000A82EDF723E7CC + // Tests interval overflow from minutes -> hours -> day -> leap year overflow (2/29 -> 3/1) + .encoded = {.raw = {0x3F, 0xB0, 0x1B, 0x9F, 0x01, 0x00, 0xB5, 0x5A, 0x7D, 0x42, 0x3B, 0x61, 0x66, 0xC0, 0xE9, 0x80, 0xB9} }, + .decoded = {.raw = {0xB5, 0xAD, 0x81, 0x01, 0xFF, 0x57, 0xEB, 0x94, 0x00, 0x00, 0x0A, 0x82, 0xED, 0xF7, 0x23, 0xE7, 0xCC} }, + .card_expiration_datetime = { .year = 2020, .month = 3, .day = 1, .hour = 0, .minute = 5 }, + .card_creation_datetime = { .year = 2020, .month = 2, .day = 29, .hour = 23, .minute = 55 }, + .card_interval = { .years = 0, .months = 0, .days = 0, .hours = 0, .minutes = 10 }, + .lock_id = 257, + .pass_number = 4085, + .sequence_and_combination = 2027, + .property_id = 999, + .card_level = 11, + .card_type = 5, + .card_id = 173, + .opening_key = 2, + .restricted_days = 20, // 0b0010100 + .deadbolt_override = true, + .checksum = 0xCC, + }, + { // C2FE64DFE4EBB1CC7D8F6FEBA6E59E5B4D / 338341A3C777557F00080082E000207B4E + // Tests interval overflow into a final leap-day expiration date + .encoded = {.raw = {0xC2, 0xFE, 0x64, 0xDF, 0xE4, 0xEB, 0xB1, 0xCC, 0x7D, 0x8F, 0x6F, 0xEB, 0xA6, 0xE5, 0x9E, 0x5B, 0x4D} }, + .decoded = {.raw = {0x33, 0x83, 0x41, 0xA3, 0xC7, 0x77, 0x55, 0x7F, 0x00, 0x08, 0x00, 0x82, 0xE0, 0x00, 0x20, 0x7B, 0x4E} }, + .card_expiration_datetime = { .year = 2020, .month = 2, .day = 29, .hour = 0, .minute = 0 }, + .card_creation_datetime = { .year = 2020, .month = 2, .day = 28, .hour = 0, .minute = 0 }, + .card_interval = { .years = 0, .months = 0, .days = 1, .hours = 0, .minutes = 0 }, + .lock_id = 419, + .pass_number = 3191, + .sequence_and_combination = 1877, + .property_id = 123, + .card_level = 3, + .card_type = 3, + .card_id = 131, + .opening_key = 1, + .restricted_days = 127, // 0b1111111 + .deadbolt_override = false, + .checksum = 0x4E, + }, +}; + +#endif // test cases for self-test + #define KEY_LENGTH sizeof(saflok_mfc_key_t) static uint32_t get_bitfield(const saflok_mfc_data_t *data, size_t start_bit, size_t num_bits); @@ -226,7 +308,7 @@ static bool set_bitfield(saflok_mfc_data_t *data, size_t start_bit, size_t num_b static inline bool set_saflok_mfc_checksum(saflok_mfc_data_t *data, uint8_t checksum) { data->raw[16] = checksum; return true; - } + } #endif // getters and setters for each bitfield in saflok_mfc_data_t #if 1 // helpers for get/set_saflok_mfc_card_creation_date() ... do not call these directly as does not validate... @@ -767,7 +849,7 @@ _Static_assert(ARRAYLEN(level_names) == 16, "level_names must have 16 elements") static int CmdHelp(const char *Cmd); static void saflok_decrypt(const saflok_mfc_data_t *encryptedCard, saflok_mfc_data_t *decryptedCard) { - + static const size_t length = ARRAYLEN(encryptedCard->raw); for (int i = 0; i < length; i++) { int num = c_aDecode[encryptedCard->raw[i]] - (i + 1); @@ -1114,6 +1196,7 @@ static int CmdHFSaflokRead(const char *Cmd) { PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Card Information")); + PrintAndLogEx(INFO, "Plaintext Data: %s", bytes_to_hex(decrypted.raw, sizeof(saflok_mfc_data_t))); PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(encrypted->raw, sizeof(saflok_mfc_data_t))); saflok_decode(&decrypted); @@ -1174,12 +1257,12 @@ static int CmdHFSaflokEncode(const char *Cmd) { arg_get_u32_def(ctx, 12, 0), // property_id dt_e, dt); - saflok_encrypt(&decrypted, &encrypted); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Encoded Card Data")); - PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(encrypted.raw, 17)); + PrintAndLogEx(INFO, "Plaintext Data: %s", bytes_to_hex(decrypted.raw, sizeof(saflok_mfc_data_t))); + PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(encrypted.raw, sizeof(saflok_mfc_data_t))); CLIParserFree(ctx); @@ -1190,7 +1273,7 @@ static int CmdHFSaflokDecode(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf saflok decode", "Decode saflok data", - "hf saflok decode"); + "hf saflok decode -d "); void *argtable[] = { arg_param_begin, @@ -1212,6 +1295,10 @@ static int CmdHFSaflokDecode(const char *Cmd) { } saflok_decrypt(&encrypted, &decrypted); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Decoded Card Data")); + PrintAndLogEx(INFO, "Plaintext Data: %s", bytes_to_hex(decrypted.raw, sizeof(saflok_mfc_data_t))); + PrintAndLogEx(INFO, "Encrypted Data: %s", bytes_to_hex(encrypted.raw, sizeof(saflok_mfc_data_t))); saflok_decode(&decrypted); return PM3_SUCCESS; @@ -1259,6 +1346,7 @@ static int CmdHFSaflokModify(const char *Cmd) { } saflok_decrypt(&encrypted, &decrypted); + PrintAndLogEx(INFO, "Plaintext Data: %s", bytes_to_hex(decrypted.raw, sizeof(saflok_mfc_data_t))); uint32_t card_level = get_saflok_mfc_card_level(&decrypted); card_level = arg_get_u32_def(ctx, 1, card_level); @@ -1319,13 +1407,12 @@ static int CmdHFSaflokModify(const char *Cmd) { property_id, dt_e, dt); - - saflok_encrypt(&decrypted, &reencrypted); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Modified Card Data")); - PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(reencrypted.raw, 17)); + PrintAndLogEx(INFO, "Plaintext Data: %s", bytes_to_hex(decrypted.raw, sizeof(saflok_mfc_data_t))); + PrintAndLogEx(SUCCESS, "Encrypted Data: " _GREEN_("%s"), bytes_to_hex(reencrypted.raw, sizeof(saflok_mfc_data_t))); CLIParserFree(ctx); @@ -1417,6 +1504,8 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { // verify the resulting decoded data matches expectations, // or investigate which one results in correct data. + + // get/set bitfield tests if (true) { int result_bitfield = PM3_SUCCESS; @@ -1445,7 +1534,7 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { PrintAndLogEx(SUCCESS, "get/set_bitfield passed"); } else { result = PM3_EFAILED; - } + } } // card_level field is 4 bits if (true) { @@ -1727,64 +1816,155 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { [ ] set_saflok_mfc_card_expiration_datetime } */ + if (true) { + int result_test_cases = PM3_SUCCESS; - if (true) { // test encode+encrypt, including interval affected by leap-year + for (size_t i = 0; i < ARRAYLEN(saflok_test_cases); ++i) { + const saflok_test_case_t * tc = &saflok_test_cases[i]; - // Equivalent to running the following commands manually: - // - // [usb] pm3 --> hf saflok encode - // --level 15 --type 7 --id 176 --open 2 --lock_id 257 --pass_num 4095 --seq_combo 2047 - // --deadbolt 1 --days 30 --expire 2020-03-01T00:00 --created 2019-02-20T00:00 --prop_id 999 - // - // [=] --- Encoded Card Data - // [+] Encrypted Data: 5A4A845610B6B1DD8F4753872BC0E91095 - // - // [usb] pm3 --> hf saflok decode -d 5A4A845610B6B1DD8F4753872BC0E91095 - // [+] Card Level: 15 (Primary Programming Key (PPK)) - // [+] Card Type: 7 - // [+] Card ID: 176 - // [+] Opening Key: 2 - // [+] Lock ID: 257 - // [+] Pass Number: 4095 - // [+] Sequence and Combination: 2047 - // [+] Deadbolt Override: 1 - // [+] Restricted Days: 30 == 0b0011110 - // [+] Card Creation Date: 2019-02-20 00:00 - // [+] Expire Date: 2020-03-01 00:00 - // [+] Property ID: 999 - // [+] Checksum: 0xC7 (ok) - - - saflok_mfc_data_t test_data = {0}; - set_saflok_mfc_card_level(&test_data, 15); - set_saflok_mfc_card_type(&test_data, 7); - set_saflok_mfc_card_id(&test_data, 176); - set_saflok_mfc_opening_key(&test_data, 2); - set_saflok_mfc_lock_id(&test_data, 257); - set_saflok_mfc_pass_number(&test_data, 4095); - set_saflok_mfc_sequence_and_combination(&test_data, 2047); - set_saflok_mfc_deadbolt_override(&test_data, true); - set_saflok_mfc_restricted_days(&test_data, 30); - saflok_mfc_datetime_t creation_date = {.year = 2019, .month = 2, .day = 20, .hour = 0, .minute = 0}; - saflok_mfc_datetime_t expiration_date = {.year = 2020, .month = 3, .day = 1, .hour = 0, .minute = 0}; - set_saflok_mfc_card_creation_datetime(&test_data, &creation_date); - set_saflok_mfc_card_expiration_datetime(&test_data, &expiration_date); - set_saflok_mfc_property_id(&test_data, 999); - uint8_t checksum = calculated_saflok_checksum(&test_data); - set_saflok_mfc_checksum(&test_data, checksum); - - saflok_mfc_data_t encrypted = {0}; - saflok_encrypt(&test_data, &encrypted); - - saflok_mfc_data_t expected = { - .raw = {0x5A, 0x4A, 0x84, 0x56, 0x10, 0xB6, 0xB1, 0xDD, - 0x8F, 0x47, 0x53, 0x87, 0x2B, 0xC0, 0xE9, 0x10, - 0x95}, - }; - if (memcmp(encrypted.raw, expected.raw, sizeof(saflok_mfc_data_t)) == 0) { - PrintAndLogEx(SUCCESS, "Self-test passed: encoded+encrypted data matches expected value."); + // encoded -> decoded -> encoded + saflok_mfc_data_t tmp = {0}; + saflok_encrypt(&tc->decoded, &tmp); + if (memcmp(tmp.raw, tc->encoded.raw, sizeof(saflok_mfc_data_t)) != 0) { + PrintAndLogEx(FAILED, "Test case %zu: encryption did not match expected data.", i); + PrintAndLogEx(DEBUG, " raw result: %s", bytes_to_hex(tmp.raw, sizeof(saflok_mfc_data_t))); + PrintAndLogEx(DEBUG, " expected result: %s", bytes_to_hex(tc->encoded.raw, sizeof(saflok_mfc_data_t))); + result_test_cases = PM3_EFAILED; + continue; + } + saflok_decrypt(&tc->encoded, &tmp); + if (memcmp(tmp.raw, tc->decoded.raw, sizeof(saflok_mfc_data_t)) != 0) { + PrintAndLogEx(FAILED, "Test case %zu: decryption did not match expected data.", i); + PrintAndLogEx(DEBUG, " raw result: %s", bytes_to_hex(tmp.raw, sizeof(saflok_mfc_data_t))); + PrintAndLogEx(DEBUG, " expected result: %s", bytes_to_hex(tc->decoded.raw, sizeof(saflok_mfc_data_t))); + result_test_cases = PM3_EFAILED; + continue; + } + saflok_mfc_datetime_t tmp_dt = get_saflok_mfc_card_creation_datetime(&tc->decoded); + if (memcmp(&tmp_dt, &tc->card_creation_datetime, sizeof(saflok_mfc_datetime_t)) != 0) { + PrintAndLogEx(FAILED, "Test case %zu: card creation datetime did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %4u-%02u-%02u %02u:%02u", + tmp_dt.year, tmp_dt.month, tmp_dt.day, + tmp_dt.hour, tmp_dt.minute); + PrintAndLogEx(DEBUG, " expected result: %4u-%02u-%02u %02u:%02u", + tc->card_creation_datetime.year, tc->card_creation_datetime.month, tc->card_creation_datetime.day, + tc->card_creation_datetime.hour, tc->card_creation_datetime.minute); + result_test_cases = PM3_EFAILED; + continue; + } + tmp_dt = get_saflok_mfc_card_expiration_datetime(&tc->decoded); + if (memcmp(&tmp_dt, &tc->card_expiration_datetime, sizeof(saflok_mfc_datetime_t)) != 0) { + PrintAndLogEx(FAILED, "Test case %zu: card expiration datetime did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %4u-%02u-%02u %02u:%02u", + tmp_dt.year, tmp_dt.month, tmp_dt.day, + tmp_dt.hour, tmp_dt.minute); + PrintAndLogEx(DEBUG, " expected result: %4u-%02u-%02u %02u:%02u", + tc->card_expiration_datetime.year, tc->card_expiration_datetime.month, tc->card_expiration_datetime.day, + tc->card_expiration_datetime.hour, tc->card_expiration_datetime.minute); + result_test_cases = PM3_EFAILED; + continue; + } + saflok_mfc_datetime_offset_t tmp_offset = get_saflok_mfc_interval(&tc->decoded); + if (memcmp(&tmp_offset, &tc->card_interval, sizeof(saflok_mfc_datetime_offset_t)) != 0) { + PrintAndLogEx(FAILED, "Test case %zu: card interval did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u years, %u months, %u days, %u hours, %u minutes", + tmp_offset.years, tmp_offset.months, tmp_offset.days, + tmp_offset.hours, tmp_offset.minutes); + PrintAndLogEx(DEBUG, " expected result: %u years, %u months, %u days, %u hours, %u minutes", + tc->card_interval.years, tc->card_interval.months, tc->card_interval.days, + tc->card_interval.hours, tc->card_interval.minutes); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_lock_id(&tc->decoded) != tc->lock_id) { + PrintAndLogEx(FAILED, "Test case %zu: lock_id did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_lock_id(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->lock_id); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_pass_number(&tc->decoded) != tc->pass_number) { + PrintAndLogEx(FAILED, "Test case %zu: pass_number did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_pass_number(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->pass_number); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_sequence_and_combination(&tc->decoded) != tc->sequence_and_combination) { + PrintAndLogEx(FAILED, "Test case %zu: sequence_and_combination did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_sequence_and_combination(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->sequence_and_combination); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_property_id(&tc->decoded) != tc->property_id) { + PrintAndLogEx(FAILED, "Test case %zu: property_id did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_property_id(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->property_id); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_card_level(&tc->decoded) != tc->card_level) { + PrintAndLogEx(FAILED, "Test case %zu: card_level did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_card_level(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->card_level); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_card_type(&tc->decoded) != tc->card_type) { + PrintAndLogEx(FAILED, "Test case %zu: card_type did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_card_type(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->card_type); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_card_id(&tc->decoded) != tc->card_id) { + PrintAndLogEx(FAILED, "Test case %zu: card_id did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_card_id(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->card_id); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_opening_key(&tc->decoded) != tc->opening_key) { + PrintAndLogEx(FAILED, "Test case %zu: opening_key did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_opening_key(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->opening_key); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_restricted_days(&tc->decoded) != tc->restricted_days) { + PrintAndLogEx(FAILED, "Test case %zu: restricted_days did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_restricted_days(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->restricted_days); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_deadbolt_override(&tc->decoded) != tc->deadbolt_override) { + PrintAndLogEx(FAILED, "Test case %zu: deadbolt_override did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_deadbolt_override(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->deadbolt_override); + result_test_cases = PM3_EFAILED; + continue; + } + if (get_saflok_mfc_checksum(&tc->decoded) != tc->checksum) { + PrintAndLogEx(FAILED, "Test case %zu: stored checksum did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", get_saflok_mfc_checksum(&tc->decoded)); + PrintAndLogEx(DEBUG, " expected result: %u", tc->checksum); + result_test_cases = PM3_EFAILED; + continue; + } + uint8_t checksum = calculated_saflok_checksum(&tc->decoded); + if (checksum != tc->checksum) { + PrintAndLogEx(FAILED, "Test case %zu: calculated checksum did not match expected data.", i); + PrintAndLogEx(DEBUG, " api result: %u", checksum); + PrintAndLogEx(DEBUG, " expected result: %u", tc->checksum); + result_test_cases = PM3_EFAILED; + continue; + } + } + if (result_test_cases == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "All predefined test cases passed."); } else { - PrintAndLogEx(FAILED, "Self-test failed: encoded+encrypted data did not match expected value."); result = PM3_EFAILED; } } @@ -1862,7 +2042,7 @@ static int CmdHFSaflokProvision(const char *Cmd) { _Static_assert(ARRAYLEN(uid) >= sizeof(saflok_mfc_uid_t), "UID array too small"); const saflok_mfc_uid_t * saflok_uid = (const saflok_mfc_uid_t *)uid; saflok_mfc_key_t keyA = {0}; - + saflok_kdf(saflok_uid, &keyA); PrintAndLogEx(INFO, "Generated UID-derived key: " _GREEN_("%s"), bytes_to_hex(keyA.key, ARRAYLEN(keyA.key))); @@ -1954,7 +2134,7 @@ static int CmdHFSaflokInterrogate(const char *Cmd) { _Static_assert(ARRAYLEN(uid) >= sizeof(saflok_mfc_uid_t), "UID array too small"); const saflok_mfc_uid_t * saflok_uid = (const saflok_mfc_uid_t *)uid; - saflok_mfc_key_t key; + saflok_mfc_key_t key = {0}; saflok_kdf(saflok_uid, &key); uint8_t block2[16]; From 7fd1179c9abfb3cd3f2d5f291bee3c5e7556f666 Mon Sep 17 00:00:00 2001 From: Henry Gabryjelski <740168+henrygab@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:13:14 -0800 Subject: [PATCH 28/28] one more api ... even if silly... --- client/src/cmdhfsaflok.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/src/cmdhfsaflok.c b/client/src/cmdhfsaflok.c index eb49655d9..3ba666868 100644 --- a/client/src/cmdhfsaflok.c +++ b/client/src/cmdhfsaflok.c @@ -1767,6 +1767,30 @@ static int CmdHFSaflokSelfTest(const char *Cmd) { result = PM3_EFAILED; } } + if (true) { + int result_checksum = PM3_SUCCESS; + saflok_mfc_data_t testdata = {0}; + for (size_t i = 0; i < 0x100; ++i) { + uint8_t value = (uint8_t)i; + bool success = set_saflok_mfc_checksum(&testdata, value); + if (!success) { + PrintAndLogEx(FAILED, "set_saflok_mfc_checksum failed for value %02x", value); + result_checksum = PM3_EFAILED; + } else { + uint8_t x = get_saflok_mfc_checksum(&testdata); + if (x != value) { + PrintAndLogEx(FAILED, "get_saflok_mfc_checksum returned %02x, expected %02x", x, value); + result_checksum = PM3_EFAILED; + } + } + } + if (result_checksum == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "get/set_saflok_mfc_checksum passed"); + } else { + result = PM3_EFAILED; + } + } + /* // raw_interval field is 20 bits -- special set processing needed // raw_expire_date field is 24 bits -- special set processing needed