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)]
162pub struct IvStash {
163 seen: HashMap<(LeafType, Vec<u8>, Vec<u8>), Iv>,
164}
165
166impl IvStash {
167 #[must_use]
168 pub fn new() -> Self {
169 Self::default()
170 }
171
172 fn key(plaintext: &Plaintext, aad: &Aad) -> (LeafType, Vec<u8>, Vec<u8>) {
173 (
174 plaintext.leaf_type(),
175 plaintext.expose().to_vec(),
176 aad.as_bytes().to_vec(),
177 )
178 }
179
180 pub fn remember(&mut self, plaintext: &Plaintext, aad: &Aad, iv: &[u8]) {
182 if let Some(iv) = Iv::from_wire_exact(iv) {
183 self.seen.insert(Self::key(plaintext, aad), iv);
184 }
185 }
186
187 #[must_use]
189 pub fn recall(&self, plaintext: &Plaintext, aad: &Aad) -> Option<Iv> {
190 self.seen.get(&Self::key(plaintext, aad)).cloned()
191 }
192
193 #[must_use]
195 pub fn len(&self) -> usize {
196 self.seen.len()
197 }
198
199 #[must_use]
200 pub fn is_empty(&self) -> bool {
201 self.seen.is_empty()
202 }
203}
204
205impl std::fmt::Debug for IvStash {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 write!(f, "IvStash({} pairs)", self.seen.len())
210 }
211}
212
213pub fn encrypt_leaf(
220 key: &DataKey,
221 plaintext: &Plaintext,
222 aad: &Aad,
223 iv: Option<Iv>,
224) -> Result<Option<EncryptedLeaf>, WireError> {
225 if plaintext.is_empty() {
226 return Ok(None);
227 }
228 let iv = match iv {
229 Some(iv) => iv,
230 None => Iv::generate()?,
231 };
232 let gcm = SopsGcm32::new_from_slice(key.expose()).map_err(|_| WireError::AeadOpen)?;
233 let sealed = gcm
234 .encrypt(
235 aes_gcm::Nonce::<aes_gcm::aead::consts::U32>::from_slice(iv.as_bytes()),
236 Payload {
237 msg: plaintext.expose(),
238 aad: aad.as_bytes(),
239 },
240 )
241 .map_err(|_| WireError::AeadOpen)?;
242 let split = sealed.len().saturating_sub(TAG_LEN);
245 let (data, tag) = sealed.split_at(split);
246 Ok(Some(EncryptedLeaf {
247 data: data.to_vec(),
248 iv: iv.as_bytes().to_vec(),
249 tag: tag.to_vec(),
250 ty: plaintext.leaf_type(),
251 }))
252}
253
254const TAG_LEN: usize = 16;
256
257pub fn decrypt_leaf(
264 key: &DataKey,
265 leaf: &EncryptedLeaf,
266 aad: &Aad,
267 stash: Option<&mut IvStash>,
268) -> Result<Plaintext, WireError> {
269 let mut sealed = Vec::with_capacity(leaf.data.len() + leaf.tag.len());
270 sealed.extend_from_slice(&leaf.data);
271 sealed.extend_from_slice(&leaf.tag);
272
273 let opened = match leaf.iv.len() {
274 Iv::LEN => {
275 let gcm = SopsGcm32::new_from_slice(key.expose()).map_err(|_| WireError::AeadOpen)?;
276 gcm.decrypt(
277 aes_gcm::Nonce::<aes_gcm::aead::consts::U32>::from_slice(&leaf.iv),
278 Payload {
279 msg: &sealed,
280 aad: aad.as_bytes(),
281 },
282 )
283 }
284 12 => {
285 let gcm = aes_gcm::Aes256Gcm::new_from_slice(key.expose())
288 .map_err(|_| WireError::AeadOpen)?;
289 gcm.decrypt(
290 aes_gcm::Nonce::<aes_gcm::aead::consts::U12>::from_slice(&leaf.iv),
291 Payload {
292 msg: &sealed,
293 aad: aad.as_bytes(),
294 },
295 )
296 }
297 _ => return Err(WireError::AeadOpen),
302 }
303 .map_err(|_| WireError::AeadOpen)?;
304
305 let plaintext = Plaintext::from_wire(opened, leaf.ty);
306 if let Some(stash) = stash {
307 stash.remember(&plaintext, aad, &leaf.iv);
308 }
309 Ok(plaintext)
310}
311
312pub(crate) fn decrypt_leaf_as_string(
314 key: &DataKey,
315 leaf: &EncryptedLeaf,
316 aad: &Aad,
317 stash: Option<&mut IvStash>,
318) -> Result<Zeroizing<String>, WireError> {
319 let pt = decrypt_leaf(key, leaf, aad, stash)?;
320 if pt.leaf_type() != LeafType::Str {
321 return Err(WireError::DatatypeMismatch { ty: "str" });
322 }
323 Ok(Zeroizing::new(
324 String::from_utf8_lossy(pt.expose()).into_owned(),
325 ))
326}
327
328fn hex_lower(bytes: &[u8]) -> String {
329 use std::fmt::Write as _;
330 bytes
331 .iter()
332 .fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
333 let _ = write!(s, "{b:02x}");
334 s
335 })
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::aad::AadPath;
342
343 fn key() -> DataKey {
344 DataKey::from_bytes(&[7u8; 32]).expect("32 bytes")
345 }
346
347 fn aad(parts: &[&str]) -> Aad {
348 let mut p = AadPath::root();
349 for c in parts {
350 p.push_key(*c);
351 }
352 p.aad()
353 }
354
355 #[test]
356 fn round_trips_through_the_wire_rendering() {
357 let a = aad(&["db", "password"]);
358 let pt = Plaintext::string("s3kr1t");
359 let leaf = encrypt_leaf(&key(), &pt, &a, None)
360 .expect("encrypt")
361 .expect("non-empty");
362 let rendered = leaf.render();
363 let reparsed = EncryptedLeaf::parse(&rendered).expect("reparse");
364 let back = decrypt_leaf(&key(), &reparsed, &a, None).expect("decrypt");
365 assert_eq!(back.expose(), b"s3kr1t");
366 assert_eq!(back.leaf_type(), LeafType::Str);
367 }
368
369 #[test]
370 fn writes_a_thirty_two_byte_nonce() {
371 let leaf = encrypt_leaf(&key(), &Plaintext::string("x"), &aad(&["k"]), None)
372 .expect("encrypt")
373 .expect("non-empty");
374 assert_eq!(leaf.iv_len(), Iv::LEN);
375 assert_eq!(leaf.tag.len(), TAG_LEN);
376 }
377
378 #[test]
381 fn a_leaf_moved_to_another_path_will_not_open() {
382 let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a", "b"]), None)
383 .expect("encrypt")
384 .expect("non-empty");
385 assert_eq!(
386 decrypt_leaf(&key(), &leaf, &aad(&["a", "c"]), None),
387 Err(WireError::AeadOpen)
388 );
389 }
390
391 #[test]
392 fn a_wrong_data_key_will_not_open() {
393 let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a"]), None)
394 .expect("encrypt")
395 .expect("non-empty");
396 let other = DataKey::from_bytes(&[9u8; 32]).expect("32 bytes");
397 assert_eq!(
398 decrypt_leaf(&other, &leaf, &aad(&["a"]), None),
399 Err(WireError::AeadOpen)
400 );
401 }
402
403 #[test]
404 fn a_flipped_ciphertext_bit_will_not_open() {
405 let mut leaf = encrypt_leaf(&key(), &Plaintext::string("value"), &aad(&["a"]), None)
406 .expect("encrypt")
407 .expect("non-empty");
408 leaf.data[0] ^= 1;
409 assert_eq!(
410 decrypt_leaf(&key(), &leaf, &aad(&["a"]), None),
411 Err(WireError::AeadOpen)
412 );
413 }
414
415 #[test]
416 fn empty_is_a_fixed_point_in_both_directions() {
417 let empty = Plaintext::string("");
418 assert!(
419 encrypt_leaf(&key(), &empty, &aad(&["k"]), None)
420 .expect("encrypt")
421 .is_none()
422 );
423 }
424
425 #[test]
428 fn the_stash_reproduces_previous_bytes_exactly() {
429 let a = aad(&["k"]);
430 let pt = Plaintext::string("unchanged");
431 let first = encrypt_leaf(&key(), &pt, &a, None)
432 .expect("encrypt")
433 .expect("non-empty");
434
435 let mut stash = IvStash::new();
436 let recovered = decrypt_leaf(&key(), &first, &a, Some(&mut stash)).expect("decrypt");
437 assert_eq!(stash.len(), 1);
438
439 let second = encrypt_leaf(&key(), &recovered, &a, stash.recall(&recovered, &a))
440 .expect("re-encrypt")
441 .expect("non-empty");
442 assert_eq!(
443 first.render(),
444 second.render(),
445 "an unchanged value must re-encrypt identically"
446 );
447 }
448
449 #[test]
453 fn the_stash_key_separates_values_that_share_a_byte_string() {
454 let a = aad(&["items"]);
455 let mut stash = IvStash::new();
456 let iv = [42u8; 32];
457
458 stash.remember(&Plaintext::integer(1), &a, &iv);
460 assert_eq!(stash.len(), 1);
461
462 assert!(
464 stash.recall(&Plaintext::string("1"), &a).is_none(),
465 "a str must not recall an int's nonce"
466 );
467 assert!(
469 stash.recall(&Plaintext::float(1.0), &a).is_none(),
470 "a float must not recall an int's nonce"
471 );
472 assert!(stash.recall(&Plaintext::integer(1), &a).is_some());
474
475 stash.remember(&Plaintext::boolean(true), &a, &iv);
477 assert!(
478 stash.recall(&Plaintext::string("True"), &a).is_none(),
479 "a str must not recall a bool's nonce"
480 );
481 assert!(stash.recall(&Plaintext::boolean(true), &a).is_some());
482
483 stash.remember(&Plaintext::string("1"), &a, &iv);
485 stash.remember(&Plaintext::float(1.0), &a, &iv);
486 stash.remember(&Plaintext::string("True"), &a, &iv);
487 assert_eq!(stash.len(), 5, "int, bool, str-1, float-1, str-True");
488 }
489
490 #[test]
493 fn the_stash_key_separates_paths() {
494 let mut stash = IvStash::new();
495 stash.remember(&Plaintext::string("v"), &aad(&["a"]), &[1u8; 32]);
496 assert!(
497 stash
498 .recall(&Plaintext::string("v"), &aad(&["b"]))
499 .is_none()
500 );
501 assert!(
502 stash
503 .recall(&Plaintext::string("v"), &aad(&["a"]))
504 .is_some()
505 );
506 }
507
508 #[test]
509 fn without_the_stash_the_bytes_change() {
510 let a = aad(&["k"]);
511 let pt = Plaintext::string("unchanged");
512 let first = encrypt_leaf(&key(), &pt, &a, None)
513 .expect("e")
514 .expect("non-empty");
515 let second = encrypt_leaf(&key(), &pt, &a, None)
516 .expect("e")
517 .expect("non-empty");
518 assert_ne!(first.render(), second.render(), "fresh nonces must differ");
519 }
520
521 #[test]
522 fn a_short_data_key_is_named_not_swallowed() {
523 let err = DataKey::from_bytes(&[0u8; 16])
524 .err()
525 .expect("16 bytes must be refused");
526 assert_eq!(err, WireError::DataKeyLength(16));
527 }
528
529 #[test]
530 fn debug_never_shows_key_or_plaintext() {
531 assert_eq!(format!("{:?}", key()), "DataKey(*** 32 bytes)");
532 let mut stash = IvStash::new();
533 stash.remember(&Plaintext::string("hunter2"), &aad(&["k"]), &[0u8; 32]);
534 let shown = format!("{stash:?}");
535 assert!(
536 !shown.contains("hunter2"),
537 "IvStash Debug leaked a plaintext: {shown}"
538 );
539 assert_eq!(shown, "IvStash(1 pairs)");
540 }
541}