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 = 8;
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; 8]);
43
44impl KeyFingerprint {
45 pub fn from_bytes(bytes: [u8; 8]) -> Self {
46 Self(bytes)
47 }
48
49 pub fn as_bytes(&self) -> &[u8; 8] {
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() != 16
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; 8] = 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 sixteen 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)]
243struct KeyEntry {
244 generation: u64,
245 key: [u8; 32],
246}
247
248#[derive(Clone)]
253pub struct MasterKeyring {
254 keys: BTreeMap<[u8; 8], 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 {
289 EncryptionService::from(self.clone()).fingerprint()
290 }
291}
292
293impl From<EncryptionService> for MasterKeyring {
294 fn from(service: EncryptionService) -> Self {
295 Self { keys: service.keys }
296 }
297}
298
299impl From<MasterKeyring> for EncryptionService {
300 fn from(keyring: MasterKeyring) -> Self {
301 EncryptionService { keys: keyring.keys }
302 }
303}
304
305fn key_fingerprint_bytes(key: &[u8; 32]) -> [u8; 8] {
308 let hash = Sha256::digest(key);
309 let mut fingerprint = [0u8; 8];
310 fingerprint.copy_from_slice(&hash[..8]);
311 fingerprint
312}
313
314#[derive(Clone)]
320pub struct EncryptionService {
321 keys: BTreeMap<[u8; 8], KeyEntry>,
329}
330impl std::fmt::Debug for EncryptionService {
331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332 f.debug_struct("EncryptionService")
333 .field("keys", &"<redacted>")
334 .finish()
335 }
336}
337impl EncryptionService {
338 pub fn new(stored_key: &str) -> Result<Self, EncryptionError> {
340 info!("Loading master key...");
341 EncryptionService::from_keyring_json(stored_key)
342 }
343
344 pub fn from_key(key: [u8; 32]) -> Self {
346 Self::from_key_at_generation(INITIAL_KEY_GENERATION, key)
347 }
348
349 pub fn from_key_at_generation(generation: u64, key: [u8; 32]) -> Self {
350 let mut keys = BTreeMap::new();
351 keys.insert(key_fingerprint_bytes(&key), KeyEntry { generation, key });
352 EncryptionService { keys }
353 }
354
355 pub fn from_keyring(
356 keys: impl IntoIterator<Item = (u64, [u8; 32])>,
357 ) -> Result<Self, EncryptionError> {
358 let keys: BTreeMap<[u8; 8], KeyEntry> = keys
359 .into_iter()
360 .map(|(generation, key)| (key_fingerprint_bytes(&key), KeyEntry { generation, key }))
361 .collect();
362 if keys.is_empty() {
363 return Err(EncryptionError::KeyManagement(
364 "keyring has no keys".to_string(),
365 ));
366 }
367 Ok(EncryptionService { keys })
368 }
369
370 fn seal_entry(&self) -> (&[u8; 8], &KeyEntry) {
376 self.keys
377 .iter()
378 .max_by(|(fingerprint_a, a), (fingerprint_b, b)| {
379 a.generation
380 .cmp(&b.generation)
381 .then_with(|| fingerprint_a.cmp(fingerprint_b))
382 })
383 .expect("a keyring always holds at least one key")
384 }
385
386 pub fn current_generation(&self) -> u64 {
387 self.seal_entry().1.generation
388 }
389
390 pub fn key_count(&self) -> usize {
393 self.keys.len()
394 }
395
396 pub fn keyring_entries(&self) -> Vec<(u64, [u8; 32])> {
397 self.keys
398 .values()
399 .map(|entry| (entry.generation, entry.key))
400 .collect()
401 }
402
403 pub fn merged_with(&self, other: &EncryptionService) -> EncryptionService {
408 let mut keys = self.keys.clone();
409 for (fingerprint, entry) in &other.keys {
410 keys.entry(*fingerprint).or_insert_with(|| entry.clone());
411 }
412 EncryptionService { keys }
413 }
414
415 pub fn to_keyring_string(&self) -> Result<String, EncryptionError> {
416 let payload = StoredKeyring {
417 keys: self
418 .keys
419 .values()
420 .map(|entry| StoredKeyringGeneration {
421 generation: entry.generation,
422 key_hex: hex::encode(entry.key),
423 })
424 .collect(),
425 };
426 serde_json::to_string(&payload)
427 .map_err(|e| EncryptionError::KeyManagement(format!("serialize keyring: {e}")))
428 }
429
430 pub fn to_keyring_payload(&self) -> Result<Vec<u8>, EncryptionError> {
431 self.to_keyring_string().map(String::into_bytes)
432 }
433
434 pub fn from_keyring_payload(plaintext: Vec<u8>) -> Result<Self, EncryptionError> {
435 let keyring = String::from_utf8(plaintext).map_err(|e| {
436 EncryptionError::KeyManagement(format!("keyring payload is not UTF-8: {e}"))
437 })?;
438 EncryptionService::from_keyring_json(&keyring)
439 }
440
441 fn from_keyring_json(keyring: &str) -> Result<Self, EncryptionError> {
442 let payload: StoredKeyring = serde_json::from_str(keyring).map_err(|e| {
443 EncryptionError::KeyManagement(format!("keyring JSON is malformed: {e}"))
444 })?;
445 let mut keys = Vec::with_capacity(payload.keys.len());
446 for entry in payload.keys {
447 let key: [u8; 32] = hex::decode(&entry.key_hex)
448 .map_err(|e| {
449 EncryptionError::KeyManagement(format!("keyring key is not hex: {e}"))
450 })?
451 .try_into()
452 .map_err(|_| {
453 EncryptionError::KeyManagement("keyring key is not 32 bytes".to_string())
454 })?;
455 keys.push((entry.generation, key));
456 }
457 EncryptionService::from_keyring(keys)
458 }
459
460 pub fn key_for_fingerprint(&self, fingerprint: &[u8; 8]) -> Result<[u8; 32], EncryptionError> {
464 self.keys.get(fingerprint).map(|e| e.key).ok_or_else(|| {
465 EncryptionError::KeyManagement(format!(
466 "no key with fingerprint {}",
467 hex::encode(fingerprint)
468 ))
469 })
470 }
471
472 pub fn service_for_fingerprint(
473 &self,
474 fingerprint: &[u8; 8],
475 ) -> Result<EncryptionService, EncryptionError> {
476 let key = self.key_for_fingerprint(fingerprint)?;
477 let generation = self
481 .keys
482 .get(fingerprint)
483 .expect("just resolved")
484 .generation;
485 Ok(EncryptionService::from_key_at_generation(generation, key))
486 }
487
488 pub fn with_appended_generation(
489 &self,
490 generation: u64,
491 key: [u8; 32],
492 ) -> Result<EncryptionService, EncryptionError> {
493 if generation <= self.current_generation() {
494 return Err(EncryptionError::KeyManagement(format!(
495 "new generation {generation} must be greater than current generation {}",
496 self.current_generation()
497 )));
498 }
499 let mut keys = self.keys.clone();
500 keys.insert(key_fingerprint_bytes(&key), KeyEntry { generation, key });
501 Ok(EncryptionService { keys })
502 }
503
504 pub fn fingerprint(&self) -> String {
507 hex::encode(self.seal_fingerprint())
508 }
509
510 pub fn seal_fingerprint(&self) -> [u8; 8] {
513 *self.seal_entry().0
514 }
515
516 pub fn seal_key_fingerprint(&self) -> KeyFingerprint {
517 KeyFingerprint::from_bytes(self.seal_fingerprint())
518 }
519
520 pub fn key_bytes(&self) -> [u8; 32] {
522 self.seal_entry().1.key
523 }
524
525 pub fn encrypt(&self, plaintext: &[u8], aad_context: &[u8]) -> Vec<u8> {
530 let mut sealer = self.sealer(plaintext.len() as u64, aad_context);
531 let mut output = sealer.base_nonce().to_vec();
532
533 if plaintext.is_empty() {
535 output.extend(sealer.seal_chunk(&[]));
536 return output;
537 }
538
539 for chunk in plaintext.chunks(CHUNK_SIZE) {
540 output.extend(sealer.seal_chunk(chunk));
541 }
542
543 output
544 }
545
546 pub fn decrypt(
548 &self,
549 encrypted_data: &[u8],
550 aad_context: &[u8],
551 ) -> Result<Vec<u8>, EncryptionError> {
552 let base_nonce = read_base_nonce(encrypted_data)?;
553 let layout = encrypted_chunk_layout(encrypted_data.len())?;
554 let key = self.key_bytes();
555 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
556
557 let mut result = Vec::with_capacity(decrypted_len_upper_bound(layout.data_len));
558 for chunk_index in 0..layout.total_chunks {
559 let (chunk_start, chunk_end) =
560 layout.chunk_bounds(encrypted_data.len(), chunk_index)?;
561 let chunk_data = &encrypted_data[chunk_start..chunk_end];
562 let decrypted = decrypt_chunk_with_cipher(
563 &cipher,
564 &base_nonce,
565 aad_context,
566 chunk_index as u64,
567 layout.total_chunks as u64,
568 chunk_data,
569 )
570 .map_err(|_| {
571 EncryptionError::Decryption(format!(
572 "Authentication failed for chunk {}",
573 chunk_index
574 ))
575 })?;
576 result.extend(decrypted);
577 }
578
579 Ok(result)
580 }
581
582 pub fn sealer(&self, plaintext_len: u64, aad_context: &[u8]) -> ChunkSealer {
585 ChunkSealer::new(&self.key_bytes(), plaintext_len, aad_context)
586 }
587
588 pub fn decrypt_chunk(
591 &self,
592 ciphertext: &[u8],
593 chunk_index: usize,
594 aad_context: &[u8],
595 ) -> Result<Vec<u8>, EncryptionError> {
596 let base_nonce = read_base_nonce(ciphertext)?;
597 let layout = encrypted_chunk_layout(ciphertext.len())?;
598 let (chunk_start, chunk_end) = layout.chunk_bounds(ciphertext.len(), chunk_index)?;
599 let chunk_data = &ciphertext[chunk_start..chunk_end];
600
601 let key = self.key_bytes();
602 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
603 decrypt_chunk_with_cipher(
604 &cipher,
605 &base_nonce,
606 aad_context,
607 chunk_index as u64,
608 layout.total_chunks as u64,
609 chunk_data,
610 )
611 .map_err(|_| EncryptionError::Decryption("Authentication failed".to_string()))
612 }
613
614 pub fn decrypt_range_with_offset(
627 &self,
628 nonce: &[u8],
629 encrypted_chunks: &[u8],
630 first_chunk_index: u64,
631 plaintext_start: u64,
632 plaintext_end: u64,
633 source_size: u64,
634 aad_context: &[u8],
635 ) -> Result<Vec<u8>, EncryptionError> {
636 if nonce.len() != NONCE_SIZE {
637 return Err(EncryptionError::Decryption(format!(
638 "Invalid nonce length: expected {}, got {}",
639 NONCE_SIZE,
640 nonce.len()
641 )));
642 }
643
644 if plaintext_start >= plaintext_end {
645 return Err(EncryptionError::Decryption(format!(
646 "Invalid range: start ({}) >= end ({})",
647 plaintext_start, plaintext_end
648 )));
649 }
650 if plaintext_end > source_size {
651 return Err(EncryptionError::Decryption(format!(
652 "Invalid range: end ({plaintext_end}) > source size ({source_size})"
653 )));
654 }
655
656 let base_nonce: [u8; NONCE_SIZE] = nonce
657 .try_into()
658 .map_err(|_| EncryptionError::Decryption("Invalid nonce".to_string()))?;
659
660 let key = self.key_bytes();
661 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
662
663 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
664 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
665 let total_chunks = chunk_count_for_plaintext_len(source_size);
666
667 let mut plaintext = Vec::new();
668
669 for absolute_chunk_idx in start_chunk..=end_chunk {
670 let relative_idx = absolute_chunk_idx - first_chunk_index;
672 let chunk_start = (relative_idx as usize) * ENCRYPTED_CHUNK_SIZE;
673
674 let chunk_end = if chunk_start + ENCRYPTED_CHUNK_SIZE > encrypted_chunks.len() {
676 encrypted_chunks.len()
677 } else {
678 chunk_start + ENCRYPTED_CHUNK_SIZE
679 };
680
681 if chunk_start >= encrypted_chunks.len() {
682 return Err(EncryptionError::Decryption(format!(
683 "Chunk {} not in provided data (first_chunk_index={})",
684 absolute_chunk_idx, first_chunk_index
685 )));
686 }
687
688 let chunk_data = &encrypted_chunks[chunk_start..chunk_end];
689 let decrypted = decrypt_chunk_with_cipher(
690 &cipher,
691 &base_nonce,
692 aad_context,
693 absolute_chunk_idx,
694 total_chunks,
695 chunk_data,
696 )
697 .map_err(|_| {
698 EncryptionError::Decryption(format!(
699 "Authentication failed for chunk {}",
700 absolute_chunk_idx
701 ))
702 })?;
703
704 plaintext.extend(decrypted);
705 }
706
707 let offset_in_first_chunk = (plaintext_start % CHUNK_SIZE as u64) as usize;
709 let len = (plaintext_end - plaintext_start) as usize;
710 let end = offset_in_first_chunk + len;
711
712 if end > plaintext.len() {
713 return Err(EncryptionError::Decryption(format!(
714 "Decrypted data too short: need {} bytes, got {}",
715 end,
716 plaintext.len()
717 )));
718 }
719
720 Ok(plaintext[offset_in_first_chunk..end].to_vec())
721 }
722
723 pub fn derive_scoped(&self, scope_id: &str) -> EncryptionService {
728 let derived = self.derive_key(&format!("coven-scope-v1:{scope_id}"));
729 EncryptionService::from_key_at_generation(self.current_generation(), derived)
730 }
731
732 pub fn derive_scoped_for_fingerprint(
733 &self,
734 fingerprint: &[u8; 8],
735 scope_id: &str,
736 ) -> Result<EncryptionService, EncryptionError> {
737 let key = self.key_for_fingerprint(fingerprint)?;
738 let generation = self
739 .keys
740 .get(fingerprint)
741 .expect("just resolved")
742 .generation;
743 let derived = derive_key_from(&key, &format!("coven-scope-v1:{scope_id}"));
744 Ok(EncryptionService::from_key_at_generation(
745 generation, derived,
746 ))
747 }
748
749 pub fn derive_key(&self, info: &str) -> [u8; 32] {
759 derive_key_from(&self.key_bytes(), info)
760 }
761
762 pub fn seal_app_data(&self, plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
780 let fingerprint = self.seal_fingerprint();
781 let mut sealed = Vec::with_capacity(
782 APP_DATA_HEADER_SIZE + chunked_encrypted_len(plaintext.len() as u64) as usize,
783 );
784 sealed.push(APP_DATA_SEAL_VERSION);
785 sealed.extend_from_slice(&fingerprint);
786 sealed.extend(self.encrypt(plaintext, aad));
787 sealed
788 }
789
790 pub fn open_app_data(&self, sealed: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
796 let (fingerprint, ciphertext) = split_sealed_app_data(sealed)?;
797 self.service_for_fingerprint(&fingerprint)
798 .map_err(|_| SealError::UnknownKey(hex::encode(fingerprint)))?
801 .decrypt(ciphertext, aad)
802 .map_err(SealError::Crypto)
803 }
804}
805
806fn split_sealed_app_data(sealed: &[u8]) -> Result<([u8; 8], &[u8]), SealError> {
813 let (&version, rest) = sealed.split_first().ok_or_else(|| {
814 SealError::Crypto(EncryptionError::Decryption(
815 "sealed app-data payload is empty".to_string(),
816 ))
817 })?;
818 if version != APP_DATA_SEAL_VERSION {
819 return Err(SealError::UnknownVersion(version));
820 }
821 if rest.len() < APP_DATA_FINGERPRINT_SIZE {
822 return Err(SealError::Crypto(EncryptionError::Decryption(
823 "sealed app-data payload is truncated before its key fingerprint".to_string(),
824 )));
825 }
826 let (fingerprint, ciphertext) = rest.split_at(APP_DATA_FINGERPRINT_SIZE);
827 let fingerprint: [u8; 8] = fingerprint
828 .try_into()
829 .expect("split_at yields exactly APP_DATA_FINGERPRINT_SIZE bytes");
830 Ok((fingerprint, ciphertext))
831}
832
833fn derive_key_from(key: &[u8; 32], info: &str) -> [u8; 32] {
834 let hk = Hkdf::<Sha256>::new(Some(b"coven-hkdf-salt-v1"), key);
835 let mut okm = [0u8; 32];
836 hk.expand(info.as_bytes(), &mut okm)
837 .expect("32 bytes is a valid HKDF output length");
838 okm
839}
840
841#[derive(Clone, Copy)]
842struct EncryptedChunkLayout {
843 data_len: usize,
844 total_chunks: usize,
845 has_partial: bool,
846}
847
848impl EncryptedChunkLayout {
849 fn chunk_bounds(
850 self,
851 ciphertext_len: usize,
852 chunk_index: usize,
853 ) -> Result<(usize, usize), EncryptionError> {
854 if chunk_index >= self.total_chunks {
855 return Err(EncryptionError::Decryption(format!(
856 "Chunk index {} out of range (total chunks: {})",
857 chunk_index, self.total_chunks
858 )));
859 }
860
861 let chunk_start = NONCE_SIZE + chunk_index * ENCRYPTED_CHUNK_SIZE;
862 let chunk_end = if chunk_index == self.total_chunks - 1 && self.has_partial {
863 ciphertext_len
864 } else {
865 chunk_start + ENCRYPTED_CHUNK_SIZE
866 };
867 Ok((chunk_start, chunk_end))
868 }
869}
870
871fn read_base_nonce(ciphertext: &[u8]) -> Result<[u8; NONCE_SIZE], EncryptionError> {
872 if ciphertext.len() < NONCE_SIZE {
873 return Err(EncryptionError::Decryption(
874 "Ciphertext too short for nonce".to_string(),
875 ));
876 }
877
878 let mut base_nonce = [0u8; NONCE_SIZE];
879 base_nonce.copy_from_slice(&ciphertext[..NONCE_SIZE]);
880 Ok(base_nonce)
881}
882
883fn encrypted_chunk_layout(ciphertext_len: usize) -> Result<EncryptedChunkLayout, EncryptionError> {
884 if ciphertext_len < NONCE_SIZE {
885 return Err(EncryptionError::Decryption(
886 "Ciphertext too short for nonce".to_string(),
887 ));
888 }
889
890 let data_len = ciphertext_len - NONCE_SIZE;
891 let num_full_chunks = data_len / ENCRYPTED_CHUNK_SIZE;
892 let has_partial = !data_len.is_multiple_of(ENCRYPTED_CHUNK_SIZE);
893 let total_chunks = num_full_chunks + usize::from(has_partial);
894 Ok(EncryptedChunkLayout {
895 data_len,
896 total_chunks,
897 has_partial,
898 })
899}
900
901fn decrypted_len_upper_bound(encrypted_data_len: usize) -> usize {
902 let chunk_count = encrypted_data_len.div_ceil(ENCRYPTED_CHUNK_SIZE);
903 encrypted_data_len.saturating_sub(chunk_count * TAG_SIZE)
904}
905
906fn chunk_count_for_plaintext_len(plaintext_len: u64) -> u64 {
907 plaintext_len.div_ceil(CHUNK_SIZE as u64).max(1)
908}
909
910fn chunk_aad(aad_context: &[u8], chunk_index: u64, total_chunks: u64) -> Vec<u8> {
911 let mut aad = Vec::with_capacity(AEAD_V2_LABEL.len() + 8 + aad_context.len() + 16);
912 aad.extend_from_slice(AEAD_V2_LABEL);
913 aad.extend_from_slice(&(aad_context.len() as u64).to_le_bytes());
914 aad.extend_from_slice(aad_context);
915 aad.extend_from_slice(&chunk_index.to_le_bytes());
916 aad.extend_from_slice(&total_chunks.to_le_bytes());
917 aad
918}
919
920fn decrypt_chunk_with_cipher(
921 cipher: &XChaCha20Poly1305,
922 base_nonce: &[u8; NONCE_SIZE],
923 aad_context: &[u8],
924 chunk_index: u64,
925 total_chunks: u64,
926 chunk_data: &[u8],
927) -> Result<Vec<u8>, ()> {
928 let nonce = chunk_nonce(base_nonce, chunk_index);
929 let nonce_arr = GenericArray::from_slice(&nonce);
930 let aad = chunk_aad(aad_context, chunk_index, total_chunks);
931 cipher
932 .decrypt(
933 nonce_arr,
934 Payload {
935 msg: chunk_data,
936 aad: &aad,
937 },
938 )
939 .map_err(|_| ())
940}
941
942fn chunk_nonce(base_nonce: &[u8; NONCE_SIZE], chunk_index: u64) -> [u8; NONCE_SIZE] {
944 let mut nonce = *base_nonce;
945 let index_bytes = chunk_index.to_le_bytes();
946 for i in 0..8 {
947 nonce[i] ^= index_bytes[i];
948 }
949 nonce
950}
951
952pub fn encrypted_chunk_range(plaintext_start: u64, plaintext_end: u64) -> (u64, u64) {
960 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
961 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
962
963 let chunk_start = NONCE_SIZE as u64 + start_chunk * ENCRYPTED_CHUNK_SIZE as u64;
964 let chunk_end = NONCE_SIZE as u64 + (end_chunk + 1) * ENCRYPTED_CHUNK_SIZE as u64;
965
966 (chunk_start, chunk_end)
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972
973 const TEST_AAD: &[u8] = b"encryption-test-context";
974
975 fn test_key() -> [u8; 32] {
976 [
978 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
979 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
980 0x1c, 0x1d, 0x1e, 0x1f,
981 ]
982 }
983
984 fn create_test_service() -> EncryptionService {
985 EncryptionService::from_key(test_key())
986 }
987
988 fn decrypt_plaintext_range(
989 service: &EncryptionService,
990 full_ciphertext: &[u8],
991 source_size: u64,
992 plaintext_start: u64,
993 plaintext_end: u64,
994 ) -> Vec<u8> {
995 let nonce = &full_ciphertext[..NONCE_SIZE];
996 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
997 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
998 let first_chunk_index = (chunk_start - NONCE_SIZE as u64) / ENCRYPTED_CHUNK_SIZE as u64;
999 service
1000 .decrypt_range_with_offset(
1001 nonce,
1002 chunks_only,
1003 first_chunk_index,
1004 plaintext_start,
1005 plaintext_end,
1006 source_size,
1007 TEST_AAD,
1008 )
1009 .unwrap()
1010 }
1011
1012 #[test]
1013 fn test_roundtrip_small() {
1014 let service = create_test_service();
1015 let plaintext = b"Hello, world!";
1016
1017 let ciphertext = service.encrypt(plaintext, TEST_AAD);
1018 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1019
1020 assert_eq!(decrypted, plaintext);
1021 }
1022
1023 #[test]
1029 fn streaming_sealer_matches_whole_buffer_format() {
1030 let service = create_test_service();
1031 for len in [
1032 0usize,
1033 1,
1034 CHUNK_SIZE - 1,
1035 CHUNK_SIZE,
1036 CHUNK_SIZE + 1,
1037 200_000,
1038 ] {
1039 let plaintext: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
1040
1041 let mut sealer = service.sealer(plaintext.len() as u64, TEST_AAD);
1043 let mut streamed = sealer.base_nonce().to_vec();
1044 if plaintext.is_empty() {
1045 streamed.extend(sealer.seal_chunk(&[]));
1046 } else {
1047 for chunk in plaintext.chunks(CHUNK_SIZE) {
1048 streamed.extend(sealer.seal_chunk(chunk));
1049 }
1050 }
1051
1052 assert_eq!(
1053 streamed.len() as u64,
1054 chunked_encrypted_len(len as u64),
1055 "predicted length wrong for len={len}"
1056 );
1057 assert_eq!(
1058 service.decrypt(&streamed, TEST_AAD).unwrap(),
1059 plaintext,
1060 "streamed ciphertext failed to round-trip for len={len}"
1061 );
1062 }
1063 }
1064
1065 #[test]
1069 fn chunked_encrypted_len_matches_encrypt() {
1070 let service = create_test_service();
1071 for n in [
1072 0usize,
1073 1,
1074 CHUNK_SIZE - 1,
1075 CHUNK_SIZE,
1076 CHUNK_SIZE + 1,
1077 200_000,
1078 ] {
1079 let produced = service.encrypt(&vec![0u8; n], TEST_AAD).len() as u64;
1080 assert_eq!(
1081 chunked_encrypted_len(n as u64),
1082 produced,
1083 "predicted length wrong for n={n}"
1084 );
1085 }
1086 }
1087
1088 #[test]
1089 fn test_roundtrip_exact_chunk() {
1090 let service = create_test_service();
1091 let plaintext = vec![0x42u8; CHUNK_SIZE];
1092
1093 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1094 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1095
1096 assert_eq!(decrypted, plaintext);
1097 }
1098
1099 #[test]
1100 fn test_roundtrip_multiple_chunks() {
1101 let service = create_test_service();
1102 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 2 + CHUNK_SIZE / 2)
1104 .map(|i| (i % 256) as u8)
1105 .collect();
1106
1107 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1108 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1109
1110 assert_eq!(decrypted, plaintext);
1111 }
1112
1113 #[test]
1114 fn test_random_access_chunk() {
1115 let service = create_test_service();
1116 let mut plaintext = vec![0x00u8; CHUNK_SIZE];
1118 plaintext.extend(vec![0x11u8; CHUNK_SIZE]);
1119 plaintext.extend(vec![0x22u8; CHUNK_SIZE]);
1120
1121 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1122
1123 let chunk1 = service.decrypt_chunk(&ciphertext, 1, TEST_AAD).unwrap();
1125 assert_eq!(chunk1, vec![0x11u8; CHUNK_SIZE]);
1126
1127 let chunk0 = service.decrypt_chunk(&ciphertext, 0, TEST_AAD).unwrap();
1129 assert_eq!(chunk0, vec![0x00u8; CHUNK_SIZE]);
1130
1131 let chunk2 = service.decrypt_chunk(&ciphertext, 2, TEST_AAD).unwrap();
1133 assert_eq!(chunk2, vec![0x22u8; CHUNK_SIZE]);
1134 }
1135
1136 #[test]
1137 fn test_random_access_partial_last_chunk() {
1138 let service = create_test_service();
1139 let mut plaintext = vec![0xAAu8; CHUNK_SIZE];
1141 plaintext.extend(vec![0xBBu8; 100]);
1142
1143 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1144
1145 let chunk0 = service.decrypt_chunk(&ciphertext, 0, TEST_AAD).unwrap();
1146 assert_eq!(chunk0, vec![0xAAu8; CHUNK_SIZE]);
1147
1148 let chunk1 = service.decrypt_chunk(&ciphertext, 1, TEST_AAD).unwrap();
1149 assert_eq!(chunk1, vec![0xBBu8; 100]);
1150 }
1151
1152 #[test]
1153 fn test_tamper_detection() {
1154 let service = create_test_service();
1155 let plaintext = b"Secret data";
1156
1157 let mut ciphertext = service.encrypt(plaintext, TEST_AAD);
1158
1159 let tamper_pos = NONCE_SIZE + 5;
1161 ciphertext[tamper_pos] ^= 0xFF;
1162
1163 let result = service.decrypt(&ciphertext, TEST_AAD);
1164 assert!(result.is_err());
1165 }
1166
1167 #[test]
1168 fn truncating_trailing_chunks_fails_to_decrypt() {
1169 let service = create_test_service();
1170 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 251) as u8).collect();
1171 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1172 let truncated = &ciphertext[..ciphertext.len() - ENCRYPTED_CHUNK_SIZE];
1173
1174 assert!(
1175 service.decrypt(truncated, TEST_AAD).is_err(),
1176 "a truncated multi-chunk object must fail, not return a short plaintext",
1177 );
1178 }
1179
1180 #[test]
1181 fn test_empty_plaintext() {
1182 let service = create_test_service();
1183 let plaintext = b"";
1184
1185 let ciphertext = service.encrypt(plaintext, TEST_AAD);
1186
1187 assert_eq!(ciphertext.len(), NONCE_SIZE + TAG_SIZE);
1189
1190 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1191 assert_eq!(decrypted, plaintext);
1192 }
1193
1194 #[test]
1195 fn test_single_byte() {
1196 let service = create_test_service();
1197 let plaintext = b"x";
1198
1199 let ciphertext = service.encrypt(plaintext, TEST_AAD);
1200 let decrypted = service.decrypt(&ciphertext, TEST_AAD).unwrap();
1201
1202 assert_eq!(decrypted, plaintext);
1203 }
1204
1205 #[test]
1206 fn test_encrypted_range_single_chunk() {
1207 let (start, end) = encrypted_chunk_range(0, 100);
1209
1210 assert_eq!(start, NONCE_SIZE as u64);
1211 assert_eq!(end, NONCE_SIZE as u64 + ENCRYPTED_CHUNK_SIZE as u64);
1212 }
1213
1214 #[test]
1215 fn test_encrypted_range_spans_chunks() {
1216 let (start, end) = encrypted_chunk_range(CHUNK_SIZE as u64 - 10, CHUNK_SIZE as u64 + 10);
1218
1219 assert_eq!(start, NONCE_SIZE as u64);
1220 assert_eq!(end, NONCE_SIZE as u64 + 2 * ENCRYPTED_CHUNK_SIZE as u64);
1221 }
1222
1223 #[test]
1224 fn test_encrypted_range_middle_chunk() {
1225 let chunk2_start = CHUNK_SIZE as u64 * 2;
1227 let (start, end) = encrypted_chunk_range(chunk2_start + 10, chunk2_start + 100);
1228
1229 assert_eq!(start, NONCE_SIZE as u64 + 2 * ENCRYPTED_CHUNK_SIZE as u64);
1230 assert_eq!(end, NONCE_SIZE as u64 + 3 * ENCRYPTED_CHUNK_SIZE as u64);
1231 }
1232
1233 #[test]
1234 fn test_different_encryptions_different_ciphertext() {
1235 let service = create_test_service();
1236 let plaintext = b"Same message";
1237
1238 let ciphertext1 = service.encrypt(plaintext, TEST_AAD);
1239 let ciphertext2 = service.encrypt(plaintext, TEST_AAD);
1240
1241 assert_ne!(ciphertext1, ciphertext2);
1243
1244 assert_eq!(service.decrypt(&ciphertext1, TEST_AAD).unwrap(), plaintext);
1246 assert_eq!(service.decrypt(&ciphertext2, TEST_AAD).unwrap(), plaintext);
1247 }
1248
1249 #[test]
1250 fn test_chunk_index_out_of_range() {
1251 let service = create_test_service();
1252 let plaintext = vec![0u8; CHUNK_SIZE]; let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1255
1256 assert!(service.decrypt_chunk(&ciphertext, 0, TEST_AAD).is_ok());
1258
1259 assert!(service.decrypt_chunk(&ciphertext, 1, TEST_AAD).is_err());
1261 }
1262
1263 #[test]
1264 fn test_decrypt_range_within_single_chunk() {
1265 let service = create_test_service();
1266 let plaintext: Vec<u8> = (0..CHUNK_SIZE).map(|i| (i % 256) as u8).collect();
1268
1269 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1270
1271 let decrypted =
1272 decrypt_plaintext_range(&service, &ciphertext, plaintext.len() as u64, 100, 200);
1273
1274 assert_eq!(decrypted.len(), 100);
1275 assert_eq!(decrypted, plaintext[100..200]);
1276 }
1277
1278 #[test]
1279 fn test_decrypt_range_spanning_chunks() {
1280 let service = create_test_service();
1281 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 256) as u8).collect();
1283
1284 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1285
1286 let start = CHUNK_SIZE as u64 - 100;
1288 let end = CHUNK_SIZE as u64 + 100;
1289 let decrypted =
1290 decrypt_plaintext_range(&service, &ciphertext, plaintext.len() as u64, start, end);
1291
1292 assert_eq!(decrypted.len(), 200);
1293 assert_eq!(decrypted, &plaintext[start as usize..end as usize]);
1294 }
1295
1296 #[test]
1297 fn test_decrypt_range_entire_middle_chunk() {
1298 let service = create_test_service();
1299 let mut plaintext = vec![0xAAu8; CHUNK_SIZE];
1301 plaintext.extend(vec![0xBBu8; CHUNK_SIZE]);
1302 plaintext.extend(vec![0xCCu8; CHUNK_SIZE]);
1303
1304 let ciphertext = service.encrypt(&plaintext, TEST_AAD);
1305
1306 let start = CHUNK_SIZE as u64;
1308 let end = (CHUNK_SIZE * 2) as u64;
1309 let decrypted =
1310 decrypt_plaintext_range(&service, &ciphertext, plaintext.len() as u64, start, end);
1311
1312 assert_eq!(decrypted, vec![0xBBu8; CHUNK_SIZE]);
1313 }
1314
1315 #[test]
1316 fn test_decrypt_range_with_partial_encrypted_data() {
1317 let service = create_test_service();
1318 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 256) as u8).collect();
1320 let full_ciphertext = service.encrypt(&plaintext, TEST_AAD);
1321
1322 let plaintext_start = CHUNK_SIZE as u64 + 100;
1324 let plaintext_end = CHUNK_SIZE as u64 + 200;
1325 let nonce = &full_ciphertext[..NONCE_SIZE];
1326 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1327 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1328 let first_chunk_index = (chunk_start - NONCE_SIZE as u64) / ENCRYPTED_CHUNK_SIZE as u64;
1329 let decrypted = service
1330 .decrypt_range_with_offset(
1331 nonce,
1332 chunks_only,
1333 first_chunk_index,
1334 plaintext_start,
1335 plaintext_end,
1336 plaintext.len() as u64,
1337 TEST_AAD,
1338 )
1339 .unwrap();
1340
1341 assert_eq!(decrypted.len(), 100);
1342 assert_eq!(
1343 decrypted,
1344 &plaintext[plaintext_start as usize..plaintext_end as usize]
1345 );
1346 }
1347
1348 #[test]
1349 fn test_encrypted_chunk_range_returns_actual_bounds() {
1350 let chunk5_start = CHUNK_SIZE as u64 * 5;
1353 let chunk5_end = chunk5_start + 1000;
1354
1355 let (enc_start, enc_end) = encrypted_chunk_range(chunk5_start, chunk5_end);
1356
1357 let expected_start = NONCE_SIZE as u64 + 5 * ENCRYPTED_CHUNK_SIZE as u64;
1359 let expected_end = NONCE_SIZE as u64 + 6 * ENCRYPTED_CHUNK_SIZE as u64;
1360
1361 assert_eq!(
1362 enc_start, expected_start,
1363 "encrypted_chunk_range should return actual chunk start, not 0"
1364 );
1365 assert_eq!(enc_end, expected_end);
1366 }
1367
1368 #[test]
1369 fn test_encrypted_chunk_range_spanning_multiple_chunks() {
1370 let start = CHUNK_SIZE as u64 * 3 + 100;
1372 let end = CHUNK_SIZE as u64 * 5 + 500;
1373
1374 let (enc_start, enc_end) = encrypted_chunk_range(start, end);
1375
1376 let expected_start = NONCE_SIZE as u64 + 3 * ENCRYPTED_CHUNK_SIZE as u64;
1377 let expected_end = NONCE_SIZE as u64 + 6 * ENCRYPTED_CHUNK_SIZE as u64;
1378
1379 assert_eq!(enc_start, expected_start);
1380 assert_eq!(enc_end, expected_end);
1381 }
1382
1383 #[test]
1384 fn test_decrypt_range_with_separate_nonce() {
1385 let service = create_test_service();
1387
1388 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 10).map(|i| (i % 256) as u8).collect();
1390 let full_ciphertext = service.encrypt(&plaintext, TEST_AAD);
1391
1392 let nonce = &full_ciphertext[..NONCE_SIZE];
1394
1395 let plaintext_start = CHUNK_SIZE as u64 * 7 + 100;
1397 let plaintext_end = CHUNK_SIZE as u64 * 7 + 500;
1398
1399 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1401
1402 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1404
1405 let first_chunk_index = plaintext_start / CHUNK_SIZE as u64;
1407
1408 let decrypted = service
1410 .decrypt_range_with_offset(
1411 nonce,
1412 chunks_only,
1413 first_chunk_index,
1414 plaintext_start,
1415 plaintext_end,
1416 plaintext.len() as u64,
1417 TEST_AAD,
1418 )
1419 .unwrap();
1420
1421 assert_eq!(decrypted.len(), 400);
1422 assert_eq!(
1423 decrypted,
1424 &plaintext[plaintext_start as usize..plaintext_end as usize]
1425 );
1426 }
1427
1428 #[test]
1429 fn test_decrypt_range_with_offset_spanning_chunks() {
1430 let service = create_test_service();
1432
1433 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 10).map(|i| (i % 256) as u8).collect();
1434 let full_ciphertext = service.encrypt(&plaintext, TEST_AAD);
1435 let nonce = &full_ciphertext[..NONCE_SIZE];
1436
1437 let plaintext_start = CHUNK_SIZE as u64 * 3 + 1000;
1439 let plaintext_end = CHUNK_SIZE as u64 * 5 + 2000;
1440
1441 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
1442 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
1443 let first_chunk_index = plaintext_start / CHUNK_SIZE as u64;
1444
1445 let decrypted = service
1446 .decrypt_range_with_offset(
1447 nonce,
1448 chunks_only,
1449 first_chunk_index,
1450 plaintext_start,
1451 plaintext_end,
1452 plaintext.len() as u64,
1453 TEST_AAD,
1454 )
1455 .unwrap();
1456
1457 let expected_len = (plaintext_end - plaintext_start) as usize;
1458 assert_eq!(decrypted.len(), expected_len);
1459 assert_eq!(
1460 decrypted,
1461 &plaintext[plaintext_start as usize..plaintext_end as usize]
1462 );
1463 }
1464
1465 #[test]
1466 fn test_fingerprint_deterministic() {
1467 let service = create_test_service();
1468 assert_eq!(service.fingerprint(), service.fingerprint());
1469 }
1470
1471 #[test]
1472 fn test_fingerprint_different_keys() {
1473 let service1 = EncryptionService::from_key([0u8; 32]);
1474 let service2 = EncryptionService::from_key([1u8; 32]);
1475 assert_ne!(service1.fingerprint(), service2.fingerprint());
1476 }
1477
1478 #[test]
1479 fn key_fingerprint_wire_form_is_strict_lowercase_hex() {
1480 let fingerprint = create_test_service().seal_key_fingerprint();
1481 let serialized = serde_json::to_string(&fingerprint).expect("serialize fingerprint");
1482 assert_eq!(
1483 serde_json::from_str::<KeyFingerprint>(&serialized).unwrap(),
1484 fingerprint
1485 );
1486 assert!(fingerprint
1487 .to_string()
1488 .to_uppercase()
1489 .parse::<KeyFingerprint>()
1490 .is_err());
1491 }
1492
1493 #[test]
1494 fn derive_scoped_deterministic() {
1495 let service = create_test_service();
1496 let derived1 = service.derive_scoped("rel-123");
1497 let derived2 = service.derive_scoped("rel-123");
1498 assert_eq!(derived1.key_bytes(), derived2.key_bytes());
1499 }
1500
1501 #[test]
1502 fn derive_scoped_different_releases() {
1503 let service = create_test_service();
1504 let key_a = service.derive_scoped("rel-aaa").key_bytes();
1505 let key_b = service.derive_scoped("rel-bbb").key_bytes();
1506 assert_ne!(key_a, key_b);
1507 }
1508
1509 #[test]
1510 fn derive_scoped_different_master_keys() {
1511 let svc1 = EncryptionService::from_key([0u8; 32]);
1512 let svc2 = EncryptionService::from_key([1u8; 32]);
1513 let key1 = svc1.derive_scoped("rel-123").key_bytes();
1514 let key2 = svc2.derive_scoped("rel-123").key_bytes();
1515 assert_ne!(key1, key2);
1516 }
1517
1518 #[test]
1519 fn derive_scoped_roundtrip() {
1520 let master = create_test_service();
1521 let release_enc = master.derive_scoped("rel-456");
1522 let plaintext = b"test audio data for this release";
1523
1524 let encrypted = release_enc.encrypt(plaintext, TEST_AAD);
1525 let decrypted = release_enc.decrypt(&encrypted, TEST_AAD).unwrap();
1526 assert_eq!(decrypted, plaintext);
1527
1528 assert!(master.decrypt(&encrypted, TEST_AAD).is_err());
1530
1531 let wrong_enc = master.derive_scoped("rel-999");
1533 assert!(wrong_enc.decrypt(&encrypted, TEST_AAD).is_err());
1534 }
1535
1536 #[test]
1537 fn master_keyring_from_serialized_accepts_the_current_keyring_format() {
1538 let keyring = MasterKeyring::generate();
1539 let serialized = keyring.to_serialized();
1540 let parsed =
1541 MasterKeyring::from_serialized(&serialized).expect("parse a generated keyring");
1542 assert_eq!(parsed.to_serialized(), serialized);
1543 assert_eq!(parsed.fingerprint(), keyring.fingerprint());
1544 }
1545
1546 #[test]
1547 fn master_keyring_from_serialized_rejects_raw_hex() {
1548 let raw_hex = hex::encode(test_key());
1549 assert!(MasterKeyring::from_serialized(&raw_hex).is_err());
1550 }
1551
1552 #[test]
1553 fn keyring_payload_requires_the_current_json_format() {
1554 let service = create_test_service()
1555 .with_appended_generation(2, [9u8; 32])
1556 .expect("append a generation");
1557 let payload = service
1558 .to_keyring_payload()
1559 .expect("serialize the current keyring payload");
1560 let parsed = EncryptionService::from_keyring_payload(payload)
1561 .expect("parse the current keyring payload");
1562
1563 assert_eq!(parsed.keyring_entries(), service.keyring_entries());
1564 assert!(EncryptionService::from_keyring_payload(test_key().to_vec()).is_err());
1565 }
1566
1567 #[test]
1568 fn master_keyring_and_encryption_service_convert_without_losing_generations() {
1569 let service = EncryptionService::from_key(test_key())
1570 .with_appended_generation(2, [9u8; 32])
1571 .expect("append a generation");
1572 let keyring: MasterKeyring = service.clone().into();
1573 assert_eq!(keyring.fingerprint(), service.fingerprint());
1574 assert_eq!(
1575 keyring.to_serialized(),
1576 service.to_keyring_string().unwrap()
1577 );
1578
1579 let round_tripped: EncryptionService = keyring.into();
1580 assert_eq!(round_tripped.current_generation(), 2);
1581 assert_eq!(round_tripped.keyring_entries(), service.keyring_entries(),);
1582 }
1583
1584 #[test]
1591 fn same_generation_fork_converges_on_one_seal_key_and_keeps_both() {
1592 let base = EncryptionService::from_key([1u8; 32]);
1593 let fork_a = base.with_appended_generation(2, [0xA0u8; 32]).unwrap();
1594 let fork_b = base.with_appended_generation(2, [0xB0u8; 32]).unwrap();
1595
1596 let a_then_b = fork_a.merged_with(&fork_b);
1597 let b_then_a = fork_b.merged_with(&fork_a);
1598 assert_eq!(
1599 a_then_b.fingerprint(),
1600 b_then_a.fingerprint(),
1601 "seal selection is order-independent, so both sides converge on one key",
1602 );
1603 assert_eq!(
1604 a_then_b.key_count(),
1605 3,
1606 "the base key and both forks are held"
1607 );
1608 assert_eq!(a_then_b.current_generation(), 2);
1609
1610 let sealed_a = fork_a.seal_app_data(b"from owner A", b"ctx");
1611 let sealed_b = fork_b.seal_app_data(b"from owner B", b"ctx");
1612 assert_eq!(
1613 a_then_b.open_app_data(&sealed_a, b"ctx").unwrap(),
1614 b"from owner A",
1615 );
1616 assert_eq!(
1617 a_then_b.open_app_data(&sealed_b, b"ctx").unwrap(),
1618 b"from owner B",
1619 );
1620 }
1621
1622 #[test]
1623 fn master_keyring_debug_redacts_keys() {
1624 let keyring = MasterKeyring::generate();
1625 let debug = format!("{keyring:?}");
1626 assert!(debug.contains("<redacted>"), "{debug}");
1627 }
1628
1629 const APP_DATA_V1_FIXTURE_PLAINTEXT: &[u8] = b"pinned app-data payload";
1638 const APP_DATA_V1_FIXTURE_AAD: &[u8] = b"pinned-app-data-context";
1639 const APP_DATA_V1_FIXTURE_HEX: &str = concat!(
1640 "01",
1641 "630dcd2966c43366",
1642 "2bdfe10d13cb397b648c2eb352bbadd92a19eafd8499b5c5",
1643 "b0d1e8eb56f757621ec41a78488c937427aac5df38b5e8af",
1644 "2b2b8c9155ead15242e0c87b00bbe8",
1645 );
1646
1647 fn sealed_fingerprint(sealed: &[u8]) -> [u8; 8] {
1651 sealed[1..9].try_into().expect("a sealed header")
1652 }
1653
1654 #[test]
1655 fn seal_app_data_round_trips_and_records_its_version_and_key() {
1656 let service = create_test_service();
1657 for payload in [b"".as_slice(), b"x", b"a longer app-data secret value"] {
1658 let sealed = service.seal_app_data(payload, TEST_AAD);
1659
1660 assert_eq!(sealed[0], APP_DATA_SEAL_VERSION, "the version byte leads");
1661 assert_eq!(
1662 sealed_fingerprint(&sealed),
1663 service.seal_fingerprint(),
1664 "the header names the key it sealed under",
1665 );
1666 assert_eq!(
1667 sealed.len(),
1668 APP_DATA_HEADER_SIZE + chunked_encrypted_len(payload.len() as u64) as usize,
1669 "the body is exactly the chunked ciphertext, behind the fixed header",
1670 );
1671 assert_eq!(service.open_app_data(&sealed, TEST_AAD).unwrap(), payload);
1672 }
1673 }
1674
1675 #[test]
1678 fn open_app_data_rejects_a_different_aad() {
1679 let service = create_test_service();
1680 let sealed = service.seal_app_data(b"bound to row 42", b"row-42");
1681
1682 let error = service
1683 .open_app_data(&sealed, b"row-99")
1684 .expect_err("a different aad must not open the payload");
1685
1686 assert!(matches!(error, SealError::Crypto(_)), "{error:?}");
1687 }
1688
1689 #[test]
1690 fn open_app_data_rejects_a_flipped_ciphertext_byte() {
1691 let service = create_test_service();
1692 let mut sealed = service.seal_app_data(b"tamper with me", TEST_AAD);
1693 let last = sealed.len() - 1;
1694 sealed[last] ^= 0xFF;
1695
1696 let error = service
1697 .open_app_data(&sealed, TEST_AAD)
1698 .expect_err("a tampered payload must fail authentication");
1699
1700 assert!(matches!(error, SealError::Crypto(_)), "{error:?}");
1701 }
1702
1703 #[test]
1706 fn open_app_data_rejects_an_unknown_version() {
1707 let service = create_test_service();
1708 let mut sealed = service.seal_app_data(b"a version-1 payload", TEST_AAD);
1709 sealed[0] = 2;
1710
1711 let error = service
1712 .open_app_data(&sealed, TEST_AAD)
1713 .expect_err("version 2 must be refused");
1714
1715 assert!(matches!(error, SealError::UnknownVersion(2)), "{error:?}");
1716 }
1717
1718 #[test]
1722 fn open_app_data_survives_rotation_and_each_payload_names_its_key() {
1723 let before_rotation = create_test_service();
1724 let sealed_under_1 = before_rotation.seal_app_data(b"sealed before rotating", TEST_AAD);
1725
1726 let after_rotation = before_rotation
1727 .with_appended_generation(2, [9u8; 32])
1728 .expect("rotate the keyring");
1729 let sealed_under_2 = after_rotation.seal_app_data(b"sealed after rotating", TEST_AAD);
1730
1731 assert_eq!(
1732 sealed_fingerprint(&sealed_under_1),
1733 before_rotation.seal_fingerprint(),
1734 );
1735 assert_eq!(
1736 sealed_fingerprint(&sealed_under_2),
1737 after_rotation.seal_fingerprint(),
1738 "sealing after a rotation records the new seal key",
1739 );
1740
1741 assert_eq!(
1742 after_rotation
1743 .open_app_data(&sealed_under_1, TEST_AAD)
1744 .unwrap(),
1745 b"sealed before rotating",
1746 "the rotated keyring still opens what the old generation sealed",
1747 );
1748 assert_eq!(
1749 after_rotation
1750 .open_app_data(&sealed_under_2, TEST_AAD)
1751 .unwrap(),
1752 b"sealed after rotating",
1753 );
1754 }
1755
1756 #[test]
1760 fn open_app_data_rejects_a_key_the_keyring_lacks() {
1761 let rotated = create_test_service()
1762 .with_appended_generation(2, [9u8; 32])
1763 .expect("rotate the keyring");
1764 let sealed_under_2 = rotated.seal_app_data(b"sealed under the rotated key", TEST_AAD);
1765
1766 let fresh_single_key = EncryptionService::from_key([7u8; 32]);
1767 let error = fresh_single_key
1768 .open_app_data(&sealed_under_2, TEST_AAD)
1769 .expect_err("a keyring without the sealing key must not open it");
1770
1771 assert!(matches!(error, SealError::UnknownKey(_)), "{error:?}");
1772 }
1773
1774 #[test]
1784 fn sealed_app_data_v1_fixture_still_opens() {
1785 let sealed = hex::decode(APP_DATA_V1_FIXTURE_HEX).expect("the fixture is valid hex");
1786
1787 assert_eq!(sealed[0], APP_DATA_SEAL_VERSION, "a version-1 payload");
1788 assert_eq!(
1789 sealed_fingerprint(&sealed),
1790 EncryptionService::from_key(test_key()).seal_fingerprint(),
1791 );
1792
1793 let opened = EncryptionService::from_key(test_key())
1794 .open_app_data(&sealed, APP_DATA_V1_FIXTURE_AAD)
1795 .expect("the pinned v1 payload must keep opening");
1796
1797 assert_eq!(opened, APP_DATA_V1_FIXTURE_PLAINTEXT);
1798 }
1799}