diff --git a/src/crypto.rs b/src/crypto.rs index 2fed43b..871e199 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -80,15 +80,22 @@ pub fn sha256_hash(data: &[u8]) -> [u8; 32] { #[cfg(test)] mod tests { use super::*; - use crate::key::KEY; use hex_literal::hex; + /// Test key matching legacy hardcoded value + const TEST_KEY: [u8; 32] = [ + 0x7A, 0x35, 0xC1, 0xD9, 0x4F, 0xE8, 0x2B, 0x6A, + 0x91, 0x0D, 0xF3, 0x58, 0xBC, 0x74, 0xA6, 0x1E, + 0x42, 0x8F, 0xD0, 0x63, 0xE5, 0x17, 0x9B, 0x2C, + 0xFA, 0x84, 0x06, 0xCD, 0x3E, 0x79, 0xB5, 0x50, + ]; + #[test] fn test_encrypt_decrypt_roundtrip() { let plaintext = b"Hello, World!"; let iv = [0u8; 16]; - let ciphertext = encrypt_data(plaintext, &KEY, &iv); - let decrypted = decrypt_data(&ciphertext, &KEY, &iv).unwrap(); + let ciphertext = encrypt_data(plaintext, &TEST_KEY, &iv); + let decrypted = decrypt_data(&ciphertext, &TEST_KEY, &iv).unwrap(); assert_eq!(decrypted, plaintext); } @@ -96,8 +103,8 @@ mod tests { fn test_encrypt_decrypt_empty() { let plaintext = b""; let iv = [0u8; 16]; - let ciphertext = encrypt_data(plaintext, &KEY, &iv); - let decrypted = decrypt_data(&ciphertext, &KEY, &iv).unwrap(); + let ciphertext = encrypt_data(plaintext, &TEST_KEY, &iv); + let decrypted = decrypt_data(&ciphertext, &TEST_KEY, &iv).unwrap(); assert_eq!(decrypted, plaintext.as_slice()); } @@ -105,23 +112,23 @@ mod tests { fn test_encrypted_size_formula() { let iv = [0u8; 16]; // 5 bytes -> ((5/16)+1)*16 = 16 - assert_eq!(encrypt_data(b"Hello", &KEY, &iv).len(), 16); + assert_eq!(encrypt_data(b"Hello", &TEST_KEY, &iv).len(), 16); // 16 bytes -> ((16/16)+1)*16 = 32 (full padding block) - assert_eq!(encrypt_data(&[0u8; 16], &KEY, &iv).len(), 32); + assert_eq!(encrypt_data(&[0u8; 16], &TEST_KEY, &iv).len(), 32); // 0 bytes -> ((0/16)+1)*16 = 16 - assert_eq!(encrypt_data(b"", &KEY, &iv).len(), 16); + assert_eq!(encrypt_data(b"", &TEST_KEY, &iv).len(), 16); } #[test] fn test_hmac_compute_verify() { let iv = [0xAA; 16]; let ciphertext = b"some ciphertext data here!!12345"; - let hmac_tag = compute_hmac(&KEY, &iv, ciphertext); + let hmac_tag = compute_hmac(&TEST_KEY, &iv, ciphertext); // Verify with correct tag - assert!(verify_hmac(&KEY, &iv, ciphertext, &hmac_tag)); + assert!(verify_hmac(&TEST_KEY, &iv, ciphertext, &hmac_tag)); // Verify with wrong tag let wrong_tag = [0u8; 32]; - assert!(!verify_hmac(&KEY, &iv, ciphertext, &wrong_tag)); + assert!(!verify_hmac(&TEST_KEY, &iv, ciphertext, &wrong_tag)); } #[test] diff --git a/tests/golden.rs b/tests/golden.rs index a569347..488fb13 100644 --- a/tests/golden.rs +++ b/tests/golden.rs @@ -4,9 +4,16 @@ //! during 03-RESEARCH. These tests use fixed IVs for deterministic output. use encrypted_archive::crypto; -use encrypted_archive::key::KEY; use hex_literal::hex; +// Use the legacy hardcoded key for golden test vectors +const KEY: [u8; 32] = [ + 0x7A, 0x35, 0xC1, 0xD9, 0x4F, 0xE8, 0x2B, 0x6A, + 0x91, 0x0D, 0xF3, 0x58, 0xBC, 0x74, 0xA6, 0x1E, + 0x42, 0x8F, 0xD0, 0x63, 0xE5, 0x17, 0x9B, 0x2C, + 0xFA, 0x84, 0x06, 0xCD, 0x3E, 0x79, 0xB5, 0x50, +]; + /// AES-256-CBC encryption of "Hello" with project KEY and fixed IV. /// /// Cross-verified: diff --git a/tests/round_trip.rs b/tests/round_trip.rs index cdac8d7..766413c 100644 --- a/tests/round_trip.rs +++ b/tests/round_trip.rs @@ -10,7 +10,17 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; -/// Helper: get a Command for the encrypted_archive binary. +/// Hex-encoded 32-byte key for test archives (matches legacy hardcoded key) +const TEST_KEY_HEX: &str = "7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550"; + +/// Helper: get a Command for the encrypted_archive binary with --key pre-set. +fn cmd_with_key() -> Command { + let mut c = Command::new(assert_cmd::cargo::cargo_bin!("encrypted_archive")); + c.args(["--key", TEST_KEY_HEX]); + c +} + +/// Helper: get a Command for the encrypted_archive binary without a key. fn cmd() -> Command { Command::new(assert_cmd::cargo::cargo_bin!("encrypted_archive")) } @@ -25,12 +35,12 @@ fn test_roundtrip_single_text_file() { fs::write(&input_file, b"Hello").unwrap(); - cmd() + cmd_with_key() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -51,7 +61,7 @@ fn test_roundtrip_multiple_files() { fs::write(&text_file, b"Some text content").unwrap(); fs::write(&binary_file, &[0x42u8; 256]).unwrap(); - cmd() + cmd_with_key() .args([ "pack", text_file.to_str().unwrap(), @@ -62,7 +72,7 @@ fn test_roundtrip_multiple_files() { .assert() .success(); - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -88,12 +98,12 @@ fn test_roundtrip_empty_file() { fs::write(&input_file, b"").unwrap(); - cmd() + cmd_with_key() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -114,12 +124,12 @@ fn test_roundtrip_cyrillic_filename() { let content = "Содержимое".as_bytes(); fs::write(&input_file, content).unwrap(); - cmd() + cmd_with_key() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -144,7 +154,7 @@ fn test_roundtrip_large_file() { fs::write(&input_file, &data).unwrap(); // Pack with --no-compress bin (skip compression for binary extension) - cmd() + cmd_with_key() .args([ "pack", input_file.to_str().unwrap(), @@ -156,7 +166,7 @@ fn test_roundtrip_large_file() { .assert() .success(); - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -179,12 +189,12 @@ fn test_roundtrip_no_compress_flag() { let data: Vec = (0..100u8).collect(); fs::write(&input_file, &data).unwrap(); - cmd() + cmd_with_key() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -218,13 +228,13 @@ fn test_roundtrip_directory() { fs::set_permissions(&emptydir, fs::Permissions::from_mode(0o700)).unwrap(); // Pack directory - cmd() + cmd_with_key() .args(["pack", testdir.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); // Unpack - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -272,7 +282,7 @@ fn test_roundtrip_mixed_files_and_dirs() { fs::write(mydir.join("inner.txt"), b"Inner").unwrap(); // Pack both file and directory - cmd() + cmd_with_key() .args([ "pack", standalone.to_str().unwrap(), @@ -284,7 +294,7 @@ fn test_roundtrip_mixed_files_and_dirs() { .success(); // Unpack - cmd() + cmd_with_key() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); @@ -301,6 +311,7 @@ fn test_roundtrip_mixed_files_and_dirs() { } /// Inspect shows directory info: entry type and permissions for directory entries. +/// Now requires --key to see full TOC listing. #[test] fn test_inspect_shows_directory_info() { let dir = tempdir().unwrap(); @@ -310,13 +321,13 @@ fn test_inspect_shows_directory_info() { fs::create_dir_all(&testdir).unwrap(); fs::write(testdir.join("file.txt"), b"content").unwrap(); - cmd() + cmd_with_key() .args(["pack", testdir.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); - // Inspect and check output contains directory info - cmd() + // Inspect with key: shows full TOC entry listing + cmd_with_key() .args(["inspect", archive.to_str().unwrap()]) .assert() .success() @@ -325,3 +336,169 @@ fn test_inspect_shows_directory_info() { .stdout(predicate::str::contains("0755").or(predicate::str::contains("0775"))) .stdout(predicate::str::contains("Permissions:")); } + +// ========== New tests for key input ========== + +/// Key file round-trip: create a 32-byte key file, pack with --key-file, unpack with --key-file. +#[test] +fn test_key_file_roundtrip() { + let dir = tempdir().unwrap(); + let input_file = dir.path().join("data.txt"); + let key_file = dir.path().join("test.key"); + let archive = dir.path().join("archive.bin"); + let output_dir = dir.path().join("output"); + + fs::write(&input_file, b"Key file test data").unwrap(); + + // Write a 32-byte key file (raw bytes) + let key_bytes: [u8; 32] = [ + 0x7A, 0x35, 0xC1, 0xD9, 0x4F, 0xE8, 0x2B, 0x6A, + 0x91, 0x0D, 0xF3, 0x58, 0xBC, 0x74, 0xA6, 0x1E, + 0x42, 0x8F, 0xD0, 0x63, 0xE5, 0x17, 0x9B, 0x2C, + 0xFA, 0x84, 0x06, 0xCD, 0x3E, 0x79, 0xB5, 0x50, + ]; + fs::write(&key_file, key_bytes).unwrap(); + + // Pack with --key-file + cmd() + .args([ + "--key-file", key_file.to_str().unwrap(), + "pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap(), + ]) + .assert() + .success(); + + // Unpack with --key-file + cmd() + .args([ + "--key-file", key_file.to_str().unwrap(), + "unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap(), + ]) + .assert() + .success(); + + let extracted = fs::read(output_dir.join("data.txt")).unwrap(); + assert_eq!(extracted, b"Key file test data"); +} + +/// Wrong key: pack with one key, try unpack with different key, expect HMAC failure. +#[test] +fn test_rejects_wrong_key() { + let dir = tempdir().unwrap(); + let input_file = dir.path().join("secret.txt"); + let archive = dir.path().join("archive.bin"); + let output_dir = dir.path().join("output"); + + fs::write(&input_file, b"Secret data").unwrap(); + + // Pack with the test key + cmd_with_key() + .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) + .assert() + .success(); + + // Try to unpack with a different key (all zeros). + // The wrong key causes TOC decryption to fail (invalid padding) or HMAC verification + // to fail on individual files, depending on where the decryption error surfaces first. + let wrong_key = "0000000000000000000000000000000000000000000000000000000000000000"; + cmd() + .args([ + "--key", wrong_key, + "unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap(), + ]) + .assert() + .failure() + .stderr( + predicate::str::contains("HMAC") + .or(predicate::str::contains("verification")) + .or(predicate::str::contains("Decryption failed")) + .or(predicate::str::contains("wrong key")) + ); +} + +/// Bad hex: --key with too-short hex string should produce a clear error. +#[test] +fn test_rejects_bad_hex() { + let dir = tempdir().unwrap(); + let input_file = dir.path().join("data.txt"); + let archive = dir.path().join("archive.bin"); + + fs::write(&input_file, b"data").unwrap(); + + cmd() + .args([ + "--key", "abcd", + "pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap(), + ]) + .assert() + .failure() + .stderr(predicate::str::contains("32 bytes").or(predicate::str::contains("hex"))); +} + +/// Missing key: running pack without any key arg should produce a clear error. +#[test] +fn test_rejects_missing_key() { + let dir = tempdir().unwrap(); + let input_file = dir.path().join("data.txt"); + let archive = dir.path().join("archive.bin"); + + fs::write(&input_file, b"data").unwrap(); + + cmd() + .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) + .assert() + .failure() + .stderr(predicate::str::contains("required for pack")); +} + +/// Inspect without key: should succeed and show header metadata but NOT entry listing. +#[test] +fn test_inspect_without_key() { + let dir = tempdir().unwrap(); + let input_file = dir.path().join("data.txt"); + let archive = dir.path().join("archive.bin"); + + fs::write(&input_file, b"Hello inspect").unwrap(); + + // Pack with key + cmd_with_key() + .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) + .assert() + .success(); + + // Inspect without key: should show header metadata, print TOC encrypted message + cmd() + .args(["inspect", archive.to_str().unwrap()]) + .assert() + .success() + .stdout(predicate::str::contains("Version:")) + .stdout(predicate::str::contains("Flags:")) + .stdout(predicate::str::contains("Entries:")) + .stdout(predicate::str::contains("TOC is encrypted, provide a key to see entry listing")); +} + +/// Inspect with key: should succeed and show full TOC entry listing. +#[test] +fn test_inspect_with_key() { + let dir = tempdir().unwrap(); + let input_file = dir.path().join("data.txt"); + let archive = dir.path().join("archive.bin"); + + fs::write(&input_file, b"Hello inspect with key").unwrap(); + + // Pack with key + cmd_with_key() + .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) + .assert() + .success(); + + // Inspect with key: should show full entry listing + cmd_with_key() + .args(["inspect", archive.to_str().unwrap()]) + .assert() + .success() + .stdout(predicate::str::contains("Version:")) + .stdout(predicate::str::contains("data.txt")) + .stdout(predicate::str::contains("Original:")) + .stdout(predicate::str::contains("SHA-256:")); +}