Skip to main content

ps_cypher/
lib.rs

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
38/// Size of the tag that trails the ECC codeword, in bytes.
39pub const TAG_SIZE: usize = 4;
40
41/// Seed of the XXH64 tag, distinguishing ps-cypher tags from the XXH64
42/// checksums that ps-ecc computes with its own seed.
43const 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        // The nonce is the trailing NSIZE of the hash's parity bytes; a change
65        // to either size fails to compile here, forcing a deliberate decision.
66        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
76/// Encrypts `data` using convergent encryption: the key is the hash of `data`.
77/// # Errors
78/// - [`EncryptionError::Compression`] is returned if compression fails.
79/// - [`EncryptionError::Hash`] is returned if hashing fails.
80/// - [`EncryptionError::ChaCha`] is returned if encryption fails.
81/// - [`EncryptionError::Ecc`] is returned if ECC encoding fails.
82/// - [`EncryptionError::Buffer`] is returned if appending the tag fails.
83pub 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
110/// Encodes `ciphertext` as an ECC codeword and appends the codeword's tag.
111fn 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
120/// Decrypts `data` using `key`, repairing up to 12 corrupted bytes per codeword.
121///
122/// The decrypted data is verified to hash to `key`, so a successful decryption
123/// yields exactly the data that produced `key`. The trailing tag is ignored,
124/// so corruption of the tag does not affect decryption.
125/// # Errors
126/// - [`DecryptionError::Ecc`] is returned if `data` is irrecoverably corrupted.
127/// - [`DecryptionError::ChaCha`] is returned if decryption fails.
128/// - [`DecryptionError::Decompression`] is returned if decompression fails.
129/// - [`DecryptionError::Hash`] is returned if hashing the decrypted data fails.
130/// - [`DecryptionError::KeyMismatch`] is returned if the decrypted data does
131///   not hash to `key`.
132pub 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]
155/// Extracts the raw ChaCha-encrypted content from the provided slice.
156///
157/// The trailing tag is stripped without being checked; [`validate`] checks it.
158/// # Errors
159/// Returns [`DecodeError`] if `data` is invalid or irrecoverably corrupted.
160pub fn extract_encrypted(data: &[u8]) -> Result<Codeword<'_>, DecodeError> {
161    decode(&data[..data.len().saturating_sub(TAG_SIZE)], PARITY)
162}
163
164#[must_use]
165/// Computes the tag of an ECC codeword: the low four bytes of its seeded
166/// XXH64 checksum.
167pub fn tag(codeword: &[u8]) -> [u8; TAG_SIZE] {
168    // The tag is the leading TAG_SIZE bytes of the little-endian checksum; a
169    // change to TAG_SIZE fails to compile here, forcing a deliberate decision.
170    let [tag @ .., _, _, _, _] = xxh64(codeword, TAG_SEED).to_le_bytes();
171
172    tag
173}
174
175/// The classification of a byte slice by [`validate`].
176#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
177pub enum Validity {
178    /// No ps-cypher codeword can be recovered from the data: it is too short
179    /// to carry a tag, or its ECC codeword fails to decode. Foreign ps-ecc
180    /// codewords and ps-cypher codewords damaged beyond the correction
181    /// capacity both land here and cannot be told apart.
182    Unrecoverable,
183    /// The ECC codeword decodes, but the tag does not match the corrected
184    /// codeword. A ps-cypher codeword whose tag itself was corrupted and
185    /// foreign data whose prefix happens to decode both land here and cannot
186    /// be told apart. [`decrypt`] may succeed anyway, since it ignores the tag.
187    TagMismatch,
188    /// The tag does not match the data as given but matches the codeword
189    /// after ECC correction, so [`decrypt`] repairs the corruption.
190    Corrupted,
191    /// The ECC codeword has zero syndromes and the tag matches, so the data
192    /// is byte for byte what [`encrypt`] produced.
193    Pristine,
194}
195
196#[must_use]
197/// Classifies `data` as a pristine ps-cypher codeword, a recoverably
198/// corrupted one, or neither.
199///
200/// The ECC codeword is verified and, if necessary, corrected; the tag is then
201/// checked against the corrected codeword. The check is unkeyed: it tells
202/// ps-cypher codewords apart from other data and detects accidental
203/// corruption, but it is not authentication. Authenticity is verified by the
204/// Poly1305 tag during [`decrypt`], which ignores the ps-cypher tag.
205///
206/// # Examples
207/// ```
208/// # use ps_cypher::{encrypt, validate, Validity};
209/// let data = b"important data";
210/// let encrypted = encrypt(data).expect("encryption failed");
211/// assert_eq!(validate(&encrypted), Validity::Pristine);
212/// ```
213pub 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    // Helper function to create a sample key (for testing purposes)
294    fn create_test_key() -> Hash {
295        hash("Hello, world!").expect("hashing test key should succeed")
296    }
297
298    /// Returns `len` deterministic, incompressible pseudo-random bytes.
299    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        // Basic check of the key and nonce values.
326        assert_eq!(&encryption_key[0..4], &[220, 186, 155, 106]); // First 4 bytes of key
327        assert_eq!(&nonce[0..4], &[46, 215, 220, 44]); // First 4 bytes of nonce
328    }
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(); // Use a different key.
359
360        let result = decrypt(&encrypted, &different_key);
361        assert!(result.is_err());
362        match result.unwrap_err() {
363            DecryptionError::ChaCha(_) => {} // Expected error type.
364            _ => 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        // Tamper with the encrypted data
373        encrypted.bytes[0] ^= 0x01; // Flip a bit
374
375        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; // Use the Deref trait
447        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        // Create a large amount of data (1MB)
467        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        // Verify the key field shows REDACTED, not actual bytes
515        assert!(
516            debug_output.contains("key: \"<REDACTED>\""),
517            "key field should be redacted"
518        );
519        // Verify other fields are present
520        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(); // Intentionally testing Clone trait
530        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        // Just verify ordering is consistent, not specific order
562        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        // Repetitive data should compress well
642        let data = vec![b'A'; 10000];
643        let encrypted = encrypt(&data).expect("encryption should succeed");
644
645        // Encrypted size should be significantly smaller due to compression
646        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        // Flip multiple bits in different bytes (within ECC correction capability)
697        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        // Test sizes around common boundaries
932        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        // Forge a ciphertext of different data under the original's key and nonce.
948        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        // 13 corruptions exceed the capacity of 12, so decryption must fail;
995        // the variant is unspecified, since Reed-Solomon may either fail to
996        // decode or miscorrect into a codeword that the AEAD then rejects.
997        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        // Corrupt 12 consecutive bytes inside the first segment, past the header.
1014        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        // Forge a ciphertext under the same key and nonce whose zstd frame
1030        // declares a content size far above the one-byte bound encoded in the key.
1031        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    /// Golden fixtures pinning the wire format of [`encrypt`].
1060    ///
1061    /// A change to any of these values is a deliberate format break tied to a
1062    /// version decision, not a routine update.
1063    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    /// The long fixture's input is `lcg_bytes(512)`.
1071    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    /// Decodes a lowercase hex string into bytes.
1078    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    /// Asserts that `input` encrypts to the pinned fixture and that the pinned
1086    /// ciphertext decrypts back to `input` under the pinned key.
1087    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}