Skip to main content

srx/session/
state.rs

1//! Active session state.
2
3use crate::crypto::{AeadPipeline, KeyDerivation};
4use crate::seed::SeedRng;
5
6/// An established SRX session holding all runtime state.
7pub struct Session {
8    /// Session identifier.
9    pub id: u64,
10    /// Current seed RNG (shared with peer).
11    pub rng: SeedRng,
12    /// Current data encryption key.
13    pub data_key: [u8; 32],
14    /// Packet counter for this session.
15    pub packet_counter: u64,
16    /// Whether the session is still active.
17    pub active: bool,
18    /// Key derivation epoch (incremented on each re-key).
19    pub key_index: u64,
20}
21
22impl Session {
23    /// Build session state from `K_master` and handshake parameters (matches peer’s `derive_initial_seed`).
24    pub fn from_master_secret(
25        id: u64,
26        master_key: &[u8; 32],
27        timestamp: u64,
28        session_nonce: &[u8],
29    ) -> crate::error::Result<Self> {
30        let seed =
31            KeyDerivation::derive_initial_seed(master_key.as_slice(), timestamp, session_nonce)?;
32        let data_key = KeyDerivation::derive_data_key(&seed, 0)?;
33        Ok(Self::new(id, seed, data_key))
34    }
35
36    /// Create a new session from handshake results.
37    pub fn new(id: u64, seed: [u8; 32], data_key: [u8; 32]) -> Self {
38        Self {
39            id,
40            rng: SeedRng::new(seed),
41            data_key,
42            packet_counter: 0,
43            active: true,
44            key_index: 0,
45        }
46    }
47
48    /// Increment the packet counter and return the new value.
49    pub fn next_packet_counter(&mut self) -> u64 {
50        self.packet_counter += 1;
51        self.packet_counter
52    }
53
54    /// Rotate the data key using KDF.
55    ///
56    /// Derives `data_key[key_index+1] = HKDF(seed, key_index+1)` and increments
57    /// `key_index`. Returns the new key.
58    pub fn rekey(&mut self) -> crate::error::Result<[u8; 32]> {
59        self.key_index += 1;
60        self.data_key = KeyDerivation::derive_data_key(&self.rng.seed_bytes(), self.key_index)?;
61        Ok(self.data_key)
62    }
63
64    /// Mark the session as closed.
65    pub fn close(&mut self) {
66        self.active = false;
67    }
68
69    /// Encrypt application payload using the parallel AEAD pool and a KDF-derived nonce.
70    ///
71    /// Advances [`Session::packet_counter`] and derives `nonce = HKDF(seed ‖ counter)`.
72    pub fn encrypt_with_pipeline(
73        &mut self,
74        pipeline: &AeadPipeline,
75        plaintext: &[u8],
76    ) -> crate::error::Result<Vec<u8>> {
77        let counter = self.next_packet_counter();
78        let nonce = KeyDerivation::derive_nonce(&self.rng.seed_bytes(), counter)?;
79        pipeline.encrypt(nonce, plaintext.to_vec())
80    }
81
82    /// Decrypt using an explicit nonce (e.g. derived on receive from frame metadata or inner payload).
83    pub fn decrypt_with_pipeline(
84        &self,
85        pipeline: &AeadPipeline,
86        nonce: [u8; 12],
87        ciphertext: Vec<u8>,
88    ) -> crate::error::Result<Vec<u8>> {
89        pipeline.decrypt(nonce, ciphertext)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::config::AeadCipher as Variant;
97    use crate::crypto::KeyDerivation;
98
99    #[test]
100    fn from_master_secret_derivation() {
101        let master = [0x77u8; 32];
102        let s = Session::from_master_secret(1, &master, 1_700_000_000, b"nonce").unwrap();
103        assert_eq!(s.id, 1);
104        assert!(s.active);
105    }
106
107    #[test]
108    fn encrypt_decrypt_via_pipeline_matches_kdf_nonce() {
109        let seed = [0xABu8; 32];
110        let key = [0xCDu8; 32];
111        let pipe = AeadPipeline::new(Variant::ChaCha20Poly1305, &key, 3).unwrap();
112        let mut session = Session::new(9, seed, key);
113
114        let ct = session.encrypt_with_pipeline(&pipe, b"payload").unwrap();
115        let nonce =
116            KeyDerivation::derive_nonce(&session.rng.seed_bytes(), session.packet_counter).unwrap();
117        let pt = session.decrypt_with_pipeline(&pipe, nonce, ct).unwrap();
118        assert_eq!(pt.as_slice(), b"payload");
119    }
120}