1#![doc = include_str!("../README.md")]
2
3mod error;
4
5pub use error::{DecryptionError, EncryptionError};
6pub use ps_buffer::Buffer;
7
8use chacha20poly1305::aead::{Aead, KeyInit};
9use chacha20poly1305::ChaCha20Poly1305;
10use ps_compress::{compress, decompress_bounded};
11use ps_ecc::{decode, encode, Codeword, DecodeError};
12use ps_hash::Hash;
13use std::ops::Deref;
14use xxhash_rust::xxh64::xxh64;
15
16#[derive(Hash, PartialEq, Eq, PartialOrd, Ord)]
17pub struct Encrypted {
18 pub bytes: Buffer,
19 pub hash: Hash,
20 pub key: Hash,
21}
22
23impl std::fmt::Debug for Encrypted {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.debug_struct("Encrypted")
26 .field("bytes", &self.bytes)
27 .field("hash", &self.hash)
28 .field("key", &"<REDACTED>")
29 .finish()
30 }
31}
32
33const KSIZE: usize = 32;
34const NSIZE: usize = 12;
35
36const PARITY: u8 = 12;
37
38pub const TAG_SIZE: usize = 4;
40
41const TAG_SEED: u64 = 0x08F5_8332_854A_E6B0;
44
45#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
46pub struct ParsedKey {
47 key: [u8; KSIZE],
48 nonce: [u8; NSIZE],
49 length: usize,
50}
51
52impl std::fmt::Debug for ParsedKey {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("ParsedKey")
55 .field("key", &"<REDACTED>")
56 .field("nonce", &self.nonce)
57 .field("length", &self.length)
58 .finish()
59 }
60}
61
62impl From<&Hash> for ParsedKey {
63 fn from(value: &Hash) -> Self {
64 let [_, _, nonce @ ..] = *value.parity();
67
68 Self {
69 key: *value.digest(),
70 length: value.data_max_len().to_usize(),
71 nonce,
72 }
73 }
74}
75
76pub fn encrypt(data: &[u8]) -> Result<Encrypted, EncryptionError> {
84 let compressed_data = compress(data)?;
85 let hash_of_raw_data = ps_hash::hash(data)?;
86
87 let ParsedKey {
88 key: encryption_key,
89 length: _,
90 nonce,
91 } = (&hash_of_raw_data).into();
92
93 let chacha = ChaCha20Poly1305::new(&encryption_key.into());
94 let encrypted_data = chacha
95 .encrypt(&nonce.into(), compressed_data.as_ref())
96 .map_err(EncryptionError::ChaCha)?;
97
98 let bytes = seal(&encrypted_data)?;
99 let hash = Hash::hash(&bytes)?;
100
101 let encrypted = Encrypted {
102 bytes,
103 hash,
104 key: hash_of_raw_data,
105 };
106
107 Ok(encrypted)
108}
109
110fn seal(ciphertext: &[u8]) -> Result<Buffer, EncryptionError> {
112 let mut bytes = encode(ciphertext, PARITY)?;
113 let tag = tag(&bytes);
114
115 bytes.extend_from_slice(tag)?;
116
117 Ok(bytes)
118}
119
120pub fn decrypt(data: &[u8], key: &Hash) -> Result<Buffer, DecryptionError> {
133 let ParsedKey {
134 key: encryption_key,
135 length: out_size,
136 nonce,
137 } = key.into();
138
139 let ecc_decoded = extract_encrypted(data)?;
140 let chacha = ChaCha20Poly1305::new(&encryption_key.into());
141 let compressed_data = chacha
142 .decrypt(&nonce.into(), &ecc_decoded[..])
143 .map_err(DecryptionError::ChaCha)?;
144
145 let plaintext = decompress_bounded(&compressed_data, out_size)?;
146
147 if ps_hash::hash(&plaintext[..])? != *key {
148 return Err(DecryptionError::KeyMismatch);
149 }
150
151 Ok(plaintext)
152}
153
154#[inline]
155pub fn extract_encrypted(data: &[u8]) -> Result<Codeword<'_>, DecodeError> {
161 decode(&data[..data.len().saturating_sub(TAG_SIZE)], PARITY)
162}
163
164#[must_use]
165pub fn tag(codeword: &[u8]) -> [u8; TAG_SIZE] {
168 let [tag @ .., _, _, _, _] = xxh64(codeword, TAG_SEED).to_le_bytes();
171
172 tag
173}
174
175#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
177pub enum Validity {
178 Unrecoverable,
183 TagMismatch,
188 Corrupted,
191 Pristine,
194}
195
196#[must_use]
197pub fn validate(data: &[u8]) -> Validity {
214 let Some((codeword, stored)) = data.split_last_chunk::<TAG_SIZE>() else {
215 return Validity::Unrecoverable;
216 };
217
218 let Ok(decoded) = decode(codeword, PARITY) else {
219 return Validity::Unrecoverable;
220 };
221
222 let corrected = decoded.into_inner();
223
224 if tag(&corrected) != *stored {
225 return Validity::TagMismatch;
226 }
227
228 if *corrected == *codeword {
229 Validity::Pristine
230 } else {
231 Validity::Corrupted
232 }
233}
234
235impl AsRef<[u8]> for Encrypted {
236 fn as_ref(&self) -> &[u8] {
237 self
238 }
239}
240
241impl Deref for Encrypted {
242 type Target = [u8];
243
244 fn deref(&self) -> &Self::Target {
245 &self.bytes
246 }
247}
248
249#[cfg(test)]
250#[allow(clippy::expect_used)]
251#[allow(clippy::panic)]
252#[allow(clippy::unwrap_used)]
253mod tests {
254 use ps_buffer::ToBuffer;
255 use ps_compress::DecompressionError;
256 use ps_hash::hash;
257
258 use super::*;
259
260 #[test]
261 fn test_encrypt_and_decrypt() {
262 let original_data = b"Hello, World!";
263
264 let encrypted_data = encrypt(original_data).expect("encryption should succeed");
265
266 let decrypted_data =
267 decrypt(&encrypted_data.bytes, &encrypted_data.key).expect("decryption should succeed");
268
269 assert_ne!(
270 original_data
271 .to_buffer()
272 .expect("conversion to buffer should succeed"),
273 encrypted_data.bytes,
274 "Encryption should modify the data"
275 );
276
277 let ecc_payload = extract_encrypted(&encrypted_data.bytes)
278 .expect("extracting ECC payload should succeed");
279
280 assert_eq!(
281 encrypted_data.bytes.len(),
282 ecc_payload.len() + 2 * usize::from(PARITY) + TAG_SIZE,
283 "sealing should add the parity bytes and the tag"
284 );
285
286 assert_eq!(
287 original_data,
288 &decrypted_data[..],
289 "Decryption should reverse encryption"
290 );
291 }
292
293 fn create_test_key() -> Hash {
295 hash("Hello, world!").expect("hashing test key should succeed")
296 }
297
298 fn lcg_bytes(len: usize) -> Vec<u8> {
300 let mut state = 0x243F_6A88_85A3_08D3_u64;
301
302 (0..len)
303 .map(|_| {
304 state = state
305 .wrapping_mul(6_364_136_223_846_793_005)
306 .wrapping_add(1_442_695_040_888_963_407);
307
308 state.to_be_bytes()[0]
309 })
310 .collect()
311 }
312
313 #[test]
314 fn test_parse_key() {
315 let key = &create_test_key();
316
317 let ParsedKey {
318 key: encryption_key,
319 length: _,
320 nonce,
321 } = key.into();
322
323 assert_eq!(encryption_key.len(), 32);
324 assert_eq!(nonce.len(), 12);
325 assert_eq!(&encryption_key[0..4], &[220, 186, 155, 106]); assert_eq!(&nonce[0..4], &[46, 215, 220, 44]); }
329
330 #[test]
331 fn test_encrypt_decrypt() {
332 let data = b"This is some data to encrypt";
333 let encrypted = encrypt(data).expect("encryption should succeed");
334 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
335 assert_eq!(&*decrypted, data);
336 }
337
338 #[test]
339 fn test_encrypt_decrypt_empty_data() {
340 let data = b"";
341 let encrypted = encrypt(data).expect("encryption should succeed");
342 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
343 assert_eq!(&*decrypted, data);
344 }
345
346 #[test]
347 fn test_encrypt_decrypt_long_data() {
348 let data = "This is a very long string to test the encryption and decryption with a large amount of data. We want to make sure that the compression and decompression work correctly, and that the encryption and decryption can handle a significant amount of data without any issues. This should be longer than any reasonable message. Let's add some more to be absolutely sure. And even more, just to be safe.".as_bytes();
349 let encrypted = encrypt(data).expect("encryption should succeed");
350 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
351 assert_eq!(&*decrypted, data);
352 }
353
354 #[test]
355 fn test_encrypt_decrypt_different_key() {
356 let data = b"This is some data";
357 let encrypted = encrypt(data).expect("encryption should succeed");
358 let different_key = create_test_key(); let result = decrypt(&encrypted, &different_key);
361 assert!(result.is_err());
362 match result.unwrap_err() {
363 DecryptionError::ChaCha(_) => {} _ => panic!("Unexpected error type"),
365 }
366 }
367
368 #[test]
369 fn test_encrypt_decrypt_tampered_data() {
370 let data = b"This is some data";
371 let mut encrypted = encrypt(data).expect("encryption should succeed");
372 encrypted.bytes[0] ^= 0x01; let decrypted = decrypt(&encrypted, &encrypted.key)
376 .expect("decryption should succeed after ECC correction");
377
378 assert_eq!(decrypted.slice(..), data);
379 }
380
381 #[test]
382 fn test_validate_pristine_and_truncated_data() {
383 let data = b"ECC validation data";
384 let encrypted = encrypt(data).expect("encryption should succeed");
385
386 assert_eq!(
387 validate(&encrypted),
388 Validity::Pristine,
389 "fresh ciphertext should be pristine"
390 );
391
392 let truncated = &encrypted.bytes[..encrypted.bytes.len() - 1];
393
394 assert_eq!(
395 validate(truncated),
396 Validity::Unrecoverable,
397 "truncated ciphertext should be unrecoverable"
398 );
399 }
400
401 #[test]
402 fn test_extract_encrypted_rejects_truncated_payload() {
403 let data = b"payload";
404 let encrypted = encrypt(data).expect("encryption should succeed");
405 let truncated = &encrypted.bytes[..encrypted.bytes.len() - 1];
406
407 let result = extract_encrypted(truncated);
408 assert!(result.is_err(), "truncated payload must fail ECC decode");
409 }
410
411 #[test]
412 fn test_decrypt_truncated_payload_returns_ecc_error() {
413 let data = b"payload";
414 let encrypted = encrypt(data).expect("encryption should succeed");
415 let truncated = &encrypted.bytes[..encrypted.bytes.len() - 1];
416
417 let result = decrypt(truncated, &encrypted.key);
418 assert!(
419 matches!(result, Err(DecryptionError::Ecc(_))),
420 "truncated payload should surface as ECC error"
421 );
422 }
423
424 #[test]
425 fn test_encrypted_hash_matches_ciphertext_bytes() {
426 let data = b"hash check";
427 let encrypted = encrypt(data).expect("encryption should succeed");
428 let recalculated = Hash::hash(&encrypted.bytes).expect("hashing bytes should succeed");
429
430 assert_eq!(encrypted.hash, recalculated);
431 }
432
433 #[test]
434 fn test_as_ref_encrypted() {
435 let data = b"Test data";
436 let encrypted = encrypt(data).expect("encryption should succeed");
437 let as_ref_data: &[u8] = encrypted.as_ref();
438 assert_eq!(as_ref_data, &*encrypted);
439 assert_eq!(as_ref_data, &encrypted.bytes[..]);
440 }
441
442 #[test]
443 fn test_deref_encrypted() {
444 let data = b"More test data";
445 let encrypted = encrypt(data).expect("encryption should succeed");
446 let deref_data: &[u8] = &encrypted; assert_eq!(deref_data, &encrypted.bytes[..]);
448 }
449
450 #[test]
451 fn test_key_from_hash() {
452 let data = b"Test data for key derivation";
453 let h = hash(data).expect("hashing should succeed");
454
455 let ParsedKey {
456 key,
457 length: _,
458 nonce: _,
459 } = (&h).into();
460
461 assert_eq!(key.len(), 32);
462 }
463
464 #[test]
465 fn test_encrypt_large_data() {
466 let data = vec![b'A'; 1024 * 1024];
468 let encrypted = encrypt(&data).expect("encryption should succeed");
469 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
470 assert_eq!(&*decrypted, &data[..]);
471 }
472
473 #[test]
474 fn test_ps_cypher_error_display() {
475 let data = b"test";
476 let encrypted = encrypt(data).expect("encryption should succeed");
477 let bad_key = hash(b"invalid_key").expect("hashing should succeed");
478 let result = decrypt(&encrypted, &bad_key);
479
480 if let Err(e) = result {
481 let error_message = format!("{e}");
482 assert_eq!(error_message, "decryption failed (chacha20poly1305)");
483 } else {
484 panic!("Expected an error, but got success");
485 }
486 }
487
488 #[test]
489 fn test_ps_cypher_error_source() {
490 let data = b"test";
491 let encrypted = encrypt(data).expect("encryption should succeed");
492 let bad_key = hash(b"invalid_key").expect("hashing should succeed");
493 let result = decrypt(&encrypted, &bad_key);
494
495 if let Err(e) = result {
496 let source = std::error::Error::source(&e).expect("ChaCha error should have a source");
497
498 assert_eq!(format!("{source}"), "aead::Error");
499 } else {
500 panic!("Expected an error, but got success");
501 }
502 }
503
504 #[test]
505 fn test_parsed_key_debug_redacts_key() {
506 let key = create_test_key();
507 let parsed: ParsedKey = (&key).into();
508 let debug_output = format!("{parsed:?}");
509
510 assert!(
511 debug_output.contains("<REDACTED>"),
512 "Debug output should redact the key"
513 );
514 assert!(
516 debug_output.contains("key: \"<REDACTED>\""),
517 "key field should be redacted"
518 );
519 assert!(debug_output.contains("nonce:"), "nonce should be present");
521 assert!(debug_output.contains("length:"), "length should be present");
522 }
523
524 #[test]
525 #[allow(clippy::clone_on_copy)]
526 fn test_parsed_key_clone_and_copy() {
527 let key = create_test_key();
528 let parsed: ParsedKey = (&key).into();
529 let cloned = parsed.clone(); let copied = parsed;
531
532 assert_eq!(parsed, cloned);
533 assert_eq!(parsed, copied);
534 }
535
536 #[test]
537 fn test_parsed_key_hash_trait() {
538 use std::collections::HashSet;
539
540 let key1 = create_test_key();
541 let key2 = hash(b"different data").expect("hashing should succeed");
542
543 let parsed1: ParsedKey = (&key1).into();
544 let parsed2: ParsedKey = (&key2).into();
545
546 let mut set = HashSet::new();
547 set.insert(parsed1);
548 set.insert(parsed2);
549
550 assert_eq!(set.len(), 2, "different keys should hash differently");
551 }
552
553 #[test]
554 fn test_parsed_key_ordering() {
555 let key1 = hash(b"aaa").expect("hashing should succeed");
556 let key2 = hash(b"bbb").expect("hashing should succeed");
557
558 let parsed1: ParsedKey = (&key1).into();
559 let parsed2: ParsedKey = (&key2).into();
560
561 let cmp1 = parsed1.cmp(&parsed2);
563 let cmp2 = parsed2.cmp(&parsed1);
564 assert_eq!(cmp1.reverse(), cmp2);
565 }
566
567 #[test]
568 fn test_encrypted_hash_trait() {
569 use std::collections::HashSet;
570
571 let encrypted1 = encrypt(b"data1").expect("encryption should succeed");
572 let encrypted2 = encrypt(b"data2").expect("encryption should succeed");
573
574 let mut set = HashSet::new();
575 set.insert(encrypted1);
576 set.insert(encrypted2);
577
578 assert_eq!(
579 set.len(),
580 2,
581 "different encryptions should hash differently"
582 );
583 }
584
585 #[test]
586 fn test_encrypted_ordering() {
587 let encrypted1 = encrypt(b"aaa").expect("encryption should succeed");
588 let encrypted2 = encrypt(b"bbb").expect("encryption should succeed");
589
590 let cmp1 = encrypted1.cmp(&encrypted2);
591 let cmp2 = encrypted2.cmp(&encrypted1);
592 assert_eq!(cmp1.reverse(), cmp2);
593 }
594
595 #[test]
596 fn test_encrypted_equality() {
597 let data = b"same data";
598 let encrypted1 = encrypt(data).expect("encryption should succeed");
599 let encrypted2 = encrypt(data).expect("encryption should succeed");
600
601 assert_eq!(
602 encrypted1, encrypted2,
603 "same input should produce equal encryptions"
604 );
605 }
606
607 #[test]
608 fn test_encrypt_decrypt_single_byte() {
609 let data = b"x";
610 let encrypted = encrypt(data).expect("encryption should succeed");
611 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
612 assert_eq!(&*decrypted, data);
613 }
614
615 #[test]
616 fn test_encrypt_decrypt_binary_with_nulls() {
617 let data: &[u8] = &[0x00, 0x01, 0x00, 0xFF, 0x00, 0xFE, 0x00];
618 let encrypted = encrypt(data).expect("encryption should succeed");
619 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
620 assert_eq!(&*decrypted, data);
621 }
622
623 #[test]
624 fn test_encrypt_decrypt_all_byte_values() {
625 let data: Vec<u8> = (0u8..=255).collect();
626 let encrypted = encrypt(&data).expect("encryption should succeed");
627 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
628 assert_eq!(&*decrypted, &data[..]);
629 }
630
631 #[test]
632 fn test_encrypt_decrypt_unicode() {
633 let data = "Hello 世界! 🎉 Привет мир".as_bytes();
634 let encrypted = encrypt(data).expect("encryption should succeed");
635 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
636 assert_eq!(&*decrypted, data);
637 }
638
639 #[test]
640 fn test_encrypt_decrypt_highly_compressible_data() {
641 let data = vec![b'A'; 10000];
643 let encrypted = encrypt(&data).expect("encryption should succeed");
644
645 assert!(
647 encrypted.bytes.len() < data.len(),
648 "highly compressible data should result in smaller ciphertext"
649 );
650
651 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
652 assert_eq!(&*decrypted, &data[..]);
653 }
654
655 #[test]
656 fn test_encryption_determinism() {
657 let data = b"deterministic test data";
658
659 let encrypted1 = encrypt(data).expect("encryption should succeed");
660 let encrypted2 = encrypt(data).expect("encryption should succeed");
661
662 assert_eq!(
663 encrypted1.bytes, encrypted2.bytes,
664 "same input should produce identical ciphertext"
665 );
666 assert_eq!(
667 encrypted1.key, encrypted2.key,
668 "same input should produce identical key"
669 );
670 assert_eq!(
671 encrypted1.hash, encrypted2.hash,
672 "same input should produce identical hash"
673 );
674 }
675
676 #[test]
677 fn test_different_inputs_produce_different_outputs() {
678 let encrypted1 = encrypt(b"input 1").expect("encryption should succeed");
679 let encrypted2 = encrypt(b"input 2").expect("encryption should succeed");
680
681 assert_ne!(
682 encrypted1.bytes, encrypted2.bytes,
683 "different inputs should produce different ciphertexts"
684 );
685 assert_ne!(
686 encrypted1.key, encrypted2.key,
687 "different inputs should produce different keys"
688 );
689 }
690
691 #[test]
692 fn test_ecc_corrects_multiple_bit_errors() {
693 let data = b"ECC multi-bit correction test";
694 let mut encrypted = encrypt(data).expect("encryption should succeed");
695
696 encrypted.bytes[0] ^= 0x01;
698 encrypted.bytes[1] ^= 0x02;
699 encrypted.bytes[2] ^= 0x04;
700
701 let decrypted = decrypt(&encrypted, &encrypted.key)
702 .expect("decryption should succeed with ECC correction");
703 assert_eq!(&*decrypted, data);
704 }
705
706 #[test]
707 fn test_validate_reports_recoverable_corruption() {
708 let data = b"ECC validation test";
709 let mut encrypted = encrypt(data).expect("encryption should succeed");
710
711 encrypted.bytes[5] ^= 0x01;
712
713 assert_eq!(
714 validate(&encrypted),
715 Validity::Corrupted,
716 "corruption within capacity should be recoverable"
717 );
718
719 let decrypted =
720 decrypt(&encrypted, &encrypted.key).expect("decryption should correct the corruption");
721
722 assert_eq!(&*decrypted, data);
723 }
724
725 #[test]
726 fn test_empty_slice_validation() {
727 assert_eq!(
728 validate(&[]),
729 Validity::Unrecoverable,
730 "empty slice should be unrecoverable"
731 );
732 }
733
734 #[test]
735 fn test_validate_rejects_input_shorter_than_tag() {
736 let short = [0xAB; TAG_SIZE - 1];
737
738 assert_eq!(validate(&short), Validity::Unrecoverable);
739 assert!(extract_encrypted(&short).is_err());
740 }
741
742 #[test]
743 fn test_validate_rejects_foreign_ecc_codeword() {
744 let mut foreign = encode(b"a codeword that is not ps-cypher's", PARITY)
745 .expect("ECC encoding should succeed");
746
747 assert_eq!(
748 validate(&foreign),
749 Validity::Unrecoverable,
750 "a bare foreign ps-ecc codeword must not pass as a ps-cypher codeword"
751 );
752
753 foreign
754 .extend_from_slice([0u8; TAG_SIZE])
755 .expect("appending should succeed");
756
757 assert_eq!(
758 validate(&foreign),
759 Validity::TagMismatch,
760 "a foreign ps-ecc codeword with trailing bytes must fail the tag check"
761 );
762 }
763
764 #[test]
765 fn test_validate_reports_corrupted_tag_as_mismatch() {
766 let data = b"tag corruption test";
767 let mut encrypted = encrypt(data).expect("encryption should succeed");
768 let last = encrypted.bytes.len() - 1;
769
770 encrypted.bytes[last] ^= 0x01;
771
772 assert_eq!(
773 validate(&encrypted),
774 Validity::TagMismatch,
775 "a corrupted tag should be reported as a mismatch"
776 );
777
778 let decrypted =
779 decrypt(&encrypted, &encrypted.key).expect("decryption should ignore the tag");
780
781 assert_eq!(&*decrypted, data);
782 }
783
784 #[test]
785 fn test_validate_reports_corruption_beyond_capacity() {
786 let data = b"ECC capacity boundary test";
787 let mut encrypted = encrypt(data).expect("encryption should succeed");
788
789 for byte in encrypted.bytes.iter_mut().take(13) {
790 *byte ^= 0xFF;
791 }
792
793 assert_eq!(
794 validate(&encrypted),
795 Validity::Unrecoverable,
796 "13 corrupted bytes must not be reported as recoverable"
797 );
798 }
799
800 #[test]
801 fn test_validate_reports_recoverable_long_format_corruption() {
802 let data = lcg_bytes(512);
803 let mut encrypted = encrypt(&data).expect("encryption should succeed");
804
805 assert!(
806 encrypted.bytes.len() > 255,
807 "incompressible 512-byte input should use the long ECC format"
808 );
809
810 for byte in encrypted.bytes.iter_mut().skip(40).take(12) {
811 *byte ^= 0xFF;
812 }
813
814 assert_eq!(validate(&encrypted), Validity::Corrupted);
815 }
816
817 #[test]
818 fn test_tag_is_deterministic_and_trails_the_codeword() {
819 let encrypted = encrypt(b"tag placement").expect("encryption should succeed");
820 let (codeword, stored) = encrypted
821 .bytes
822 .split_last_chunk::<TAG_SIZE>()
823 .expect("ciphertext should carry a tag");
824
825 assert_eq!(tag(codeword), *stored);
826 assert_eq!(tag(codeword), tag(codeword));
827 }
828
829 #[test]
830 fn test_extract_encrypted_empty_slice() {
831 let result = extract_encrypted(&[]);
832 assert!(result.is_err(), "empty slice should fail extraction");
833 }
834
835 #[test]
836 fn test_decrypt_empty_slice() {
837 let key = create_test_key();
838 let result = decrypt(&[], &key);
839 assert!(
840 matches!(result, Err(DecryptionError::Ecc(_))),
841 "empty slice should return ECC error"
842 );
843 }
844
845 #[test]
846 fn test_decryption_error_clone() {
847 let data = b"test";
848 let encrypted = encrypt(data).expect("encryption should succeed");
849 let bad_key = hash(b"wrong_key").expect("hashing should succeed");
850
851 let result = decrypt(&encrypted, &bad_key);
852 if let Err(e) = result {
853 let cloned = e.clone();
854 assert_eq!(format!("{e}"), format!("{cloned}"));
855 } else {
856 panic!("Expected decryption error");
857 }
858 }
859
860 #[test]
861 fn test_chacha_error_display_and_source() {
862 let encryption_error = EncryptionError::ChaCha(chacha20poly1305::Error);
863 let decryption_error = DecryptionError::ChaCha(chacha20poly1305::Error);
864
865 assert_eq!(
866 format!("{encryption_error}"),
867 "encryption failed (chacha20poly1305)"
868 );
869 assert_eq!(
870 format!("{decryption_error}"),
871 "decryption failed (chacha20poly1305)"
872 );
873 assert!(std::error::Error::source(&encryption_error).is_some());
874 assert!(std::error::Error::source(&decryption_error).is_some());
875 }
876
877 #[test]
878 fn test_decryption_error_debug() {
879 let err = DecryptionError::ChaCha(chacha20poly1305::Error);
880 let debug_output = format!("{err:?}");
881 assert!(debug_output.contains("ChaCha"));
882 }
883
884 #[test]
885 fn test_encryption_error_debug() {
886 let err = EncryptionError::ChaCha(chacha20poly1305::Error);
887 let debug_output = format!("{err:?}");
888 assert!(debug_output.contains("ChaCha"));
889 }
890
891 #[test]
892 fn test_encrypted_debug_redacts_key() {
893 let encrypted = encrypt(b"debug test").expect("encryption should succeed");
894 let debug_output = format!("{encrypted:?}");
895
896 assert!(debug_output.contains("Encrypted"));
897 assert!(debug_output.contains("bytes"));
898 assert!(debug_output.contains("hash"));
899 assert!(
900 debug_output.contains("key: \"<REDACTED>\""),
901 "key field should be redacted"
902 );
903 assert!(
904 !debug_output.contains(&encrypted.key.to_string()),
905 "Debug output must not leak the key"
906 );
907 }
908
909 #[test]
910 fn test_parsed_key_same_hash_produces_same_key() {
911 let h = hash(b"consistent").expect("hashing should succeed");
912 let parsed1: ParsedKey = (&h).into();
913 let parsed2: ParsedKey = (&h).into();
914
915 assert_eq!(parsed1, parsed2);
916 }
917
918 #[test]
919 fn test_encrypt_decrypt_powers_of_two_sizes() {
920 for power in 0..=10 {
921 let size = 1 << power;
922 let data = vec![0xAB_u8; size];
923 let encrypted = encrypt(&data).expect("encryption should succeed");
924 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
925 assert_eq!(&*decrypted, &data[..], "failed for size {size}");
926 }
927 }
928
929 #[test]
930 fn test_encrypt_decrypt_boundary_sizes() {
931 for size in [
933 127, 128, 129, 255, 256, 257, 511, 512, 513, 1023, 1024, 1025,
934 ] {
935 let data = vec![0xCD_u8; size];
936 let encrypted = encrypt(&data).expect("encryption should succeed");
937 let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
938 assert_eq!(&*decrypted, &data[..], "failed for size {size}");
939 }
940 }
941
942 #[test]
943 fn test_decrypt_rejects_substituted_plaintext() {
944 let original = b"the original plaintext, which is longer than the forgery";
945 let key_hash = hash(original).expect("hashing should succeed");
946
947 let forged = compress(b"forged").expect("compression should succeed");
949
950 let ParsedKey {
951 key,
952 nonce,
953 length: _,
954 } = (&key_hash).into();
955
956 let chacha = ChaCha20Poly1305::new(&key.into());
957 let ciphertext = chacha
958 .encrypt(&nonce.into(), forged.as_ref())
959 .expect("forged encryption should succeed");
960 let bytes = seal(&ciphertext).expect("sealing should succeed");
961
962 let result = decrypt(&bytes, &key_hash);
963
964 assert!(
965 matches!(result, Err(DecryptionError::KeyMismatch)),
966 "substituted plaintext must be rejected, got {result:?}"
967 );
968 }
969
970 #[test]
971 fn test_decrypt_succeeds_at_ecc_capacity() {
972 let data = b"ECC capacity boundary test";
973 let mut encrypted = encrypt(data).expect("encryption should succeed");
974
975 for byte in encrypted.bytes.iter_mut().take(12) {
976 *byte ^= 0xFF;
977 }
978
979 let decrypted = decrypt(&encrypted, &encrypted.key)
980 .expect("12 corrupted bytes should be within ECC capacity");
981
982 assert_eq!(&*decrypted, data);
983 }
984
985 #[test]
986 fn test_decrypt_fails_beyond_ecc_capacity() {
987 let data = b"ECC capacity boundary test";
988 let mut encrypted = encrypt(data).expect("encryption should succeed");
989
990 for byte in encrypted.bytes.iter_mut().take(13) {
991 *byte ^= 0xFF;
992 }
993
994 assert!(
998 decrypt(&encrypted, &encrypted.key).is_err(),
999 "13 corrupted bytes must exceed ECC capacity"
1000 );
1001 }
1002
1003 #[test]
1004 fn test_decrypt_corrects_long_format_corruption() {
1005 let data = lcg_bytes(512);
1006 let mut encrypted = encrypt(&data).expect("encryption should succeed");
1007
1008 assert!(
1009 encrypted.bytes.len() > 255,
1010 "incompressible 512-byte input should use the long ECC format"
1011 );
1012
1013 for byte in encrypted.bytes.iter_mut().skip(40).take(12) {
1015 *byte ^= 0xFF;
1016 }
1017
1018 let decrypted = decrypt(&encrypted, &encrypted.key)
1019 .expect("long-format corruption within capacity should be corrected");
1020
1021 assert_eq!(&*decrypted, &data[..]);
1022 }
1023
1024 #[test]
1025 fn test_decrypt_enforces_length_bound_from_key() {
1026 let short = b"S";
1027 let key_hash = hash(short).expect("hashing should succeed");
1028
1029 let oversized = lcg_bytes(2000);
1032 let compressed = compress(&oversized).expect("compression should succeed");
1033
1034 let ParsedKey {
1035 key,
1036 nonce,
1037 length: _,
1038 } = (&key_hash).into();
1039
1040 let chacha = ChaCha20Poly1305::new(&key.into());
1041 let ciphertext = chacha
1042 .encrypt(&nonce.into(), compressed.as_ref())
1043 .expect("forged encryption should succeed");
1044 let bytes = seal(&ciphertext).expect("sealing should succeed");
1045
1046 let result = decrypt(&bytes, &key_hash);
1047
1048 assert!(
1049 matches!(
1050 result,
1051 Err(DecryptionError::Decompression(
1052 DecompressionError::TooLarge { .. }
1053 ))
1054 ),
1055 "oversized frame must be rejected before allocation, got {result:?}"
1056 );
1057 }
1058
1059 const GOLDEN_SHORT_INPUT: &[u8] = b"Hello, World!";
1064 const GOLDEN_SHORT_CIPHERTEXT_HEX: &str = "fe4ffee7417540f84292fd29e36031c85e6ba66113ed6cdf1d0d326f7d04516ce1c964c52e4fdfa78192d486e464bdba1a75ef1b741fcda668698fda12471d3033cd";
1065 const GOLDEN_SHORT_KEY: &str =
1066 "YXVYD1H41DV6CWXAN3KS683DEH4YGQRQF86Q6P6J8BMEYMZSC2BGT045TN01ZAW1XC9684A6QE52J";
1067 const GOLDEN_SHORT_HASH: &str =
1068 "EC5S9YSB9KP53EEEX66G7TBACEF3DF14YJBFSQ8S4DFKZ8TCE0CM4031CW5MX0KEPCDPMH1X5G94W";
1069
1070 const GOLDEN_LONG_CIPHERTEXT_HEX: &str = "4c48010c000002820000021a22bfd2fdc12d099939a9694c6af640a8b52d1bf6942cda1e9bb0e87df73270d25cc9bc7b096447bae9cc2234f49665228176f93c66f0ae1fcde50bd86cbef3f39a085d99557c20e8772294a1b7db41016e428daadd949841ee5b358be5e5bcb13b63f6e15beb8344e60d9d9d7c738a8267fb2166db2bd8166606d8a619a23c66f2a1fd1dbdc12b76c89b2407b3ee0fbf4b85478828e3b3d3a153da528165888ceaf58aa6b399c6f2b3a4197e2013c7d50412be066e0a561105ddda6239968896e62c9dda16c0d1af78bbd3130b8cb9bcbb6c18a4f7954486832e8cfa4103b3ac0960da78e1a16abdedb75874be3442bf702a1a6ac4507aab3d91e88ff5bbf3251092f6b8a75263ff3ee1a34ba54bf8edcc6025eb1b7af4e9da309a2504d6715b8f8445a45ab66ab4a96f68c0fdcbb526b1b57de9a44137fe925d6bb848692ba881278ea4c73e33d6809f9f9f073026d45587e0913e3392b53702cbc9c2eb1de53147a896800efef0ac9e0ad9581b6b61ed6297bf1fd5e02adce0d180503faa634af913f36ff0794e5e57c9b75ab8ee2b42eaaceb26622ba509c01148f9a7d5de62fb43ab79fb6f03536ac535fbd229110e39b971d728dc422777448036652e5c795ce69d3884e7c3eeb32f5722e5273948e737ba586dc45cb407b2c4ac867075f6b39f8db83b8a89b353075a9e25bdeec0a2ad3759cf67b17f1d2467115b9845570a6f79bd10df8bf45160f43cc43da9a048f6acab8a10d615e134b80ac7beaaf5836322ca2418e44c5525249ece6161f3f322ace5ae931bc86b0c9223ba97ed60b971b76c3e13c24d86b685eee38b06f1f04465e893c46308f518317b0ad258576ea093545ce5bf13a8e7a1386c4afc2cafdfb9ae60905ec8cd";
1072 const GOLDEN_LONG_KEY: &str =
1073 "V2Q9QNXEKT9EMZ9C1FEDWSX1P2A516SG5DRFN0Z4NBH2DRHWKCHG00JQ83X9238V9014AP55BFDWG";
1074 const GOLDEN_LONG_HASH: &str =
1075 "CB76NR0HYJK6HEK3T1Z5EB3ZE2AJVE3W0J9KAVSJ1F6JJEY8F69460J08112WA9W8MYSSD2V7WFYW";
1076
1077 fn hex_decode(hex: &str) -> Vec<u8> {
1079 (0..hex.len())
1080 .step_by(2)
1081 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("fixture hex should be valid"))
1082 .collect()
1083 }
1084
1085 fn check_golden(input: &[u8], ciphertext_hex: &str, key_str: &str, hash_str: &str) {
1088 let expected_bytes = hex_decode(ciphertext_hex);
1089
1090 let encrypted = encrypt(input).expect("encryption should succeed");
1091
1092 assert_eq!(
1093 &encrypted.bytes[..],
1094 &expected_bytes[..],
1095 "ciphertext should match the golden fixture"
1096 );
1097 assert_eq!(
1098 encrypted.key.to_string(),
1099 key_str,
1100 "key should match the golden fixture"
1101 );
1102 assert_eq!(
1103 encrypted.hash.to_string(),
1104 hash_str,
1105 "hash should match the golden fixture"
1106 );
1107
1108 let key = Hash::try_from(key_str).expect("fixture key should parse");
1109
1110 let decrypted = decrypt(&expected_bytes, &key).expect("decryption should succeed");
1111
1112 assert_eq!(
1113 &decrypted[..],
1114 input,
1115 "decrypting the golden ciphertext should yield the input"
1116 );
1117 }
1118
1119 #[test]
1120 fn test_golden_short_ciphertext() {
1121 check_golden(
1122 GOLDEN_SHORT_INPUT,
1123 GOLDEN_SHORT_CIPHERTEXT_HEX,
1124 GOLDEN_SHORT_KEY,
1125 GOLDEN_SHORT_HASH,
1126 );
1127 }
1128
1129 #[test]
1130 fn test_golden_long_ciphertext() {
1131 let input = lcg_bytes(512);
1132
1133 assert!(
1134 hex_decode(GOLDEN_LONG_CIPHERTEXT_HEX).len() > 255,
1135 "long fixture should use the long ECC format"
1136 );
1137
1138 check_golden(
1139 &input,
1140 GOLDEN_LONG_CIPHERTEXT_HEX,
1141 GOLDEN_LONG_KEY,
1142 GOLDEN_LONG_HASH,
1143 );
1144 }
1145}