1use chacha20poly1305::aead::generic_array::GenericArray;
2use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305};
3use hkdf::Hkdf;
4use hmac::{Hmac, Mac};
5use rand::RngCore;
6use sha2::{Digest, Sha256};
7use thiserror::Error;
8use tracing::info;
9
10pub const NONCE_SIZE: usize = 24;
12
13pub const TAG_SIZE: usize = 16;
15
16pub const CHUNK_SIZE: usize = 65536;
18pub const ENCRYPTED_CHUNK_SIZE: usize = CHUNK_SIZE + TAG_SIZE;
20
21pub fn generate_random_key() -> [u8; 32] {
23 let mut key = [0u8; 32];
24 rand::rng().fill_bytes(&mut key);
25 key
26}
27
28#[derive(Error, Debug)]
29pub enum EncryptionError {
30 #[error("Encryption failed: {0}")]
31 Encryption(String),
32 #[error("Decryption failed: {0}")]
33 Decryption(String),
34 #[error("Key management error: {0}")]
35 KeyManagement(String),
36 #[error("IO error: {0}")]
37 Io(#[from] std::io::Error),
38}
39#[derive(Clone)]
45pub struct EncryptionService {
46 key: [u8; 32],
47}
48impl std::fmt::Debug for EncryptionService {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("EncryptionService")
51 .field("cipher", &"<initialized>")
52 .finish()
53 }
54}
55impl EncryptionService {
56 pub fn new(key_hex: &str) -> Result<Self, EncryptionError> {
58 info!("Loading master key...");
59 let key_bytes = hex::decode(key_hex)
60 .map_err(|e| EncryptionError::KeyManagement(format!("Invalid key format: {}", e)))?;
61 if key_bytes.len() != 32 {
62 return Err(EncryptionError::KeyManagement(
63 "Invalid key length, expected 32 bytes".to_string(),
64 ));
65 }
66 let key: [u8; 32] = key_bytes.try_into().map_err(|_| {
67 EncryptionError::KeyManagement("Failed to convert key bytes to array".to_string())
68 })?;
69 Ok(EncryptionService { key })
70 }
71
72 pub fn from_key(key: [u8; 32]) -> Self {
74 EncryptionService { key }
75 }
76
77 pub fn fingerprint(&self) -> String {
80 let hash = Sha256::digest(self.key);
81 hex::encode(&hash[..8])
82 }
83
84 #[cfg(any(test, feature = "test-utils"))]
86 pub fn new_with_key(key_bytes: &[u8]) -> Self {
87 if key_bytes.len() != 32 {
88 panic!("Invalid key length, expected 32 bytes");
89 }
90 let key: [u8; 32] = key_bytes.try_into().unwrap();
91 EncryptionService { key }
92 }
93
94 pub fn key_bytes(&self) -> [u8; 32] {
96 self.key
97 }
98
99 pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8> {
104 self.encrypt_chunked(plaintext)
105 }
106
107 pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
109 self.decrypt_chunked(encrypted_data)
110 }
111
112 pub fn encrypt_chunked(&self, plaintext: &[u8]) -> Vec<u8> {
116 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&self.key));
117
118 let mut base_nonce = [0u8; NONCE_SIZE];
120 rand::rng().fill_bytes(&mut base_nonce);
121
122 let mut output = base_nonce.to_vec();
123
124 if plaintext.is_empty() {
126 let nonce = chunk_nonce(&base_nonce, 0);
127 let nonce_arr = GenericArray::from_slice(&nonce);
128 let ct = cipher
129 .encrypt(nonce_arr, &[][..])
130 .expect("encryption should not fail");
131 output.extend(ct);
132 return output;
133 }
134
135 for (i, chunk) in plaintext.chunks(CHUNK_SIZE).enumerate() {
136 let nonce = chunk_nonce(&base_nonce, i as u64);
137 let nonce_arr = GenericArray::from_slice(&nonce);
138 let ct = cipher
139 .encrypt(nonce_arr, chunk)
140 .expect("encryption should not fail");
141 output.extend(ct);
142 }
143
144 output
145 }
146
147 pub fn decrypt_chunk(
150 &self,
151 ciphertext: &[u8],
152 chunk_index: usize,
153 ) -> Result<Vec<u8>, EncryptionError> {
154 if ciphertext.len() < NONCE_SIZE {
155 return Err(EncryptionError::Decryption(
156 "Ciphertext too short for nonce".to_string(),
157 ));
158 }
159
160 let base_nonce: [u8; NONCE_SIZE] = ciphertext[..NONCE_SIZE]
161 .try_into()
162 .map_err(|_| EncryptionError::Decryption("Invalid nonce".to_string()))?;
163
164 let data_start = NONCE_SIZE;
165 let total_data_len = ciphertext.len() - data_start;
166
167 let num_full_chunks = total_data_len / ENCRYPTED_CHUNK_SIZE;
169 let has_partial = !total_data_len.is_multiple_of(ENCRYPTED_CHUNK_SIZE);
170 let total_chunks = num_full_chunks + if has_partial { 1 } else { 0 };
171
172 if chunk_index >= total_chunks {
173 return Err(EncryptionError::Decryption(format!(
174 "Chunk index {} out of range (total chunks: {})",
175 chunk_index, total_chunks
176 )));
177 }
178
179 let chunk_start = data_start + chunk_index * ENCRYPTED_CHUNK_SIZE;
180 let chunk_end = if chunk_index == total_chunks - 1 && has_partial {
181 ciphertext.len()
182 } else {
183 chunk_start + ENCRYPTED_CHUNK_SIZE
184 };
185
186 let chunk_data = &ciphertext[chunk_start..chunk_end];
187 let nonce = chunk_nonce(&base_nonce, chunk_index as u64);
188 let nonce_arr = GenericArray::from_slice(&nonce);
189
190 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&self.key));
191 cipher
192 .decrypt(nonce_arr, chunk_data)
193 .map_err(|_| EncryptionError::Decryption("Authentication failed".to_string()))
194 }
195
196 pub fn decrypt_chunked(&self, ciphertext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
198 if ciphertext.len() < NONCE_SIZE {
199 return Err(EncryptionError::Decryption(
200 "Ciphertext too short for nonce".to_string(),
201 ));
202 }
203
204 let data_start = NONCE_SIZE;
205 let total_data_len = ciphertext.len() - data_start;
206
207 let num_full_chunks = total_data_len / ENCRYPTED_CHUNK_SIZE;
208 let has_partial = !total_data_len.is_multiple_of(ENCRYPTED_CHUNK_SIZE);
209 let total_chunks = num_full_chunks + if has_partial { 1 } else { 0 };
210
211 let mut result = Vec::new();
212 for i in 0..total_chunks {
213 let chunk = self.decrypt_chunk(ciphertext, i)?;
214 result.extend(chunk);
215 }
216
217 Ok(result)
218 }
219
220 pub fn decrypt_range(
227 &self,
228 ciphertext: &[u8],
229 plaintext_start: u64,
230 plaintext_end: u64,
231 ) -> Result<Vec<u8>, EncryptionError> {
232 if plaintext_start >= plaintext_end {
233 return Err(EncryptionError::Decryption(format!(
234 "Invalid range: start ({}) >= end ({})",
235 plaintext_start, plaintext_end
236 )));
237 }
238
239 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
240 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
241
242 let mut plaintext = Vec::new();
243 for chunk_idx in start_chunk..=end_chunk {
244 let chunk = self.decrypt_chunk(ciphertext, chunk_idx as usize)?;
245 plaintext.extend(chunk);
246 }
247
248 let offset_in_first_chunk = (plaintext_start % CHUNK_SIZE as u64) as usize;
250 let len = (plaintext_end - plaintext_start) as usize;
251 let end = offset_in_first_chunk + len;
252
253 if end > plaintext.len() {
254 return Err(EncryptionError::Decryption(format!(
255 "Decrypted data too short: need {} bytes, got {}",
256 end,
257 plaintext.len()
258 )));
259 }
260
261 Ok(plaintext[offset_in_first_chunk..end].to_vec())
262 }
263
264 pub fn decrypt_range_with_offset(
277 &self,
278 nonce: &[u8],
279 encrypted_chunks: &[u8],
280 first_chunk_index: u64,
281 plaintext_start: u64,
282 plaintext_end: u64,
283 ) -> Result<Vec<u8>, EncryptionError> {
284 if nonce.len() != NONCE_SIZE {
285 return Err(EncryptionError::Decryption(format!(
286 "Invalid nonce length: expected {}, got {}",
287 NONCE_SIZE,
288 nonce.len()
289 )));
290 }
291
292 if plaintext_start >= plaintext_end {
293 return Err(EncryptionError::Decryption(format!(
294 "Invalid range: start ({}) >= end ({})",
295 plaintext_start, plaintext_end
296 )));
297 }
298
299 let base_nonce: [u8; NONCE_SIZE] = nonce
300 .try_into()
301 .map_err(|_| EncryptionError::Decryption("Invalid nonce".to_string()))?;
302
303 let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&self.key));
304
305 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
306 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
307
308 let mut plaintext = Vec::new();
309
310 for absolute_chunk_idx in start_chunk..=end_chunk {
311 let relative_idx = absolute_chunk_idx - first_chunk_index;
313 let chunk_start = (relative_idx as usize) * ENCRYPTED_CHUNK_SIZE;
314
315 let chunk_end = if chunk_start + ENCRYPTED_CHUNK_SIZE > encrypted_chunks.len() {
317 encrypted_chunks.len()
318 } else {
319 chunk_start + ENCRYPTED_CHUNK_SIZE
320 };
321
322 if chunk_start >= encrypted_chunks.len() {
323 return Err(EncryptionError::Decryption(format!(
324 "Chunk {} not in provided data (first_chunk_index={})",
325 absolute_chunk_idx, first_chunk_index
326 )));
327 }
328
329 let chunk_data = &encrypted_chunks[chunk_start..chunk_end];
330 let nonce = chunk_nonce(&base_nonce, absolute_chunk_idx);
331 let nonce_arr = GenericArray::from_slice(&nonce);
332
333 let decrypted = cipher.decrypt(nonce_arr, chunk_data).map_err(|_| {
334 EncryptionError::Decryption(format!(
335 "Authentication failed for chunk {}",
336 absolute_chunk_idx
337 ))
338 })?;
339
340 plaintext.extend(decrypted);
341 }
342
343 let offset_in_first_chunk = (plaintext_start % CHUNK_SIZE as u64) as usize;
345 let len = (plaintext_end - plaintext_start) as usize;
346 let end = offset_in_first_chunk + len;
347
348 if end > plaintext.len() {
349 return Err(EncryptionError::Decryption(format!(
350 "Decrypted data too short: need {} bytes, got {}",
351 end,
352 plaintext.len()
353 )));
354 }
355
356 Ok(plaintext[offset_in_first_chunk..end].to_vec())
357 }
358
359 pub fn derive_scoped(&self, release_id: &str) -> EncryptionService {
364 let derived = self.derive_key(&format!("bae-release-v1:{release_id}"));
365 EncryptionService::from_key(derived)
366 }
367
368 pub fn derive_key(&self, info: &str) -> [u8; 32] {
377 let mut mac =
378 <Hmac<Sha256> as Mac>::new_from_slice(&self.key).expect("HMAC accepts any key length");
379 mac.update(b"bae-hkdf-salt-v1");
380 let salt = mac.finalize().into_bytes();
381
382 let hk = Hkdf::<Sha256>::new(Some(&salt), &self.key);
383 let mut okm = [0u8; 32];
384 hk.expand(info.as_bytes(), &mut okm)
385 .expect("32 bytes is a valid HKDF output length");
386 okm
387 }
388}
389
390fn chunk_nonce(base_nonce: &[u8; NONCE_SIZE], chunk_index: u64) -> [u8; NONCE_SIZE] {
392 let mut nonce = *base_nonce;
393 let index_bytes = chunk_index.to_le_bytes();
394 for i in 0..8 {
395 nonce[i] ^= index_bytes[i];
396 }
397 nonce
398}
399
400pub fn encrypted_chunk_range(plaintext_start: u64, plaintext_end: u64) -> (u64, u64) {
408 let start_chunk = plaintext_start / CHUNK_SIZE as u64;
409 let end_chunk = (plaintext_end.saturating_sub(1)) / CHUNK_SIZE as u64;
410
411 let chunk_start = NONCE_SIZE as u64 + start_chunk * ENCRYPTED_CHUNK_SIZE as u64;
412 let chunk_end = NONCE_SIZE as u64 + (end_chunk + 1) * ENCRYPTED_CHUNK_SIZE as u64;
413
414 (chunk_start, chunk_end)
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 fn encrypted_range_for_plaintext(start: u64, end: u64) -> (u64, u64) {
424 let start_chunk = start / CHUNK_SIZE as u64;
425 let end_chunk = (end.saturating_sub(1)) / CHUNK_SIZE as u64;
426
427 let enc_start = NONCE_SIZE as u64 + start_chunk * ENCRYPTED_CHUNK_SIZE as u64;
428 let enc_end = NONCE_SIZE as u64 + (end_chunk + 1) * ENCRYPTED_CHUNK_SIZE as u64;
429
430 (0, enc_end.max(enc_start))
432 }
433
434 fn test_key() -> [u8; 32] {
435 [
437 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
438 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
439 0x1c, 0x1d, 0x1e, 0x1f,
440 ]
441 }
442
443 fn create_test_service() -> EncryptionService {
444 EncryptionService::new_with_key(&test_key())
445 }
446
447 #[test]
448 fn test_roundtrip_small() {
449 let service = create_test_service();
450 let plaintext = b"Hello, world!";
451
452 let ciphertext = service.encrypt(plaintext);
453 let decrypted = service.decrypt(&ciphertext).unwrap();
454
455 assert_eq!(decrypted, plaintext);
456 }
457
458 #[test]
459 fn test_roundtrip_exact_chunk() {
460 let service = create_test_service();
461 let plaintext = vec![0x42u8; CHUNK_SIZE];
462
463 let ciphertext = service.encrypt(&plaintext);
464 let decrypted = service.decrypt(&ciphertext).unwrap();
465
466 assert_eq!(decrypted, plaintext);
467 }
468
469 #[test]
470 fn test_roundtrip_multiple_chunks() {
471 let service = create_test_service();
472 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 2 + CHUNK_SIZE / 2)
474 .map(|i| (i % 256) as u8)
475 .collect();
476
477 let ciphertext = service.encrypt(&plaintext);
478 let decrypted = service.decrypt(&ciphertext).unwrap();
479
480 assert_eq!(decrypted, plaintext);
481 }
482
483 #[test]
484 fn test_random_access_chunk() {
485 let service = create_test_service();
486 let mut plaintext = vec![0x00u8; CHUNK_SIZE];
488 plaintext.extend(vec![0x11u8; CHUNK_SIZE]);
489 plaintext.extend(vec![0x22u8; CHUNK_SIZE]);
490
491 let ciphertext = service.encrypt(&plaintext);
492
493 let chunk1 = service.decrypt_chunk(&ciphertext, 1).unwrap();
495 assert_eq!(chunk1, vec![0x11u8; CHUNK_SIZE]);
496
497 let chunk0 = service.decrypt_chunk(&ciphertext, 0).unwrap();
499 assert_eq!(chunk0, vec![0x00u8; CHUNK_SIZE]);
500
501 let chunk2 = service.decrypt_chunk(&ciphertext, 2).unwrap();
503 assert_eq!(chunk2, vec![0x22u8; CHUNK_SIZE]);
504 }
505
506 #[test]
507 fn test_random_access_partial_last_chunk() {
508 let service = create_test_service();
509 let mut plaintext = vec![0xAAu8; CHUNK_SIZE];
511 plaintext.extend(vec![0xBBu8; 100]);
512
513 let ciphertext = service.encrypt(&plaintext);
514
515 let chunk0 = service.decrypt_chunk(&ciphertext, 0).unwrap();
516 assert_eq!(chunk0, vec![0xAAu8; CHUNK_SIZE]);
517
518 let chunk1 = service.decrypt_chunk(&ciphertext, 1).unwrap();
519 assert_eq!(chunk1, vec![0xBBu8; 100]);
520 }
521
522 #[test]
523 fn test_tamper_detection() {
524 let service = create_test_service();
525 let plaintext = b"Secret data";
526
527 let mut ciphertext = service.encrypt(plaintext);
528
529 let tamper_pos = NONCE_SIZE + 5;
531 ciphertext[tamper_pos] ^= 0xFF;
532
533 let result = service.decrypt(&ciphertext);
534 assert!(result.is_err());
535 }
536
537 #[test]
538 fn test_empty_plaintext() {
539 let service = create_test_service();
540 let plaintext = b"";
541
542 let ciphertext = service.encrypt(plaintext);
543
544 assert_eq!(ciphertext.len(), NONCE_SIZE + TAG_SIZE);
546
547 let decrypted = service.decrypt(&ciphertext).unwrap();
548 assert_eq!(decrypted, plaintext);
549 }
550
551 #[test]
552 fn test_single_byte() {
553 let service = create_test_service();
554 let plaintext = b"x";
555
556 let ciphertext = service.encrypt(plaintext);
557 let decrypted = service.decrypt(&ciphertext).unwrap();
558
559 assert_eq!(decrypted, plaintext);
560 }
561
562 #[test]
563 fn test_encrypted_range_single_chunk() {
564 let (start, end) = encrypted_range_for_plaintext(0, 100);
566
567 assert_eq!(start, 0); assert_eq!(end, NONCE_SIZE as u64 + ENCRYPTED_CHUNK_SIZE as u64);
569 }
570
571 #[test]
572 fn test_encrypted_range_spans_chunks() {
573 let (start, end) =
575 encrypted_range_for_plaintext(CHUNK_SIZE as u64 - 10, CHUNK_SIZE as u64 + 10);
576
577 assert_eq!(start, 0); assert_eq!(end, NONCE_SIZE as u64 + 2 * ENCRYPTED_CHUNK_SIZE as u64);
579 }
580
581 #[test]
582 fn test_encrypted_range_middle_chunk() {
583 let chunk2_start = CHUNK_SIZE as u64 * 2;
585 let (start, end) = encrypted_range_for_plaintext(chunk2_start + 10, chunk2_start + 100);
586
587 assert_eq!(start, 0); assert_eq!(end, NONCE_SIZE as u64 + 3 * ENCRYPTED_CHUNK_SIZE as u64);
589 }
590
591 #[test]
592 fn test_different_encryptions_different_ciphertext() {
593 let service = create_test_service();
594 let plaintext = b"Same message";
595
596 let ciphertext1 = service.encrypt(plaintext);
597 let ciphertext2 = service.encrypt(plaintext);
598
599 assert_ne!(ciphertext1, ciphertext2);
601
602 assert_eq!(service.decrypt(&ciphertext1).unwrap(), plaintext);
604 assert_eq!(service.decrypt(&ciphertext2).unwrap(), plaintext);
605 }
606
607 #[test]
608 fn test_chunk_index_out_of_range() {
609 let service = create_test_service();
610 let plaintext = vec![0u8; CHUNK_SIZE]; let ciphertext = service.encrypt(&plaintext);
613
614 assert!(service.decrypt_chunk(&ciphertext, 0).is_ok());
616
617 assert!(service.decrypt_chunk(&ciphertext, 1).is_err());
619 }
620
621 #[test]
622 fn test_decrypt_range_within_single_chunk() {
623 let service = create_test_service();
624 let plaintext: Vec<u8> = (0..CHUNK_SIZE).map(|i| (i % 256) as u8).collect();
626
627 let ciphertext = service.encrypt(&plaintext);
628
629 let decrypted = service.decrypt_range(&ciphertext, 100, 200).unwrap();
631
632 assert_eq!(decrypted.len(), 100);
633 assert_eq!(decrypted, plaintext[100..200]);
634 }
635
636 #[test]
637 fn test_decrypt_range_spanning_chunks() {
638 let service = create_test_service();
639 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 256) as u8).collect();
641
642 let ciphertext = service.encrypt(&plaintext);
643
644 let start = CHUNK_SIZE as u64 - 100;
646 let end = CHUNK_SIZE as u64 + 100;
647 let decrypted = service.decrypt_range(&ciphertext, start, end).unwrap();
648
649 assert_eq!(decrypted.len(), 200);
650 assert_eq!(decrypted, &plaintext[start as usize..end as usize]);
651 }
652
653 #[test]
654 fn test_decrypt_range_entire_middle_chunk() {
655 let service = create_test_service();
656 let mut plaintext = vec![0xAAu8; CHUNK_SIZE];
658 plaintext.extend(vec![0xBBu8; CHUNK_SIZE]);
659 plaintext.extend(vec![0xCCu8; CHUNK_SIZE]);
660
661 let ciphertext = service.encrypt(&plaintext);
662
663 let start = CHUNK_SIZE as u64;
665 let end = (CHUNK_SIZE * 2) as u64;
666 let decrypted = service.decrypt_range(&ciphertext, start, end).unwrap();
667
668 assert_eq!(decrypted, vec![0xBBu8; CHUNK_SIZE]);
669 }
670
671 #[test]
672 fn test_decrypt_range_with_partial_encrypted_data() {
673 let service = create_test_service();
674 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 3).map(|i| (i % 256) as u8).collect();
676 let full_ciphertext = service.encrypt(&plaintext);
677
678 let plaintext_start = CHUNK_SIZE as u64 + 100;
680 let plaintext_end = CHUNK_SIZE as u64 + 200;
681 let (enc_start, enc_end) = encrypted_range_for_plaintext(plaintext_start, plaintext_end);
682
683 let partial_ciphertext = full_ciphertext[enc_start as usize..enc_end as usize].to_vec();
685
686 let decrypted = service
688 .decrypt_range(&partial_ciphertext, plaintext_start, plaintext_end)
689 .unwrap();
690
691 assert_eq!(decrypted.len(), 100);
692 assert_eq!(
693 decrypted,
694 &plaintext[plaintext_start as usize..plaintext_end as usize]
695 );
696 }
697
698 #[test]
699 fn test_encrypted_chunk_range_returns_actual_bounds() {
700 let chunk5_start = CHUNK_SIZE as u64 * 5;
703 let chunk5_end = chunk5_start + 1000;
704
705 let (enc_start, enc_end) = encrypted_chunk_range(chunk5_start, chunk5_end);
706
707 let expected_start = NONCE_SIZE as u64 + 5 * ENCRYPTED_CHUNK_SIZE as u64;
709 let expected_end = NONCE_SIZE as u64 + 6 * ENCRYPTED_CHUNK_SIZE as u64;
710
711 assert_eq!(
712 enc_start, expected_start,
713 "encrypted_chunk_range should return actual chunk start, not 0"
714 );
715 assert_eq!(enc_end, expected_end);
716 }
717
718 #[test]
719 fn test_encrypted_chunk_range_spanning_multiple_chunks() {
720 let start = CHUNK_SIZE as u64 * 3 + 100;
722 let end = CHUNK_SIZE as u64 * 5 + 500;
723
724 let (enc_start, enc_end) = encrypted_chunk_range(start, end);
725
726 let expected_start = NONCE_SIZE as u64 + 3 * ENCRYPTED_CHUNK_SIZE as u64;
727 let expected_end = NONCE_SIZE as u64 + 6 * ENCRYPTED_CHUNK_SIZE as u64;
728
729 assert_eq!(enc_start, expected_start);
730 assert_eq!(enc_end, expected_end);
731 }
732
733 #[test]
734 fn test_decrypt_range_with_separate_nonce() {
735 let service = create_test_service();
737
738 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 10).map(|i| (i % 256) as u8).collect();
740 let full_ciphertext = service.encrypt(&plaintext);
741
742 let nonce = &full_ciphertext[..NONCE_SIZE];
744
745 let plaintext_start = CHUNK_SIZE as u64 * 7 + 100;
747 let plaintext_end = CHUNK_SIZE as u64 * 7 + 500;
748
749 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
751
752 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
754
755 let first_chunk_index = plaintext_start / CHUNK_SIZE as u64;
757
758 let decrypted = service
760 .decrypt_range_with_offset(
761 nonce,
762 chunks_only,
763 first_chunk_index,
764 plaintext_start,
765 plaintext_end,
766 )
767 .unwrap();
768
769 assert_eq!(decrypted.len(), 400);
770 assert_eq!(
771 decrypted,
772 &plaintext[plaintext_start as usize..plaintext_end as usize]
773 );
774 }
775
776 #[test]
777 fn test_decrypt_range_with_offset_spanning_chunks() {
778 let service = create_test_service();
780
781 let plaintext: Vec<u8> = (0..CHUNK_SIZE * 10).map(|i| (i % 256) as u8).collect();
782 let full_ciphertext = service.encrypt(&plaintext);
783 let nonce = &full_ciphertext[..NONCE_SIZE];
784
785 let plaintext_start = CHUNK_SIZE as u64 * 3 + 1000;
787 let plaintext_end = CHUNK_SIZE as u64 * 5 + 2000;
788
789 let (chunk_start, chunk_end) = encrypted_chunk_range(plaintext_start, plaintext_end);
790 let chunks_only = &full_ciphertext[chunk_start as usize..chunk_end as usize];
791 let first_chunk_index = plaintext_start / CHUNK_SIZE as u64;
792
793 let decrypted = service
794 .decrypt_range_with_offset(
795 nonce,
796 chunks_only,
797 first_chunk_index,
798 plaintext_start,
799 plaintext_end,
800 )
801 .unwrap();
802
803 let expected_len = (plaintext_end - plaintext_start) as usize;
804 assert_eq!(decrypted.len(), expected_len);
805 assert_eq!(
806 decrypted,
807 &plaintext[plaintext_start as usize..plaintext_end as usize]
808 );
809 }
810
811 #[test]
812 fn test_fingerprint_deterministic() {
813 let service = create_test_service();
814 assert_eq!(service.fingerprint(), service.fingerprint());
815 }
816
817 #[test]
818 fn test_fingerprint_different_keys() {
819 let service1 = EncryptionService::new_with_key(&[0u8; 32]);
820 let service2 = EncryptionService::new_with_key(&[1u8; 32]);
821 assert_ne!(service1.fingerprint(), service2.fingerprint());
822 }
823
824 #[test]
825 fn derive_scoped_deterministic() {
826 let service = create_test_service();
827 let derived1 = service.derive_scoped("rel-123");
828 let derived2 = service.derive_scoped("rel-123");
829 assert_eq!(derived1.key_bytes(), derived2.key_bytes());
830 }
831
832 #[test]
833 fn derive_scoped_different_releases() {
834 let service = create_test_service();
835 let key_a = service.derive_scoped("rel-aaa").key_bytes();
836 let key_b = service.derive_scoped("rel-bbb").key_bytes();
837 assert_ne!(key_a, key_b);
838 }
839
840 #[test]
841 fn derive_scoped_different_master_keys() {
842 let svc1 = EncryptionService::new_with_key(&[0u8; 32]);
843 let svc2 = EncryptionService::new_with_key(&[1u8; 32]);
844 let key1 = svc1.derive_scoped("rel-123").key_bytes();
845 let key2 = svc2.derive_scoped("rel-123").key_bytes();
846 assert_ne!(key1, key2);
847 }
848
849 #[test]
850 fn derive_scoped_roundtrip() {
851 let master = create_test_service();
852 let release_enc = master.derive_scoped("rel-456");
853 let plaintext = b"test audio data for this release";
854
855 let encrypted = release_enc.encrypt(plaintext);
856 let decrypted = release_enc.decrypt(&encrypted).unwrap();
857 assert_eq!(decrypted, plaintext);
858
859 assert!(master.decrypt(&encrypted).is_err());
861
862 let wrong_enc = master.derive_scoped("rel-999");
864 assert!(wrong_enc.decrypt(&encrypted).is_err());
865 }
866}