feat: split crate into library and thin CLI binary
The crypto engine was only reachable through the fcry binary; embedding
it in another Rust project meant shelling out to the CLI. Restructure
the crate so the binary sits on top of a proper library API.
- Add src/lib.rs exposing encrypt/decrypt/decrypt_range/derive_key, the
header and policy types, and the secret-handling primitives.
- Replace the positional-argument wrapper ladder
(encrypt_with_output_options, decrypt_with_argon_cap, ...) with
options structs: EncryptOptions, DecryptOptions, DecryptRangeOptions
and HeaderReadOptions. OutSinkOptions becomes the public
OutputOptions and no longer carries the input path; the input is now
an explicit parameter to OutSink::open_with_options so the
same-file-aliasing guard's inputs are visible at each call site.
- File parameters take Option<PathBuf>/&Path instead of AsRef<str>, so
non-UTF-8 paths work.
- FcryError implements Display and std::error::Error so it composes
with anyhow/thiserror-style error handling in downstream crates.
- Move read_key_file and normalize_passphrase from main.rs into
secrets.rs so library users get the same strict 32-byte key-file
parsing and NFC passphrase normalization. The world-readable
key-file warning stays in the CLI wrapper (read_key_file_cli).
- Drop now-unneeded #[allow(dead_code)] markers; ReadInfoChunk::Normal
loses its unused byte-count payload.
- Add rustfmt.toml (StdExternalCrate grouping, crate-granularity
imports) and reformat imports accordingly.
- Add tests/library_api.rs covering a file round-trip and a range
decrypt through the public API with a raw key.
User-visible change: CLI behavior is unchanged except error output,
which is now human-readable Display text ("Error: wrong key or
passphrase") instead of the Rust Debug representation.
Test plan: cargo clippy (default, --tests, --benches) is clean;
cargo +nightly fmt produces no diff; cargo test passes 43 tests
including the new library_api integration tests.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
use std::fs;
|
||||
|
||||
use fcry::{
|
||||
DecryptOptions,
|
||||
DecryptRangeOptions,
|
||||
EncryptOptions,
|
||||
KdfParams,
|
||||
OutputOptions,
|
||||
SecretBytes32,
|
||||
decrypt,
|
||||
decrypt_range,
|
||||
encrypt,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_key() -> SecretBytes32 {
|
||||
let mut key = SecretBytes32::zeroed();
|
||||
key.with_mut_array(|key| key.copy_from_slice(b"0123456789abcdef0123456789abcdef"));
|
||||
key
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_file_roundtrip_raw_key() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let plain = dir.path().join("plain.bin");
|
||||
let ct = dir.path().join("cipher.fcry");
|
||||
let out = dir.path().join("out.bin");
|
||||
let data: Vec<u8> = (0..=255).cycle().take(100_000).collect();
|
||||
fs::write(&plain, &data).unwrap();
|
||||
|
||||
let key = test_key();
|
||||
let encrypt_options = EncryptOptions {
|
||||
input_file: Some(plain),
|
||||
output_file: Some(ct.clone()),
|
||||
chunk_size: 4096,
|
||||
threads: 1,
|
||||
output: OutputOptions::default(),
|
||||
};
|
||||
encrypt(&encrypt_options, &key, KdfParams::Raw).unwrap();
|
||||
|
||||
let decrypt_options = DecryptOptions {
|
||||
input_file: Some(ct),
|
||||
output_file: Some(out.clone()),
|
||||
threads: 1,
|
||||
..DecryptOptions::default()
|
||||
};
|
||||
decrypt(&decrypt_options, Some(&key), None).unwrap();
|
||||
|
||||
assert_eq!(fs::read(out).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_range_decrypt_raw_key() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let plain = dir.path().join("plain.bin");
|
||||
let ct = dir.path().join("cipher.fcry");
|
||||
let out = dir.path().join("slice.bin");
|
||||
let data: Vec<u8> = (0..=255).cycle().take(50_000).collect();
|
||||
fs::write(&plain, &data).unwrap();
|
||||
|
||||
let key = test_key();
|
||||
let encrypt_options = EncryptOptions {
|
||||
input_file: Some(plain),
|
||||
output_file: Some(ct.clone()),
|
||||
chunk_size: 1024,
|
||||
threads: 2,
|
||||
output: OutputOptions::default(),
|
||||
};
|
||||
encrypt(&encrypt_options, &key, KdfParams::Raw).unwrap();
|
||||
|
||||
let range_options = DecryptRangeOptions {
|
||||
input_file: ct,
|
||||
output_file: Some(out.clone()),
|
||||
offset: 1234,
|
||||
length: 20_000,
|
||||
max_argon_memory_mib: DecryptOptions::default().max_argon_memory_mib,
|
||||
output: OutputOptions::default(),
|
||||
};
|
||||
decrypt_range(&range_options, Some(&key), None).unwrap();
|
||||
|
||||
assert_eq!(fs::read(out).unwrap(), data[1234..21_234]);
|
||||
}
|
||||
+8
-10
@@ -6,9 +6,11 @@
|
||||
// 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 std::{
|
||||
fs,
|
||||
io::{ErrorKind, Write},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use assert_cmd::cargo::CommandCargoExt;
|
||||
use tempfile::TempDir;
|
||||
@@ -209,8 +211,8 @@ fn rejects_wrong_key() {
|
||||
.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).contains("wrong key or passphrase"),
|
||||
"expected distinct wrong-key error, got {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
@@ -422,11 +424,7 @@ fn non_utf8_key_file_roundtrips() {
|
||||
#[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;
|
||||
use std::{ffi::CString, fs::OpenOptions, os::unix::ffi::OsStrExt, thread, time::Duration};
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let fifo = dir.path().join("key.fifo");
|
||||
|
||||
Reference in New Issue
Block a user