3f53c221c8
The chunk-size-zero regression test spawned fcry with stdin piped and unconditionally unwrapped the write into that pipe. That made the test depend on scheduler timing: the child may validate --chunk-size 0, report the error, and exit before it has drained stdin. Treat BrokenPipe as the expected early-exit shape for this failing command, while still failing on any other write error and still asserting that the process exits unsuccessfully. The valid empty-stdin chunk-size case remains unchanged. Test Plan: - cargo fmt --check - cargo test - cargo clippy --all-targets -- -D warnings - git diff --check Refs: none
1251 lines
35 KiB
Rust
1251 lines
35 KiB
Rust
// SPDX-License-Identifier: GPL-3.0-only
|
|
//
|
|
// Integration tests for the `fcry` binary.
|
|
//
|
|
// These exercise the CLI as a black box: encrypt then decrypt and check that
|
|
// plaintext bytes are preserved, plus a handful of failure cases (tampering,
|
|
// wrong key, truncation, bad magic).
|
|
|
|
use std::fs;
|
|
use std::io::{ErrorKind, Write};
|
|
use std::process::{Command, Stdio};
|
|
|
|
use assert_cmd::cargo::CommandCargoExt;
|
|
use tempfile::TempDir;
|
|
|
|
const KEY: &[u8; 32] = b"0123456789abcdef0123456789abcdef";
|
|
|
|
fn fcry() -> Command {
|
|
Command::cargo_bin("fcry").unwrap()
|
|
}
|
|
|
|
fn write_key_file(dir: &std::path::Path) -> std::path::PathBuf {
|
|
let key = dir.join("key.bin");
|
|
fs::write(&key, KEY).unwrap();
|
|
key
|
|
}
|
|
|
|
fn key_file_near(path: &std::path::Path) -> std::path::PathBuf {
|
|
write_key_file(path.parent().unwrap())
|
|
}
|
|
|
|
/// Deterministic pseudo-random plaintext of `n` bytes (xorshift, seedable).
|
|
/// We avoid `/dev/urandom` so tests are reproducible on failure.
|
|
fn pseudo_random(seed: u64, n: usize) -> Vec<u8> {
|
|
let mut s = seed.wrapping_add(0x9E3779B97F4A7C15);
|
|
let mut out = Vec::with_capacity(n);
|
|
while out.len() < n {
|
|
s ^= s << 13;
|
|
s ^= s >> 7;
|
|
s ^= s << 17;
|
|
out.extend_from_slice(&s.to_le_bytes());
|
|
}
|
|
out.truncate(n);
|
|
out
|
|
}
|
|
|
|
fn encrypt_file(plain: &std::path::Path, ct: &std::path::Path, chunk_size: Option<u32>) {
|
|
let mut cmd = fcry();
|
|
let key = key_file_near(ct);
|
|
cmd.arg("-i")
|
|
.arg(plain)
|
|
.arg("-o")
|
|
.arg(ct)
|
|
.arg("--key-file")
|
|
.arg(key);
|
|
if let Some(cs) = chunk_size {
|
|
cmd.arg("--chunk-size").arg(cs.to_string());
|
|
}
|
|
let out = cmd.output().unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"encrypt failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
fn decrypt_file(ct: &std::path::Path, rt: &std::path::Path) {
|
|
let key = key_file_near(ct);
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(ct)
|
|
.arg("-o")
|
|
.arg(rt)
|
|
.arg("--key-file")
|
|
.arg(key)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"decrypt failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
fn roundtrip_with_size(plaintext_size: usize, chunk_size: Option<u32>) {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("plain.bin");
|
|
let ct = dir.path().join("ct.bin");
|
|
let rt = dir.path().join("rt.bin");
|
|
|
|
let data = pseudo_random(plaintext_size as u64, plaintext_size);
|
|
fs::write(&plain, &data).unwrap();
|
|
|
|
encrypt_file(&plain, &ct, chunk_size);
|
|
decrypt_file(&ct, &rt);
|
|
|
|
let got = fs::read(&rt).unwrap();
|
|
assert_eq!(got, data, "roundtrip mismatch at size {plaintext_size}");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_empty() {
|
|
roundtrip_with_size(0, None);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_one_byte() {
|
|
roundtrip_with_size(1, None);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_smaller_than_chunk() {
|
|
roundtrip_with_size(100, None);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_exactly_one_chunk() {
|
|
roundtrip_with_size(1024 * 1024, None);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_just_over_one_chunk() {
|
|
roundtrip_with_size(1024 * 1024 + 1, None);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_multi_chunk() {
|
|
roundtrip_with_size(5 * 1024 * 1024 + 12345, None);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_custom_small_chunk_size() {
|
|
// forces many chunks for a small input
|
|
roundtrip_with_size(50_000, Some(4096));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_chunk_size_one_byte() {
|
|
// pathological but should still work
|
|
roundtrip_with_size(257, Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_pipe_stdin_stdout() {
|
|
let data = pseudo_random(42, 200_000);
|
|
let dir = TempDir::new().unwrap();
|
|
let key = write_key_file(dir.path());
|
|
|
|
let mut enc = fcry()
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
enc.stdin.as_mut().unwrap().write_all(&data).unwrap();
|
|
let enc_out = enc.wait_with_output().unwrap();
|
|
assert!(
|
|
enc_out.status.success(),
|
|
"pipe encrypt failed: {}",
|
|
String::from_utf8_lossy(&enc_out.stderr)
|
|
);
|
|
|
|
let mut dec = fcry()
|
|
.arg("-d")
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
dec.stdin
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(&enc_out.stdout)
|
|
.unwrap();
|
|
let dec_out = dec.wait_with_output().unwrap();
|
|
assert!(
|
|
dec_out.status.success(),
|
|
"pipe decrypt failed: {}",
|
|
String::from_utf8_lossy(&dec_out.stderr)
|
|
);
|
|
|
|
assert_eq!(dec_out.stdout, data);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_wrong_key() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, pseudo_random(1, 1000)).unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
|
|
let wrong = dir.path().join("wrong.key");
|
|
fs::write(&wrong, b"ffffffffffffffffffffffffffffffff").unwrap();
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(dir.path().join("rt.bin"))
|
|
.arg("--key-file")
|
|
.arg(wrong)
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success(), "decrypt with wrong key should fail");
|
|
assert!(
|
|
String::from_utf8_lossy(&out.stderr).contains("WrongKey"),
|
|
"expected distinct WrongKey error, got {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_tampered_header() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, pseudo_random(2, 1000)).unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
|
|
// Flip a byte in the chunk_size field of the header (offset 8: 4 magic + 4 fixed).
|
|
let mut bytes = fs::read(&ct).unwrap();
|
|
bytes[8] ^= 0xff;
|
|
fs::write(&ct, &bytes).unwrap();
|
|
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(dir.path().join("rt.bin"))
|
|
.arg("--key-file")
|
|
.arg(key_file_near(&ct))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
!out.status.success(),
|
|
"decrypt with tampered header should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_tampered_ciphertext() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, pseudo_random(3, 5000)).unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
|
|
// Flip a byte well past the header (in the first ciphertext chunk).
|
|
let mut bytes = fs::read(&ct).unwrap();
|
|
let off = bytes.len() / 2;
|
|
bytes[off] ^= 0x01;
|
|
fs::write(&ct, &bytes).unwrap();
|
|
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(dir.path().join("rt.bin"))
|
|
.arg("--key-file")
|
|
.arg(key_file_near(&ct))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
!out.status.success(),
|
|
"decrypt of tampered ciphertext should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_truncated_ciphertext() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, pseudo_random(4, 3 * 1024 * 1024)).unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
|
|
// Drop the trailing 16-byte tag of the last chunk (and then some).
|
|
let mut bytes = fs::read(&ct).unwrap();
|
|
bytes.truncate(bytes.len() - 32);
|
|
fs::write(&ct, &bytes).unwrap();
|
|
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(dir.path().join("rt.bin"))
|
|
.arg("--key-file")
|
|
.arg(key_file_near(&ct))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
!out.status.success(),
|
|
"decrypt of truncated ciphertext should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_bad_magic() {
|
|
let dir = TempDir::new().unwrap();
|
|
let bogus = dir.path().join("bogus.bin");
|
|
fs::write(&bogus, b"NOPE\x01\x01\x00\x00\x00\x10\x00\x00\x00").unwrap();
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&bogus)
|
|
.arg("-o")
|
|
.arg(dir.path().join("rt.bin"))
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
!out.status.success(),
|
|
"decrypt of file with bad magic should fail"
|
|
);
|
|
assert!(
|
|
String::from_utf8_lossy(&out.stderr).contains("magic"),
|
|
"expected 'magic' in stderr, got: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_short_key_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let key = dir.path().join("short.key");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
fs::write(&key, b"tooshort").unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(dir.path().join("c.bin"))
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
!out.status.success(),
|
|
"encrypt with short key file should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_long_key_file_and_trailing_newline() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let key = dir.path().join("long.key");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
fs::write(&key, b"0123456789abcdef0123456789abcdef\n").unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(dir.path().join("c.bin"))
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success(), "long key file should fail");
|
|
assert!(
|
|
String::from_utf8_lossy(&out.stderr).contains("too long"),
|
|
"expected too-long error, got {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_utf8_key_file_roundtrips() {
|
|
let dir = TempDir::new().unwrap();
|
|
let key = dir.path().join("key.bin");
|
|
let plain = dir.path().join("plain.bin");
|
|
let ct = dir.path().join("ct.bin");
|
|
let rt = dir.path().join("rt.bin");
|
|
let key_bytes: Vec<u8> = (0..32u8).map(|b| b ^ 0x80).collect();
|
|
let data = pseudo_random(31, 8192);
|
|
fs::write(&key, key_bytes).unwrap();
|
|
fs::write(&plain, &data).unwrap();
|
|
|
|
let enc = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
enc.status.success(),
|
|
"non-UTF-8 key encrypt failed: {}",
|
|
String::from_utf8_lossy(&enc.stderr)
|
|
);
|
|
let dec = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(&rt)
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
dec.status.success(),
|
|
"non-UTF-8 key decrypt failed: {}",
|
|
String::from_utf8_lossy(&dec.stderr)
|
|
);
|
|
assert_eq!(fs::read(&rt).unwrap(), data);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn split_fifo_key_file_read_roundtrips() {
|
|
use std::ffi::CString;
|
|
use std::fs::OpenOptions;
|
|
use std::os::unix::ffi::OsStrExt;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let fifo = dir.path().join("key.fifo");
|
|
let fifo_c = CString::new(fifo.as_os_str().as_bytes()).unwrap();
|
|
let rc = unsafe { libc::mkfifo(fifo_c.as_ptr(), 0o600) };
|
|
assert_eq!(rc, 0, "mkfifo failed: {}", std::io::Error::last_os_error());
|
|
|
|
let plain = dir.path().join("plain.bin");
|
|
let ct = dir.path().join("ct.bin");
|
|
let rt = dir.path().join("rt.bin");
|
|
let data = pseudo_random(33, 8192);
|
|
fs::write(&plain, &data).unwrap();
|
|
|
|
let fifo_writer = fifo.clone();
|
|
let writer = thread::spawn(move || {
|
|
let mut file = OpenOptions::new().write(true).open(&fifo_writer).unwrap();
|
|
file.write_all(&KEY[..8]).unwrap();
|
|
file.flush().unwrap();
|
|
thread::sleep(Duration::from_millis(50));
|
|
file.write_all(&KEY[8..]).unwrap();
|
|
});
|
|
|
|
let enc = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--key-file")
|
|
.arg(&fifo)
|
|
.output()
|
|
.unwrap();
|
|
writer.join().unwrap();
|
|
assert!(
|
|
enc.status.success(),
|
|
"split FIFO key encrypt failed: {}",
|
|
String::from_utf8_lossy(&enc.stderr)
|
|
);
|
|
|
|
decrypt_file(&ct, &rt);
|
|
assert_eq!(fs::read(&rt).unwrap(), data);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_passphrase_argon2id() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
let rt = dir.path().join("r.bin");
|
|
let data = pseudo_random(7, 100_000);
|
|
fs::write(&plain, &data).unwrap();
|
|
|
|
// Use cheap argon2 params so the test stays fast.
|
|
let enc = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--passphrase-env")
|
|
.arg("FCRY_TEST_PW")
|
|
.arg("--argon-memory")
|
|
.arg("8")
|
|
.arg("--argon-passes")
|
|
.arg("1")
|
|
.arg("--allow-weak-kdf")
|
|
.env("FCRY_TEST_PW", "correct horse battery staple")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
enc.status.success(),
|
|
"passphrase encrypt failed: {}",
|
|
String::from_utf8_lossy(&enc.stderr)
|
|
);
|
|
|
|
let dec = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(&rt)
|
|
.arg("--passphrase-env")
|
|
.arg("FCRY_TEST_PW")
|
|
.env("FCRY_TEST_PW", "correct horse battery staple")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
dec.status.success(),
|
|
"passphrase decrypt failed: {}",
|
|
String::from_utf8_lossy(&dec.stderr)
|
|
);
|
|
assert_eq!(fs::read(&rt).unwrap(), data);
|
|
|
|
// Wrong passphrase must fail.
|
|
let bad = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(dir.path().join("bad.bin"))
|
|
.arg("--passphrase-env")
|
|
.arg("FCRY_TEST_PW")
|
|
.env("FCRY_TEST_PW", "wrong passphrase")
|
|
.output()
|
|
.unwrap();
|
|
assert!(!bad.status.success(), "wrong passphrase should fail");
|
|
}
|
|
|
|
#[test]
|
|
fn weak_passphrase_kdf_rejected_without_override() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
let enc = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(dir.path().join("c.bin"))
|
|
.arg("--passphrase-env")
|
|
.arg("FCRY_TEST_PW")
|
|
.arg("--argon-memory")
|
|
.arg("8")
|
|
.arg("--argon-passes")
|
|
.arg("1")
|
|
.env("FCRY_TEST_PW", "short")
|
|
.output()
|
|
.unwrap();
|
|
assert!(!enc.status.success(), "weak KDF/passphrase should fail");
|
|
}
|
|
|
|
#[test]
|
|
fn decrypt_argon_memory_cap_rejects_hostile_header() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
let enc = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--passphrase-env")
|
|
.arg("FCRY_TEST_PW")
|
|
.arg("--argon-memory")
|
|
.arg("8")
|
|
.arg("--argon-passes")
|
|
.arg("1")
|
|
.arg("--allow-weak-kdf")
|
|
.env("FCRY_TEST_PW", "correct horse battery staple")
|
|
.output()
|
|
.unwrap();
|
|
assert!(enc.status.success());
|
|
|
|
let dec = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("--passphrase-env")
|
|
.arg("FCRY_TEST_PW")
|
|
.arg("--max-argon-memory-mib")
|
|
.arg("1")
|
|
.env("FCRY_TEST_PW", "correct horse battery staple")
|
|
.output()
|
|
.unwrap();
|
|
assert!(!dec.status.success(), "low decrypt cap should reject file");
|
|
assert!(
|
|
String::from_utf8_lossy(&dec.stderr).contains("decrypt cap"),
|
|
"expected cap error, got {}",
|
|
String::from_utf8_lossy(&dec.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn atomic_output_no_stale_tmp_on_failure() {
|
|
// A failed decrypt (wrong key) should not leave the output file behind.
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
let rt = dir.path().join("r.bin");
|
|
fs::write(&plain, b"hello world").unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
|
|
let wrong = dir.path().join("wrong.key");
|
|
fs::write(&wrong, b"ffffffffffffffffffffffffffffffff").unwrap();
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(&rt)
|
|
.arg("--key-file")
|
|
.arg(&wrong)
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success());
|
|
assert!(!rt.exists(), "final output must not exist after failure");
|
|
let mut tmp = rt.clone();
|
|
tmp.set_file_name("r.bin.tmp");
|
|
assert!(!tmp.exists(), "temp file must be cleaned up");
|
|
}
|
|
|
|
#[test]
|
|
fn existing_output_refuses_without_force() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
fs::write(&ct, b"existing").unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success(), "existing output should refuse");
|
|
assert_eq!(fs::read(&ct).unwrap(), b"existing");
|
|
}
|
|
|
|
#[test]
|
|
fn force_replaces_only_after_success() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
fs::write(&ct, b"existing").unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--force")
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"force encrypt failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
assert_ne!(fs::read(&ct).unwrap(), b"existing");
|
|
}
|
|
|
|
#[test]
|
|
fn in_place_replacement_roundtrips() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("data.bin");
|
|
let original = pseudo_random(41, 50_000);
|
|
fs::write(&path, &original).unwrap();
|
|
|
|
let enc = fcry()
|
|
.arg("-i")
|
|
.arg(&path)
|
|
.arg("-o")
|
|
.arg(&path)
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
enc.status.success(),
|
|
"in-place encrypt failed: {}",
|
|
String::from_utf8_lossy(&enc.stderr)
|
|
);
|
|
assert_ne!(fs::read(&path).unwrap(), original);
|
|
|
|
let dec = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&path)
|
|
.arg("-o")
|
|
.arg(&path)
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
dec.status.success(),
|
|
"in-place decrypt failed: {}",
|
|
String::from_utf8_lossy(&dec.stderr)
|
|
);
|
|
assert_eq!(fs::read(&path).unwrap(), original);
|
|
}
|
|
|
|
#[test]
|
|
fn old_predictable_temp_name_input_is_not_truncated() {
|
|
let dir = TempDir::new().unwrap();
|
|
let input = dir.path().join("out.bin.tmp");
|
|
let output = dir.path().join("out.bin");
|
|
let original = pseudo_random(42, 1024);
|
|
fs::write(&input, &original).unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&input)
|
|
.arg("-o")
|
|
.arg(&output)
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"encrypt failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
assert_eq!(fs::read(&input).unwrap(), original);
|
|
assert!(output.exists());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn output_file_mode_is_0600() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
let mode = fs::metadata(&ct).unwrap().permissions().mode() & 0o777;
|
|
assert_eq!(mode, 0o600);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Multi-threaded pipeline + length-committed + random-access tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn encrypt_file_threads(
|
|
plain: &std::path::Path,
|
|
ct: &std::path::Path,
|
|
chunk_size: Option<u32>,
|
|
threads: usize,
|
|
) {
|
|
let mut cmd = fcry();
|
|
let key = key_file_near(ct);
|
|
cmd.arg("-i")
|
|
.arg(plain)
|
|
.arg("-o")
|
|
.arg(ct)
|
|
.arg("--key-file")
|
|
.arg(key)
|
|
.arg("-j")
|
|
.arg(threads.to_string());
|
|
if let Some(cs) = chunk_size {
|
|
cmd.arg("--chunk-size").arg(cs.to_string());
|
|
}
|
|
let out = cmd.output().unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"encrypt -j{threads} failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
fn decrypt_file_threads(ct: &std::path::Path, rt: &std::path::Path, threads: usize) {
|
|
let key = key_file_near(ct);
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(ct)
|
|
.arg("-o")
|
|
.arg(rt)
|
|
.arg("--key-file")
|
|
.arg(key)
|
|
.arg("-j")
|
|
.arg(threads.to_string())
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"decrypt -j{threads} failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_multi_threaded() {
|
|
// Multi-chunk input. Encrypt+decrypt with -j 4 must round-trip.
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
let rt = dir.path().join("r.bin");
|
|
let data = pseudo_random(11, 5 * 1024 * 1024 + 12345);
|
|
fs::write(&plain, &data).unwrap();
|
|
|
|
encrypt_file_threads(&plain, &ct, Some(64 * 1024), 4);
|
|
decrypt_file_threads(&ct, &rt, 4);
|
|
assert_eq!(fs::read(&rt).unwrap(), data);
|
|
}
|
|
|
|
#[test]
|
|
fn parallel_and_serial_outputs_round_trip() {
|
|
// Encrypt with -j 4 and decrypt serially (and vice-versa); both directions
|
|
// must yield the original plaintext.
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let data = pseudo_random(13, 256 * 1024 + 17);
|
|
fs::write(&plain, &data).unwrap();
|
|
|
|
let ct_par = dir.path().join("c_par.bin");
|
|
let ct_ser = dir.path().join("c_ser.bin");
|
|
encrypt_file_threads(&plain, &ct_par, Some(8192), 4);
|
|
encrypt_file_threads(&plain, &ct_ser, Some(8192), 1);
|
|
|
|
let rt1 = dir.path().join("r1.bin");
|
|
let rt2 = dir.path().join("r2.bin");
|
|
// par-encrypted, serial-decrypted
|
|
decrypt_file_threads(&ct_par, &rt1, 1);
|
|
// serial-encrypted, par-decrypted
|
|
decrypt_file_threads(&ct_ser, &rt2, 4);
|
|
assert_eq!(fs::read(&rt1).unwrap(), data);
|
|
assert_eq!(fs::read(&rt2).unwrap(), data);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_pipe_multi_threaded() {
|
|
// stdin/stdout mode with -j 4: length flag must NOT be set (no committed
|
|
// length when we don't know the input size), but encrypt/decrypt must still
|
|
// round-trip cleanly across the pipeline.
|
|
let data = pseudo_random(14, 200_000);
|
|
let dir = TempDir::new().unwrap();
|
|
let key = write_key_file(dir.path());
|
|
|
|
let mut enc = fcry()
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.arg("-j")
|
|
.arg("4")
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
enc.stdin.as_mut().unwrap().write_all(&data).unwrap();
|
|
let enc_out = enc.wait_with_output().unwrap();
|
|
assert!(
|
|
enc_out.status.success(),
|
|
"pipe encrypt -j4 failed: {}",
|
|
String::from_utf8_lossy(&enc_out.stderr)
|
|
);
|
|
|
|
// flags byte at offset 6 must not set length commitment for stdin input.
|
|
assert_eq!(
|
|
enc_out.stdout[6] & 0x01,
|
|
0,
|
|
"stdin-encrypted file unexpectedly committed length"
|
|
);
|
|
|
|
let mut dec = fcry()
|
|
.arg("-d")
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.arg("-j")
|
|
.arg("4")
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
dec.stdin
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(&enc_out.stdout)
|
|
.unwrap();
|
|
let dec_out = dec.wait_with_output().unwrap();
|
|
assert!(
|
|
dec_out.status.success(),
|
|
"pipe decrypt -j4 failed: {}",
|
|
String::from_utf8_lossy(&dec_out.stderr)
|
|
);
|
|
assert_eq!(dec_out.stdout, data);
|
|
}
|
|
|
|
#[test]
|
|
fn stdin_chunk_size_zero_fails_but_empty_valid_chunk_succeeds() {
|
|
let dir = TempDir::new().unwrap();
|
|
let key = write_key_file(dir.path());
|
|
let mut bad = fcry()
|
|
.arg("--chunk-size")
|
|
.arg("0")
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
// Invalid options can make the child exit before it drains stdin.
|
|
if let Err(err) = bad.stdin.as_mut().unwrap().write_all(b"x") {
|
|
assert_eq!(
|
|
err.kind(),
|
|
ErrorKind::BrokenPipe,
|
|
"unexpected stdin write error for failing chunk-size 0 process: {err}"
|
|
);
|
|
}
|
|
let bad_out = bad.wait_with_output().unwrap();
|
|
assert!(!bad_out.status.success(), "chunk-size 0 should fail");
|
|
|
|
let mut good = fcry()
|
|
.arg("--chunk-size")
|
|
.arg("1")
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
drop(good.stdin.take());
|
|
let good_out = good.wait_with_output().unwrap();
|
|
assert!(
|
|
good_out.status.success(),
|
|
"empty stdin with valid chunk should succeed: {}",
|
|
String::from_utf8_lossy(&good_out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn huge_thread_count_is_bounded() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.arg("-j")
|
|
.arg("1000000")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
out.status.success(),
|
|
"huge -j should be capped, got {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
assert!(String::from_utf8_lossy(&out.stderr).contains("capped"));
|
|
}
|
|
|
|
#[test]
|
|
fn forged_huge_chunk_header_fails_before_allocation() {
|
|
let dir = TempDir::new().unwrap();
|
|
let forged = dir.path().join("forged.bin");
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(b"fcry");
|
|
bytes.push(3); // version
|
|
bytes.push(1); // alg
|
|
bytes.push(0x02); // key commitment flag
|
|
bytes.push(0); // reserved
|
|
bytes.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
fs::write(&forged, bytes).unwrap();
|
|
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&forged)
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success(), "huge chunk header should fail");
|
|
assert!(
|
|
String::from_utf8_lossy(&out.stderr).contains("chunk_size"),
|
|
"expected chunk_size error, got {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_input_commits_length() {
|
|
// Encrypting from a regular file must auto-set FLAG_LENGTH_COMMITTED (bit 0
|
|
// of the flags byte at offset 6) and embed the length.
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
let data = pseudo_random(15, 50_000);
|
|
fs::write(&plain, &data).unwrap();
|
|
encrypt_file(&plain, &ct, Some(4096));
|
|
|
|
let bytes = fs::read(&ct).unwrap();
|
|
// Magic(4) + version(1) + alg(1) + flags(1) = byte 6
|
|
assert_eq!(bytes[4], 3, "version should be 3");
|
|
assert_eq!(bytes[6] & 0x01, 0x01, "length-committed flag should be set");
|
|
assert_eq!(bytes[6] & 0x02, 0x02, "key-committed flag should be set");
|
|
}
|
|
|
|
#[test]
|
|
fn v3_downgrade_or_commitment_stripping_fails_authentication() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
let rt = dir.path().join("r.bin");
|
|
fs::write(&plain, pseudo_random(51, 1000)).unwrap();
|
|
encrypt_file(&plain, &ct, None);
|
|
|
|
let mut bytes = fs::read(&ct).unwrap();
|
|
bytes[4] = 2;
|
|
bytes[6] &= !0x02;
|
|
fs::write(&ct, bytes).unwrap();
|
|
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("-o")
|
|
.arg(&rt)
|
|
.arg("--key-file")
|
|
.arg(key_file_near(&ct))
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
!out.status.success(),
|
|
"downgraded/stripped v3 header must fail authentication"
|
|
);
|
|
}
|
|
|
|
fn encrypt_random_access_fixture(
|
|
dir: &std::path::Path,
|
|
data: &[u8],
|
|
chunk_size: u32,
|
|
) -> std::path::PathBuf {
|
|
let plain = dir.join("p.bin");
|
|
let ct = dir.join("c.bin");
|
|
fs::write(&plain, data).unwrap();
|
|
encrypt_file(&plain, &ct, Some(chunk_size));
|
|
ct
|
|
}
|
|
|
|
fn random_access_decrypt(
|
|
ct: &std::path::Path,
|
|
out: &std::path::Path,
|
|
offset: u64,
|
|
length: u64,
|
|
) -> std::process::Output {
|
|
fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(ct)
|
|
.arg("-o")
|
|
.arg(out)
|
|
.arg("--key-file")
|
|
.arg(key_file_near(ct))
|
|
.arg("--offset")
|
|
.arg(offset.to_string())
|
|
.arg("--length")
|
|
.arg(length.to_string())
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn random_access_decrypt_slices() {
|
|
let dir = TempDir::new().unwrap();
|
|
let chunk = 4096u32;
|
|
let total = 5 * 1024 * 1024 + 12345;
|
|
let data = pseudo_random(16, total);
|
|
let ct = encrypt_random_access_fixture(dir.path(), &data, chunk);
|
|
|
|
// (offset, length) cases:
|
|
// - chunk-aligned start, mid-chunk end
|
|
// - mid-chunk start crossing several chunks
|
|
// - last partial chunk
|
|
// - last byte
|
|
// - entire file
|
|
let cases: &[(u64, u64)] = &[
|
|
(0, 1),
|
|
(chunk as u64, 7),
|
|
(chunk as u64 - 5, 100),
|
|
(10, chunk as u64 * 3 + 17),
|
|
(total as u64 - 1, 1),
|
|
(total as u64 - 100, 100),
|
|
(0, total as u64),
|
|
];
|
|
for (i, (offset, length)) in cases.iter().copied().enumerate() {
|
|
let out = dir.path().join(format!("slice_{i}.bin"));
|
|
let r = random_access_decrypt(&ct, &out, offset, length);
|
|
assert!(
|
|
r.status.success(),
|
|
"slice {i} ({offset}, {length}) failed: {}",
|
|
String::from_utf8_lossy(&r.stderr)
|
|
);
|
|
let got = fs::read(&out).unwrap();
|
|
let expected = &data[offset as usize..(offset + length) as usize];
|
|
assert_eq!(got, expected, "slice {i} mismatch");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn random_access_rejects_out_of_range() {
|
|
let dir = TempDir::new().unwrap();
|
|
let data = pseudo_random(17, 1000);
|
|
let ct = encrypt_random_access_fixture(dir.path(), &data, 256);
|
|
let out = dir.path().join("oob.bin");
|
|
let r = random_access_decrypt(&ct, &out, 900, 1000); // 900+1000 > 1000
|
|
assert!(!r.status.success(), "out-of-range slice should fail");
|
|
}
|
|
|
|
#[test]
|
|
fn random_access_rejects_stdin_encrypted() {
|
|
// Encrypt via stdin → no length committed → random access must refuse.
|
|
let data = pseudo_random(18, 2000);
|
|
let dir = TempDir::new().unwrap();
|
|
let ct = dir.path().join("c.bin");
|
|
let key = write_key_file(dir.path());
|
|
|
|
let mut enc = fcry()
|
|
.arg("--key-file")
|
|
.arg(&key)
|
|
.arg("-o")
|
|
.arg(&ct)
|
|
.stdin(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
enc.stdin.as_mut().unwrap().write_all(&data).unwrap();
|
|
assert!(enc.wait().unwrap().success());
|
|
|
|
let out = dir.path().join("slice.bin");
|
|
let r = random_access_decrypt(&ct, &out, 0, 100);
|
|
assert!(
|
|
!r.status.success(),
|
|
"random access on stdin-encrypted file should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn random_access_rejects_zero_length() {
|
|
let dir = TempDir::new().unwrap();
|
|
let data = pseudo_random(19, 1000);
|
|
let ct = encrypt_random_access_fixture(dir.path(), &data, 256);
|
|
let out = dir.path().join("empty.bin");
|
|
let r = random_access_decrypt(&ct, &out, 500, 0);
|
|
assert!(!r.status.success(), "zero-length slice should fail");
|
|
}
|
|
|
|
#[test]
|
|
fn random_access_tampered_length_fails() {
|
|
// Flip a byte inside the committed plaintext_length field. The header is
|
|
// AAD for every chunk, so the AEAD must reject decryption.
|
|
let dir = TempDir::new().unwrap();
|
|
let data = pseudo_random(20, 4000);
|
|
let ct = encrypt_random_access_fixture(dir.path(), &data, 1024);
|
|
let mut bytes = fs::read(&ct).unwrap();
|
|
// For raw-kdf header: magic(4)+ver(1)+alg(1)+flags(1)+rsv(1)+chunksize(4)+kdf_id(1)+nonce_prefix(19) = 32
|
|
// plaintext_length is at offset 32..40.
|
|
bytes[34] ^= 0xff;
|
|
fs::write(&ct, &bytes).unwrap();
|
|
let out = dir.path().join("bad.bin");
|
|
let r = random_access_decrypt(&ct, &out, 0, 100);
|
|
assert!(
|
|
!r.status.success(),
|
|
"tampered plaintext_length must fail authentication"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_verify_stdout_emits_nothing_on_truncated_ciphertext() {
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
fs::write(&plain, pseudo_random(61, 3 * 1024 * 1024)).unwrap();
|
|
encrypt_file(&plain, &ct, Some(64 * 1024));
|
|
let mut bytes = fs::read(&ct).unwrap();
|
|
bytes.truncate(bytes.len() - 32);
|
|
fs::write(&ct, bytes).unwrap();
|
|
|
|
let out = fcry()
|
|
.arg("-d")
|
|
.arg("-i")
|
|
.arg(&ct)
|
|
.arg("--buffer-verify")
|
|
.arg("--key-file")
|
|
.arg(key_file_near(&ct))
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success(), "truncated decrypt should fail");
|
|
assert!(
|
|
out.stdout.is_empty(),
|
|
"buffer-verify must suppress partial stdout"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_zero_threads() {
|
|
// -j 0 is almost certainly a user mistake. Clap should reject it before
|
|
// we ever reach the pipeline.
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
fs::write(&plain, b"hello").unwrap();
|
|
let out = fcry()
|
|
.arg("-i")
|
|
.arg(&plain)
|
|
.arg("-o")
|
|
.arg(dir.path().join("c.bin"))
|
|
.arg("--key-file")
|
|
.arg(write_key_file(dir.path()))
|
|
.arg("-j")
|
|
.arg("0")
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success(), "-j 0 should be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn header_chunk_size_is_authoritative_on_decrypt() {
|
|
// Encrypt with a non-default chunk size; decrypt without specifying one.
|
|
// The decryptor must read chunk_size from the header.
|
|
let dir = TempDir::new().unwrap();
|
|
let plain = dir.path().join("p.bin");
|
|
let ct = dir.path().join("c.bin");
|
|
let rt = dir.path().join("r.bin");
|
|
let data = pseudo_random(5, 100_000);
|
|
fs::write(&plain, &data).unwrap();
|
|
encrypt_file(&plain, &ct, Some(7919)); // prime, deliberately weird
|
|
decrypt_file(&ct, &rt);
|
|
assert_eq!(fs::read(&rt).unwrap(), data);
|
|
}
|