hf mfdes sim: ChangeKey

A reader can rekey the simulated card now, at PICC level and inside an
application, and the new keys are in what `hf mfdes esave` writes out.

The reader enciphers the key data under the session key, and its shape depends
on whether the key being changed is the one the session was opened with. Change
that key and the frame is just the new key; change any other and the new key
arrives XORed with the current one, with a second CRC32 over the new key alone
so the card can tell the XOR came apart correctly. The card holds both keys in
that case, so it can undo the XOR -- and a key the image only knows the version
of is refused rather than guessed at. The shape being fixed means the plaintext
length is known rather than searched for: the key, an AES version byte, the
CRC32 over command, key number and that lot, and the second CRC32 when it
applies.

Which key has to be authenticated comes from the key settings, not from the
frame. The master key changes only with the master key and only while bit 0
still says it is changeable; a change-key nibble of 0x0F freezes every other
key; 0x0E lets a key be changed by whoever authenticated with it; any other
value names the one key that may change the others (M134034 9.3.4, 9.3.6).

At PICC level the top two bits of the key number choose the algorithm the new
master key is to be, since there is no application creation to fix it. Inside an
application the key type cannot change, so those bits are ignored there.

"After a successful change of the key used to reach the current authentication
status, this authentication is invalidated" -- so the session is dropped after
changing the key it was opened with, but only after the MACed answer has been
built with it, since the reader still has to verify that.

