From afacb2a565220f9d39e6c7f1e164830ff5775905 Mon Sep 17 00:00:00 2001 From: dogty Date: Sun, 26 Oct 2025 12:32:10 +0100 Subject: [PATCH 1/6] fix: handle REQA and WUP commands for NFC tag 14a --- firmware/application/src/rfid/nfctag/hf/nfc_14a.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c index 043f0e4..a3e0c99 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c @@ -397,6 +397,15 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { m_tag_state_14a = NFC_TAG_STATE_14A_IDLE; } return; + case NFC_TAG_14A_CMD_REQA: + case NFC_TAG_14A_CMD_WUPA: + // Reader is re-sending REQA/WUPA while in READY state + // This can happen if reader retries or if frame was received incorrectly + // Respond with ATQA again and stay in READY state + if (auto_coll_res != NULL) { + nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false); + } + return; default: { // After receiving the wrong level instruction, directly reset the status machine NRF_LOG_INFO("[MFEMUL_SELECT] Incorrect cascade level received: %02x", p_data[0]); From 494d397e7b2220bdc6aef7321053807e3912c1a3 Mon Sep 17 00:00:00 2001 From: dogty Date: Sat, 1 Nov 2025 18:09:17 +0100 Subject: [PATCH 2/6] fix(nfc 14a): The fast read command now handles the last block --- firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c index 5d40c7e..0747422 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c @@ -675,14 +675,14 @@ static void handle_fast_read_command(uint8_t block_num, uint8_t end_block_num) { int block_max = get_block_max_by_tag_type(m_tag_type, true); - if (block_num >= end_block_num || end_block_num >= block_max) { + if (block_num > end_block_num || end_block_num >= block_max) { nfc_tag_14a_tx_nbit(NAK_INVALID_OPERATION_TBV, 4); return; } NRF_LOG_INFO("HANDLING FAST READ %02x %02x", block_num, end_block_num); - - handle_any_read(block_num, end_block_num - block_num, block_max); + // FAST_READ is inclusive: read from block_num to end_block_num (both included) + handle_any_read(block_num, end_block_num - block_num + 1, block_max); } static bool check_ro_lock_on_page(int block_num) { From 335ffee69db9758dc4c1357be149f0d38c17ddcc Mon Sep 17 00:00:00 2001 From: dogty Date: Sat, 1 Nov 2025 18:11:11 +0100 Subject: [PATCH 3/6] fix(ntag): The page lock check doesn't take into account the blocking lock bits (only for NTAG213, NTAG215 and NTA216) --- .../src/rfid/nfctag/hf/nfc_mf0_ntag.c | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c index 0747422..8678a80 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c @@ -687,17 +687,37 @@ static void handle_fast_read_command(uint8_t block_num, uint8_t end_block_num) { static bool check_ro_lock_on_page(int block_num) { if (block_num < 3) return true; - else if (block_num == 3) return (m_tag_information->memory[2][2] & 9) != 0; // bits 0 and 3 - else if (block_num <= MF0ICU1_PAGES) { + else if (block_num == 3) { + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + //page 3 can be locked or not independant of BL CC bit + //the BL bit only freezes the lock bytes ! + return (m_tag_information->memory[2][2] & 8) != 0; + default: + return (m_tag_information->memory[2][2] & 9) != 0; + } + // bits 0 and 3 + } else if (block_num <= MF0ICU1_PAGES) { bool locked = false; + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + // pages can be locked or not independant of BL bits + //the BL bits only freezes the lock bytes ! + uint16_t lock_bits = *(uint16_t *)&m_tag_information->memory[2][2]; + return ((lock_bits >> block_num) & 0x01) == 1; + default: + // check block locking bits + if (block_num <= 9) locked |= (m_tag_information->memory[2][2] & 2) == 2; + else locked |= (m_tag_information->memory[2][2] & 4) == 4; - // check block locking bits - if (block_num <= 9) locked |= (m_tag_information->memory[2][2] & 2) == 2; - else locked |= (m_tag_information->memory[2][2] & 4) == 4; + locked |= (((*(uint16_t *)&m_tag_information->memory[2][2]) >> block_num) & 1) == 1; - locked |= (((*(uint16_t *)&m_tag_information->memory[2][2]) >> block_num) & 1) == 1; - - return locked; + return locked; + } } else { uint8_t *p_lock_bytes = NULL; int user_memory_end = 0; @@ -776,9 +796,18 @@ static bool check_ro_lock_on_page(int block_num) { bool locked_small_range = ((lock_word >> (index / dyn_lock_bit_page_cnt)) & 1) != 0; bool locked_large_range = ((p_lock_bytes[2] >> (index / dyn_lock_bit_page_cnt / 2)) & 1) != 0; - - return locked_small_range | locked_large_range; + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + // For NTAG213/215/216: byte 2 contains block-locking bits (BL) which only freeze + // the lock configuration. We only check the actual lock bits (L0-L15) in bytes 0-1. + return locked_small_range; + default: + return locked_small_range | locked_large_range; + } } else { + //TODO needs to check the block locking bits to see if we can touch the dynamic locks bytes for NTAG tags // check CFGLCK bit int first_cfg_page = get_first_cfg_page_by_tag_type(m_tag_type); uint8_t access = m_tag_information->memory[first_cfg_page + CONF_ACCESS_PAGE_OFFSET][CONF_ACCESS_BYTE]; From f6cde1629ae8907ee4108b28e19845bfd6738bd2 Mon Sep 17 00:00:00 2001 From: dogty Date: Sat, 1 Nov 2025 18:12:41 +0100 Subject: [PATCH 4/6] fix(ntag): CFGLCK bit is checked before to see if CFG0 and CFG1 are writable even if a password is set (only for NTAG215, NTAG216 and NTAG213) --- .../src/rfid/nfctag/hf/nfc_mf0_ntag.c | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c index 8678a80..1229239 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c @@ -822,7 +822,25 @@ static bool check_ro_lock_on_page(int block_num) { static int handle_write_command(uint8_t block_num, uint8_t *p_data) { int block_max = get_block_max_by_tag_type(m_tag_type, false); - if (block_num >= block_max) { + bool out_of_bounds = false; + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + int first_cfg_page = get_first_cfg_page_by_tag_type(m_tag_type); + uint8_t cfglck = m_tag_information->memory[first_cfg_page][0] & 0x40; + // For NTAG cards we need to check CFGLCK bit for config pages + bool is_config_page = (block_num >= first_cfg_page) && (block_num <= first_cfg_page + 1); + bool config_locked = (cfglck != 0) && (!m_tag_information->config.mode_uid_magic); + bool is_beyond_user_memory = (block_num >= block_max); + out_of_bounds = (is_beyond_user_memory && !is_config_page) || (config_locked && is_config_page); + break; + default: + out_of_bounds = block_num >= block_max; + break; + } + // Reject out-of-bounds writes (except config pages) + if (out_of_bounds) { NRF_LOG_ERROR("Write failed: block_num %08x >= block_max %08x", block_num, block_max); return NAK_INVALID_OPERATION_TBV; } From 64f22dfc53c2ed5656865e6c05b0218e4fefa08a Mon Sep 17 00:00:00 2001 From: dogty Date: Sat, 1 Nov 2025 18:13:07 +0100 Subject: [PATCH 5/6] docs: updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bea1f7..83b0f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ 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] + - Fix for FAST_READ command for nfc - mf0 tags + - Rewrite of the dynamic and static locks logic for NTAG213, NTAG215 and NTAG216; we shouldn't take into account the block lock bits + - Fixed an issue where we wouldn't be able to change CFG0 and CFG1 for NTAG213, NTAG215 and NTG216 once a password was added even if the cfg bit was reset. - Fix for static nested key recovery (@jekkos) ## [v2.1.0][2025-09-02] From 11bafac176d245cba7eba38ee463a28e68449df6 Mon Sep 17 00:00:00 2001 From: dogty Date: Mon, 3 Nov 2025 13:55:30 +0100 Subject: [PATCH 6/6] fix(ntag): Check if the dynamic lock page can be written or not (for NTAG215, NTAG213 and NTAG216) --- .../src/rfid/nfctag/hf/nfc_mf0_ntag.c | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c index 1229239..dc0a319 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c @@ -807,7 +807,32 @@ static bool check_ro_lock_on_page(int block_num) { return locked_small_range | locked_large_range; } } else { - //TODO needs to check the block locking bits to see if we can touch the dynamic locks bytes for NTAG tags + //Check the block locking bits to see if we can touch the dynamic locks bytes for NTAG tags + if(block_num == user_memory_end) + { + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + { + uint8_t block_bytes = m_tag_information->memory[user_memory_end][2]; + uint16_t block_world = 0; + + // Each bit in block_bytes maps to 2 bits in block_world + for (int i = 0; i < 8; i++) { + if (block_bytes & (0x01 << i)) { + block_world |= (0x0003 << (i * 2)); + } + } + + p_lock_bytes = m_tag_information->memory[user_memory_end]; + uint16_t lock_word = (((uint16_t)p_lock_bytes[1]) << 8) | (uint16_t)p_lock_bytes[0]; + return (lock_word & block_world) != 0; + } + default: + break; + } + } // check CFGLCK bit int first_cfg_page = get_first_cfg_page_by_tag_type(m_tag_type); uint8_t access = m_tag_information->memory[first_cfg_page + CONF_ACCESS_PAGE_OFFSET][CONF_ACCESS_BYTE];