1use std::collections::BTreeMap;
2use std::fmt;
3use std::str::FromStr;
4
5use crate::keys::KeyError;
6use chacha20poly1305::aead::generic_array::GenericArray;
7use chacha20poly1305::aead::{Aead, Payload};
8use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
9use hkdf::Hkdf;
10use rand::RngCore;
11use sha2::{Digest, Sha256};
12use thiserror::Error;
13use tracing::info;
14
15pub const NONCE_SIZE: usize = 24;
17
18pub const TAG_SIZE: usize = 16;
20
21pub const CHUNK_SIZE: usize = 65536;
23pub const ENCRYPTED_CHUNK_SIZE: usize = CHUNK_SIZE + TAG_SIZE;
25pub const INITIAL_KEY_GENERATION: u64 = 1;
26const AEAD_V2_LABEL: &[u8] = b"coven-aead-v2";
27
28pub const APP_DATA_SEAL_VERSION: u8 = 1;
33
34const APP_DATA_FINGERPRINT_SIZE: usize = 32;
37const APP_DATA_HEADER_SIZE: usize = 1 + APP_DATA_FINGERPRINT_SIZE;
38
39#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct KeyFingerprint([u8; 32]);
43
44impl KeyFingerprint {
45 pub fn from_bytes(bytes: [u8; 32]) -> Self {
46 Self(bytes)
47 }
48
49 pub fn as_bytes(&self) -> &[u8; 32] {
50 &self.0
51 }
52}
53
54impl fmt::Debug for KeyFingerprint {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 fmt::Display::fmt(self, formatter)
57 }
58}
59
60impl fmt::Display for KeyFingerprint {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter.write_str(&hex::encode(self.0))
63 }
64}
65
66impl FromStr for KeyFingerprint {
67 type Err = KeyFingerprintParseError;
68
69 fn from_str(value: &str) -> Result<Self, Self::Err> {
70 if value.len() != 64
71 || value
72 .bytes()
73 .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
74 {
75 return Err(KeyFingerprintParseError(value.to_string()));
76 }
77 let bytes: [u8; 32] = hex::decode(value)
78 .map_err(|_| KeyFingerprintParseError(value.to_string()))?
79 .try_into()
80 .map_err(|_| KeyFingerprintParseError(value.to_string()))?;
81 Ok(Self(bytes))
82 }
83}
84
85impl serde::Serialize for KeyFingerprint {
86 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87 where
88 S: serde::Serializer,
89 {
90 serializer.serialize_str(&self.to_string())
91 }
92}
93
94impl<'de> serde::Deserialize<'de> for KeyFingerprint {
95 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96 where
97 D: serde::Deserializer<'de>,
98 {
99 <String as serde::Deserialize>::deserialize(deserializer)?
100 .parse()
101 .map_err(serde::de::Error::custom)
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
106#[error("key fingerprint must be exactly 64 lowercase hexadecimal characters: {0:?}")]
107pub struct KeyFingerprintParseError(String);
108
109pub fn generate_random_key() -> [u8; 32] {
111 let mut key = [0u8; 32];
112 rand::rng().fill_bytes(&mut key);
113 key
114}
115
116pub fn chunked_encrypted_len(plaintext_len: u64) -> u64 {
122 NONCE_SIZE as u64
123 + plaintext_len
124 + chunk_count_for_plaintext_len(plaintext_len) * TAG_SIZE as u64
125}
126
127pub struct ChunkSealer {
133 cipher: XChaCha20Poly1305,
134 base_nonce: [u8; NONCE_SIZE],
135 aad_context: Vec<u8>,
136 total_chunks: u64,
137 next_index: u64,
138}
139
140impl ChunkSealer {
141 fn new(key: &[u8; 32], plaintext_len: u64, aad_context: &[u8]) -> Self {
143 let mut base_nonce = [0u8; NONCE_SIZE];
144 rand::rng().fill_bytes(&mut base_nonce);
145 Self {
146 cipher: XChaCha20Poly1305::new(GenericArray::from_slice(key)),
147 base_nonce,
148 aad_context: aad_context.to_vec(),
149 total_chunks: chunk_count_for_plaintext_len(plaintext_len),
150 next_index: 0,
151 }
152 }
153
154 pub fn base_nonce(&self) -> [u8; NONCE_SIZE] {
157 self.base_nonce
158 }
159
160 pub fn seal_chunk(&mut self, plaintext: &[u8]) -> Vec<u8> {
165 debug_assert!(
166 plaintext.len() <= CHUNK_SIZE,
167 "a sealed chunk must be at most CHUNK_SIZE bytes"
168 );
169 let nonce = chunk_nonce(&self.base_nonce, self.next_index);
170 let aad = chunk_aad(&self.aad_context, self.next_index, self.total_chunks);
171 self.next_index += 1;
172 self.cipher
173 .encrypt(
174 GenericArray::from_slice(&nonce),
175 Payload {
176 msg: plaintext,
177 aad: &aad,
178 },
179 )
180 .expect("encryption should not fail")
181 }
182}
183
184#[derive(Error, Debug)]
185pub enum EncryptionError {
186 #[error("Encryption failed: {0}")]
187 Encryption(String),
188 #[error("Decryption failed: {0}")]
189 Decryption(String),
190 #[error("Key management error: {0}")]
191 KeyManagement(String),
192 #[error("IO error: {0}")]
193 Io(#[from] std::io::Error),
194}
195
196#[derive(Debug, Error)]
203pub enum SealError {
204 #[error("no master key is established for this store (locked, or never initialized)")]
209 Locked,
210 #[error("custody error: {0}")]
214 Custody(#[from] KeyError),
215 #[error("unsupported sealed app-data version {0}")]
217 UnknownVersion(u8),
218 #[error("sealed app-data names key {0}, which this keyring does not hold")]
221 UnknownKey(String),
222 #[error("app-data cryptography failed: {0}")]
225 Crypto(#[from] EncryptionError),
226}
227
228#[derive(serde::Serialize, serde::Deserialize)]
229struct StoredKeyring {
230 keys: Vec<StoredKeyringGeneration>,
231}
232
233#[derive(serde::Serialize, serde::Deserialize)]
234struct StoredKeyringGeneration {
235 generation: u64,
236 key_hex: String,
237}
238
239#[derive(Clone, PartialEq, Eq)]
243struct KeyEntry {
244 generation: u64,
245 key: [u8; 32],
246}
247
248#[derive(Clone)]
253pub struct MasterKeyring {
254 keys: BTreeMap<KeyFingerprint, KeyEntry>,
255}
256
257impl std::fmt::Debug for MasterKeyring {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 f.debug_struct("MasterKeyring")
260 .field("keys", &"<redacted>")
261 .finish()
262 }
263}
264
265impl MasterKeyring {
266 pub fn generate() -> Self {
268 Self::from(EncryptionService::from_key(generate_random_key()))
269 }
270
271 pub fn to_serialized(&self) -> String {
275 EncryptionService::from(self.clone())
276 .to_keyring_string()
277 .expect("a MasterKeyring always holds at least one generation")
278 }
279
280 pub fn from_serialized(s: &str) -> Result<Self, EncryptionError> {
282 EncryptionService::new(s).map(Self::from)
283 }
284
285 pub fn fingerprint(&self) -> String {
288 EncryptionService::from(self.clone()).fingerprint()
289 }
290}
291
292impl From<EncryptionService> for MasterKeyring {
293 fn from(service: EncryptionService) -> Self {
294 Self { keys: service.keys }
295 }
296}
297
298impl From<MasterKeyring> for EncryptionService {
299 fn from(keyring: MasterKeyring) -> Self {
300 EncryptionService { keys: keyring.keys }
301 }
302}
303
304fn key_fingerprint(key: &[u8; 32]) -> KeyFingerprint {
307 KeyFingerprint::from_bytes(Sha256::digest(key).into())
308}
309
310fn insert_key_entry(
311 keys: &mut BTreeMap<KeyFingerprint, KeyEntry>,
312 fingerprint: KeyFingerprint,
313 entry: KeyEntry,
314) -> Result<(), EncryptionError> {
315 match keys.get(&fingerprint) {
316 None => {
317 keys.insert(fingerprint, entry);
318 Ok(())
319 }
320 Some(existing) if existing == &entry => Ok(()),
321 Some(existing) => Err(EncryptionError::KeyManagement(format!(
322 "key fingerprint {fingerprint} identifies conflicting entries at generations {} and {}",
323 existing.generation, entry.generation,
324 ))),
325 }
326}
327
328#[derive(Clone)]
334pub struct EncryptionService {
335 keys: BTreeMap<KeyFingerprint, KeyEntry>,
343}
344impl std::fmt::Debug for EncryptionService {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("EncryptionService")
347 .field("keys", &"<redacted>")
348 .finish()
349 }
350}
351impl EncryptionService {
352 pub fn new(stored_key: &str) -> Result<Self, EncryptionError> {
354 info!("Loading master key...");
355 EncryptionService::from_keyring_json(stored_key)
356 }
357
358 pub fn from_key(key: [u8; 32]) -> Self {
360 Self::from_key_at_generation(INITIAL_KEY_GENERATION, key)
361 }
362
363 pub fn from_key_at_generation(generation: u64, key: [u8; 32]) -> Self {
364 let mut keys = BTreeMap::new();
365 keys.insert(key_fingerprint(&key), KeyEntry { generation, key });
366 EncryptionService { keys }
367 }
368
369 pub fn from_keyring(
370 keys: impl IntoIterator<Item = (u64, [u8; 32])>,
371 ) -> Result<Self, EncryptionError> {
372 let mut keyring = BTreeMap::new();
373 for (generation, key) in keys {
374 insert_key_entry(
375 &mut keyring,
376 key_fingerprint(&key),
377 KeyEntry { generation, key },
378 )?;
379 }
380 if keyring.is_empty() {
381 return Err(EncryptionError::KeyManagement(
382 "keyring has no keys".to_string(),
383 ));
384 }
385 Ok(EncryptionService { keys: keyring })
386 }
387
388 fn seal_entry(&self) -> (&KeyFingerprint, &KeyEntry) {
394 self.keys
395 .iter()
396 .max_by(|(fingerprint_a, a), (fingerprint_b, b)| {
397 a.generation
398 .cmp(&b.generation)
399 .then_with(|| fingerprint_a.cmp(fingerprint_b))
400 })
401 .expect("a keyring always holds at least one key")
402 }
403
404 pub fn current_generation(&self) -> u64 {
405 self.seal_entry().1.generation
406 }
407
408 pub fn key_count(&self) -> usize {
411 self.keys.len()
412 }
413
414 pub fn keyring_entries(&self) -> Vec<(u64, [u8; 32])> {
415 self.keys
416 .values()
417 .map(|entry| (entry.generation, entry.key))
418 .collect()
419 }
420
421 pub fn merged_with(
425 &self,
426 other: &EncryptionService,
427 ) -> Result<EncryptionService, EncryptionError> {
428 let mut keys = self.keys.clone();
429 for (fingerprint, entry) in &other.keys {
430 insert_key_entry(&mut keys, *fingerprint, entry.clone())?;
431 }
432 Ok(EncryptionService { keys })
433 }
434
435 pub fn to_keyring_string(&self) -> Result<String, EncryptionError> {
436 let payload = StoredKeyring {
437 keys: self
438 .keys
439 .values()
440 .map(|entry| StoredKeyringGeneration {
441 generation: entry.generation,
442 key_hex: hex::encode(entry.key),
443 })
444 .collect(),
445 };
446 serde_json::to_string(&payload)
447 .map_err(|e| EncryptionError::KeyManagement(format!("serialize keyring: {e}")))
448 }
449
450 pub fn to_keyring_payload(&self) -> Result<Vec<u8>, EncryptionError> {
451 self.to_keyring_string().map(String::into_bytes)
452 }
453
454 pub fn from_keyring_payload(plaintext: Vec<u8>) -> Result<Self, EncryptionError> {
455 let keyring = String::from_utf8(plaintext).map_err(|e| {
456 EncryptionError::KeyManagement(format!("keyring payload is not UTF-8: {e}"))
457 })?;
458 EncryptionService::from_keyring_json(&keyring)
459 }
460
461 fn from_keyring_json(keyring: &str) -> Result<Self, EncryptionError> {
462 let payload: StoredKeyring = serde_json::from_str(keyring).map_err(|e| {
463 EncryptionError::KeyManagement(format!("keyring JSON is malformed: {e}"))
464 })?;
465 let mut keys = Vec::with_capacity(payload.keys.len());
466 for entry in payload.keys {
467 let key: [u8; 32] = hex::decode(&entry.key_hex)
468 .map_err(|e| {
469 EncryptionError::KeyManagement(format!("keyring key is not hex: {e}"))
470 })?
471 .try_into()
472 .map_err(|_| {
473 EncryptionError::KeyManagement("keyring key is not 32 bytes".to_string())
474 })?;
475 keys.push((entry.generation, key));
476 }
477 EncryptionService::from_keyring(keys)
478 }
479
480 pub fn key_for_fingerprint(&self, fingerprint: &[u8; 32]) -> Result<[u8; 32], EncryptionError> {
484 let fingerprint = KeyFingerprint::from_bytes(*fingerprint);
485 self.keys.get(&fingerprint).map(|e| e.key).ok_or_else(|| {
486 EncryptionError::KeyManagement(format!("no key with fingerprint {fingerprint}"))
487 })
488 }
489
490 pub fn service_for_fingerprint(
491 &self,
492 fingerprint: &[u8; 32],
493 ) -> Result<EncryptionService, EncryptionError> {
494 let key = self.key_for_fingerprint(fingerprint)?;
495 let generation = self
499 .keys
500 .get(&KeyFingerprint::from_bytes(*fingerprint))
501 .expect("just resolved")
502 .generation;
503 Ok(EncryptionService::from_key_at_generation(generation, key))
504 }
505
506 pub fn with_appended_generation(
507 &self,
508 generation: u64,
509 key: [u8; 32],
510 ) -> Result<EncryptionService, EncryptionError> {
511 if generation <= self.current_generation() {
512 return Err(EncryptionError::KeyManagement(format!(
513 "new generation {generation} must be greater than current generation {}",
514 self.current_generation()
515 )));
516 }
517 let mut keys = self.keys.clone();
518 insert_key_entry(
519 &mut keys,
520 key_fingerprint(&key),
521 KeyEntry { generation, key },
522 )?;
523 Ok(EncryptionService { keys })
524 }
525
526 pub fn fingerprint(&self) -> String {
528 hex::encode(self.seal_fingerprint())
529 }
530
531 pub fn seal_fingerprint(&self) -> [u8; 32] {
534 *self.seal_entry().0.as_bytes()
535 }
536
537 pub fn seal_key_fingerprint(&self) -> KeyFingerprint {
538 KeyFingerprint::from_bytes(self.seal_fingerprint())
539 }
540
541 pub fn key_bytes(&self) -> [u8; 32] {
543 self.seal_entry().1.key
544 }
545
546 pub fn encrypt(&self, plaintext: &[u8], aad_context: &[u8]) -> Vec<u8> {
551 let mut sealer = self.sealer(plaintext.len() as u64, aad_context);
552 let mut output = sealer.base_nonce().to_vec();
553
554 if plaintext.is_empty() {
556 output.extend(sealer.seal_chunk(&[]));
557 return output;
558 }
559
560 for chunk in plaintext.chunks(CHUNK_SIZE) {
561 output.extend(sealer.seal_chunk(chunk));
562 }
563
564 output
565 }
566
567 pub fn decrypt(
569 &self,
570 encrypted_data: &[u8],
571 aad_context: &[u8],
572 ) -> Result<Vec<u8>, EncryptionError> {
573 let base_nonce = read_base_nonce(encrypted_data)?;
574 let layout = encrypted_chunk_layout(encrypted_data.len())?;
575 let key = self.key_bytes();
576 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
577
578 let mut result = Vec::with_capacity(decrypted_len_upper_bound(layout.data_len));
579 for chunk_index in 0..layout.total_chunks {
580 let (chunk_start, chunk_end) =
581 layout.chunk_bounds(encrypted_data.len(), chunk_index)?;
582 let chunk_data = &encrypted_data[chunk_start..chunk_end];
583 let decrypted = decrypt_chunk_with_cipher(
584 &cipher,
585 &base_nonce,
586 aad_context,
587 chunk_index as u64,
588 layout.total_chunks as u64,
589 chunk_data,
590 )
591 .map_err(|_| {
592 EncryptionError::Decryption(format!(
593 "Authentication failed for chunk {}",
594 chunk_index
595 ))
596 })?;
597 result.extend(decrypted);
598 }
599
600 Ok(result)
601 }
602
603 pub fn sealer(&self, plaintext_len: u64, aad_context: &[u8]) -> ChunkSealer {
606 ChunkSealer::new(&self.key_bytes(), plaintext_len, aad_context)
607 }
608
609 pub fn decrypt_chunk(
612 &self,
613 ciphertext: &[u8],
614 chunk_index: usize,
615 aad_context: &[u8],
616 ) -> Result<Vec<u8>, EncryptionError> {
617 let base_nonce = read_base_nonce(ciphertext)?;
618 let layout = encrypted_chunk_layout(ciphertext.len())?;
619 let (chunk_start, chunk_end) = layout.chunk_bounds(ciphertext.len(), chunk_index)?;
620 let chunk_data = &ciphertext[chunk_start..chunk_end];
621
622 let key = self.key_bytes();
623 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
624 decrypt_chunk_with_cipher(
625 &cipher,
626 &base_nonce,
627 aad_context,
628 chunk_index as u64,
629 layout.total_chunks as u64,
630 chunk_data,
631 )
632 .map_err(|_| EncryptionError::Decryption("Authentication failed".to_string()))
633 }
634
635 pub fn decrypt_range_with_offset(
648 &self,
649 nonce: &[u8],
650 encrypted_chunks: &[u8],
651 first_chunk_index: u64,
652 plaintext_start: u64,
653 plaintext_end: u64,
654 source_size: u64,
655 aad_context: &[u8],
656 ) -> Result<Vec<u8>, EncryptionError> {
657 if nonce.len() != NONCE_SIZE {
658 return Err(EncryptionError::Decryption(format!(
659 "Invalid nonce length: expected {}, got {}",
660 NONCE_SIZE,
661 nonce.len()
662 )));
663 }
664
665 if plaintext_start >= plaintext_end {
666 return Err(EncryptionError::Decryption(format!(
667 "Invalid range: start ({}) >= end ({})",
668 plaintext_start, plaintext_end
669 )));
670 }
671 if plaintext_end > source_size {
672 return Err(EncryptionError::Decryption(format!(
673 "Invalid range: end ({plaintext_end}) > source size ({source_size})"
674 )));
675 }
676
677 let base_nonce: [u8; NONCE_SIZE] = nonce
678 .try_into()
679 .map_err(|_| EncryptionError::Decryption("Invalid nonce".to_string()))?;
680
681 let key = self.key_bytes();
682 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
683
684 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
685 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
686 let total_chunks = chunk_count_for_plaintext_len(source_size);
687
688 let mut plaintext = Vec::new();
689
690 for absolute_chunk_idx in start_chunk..=end_chunk {
691 let relative_idx = absolute_chunk_idx - first_chunk_index;
693 let chunk_start = (relative_idx as usize) * ENCRYPTED_CHUNK_SIZE;
694
695 let chunk_end = if chunk_start + ENCRYPTED_CHUNK_SIZE > encrypted_chunks.len() {
697 encrypted_chunks.len()
698 } else {
699 chunk_start + ENCRYPTED_CHUNK_SIZE
700 };
701
702 if chunk_start >= encrypted_chunks.len() {
703 return Err(EncryptionError::Decryption(format!(
704 "Chunk {} not in provided data (first_chunk_index={})",
705 absolute_chunk_idx, first_chunk_index
706 )));
707 }
708
709 let chunk_data = &encrypted_chunks[chunk_start..chunk_end];
710 let decrypted = decrypt_chunk_with_cipher(
711 &cipher,
712 &base_nonce,
713 aad_context,
714 absolute_chunk_idx,
715 total_chunks,
716 chunk_data,
717 )
718 .map_err(|_| {
719 EncryptionError::Decryption(format!(
720 "Authentication failed for chunk {}",
721 absolute_chunk_idx
722 ))
723 })?;
724
725 plaintext.extend(decrypted);
726 }
727
728 let offset_in_first_chunk = (plaintext_start % CHUNK_SIZE as u64) as usize;
730 let len = (plaintext_end - plaintext_start) as usize;
731 let end = offset_in_first_chunk + len;
732
733 if end > plaintext.len() {
734 return Err(EncryptionError::Decryption(format!(
735 "Decrypted data too short: need {} bytes, got {}",
736 end,
737 plaintext.len()
738 )));
739 }
740
741 Ok(plaintext[offset_in_first_chunk..end].to_vec())
742 }
743
744 pub fn derive_scoped(&self, scope_id: &str) -> EncryptionService {
749 let derived = self.derive_key(&format!("coven-scope-v1:{scope_id}"));
750 EncryptionService::from_key_at_generation(self.current_generation(), derived)
751 }
752
753 pub fn derive_scoped_for_fingerprint(
754 &self,
755 fingerprint: &[u8; 32],
756 scope_id: &str,
757 ) -> Result<EncryptionService, EncryptionError> {
758 let key = self.key_for_fingerprint(fingerprint)?;
759 let generation = self
760 .keys
761 .get(&KeyFingerprint::from_bytes(*fingerprint))
762 .expect("just resolved")
763 .generation;
764 let derived = derive_key_from(&key, &format!("coven-scope-v1:{scope_id}"));
765 Ok(EncryptionService::from_key_at_generation(
766 generation, derived,
767 ))
768 }
769
770 pub fn derive_key(&self, info: &str) -> [u8; 32] {
780 derive_key_from(&self.key_bytes(), info)
781 }
782
783 pub fn seal_app_data(&self, plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
801 let fingerprint = self.seal_fingerprint();
802 let mut sealed = Vec::with_capacity(
803 APP_DATA_HEADER_SIZE + chunked_encrypted_len(plaintext.len() as u64) as usize,
804 );
805 sealed.push(APP_DATA_SEAL_VERSION);
806 sealed.extend_from_slice(&fingerprint);
807 sealed.extend(self.encrypt(plaintext, aad));
808 sealed
809 }
810
811 pub fn open_app_data(&self, sealed: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
817 let (fingerprint, ciphertext) = split_sealed_app_data(sealed)?;
818 self.service_for_fingerprint(&fingerprint)
819 .map_err(|_| SealError::UnknownKey(hex::encode(fingerprint)))?
822 .decrypt(ciphertext, aad)
823 .map_err(SealError::Crypto)
824 }
825}
826
827fn split_sealed_app_data(sealed: &[u8]) -> Result<([u8; 32], &[u8]), SealError> {
834 let (&version, rest) = sealed.split_first().ok_or_else(|| {
835 SealError::Crypto(EncryptionError::Decryption(
836 "sealed app-data payload is empty".to_string(),
837 ))
838 })?;
839 if version != APP_DATA_SEAL_VERSION {
840 return Err(SealError::UnknownVersion(version));
841 }
842 if rest.len() < APP_DATA_FINGERPRINT_SIZE {
843 return Err(SealError::Crypto(EncryptionError::Decryption(
844 "sealed app-data payload is truncated before its key fingerprint".to_string(),
845 )));
846 }
847 let (fingerprint, ciphertext) = rest.split_at(APP_DATA_FINGERPRINT_SIZE);
848 let fingerprint: [u8; 32] = fingerprint
849 .try_into()
850 .expect("split_at yields exactly APP_DATA_FINGERPRINT_SIZE bytes");
851 Ok((fingerprint, ciphertext))
852}
853
854fn derive_key_from(key: &[u8; 32], info: &str) -> [u8; 32] {
855 let hk = Hkdf::<Sha256>::new(Some(b"coven-hkdf-salt-v1"), key);
856 let mut okm = [0u8; 32];
857 hk.expand(info.as_bytes(), &mut okm)
858 .expect("32 bytes is a valid HKDF output length");
859 okm
860}
861
862#[derive(Clone, Copy)]
863struct EncryptedChunkLayout {
864 data_len: usize,
865 total_chunks: usize,
866 has_partial: bool,
867}
868
869impl EncryptedChunkLayout {
870 fn chunk_bounds(
871 self,
872 ciphertext_len: usize,
873 chunk_index: usize,
874 ) -> Result<(usize, usize), EncryptionError> {
875 if chunk_index >= self.total_chunks {
876 return Err(EncryptionError::Decryption(format!(
877 "Chunk index {} out of range (total chunks: {})",
878 chunk_index, self.total_chunks
879 )));
880 }
881
882 let chunk_start = NONCE_SIZE + chunk_index * ENCRYPTED_CHUNK_SIZE;
883 let chunk_end = if chunk_index == self.total_chunks - 1 && self.has_partial {
884 ciphertext_len
885 } else {
886 chunk_start + ENCRYPTED_CHUNK_SIZE
887 };
888 Ok((chunk_start, chunk_end))
889 }
890}
891
892fn read_base_nonce(ciphertext: &[u8]) -> Result<[u8; NONCE_SIZE], EncryptionError> {
893 if ciphertext.len() < NONCE_SIZE {
894 return Err(EncryptionError::Decryption(
895 "Ciphertext too short for nonce".to_string(),
896 ));
897 }
898
899 let mut base_nonce = [0u8; NONCE_SIZE];
900 base_nonce.copy_from_slice(&ciphertext[..NONCE_SIZE]);
901 Ok(base_nonce)
902}
903
904fn encrypted_chunk_layout(ciphertext_len: usize) -> Result<EncryptedChunkLayout, EncryptionError> {
905 if ciphertext_len < NONCE_SIZE {
906 return Err(EncryptionError::Decryption(
907 "Ciphertext too short for nonce".to_string(),
908 ));
909 }
910
911 let data_len = ciphertext_len - NONCE_SIZE;
912 let num_full_chunks = data_len / ENCRYPTED_CHUNK_SIZE;
913 let has_partial = !data_len.is_multiple_of(ENCRYPTED_CHUNK_SIZE);
914 let total_chunks = num_full_chunks + usize::from(has_partial);
915 Ok(EncryptedChunkLayout {
916 data_len,
917 total_chunks,
918 has_partial,
919 })
920}
921
922fn decrypted_len_upper_bound(encrypted_data_len: usize) -> usize {
923 let chunk_count = encrypted_data_len.div_ceil(ENCRYPTED_CHUNK_SIZE);
924 encrypted_data_len.saturating_sub(chunk_count * TAG_SIZE)
925}
926
927fn chunk_count_for_plaintext_len(plaintext_len: u64) -> u64 {
928 plaintext_len.div_ceil(CHUNK_SIZE as u64).max(1)
929}
930
931fn chunk_aad(aad_context: &[u8], chunk_index: u64, total_chunks: u64) -> Vec<u8> {
932 let mut aad = Vec::with_capacity(AEAD_V2_LABEL.len() + 8 + aad_context.len() + 16);
933 aad.extend_from_slice(AEAD_V2_LABEL);
934 aad.extend_from_slice(&(aad_context.len() as u64).to_le_bytes());
935 aad.extend_from_slice(aad_context);
936 aad.extend_from_slice(&chunk_index.to_le_bytes());
937 aad.extend_from_slice(&total_chunks.to_le_bytes());
938 aad
939}
940
941fn decrypt_chunk_with_cipher(
942 cipher: &XChaCha20Poly1305,
943 base_nonce: &[u8; NONCE_SIZE],
944 aad_context: &[u8],
945 chunk_index: u64,
946 total_chunks: u64,
947 chunk_data: &[u8],
948) -> Result<Vec<u8>, ()> {
949 let nonce = chunk_nonce(base_nonce, chunk_index);
950 let nonce_arr = GenericArray::from_slice(&nonce);
951 let aad = chunk_aad(aad_context, chunk_index, total_chunks);
952 cipher
953 .decrypt(
954 nonce_arr,
955 Payload {
956 msg: chunk_data,
957 aad: &aad,
958 },
959 )
960 .map_err(|_| ())
961}
962
963fn chunk_nonce(base_nonce: &[u8; NONCE_SIZE], chunk_index: u64) -> [u8; NONCE_SIZE] {
965 let mut nonce = *base_nonce;
966 let index_bytes = chunk_index.to_le_bytes();
967 for i in 0..8 {
968 nonce[i] ^= index_bytes[i];
969 }
970 nonce
971}
972
973pub fn encrypted_chunk_range(plaintext_start: u64, plaintext_end: u64) -> (u64, u64) {
981 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
982 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
983
984 let chunk_start = NONCE_SIZE as u64 + start_chunk * ENCRYPTED_CHUNK_SIZE as u64;
985 let chunk_end = NONCE_SIZE as u64 + (end_chunk + 1) * ENCRYPTED_CHUNK_SIZE as u64;
986
987 (chunk_start, chunk_end)
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993
994 const TEST_AAD: &[u8] = b"encryption-test-context";
995
996 fn test_key() -> [u8; 32] {
997 [
999 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
1000 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
1001 0x1c, 0x1d, 0x1e, 0x1f,
1002 ]
1003 }
1004
1005 fn create_test_service() -> EncryptionService {
1006 EncryptionService::from_key(test_key())
1007 }
1008
1009 fn decrypt_plaintext_range(
1010 service: &EncryptionService,
1011 full_ciphertext: &[u8],
1012 source_size: u64,
1013 plaintext_start: u64,
1014 plaintext_end: u64,
1015 ) -> Vec<u8> {
1016 let nonce = &full_ciphertext[..NONCE_SIZE];
1017 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1018 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1019 let first_chunk_index = (chunk_start - NONCE_SIZE as u64) / ENCRYPTED_CHUNK_SIZE as u64;
1020 service
1021 .decrypt_range_with_offset(
1022 nonce,
1023 chunks_only,
1024 first_chunk_index,
1025 plaintext_start,
1026 plaintext_end,
1027 source_size,
1028 TEST_AAD,
1029 )
1030 .unwrap()
1031 }
1032
1033 #[test]
1034 fn test_roundtrip_small() {
1035 let service = create_test_service();
1036 let plaintext = b"Hello, world!";
1037
1038 let ciphertext = service.encrypt(plaintext, TEST_AAD);
1039 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1040
1041 assert_eq!(decrypted, plaintext);
1042 }
1043
1044 #[test]
1050 fn streaming_sealer_matches_whole_buffer_format() {
1051 let service = create_test_service();
1052 for len in [
1053 0usize,
1054 1,
1055 CHUNK_SIZE - 1,
1056 CHUNK_SIZE,
1057 CHUNK_SIZE + 1,
1058 200_000,
1059 ] {
1060 let plaintext: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
1061
1062 let mut sealer = service.sealer(plaintext.len() as u64, TEST_AAD);
1064 let mut streamed = sealer.base_nonce().to_vec();
1065 if plaintext.is_empty() {
1066 streamed.extend(sealer.seal_chunk(&[]));
1067 } else {
1068 for chunk in plaintext.chunks(CHUNK_SIZE) {
1069 streamed.extend(sealer.seal_chunk(chunk));
1070 }
1071 }
1072
1073 assert_eq!(
1074 streamed.len() as u64,
1075 chunked_encrypted_len(len as u64),
1076 "predicted length wrong for len={len}"
1077 );
1078 assert_eq!(
1079 service.decrypt(&streamed, TEST_AAD).unwrap(),
1080 plaintext,
1081 "streamed ciphertext failed to round-trip for len={len}"
1082 );
1083 }
1084 }
1085
1086 #[test]
1090 fn chunked_encrypted_len_matches_encrypt() {
1091 let service = create_test_service();
1092 for n in [
1093 0usize,
1094 1,
1095 CHUNK_SIZE - 1,
1096 CHUNK_SIZE,
1097 CHUNK_SIZE + 1,
1098 200_000,
1099 ] {
1100 let produced = service.encrypt(&vec![0u8; n], TEST_AAD).len() as u64;
1101 assert_eq!(
1102 chunked_encrypted_len(n as u64),
1103 produced,
1104 "predicted length wrong for n={n}"
1105 );
1106 }
1107 }
1108
1109 #[test]
1110 fn test_roundtrip_exact_chunk() {
1111 let service = create_test_service();
1112 let plaintext = vec![0x42u8; CHUNK_SIZE];
1113
1114 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1115 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1116
1117 assert_eq!(decrypted, plaintext);
1118 }
1119
1120 #[test]
1121 fn test_roundtrip_multiple_chunks() {
1122 let service = create_test_service();
1123 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 2 + CHUNK_SIZE / 2)
1125 .map(|i| (i % 256) as u8)
1126 .collect();
1127
1128 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1129 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1130
1131 assert_eq!(decrypted, plaintext);
1132 }
1133
1134 #[test]
1135 fn test_random_access_chunk() {
1136 let service = create_test_service();
1137 let mut plaintext = vec![0x00u8; CHUNK_SIZE];
1139 plaintext.extend(vec![0x11u8; CHUNK_SIZE]);
1140 plaintext.extend(vec![0x22u8; CHUNK_SIZE]);
1141
1142 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1143
1144 let chunk1 = service.decrypt_chunk(&ciphertext, 1, TEST_AAD).unwrap();
1146 assert_eq!(chunk1, vec![0x11u8; CHUNK_SIZE]);
1147
1148 let chunk0 = service.decrypt_chunk(&ciphertext, 0, TEST_AAD).unwrap();
1150 assert_eq!(chunk0, vec![0x00u8; CHUNK_SIZE]);
1151
1152 let chunk2 = service.decrypt_chunk(&ciphertext, 2, TEST_AAD).unwrap();
1154 assert_eq!(chunk2, vec![0x22u8; CHUNK_SIZE]);
1155 }
1156
1157 #[test]
1158 fn test_random_access_partial_last_chunk() {
1159 let service = create_test_service();
1160 let mut plaintext = vec![0xAAu8; CHUNK_SIZE];
1162 plaintext.extend(vec![0xBBu8; 100]);
1163
1164 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1165
1166 let chunk0 = service.decrypt_chunk(&ciphertext, 0, TEST_AAD).unwrap();
1167 assert_eq!(chunk0, vec![0xAAu8; CHUNK_SIZE]);
1168
1169 let chunk1 = service.decrypt_chunk(&ciphertext, 1, TEST_AAD).unwrap();
1170 assert_eq!(chunk1, vec![0xBBu8; 100]);
1171 }
1172
1173 #[test]
1174 fn test_tamper_detection() {
1175 let service = create_test_service();
1176 let plaintext = b"Secret data";
1177
1178 let mut ciphertext = service.encrypt(plaintext, TEST_AAD);
1179
1180 let tamper_pos = NONCE_SIZE + 5;
1182 ciphertext[tamper_pos] ^= 0xFF;
1183
1184 let result = service.decrypt(&ciphertext, TEST_AAD);
1185 assert!(result.is_err());
1186 }
1187
1188 #[test]
1189 fn truncating_trailing_chunks_fails_to_decrypt() {
1190 let service = create_test_service();
1191 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 251) as u8).collect();
1192 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1193 let truncated = &ciphertext[..ciphertext.len() - ENCRYPTED_CHUNK_SIZE];
1194
1195 assert!(
1196 service.decrypt(truncated, TEST_AAD).is_err(),
1197 "a truncated multi-chunk object must fail, not return a short plaintext",
1198 );
1199 }
1200
1201 #[test]
1202 fn test_empty_plaintext() {
1203 let service = create_test_service();
1204 let plaintext = b"";
1205
1206 let ciphertext = service.encrypt(plaintext, TEST_AAD);
1207
1208 assert_eq!(ciphertext.len(), NONCE_SIZE + TAG_SIZE);
1210
1211 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1212 assert_eq!(decrypted, plaintext);
1213 }
1214
1215 #[test]
1216 fn test_single_byte() {
1217 let service = create_test_service();
1218 let plaintext = b"x";
1219
1220 let ciphertext = service.encrypt(plaintext, TEST_AAD);
1221 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1222
1223 assert_eq!(decrypted, plaintext);
1224 }
1225
1226 #[test]
1227 fn test_encrypted_range_single_chunk() {
1228 let (start, end) = encrypted_chunk_range(0, 100);
1230
1231 assert_eq!(start, NONCE_SIZE as u64);
1232 assert_eq!(end, NONCE_SIZE as u64 + ENCRYPTED_CHUNK_SIZE as u64);
1233 }
1234
1235 #[test]
1236 fn test_encrypted_range_spans_chunks() {
1237 let (start, end) = encrypted_chunk_range(CHUNK_SIZE as u64 - 10, CHUNK_SIZE as u64 + 10);
1239
1240 assert_eq!(start, NONCE_SIZE as u64);
1241 assert_eq!(end, NONCE_SIZE as u64 + 2 * ENCRYPTED_CHUNK_SIZE as u64);
1242 }
1243
1244 #[test]
1245 fn test_encrypted_range_middle_chunk() {
1246 let chunk2_start = CHUNK_SIZE as u64 * 2;
1248 let (start, end) = encrypted_chunk_range(chunk2_start + 10, chunk2_start + 100);
1249
1250 assert_eq!(start, NONCE_SIZE as u64 + 2 * ENCRYPTED_CHUNK_SIZE as u64);
1251 assert_eq!(end, NONCE_SIZE as u64 + 3 * ENCRYPTED_CHUNK_SIZE as u64);
1252 }
1253
1254 #[test]
1255 fn test_different_encryptions_different_ciphertext() {
1256 let service = create_test_service();
1257 let plaintext = b"Same message";
1258
1259 let ciphertext1 = service.encrypt(plaintext, TEST_AAD);
1260 let ciphertext2 = service.encrypt(plaintext, TEST_AAD);
1261
1262 assert_ne!(ciphertext1, ciphertext2);
1264
1265 assert_eq!(service.decrypt(&ciphertext1, TEST_AAD).unwrap(), plaintext);
1267 assert_eq!(service.decrypt(&ciphertext2, TEST_AAD).unwrap(), plaintext);
1268 }
1269
1270 #[test]
1271 fn test_chunk_index_out_of_range() {
1272 let service = create_test_service();
1273 let plaintext = vec![0u8; CHUNK_SIZE]; let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1276
1277 assert!(service.decrypt_chunk(&ciphertext, 0, TEST_AAD).is_ok());
1279
1280 assert!(service.decrypt_chunk(&ciphertext, 1, TEST_AAD).is_err());
1282 }
1283
1284 #[test]
1285 fn test_decrypt_range_within_single_chunk() {
1286 let service = create_test_service();
1287 let plaintext: Vec<u8> = (0..CHUNK_SIZE).map(|i| (i % 256) as u8).collect();
1289
1290 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1291
1292 let decrypted =
1293 decrypt_plaintext_range(&service, &ciphertext, plaintext.len() as u64, 100, 200);
1294
1295 assert_eq!(decrypted.len(), 100);
1296 assert_eq!(decrypted, plaintext[100..200]);
1297 }
1298
1299 #[test]
1300 fn test_decrypt_range_spanning_chunks() {
1301 let service = create_test_service();
1302 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 256) as u8).collect();
1304
1305 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1306
1307 let start = CHUNK_SIZE as u64 - 100;
1309 let end = CHUNK_SIZE as u64 + 100;
1310 let decrypted =
1311 decrypt_plaintext_range(&service, &ciphertext, plaintext.len() as u64, start, end);
1312
1313 assert_eq!(decrypted.len(), 200);
1314 assert_eq!(decrypted, &plaintext[start as usize..end as usize]);
1315 }
1316
1317 #[test]
1318 fn test_decrypt_range_entire_middle_chunk() {
1319 let service = create_test_service();
1320 let mut plaintext = vec![0xAAu8; CHUNK_SIZE];
1322 plaintext.extend(vec![0xBBu8; CHUNK_SIZE]);
1323 plaintext.extend(vec![0xCCu8; CHUNK_SIZE]);
1324
1325 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1326
1327 let start = CHUNK_SIZE as u64;
1329 let end = (CHUNK_SIZE * 2) as u64;
1330 let decrypted =
1331 decrypt_plaintext_range(&service, &ciphertext, plaintext.len() as u64, start, end);
1332
1333 assert_eq!(decrypted, vec![0xBBu8; CHUNK_SIZE]);
1334 }
1335
1336 #[test]
1337 fn test_decrypt_range_with_partial_encrypted_data() {
1338 let service = create_test_service();
1339 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 256) as u8).collect();
1341 let full_ciphertext = service.encrypt(&plaintext, TEST_AAD);
1342
1343 let plaintext_start = CHUNK_SIZE as u64 + 100;
1345 let plaintext_end = CHUNK_SIZE as u64 + 200;
1346 let nonce = &full_ciphertext[..NONCE_SIZE];
1347 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1348 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1349 let first_chunk_index = (chunk_start - NONCE_SIZE as u64) / ENCRYPTED_CHUNK_SIZE as u64;
1350 let decrypted = service
1351 .decrypt_range_with_offset(
1352 nonce,
1353 chunks_only,
1354 first_chunk_index,
1355 plaintext_start,
1356 plaintext_end,
1357 plaintext.len() as u64,
1358 TEST_AAD,
1359 )
1360 .unwrap();
1361
1362 assert_eq!(decrypted.len(), 100);
1363 assert_eq!(
1364 decrypted,
1365 &plaintext[plaintext_start as usize..plaintext_end as usize]
1366 );
1367 }
1368
1369 #[test]
1370 fn test_encrypted_chunk_range_returns_actual_bounds() {
1371 let chunk5_start = CHUNK_SIZE as u64 * 5;
1374 let chunk5_end = chunk5_start + 1000;
1375
1376 let (enc_start, enc_end) = encrypted_chunk_range(chunk5_start, chunk5_end);
1377
1378 let expected_start = NONCE_SIZE as u64 + 5 * ENCRYPTED_CHUNK_SIZE as u64;
1380 let expected_end = NONCE_SIZE as u64 + 6 * ENCRYPTED_CHUNK_SIZE as u64;
1381
1382 assert_eq!(
1383 enc_start, expected_start,
1384 "encrypted_chunk_range should return actual chunk start, not 0"
1385 );
1386 assert_eq!(enc_end, expected_end);
1387 }
1388
1389 #[test]
1390 fn test_encrypted_chunk_range_spanning_multiple_chunks() {
1391 let start = CHUNK_SIZE as u64 * 3 + 100;
1393 let end = CHUNK_SIZE as u64 * 5 + 500;
1394
1395 let (enc_start, enc_end) = encrypted_chunk_range(start, end);
1396
1397 let expected_start = NONCE_SIZE as u64 + 3 * ENCRYPTED_CHUNK_SIZE as u64;
1398 let expected_end = NONCE_SIZE as u64 + 6 * ENCRYPTED_CHUNK_SIZE as u64;
1399
1400 assert_eq!(enc_start, expected_start);
1401 assert_eq!(enc_end, expected_end);
1402 }
1403
1404 #[test]
1405 fn test_decrypt_range_with_separate_nonce() {
1406 let service = create_test_service();
1408
1409 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 10).map(|i| (i % 256) as u8).collect();
1411 let full_ciphertext = service.encrypt(&plaintext, TEST_AAD);
1412
1413 let nonce = &full_ciphertext[..NONCE_SIZE];
1415
1416 let plaintext_start = CHUNK_SIZE as u64 * 7 + 100;
1418 let plaintext_end = CHUNK_SIZE as u64 * 7 + 500;
1419
1420 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1422
1423 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1425
1426 let first_chunk_index = plaintext_start / CHUNK_SIZE as u64;
1428
1429 let decrypted = service
1431 .decrypt_range_with_offset(
1432 nonce,
1433 chunks_only,
1434 first_chunk_index,
1435 plaintext_start,
1436 plaintext_end,
1437 plaintext.len() as u64,
1438 TEST_AAD,
1439 )
1440 .unwrap();
1441
1442 assert_eq!(decrypted.len(), 400);
1443 assert_eq!(
1444 decrypted,
1445 &plaintext[plaintext_start as usize..plaintext_end as usize]
1446 );
1447 }
1448
1449 #[test]
1450 fn test_decrypt_range_with_offset_spanning_chunks() {
1451 let service = create_test_service();
1453
1454 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 10).map(|i| (i % 256) as u8).collect();
1455 let full_ciphertext = service.encrypt(&plaintext, TEST_AAD);
1456 let nonce = &full_ciphertext[..NONCE_SIZE];
1457
1458 let plaintext_start = CHUNK_SIZE as u64 * 3 + 1000;
1460 let plaintext_end = CHUNK_SIZE as u64 * 5 + 2000;
1461
1462 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1463 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1464 let first_chunk_index = plaintext_start / CHUNK_SIZE as u64;
1465
1466 let decrypted = service
1467 .decrypt_range_with_offset(
1468 nonce,
1469 chunks_only,
1470 first_chunk_index,
1471 plaintext_start,
1472 plaintext_end,
1473 plaintext.len() as u64,
1474 TEST_AAD,
1475 )
1476 .unwrap();
1477
1478 let expected_len = (plaintext_end - plaintext_start) as usize;
1479 assert_eq!(decrypted.len(), expected_len);
1480 assert_eq!(
1481 decrypted,
1482 &plaintext[plaintext_start as usize..plaintext_end as usize]
1483 );
1484 }
1485
1486 #[test]
1487 fn test_fingerprint_deterministic() {
1488 let service = create_test_service();
1489 assert_eq!(service.fingerprint(), service.fingerprint());
1490 }
1491
1492 #[test]
1493 fn test_fingerprint_different_keys() {
1494 let service1 = EncryptionService::from_key([0u8; 32]);
1495 let service2 = EncryptionService::from_key([1u8; 32]);
1496 assert_ne!(service1.fingerprint(), service2.fingerprint());
1497 }
1498
1499 #[test]
1500 fn key_fingerprint_wire_form_is_strict_lowercase_hex() {
1501 let fingerprint = create_test_service().seal_key_fingerprint();
1502 let serialized = serde_json::to_string(&fingerprint).expect("serialize fingerprint");
1503 assert_eq!(
1504 serde_json::from_str::<KeyFingerprint>(&serialized).unwrap(),
1505 fingerprint
1506 );
1507 assert!(fingerprint
1508 .to_string()
1509 .to_uppercase()
1510 .parse::<KeyFingerprint>()
1511 .is_err());
1512 }
1513
1514 #[test]
1515 fn key_fingerprint_is_the_full_sha256_digest() {
1516 let fingerprint = create_test_service().seal_key_fingerprint();
1517 let expected: [u8; 32] = Sha256::digest(test_key()).into();
1518
1519 assert_eq!(fingerprint.as_bytes().as_slice(), expected.as_slice());
1520 assert_eq!(fingerprint.to_string(), hex::encode(expected));
1521 assert_eq!(fingerprint.to_string().len(), 64);
1522 assert!("630dcd2966c43366".parse::<KeyFingerprint>().is_err());
1523 }
1524
1525 #[test]
1526 fn derive_scoped_deterministic() {
1527 let service = create_test_service();
1528 let derived1 = service.derive_scoped("rel-123");
1529 let derived2 = service.derive_scoped("rel-123");
1530 assert_eq!(derived1.key_bytes(), derived2.key_bytes());
1531 }
1532
1533 #[test]
1534 fn derive_scoped_different_releases() {
1535 let service = create_test_service();
1536 let key_a = service.derive_scoped("rel-aaa").key_bytes();
1537 let key_b = service.derive_scoped("rel-bbb").key_bytes();
1538 assert_ne!(key_a, key_b);
1539 }
1540
1541 #[test]
1542 fn derive_scoped_different_master_keys() {
1543 let svc1 = EncryptionService::from_key([0u8; 32]);
1544 let svc2 = EncryptionService::from_key([1u8; 32]);
1545 let key1 = svc1.derive_scoped("rel-123").key_bytes();
1546 let key2 = svc2.derive_scoped("rel-123").key_bytes();
1547 assert_ne!(key1, key2);
1548 }
1549
1550 #[test]
1551 fn derive_scoped_roundtrip() {
1552 let master = create_test_service();
1553 let release_enc = master.derive_scoped("rel-456");
1554 let plaintext = b"test audio data for this release";
1555
1556 let encrypted = release_enc.encrypt(plaintext, TEST_AAD);
1557 let decrypted = release_enc.decrypt(&encrypted, TEST_AAD).unwrap();
1558 assert_eq!(decrypted, plaintext);
1559
1560 assert!(master.decrypt(&encrypted, TEST_AAD).is_err());
1562
1563 let wrong_enc = master.derive_scoped("rel-999");
1565 assert!(wrong_enc.decrypt(&encrypted, TEST_AAD).is_err());
1566 }
1567
1568 #[test]
1569 fn master_keyring_from_serialized_accepts_the_current_keyring_format() {
1570 let keyring = MasterKeyring::generate();
1571 let serialized = keyring.to_serialized();
1572 let parsed =
1573 MasterKeyring::from_serialized(&serialized).expect("parse a generated keyring");
1574 assert_eq!(parsed.to_serialized(), serialized);
1575 assert_eq!(parsed.fingerprint(), keyring.fingerprint());
1576 }
1577
1578 #[test]
1579 fn master_keyring_from_serialized_rejects_raw_hex() {
1580 let raw_hex = hex::encode(test_key());
1581 assert!(MasterKeyring::from_serialized(&raw_hex).is_err());
1582 }
1583
1584 #[test]
1585 fn keyring_payload_requires_the_current_json_format() {
1586 let service = create_test_service()
1587 .with_appended_generation(2, [9u8; 32])
1588 .expect("append a generation");
1589 let payload = service
1590 .to_keyring_payload()
1591 .expect("serialize the current keyring payload");
1592 let parsed = EncryptionService::from_keyring_payload(payload)
1593 .expect("parse the current keyring payload");
1594
1595 assert_eq!(parsed.keyring_entries(), service.keyring_entries());
1596 assert!(EncryptionService::from_keyring_payload(test_key().to_vec()).is_err());
1597 }
1598
1599 #[test]
1600 fn master_keyring_and_encryption_service_convert_without_losing_generations() {
1601 let service = EncryptionService::from_key(test_key())
1602 .with_appended_generation(2, [9u8; 32])
1603 .expect("append a generation");
1604 let keyring: MasterKeyring = service.clone().into();
1605 assert_eq!(keyring.fingerprint(), service.fingerprint());
1606 assert_eq!(
1607 keyring.to_serialized(),
1608 service.to_keyring_string().unwrap()
1609 );
1610
1611 let round_tripped: EncryptionService = keyring.into();
1612 assert_eq!(round_tripped.current_generation(), 2);
1613 assert_eq!(round_tripped.keyring_entries(), service.keyring_entries(),);
1614 }
1615
1616 #[test]
1623 fn same_generation_fork_converges_on_one_seal_key_and_keeps_both() {
1624 let base = EncryptionService::from_key([1u8; 32]);
1625 let fork_a = base.with_appended_generation(2, [0xA0u8; 32]).unwrap();
1626 let fork_b = base.with_appended_generation(2, [0xB0u8; 32]).unwrap();
1627
1628 let a_then_b = fork_a.merged_with(&fork_b).unwrap();
1629 let b_then_a = fork_b.merged_with(&fork_a).unwrap();
1630 assert_eq!(
1631 a_then_b.fingerprint(),
1632 b_then_a.fingerprint(),
1633 "seal selection is order-independent, so both sides converge on one key",
1634 );
1635 assert_eq!(
1636 a_then_b.key_count(),
1637 3,
1638 "the base key and both forks are held"
1639 );
1640 assert_eq!(a_then_b.current_generation(), 2);
1641
1642 let sealed_a = fork_a.seal_app_data(b"from owner A", b"ctx");
1643 let sealed_b = fork_b.seal_app_data(b"from owner B", b"ctx");
1644 assert_eq!(
1645 a_then_b.open_app_data(&sealed_a, b"ctx").unwrap(),
1646 b"from owner A",
1647 );
1648 assert_eq!(
1649 a_then_b.open_app_data(&sealed_b, b"ctx").unwrap(),
1650 b"from owner B",
1651 );
1652 }
1653
1654 #[test]
1655 fn keyring_construction_rejects_one_key_at_conflicting_generations() {
1656 let key = [0x44u8; 32];
1657
1658 assert!(EncryptionService::from_keyring([(1, key), (2, key)]).is_err());
1659 }
1660
1661 #[test]
1662 fn appending_a_generation_rejects_an_existing_key_fingerprint() {
1663 let key = [0x55u8; 32];
1664 let keyring = EncryptionService::from_key_at_generation(1, key);
1665
1666 assert!(keyring.with_appended_generation(2, key).is_err());
1667 }
1668
1669 #[test]
1670 fn merging_rejects_one_key_at_conflicting_generations() {
1671 let key = [0x66u8; 32];
1672 let generation_one = EncryptionService::from_key_at_generation(1, key);
1673 let generation_two = EncryptionService::from_key_at_generation(2, key);
1674
1675 assert!(generation_one.merged_with(&generation_two).is_err());
1676 }
1677
1678 #[test]
1679 fn identical_duplicate_keyring_entries_are_deduplicated() {
1680 let key = [0x77u8; 32];
1681 let keyring =
1682 EncryptionService::from_keyring([(1, key), (1, key)]).expect("identical entries");
1683
1684 assert_eq!(keyring.key_count(), 1);
1685 }
1686
1687 #[test]
1688 fn merging_identical_keyring_entries_deduplicates_them() {
1689 let key = [0x88u8; 32];
1690 let left = EncryptionService::from_key_at_generation(1, key);
1691 let right = EncryptionService::from_key_at_generation(1, key);
1692 let merged = left.merged_with(&right).expect("identical entries");
1693
1694 assert_eq!(merged.key_count(), 1);
1695 }
1696
1697 #[test]
1698 fn master_keyring_debug_redacts_keys() {
1699 let keyring = MasterKeyring::generate();
1700 let debug = format!("{keyring:?}");
1701 assert!(debug.contains("<redacted>"), "{debug}");
1702 }
1703
1704 const APP_DATA_V1_FIXTURE_PLAINTEXT: &[u8] = b"pinned app-data payload";
1713 const APP_DATA_V1_FIXTURE_AAD: &[u8] = b"pinned-app-data-context";
1714 const APP_DATA_V1_FIXTURE_HEX: &str = concat!(
1715 "01",
1716 "630dcd2966c4336691125448bbb25b4ff412a49c732db2c8abc1b8581bd710dd",
1717 "2bdfe10d13cb397b648c2eb352bbadd92a19eafd8499b5c5",
1718 "b0d1e8eb56f757621ec41a78488c937427aac5df38b5e8af",
1719 "2b2b8c9155ead15242e0c87b00bbe8",
1720 );
1721
1722 fn sealed_fingerprint(sealed: &[u8]) -> [u8; 32] {
1726 sealed[1..33].try_into().expect("a sealed header")
1727 }
1728
1729 #[test]
1730 fn seal_app_data_round_trips_and_records_its_version_and_key() {
1731 let service = create_test_service();
1732 for payload in [b"".as_slice(), b"x", b"a longer app-data secret value"] {
1733 let sealed = service.seal_app_data(payload, TEST_AAD);
1734
1735 assert_eq!(sealed[0], APP_DATA_SEAL_VERSION, "the version byte leads");
1736 assert_eq!(
1737 sealed_fingerprint(&sealed),
1738 service.seal_fingerprint(),
1739 "the header names the key it sealed under",
1740 );
1741 assert_eq!(
1742 sealed.len(),
1743 APP_DATA_HEADER_SIZE + chunked_encrypted_len(payload.len() as u64) as usize,
1744 "the body is exactly the chunked ciphertext, behind the fixed header",
1745 );
1746 assert_eq!(service.open_app_data(&sealed, TEST_AAD).unwrap(), payload);
1747 }
1748 }
1749
1750 #[test]
1751 fn sealed_app_data_header_carries_the_full_key_digest() {
1752 let service = create_test_service();
1753 let plaintext = b"full fingerprint header";
1754 let sealed = service.seal_app_data(plaintext, TEST_AAD);
1755 let expected: [u8; 32] = Sha256::digest(test_key()).into();
1756
1757 assert_eq!(&sealed[1..33], expected.as_slice());
1758 assert_eq!(
1759 sealed.len(),
1760 33 + chunked_encrypted_len(plaintext.len() as u64) as usize
1761 );
1762 assert_eq!(service.open_app_data(&sealed, TEST_AAD).unwrap(), plaintext);
1763 }
1764
1765 #[test]
1768 fn open_app_data_rejects_a_different_aad() {
1769 let service = create_test_service();
1770 let sealed = service.seal_app_data(b"bound to row 42", b"row-42");
1771
1772 let error = service
1773 .open_app_data(&sealed, b"row-99")
1774 .expect_err("a different aad must not open the payload");
1775
1776 assert!(matches!(error, SealError::Crypto(_)), "{error:?}");
1777 }
1778
1779 #[test]
1780 fn open_app_data_rejects_a_flipped_ciphertext_byte() {
1781 let service = create_test_service();
1782 let mut sealed = service.seal_app_data(b"tamper with me", TEST_AAD);
1783 let last = sealed.len() - 1;
1784 sealed[last] ^= 0xFF;
1785
1786 let error = service
1787 .open_app_data(&sealed, TEST_AAD)
1788 .expect_err("a tampered payload must fail authentication");
1789
1790 assert!(matches!(error, SealError::Crypto(_)), "{error:?}");
1791 }
1792
1793 #[test]
1796 fn open_app_data_rejects_an_unknown_version() {
1797 let service = create_test_service();
1798 let mut sealed = service.seal_app_data(b"a version-1 payload", TEST_AAD);
1799 sealed[0] = 2;
1800
1801 let error = service
1802 .open_app_data(&sealed, TEST_AAD)
1803 .expect_err("version 2 must be refused");
1804
1805 assert!(matches!(error, SealError::UnknownVersion(2)), "{error:?}");
1806 }
1807
1808 #[test]
1812 fn open_app_data_survives_rotation_and_each_payload_names_its_key() {
1813 let before_rotation = create_test_service();
1814 let sealed_under_1 = before_rotation.seal_app_data(b"sealed before rotating", TEST_AAD);
1815
1816 let after_rotation = before_rotation
1817 .with_appended_generation(2, [9u8; 32])
1818 .expect("rotate the keyring");
1819 let sealed_under_2 = after_rotation.seal_app_data(b"sealed after rotating", TEST_AAD);
1820
1821 assert_eq!(
1822 sealed_fingerprint(&sealed_under_1),
1823 before_rotation.seal_fingerprint(),
1824 );
1825 assert_eq!(
1826 sealed_fingerprint(&sealed_under_2),
1827 after_rotation.seal_fingerprint(),
1828 "sealing after a rotation records the new seal key",
1829 );
1830
1831 assert_eq!(
1832 after_rotation
1833 .open_app_data(&sealed_under_1, TEST_AAD)
1834 .unwrap(),
1835 b"sealed before rotating",
1836 "the rotated keyring still opens what the old generation sealed",
1837 );
1838 assert_eq!(
1839 after_rotation
1840 .open_app_data(&sealed_under_2, TEST_AAD)
1841 .unwrap(),
1842 b"sealed after rotating",
1843 );
1844 }
1845
1846 #[test]
1850 fn open_app_data_rejects_a_key_the_keyring_lacks() {
1851 let rotated = create_test_service()
1852 .with_appended_generation(2, [9u8; 32])
1853 .expect("rotate the keyring");
1854 let sealed_under_2 = rotated.seal_app_data(b"sealed under the rotated key", TEST_AAD);
1855
1856 let fresh_single_key = EncryptionService::from_key([7u8; 32]);
1857 let error = fresh_single_key
1858 .open_app_data(&sealed_under_2, TEST_AAD)
1859 .expect_err("a keyring without the sealing key must not open it");
1860
1861 assert!(matches!(error, SealError::UnknownKey(_)), "{error:?}");
1862 }
1863
1864 #[test]
1874 fn sealed_app_data_v1_fixture_still_opens() {
1875 let sealed = hex::decode(APP_DATA_V1_FIXTURE_HEX).expect("the fixture is valid hex");
1876
1877 assert_eq!(sealed[0], APP_DATA_SEAL_VERSION, "a version-1 payload");
1878 assert_eq!(
1879 sealed_fingerprint(&sealed),
1880 EncryptionService::from_key(test_key()).seal_fingerprint(),
1881 );
1882
1883 let opened = EncryptionService::from_key(test_key())
1884 .open_app_data(&sealed, APP_DATA_V1_FIXTURE_AAD)
1885 .expect("the pinned v1 payload must keep opening");
1886
1887 assert_eq!(opened, APP_DATA_V1_FIXTURE_PLAINTEXT);
1888 }
1889}