1use std::marker::PhantomData;
33
34use zeroize::{Zeroize, Zeroizing};
35
36use auths_crypto::default_provider;
37
38use crate::domain_separation::ENVELOPE_INFO;
39use crate::sas::TransportKey;
40
41pub const MAX_MESSAGES_PER_SESSION: u32 = 1024;
44
45#[derive(Debug, thiserror::Error)]
47pub enum EnvelopeError {
48 #[error("envelope authentication failed")]
51 TagMismatch,
52
53 #[error("envelope counter not strictly monotonic: expected > {expected}, got {got}")]
56 CounterNotMonotonic {
57 expected: u32,
59 got: u32,
61 },
62
63 #[error("envelope AAD mismatch")]
66 AadMismatch,
67
68 #[error("envelope session exhausted (>{MAX_MESSAGES_PER_SESSION} messages)")]
71 SessionExhausted,
72
73 #[error("envelope key derivation failed: {0}")]
75 KeyDerivation(String),
76
77 #[error("envelope encrypt/decrypt failed: {0}")]
80 Cipher(String),
81}
82
83pub struct Sealed;
85pub struct Open;
87
88pub struct Envelope<S> {
92 nonce: [u8; 12],
93 counter: u32,
94 payload: Vec<u8>,
95 aad_session_id: String,
96 aad_path: String,
97 _state: PhantomData<S>,
98}
99
100impl<S> std::fmt::Debug for Envelope<S> {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("Envelope")
106 .field("counter", &self.counter)
107 .field("session_id", &self.aad_session_id)
108 .field("path", &self.aad_path)
109 .field(
110 "payload",
111 &format_args!("<{} bytes redacted>", self.payload.len()),
112 )
113 .finish()
114 }
115}
116
117impl<S> Envelope<S> {
118 pub fn counter(&self) -> u32 {
120 self.counter
121 }
122 pub fn session_id(&self) -> &str {
124 &self.aad_session_id
125 }
126 pub fn path(&self) -> &str {
128 &self.aad_path
129 }
130}
131
132impl Envelope<Sealed> {
133 pub fn ciphertext(&self) -> &[u8] {
135 &self.payload
136 }
137
138 pub fn nonce(&self) -> &[u8; 12] {
140 &self.nonce
141 }
142}
143
144impl Envelope<Open> {
145 pub fn plaintext(&self) -> &[u8] {
147 &self.payload
148 }
149}
150
151pub struct EnvelopeSession {
156 key: Zeroizing<[u8; 32]>,
157 iv: [u8; 12],
158 next_counter: u32,
159 last_opened_counter: Option<u32>,
160 session_id: String,
161}
162
163impl EnvelopeSession {
164 pub async fn new(
179 transport_key: &TransportKey,
180 session_id: String,
181 iv: [u8; 12],
182 ) -> Result<Self, EnvelopeError> {
183 let provider = default_provider();
184 let okm = provider
185 .hkdf_sha256_expand(transport_key.as_bytes(), &[], ENVELOPE_INFO, 32)
186 .await
187 .map_err(|e| EnvelopeError::KeyDerivation(e.to_string()))?;
188 let mut key_bytes = [0u8; 32];
189 key_bytes.copy_from_slice(&okm);
190 Ok(Self {
191 key: Zeroizing::new(key_bytes),
192 iv,
193 next_counter: 1,
194 last_opened_counter: None,
195 session_id,
196 })
197 }
198
199 pub async fn seal(
201 &mut self,
202 path: &str,
203 plaintext: &[u8],
204 ) -> Result<Envelope<Sealed>, EnvelopeError> {
205 if self.next_counter >= MAX_MESSAGES_PER_SESSION {
206 return Err(EnvelopeError::SessionExhausted);
207 }
208 let counter = self.next_counter;
209 self.next_counter = self.next_counter.wrapping_add(1);
210
211 let nonce = nonce_for_counter(&self.iv, counter);
212 let aad = build_aad(&self.session_id, path, counter);
213 let provider = default_provider();
214 let ct = provider
215 .aead_encrypt(&self.key, &nonce, &aad, plaintext)
216 .await
217 .map_err(|e| EnvelopeError::Cipher(e.to_string()))?;
218
219 Ok(Envelope {
220 nonce,
221 counter,
222 payload: ct,
223 aad_session_id: self.session_id.clone(),
224 aad_path: path.to_string(),
225 _state: PhantomData,
226 })
227 }
228
229 pub async fn open(
232 &mut self,
233 path: &str,
234 env: Envelope<Sealed>,
235 ) -> Result<Envelope<Open>, EnvelopeError> {
236 if let Some(last) = self.last_opened_counter
237 && env.counter <= last
238 {
239 return Err(EnvelopeError::CounterNotMonotonic {
240 expected: last,
241 got: env.counter,
242 });
243 }
244
245 let nonce = nonce_for_counter(&self.iv, env.counter);
246 if nonce != env.nonce {
247 return Err(EnvelopeError::AadMismatch);
248 }
249 if path != env.aad_path {
250 return Err(EnvelopeError::AadMismatch);
251 }
252 if self.session_id != env.aad_session_id {
253 return Err(EnvelopeError::AadMismatch);
254 }
255 let aad = build_aad(&self.session_id, path, env.counter);
256
257 let provider = default_provider();
258 let pt = provider
259 .aead_decrypt(&self.key, &nonce, &aad, &env.payload)
260 .await
261 .map_err(|_| EnvelopeError::TagMismatch)?;
262
263 self.last_opened_counter = Some(env.counter);
264 Ok(Envelope {
265 nonce: env.nonce,
266 counter: env.counter,
267 payload: pt,
268 aad_session_id: env.aad_session_id,
269 aad_path: env.aad_path,
270 _state: PhantomData,
271 })
272 }
273}
274
275impl Drop for EnvelopeSession {
276 fn drop(&mut self) {
277 self.iv.zeroize();
278 }
279}
280
281fn nonce_for_counter(iv: &[u8; 12], counter: u32) -> [u8; 12] {
282 let mut nonce = *iv;
283 let ctr = counter.to_be_bytes();
284 nonce[8] ^= ctr[0];
285 nonce[9] ^= ctr[1];
286 nonce[10] ^= ctr[2];
287 nonce[11] ^= ctr[3];
288 nonce
289}
290
291fn build_aad(session_id: &str, path: &str, counter: u32) -> Vec<u8> {
292 let sid = session_id.as_bytes();
293 let p = path.as_bytes();
294 let mut aad = Vec::with_capacity(4 + sid.len() + 4 + p.len() + 4);
295 aad.extend_from_slice(&(sid.len() as u32).to_be_bytes());
296 aad.extend_from_slice(sid);
297 aad.extend_from_slice(&(p.len() as u32).to_be_bytes());
298 aad.extend_from_slice(p);
299 aad.extend_from_slice(&counter.to_be_bytes());
300 aad
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::sas::TransportKey;
307
308 fn session_with_transport_key() -> (TransportKey, [u8; 12], String) {
309 let tk = TransportKey::new([0xA5; 32]);
310 let iv = [0x07; 12];
311 let session_id = "sess-kat".to_string();
312 (tk, iv, session_id)
313 }
314
315 #[tokio::test]
316 async fn seal_open_round_trip() {
317 let (tk, iv, sid) = session_with_transport_key();
318 let mut sender = EnvelopeSession::new(&tk, sid.clone(), iv).await.unwrap();
319 let mut receiver = EnvelopeSession::new(&TransportKey::new([0xA5; 32]), sid.clone(), iv)
321 .await
322 .unwrap();
323
324 let env = sender
325 .seal("/v1/pairing/sessions/x/response", b"hello world")
326 .await
327 .unwrap();
328 let opened = receiver
329 .open("/v1/pairing/sessions/x/response", env)
330 .await
331 .unwrap();
332 assert_eq!(opened.plaintext(), b"hello world");
333 }
334
335 #[tokio::test]
336 async fn tampered_tag_yields_tag_mismatch() {
337 let (tk, iv, sid) = session_with_transport_key();
338 let mut sender = EnvelopeSession::new(&tk, sid.clone(), iv).await.unwrap();
339 let mut receiver = EnvelopeSession::new(&TransportKey::new([0xA5; 32]), sid.clone(), iv)
340 .await
341 .unwrap();
342
343 let env = sender.seal("/path", b"payload").await.unwrap();
344 let mut ct = env.payload.clone();
346 let last = ct.len() - 1;
347 ct[last] ^= 0x01;
348 let tampered = Envelope {
349 nonce: env.nonce,
350 counter: env.counter,
351 payload: ct,
352 aad_session_id: env.aad_session_id,
353 aad_path: env.aad_path,
354 _state: PhantomData::<Sealed>,
355 };
356 let err = receiver.open("/path", tampered).await.unwrap_err();
357 assert!(matches!(err, EnvelopeError::TagMismatch));
358 }
359
360 #[tokio::test]
361 async fn aad_path_mismatch_yields_aad_mismatch_or_tag_mismatch() {
362 let (tk, iv, sid) = session_with_transport_key();
363 let mut sender = EnvelopeSession::new(&tk, sid.clone(), iv).await.unwrap();
364 let mut receiver = EnvelopeSession::new(&TransportKey::new([0xA5; 32]), sid.clone(), iv)
365 .await
366 .unwrap();
367
368 let env = sender.seal("/path-a", b"payload").await.unwrap();
369 let err = receiver.open("/path-b", env).await.unwrap_err();
370 assert!(matches!(err, EnvelopeError::AadMismatch));
372 }
373
374 #[tokio::test]
375 async fn counter_rollback_rejected() {
376 let (tk, iv, sid) = session_with_transport_key();
377 let mut sender = EnvelopeSession::new(&tk, sid.clone(), iv).await.unwrap();
378 let mut receiver = EnvelopeSession::new(&TransportKey::new([0xA5; 32]), sid.clone(), iv)
379 .await
380 .unwrap();
381
382 let e1 = sender.seal("/p", b"a").await.unwrap();
384 let e2 = sender.seal("/p", b"b").await.unwrap();
385 let e3 = sender.seal("/p", b"c").await.unwrap();
386 let _ = receiver.open("/p", e1).await.unwrap();
387 let _ = receiver.open("/p", e3).await.unwrap();
388 let err = receiver.open("/p", e2).await.unwrap_err();
390 assert!(matches!(err, EnvelopeError::CounterNotMonotonic { .. }));
391 }
392
393 #[tokio::test]
394 async fn cross_session_key_rejected() {
395 let iv = [0x07; 12];
396 let sid = "sess-cross".to_string();
397 let mut sender = EnvelopeSession::new(&TransportKey::new([0xA5; 32]), sid.clone(), iv)
398 .await
399 .unwrap();
400 let mut receiver = EnvelopeSession::new(&TransportKey::new([0x5A; 32]), sid.clone(), iv)
402 .await
403 .unwrap();
404
405 let env = sender.seal("/p", b"payload").await.unwrap();
406 let err = receiver.open("/p", env).await.unwrap_err();
407 assert!(matches!(err, EnvelopeError::TagMismatch));
408 }
409}