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.
96 lines
2.4 KiB
Rust
96 lines
2.4 KiB
Rust
// SPDX-License-Identifier: MIT-0
|
|
|
|
use std::{
|
|
io,
|
|
io::{BufRead, Read},
|
|
};
|
|
|
|
use zeroize::Zeroizing;
|
|
|
|
pub(crate) enum ReadInfoChunk {
|
|
Normal,
|
|
Last(usize),
|
|
Empty,
|
|
}
|
|
|
|
pub(crate) struct AheadReader {
|
|
inner: Box<dyn BufRead + Send>,
|
|
buf: Zeroizing<Vec<u8>>,
|
|
bufsz: usize,
|
|
capacity: usize,
|
|
}
|
|
|
|
impl AheadReader {
|
|
pub(crate) fn from(reader: Box<dyn BufRead + Send>, capacity: usize) -> Self {
|
|
Self {
|
|
inner: reader,
|
|
buf: Zeroizing::new(vec![0; capacity]),
|
|
bufsz: 0,
|
|
capacity,
|
|
}
|
|
}
|
|
|
|
fn read_until_full(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
|
|
let mut total = 0;
|
|
loop {
|
|
match self.inner.read(buf) {
|
|
Ok(0) => break,
|
|
Ok(n) => {
|
|
total += n;
|
|
let tmp = buf;
|
|
buf = &mut tmp[n..];
|
|
}
|
|
Err(e) => match e.kind() {
|
|
io::ErrorKind::Interrupted => continue,
|
|
_ => return Err(e),
|
|
},
|
|
}
|
|
}
|
|
|
|
Ok(total)
|
|
}
|
|
|
|
pub(crate) fn read_ahead(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
|
if self.bufsz == 0 {
|
|
return self.first_read(userbuf);
|
|
}
|
|
self.normal_read(userbuf)
|
|
}
|
|
|
|
fn first_read(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
|
// 1st read directly to userbuf (we have no cached data yet)
|
|
let n = self.read_until_full(userbuf)?;
|
|
if n == 0 {
|
|
return Ok(ReadInfoChunk::Empty);
|
|
}
|
|
|
|
// 2nd read directly into our internal buf
|
|
let mut tmp = Zeroizing::new(vec![0u8; self.capacity]);
|
|
let n2 = self.read_until_full(&mut tmp)?;
|
|
self.buf = tmp;
|
|
self.bufsz = n2;
|
|
if n2 == 0 {
|
|
return Ok(ReadInfoChunk::Last(n));
|
|
}
|
|
|
|
Ok(ReadInfoChunk::Normal)
|
|
}
|
|
|
|
fn normal_read(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
|
// copy internal buf to userbuf
|
|
userbuf.copy_from_slice(&self.buf);
|
|
let userbuf_sz = self.bufsz;
|
|
|
|
// 2nd read directly into our internal buf
|
|
let mut tmp = Zeroizing::new(vec![0u8; self.capacity]);
|
|
let n2 = self.read_until_full(&mut tmp)?;
|
|
self.buf = tmp;
|
|
self.bufsz = n2;
|
|
if n2 == 0 {
|
|
return Ok(ReadInfoChunk::Last(userbuf_sz));
|
|
}
|
|
|
|
Ok(ReadInfoChunk::Normal)
|
|
}
|
|
}
|