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}