saflok - more self-test capabilities

This commit is contained in:
Henry Gabryjelski
2025-11-17 17:05:55 -08:00
parent af7ec87092
commit c0bc988e91
+247 -67
View File
@@ -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 <encrypted data>");
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];