Decrypts a channel message, with replay protection and epoch checks.
Replay protection (F7 — sliding window): the last REPLAY_WINDOW_SIZE
(1000) successfully decrypted sequence numbers are remembered per
channel. A duplicate inside the window is rejected (REPLAY_DETECTED);
out-of-order delivery WITHIN the window is accepted (breaking change
vs the previous strictly-increasing-seq policy, which rejected any
non-advancing sequence). Sequences evicted from the window can replay
again — keep the window sized for the expected out-of-order delay and
rotate high-volume channels. Failed decrypt attempts do not consume
window slots (the sequence is recorded only after successful
authentication).
Window reset: rotateChannel starts a fresh epoch with a fresh
sequence space — the window resets with it.
Epoch check: msg.epoch must match the channel's current epoch.
Messages from previous epochs are rejected — that is the point
of epoch rotation (forward secrecy at rotation points).
Decrypts a channel message, with replay protection and epoch checks.
Replay protection (F7 — sliding window): the last REPLAY_WINDOW_SIZE (1000) successfully decrypted sequence numbers are remembered per channel. A duplicate inside the window is rejected (REPLAY_DETECTED); out-of-order delivery WITHIN the window is accepted (breaking change vs the previous strictly-increasing-seq policy, which rejected any non-advancing sequence). Sequences evicted from the window can replay again — keep the window sized for the expected out-of-order delay and rotate high-volume channels. Failed decrypt attempts do not consume window slots (the sequence is recorded only after successful authentication).
Window reset: rotateChannel starts a fresh epoch with a fresh sequence space — the window resets with it.
Epoch check: msg.epoch must match the channel's current epoch. Messages from previous epochs are rejected — that is the point of epoch rotation (forward secrecy at rotation points).