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