#![doc = include_str!("../README.md")]
mod error;
pub use error::{DecryptionError, EncryptionError};
pub use ps_buffer::Buffer;
use chacha20poly1305::aead::{Aead, KeyInit};
use chacha20poly1305::ChaCha20Poly1305;
use ps_compress::{compress, decompress_bounded};
use ps_ecc::{decode, encode, Codeword, DecodeError};
use ps_hash::Hash;
use std::ops::Deref;
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Encrypted {
pub bytes: Buffer,
pub hash: Hash,
pub key: Hash,
}
impl std::fmt::Debug for Encrypted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Encrypted")
.field("bytes", &self.bytes)
.field("hash", &self.hash)
.field("key", &"<REDACTED>")
.finish()
}
}
const KSIZE: usize = 32;
const NSIZE: usize = 12;
const PARITY: u8 = 12;
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ParsedKey {
key: [u8; KSIZE],
nonce: [u8; NSIZE],
length: usize,
}
impl std::fmt::Debug for ParsedKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParsedKey")
.field("key", &"<REDACTED>")
.field("nonce", &self.nonce)
.field("length", &self.length)
.finish()
}
}
impl From<&Hash> for ParsedKey {
fn from(value: &Hash) -> Self {
// The nonce is the trailing NSIZE of the hash's parity bytes; a change
// to either size fails to compile here rather than silently shifting it.
let [_, _, nonce @ ..] = *value.parity();
Self {
key: *value.digest(),
length: value.data_max_len().to_usize(),
nonce,
}
}
}
/// Encrypts `data` using convergent encryption: the key is the hash of `data`.
/// # Errors
/// - [`EncryptionError::Compression`] is returned if compression fails.
/// - [`EncryptionError::Hash`] is returned if hashing fails.
/// - [`EncryptionError::ChaCha`] is returned if encryption fails.
/// - [`EncryptionError::Ecc`] is returned if ECC encoding fails.
pub fn encrypt(data: &[u8]) -> Result<Encrypted, EncryptionError> {
let compressed_data = compress(data)?;
let hash_of_raw_data = ps_hash::hash(data)?;
let ParsedKey {
key: encryption_key,
length: _,
nonce,
} = (&hash_of_raw_data).into();
let chacha = ChaCha20Poly1305::new(&encryption_key.into());
let encrypted_data = chacha
.encrypt(&nonce.into(), compressed_data.as_ref())
.map_err(EncryptionError::ChaCha)?;
let bytes = encode(&encrypted_data, PARITY)?;
let hash = Hash::hash(&bytes)?;
let encrypted = Encrypted {
bytes,
hash,
key: hash_of_raw_data,
};
Ok(encrypted)
}
/// Decrypts `data` using `key`, repairing up to 12 corrupted bytes per codeword.
///
/// The decrypted data is verified to hash to `key`, so a successful decryption
/// yields exactly the data that produced `key`.
/// # Errors
/// - [`DecryptionError::Ecc`] is returned if `data` is irrecoverably corrupted.
/// - [`DecryptionError::ChaCha`] is returned if decryption fails.
/// - [`DecryptionError::Decompression`] is returned if decompression fails.
/// - [`DecryptionError::Hash`] is returned if hashing the decrypted data fails.
/// - [`DecryptionError::KeyMismatch`] is returned if the decrypted data does
/// not hash to `key`.
pub fn decrypt(data: &[u8], key: &Hash) -> Result<Buffer, DecryptionError> {
let ParsedKey {
key: encryption_key,
length: out_size,
nonce,
} = key.into();
let ecc_decoded = extract_encrypted(data)?;
let chacha = ChaCha20Poly1305::new(&encryption_key.into());
let compressed_data = chacha
.decrypt(&nonce.into(), &ecc_decoded[..])
.map_err(DecryptionError::ChaCha)?;
let plaintext = decompress_bounded(&compressed_data, out_size)?;
if ps_hash::hash(&plaintext[..])? != *key {
return Err(DecryptionError::KeyMismatch);
}
Ok(plaintext)
}
#[inline]
/// Extracts the raw ChaCha-encrypted content from the provided slice.
/// # Errors
/// Returns [`DecodeError`] if `data` is invalid or irrecoverably corrupted.
pub fn extract_encrypted(data: &[u8]) -> Result<Codeword<'_>, DecodeError> {
decode(data, PARITY)
}
#[inline]
#[must_use]
/// Checks whether `data` is a pristine ECC codeword.
///
/// This is an unkeyed Reed-Solomon syndrome check: it detects accidental
/// corruption, not tampering. Authenticity is verified by the Poly1305 tag
/// during [`decrypt`]. Returns `false` for corrupted-but-correctable
/// codewords that [`decrypt`] still accepts.
///
/// # Examples
/// ```
/// # use ps_cypher::{encrypt, validate_ecc};
/// let data = b"important data";
/// let encrypted = encrypt(data).expect("encryption failed");
/// assert!(validate_ecc(&encrypted));
/// ```
pub fn validate_ecc(data: &[u8]) -> bool {
ps_ecc::validate(data, PARITY)
}
impl AsRef<[u8]> for Encrypted {
fn as_ref(&self) -> &[u8] {
self
}
}
impl Deref for Encrypted {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.bytes
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::panic)]
#[allow(clippy::unwrap_used)]
mod tests {
use ps_buffer::ToBuffer;
use ps_compress::DecompressionError;
use ps_hash::hash;
use super::*;
#[test]
fn test_encrypt_and_decrypt() {
let original_data = b"Hello, World!";
let encrypted_data = encrypt(original_data).expect("encryption should succeed");
let decrypted_data =
decrypt(&encrypted_data.bytes, &encrypted_data.key).expect("decryption should succeed");
assert_ne!(
original_data
.to_buffer()
.expect("conversion to buffer should succeed"),
encrypted_data.bytes,
"Encryption should modify the data"
);
let ecc_payload = extract_encrypted(&encrypted_data.bytes)
.expect("extracting ECC payload should succeed");
assert_eq!(
encrypted_data.bytes.len(),
ecc_payload.len() + 2 * usize::from(PARITY),
"ECC encoding should add parity bytes"
);
assert_eq!(
original_data,
&decrypted_data[..],
"Decryption should reverse encryption"
);
}
// Helper function to create a sample key (for testing purposes)
fn create_test_key() -> Hash {
hash("Hello, world!").expect("hashing test key should succeed")
}
/// Returns `len` deterministic, incompressible pseudo-random bytes.
fn lcg_bytes(len: usize) -> Vec<u8> {
let mut state = 0x243F_6A88_85A3_08D3_u64;
(0..len)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
state.to_be_bytes()[0]
})
.collect()
}
#[test]
fn test_parse_key() {
let key = &create_test_key();
let ParsedKey {
key: encryption_key,
length: _,
nonce,
} = key.into();
assert_eq!(encryption_key.len(), 32);
assert_eq!(nonce.len(), 12);
// Basic check of the key and nonce values.
assert_eq!(&encryption_key[0..4], &[220, 186, 155, 106]); // First 4 bytes of key
assert_eq!(&nonce[0..4], &[46, 215, 220, 44]); // First 4 bytes of nonce
}
#[test]
fn test_encrypt_decrypt() {
let data = b"This is some data to encrypt";
let encrypted = encrypt(data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_encrypt_decrypt_empty_data() {
let data = b"";
let encrypted = encrypt(data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_encrypt_decrypt_long_data() {
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();
let encrypted = encrypt(data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_encrypt_decrypt_different_key() {
let data = b"This is some data";
let encrypted = encrypt(data).expect("encryption should succeed");
let different_key = create_test_key(); // Use a different key.
let result = decrypt(&encrypted, &different_key);
assert!(result.is_err());
match result.unwrap_err() {
DecryptionError::ChaCha(_) => {} // Expected error type.
_ => panic!("Unexpected error type"),
}
}
#[test]
fn test_encrypt_decrypt_tampered_data() {
let data = b"This is some data";
let mut encrypted = encrypt(data).expect("encryption should succeed");
// Tamper with the encrypted data
encrypted.bytes[0] ^= 0x01; // Flip a bit
let decrypted = decrypt(&encrypted, &encrypted.key)
.expect("decryption should succeed after ECC correction");
assert_eq!(decrypted.slice(..), data);
}
#[test]
fn test_validate_ecc_for_valid_and_truncated_data() {
let data = b"ECC validation data";
let encrypted = encrypt(data).expect("encryption should succeed");
assert!(validate_ecc(&encrypted), "fresh ciphertext should validate");
let truncated = &encrypted.bytes[..encrypted.bytes.len() - 1];
assert!(
!validate_ecc(truncated),
"truncated ciphertext should not validate"
);
}
#[test]
fn test_extract_encrypted_rejects_truncated_payload() {
let data = b"payload";
let encrypted = encrypt(data).expect("encryption should succeed");
let truncated = &encrypted.bytes[..encrypted.bytes.len() - 1];
let result = extract_encrypted(truncated);
assert!(result.is_err(), "truncated payload must fail ECC decode");
}
#[test]
fn test_decrypt_truncated_payload_returns_ecc_error() {
let data = b"payload";
let encrypted = encrypt(data).expect("encryption should succeed");
let truncated = &encrypted.bytes[..encrypted.bytes.len() - 1];
let result = decrypt(truncated, &encrypted.key);
assert!(
matches!(result, Err(DecryptionError::Ecc(_))),
"truncated payload should surface as ECC error"
);
}
#[test]
fn test_encrypted_hash_matches_ciphertext_bytes() {
let data = b"hash check";
let encrypted = encrypt(data).expect("encryption should succeed");
let recalculated = Hash::hash(&encrypted.bytes).expect("hashing bytes should succeed");
assert_eq!(encrypted.hash, recalculated);
}
#[test]
fn test_as_ref_encrypted() {
let data = b"Test data";
let encrypted = encrypt(data).expect("encryption should succeed");
let as_ref_data: &[u8] = encrypted.as_ref();
assert_eq!(as_ref_data, &*encrypted);
assert_eq!(as_ref_data, &encrypted.bytes[..]);
}
#[test]
fn test_deref_encrypted() {
let data = b"More test data";
let encrypted = encrypt(data).expect("encryption should succeed");
let deref_data: &[u8] = &encrypted; // Use the Deref trait
assert_eq!(deref_data, &encrypted.bytes[..]);
}
#[test]
fn test_key_from_hash() {
let data = b"Test data for key derivation";
let h = hash(data).expect("hashing should succeed");
let ParsedKey {
key,
length: _,
nonce: _,
} = (&h).into();
assert_eq!(key.len(), 32);
}
#[test]
fn test_encrypt_large_data() {
// Create a large amount of data (1MB)
let data = vec![b'A'; 1024 * 1024];
let encrypted = encrypt(&data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, &data[..]);
}
#[test]
fn test_ps_cypher_error_display() {
let data = b"test";
let encrypted = encrypt(data).expect("encryption should succeed");
let bad_key = hash(b"invalid_key").expect("hashing should succeed");
let result = decrypt(&encrypted, &bad_key);
if let Err(e) = result {
let error_message = format!("{e}");
assert_eq!(error_message, "decryption failed (chacha20poly1305)");
} else {
panic!("Expected an error, but got success");
}
}
#[test]
fn test_ps_cypher_error_source() {
let data = b"test";
let encrypted = encrypt(data).expect("encryption should succeed");
let bad_key = hash(b"invalid_key").expect("hashing should succeed");
let result = decrypt(&encrypted, &bad_key);
if let Err(e) = result {
let source = std::error::Error::source(&e).expect("ChaCha error should have a source");
assert_eq!(format!("{source}"), "aead::Error");
} else {
panic!("Expected an error, but got success");
}
}
#[test]
fn test_parsed_key_debug_redacts_key() {
let key = create_test_key();
let parsed: ParsedKey = (&key).into();
let debug_output = format!("{parsed:?}");
assert!(
debug_output.contains("<REDACTED>"),
"Debug output should redact the key"
);
// Verify the key field shows REDACTED, not actual bytes
assert!(
debug_output.contains("key: \"<REDACTED>\""),
"key field should be redacted"
);
// Verify other fields are present
assert!(debug_output.contains("nonce:"), "nonce should be present");
assert!(debug_output.contains("length:"), "length should be present");
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_parsed_key_clone_and_copy() {
let key = create_test_key();
let parsed: ParsedKey = (&key).into();
let cloned = parsed.clone(); // Intentionally testing Clone trait
let copied = parsed;
assert_eq!(parsed, cloned);
assert_eq!(parsed, copied);
}
#[test]
fn test_parsed_key_hash_trait() {
use std::collections::HashSet;
let key1 = create_test_key();
let key2 = hash(b"different data").expect("hashing should succeed");
let parsed1: ParsedKey = (&key1).into();
let parsed2: ParsedKey = (&key2).into();
let mut set = HashSet::new();
set.insert(parsed1);
set.insert(parsed2);
assert_eq!(set.len(), 2, "different keys should hash differently");
}
#[test]
fn test_parsed_key_ordering() {
let key1 = hash(b"aaa").expect("hashing should succeed");
let key2 = hash(b"bbb").expect("hashing should succeed");
let parsed1: ParsedKey = (&key1).into();
let parsed2: ParsedKey = (&key2).into();
// Just verify ordering is consistent, not specific order
let cmp1 = parsed1.cmp(&parsed2);
let cmp2 = parsed2.cmp(&parsed1);
assert_eq!(cmp1.reverse(), cmp2);
}
#[test]
fn test_encrypted_hash_trait() {
use std::collections::HashSet;
let encrypted1 = encrypt(b"data1").expect("encryption should succeed");
let encrypted2 = encrypt(b"data2").expect("encryption should succeed");
let mut set = HashSet::new();
set.insert(encrypted1);
set.insert(encrypted2);
assert_eq!(
set.len(),
2,
"different encryptions should hash differently"
);
}
#[test]
fn test_encrypted_ordering() {
let encrypted1 = encrypt(b"aaa").expect("encryption should succeed");
let encrypted2 = encrypt(b"bbb").expect("encryption should succeed");
let cmp1 = encrypted1.cmp(&encrypted2);
let cmp2 = encrypted2.cmp(&encrypted1);
assert_eq!(cmp1.reverse(), cmp2);
}
#[test]
fn test_encrypted_equality() {
let data = b"same data";
let encrypted1 = encrypt(data).expect("encryption should succeed");
let encrypted2 = encrypt(data).expect("encryption should succeed");
assert_eq!(
encrypted1, encrypted2,
"same input should produce equal encryptions"
);
}
#[test]
fn test_encrypt_decrypt_single_byte() {
let data = b"x";
let encrypted = encrypt(data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_encrypt_decrypt_binary_with_nulls() {
let data: &[u8] = &[0x00, 0x01, 0x00, 0xFF, 0x00, 0xFE, 0x00];
let encrypted = encrypt(data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_encrypt_decrypt_all_byte_values() {
let data: Vec<u8> = (0u8..=255).collect();
let encrypted = encrypt(&data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, &data[..]);
}
#[test]
fn test_encrypt_decrypt_unicode() {
let data = "Hello 世界! 🎉 Привет мир".as_bytes();
let encrypted = encrypt(data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_encrypt_decrypt_highly_compressible_data() {
// Repetitive data should compress well
let data = vec![b'A'; 10000];
let encrypted = encrypt(&data).expect("encryption should succeed");
// Encrypted size should be significantly smaller due to compression
assert!(
encrypted.bytes.len() < data.len(),
"highly compressible data should result in smaller ciphertext"
);
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, &data[..]);
}
#[test]
fn test_encryption_determinism() {
let data = b"deterministic test data";
let encrypted1 = encrypt(data).expect("encryption should succeed");
let encrypted2 = encrypt(data).expect("encryption should succeed");
assert_eq!(
encrypted1.bytes, encrypted2.bytes,
"same input should produce identical ciphertext"
);
assert_eq!(
encrypted1.key, encrypted2.key,
"same input should produce identical key"
);
assert_eq!(
encrypted1.hash, encrypted2.hash,
"same input should produce identical hash"
);
}
#[test]
fn test_different_inputs_produce_different_outputs() {
let encrypted1 = encrypt(b"input 1").expect("encryption should succeed");
let encrypted2 = encrypt(b"input 2").expect("encryption should succeed");
assert_ne!(
encrypted1.bytes, encrypted2.bytes,
"different inputs should produce different ciphertexts"
);
assert_ne!(
encrypted1.key, encrypted2.key,
"different inputs should produce different keys"
);
}
#[test]
fn test_ecc_corrects_multiple_bit_errors() {
let data = b"ECC multi-bit correction test";
let mut encrypted = encrypt(data).expect("encryption should succeed");
// Flip multiple bits in different bytes (within ECC correction capability)
encrypted.bytes[0] ^= 0x01;
encrypted.bytes[1] ^= 0x02;
encrypted.bytes[2] ^= 0x04;
let decrypted = decrypt(&encrypted, &encrypted.key)
.expect("decryption should succeed with ECC correction");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_validate_ecc_detects_corruption_within_capability() {
let data = b"ECC validation test";
let mut encrypted = encrypt(data).expect("encryption should succeed");
encrypted.bytes[5] ^= 0x01;
assert!(
!validate_ecc(&encrypted),
"corrupted codeword should not validate as pristine"
);
let decrypted =
decrypt(&encrypted, &encrypted.key).expect("decryption should correct the corruption");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_empty_slice_validation() {
assert!(
!validate_ecc(&[]),
"empty slice should not validate as valid ECC"
);
}
#[test]
fn test_extract_encrypted_empty_slice() {
let result = extract_encrypted(&[]);
assert!(result.is_err(), "empty slice should fail extraction");
}
#[test]
fn test_decrypt_empty_slice() {
let key = create_test_key();
let result = decrypt(&[], &key);
assert!(
matches!(result, Err(DecryptionError::Ecc(_))),
"empty slice should return ECC error"
);
}
#[test]
fn test_decryption_error_clone() {
let data = b"test";
let encrypted = encrypt(data).expect("encryption should succeed");
let bad_key = hash(b"wrong_key").expect("hashing should succeed");
let result = decrypt(&encrypted, &bad_key);
if let Err(e) = result {
let cloned = e.clone();
assert_eq!(format!("{e}"), format!("{cloned}"));
} else {
panic!("Expected decryption error");
}
}
#[test]
fn test_chacha_error_display_and_source() {
let encryption_error = EncryptionError::ChaCha(chacha20poly1305::Error);
let decryption_error = DecryptionError::ChaCha(chacha20poly1305::Error);
assert_eq!(
format!("{encryption_error}"),
"encryption failed (chacha20poly1305)"
);
assert_eq!(
format!("{decryption_error}"),
"decryption failed (chacha20poly1305)"
);
assert!(std::error::Error::source(&encryption_error).is_some());
assert!(std::error::Error::source(&decryption_error).is_some());
}
#[test]
fn test_decryption_error_debug() {
let err = DecryptionError::ChaCha(chacha20poly1305::Error);
let debug_output = format!("{err:?}");
assert!(debug_output.contains("ChaCha"));
}
#[test]
fn test_encryption_error_debug() {
let err = EncryptionError::ChaCha(chacha20poly1305::Error);
let debug_output = format!("{err:?}");
assert!(debug_output.contains("ChaCha"));
}
#[test]
fn test_encrypted_debug_redacts_key() {
let encrypted = encrypt(b"debug test").expect("encryption should succeed");
let debug_output = format!("{encrypted:?}");
assert!(debug_output.contains("Encrypted"));
assert!(debug_output.contains("bytes"));
assert!(debug_output.contains("hash"));
assert!(
debug_output.contains("key: \"<REDACTED>\""),
"key field should be redacted"
);
assert!(
!debug_output.contains(&encrypted.key.to_string()),
"Debug output must not leak the key"
);
}
#[test]
fn test_parsed_key_same_hash_produces_same_key() {
let h = hash(b"consistent").expect("hashing should succeed");
let parsed1: ParsedKey = (&h).into();
let parsed2: ParsedKey = (&h).into();
assert_eq!(parsed1, parsed2);
}
#[test]
fn test_encrypt_decrypt_powers_of_two_sizes() {
for power in 0..=10 {
let size = 1 << power;
let data = vec![0xAB_u8; size];
let encrypted = encrypt(&data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, &data[..], "failed for size {size}");
}
}
#[test]
fn test_encrypt_decrypt_boundary_sizes() {
// Test sizes around common boundaries
for size in [
127, 128, 129, 255, 256, 257, 511, 512, 513, 1023, 1024, 1025,
] {
let data = vec![0xCD_u8; size];
let encrypted = encrypt(&data).expect("encryption should succeed");
let decrypted = decrypt(&encrypted, &encrypted.key).expect("decryption should succeed");
assert_eq!(&*decrypted, &data[..], "failed for size {size}");
}
}
#[test]
fn test_decrypt_rejects_substituted_plaintext() {
let original = b"the original plaintext, which is longer than the forgery";
let key_hash = hash(original).expect("hashing should succeed");
// Forge a ciphertext of different data under the original's key and nonce.
let forged = compress(b"forged").expect("compression should succeed");
let ParsedKey {
key,
nonce,
length: _,
} = (&key_hash).into();
let chacha = ChaCha20Poly1305::new(&key.into());
let ciphertext = chacha
.encrypt(&nonce.into(), forged.as_ref())
.expect("forged encryption should succeed");
let bytes = encode(&ciphertext, PARITY).expect("ECC encoding should succeed");
let result = decrypt(&bytes, &key_hash);
assert!(
matches!(result, Err(DecryptionError::KeyMismatch)),
"substituted plaintext must be rejected, got {result:?}"
);
}
#[test]
fn test_decrypt_succeeds_at_ecc_capacity() {
let data = b"ECC capacity boundary test";
let mut encrypted = encrypt(data).expect("encryption should succeed");
for byte in encrypted.bytes.iter_mut().take(12) {
*byte ^= 0xFF;
}
let decrypted = decrypt(&encrypted, &encrypted.key)
.expect("12 corrupted bytes should be within ECC capacity");
assert_eq!(&*decrypted, data);
}
#[test]
fn test_decrypt_fails_beyond_ecc_capacity() {
let data = b"ECC capacity boundary test";
let mut encrypted = encrypt(data).expect("encryption should succeed");
for byte in encrypted.bytes.iter_mut().take(13) {
*byte ^= 0xFF;
}
// 13 corruptions exceed the capacity of 12, so decryption must fail;
// the variant is unspecified, since Reed-Solomon may either fail to
// decode or miscorrect into a codeword that the AEAD then rejects.
assert!(
decrypt(&encrypted, &encrypted.key).is_err(),
"13 corrupted bytes must exceed ECC capacity"
);
}
#[test]
fn test_decrypt_corrects_long_format_corruption() {
let data = lcg_bytes(512);
let mut encrypted = encrypt(&data).expect("encryption should succeed");
assert!(
encrypted.bytes.len() > 255,
"incompressible 512-byte input should use the long ECC format"
);
// Corrupt 12 consecutive bytes inside the first segment, past the header.
for byte in encrypted.bytes.iter_mut().skip(40).take(12) {
*byte ^= 0xFF;
}
let decrypted = decrypt(&encrypted, &encrypted.key)
.expect("long-format corruption within capacity should be corrected");
assert_eq!(&*decrypted, &data[..]);
}
#[test]
fn test_decrypt_enforces_length_bound_from_key() {
let short = b"S";
let key_hash = hash(short).expect("hashing should succeed");
// Forge a ciphertext under the same key and nonce whose zstd frame
// declares a content size far above the one-byte bound encoded in the key.
let oversized = lcg_bytes(2000);
let compressed = compress(&oversized).expect("compression should succeed");
let ParsedKey {
key,
nonce,
length: _,
} = (&key_hash).into();
let chacha = ChaCha20Poly1305::new(&key.into());
let ciphertext = chacha
.encrypt(&nonce.into(), compressed.as_ref())
.expect("forged encryption should succeed");
let bytes = encode(&ciphertext, PARITY).expect("ECC encoding should succeed");
let result = decrypt(&bytes, &key_hash);
assert!(
matches!(
result,
Err(DecryptionError::Decompression(
DecompressionError::TooLarge { .. }
))
),
"oversized frame must be rejected before allocation, got {result:?}"
);
}
/// Golden fixtures pinning the wire format of [`encrypt`].
///
/// A change to any of these values is a deliberate format break tied to a
/// version decision, not a routine update.
const GOLDEN_SHORT_INPUT: &[u8] = b"Hello, World!";
const GOLDEN_SHORT_CIPHERTEXT_HEX: &str = "fe4ffee7417540f84292fd29e36031c85e6ba66113ed6cdf1d0d326f7d04516ce1c964c52e4fdfa78192d486e464bdba1a75ef1b741fcda668698fda1247";
const GOLDEN_SHORT_KEY: &str =
"YXVYD1H41DV6CWXAN3KS683DEH4YGQRQF86Q6P6J8BMEYMZSC2BGT045TN01ZAW1XC9684A6QE52J";
const GOLDEN_SHORT_HASH: &str =
"Y5WEEW9MQT4476NRH2YPCHGD3W2PK002WQG8R1MFJ5AKM09Q48P3W03GG9WV81DJQM5KYSXNDNWZG";
/// The long fixture's input is `lcg_bytes(512)`.
const GOLDEN_LONG_CIPHERTEXT_HEX: &str = "4c48010c000002820000021a22bfd2fdc12d099939a9694c6af640a8b52d1bf6942cda1e9bb0e87df73270d25cc9bc7b096447bae9cc2234f49665228176f93c66f0ae1fcde50bd86cbef3f39a085d99557c20e8772294a1b7db41016e428daadd949841ee5b358be5e5bcb13b63f6e15beb8344e60d9d9d7c738a8267fb2166db2bd8166606d8a619a23c66f2a1fd1dbdc12b76c89b2407b3ee0fbf4b85478828e3b3d3a153da528165888ceaf58aa6b399c6f2b3a4197e2013c7d50412be066e0a561105ddda6239968896e62c9dda16c0d1af78bbd3130b8cb9bcbb6c18a4f7954486832e8cfa4103b3ac0960da78e1a16abdedb75874be3442bf702a1a6ac4507aab3d91e88ff5bbf3251092f6b8a75263ff3ee1a34ba54bf8edcc6025eb1b7af4e9da309a2504d6715b8f8445a45ab66ab4a96f68c0fdcbb526b1b57de9a44137fe925d6bb848692ba881278ea4c73e33d6809f9f9f073026d45587e0913e3392b53702cbc9c2eb1de53147a896800efef0ac9e0ad9581b6b61ed6297bf1fd5e02adce0d180503faa634af913f36ff0794e5e57c9b75ab8ee2b42eaaceb26622ba509c01148f9a7d5de62fb43ab79fb6f03536ac535fbd229110e39b971d728dc422777448036652e5c795ce69d3884e7c3eeb32f5722e5273948e737ba586dc45cb407b2c4ac867075f6b39f8db83b8a89b353075a9e25bdeec0a2ad3759cf67b17f1d2467115b9845570a6f79bd10df8bf45160f43cc43da9a048f6acab8a10d615e134b80ac7beaaf5836322ca2418e44c5525249ece6161f3f322ace5ae931bc86b0c9223ba97ed60b971b76c3e13c24d86b685eee38b06f1f04465e893c46308f518317b0ad258576ea093545ce5bf13a8e7a1386c4afc2cafdfb9ae60";
const GOLDEN_LONG_KEY: &str =
"V2Q9QNXEKT9EMZ9C1FEDWSX1P2A516SG5DRFN0Z4NBH2DRHWKCHG00JQ83X9238V9014AP55BFDWG";
const GOLDEN_LONG_HASH: &str =
"PHHNMJ91KE5VDB3MHHAYFRMMDGW7J1MGKWFE32H9EEFT7NE2GF5420MVEJAP2GFCMY6CPGJ0G1A36";
/// Decodes a lowercase hex string into bytes.
fn hex_decode(hex: &str) -> Vec<u8> {
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("fixture hex should be valid"))
.collect()
}
/// Asserts that `input` encrypts to the pinned fixture and that the pinned
/// ciphertext decrypts back to `input` under the pinned key.
fn check_golden(input: &[u8], ciphertext_hex: &str, key_str: &str, hash_str: &str) {
let expected_bytes = hex_decode(ciphertext_hex);
let encrypted = encrypt(input).expect("encryption should succeed");
assert_eq!(
&encrypted.bytes[..],
&expected_bytes[..],
"ciphertext should match the golden fixture"
);
assert_eq!(
encrypted.key.to_string(),
key_str,
"key should match the golden fixture"
);
assert_eq!(
encrypted.hash.to_string(),
hash_str,
"hash should match the golden fixture"
);
let key = Hash::try_from(key_str).expect("fixture key should parse");
let decrypted = decrypt(&expected_bytes, &key).expect("decryption should succeed");
assert_eq!(
&decrypted[..],
input,
"decrypting the golden ciphertext should yield the input"
);
}
#[test]
fn test_golden_short_ciphertext() {
check_golden(
GOLDEN_SHORT_INPUT,
GOLDEN_SHORT_CIPHERTEXT_HEX,
GOLDEN_SHORT_KEY,
GOLDEN_SHORT_HASH,
);
}
#[test]
fn test_golden_long_ciphertext() {
let input = lcg_bytes(512);
assert!(
hex_decode(GOLDEN_LONG_CIPHERTEXT_HEX).len() > 255,
"long fixture should use the long ECC format"
);
check_golden(
&input,
GOLDEN_LONG_CIPHERTEXT_HEX,
GOLDEN_LONG_KEY,
GOLDEN_LONG_HASH,
);
}
}