1use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng, Payload};
5use aes_gcm::{Aes256Gcm, Nonce};
6use base64::Engine as _;
7use base64::engine::general_purpose::STANDARD as BASE64;
8use hkdf::Hkdf;
9use hmac::{Hmac, Mac};
10use sha2::Sha256;
11use zeroize::Zeroize;
12
13use crate::error::{Error, Result};
14
15const KEY_SIZE: usize = 32;
17const NONCE_SIZE: usize = 12;
19const MIN_MASTER_KEY: usize = 32;
21
22type HmacSha256 = Hmac<Sha256>;
23
24pub struct Cipher {
28 cipher: Aes256Gcm,
29 hmac_key: [u8; KEY_SIZE],
30}
31
32impl Cipher {
33 pub fn new(master_key: &[u8]) -> Result<Self> {
38 if master_key.len() < MIN_MASTER_KEY {
39 return Err(Error::Other(format!(
40 "crypto: master key must be at least {} bytes, got {}",
41 MIN_MASTER_KEY,
42 master_key.len()
43 )));
44 }
45 let mut enc_key = derive_key(master_key, b"geode-encrypt")?;
46 let hmac_key = derive_key(master_key, b"geode-hmac")?;
47
48 let cipher = Aes256Gcm::new_from_slice(&enc_key)
49 .map_err(|e| Error::Other(format!("crypto: invalid key: {e}")))?;
50 enc_key.zeroize();
51
52 Ok(Self { cipher, hmac_key })
53 }
54
55 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
60 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
61
62 let ct = self
63 .cipher
64 .encrypt(
65 &nonce,
66 Payload {
67 msg: plaintext,
68 aad: &[],
69 },
70 )
71 .map_err(|e| Error::Other(format!("crypto: encrypt: {e}")))?;
72
73 let mut out = Vec::with_capacity(NONCE_SIZE + ct.len());
74 out.extend_from_slice(nonce.as_slice());
75 out.extend_from_slice(&ct);
76 Ok(out)
77 }
78
79 pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
81 if ciphertext.len() < NONCE_SIZE {
82 return Err(Error::Other("crypto: ciphertext too short".into()));
83 }
84 let (nonce_bytes, data) = ciphertext.split_at(NONCE_SIZE);
85 let nonce = Nonce::from_slice(nonce_bytes);
86 self.cipher
87 .decrypt(
88 nonce,
89 Payload {
90 msg: data,
91 aad: &[],
92 },
93 )
94 .map_err(|e| Error::Other(format!("crypto: decrypt: {e}")))
95 }
96
97 pub fn encrypt_string(&self, plaintext: &str) -> Result<String> {
99 let ct = self.encrypt(plaintext.as_bytes())?;
100 Ok(BASE64.encode(ct))
101 }
102
103 pub fn decrypt_string(&self, encoded: &str) -> Result<String> {
105 let ct = BASE64
106 .decode(encoded)
107 .map_err(|e| Error::Other(format!("crypto: base64 decode: {e}")))?;
108 let pt = self.decrypt(&ct)?;
109 String::from_utf8(pt).map_err(|e| Error::Other(format!("crypto: utf8: {e}")))
110 }
111
112 pub fn hmac_hex(&self, data: &str) -> String {
115 let mut mac = <HmacSha256 as Mac>::new_from_slice(&self.hmac_key)
116 .expect("HMAC accepts any key length");
117 mac.update(data.as_bytes());
118 hex::encode(mac.finalize().into_bytes())
119 }
120}
121
122impl Drop for Cipher {
123 fn drop(&mut self) {
124 self.hmac_key.zeroize();
125 }
126}
127
128fn derive_key(master_key: &[u8], info: &[u8]) -> Result<[u8; KEY_SIZE]> {
131 let hk = Hkdf::<Sha256>::new(None, master_key);
132 let mut okm = [0u8; KEY_SIZE];
133 hk.expand(info, &mut okm)
134 .map_err(|e| Error::Other(format!("crypto: hkdf: {e}")))?;
135 Ok(okm)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn key() -> Vec<u8> {
143 vec![b'k'; 32]
144 }
145
146 #[test]
147 fn test_new_rejects_short_key() {
148 assert!(Cipher::new(&[b'k'; 5]).is_err());
149 assert!(Cipher::new(&[]).is_err());
150 assert!(Cipher::new(&[b'k'; 32]).is_ok());
151 }
152
153 #[test]
154 fn test_encrypt_decrypt_roundtrip() {
155 let c = Cipher::new(&key()).unwrap();
156 let pt = vec![0x00, 0x01, 0xFF, 0xFE, 0x80, 0x7F];
157 let ct = c.encrypt(&pt).unwrap();
158 assert_eq!(ct.len(), 12 + pt.len() + 16);
160 assert_eq!(c.decrypt(&ct).unwrap(), pt);
161 }
162
163 #[test]
164 fn test_encrypt_nondeterministic() {
165 let c = Cipher::new(&key()).unwrap();
166 let a = c.encrypt(b"same plaintext").unwrap();
167 let b = c.encrypt(b"same plaintext").unwrap();
168 assert_ne!(a, b, "random nonce must make ciphertexts differ");
169 }
170
171 #[test]
172 fn test_decrypt_tampered_fails() {
173 let c = Cipher::new(&key()).unwrap();
174 let mut ct = c.encrypt(b"secret").unwrap();
175 let last = ct.len() - 1;
176 ct[last] ^= 0xFF;
177 assert!(c.decrypt(&ct).is_err());
178 }
179
180 #[test]
181 fn test_decrypt_too_short() {
182 let c = Cipher::new(&key()).unwrap();
183 assert!(c.decrypt(&[0x01, 0x02]).is_err());
184 }
185
186 #[test]
187 fn test_encrypt_string_roundtrip() {
188 let c = Cipher::new(&key()).unwrap();
189 let enc = c.encrypt_string("hello, geode!").unwrap();
190 assert_eq!(c.decrypt_string(&enc).unwrap(), "hello, geode!");
191 }
192
193 #[test]
194 fn test_decrypt_string_invalid_base64() {
195 let c = Cipher::new(&key()).unwrap();
196 assert!(c.decrypt_string("not-valid-base64!!!").is_err());
197 }
198
199 #[test]
200 fn test_empty_plaintext_roundtrips() {
201 let c = Cipher::new(&key()).unwrap();
202 let ct = c.encrypt(&[]).unwrap();
203 assert_eq!(ct.len(), 12 + 16);
204 assert_eq!(c.decrypt(&ct).unwrap(), Vec::<u8>::new());
205 }
206
207 #[test]
208 fn test_hmac_hex_deterministic_and_distinct() {
209 let c = Cipher::new(&key()).unwrap();
210 let h1 = c.hmac_hex("user@example.com");
211 let h2 = c.hmac_hex("user@example.com");
212 assert_eq!(h1, h2);
213 assert_eq!(h1.len(), 64); assert!(h1.chars().all(|ch| ch.is_ascii_hexdigit()));
215 assert_ne!(c.hmac_hex("alice@x.com"), c.hmac_hex("bob@x.com"));
216 let c2 = Cipher::new(&[b'a'; 32]).unwrap();
218 assert_ne!(
219 c.hmac_hex("user@example.com"),
220 c2.hmac_hex("user@example.com")
221 );
222 }
223
224 #[test]
225 fn test_hmac_hex_matches_known_vector() {
226 let c = Cipher::new(&key()).unwrap();
229 let h = c.hmac_hex("test");
230 assert_eq!(h.len(), 64);
231 }
232}