1use crate::WireError;
26use crate::aad::Aad;
27use crate::leaf::{EncryptedLeaf, LeafType, Plaintext};
28use aes_gcm::AesGcm;
29use aes_gcm::aead::{Aead, KeyInit, Payload};
30use std::collections::HashMap;
31use zeroize::Zeroizing;
32
33type SopsGcm32 = AesGcm<aes::Aes256, aes_gcm::aead::consts::U32>;
35
36#[derive(Clone)]
42pub struct DataKey(Zeroizing<[u8; 32]>);
43
44impl DataKey {
45 pub const LEN: usize = 32;
48
49 pub fn generate() -> Result<Self, WireError> {
51 let mut k = [0u8; Self::LEN];
52 getrandom::getrandom(&mut k).map_err(|e| WireError::Randomness(e.to_string()))?;
53 Ok(Self(Zeroizing::new(k)))
54 }
55
56 pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {
62 if bytes.len() != Self::LEN {
63 return Err(WireError::DataKeyLength(bytes.len()));
64 }
65 let mut k = [0u8; Self::LEN];
66 k.copy_from_slice(bytes);
67 Ok(Self(Zeroizing::new(k)))
68 }
69
70 #[must_use]
74 pub fn expose(&self) -> &[u8; 32] {
75 &self.0
76 }
77}
78
79impl std::fmt::Debug for DataKey {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.write_str("DataKey(*** 32 bytes)")
82 }
83}
84
85#[derive(Clone, PartialEq, Eq, Hash)]
90pub struct Iv([u8; 32]);
91
92impl Iv {
93 pub const LEN: usize = 32;
95
96 pub fn generate() -> Result<Self, WireError> {
98 let mut iv = [0u8; Self::LEN];
99 getrandom::getrandom(&mut iv).map_err(|e| WireError::Randomness(e.to_string()))?;
100 Ok(Self(iv))
101 }
102
103 #[must_use]
107 pub fn from_wire_exact(bytes: &[u8]) -> Option<Self> {
108 let arr: [u8; Self::LEN] = bytes.try_into().ok()?;
109 Some(Self(arr))
110 }
111
112 #[must_use]
113 pub fn as_bytes(&self) -> &[u8; 32] {
114 &self.0
115 }
116}
117
118impl std::fmt::Debug for Iv {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 write!(f, "Iv({})", hex_lower(&self.0))
123 }
124}
125
126#[derive(Default)]
142pub struct IvStash {
143 seen: HashMap<(Vec<u8>, Vec<u8>), Iv>,
144}
145
146impl IvStash {
147 #[must_use]
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn remember(&mut self, plaintext: &Plaintext, aad: &Aad, iv: &[u8]) {
154 if let Some(iv) = Iv::from_wire_exact(iv) {
155 self.seen
156 .insert((plaintext.expose().to_vec(), aad.as_bytes().to_vec()), iv);
157 }
158 }
159
160 #[must_use]
162 pub fn recall(&self, plaintext: &Plaintext, aad: &Aad) -> Option<Iv> {
163 self.seen
164 .get(&(plaintext.expose().to_vec(), aad.as_bytes().to_vec()))
165 .cloned()
166 }
167
168 #[must_use]
170 pub fn len(&self) -> usize {
171 self.seen.len()
172 }
173
174 #[must_use]
175 pub fn is_empty(&self) -> bool {
176 self.seen.is_empty()
177 }
178}
179
180impl std::fmt::Debug for IvStash {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(f, "IvStash({} pairs)", self.seen.len())
185 }
186}
187
188pub fn encrypt_leaf(
195 key: &DataKey,
196 plaintext: &Plaintext,
197 aad: &Aad,
198 iv: Option<Iv>,
199) -> Result<Option<EncryptedLeaf>, WireError> {
200 if plaintext.is_empty() {
201 return Ok(None);
202 }
203 let iv = match iv {
204 Some(iv) => iv,
205 None => Iv::generate()?,
206 };
207 let gcm = SopsGcm32::new_from_slice(key.expose()).map_err(|_| WireError::AeadOpen)?;
208 let sealed = gcm
209 .encrypt(
210 aes_gcm::Nonce::<aes_gcm::aead::consts::U32>::from_slice(iv.as_bytes()),
211 Payload {
212 msg: plaintext.expose(),
213 aad: aad.as_bytes(),
214 },
215 )
216 .map_err(|_| WireError::AeadOpen)?;
217 let split = sealed.len().saturating_sub(TAG_LEN);
220 let (data, tag) = sealed.split_at(split);
221 Ok(Some(EncryptedLeaf {
222 data: data.to_vec(),
223 iv: iv.as_bytes().to_vec(),
224 tag: tag.to_vec(),
225 ty: plaintext.leaf_type(),
226 }))
227}
228
229const TAG_LEN: usize = 16;
231
232pub fn decrypt_leaf(
239 key: &DataKey,
240 leaf: &EncryptedLeaf,
241 aad: &Aad,
242 stash: Option<&mut IvStash>,
243) -> Result<Plaintext, WireError> {
244 let mut sealed = Vec::with_capacity(leaf.data.len() + leaf.tag.len());
245 sealed.extend_from_slice(&leaf.data);
246 sealed.extend_from_slice(&leaf.tag);
247
248 let opened = match leaf.iv.len() {
249 Iv::LEN => {
250 let gcm = SopsGcm32::new_from_slice(key.expose()).map_err(|_| WireError::AeadOpen)?;
251 gcm.decrypt(
252 aes_gcm::Nonce::<aes_gcm::aead::consts::U32>::from_slice(&leaf.iv),
253 Payload {
254 msg: &sealed,
255 aad: aad.as_bytes(),
256 },
257 )
258 }
259 12 => {
260 let gcm = aes_gcm::Aes256Gcm::new_from_slice(key.expose())
263 .map_err(|_| WireError::AeadOpen)?;
264 gcm.decrypt(
265 aes_gcm::Nonce::<aes_gcm::aead::consts::U12>::from_slice(&leaf.iv),
266 Payload {
267 msg: &sealed,
268 aad: aad.as_bytes(),
269 },
270 )
271 }
272 _ => return Err(WireError::AeadOpen),
277 }
278 .map_err(|_| WireError::AeadOpen)?;
279
280 let plaintext = Plaintext::from_wire(opened, leaf.ty);
281 if let Some(stash) = stash {
282 stash.remember(&plaintext, aad, &leaf.iv);
283 }
284 Ok(plaintext)
285}
286
287pub(crate) fn decrypt_leaf_as_string(
289 key: &DataKey,
290 leaf: &EncryptedLeaf,
291 aad: &Aad,
292 stash: Option<&mut IvStash>,
293) -> Result<Zeroizing<String>, WireError> {
294 let pt = decrypt_leaf(key, leaf, aad, stash)?;
295 if pt.leaf_type() != LeafType::Str {
296 return Err(WireError::DatatypeMismatch { ty: "str" });
297 }
298 Ok(Zeroizing::new(
299 String::from_utf8_lossy(pt.expose()).into_owned(),
300 ))
301}
302
303fn hex_lower(bytes: &[u8]) -> String {
304 use std::fmt::Write as _;
305 bytes
306 .iter()
307 .fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
308 let _ = write!(s, "{b:02x}");
309 s
310 })
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::aad::AadPath;
317
318 fn key() -> DataKey {
319 DataKey::from_bytes(&[7u8; 32]).expect("32 bytes")
320 }
321
322 fn aad(parts: &[&str]) -> Aad {
323 let mut p = AadPath::root();
324 for c in parts {
325 p.push_key(*c);
326 }
327 p.aad()
328 }
329
330 #[test]
331 fn round_trips_through_the_wire_rendering() {
332 let a = aad(&["db", "password"]);
333 let pt = Plaintext::string("s3kr1t");
334 let leaf = encrypt_leaf(&key(), &pt, &a, None)
335 .expect("encrypt")
336 .expect("non-empty");
337 let rendered = leaf.render();
338 let reparsed = EncryptedLeaf::parse(&rendered).expect("reparse");
339 let back = decrypt_leaf(&key(), &reparsed, &a, None).expect("decrypt");
340 assert_eq!(back.expose(), b"s3kr1t");
341 assert_eq!(back.leaf_type(), LeafType::Str);
342 }
343
344 #[test]
345 fn writes_a_thirty_two_byte_nonce() {
346 let leaf = encrypt_leaf(&key(), &Plaintext::string("x"), &aad(&["k"]), None)
347 .expect("encrypt")
348 .expect("non-empty");
349 assert_eq!(leaf.iv_len(), Iv::LEN);
350 assert_eq!(leaf.tag.len(), TAG_LEN);
351 }
352
353 #[test]
356 fn a_leaf_moved_to_another_path_will_not_open() {
357 let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a", "b"]), None)
358 .expect("encrypt")
359 .expect("non-empty");
360 assert_eq!(
361 decrypt_leaf(&key(), &leaf, &aad(&["a", "c"]), None),
362 Err(WireError::AeadOpen)
363 );
364 }
365
366 #[test]
367 fn a_wrong_data_key_will_not_open() {
368 let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a"]), None)
369 .expect("encrypt")
370 .expect("non-empty");
371 let other = DataKey::from_bytes(&[9u8; 32]).expect("32 bytes");
372 assert_eq!(
373 decrypt_leaf(&other, &leaf, &aad(&["a"]), None),
374 Err(WireError::AeadOpen)
375 );
376 }
377
378 #[test]
379 fn a_flipped_ciphertext_bit_will_not_open() {
380 let mut leaf = encrypt_leaf(&key(), &Plaintext::string("value"), &aad(&["a"]), None)
381 .expect("encrypt")
382 .expect("non-empty");
383 leaf.data[0] ^= 1;
384 assert_eq!(
385 decrypt_leaf(&key(), &leaf, &aad(&["a"]), None),
386 Err(WireError::AeadOpen)
387 );
388 }
389
390 #[test]
391 fn empty_is_a_fixed_point_in_both_directions() {
392 let empty = Plaintext::string("");
393 assert!(
394 encrypt_leaf(&key(), &empty, &aad(&["k"]), None)
395 .expect("encrypt")
396 .is_none()
397 );
398 }
399
400 #[test]
403 fn the_stash_reproduces_previous_bytes_exactly() {
404 let a = aad(&["k"]);
405 let pt = Plaintext::string("unchanged");
406 let first = encrypt_leaf(&key(), &pt, &a, None)
407 .expect("encrypt")
408 .expect("non-empty");
409
410 let mut stash = IvStash::new();
411 let recovered = decrypt_leaf(&key(), &first, &a, Some(&mut stash)).expect("decrypt");
412 assert_eq!(stash.len(), 1);
413
414 let second = encrypt_leaf(&key(), &recovered, &a, stash.recall(&recovered, &a))
415 .expect("re-encrypt")
416 .expect("non-empty");
417 assert_eq!(
418 first.render(),
419 second.render(),
420 "an unchanged value must re-encrypt identically"
421 );
422 }
423
424 #[test]
425 fn without_the_stash_the_bytes_change() {
426 let a = aad(&["k"]);
427 let pt = Plaintext::string("unchanged");
428 let first = encrypt_leaf(&key(), &pt, &a, None)
429 .expect("e")
430 .expect("non-empty");
431 let second = encrypt_leaf(&key(), &pt, &a, None)
432 .expect("e")
433 .expect("non-empty");
434 assert_ne!(first.render(), second.render(), "fresh nonces must differ");
435 }
436
437 #[test]
438 fn a_short_data_key_is_named_not_swallowed() {
439 let err = DataKey::from_bytes(&[0u8; 16])
440 .err()
441 .expect("16 bytes must be refused");
442 assert_eq!(err, WireError::DataKeyLength(16));
443 }
444
445 #[test]
446 fn debug_never_shows_key_or_plaintext() {
447 assert_eq!(format!("{:?}", key()), "DataKey(*** 32 bytes)");
448 let mut stash = IvStash::new();
449 stash.remember(&Plaintext::string("hunter2"), &aad(&["k"]), &[0u8; 32]);
450 let shown = format!("{stash:?}");
451 assert!(
452 !shown.contains("hunter2"),
453 "IvStash Debug leaked a plaintext: {shown}"
454 );
455 assert_eq!(shown, "IvStash(1 pairs)");
456 }
457}