Skip to content

Add replay protection to encrypted commands (MJ-11)#5

Open
balloob wants to merge 1 commit into
OpenDisplay:mainfrom
balloob:fix/encrypted-replay-protection
Open

Add replay protection to encrypted commands (MJ-11)#5
balloob wants to merge 1 commit into
OpenDisplay:mainfrom
balloob:fix/encrypted-replay-protection

Conversation

@balloob

@balloob balloob commented Jul 5, 2026

Copy link
Copy Markdown

Problem

The Silabs inbound encrypted-command path performs no replay protection. decrypt_encrypted_payload only checked session_alive() and the CCM tag; the replay-window fields in the session struct (last_seen_counter, replay_window, replay_idx) were never consulted on the inbound path. An attacker who captures any encrypted command frame in a live session can replay it verbatim and the device re-executes it.

The nRF firmware already validates a sliding nonce-counter window (encryption.c verifyNonceReplay, called from decryptCommand), so the two firmwares diverged in security behavior.

Fix (mirrors nRF)

Port nRF's verifyNonceReplay to Silabs as verify_nonce_replay and wire it into decrypt_encrypted_payload at the point the CCM tag is verified, matching nRF's decryptCommand semantics so both firmwares behave identically against the same client:

  • Parse the session_id (nonce[0..8]) and big-endian counter (nonce[8..16]) from the cleartext nonce.
  • Reject on session_id mismatch, or when the counter falls outside the +/-32 window, or when the counter is at/below last_seen_counter and already present in the 64-entry replay_window.
  • On accept: advance last_seen_counter when the counter is newer, and record the counter in the circular replay_window.
  • On replay/verification failure: increment integrity_failures and clear the session after 3, mirroring nRF; reset integrity_failures to 0 on a successful decrypt.

This uses the existing nonce counter only — the nonce direction-bit change (MJ-12) is a separate breaking change handled elsewhere and is intentionally not touched here. No other command handling is modified.

Testing

Full SLC/Simplicity firmware build could not be run in this environment (no arm-none-eabi-gcc / slc toolchain available). Verification done:

  • Manual compile-check of the new function against a mock of EncryptionSession with host GCC (-Wall -Wextra -std=c11): clean, no warnings; all referenced struct fields exist and types match.
  • Behavioral test of the ported logic confirms it matches nRF's verifyNonceReplay accept/reject decisions: fresh counter accepted, out-of-order replay below the window rejected, counter far outside the +/-32 window rejected, session_id mismatch rejected. The one nRF quirk (a replay of the exact current last_seen_counter, diff==0, is accepted until a newer counter arrives) is reproduced identically, keeping the two firmwares in lockstep.

🤖 Generated with Claude Code

https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR

The inbound encrypted-command path only verified session liveness and the
CCM tag, never the nonce counter. An attacker who captured any encrypted
command frame in a live session could replay it verbatim and the device
would re-execute it. The replay-window fields already existed in the
session struct but were unused on the inbound path.

Port the nRF firmware's verifyNonceReplay sliding-window check to Silabs
and wire it into decrypt_encrypted_payload at the point the CCM tag is
verified, mirroring nRF's decryptCommand semantics so both firmwares
behave identically against the same client: reject replays (already-seen
or below the +/-32 window), advance last_seen_counter/replay_window on
accept, and clear the session after repeated integrity failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant