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) -> Result<Zeroizing<String>, WireError> {
293 let pt = decrypt_leaf(key, leaf, aad, None)?;
294 if pt.leaf_type() != LeafType::Str {
295 return Err(WireError::DatatypeMismatch { ty: "str" });
296 }
297 Ok(Zeroizing::new(
298 String::from_utf8_lossy(pt.expose()).into_owned(),
299 ))
300}
301
302fn hex_lower(bytes: &[u8]) -> String {
303 use std::fmt::Write as _;
304 bytes
305 .iter()
306 .fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
307 let _ = write!(s, "{b:02x}");
308 s
309 })
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::aad::AadPath;
316
317 fn key() -> DataKey {
318 DataKey::from_bytes(&[7u8; 32]).expect("32 bytes")
319 }
320
321 fn aad(parts: &[&str]) -> Aad {
322 let mut p = AadPath::root();
323 for c in parts {
324 p.push_key(*c);
325 }
326 p.aad()
327 }
328
329 #[test]
330 fn round_trips_through_the_wire_rendering() {
331 let a = aad(&["db", "password"]);
332 let pt = Plaintext::string("s3kr1t");
333 let leaf = encrypt_leaf(&key(), &pt, &a, None)
334 .expect("encrypt")
335 .expect("non-empty");
336 let rendered = leaf.render();
337 let reparsed = EncryptedLeaf::parse(&rendered).expect("reparse");
338 let back = decrypt_leaf(&key(), &reparsed, &a, None).expect("decrypt");
339 assert_eq!(back.expose(), b"s3kr1t");
340 assert_eq!(back.leaf_type(), LeafType::Str);
341 }
342
343 #[test]
344 fn writes_a_thirty_two_byte_nonce() {
345 let leaf = encrypt_leaf(&key(), &Plaintext::string("x"), &aad(&["k"]), None)
346 .expect("encrypt")
347 .expect("non-empty");
348 assert_eq!(leaf.iv_len(), Iv::LEN);
349 assert_eq!(leaf.tag.len(), TAG_LEN);
350 }
351
352 #[test]
355 fn a_leaf_moved_to_another_path_will_not_open() {
356 let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a", "b"]), None)
357 .expect("encrypt")
358 .expect("non-empty");
359 assert_eq!(
360 decrypt_leaf(&key(), &leaf, &aad(&["a", "c"]), None),
361 Err(WireError::AeadOpen)
362 );
363 }
364
365 #[test]
366 fn a_wrong_data_key_will_not_open() {
367 let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a"]), None)
368 .expect("encrypt")
369 .expect("non-empty");
370 let other = DataKey::from_bytes(&[9u8; 32]).expect("32 bytes");
371 assert_eq!(
372 decrypt_leaf(&other, &leaf, &aad(&["a"]), None),
373 Err(WireError::AeadOpen)
374 );
375 }
376
377 #[test]
378 fn a_flipped_ciphertext_bit_will_not_open() {
379 let mut leaf = encrypt_leaf(&key(), &Plaintext::string("value"), &aad(&["a"]), None)
380 .expect("encrypt")
381 .expect("non-empty");
382 leaf.data[0] ^= 1;
383 assert_eq!(
384 decrypt_leaf(&key(), &leaf, &aad(&["a"]), None),
385 Err(WireError::AeadOpen)
386 );
387 }
388
389 #[test]
390 fn empty_is_a_fixed_point_in_both_directions() {
391 let empty = Plaintext::string("");
392 assert!(
393 encrypt_leaf(&key(), &empty, &aad(&["k"]), None)
394 .expect("encrypt")
395 .is_none()
396 );
397 }
398
399 #[test]
402 fn the_stash_reproduces_previous_bytes_exactly() {
403 let a = aad(&["k"]);
404 let pt = Plaintext::string("unchanged");
405 let first = encrypt_leaf(&key(), &pt, &a, None)
406 .expect("encrypt")
407 .expect("non-empty");
408
409 let mut stash = IvStash::new();
410 let recovered = decrypt_leaf(&key(), &first, &a, Some(&mut stash)).expect("decrypt");
411 assert_eq!(stash.len(), 1);
412
413 let second = encrypt_leaf(&key(), &recovered, &a, stash.recall(&recovered, &a))
414 .expect("re-encrypt")
415 .expect("non-empty");
416 assert_eq!(
417 first.render(),
418 second.render(),
419 "an unchanged value must re-encrypt identically"
420 );
421 }
422
423 #[test]
424 fn without_the_stash_the_bytes_change() {
425 let a = aad(&["k"]);
426 let pt = Plaintext::string("unchanged");
427 let first = encrypt_leaf(&key(), &pt, &a, None)
428 .expect("e")
429 .expect("non-empty");
430 let second = encrypt_leaf(&key(), &pt, &a, None)
431 .expect("e")
432 .expect("non-empty");
433 assert_ne!(first.render(), second.render(), "fresh nonces must differ");
434 }
435
436 #[test]
437 fn a_short_data_key_is_named_not_swallowed() {
438 let err = DataKey::from_bytes(&[0u8; 16])
439 .err()
440 .expect("16 bytes must be refused");
441 assert_eq!(err, WireError::DataKeyLength(16));
442 }
443
444 #[test]
445 fn debug_never_shows_key_or_plaintext() {
446 assert_eq!(format!("{:?}", key()), "DataKey(*** 32 bytes)");
447 let mut stash = IvStash::new();
448 stash.remember(&Plaintext::string("hunter2"), &aad(&["k"]), &[0u8; 32]);
449 let shown = format!("{stash:?}");
450 assert!(
451 !shown.contains("hunter2"),
452 "IvStash Debug leaked a plaintext: {shown}"
453 );
454 assert_eq!(shown, "IvStash(1 pairs)");
455 }
456}