From 55fb66538087efa7a64ae660a2940ec46dfffe7e Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 15 Sep 2026 21:28:31 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + armsrc/desfiresim.c | 212 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 341e6e1b3..0c18cc4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/armsrc/desfiresim.c b/armsrc/desfiresim.c index e9974b5b4..2d6025c01 100644 --- a/armsrc/desfiresim.c +++ b/armsrc/desfiresim.c @@ -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) {