nox_crypto/sphinx/
packet.rs1use chacha20poly1305::{
4 aead::{Aead, KeyInit},
5 ChaCha20Poly1305, Nonce,
6};
7use thiserror::Error;
8
9pub const PACKET_SIZE: usize = 32_768;
10
11pub 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#[derive(Debug, Clone)]
36pub struct SphinxPacket(Vec<u8>);
37
38impl SphinxPacket {
39 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 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 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 #[inline]
100 #[must_use]
101 pub fn as_bytes(&self) -> &[u8] {
102 &self.0
103 }
104
105 #[inline]
107 #[must_use]
108 pub fn into_bytes(self) -> Vec<u8> {
109 self.0
110 }
111
112 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 #[inline]
125 pub fn header_mut(&mut self) -> &mut [u8] {
126 &mut self.0[..HEADER_SIZE]
127 }
128
129 #[inline]
131 #[must_use]
132 pub fn header(&self) -> &[u8] {
133 &self.0[..HEADER_SIZE]
134 }
135}
136
137fn 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
147fn 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 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}