Tested against the simulation on an AES application and on the PICC master key:
changing the authenticated key leaves the old key unable to authenticate and the
new one able to, changing a different key does the same to that key while the
session carries on, and changing a key while authenticated with one that the key
settings do not name is refused with the key left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iceman1001
2026-09-15 21:28:31 +02:00
co-authored by Claude Opus 5
parent 31d05aef53
commit 55fb665380
2 changed files with 211 additions and 2 deletions
+1
View File
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
## [unreleased][unreleased]
- Added `hf mfdes sim` - ChangeKey is answered, both the plain and the XORed key frame, so a reader can rekey the simulated card and the new keys are in what `hf mfdes esave` writes out (@iceman1001)
- Added `hf mfdes sim` - ChangeFileSettings is answered, plain when the change access right is free and enciphered under the key it names otherwise (@iceman1001)
- Added `hf mfdes esave` - `--keep` saves applications and files a reader deleted, which are otherwise left out so the dump is the card as a reader sees it (@iceman1001)
- Added `hf mfdes sim` - CreateFile in all five EV1 flavours and DeleteFile are answered, so a reader can lay out an application's file system on the simulated card (@iceman1001)
+210 -2
View File
@@ -786,7 +786,8 @@ static uint8_t desfire_sim_hdrlen(uint8_t cmd) {
case MFDES_LIMITED_CREDIT:
case MFDES_GET_VALUE:
case MFDES_CHANGE_FILE_SETTINGS:
return 1; // file number only
case MFDES_CHANGE_KEY:
return 1; // file number, or key number, only
default:
return 0;
}
@@ -822,7 +823,9 @@ static bool desfire_sim_cmd_is_write(uint8_t cmd) {
// enciphered, and either way the command's own handler does the unwrapping and
// moves the IV along, so the blanket CMAC has to keep its hands off.
static bool desfire_sim_cmd_unwraps_own(uint8_t cmd) {
return (desfire_sim_cmd_is_write(cmd) || cmd == MFDES_CHANGE_FILE_SETTINGS);
return (desfire_sim_cmd_is_write(cmd) ||
cmd == MFDES_CHANGE_FILE_SETTINGS ||
cmd == MFDES_CHANGE_KEY);
}
// Take the reader's secure messaging off a gathered command.
@@ -1143,6 +1146,40 @@ static uint16_t desfire_sim_dfname_record(const desfire_sim_state_t *st, uint8_t
return 0;
}
// ----------------------------------------------------------------- key change
// Key length for an algorithm. A single DES key is carried and stored as its
// 16 byte doubled form, which is how the reader sends it too.
static uint8_t desfire_sim_keylen(uint8_t algo) {
return (algo == T_3K3DES) ? 24 : 16;
}
// For everything but AES the key version lives in the low bit of each of the
// first eight key bytes -- the DES parity bits, which the cipher ignores.
static uint8_t desfire_sim_key_version(uint8_t algo, const uint8_t *key, uint8_t sent) {
if (algo == T_AES) {
return sent; // AES carries the version as its own byte
}
uint8_t ver = 0;
for (uint8_t i = 0; i < 8; i++) {
ver |= (key[i] & 1) << (7 - i);
}
return ver;
}
// Find a key entry to write to, whether or not we hold its current value.
static desfire_em_key_t *desfire_sim_key_slot(desfire_sim_state_t *st, uint8_t app, uint8_t keyno) {
for (uint16_t i = 0; i < st->hdr->keycount; i++) {
if (st->keys[i].app == app && st->keys[i].num == keyno) {
return (desfire_em_key_t *)&st->keys[i];
}
}
return NULL;
}
// ------------------------------------------------------------ file creation
// Bytes the committed region of a file occupies, before any shadow. The same
@@ -2170,6 +2207,177 @@ static uint16_t desfire_sim_command(desfire_sim_state_t *st, uint8_t cmd, const
return desfire_sim_maced(st, out, MFDES_S_OPERATION_OK, NULL, 0);
}
case MFDES_CHANGE_KEY: {
if (inlen < 1) {
return desfire_sim_status(out, MFDES_E_LENGTH);
}
if (st->authenticated == false) {
return desfire_sim_status(out, MFDES_E_AUTHENTICATION_ERROR);
}
uint8_t keyno = in[0] & 0x3F;
const desfire_em_app_t *app = &st->apps[st->selected];
uint8_t algo = app->keytype;
// At PICC level the top two bits of the key number choose the
// algorithm the new master key is to be, since there is no
// application creation to fix it -- M134034 9.3.6. Inside an
// application the key type cannot change after creation.
if (st->selected == 0) {
if (keyno != 0) {
return desfire_sim_status(out, MFDES_E_PARAMETER_ERROR);
}
switch ((in[0] >> 6) & 0x03) {
case 0x01:
algo = T_3K3DES;
break;
case 0x02:
algo = T_AES;
break;
default:
algo = T_3DES;
break;
}
} else if (keyno >= (app->numkeysraw & 0x0F)) {
return desfire_sim_status(out, MFDES_E_NO_SUCH_KEY);
}
// Which key had to be authenticated: the master key changes only
// with the master key and only while it is still changeable, and
// everything else is governed by the change-key nibble of the
// application's key settings (M134034 9.3.4 and 9.3.6).
uint8_t ck = (app->keysettings >> 4) & 0x0F;
if (keyno == 0) {
if ((app->keysettings & 0x01) == 0) {
return desfire_sim_status(out, MFDES_E_PERMISSION_DENIED);
}
if (st->auth_keyno != 0) {
return desfire_sim_status(out, MFDES_E_AUTHENTICATION_ERROR);
}
} else if (ck == 0x0F) {
// every key but the master key is frozen
return desfire_sim_status(out, MFDES_E_PERMISSION_DENIED);
} else if (ck == 0x0E) {
// free: a key is changed by whoever authenticated with it
if (st->auth_keyno != keyno) {
return desfire_sim_status(out, MFDES_E_AUTHENTICATION_ERROR);
}
} else if (st->auth_keyno != ck) {
return desfire_sim_status(out, MFDES_E_AUTHENTICATION_ERROR);
}
desfire_em_key_t *slot = desfire_sim_key_slot(st, st->selected, keyno);
if (slot == NULL) {
return desfire_sim_status(out, MFDES_E_NO_SUCH_KEY);
}
// The reader enciphers the key data under the session key. Its
// shape is fixed, so the length is known rather than searched for:
// the key, an AES version byte, the CRC32 over command, key number
// and that lot, and -- only when the key being changed is not the
// one the session was opened with -- a CRC32 of the new key on top.
uint8_t keylen = desfire_sim_keylen(algo);
bool xored = (keyno != st->auth_keyno);
uint16_t plainlen = keylen + ((algo == T_AES) ? 1 : 0) + 4 + (xored ? 4 : 0);
size_t kbs = key_block_size(&st->sesskey);
if (kbs == 0) {
return desfire_sim_status(out, MFDES_E_AUTHENTICATION_ERROR);
}
uint16_t enclen = inlen - 1;
if (enclen != padded_data_length(plainlen, kbs)) {
return desfire_sim_status(out, MFDES_E_LENGTH);
}
uint8_t buf[DESFIRE_SIM_WRITE_MAX] = {0};
if (enclen > sizeof(buf)) {
return desfire_sim_status(out, MFDES_E_LENGTH);
}
desfire_sim_crypt(&st->sesskey, in + 1, buf, enclen, st->iv, false);
// the CRC32 covers the command byte, the key number as it arrived,
// and the plaintext up to but not including the CRC itself
uint16_t upto = keylen + ((algo == T_AES) ? 1 : 0);
uint8_t crcbuf[DESFIRE_SIM_WRITE_MAX + 8] = {0};
crcbuf[0] = cmd;
crcbuf[1] = in[0];
memcpy(crcbuf + 2, buf, upto);
uint8_t want[4] = {0};
crc32_ex(crcbuf, upto + 2, want);
if (memcmp(want, buf + upto, 4) != 0) {
desfire_sim_auth_clear(st);
return desfire_sim_status(out, MFDES_E_INTEGRITY_ERROR);
}
uint8_t newkey[DESFIRE_MAX_KEY_SIZE] = {0};
memcpy(newkey, buf, keylen);
if (xored) {
// what arrived is the new key XORed with the current one, so the
// card needs to hold the current one to recover it
if ((slot->flags & DESFIRE_EM_KEY_PRESENT) == 0) {
return desfire_sim_status(out, MFDES_E_AUTHENTICATION_ERROR);
}
for (uint8_t i = 0; i < keylen; i++) {
newkey[i] ^= slot->key[i];
}
// and a CRC32 of the new key alone proves the XOR came apart
uint8_t want2[4] = {0};
crc32_ex(newkey, keylen, want2);
if (memcmp(want2, buf + upto + 4, 4) != 0) {
desfire_sim_auth_clear(st);
return desfire_sim_status(out, MFDES_E_INTEGRITY_ERROR);
}
}
memset(slot->key, 0, sizeof(slot->key));
memcpy(slot->key, newkey, keylen);
slot->ver = desfire_sim_key_version(algo, newkey, (algo == T_AES) ? buf[keylen] : 0);
slot->flags |= DESFIRE_EM_KEY_PRESENT | DESFIRE_EM_KEY_VERKNOWN;
// changing the PICC master key can change its algorithm with it
if (st->selected == 0 && algo != app->keytype) {
desfire_em_app_t *w = (desfire_em_app_t *)app;
w->keytype = algo;
w->numkeysraw = (w->numkeysraw & 0x3F) |
((algo == T_AES) ? 0x80 : ((algo == T_3K3DES) ? 0x40 : 0x00));
}
// "After a successful change of the key used to reach the current
// authentication status, this authentication is invalidated"
// -- M134034 9.3.6. The answer to this one still carries its MAC,
// so the session is dropped after it is built.
if (xored == false) {
uint16_t n = desfire_sim_maced(st, out, MFDES_S_OPERATION_OK, NULL, 0);
desfire_sim_auth_clear(st);
return n;
}
return desfire_sim_maced(st, out, MFDES_S_OPERATION_OK, NULL, 0);
}
case MFDES_CHANGE_FILE_SETTINGS: {
if (inlen < 1) {