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:
NikitolProject
2026-02-27 00:01:23 +03:00
parent 035879b7e6
commit 4077847caa
3 changed files with 215 additions and 16 deletions

View File

@@ -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"
);
}