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}