#![cfg(all(feature = "aes", feature = "lzma2"))]
use std::io::Cursor;
use zesven::read::Archive;
use zesven::{ArchivePath, Error, Password, PasswordDetectionMethod, WriteOptions, Writer};
fn test_content() -> Vec<u8> {
b"This is secret content for encryption testing.".to_vec()
}
fn create_header_encrypted_archive(password: &str) -> Vec<u8> {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(WriteOptions::new().password(password).encrypt_header(true));
let path = ArchivePath::new("secret.txt").expect("Invalid path");
writer
.add_bytes(path, &test_content())
.expect("Failed to add entry");
let _ = writer.finish().expect("Failed to finish archive");
}
archive_bytes
}
#[test]
fn test_header_encryption_correct_password_succeeds() {
let password = "correct_password";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let archive =
Archive::open_with_password(cursor, password).expect("Should open with correct password");
assert_eq!(archive.len(), 1, "Should have 1 entry");
let entry = &archive.entries()[0];
assert_eq!(entry.path.as_str(), "secret.txt");
assert_eq!(entry.size, test_content().len() as u64);
}
#[test]
fn test_header_encryption_full_roundtrip() {
let password = "roundtrip_test_password";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive =
Archive::open_with_password(cursor, password).expect("Should open with correct password");
let info = archive.info();
assert_eq!(info.entry_count, 1);
assert!(
info.has_encrypted_header,
"Archive should report header is encrypted"
);
let entry = &archive.entries()[0];
assert_eq!(entry.path.as_str(), "secret.txt");
assert!(!entry.is_directory);
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with correct password");
assert_eq!(
extracted,
test_content(),
"Decrypted content should match original"
);
}
#[test]
fn test_header_encryption_wrong_password_rejected() {
let correct_password = "correct_password";
let wrong_password = "wrong_password";
let archive_bytes = create_header_encrypted_archive(correct_password);
let cursor = Cursor::new(&archive_bytes);
match Archive::open_with_password(cursor, wrong_password) {
Ok(_) => panic!("Opening header-encrypted archive with wrong password should fail"),
Err(Error::WrongPassword {
detection_method, ..
}) => {
assert_eq!(
detection_method,
PasswordDetectionMethod::EarlyHeaderValidation,
"Expected early header validation for header encryption"
);
}
Err(other) => {
assert!(
!other.to_string().is_empty(),
"rejection should carry a message"
);
}
}
}
#[test]
fn test_header_encryption_no_password() {
let password = "secret_password";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
match Archive::open(cursor) {
Ok(_) => {
panic!("Opening header-encrypted archive without password should fail");
}
Err(Error::PasswordRequired) => {
}
Err(e) => {
eprintln!("Got error (acceptable): {e}");
}
}
}
#[test]
fn test_header_encryption_empty_password_rejected() {
let correct_password = "non_empty_password";
let archive_bytes = create_header_encrypted_archive(correct_password);
let cursor = Cursor::new(&archive_bytes);
let result = Archive::open_with_password(cursor, "");
assert!(
result.is_err(),
"Empty password should not decrypt header-encrypted archive"
);
}
#[test]
fn test_header_encryption_long_password_succeeds() {
let password: String = "a".repeat(100);
let archive_bytes = create_header_encrypted_archive(&password);
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open_with_password(cursor, password.as_str())
.expect("Should open with correct long password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with correct password");
assert_eq!(extracted, test_content(), "Decrypted content should match");
}
#[test]
fn test_header_encryption_mixed_whitespace_password_succeeds() {
let password = " \t\n \t\n "; let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open_with_password(cursor, password)
.expect("Should open with mixed whitespace password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with mixed whitespace password");
assert_eq!(extracted, test_content(), "Decrypted content should match");
}
#[test]
fn test_write_with_empty_password_behavior() {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(WriteOptions::new().password("").encrypt_header(true));
let path = ArchivePath::new("test.txt").expect("valid path");
writer
.add_bytes(path, b"test content")
.expect("add_bytes should succeed with empty password");
let _result = writer
.finish()
.expect("finish should succeed with empty password");
}
let cursor = Cursor::new(&archive_bytes);
let open_result = Archive::open(cursor.clone());
assert!(
open_result.is_err(),
"Archive encrypted with empty password should require a password to open"
);
let mut archive = Archive::open_with_password(cursor, "")
.expect("Archive created with empty password should open with empty password");
assert!(
archive.info().has_encrypted_header,
"Archive should have encrypted header"
);
let extracted = archive
.extract_to_vec("test.txt")
.expect("Should extract file from archive opened with empty password");
assert_eq!(extracted, b"test content");
}
#[test]
fn test_data_encryption_roundtrip() {
let cursor = Cursor::new(Vec::new());
let password = "test_password";
let test_content = b"test content for data encryption";
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(
WriteOptions::new().password(password).encrypt_data(true), );
let path = ArchivePath::new("test.txt").expect("valid path");
writer
.add_bytes(path, test_content)
.expect("Should write encrypted content");
let (_result, cursor) = writer.finish_into_inner().expect("Should finish archive");
let archive_data = cursor.into_inner();
let mut archive =
Archive::open_with_password(Cursor::new(archive_data), Password::new(password))
.expect("Should open encrypted archive");
assert_eq!(
archive.entries()[0].size,
test_content.len() as u64,
"Encrypted entry should report its original size"
);
let extracted = archive
.extract_to_vec("test.txt")
.expect("Should extract encrypted content");
assert_eq!(
extracted, test_content,
"Decrypted content should match original"
);
}
#[test]
fn test_filtered_data_encryption_reports_original_size() {
use zesven::WriteFilter;
let cursor = Cursor::new(Vec::new());
let password = "test_password";
let test_content: Vec<u8> = (0..1024u32)
.map(|i| if i % 16 == 0 { 0xE8 } else { (i % 251) as u8 })
.collect();
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(
WriteOptions::new()
.password(password)
.filter(WriteFilter::BcjX86)
.encrypt_data(true),
);
let path = ArchivePath::new("program.bin").expect("valid path");
writer
.add_bytes(path, &test_content)
.expect("Should write filtered encrypted content");
let (_result, cursor) = writer.finish_into_inner().expect("Should finish archive");
let mut archive =
Archive::open_with_password(Cursor::new(cursor.into_inner()), Password::new(password))
.expect("Should open encrypted archive");
assert_eq!(
archive.entries()[0].size,
test_content.len() as u64,
"Filtered encrypted entry should report its original size"
);
let extracted = archive
.extract_to_vec("program.bin")
.expect("Should extract filtered encrypted content");
assert_eq!(extracted, test_content);
}
#[test]
fn test_password_encoding_edge_cases() {
let password = "test\0null\0bytes";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive =
Archive::open_with_password(cursor, password).expect("Should open with null byte password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with null byte password");
assert_eq!(extracted, test_content());
}
#[test]
fn test_password_unicode_codepoints() {
let test_cases = [
("cafe\u{0301}", "combining character"),
(
"family\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}",
"emoji sequence",
),
("كلمة_السر_123", "RTL characters"),
("test\u{1F4A9}\u{10000}end", "supplementary characters"),
];
for (password, description) in test_cases {
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open_with_password(cursor, password)
.unwrap_or_else(|e| panic!("Should open with {} password: {:?}", description, e));
let extracted = archive
.extract_to_vec("secret.txt")
.unwrap_or_else(|e| panic!("Should extract with {} password: {:?}", description, e));
assert_eq!(
extracted,
test_content(),
"Content mismatch for {} password",
description
);
}
}
#[test]
fn test_unicode_normalization_treated_as_different() {
let password_nfc = "caf\u{00E9}";
let password_nfd = "cafe\u{0301}";
assert_ne!(
password_nfc.as_bytes(),
password_nfd.as_bytes(),
"NFC and NFD should be different byte sequences"
);
let archive_bytes = create_header_encrypted_archive(password_nfc);
let cursor = Cursor::new(&archive_bytes);
let result_nfc = Archive::open_with_password(cursor, password_nfc);
assert!(result_nfc.is_ok(), "NFC password should work");
let cursor = Cursor::new(&archive_bytes);
let result_nfd = Archive::open_with_password(cursor, password_nfd);
assert!(
result_nfd.is_err(),
"NFD password should fail (different from NFC)"
);
}
#[test]
fn test_header_encryption_records_header_crc() {
let archive_bytes = create_header_encrypted_archive("right_password");
let offset = u64::from_le_bytes(archive_bytes[12..20].try_into().unwrap()) as usize + 32;
let size = u64::from_le_bytes(archive_bytes[20..28].try_into().unwrap()) as usize;
let structure = &archive_bytes[offset..offset + size];
assert!(
structure.windows(2).any(|w| w == [0x0A, 0x01]),
"the encrypted header must record the CRC of what it decodes to"
);
let cursor = Cursor::new(&archive_bytes);
match Archive::open_with_password(cursor, "wrong_password") {
Ok(_) => panic!("a wrong password must be rejected"),
Err(error) => assert!(!error.to_string().is_empty()),
}
}
#[test]
fn test_one_key_derivation_per_archive() {
use zesven::crypto::AesProperties;
use zesven::format::parser::read_archive_header_with_password;
let password = "shared_password";
let cursor = Cursor::new(Vec::new());
let mut writer = Writer::create(cursor)
.expect("create writer")
.options(WriteOptions::new().password(password));
for i in 0..8 {
writer
.add_bytes(
ArchivePath::new(&format!("file{i}.txt")).expect("valid path"),
format!("payload {i}").as_bytes(),
)
.expect("add entry");
}
let (_result, cursor) = writer.finish_into_inner().expect("finish");
let archive_bytes = cursor.into_inner();
let (_start, header) = read_archive_header_with_password(
&mut Cursor::new(&archive_bytes),
None,
Some(Password::new(password)),
)
.expect("read header");
let mut salts = Vec::new();
let mut ivs = Vec::new();
for folder in &header.unpack_info.expect("unpack info").folders {
for coder in &folder.coders {
if coder.method_id.as_slice() != [0x06, 0xF1, 0x07, 0x01] {
continue;
}
let props = AesProperties::parse(coder.properties.as_deref().unwrap_or(&[]))
.expect("valid AES properties");
salts.push(props.salt);
ivs.push(props.iv);
}
}
assert!(salts.len() >= 8, "expected one AES coder per entry");
assert!(
salts.windows(2).all(|w| w[0] == w[1]),
"every stream carries its own salt, so the key is derived per stream"
);
assert_eq!(
ivs.iter().collect::<std::collections::HashSet<_>>().len(),
ivs.len(),
"each stream needs its own IV"
);
}