Skip to main content

fips_core/noise/
session.rs

1use super::{CipherState, HandshakeRole, NoiseError, ReplayWindow};
2use ring::aead::LessSafeKey;
3use secp256k1::{PublicKey, XOnlyPublicKey};
4use std::fmt;
5use std::sync::Arc;
6
7/// Completed Noise session for transport encryption.
8///
9/// Provides bidirectional authenticated encryption with replay protection.
10/// The send counter is monotonically incremented; received counters are
11/// validated against a sliding window to prevent replay attacks.
12pub struct NoiseSession {
13    /// Our role in the original handshake.
14    role: HandshakeRole,
15    /// Cipher for sending.
16    send_cipher: CipherState,
17    /// Cipher for receiving.
18    recv_cipher: CipherState,
19    /// Handshake hash for channel binding.
20    handshake_hash: [u8; 32],
21    /// Remote peer's static public key.
22    remote_static: PublicKey,
23    /// Replay window for received packets.
24    replay_window: ReplayWindow,
25}
26
27impl NoiseSession {
28    /// Create a new session from completed handshake data.
29    pub(super) fn from_handshake(
30        role: HandshakeRole,
31        send_cipher: CipherState,
32        recv_cipher: CipherState,
33        handshake_hash: [u8; 32],
34        remote_static: PublicKey,
35    ) -> Self {
36        Self {
37            role,
38            send_cipher,
39            recv_cipher,
40            handshake_hash,
41            remote_static,
42            replay_window: ReplayWindow::new(),
43        }
44    }
45
46    /// Encrypt a message for sending (using internal counter).
47    ///
48    /// Returns the ciphertext. The current send counter should be included
49    /// in the wire format before calling this method.
50    pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, NoiseError> {
51        self.send_cipher.encrypt(plaintext)
52    }
53
54    /// Get the current send counter (before incrementing).
55    ///
56    /// Use this to get the counter to include in the wire format.
57    /// The counter will be incremented when `encrypt` is called.
58    pub fn current_send_counter(&self) -> u64 {
59        self.send_cipher.nonce
60    }
61
62    /// Decrypt a received message (using internal counter).
63    ///
64    /// This is for handshake-phase decryption. For transport phase with
65    /// explicit counters, use `decrypt_with_replay_check` instead.
66    pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, NoiseError> {
67        self.recv_cipher.decrypt(ciphertext)
68    }
69
70    /// Check if a counter passes the replay window.
71    ///
72    /// Returns Ok(()) if the counter is acceptable, Err if it should be rejected.
73    /// Call this before attempting decryption to avoid wasting CPU on replay attacks.
74    pub fn check_replay(&self, counter: u64) -> Result<(), NoiseError> {
75        if self.replay_window.check(counter) {
76            Ok(())
77        } else {
78            Err(NoiseError::ReplayDetected(counter))
79        }
80    }
81
82    /// Decrypt with explicit counter and replay protection.
83    ///
84    /// This is the primary decryption method for transport phase.
85    /// The counter comes from the wire format and is validated against
86    /// the replay window before and after decryption.
87    ///
88    /// On success, the counter is accepted into the replay window.
89    pub fn decrypt_with_replay_check(
90        &mut self,
91        ciphertext: &[u8],
92        counter: u64,
93    ) -> Result<Vec<u8>, NoiseError> {
94        // Check replay window first (cheap)
95        if !self.replay_window.check(counter) {
96            return Err(NoiseError::ReplayDetected(counter));
97        }
98
99        // Attempt decryption (expensive)
100        let plaintext = self.recv_cipher.decrypt_with_counter(ciphertext, counter)?;
101
102        // Only accept into window after successful decryption
103        // This prevents DoS attacks that exhaust the window
104        self.replay_window.accept(counter);
105
106        Ok(plaintext)
107    }
108
109    /// Encrypt a message with Additional Authenticated Data (AAD).
110    ///
111    /// Returns the ciphertext. The current send counter should be included
112    /// in the wire format before calling this method.
113    pub fn encrypt_with_aad(
114        &mut self,
115        plaintext: &[u8],
116        aad: &[u8],
117    ) -> Result<Vec<u8>, NoiseError> {
118        self.send_cipher.encrypt_with_aad(plaintext, aad)
119    }
120
121    /// Decrypt with explicit counter, replay protection, and AAD.
122    ///
123    /// This is the primary decryption method for the FMP transport phase
124    /// with AAD binding. The AAD (typically the 16-byte outer header) must
125    /// match what was used during encryption.
126    pub fn decrypt_with_replay_check_and_aad(
127        &mut self,
128        ciphertext: &[u8],
129        counter: u64,
130        aad: &[u8],
131    ) -> Result<Vec<u8>, NoiseError> {
132        // Check replay window first (cheap)
133        if !self.replay_window.check(counter) {
134            return Err(NoiseError::ReplayDetected(counter));
135        }
136
137        // Attempt decryption with AAD (expensive)
138        let plaintext = self
139            .recv_cipher
140            .decrypt_with_counter_and_aad(ciphertext, counter, aad)?;
141
142        // Only accept into window after successful decryption
143        self.replay_window.accept(counter);
144
145        Ok(plaintext)
146    }
147
148    /// In-place variant of [`Self::decrypt_with_replay_check_and_aad`].
149    ///
150    /// On entry, `buf` holds `ciphertext + 16-byte AEAD tag`. On
151    /// successful return, `buf[..returned_len]` holds the plaintext.
152    /// The caller can then slice into `buf` without paying for an
153    /// extra heap allocation + memcpy per packet — at multi-Gbps
154    /// single-stream the by-value variant's `ciphertext.to_vec()`
155    /// alone is a measurable fraction of the rx_loop's per-packet
156    /// cost.
157    pub fn decrypt_with_replay_check_and_aad_in_place(
158        &mut self,
159        buf: &mut [u8],
160        counter: u64,
161        aad: &[u8],
162    ) -> Result<usize, NoiseError> {
163        if !self.replay_window.check(counter) {
164            return Err(NoiseError::ReplayDetected(counter));
165        }
166        let plaintext_len = self
167            .recv_cipher
168            .decrypt_with_counter_and_aad_in_place(buf, counter, aad)?;
169        self.replay_window.accept(counter);
170        Ok(plaintext_len)
171    }
172
173    /// Get the highest received counter.
174    pub fn highest_received_counter(&self) -> u64 {
175        self.replay_window.highest()
176    }
177
178    /// Clone the recv-side AEAD instance, for off-task decrypt.
179    ///
180    /// Returns `None` if the recv cipher has no key (transport phase has
181    /// not begun). The cloned cipher pairs with `decrypt_with_counter[_and_aad]`
182    /// on `CipherState`: a dispatcher can `check_replay` here, fan the
183    /// AEAD work out to a worker holding the clone + counter + aad, then
184    /// call `accept_replay` here once the worker reports success.
185    pub fn recv_cipher_clone(&self) -> Option<LessSafeKey> {
186        self.recv_cipher.cipher_clone()
187    }
188
189    /// Snapshot the current replay-window state as an **owned**
190    /// `ReplayWindow` value, for hand-off to a shard-owning decrypt
191    /// worker.
192    ///
193    /// **The worker becomes the sole authority for replay protection
194    /// on this session after this snapshot.** The local
195    /// `self.replay_window` is no longer the source of truth — it
196    /// only matters for rare-slow-path uses (rekey, drain-window
197    /// fallback). The worker keeps its copy in its own
198    /// thread-local `HashMap`, so there's no Mutex / no Arc / no
199    /// sharing — direct `&mut` access on every packet.
200    ///
201    /// (Previously this returned an `Arc<Mutex<ReplayWindow>>` for
202    /// concurrent access; the data-plane shard restructure now hands
203    /// the worker exclusive ownership instead.)
204    pub fn recv_replay_snapshot_owned(&self) -> crate::noise::ReplayWindow {
205        self.replay_window.clone()
206    }
207
208    /// Clone the send-side AEAD instance as an owned key.
209    ///
210    /// Returns `None` if the send cipher has no key. Pairs with
211    /// `encrypt_with_counter[_and_aad]` on `CipherState`. Hot send-worker
212    /// paths should prefer [`Self::send_cipher_handle`] so they do not rebuild
213    /// the keyed AEAD per packet. The caller must own counter sequencing —
214    /// `take_send_counter` hands out monotonic counters under the session's
215    /// own &mut.
216    pub fn send_cipher_clone(&self) -> Option<LessSafeKey> {
217        self.send_cipher.cipher_clone()
218    }
219
220    /// Cheap handle to the send-side AEAD for explicit-counter worker sends.
221    ///
222    /// The session still owns nonce assignment through `take_send_counter`.
223    pub(crate) fn send_cipher_handle(&self) -> Option<Arc<LessSafeKey>> {
224        self.send_cipher.cipher_handle()
225    }
226
227    /// Whether the send-side cipher is keyed for worker-side encryption.
228    pub fn has_send_cipher(&self) -> bool {
229        self.send_cipher.has_key()
230    }
231
232    /// Reserve and return the next send counter, advancing the internal
233    /// nonce. For pipelined encrypt paths, the dispatcher pre-assigns the
234    /// counter here (under the session's &mut) and the worker performs the
235    /// AEAD using the shared send-cipher handle with no further mutation of
236    /// session state.
237    pub fn take_send_counter(&mut self) -> Result<u64, NoiseError> {
238        if self.send_cipher.nonce == u64::MAX {
239            return Err(NoiseError::NonceOverflow);
240        }
241        let counter = self.send_cipher.nonce;
242        self.send_cipher.nonce += 1;
243        Ok(counter)
244    }
245
246    /// Accept a counter into the replay window after a successful out-of-task
247    /// decrypt. Caller is responsible for verifying decrypt success first.
248    pub fn accept_replay(&mut self, counter: u64) {
249        self.replay_window.accept(counter);
250    }
251
252    /// Reset the replay window (use when rekeying).
253    pub fn reset_replay_window(&mut self) {
254        self.replay_window.reset();
255    }
256
257    /// Get the handshake hash for channel binding.
258    pub fn handshake_hash(&self) -> &[u8; 32] {
259        &self.handshake_hash
260    }
261
262    /// Get the remote peer's static public key.
263    pub fn remote_static(&self) -> &PublicKey {
264        &self.remote_static
265    }
266
267    /// Get the remote peer's x-only public key.
268    pub fn remote_static_xonly(&self) -> XOnlyPublicKey {
269        self.remote_static.x_only_public_key().0
270    }
271
272    /// Get our role in the handshake.
273    pub fn role(&self) -> HandshakeRole {
274        self.role
275    }
276
277    /// Get the send nonce (for debugging).
278    pub fn send_nonce(&self) -> u64 {
279        self.send_cipher.nonce()
280    }
281
282    /// Get the receive nonce (for debugging).
283    pub fn recv_nonce(&self) -> u64 {
284        self.recv_cipher.nonce()
285    }
286}
287
288impl fmt::Debug for NoiseSession {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        f.debug_struct("NoiseSession")
291            .field("role", &self.role)
292            .field("send_nonce", &self.send_cipher.nonce())
293            .field("recv_nonce", &self.recv_cipher.nonce())
294            .field("handshake_hash", &hex::encode(&self.handshake_hash[..8]))
295            .finish()
296    }
297}