a3s_code_core/store/
encryption.rs1use aes_gcm::aead::{Aead, KeyInit, OsRng};
8use aes_gcm::{AeadCore, Aes256Gcm, Nonce};
9use anyhow::{bail, Context, Result};
10
11const ENVELOPE_MAGIC: &[u8; 4] = b"A3SE";
12const ENVELOPE_VERSION: u8 = 1;
13const NONCE_LEN: usize = 12;
14const HEADER_LEN: usize = 4 + 1 + NONCE_LEN;
15
16#[derive(Clone)]
19pub struct SessionStoreAtRestCipher {
20 cipher: Aes256Gcm,
21}
22
23impl SessionStoreAtRestCipher {
24 pub fn new(key: &[u8; 32]) -> Result<Self> {
25 let cipher = Aes256Gcm::new_from_slice(key)
26 .context("session store at-rest cipher key must be 32 bytes")?;
27 Ok(Self { cipher })
28 }
29
30 pub fn is_sealed(bytes: &[u8]) -> bool {
31 bytes.len() > HEADER_LEN
32 && bytes.starts_with(ENVELOPE_MAGIC)
33 && bytes[4] == ENVELOPE_VERSION
34 }
35
36 pub fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
37 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
38 let ciphertext = self
39 .cipher
40 .encrypt(&nonce, plaintext)
41 .map_err(|_| anyhow::anyhow!("session store at-rest encryption failed"))?;
42 let mut out = Vec::with_capacity(HEADER_LEN + ciphertext.len());
43 out.extend_from_slice(ENVELOPE_MAGIC);
44 out.push(ENVELOPE_VERSION);
45 out.extend_from_slice(nonce.as_slice());
46 out.extend_from_slice(&ciphertext);
47 Ok(out)
48 }
49
50 pub fn open(&self, sealed: &[u8]) -> Result<Vec<u8>> {
51 if !Self::is_sealed(sealed) {
52 bail!("session store document is not an at-rest sealed envelope");
53 }
54 let nonce = Nonce::from_slice(&sealed[5..5 + NONCE_LEN]);
55 self.cipher
56 .decrypt(nonce, &sealed[HEADER_LEN..])
57 .map_err(|_| anyhow::anyhow!("session store at-rest decryption failed"))
58 }
59
60 pub fn open_or_plaintext(&self, bytes: &[u8]) -> Result<Vec<u8>> {
63 if Self::is_sealed(bytes) {
64 self.open(bytes)
65 } else {
66 Ok(bytes.to_vec())
67 }
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn seal_round_trip_and_wrong_key_fail_closed() {
77 let a = SessionStoreAtRestCipher::new(&[7u8; 32]).unwrap();
78 let b = SessionStoreAtRestCipher::new(&[8u8; 32]).unwrap();
79 let sealed = a.seal(b"{\"ok\":true}").unwrap();
80 assert!(SessionStoreAtRestCipher::is_sealed(&sealed));
81 assert_ne!(&sealed[..], b"{\"ok\":true}");
82 assert_eq!(a.open(&sealed).unwrap(), b"{\"ok\":true}");
83 assert!(b.open(&sealed).is_err());
84 assert_eq!(a.open_or_plaintext(b"plain").unwrap(), b"plain");
85 }
86}