Skip to main content

nox_crypto/sphinx/
packet.rs

1//! Fixed-size (32KB) `ChaCha20Poly1305` AEAD packet encapsulation with ISO/IEC 7816-4 padding.
2
3use chacha20poly1305::{
4    aead::{Aead, KeyInit},
5    ChaCha20Poly1305, Nonce,
6};
7use thiserror::Error;
8
9pub const PACKET_SIZE: usize = 32_768;
10
11/// Reserved for Sphinx header (1KB). Accommodates ephemeral key + routing info + MAC + nonce + extensions.
12pub const HEADER_SIZE: usize = 1024;
13
14pub const POLY1305_TAG_SIZE: usize = 16;
15pub const NONCE_SIZE: usize = 12;
16pub const PAYLOAD_OVERHEAD: usize = POLY1305_TAG_SIZE + NONCE_SIZE;
17pub const MAX_PAYLOAD_SIZE: usize = PACKET_SIZE - HEADER_SIZE - PAYLOAD_OVERHEAD;
18
19#[derive(Debug, Error)]
20pub enum PacketError {
21    #[error("Payload too large: {size} bytes exceeds maximum of {max} bytes")]
22    PayloadTooLarge { size: usize, max: usize },
23
24    #[error("Decryption failed: packet integrity check failed")]
25    DecryptionFailed,
26
27    #[error("Invalid padding: could not find 0x80 padding marker")]
28    InvalidPadding,
29
30    #[error("Invalid packet size: expected {expected} bytes, got {actual} bytes")]
31    InvalidSize { expected: usize, actual: usize },
32}
33
34/// Fixed-size (32KB) encrypted packet: `[Header: 1024][Nonce: 12][Ciphertext + Tag]`.
35#[derive(Debug, Clone)]
36pub struct SphinxPacket(Vec<u8>);
37
38impl SphinxPacket {
39    /// Encrypts a variable-length payload into a fixed-size 32KB packet.
40    pub fn new(payload: &[u8], key: &[u8; 32], nonce: &[u8; 12]) -> Result<Self, PacketError> {
41        if payload.len() + 1 > MAX_PAYLOAD_SIZE {
42            return Err(PacketError::PayloadTooLarge {
43                size: payload.len(),
44                max: MAX_PAYLOAD_SIZE - 1,
45            });
46        }
47
48        let padded = pad_iso7816(payload);
49        debug_assert_eq!(padded.len(), MAX_PAYLOAD_SIZE);
50
51        let cipher = ChaCha20Poly1305::new(key.into());
52        let nonce_obj = Nonce::from_slice(nonce);
53        let ciphertext = cipher
54            .encrypt(nonce_obj, padded.as_slice())
55            .map_err(|_| PacketError::DecryptionFailed)?;
56
57        debug_assert_eq!(ciphertext.len(), MAX_PAYLOAD_SIZE + POLY1305_TAG_SIZE);
58
59        let mut packet = vec![0u8; PACKET_SIZE];
60
61        // Header area left as zeros -- filled by Sphinx layer
62        let nonce_start = HEADER_SIZE;
63        let nonce_end = nonce_start + NONCE_SIZE;
64        packet[nonce_start..nonce_end].copy_from_slice(nonce);
65
66        let ciphertext_start = nonce_end;
67        packet[ciphertext_start..ciphertext_start + ciphertext.len()].copy_from_slice(&ciphertext);
68
69        debug_assert_eq!(packet.len(), PACKET_SIZE);
70
71        Ok(Self(packet))
72    }
73
74    /// Decrypts and unpads the packet to recover the original payload.
75    pub fn unwrap(&self, key: &[u8; 32]) -> Result<Vec<u8>, PacketError> {
76        if self.0.len() != PACKET_SIZE {
77            return Err(PacketError::InvalidSize {
78                expected: PACKET_SIZE,
79                actual: self.0.len(),
80            });
81        }
82
83        let nonce_start = HEADER_SIZE;
84        let nonce_end = nonce_start + NONCE_SIZE;
85        let nonce = Nonce::from_slice(&self.0[nonce_start..nonce_end]);
86
87        let ciphertext_start = nonce_end;
88        let ciphertext = &self.0[ciphertext_start..];
89
90        let cipher = ChaCha20Poly1305::new(key.into());
91        let plaintext = cipher
92            .decrypt(nonce, ciphertext)
93            .map_err(|_| PacketError::DecryptionFailed)?;
94
95        unpad_iso7816(&plaintext)
96    }
97
98    /// Returns the raw packet bytes (always exactly `PACKET_SIZE`).
99    #[inline]
100    #[must_use]
101    pub fn as_bytes(&self) -> &[u8] {
102        &self.0
103    }
104
105    /// Consumes the packet, returning the underlying buffer.
106    #[inline]
107    #[must_use]
108    pub fn into_bytes(self) -> Vec<u8> {
109        self.0
110    }
111
112    /// Creates a `SphinxPacket` from raw bytes. Fails if not exactly `PACKET_SIZE`.
113    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, PacketError> {
114        if bytes.len() != PACKET_SIZE {
115            return Err(PacketError::InvalidSize {
116                expected: PACKET_SIZE,
117                actual: bytes.len(),
118            });
119        }
120        Ok(Self(bytes))
121    }
122
123    /// Mutable access to the header section for Sphinx routing info.
124    #[inline]
125    pub fn header_mut(&mut self) -> &mut [u8] {
126        &mut self.0[..HEADER_SIZE]
127    }
128
129    /// Read access to the header section.
130    #[inline]
131    #[must_use]
132    pub fn header(&self) -> &[u8] {
133        &self.0[..HEADER_SIZE]
134    }
135}
136
137/// Applies ISO/IEC 7816-4 padding: append `0x80` then fill with `0x00`.
138fn pad_iso7816(payload: &[u8]) -> Vec<u8> {
139    let mut padded = Vec::with_capacity(MAX_PAYLOAD_SIZE);
140    padded.extend_from_slice(payload);
141    padded.push(0x80);
142    padded.resize(MAX_PAYLOAD_SIZE, 0x00);
143
144    padded
145}
146
147/// Removes ISO/IEC 7816-4 padding (constant-time to prevent padding oracle attacks).
148fn unpad_iso7816(padded: &[u8]) -> Result<Vec<u8>, PacketError> {
149    super::unpad_iso7816_inner(padded).ok_or(PacketError::InvalidPadding)
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use rand::Rng;
156
157    #[test]
158    fn test_encrypt_decrypt_short_payload() {
159        let payload = b"Hello World";
160        let key = [0x42u8; 32];
161        let nonce = [0x13u8; 12];
162
163        let packet = SphinxPacket::new(payload, &key, &nonce).expect("Encryption failed");
164        let decrypted = packet.unwrap(&key).expect("Decryption failed");
165
166        assert_eq!(decrypted, payload);
167    }
168
169    #[test]
170    fn test_encrypt_decrypt_max_payload() {
171        let mut rng = rand::thread_rng();
172
173        // Max payload is MAX_PAYLOAD_SIZE - 1 (need 1 byte for 0x80 marker)
174        let payload_len = MAX_PAYLOAD_SIZE - 1;
175        let payload: Vec<u8> = (0..payload_len).map(|_| rng.gen()).collect();
176
177        let key: [u8; 32] = rng.gen();
178        let nonce: [u8; 12] = rng.gen();
179
180        let packet = SphinxPacket::new(&payload, &key, &nonce).expect("Encryption failed");
181        let decrypted = packet.unwrap(&key).expect("Decryption failed");
182
183        assert_eq!(decrypted, payload);
184    }
185
186    #[test]
187    fn test_payload_too_large() {
188        let payload = vec![0xAA; MAX_PAYLOAD_SIZE];
189        let key = [0x00u8; 32];
190        let nonce = [0x00u8; 12];
191
192        let result = SphinxPacket::new(&payload, &key, &nonce);
193
194        assert!(matches!(result, Err(PacketError::PayloadTooLarge { .. })));
195    }
196
197    #[test]
198    fn test_tampered_packet() {
199        let payload = b"Sensitive Data";
200        let key = [0x55u8; 32];
201        let nonce = [0xAAu8; 12];
202
203        let mut packet = SphinxPacket::new(payload, &key, &nonce).expect("Encryption failed");
204
205        let tamper_pos = HEADER_SIZE + NONCE_SIZE + 10;
206        packet.0[tamper_pos] ^= 0xFF;
207
208        let result = packet.unwrap(&key);
209
210        assert!(matches!(result, Err(PacketError::DecryptionFailed)));
211    }
212
213    #[test]
214    fn test_packet_size_exactly_32kb() {
215        let payload = b"Any payload";
216        let key = [0x11u8; 32];
217        let nonce = [0x22u8; 12];
218
219        let packet = SphinxPacket::new(payload, &key, &nonce).expect("Encryption failed");
220
221        assert_eq!(
222            packet.as_bytes().len(),
223            PACKET_SIZE,
224            "Packet must be exactly {} bytes",
225            PACKET_SIZE
226        );
227        assert_eq!(packet.as_bytes().len(), 32_768);
228    }
229
230    #[test]
231    fn test_empty_payload() {
232        let payload = b"";
233        let key = [0x33u8; 32];
234        let nonce = [0x44u8; 12];
235
236        let packet = SphinxPacket::new(payload, &key, &nonce).expect("Encryption failed");
237        let decrypted = packet.unwrap(&key).expect("Decryption failed");
238
239        assert_eq!(decrypted, payload);
240        assert_eq!(packet.as_bytes().len(), PACKET_SIZE);
241    }
242
243    #[test]
244    fn test_constants() {
245        assert_eq!(PACKET_SIZE, 32_768);
246        assert_eq!(HEADER_SIZE, 1024);
247        assert_eq!(POLY1305_TAG_SIZE, 16);
248        assert_eq!(NONCE_SIZE, 12);
249        assert_eq!(PAYLOAD_OVERHEAD, 28);
250        assert_eq!(MAX_PAYLOAD_SIZE, 31_716);
251
252        assert_eq!(
253            HEADER_SIZE + NONCE_SIZE + MAX_PAYLOAD_SIZE + POLY1305_TAG_SIZE,
254            PACKET_SIZE
255        );
256    }
257}