From 8266982e4e2bb9f21a4779ec341561125fa347ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Jul 2026 03:06:50 -0400 Subject: [PATCH] Advance replay window only after CCM tag verifies verifyNonceReplay() both checked and advanced the replay window on the cleartext nonce, before the AES-CCM tag was verified. In an active session an attacker could send a forged frame with a high counter: it passed the replay check (not yet seen), the window advanced to that high counter, then the CCM tag failed and the frame was rejected -- but the window was now desynced, so subsequent legitimate frames with lower counters were rejected as replays. This is a denial-of-service on the session. Split the advance out of verifyNonceReplay() into advanceNonceReplay() and call it from decryptCommand() only after the CCM tag has verified. The replay-reject check stays before decrypt (a cheap, correct early-out for genuine replays); a forged frame with a bad tag no longer mutates last_seen_counter/replay_window. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR --- encryption.c | 21 +++++++++++++++++++-- encryption.h | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/encryption.c b/encryption.c index 626cdd9..c9b4616 100644 --- a/encryption.c +++ b/encryption.c @@ -250,13 +250,27 @@ bool verifyNonceReplay(uint8_t* nonce) } } + /* Do NOT advance the replay window here: a forged frame with a fresh + * counter passes this check but must not desync the window before its + * CCM tag is verified. advanceNonceReplay() is called only after the + * tag verifies successfully. */ + return true; +} + +void advanceNonceReplay(uint8_t* nonce) +{ + if (!encryptionSession.authenticated) return; + + uint64_t nonce_counter = 0; + for (int i = 0; i < 8; i++) { + nonce_counter = (nonce_counter << 8) | nonce[8 + i]; + } + if (nonce_counter > encryptionSession.last_seen_counter) encryptionSession.last_seen_counter = nonce_counter; encryptionSession.replay_window[encryptionSession.replay_idx] = nonce_counter; encryptionSession.replay_idx = (encryptionSession.replay_idx + 1) % 64; - - return true; } bool handleAuthenticate(uint8_t* data, uint16_t len, @@ -482,6 +496,9 @@ bool decryptCommand(uint8_t* ciphertext, uint16_t ciphertext_len, memcpy(plaintext, decrypted_with_length + 1, payload_length); } *plaintext_len = payload_length; + /* Tag verified: only now advance the replay window so a forged + * frame (bad tag) cannot desync it. */ + advanceNonceReplay(nonce_full); encryptionSession.integrity_failures = 0; updateEncryptionSessionActivity(); memset(decrypted_with_length, 0, sizeof(decrypted_with_length)); diff --git a/encryption.h b/encryption.h index 42d827f..b3fe445 100644 --- a/encryption.h +++ b/encryption.h @@ -31,6 +31,8 @@ void incrementNonceCounter(void); bool verifyNonceReplay(uint8_t* nonce); +void advanceNonceReplay(uint8_t* nonce); + bool decryptCommand(uint8_t* ciphertext, uint16_t ciphertext_len, uint8_t* plaintext, uint16_t* plaintext_len, uint8_t* nonce_full, uint8_t* auth_tag,