Skip to main content

fips_core/noise/
session.rs

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