nomad_protocol/crypto/
nonce.rs

1//! Nonce construction for XChaCha20-Poly1305
2//!
3//! Per 1-SECURITY.md, nonces are 24 bytes:
4//! - Epoch (4 bytes)
5//! - Direction (1 byte): 0x00 = Initiator→Responder, 0x01 = Responder→Initiator
6//! - Zeros (11 bytes)
7//! - Counter (8 bytes)
8
9use crate::core::{AEAD_NONCE_SIZE, NONCE_DIR_INITIATOR, NONCE_DIR_RESPONDER};
10
11/// Direction of communication for nonce construction.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Direction {
14    /// Initiator → Responder (0x00)
15    InitiatorToResponder,
16    /// Responder → Initiator (0x01)
17    ResponderToInitiator,
18}
19
20impl Direction {
21    /// Get the byte representation.
22    pub fn as_byte(self) -> u8 {
23        match self {
24            Direction::InitiatorToResponder => NONCE_DIR_INITIATOR,
25            Direction::ResponderToInitiator => NONCE_DIR_RESPONDER,
26        }
27    }
28
29    /// Get the opposite direction.
30    pub fn opposite(self) -> Self {
31        match self {
32            Direction::InitiatorToResponder => Direction::ResponderToInitiator,
33            Direction::ResponderToInitiator => Direction::InitiatorToResponder,
34        }
35    }
36}
37
38/// Construct a 24-byte XChaCha20-Poly1305 nonce.
39///
40/// Layout:
41/// ```text
42/// [ epoch (4) | direction (1) | zeros (11) | counter (8) ]
43/// ```
44///
45/// # Arguments
46/// * `epoch` - Current key epoch (increments on rekey)
47/// * `direction` - Communication direction
48/// * `counter` - Per-direction frame counter
49pub fn construct_nonce(epoch: u32, direction: Direction, counter: u64) -> [u8; AEAD_NONCE_SIZE] {
50    let mut nonce = [0u8; AEAD_NONCE_SIZE];
51
52    // Epoch (bytes 0-3, little-endian)
53    nonce[0..4].copy_from_slice(&epoch.to_le_bytes());
54
55    // Direction (byte 4)
56    nonce[4] = direction.as_byte();
57
58    // Zeros (bytes 5-15) - already zeroed
59
60    // Counter (bytes 16-23, little-endian)
61    nonce[16..24].copy_from_slice(&counter.to_le_bytes());
62
63    nonce
64}
65
66/// Parse a nonce back into its components.
67///
68/// Useful for debugging and testing.
69pub fn parse_nonce(nonce: &[u8; AEAD_NONCE_SIZE]) -> (u32, Direction, u64) {
70    let epoch = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
71
72    let direction = if nonce[4] == NONCE_DIR_INITIATOR {
73        Direction::InitiatorToResponder
74    } else {
75        Direction::ResponderToInitiator
76    };
77
78    let counter = u64::from_le_bytes([
79        nonce[16], nonce[17], nonce[18], nonce[19], nonce[20], nonce[21], nonce[22], nonce[23],
80    ]);
81
82    (epoch, direction, counter)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_nonce_construction() {
91        let nonce = construct_nonce(1, Direction::InitiatorToResponder, 42);
92
93        assert_eq!(nonce.len(), AEAD_NONCE_SIZE);
94
95        // Verify epoch
96        assert_eq!(&nonce[0..4], &1u32.to_le_bytes());
97
98        // Verify direction
99        assert_eq!(nonce[4], 0x00);
100
101        // Verify zeros
102        assert_eq!(&nonce[5..16], &[0u8; 11]);
103
104        // Verify counter
105        assert_eq!(&nonce[16..24], &42u64.to_le_bytes());
106    }
107
108    #[test]
109    fn test_nonce_roundtrip() {
110        let epoch = 0x12345678;
111        let direction = Direction::ResponderToInitiator;
112        let counter = 0xDEADBEEFCAFEBABE;
113
114        let nonce = construct_nonce(epoch, direction, counter);
115        let (parsed_epoch, parsed_dir, parsed_counter) = parse_nonce(&nonce);
116
117        assert_eq!(parsed_epoch, epoch);
118        assert_eq!(parsed_dir, direction);
119        assert_eq!(parsed_counter, counter);
120    }
121
122    #[test]
123    fn test_direction_opposite() {
124        assert_eq!(
125            Direction::InitiatorToResponder.opposite(),
126            Direction::ResponderToInitiator
127        );
128        assert_eq!(
129            Direction::ResponderToInitiator.opposite(),
130            Direction::InitiatorToResponder
131        );
132    }
133
134    #[test]
135    fn test_direction_bytes() {
136        assert_eq!(Direction::InitiatorToResponder.as_byte(), 0x00);
137        assert_eq!(Direction::ResponderToInitiator.as_byte(), 0x01);
138    }
139}