1use aes_gcm::{
21 aead::{rand_core::RngCore, AeadInPlace, KeyInit, OsRng},
22 Aes256Gcm, Nonce,
23};
24use argon2::{Algorithm, Argon2, Params, Version};
25use hkdf::Hkdf;
26use sha2::Sha256;
27use subtle::ConstantTimeEq;
28use zeroize::{ZeroizeOnDrop, Zeroizing};
29
30use crate::error::{Error, Result};
31use crate::kdf::KdfParams;
32
33pub const KEY_LEN: usize = 32;
35pub const NONCE_LEN: usize = 12;
37pub const TAG_LEN: usize = 16;
39
40pub const SALT_LEN: usize = 32;
42
43#[derive(Clone, ZeroizeOnDrop)]
45pub struct Key(Zeroizing<[u8; KEY_LEN]>);
46
47impl Key {
48 pub fn generate() -> Self {
50 Self(Zeroizing::new(random_key_bytes()))
51 }
52
53 pub fn from_bytes(bytes: [u8; KEY_LEN]) -> Self {
56 Self(Zeroizing::new(bytes))
57 }
58
59 pub fn expose(&self) -> &[u8; KEY_LEN] {
61 &self.0
62 }
63
64 pub(crate) fn cipher(&self) -> Result<Aes256Gcm> {
65 Aes256Gcm::new_from_slice(self.0.as_slice()).map_err(|_| Error::Encryption)
66 }
67}
68
69impl core::fmt::Debug for Key {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 f.write_str("Key([REDACTED])")
72 }
73}
74
75pub type Nonce12 = [u8; NONCE_LEN];
77
78pub fn fill_random(buf: &mut [u8]) {
80 OsRng.fill_bytes(buf);
81}
82
83pub(crate) fn random_key_bytes() -> [u8; KEY_LEN] {
84 let mut k = [0u8; KEY_LEN];
85 fill_random(&mut k);
86 k
87}
88
89pub fn random_nonce() -> Nonce12 {
91 let mut n = [0u8; NONCE_LEN];
92 fill_random(&mut n);
93 n
94}
95
96pub fn seal(plaintext: &[u8], key: &Key, aad: &[u8]) -> Result<(Nonce12, Vec<u8>)> {
101 let nonce = random_nonce();
102 let mut buf = plaintext.to_vec();
103 seal_in_place(&nonce, &mut buf, key, aad)?;
104 Ok((nonce, buf))
105}
106
107pub(crate) fn seal_in_place(
109 nonce: &Nonce12,
110 buf: &mut Vec<u8>,
111 key: &Key,
112 aad: &[u8],
113) -> Result<()> {
114 let cipher = key.cipher()?;
115 cipher
116 .encrypt_in_place(Nonce::from_slice(nonce), aad, buf)
117 .map_err(|_| Error::Encryption)
118}
119
120pub fn open(
126 ciphertext_with_tag: &[u8],
127 nonce: &Nonce12,
128 key: &Key,
129 aad: &[u8],
130) -> Result<Zeroizing<Vec<u8>>> {
131 let mut buf = Zeroizing::new(ciphertext_with_tag.to_vec());
132 open_in_place(nonce, &mut buf, key, aad)?;
133 Ok(buf)
134}
135
136pub(crate) fn open_in_place(
138 nonce: &Nonce12,
139 buf: &mut Vec<u8>,
140 key: &Key,
141 aad: &[u8],
142) -> Result<()> {
143 let cipher = key.cipher()?;
144 cipher
145 .decrypt_in_place(Nonce::from_slice(nonce), aad, buf)
146 .map_err(|_| Error::Authentication)
147}
148
149struct Argon2Instance(Argon2<'static>);
150
151fn make_argon2(params: KdfParams) -> Result<Argon2Instance> {
152 let p = Params::new(
153 params.m_cost_kib,
154 params.t_cost,
155 params.p_cost,
156 Some(KEY_LEN),
157 )
158 .map_err(|_| Error::InvalidKdfParams)?;
159 Ok(Argon2Instance(Argon2::new(
160 Algorithm::Argon2id,
161 Version::V0x13,
162 p,
163 )))
164}
165
166pub fn derive_key(password: &[u8], salt: &[u8], params: KdfParams) -> Result<Key> {
170 params.validate()?;
171 if salt.len() < 8 {
172 return Err(Error::InvalidHeader);
173 }
174
175 let argon2 = make_argon2(params)?;
176
177 let mut out = Zeroizing::new([0u8; KEY_LEN]);
178 argon2
179 .0
180 .hash_password_into(password, salt, out.as_mut())
181 .map_err(|_| Error::KeyDerivation)?;
182 Ok(Key(out))
183}
184
185pub(crate) fn derive_subkey(master: &Key, salt: &[u8], info: &[u8]) -> Key {
192 let hk = Hkdf::<Sha256>::new(Some(salt), master.expose());
193 let mut okm = Zeroizing::new([0u8; KEY_LEN]);
194 hk.expand(info, okm.as_mut())
195 .expect("32-byte OKM is valid for SHA-256");
196 Key(okm)
197}
198
199pub fn secure_compare(a: &[u8], b: &[u8]) -> bool {
205 if a.len() != b.len() {
206 return false;
207 }
208 bool::from(a.ct_eq(b))
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn seal_open_roundtrip() {
217 let key = Key::generate();
218 let (nonce, ct) = seal(b"attack at dawn", &key, b"context").unwrap();
219 let pt = open(&ct, &nonce, &key, b"context").unwrap();
220 assert_eq!(&pt[..], b"attack at dawn");
221 }
222
223 #[test]
224 fn aad_is_binding() {
225 let key = Key::generate();
226 let (nonce, ct) = seal(b"secret", &key, b"aad-1").unwrap();
227 assert!(open(&ct, &nonce, &key, b"aad-2").is_err());
228 }
229
230 #[test]
231 fn wrong_key_fails() {
232 let (nonce, ct) = seal(b"secret", &Key::generate(), b"").unwrap();
233 assert!(open(&ct, &nonce, &Key::generate(), b"").is_err());
234 }
235
236 #[test]
237 fn tampered_ciphertext_fails() {
238 let key = Key::generate();
239 let (nonce, mut ct) = seal(b"secret", &key, b"").unwrap();
240 ct[0] ^= 1;
241 assert!(open(&ct, &nonce, &key, b"").is_err());
242 }
243
244 #[test]
245 fn derive_key_matches_params_and_salt() {
246 let params = KdfParams {
247 m_cost_kib: 8 * 1024,
248 t_cost: 1,
249 p_cost: 1,
250 };
251 let k1 = derive_key(b"pw", b"0123456789abcdef", params).unwrap();
252 let k2 = derive_key(b"pw", b"0123456789abcdef", params).unwrap();
253 let k3 = derive_key(b"pw", b"fedcba9876543210", params).unwrap();
254 assert_eq!(k1.expose(), k2.expose());
255 assert_ne!(k1.expose(), k3.expose());
256 }
257
258 #[test]
259 fn debug_redacts_keys() {
260 let key = Key::generate();
261 let rendered = format!("{key:?}");
262 assert!(
263 !rendered.contains("Key(") && !rendered.ends_with(')') || rendered.contains("REDACTED")
264 );
265 }
266
267 #[test]
268 fn secure_compare_basics() {
269 assert!(secure_compare(b"abc", b"abc"));
270 assert!(!secure_compare(b"abc", b"abd"));
271 assert!(!secure_compare(b"abc", b"abcd"));
272 }
273
274 #[test]
275 fn subkeys_are_distinct_per_salt() {
276 let master = Key::generate();
277 let a = derive_subkey(&master, b"salt-a", b"info");
278 let b = derive_subkey(&master, b"salt-b", b"info");
279 assert_ne!(a.expose(), b.expose());
280 }
281}