feat(12-02): wire salt into pack/unpack, update main.rs, add password tests
- Pack signature accepts optional salt, writes 16-byte salt between header and TOC - Set flags bit 4 and adjust toc_offset to 56 when salt present - read_archive_metadata returns salt alongside header and TOC entries - Add read_archive_salt() public helper for pre-unpack salt reading - main.rs uses resolve_key_for_pack/resolve_key_for_unpack for two-phase password flow - Add 5 new integration tests: password roundtrip, wrong password rejection, salt flag presence, no-salt flag for key archives, directory password roundtrip - All 52 tests pass (25 unit + 7 golden + 20 integration)
This commit is contained in:
@@ -502,3 +502,164 @@ fn test_inspect_with_key() {
|
||||
.stdout(predicate::str::contains("Original:"))
|
||||
.stdout(predicate::str::contains("SHA-256:"));
|
||||
}
|
||||
|
||||
// ========== Password-based key derivation tests ==========
|
||||
|
||||
/// Password round-trip: pack with --password, unpack with same --password, verify byte-identical.
|
||||
#[test]
|
||||
fn test_password_roundtrip() {
|
||||
let dir = tempdir().unwrap();
|
||||
let input_file = dir.path().join("secret.txt");
|
||||
let archive = dir.path().join("archive.aea");
|
||||
let output_dir = dir.path().join("output");
|
||||
|
||||
fs::write(&input_file, b"Password protected data").unwrap();
|
||||
|
||||
// Pack with --password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "testpass123",
|
||||
"pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Unpack with same --password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "testpass123",
|
||||
"unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let extracted = fs::read(output_dir.join("secret.txt")).unwrap();
|
||||
assert_eq!(extracted, b"Password protected data");
|
||||
}
|
||||
|
||||
/// Wrong password: pack with correct, unpack with wrong, expect HMAC/decryption failure.
|
||||
#[test]
|
||||
fn test_password_wrong_rejects() {
|
||||
let dir = tempdir().unwrap();
|
||||
let input_file = dir.path().join("data.txt");
|
||||
let archive = dir.path().join("archive.aea");
|
||||
let output_dir = dir.path().join("output");
|
||||
|
||||
fs::write(&input_file, b"Sensitive data").unwrap();
|
||||
|
||||
// Pack with correct password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "correctpassword",
|
||||
"pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Try unpack with wrong password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "wrongpassword",
|
||||
"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"))
|
||||
);
|
||||
}
|
||||
|
||||
/// Password archive has salt flag: flags should contain bit 4 (0x10).
|
||||
#[test]
|
||||
fn test_password_archive_has_salt_flag() {
|
||||
let dir = tempdir().unwrap();
|
||||
let input_file = dir.path().join("data.txt");
|
||||
let archive = dir.path().join("archive.aea");
|
||||
|
||||
fs::write(&input_file, b"Flagged data").unwrap();
|
||||
|
||||
// Pack with --password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "testpass",
|
||||
"pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Inspect with --password to see flags
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "testpass",
|
||||
"inspect", archive.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Flags: 0x1F")); // 0x0F (bits 0-3) + 0x10 (bit 4) = 0x1F
|
||||
}
|
||||
|
||||
/// Key archive has no salt flag: flags should NOT contain bit 4 (0x10).
|
||||
#[test]
|
||||
fn test_key_archive_no_salt_flag() {
|
||||
let dir = tempdir().unwrap();
|
||||
let input_file = dir.path().join("data.txt");
|
||||
let archive = dir.path().join("archive.aea");
|
||||
|
||||
fs::write(&input_file, b"No salt data").unwrap();
|
||||
|
||||
// Pack with --key (no password, no salt)
|
||||
cmd_with_key()
|
||||
.args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Inspect with --key
|
||||
cmd_with_key()
|
||||
.args(["inspect", archive.to_str().unwrap()])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Flags: 0x0F")); // bits 0-3 set, bit 4 clear
|
||||
}
|
||||
|
||||
/// Password archive multiple files: pack a directory with --password, unpack, verify.
|
||||
#[test]
|
||||
fn test_password_roundtrip_directory() {
|
||||
let dir = tempdir().unwrap();
|
||||
let testdir = dir.path().join("mydir");
|
||||
let archive = dir.path().join("archive.aea");
|
||||
let output_dir = dir.path().join("output");
|
||||
|
||||
fs::create_dir_all(&testdir).unwrap();
|
||||
fs::write(testdir.join("file1.txt"), b"File one content").unwrap();
|
||||
fs::write(testdir.join("file2.txt"), b"File two content").unwrap();
|
||||
|
||||
// Pack with --password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "dirpass",
|
||||
"pack", testdir.to_str().unwrap(), "-o", archive.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Unpack with same --password
|
||||
cmd()
|
||||
.args([
|
||||
"--password", "dirpass",
|
||||
"unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert_eq!(
|
||||
fs::read(output_dir.join("mydir/file1.txt")).unwrap(),
|
||||
b"File one content"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(output_dir.join("mydir/file2.txt")).unwrap(),
|
||||
b"File two content"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user