encrypt decrypt file, clap command line

This commit is contained in:
sam.hadow committed 2026-09-24 09:36:43 +02:00
1 parent 44fe608942
commit cbce59293d
11 files changed
+1190 -10

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
use std::ffi::OsString;
use std::fs::{self, File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use super::error::FileCryptError;
pub(super) struct AtomicOutput {
tmp_path: PathBuf,
final_path: PathBuf,
committed: bool,
}
impl AtomicOutput {
pub(super) fn create(final_path: &Path) -> Result<(Self, File), FileCryptError> {
let file_name = final_path.file_name().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "output path has no file name")
})?;
let mut suffix = [0u8; 8];
getrandom::fill(&mut suffix).map_err(FileCryptError::Random)?;
let mut tmp_name: OsString = file_name.to_os_string();
tmp_name.push(".");
for byte in suffix {
tmp_name.push(format!("{byte:02x}"));
}
tmp_name.push(".tmp");
let tmp_path = final_path.with_file_name(tmp_name);
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let file = options.open(&tmp_path)?;
Ok((
Self {
tmp_path,
final_path: final_path.to_path_buf(),
committed: false,
},
file,
))
}
pub(super) fn commit(mut self) -> io::Result<()> {
fs::rename(&self.tmp_path, &self.final_path)?;
self.committed = true;
Ok(())
}
}
impl Drop for AtomicOutput {
fn drop(&mut self) {
if !self.committed {
let _ = fs::remove_file(&self.tmp_path);
}
}
}
+102
View File
@@ -0,0 +1,102 @@
use std::error::Error;
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum FileCryptError {
Io(io::Error),
Random(getrandom::Error),
/// Key length outside `MIN_KEY_LEN..=MAX_KEY_LEN`.
InvalidKeyLength(usize),
/// The key passed in does not have the length recorded in the header.
KeyLengthMismatch {
header: usize,
provided: usize,
},
BadMagic,
UnsupportedVersion(u8),
MalformedHeader,
/// The file ends before the header, ciphertext or tag is complete.
Truncated,
/// Bytes follow the tag.
TrailingData,
/// The input yielded a different number of bytes than expected.
InputLengthChanged,
/// The tag does not match: wrong key, or the file was modified.
AuthenticationFailed,
}
impl fmt::Display for FileCryptError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::Random(e) => {
write!(f, "random number generator error: {e}")
}
Self::InvalidKeyLength(n) => write!(
f,
"key length {n} is invalid (must be {min}..={max} bytes)",
min = super::MIN_KEY_LEN,
max = super::MAX_KEY_LEN,
),
Self::KeyLengthMismatch { header, provided } => write!(
f,
"file was encrypted with a {header}-byte key, \
but a {provided}-byte key was provided"
),
Self::BadMagic => {
write!(f, "not a KAEF file (bad magic)")
}
Self::UnsupportedVersion(v) => {
write!(f, "unsupported format version {v}")
}
Self::MalformedHeader => {
write!(f, "malformed header")
}
Self::Truncated => {
write!(f, "file is truncated")
}
Self::TrailingData => {
write!(f, "unexpected data after the authentication tag")
}
Self::InputLengthChanged => {
write!(f, "input length changed while it was being read")
}
Self::AuthenticationFailed => {
write!(f, "authentication failed (wrong key or corrupted file)")
}
}
}
}
impl Error for FileCryptError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for FileCryptError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
+71
View File
@@ -0,0 +1,71 @@
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
use super::atomic::AtomicOutput;
use super::error::FileCryptError;
use super::stream::{decrypt_stream, encrypt_stream};
use super::{BUF_SIZE, check_key};
/// Encrypt the file at `input` into `output`.
///
/// `output` is only replaced if encryption succeeds.
pub fn encrypt_file(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
key: &[u8],
) -> Result<(), FileCryptError> {
check_key(key)?;
let input_file = File::open(input.as_ref())?;
let plaintext_len = input_file.metadata()?.len();
let (guard, out_file) = AtomicOutput::create(output.as_ref())?;
let mut writer = BufWriter::with_capacity(BUF_SIZE, out_file);
encrypt_stream(input_file, &mut writer, key, plaintext_len)?;
let out_file = writer
.into_inner()
.map_err(|e| FileCryptError::Io(e.into_error()))?;
out_file.sync_all()?;
drop(out_file);
guard.commit()?;
Ok(())
}
/// Decrypt the file at `input` into `output`.
///
/// The destination is left untouched if authentication or any other part
/// of decryption fails.
pub fn decrypt_file(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
key: &[u8],
) -> Result<(), FileCryptError> {
check_key(key)?;
let input_file = File::open(input.as_ref())?;
let (guard, out_file) = AtomicOutput::create(output.as_ref())?;
let mut writer = BufWriter::with_capacity(BUF_SIZE, out_file);
decrypt_stream(input_file, &mut writer, key)?;
let out_file = writer
.into_inner()
.map_err(|e| FileCryptError::Io(e.into_error()))?;
out_file.sync_all()?;
drop(out_file);
guard.commit()?;
Ok(())
}
+63
View File
@@ -0,0 +1,63 @@
use super::error::FileCryptError;
pub const IV_LEN: usize = 16;
pub const NONCE_LEN: usize = 16;
/// The tag is as long as the key.
pub const MIN_KEY_LEN: usize = 16;
/// Must match `CAPACITY_SIZE` in `keccak_aead.rs`.
pub const MAX_KEY_LEN: usize = 24;
pub const HEADER_LEN: usize = 48;
const MAGIC: [u8; 4] = *b"KAEF";
const VERSION: u8 = 1;
pub(super) struct Header {
pub(super) key_len: usize,
pub(super) iv: [u8; IV_LEN],
pub(super) nonce: [u8; NONCE_LEN],
pub(super) plaintext_len: u64,
}
impl Header {
pub(super) fn to_bytes(&self) -> [u8; HEADER_LEN] {
let mut out = [0u8; HEADER_LEN];
out[0..4].copy_from_slice(&MAGIC);
out[4] = VERSION;
out[5] = self.key_len as u8;
// out[6..8] reserved, zero.
out[8..24].copy_from_slice(&self.iv);
out[24..40].copy_from_slice(&self.nonce);
out[40..48].copy_from_slice(&self.plaintext_len.to_le_bytes());
out
}
pub(super) fn from_bytes(bytes: &[u8; HEADER_LEN]) -> Result<Self, FileCryptError> {
if bytes[0..4] != MAGIC {
return Err(FileCryptError::BadMagic);
}
if bytes[4] != VERSION {
return Err(FileCryptError::UnsupportedVersion(bytes[4]));
}
let key_len = bytes[5] as usize;
if !(MIN_KEY_LEN..=MAX_KEY_LEN).contains(&key_len) || bytes[6..8] != [0, 0] {
return Err(FileCryptError::MalformedHeader);
}
Ok(Self {
key_len,
iv: bytes[8..24].try_into().unwrap(),
nonce: bytes[24..40].try_into().unwrap(),
plaintext_len: u64::from_le_bytes(bytes[40..48].try_into().unwrap()),
})
}
}
+28
View File
@@ -0,0 +1,28 @@
mod atomic;
mod error;
mod file;
mod format;
mod stream;
#[cfg(test)]
mod tests;
pub use error::FileCryptError;
pub use file::{decrypt_file, encrypt_file};
pub use format::{HEADER_LEN, MAX_KEY_LEN, MIN_KEY_LEN};
/// Chunk size for file I/O.
///
/// Must be a multiple of the AEAD's 16-byte chunk.
pub(super) const BUF_SIZE: usize = 64 * 1024;
const _: () = assert!(HEADER_LEN.is_multiple_of(16));
const _: () = assert!(BUF_SIZE.is_multiple_of(16));
pub(super) fn check_key(key: &[u8]) -> Result<(), FileCryptError> {
if (MIN_KEY_LEN..=MAX_KEY_LEN).contains(&key.len()) {
Ok(())
} else {
Err(FileCryptError::InvalidKeyLength(key.len()))
}
}
+167
View File
@@ -0,0 +1,167 @@
use std::io::{self, Read, Write};
use subtle::ConstantTimeEq;
use crate::keccak_aead::KeccakAead;
use super::error::FileCryptError;
use super::format::Header;
use super::{BUF_SIZE, HEADER_LEN, check_key};
/// `read_exact`, but EOF becomes `on_eof` instead of an `io::Error`.
fn read_exact_or<R: Read>(
reader: &mut R,
buf: &mut [u8],
on_eof: FileCryptError,
) -> Result<(), FileCryptError> {
reader.read_exact(buf).map_err(|e| {
if e.kind() == io::ErrorKind::UnexpectedEof {
on_eof
} else {
FileCryptError::Io(e)
}
})
}
fn at_eof<R: Read>(reader: &mut R) -> io::Result<bool> {
let mut byte = [0u8; 1];
loop {
match reader.read(&mut byte) {
Ok(n) => return Ok(n == 0),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {
continue;
}
Err(e) => return Err(e),
}
}
}
/// Encrypt exactly `plaintext_len` bytes from `reader` into `writer`.
///
/// The length is authenticated as part of the header.
pub fn encrypt_stream<R: Read, W: Write>(
mut reader: R,
mut writer: W,
key: &[u8],
plaintext_len: u64,
) -> Result<(), FileCryptError> {
check_key(key)?;
let mut iv = [0u8; 16];
let mut nonce = [0u8; 16];
getrandom::fill(&mut iv).map_err(FileCryptError::Random)?;
getrandom::fill(&mut nonce).map_err(FileCryptError::Random)?;
let header = Header {
key_len: key.len(),
iv,
nonce,
plaintext_len,
};
let header_bytes = header.to_bytes();
writer.write_all(&header_bytes)?;
let mut aead = KeccakAead::new(&header.iv, key, &header.nonce);
aead.associated_data_processing(&header_bytes);
let mut buf = vec![0u8; BUF_SIZE];
let mut remaining = plaintext_len;
while remaining > 0 {
let n = remaining.min(BUF_SIZE as u64) as usize;
read_exact_or(
&mut reader,
&mut buf[..n],
FileCryptError::InputLengthChanged,
)?;
let ciphertext = aead.plaintext_processing(&buf[..n]);
writer.write_all(&ciphertext)?;
remaining -= n as u64;
}
if !at_eof(&mut reader)? {
return Err(FileCryptError::InputLengthChanged);
}
let tag = aead.finalize(key);
writer.write_all(&tag)?;
writer.flush()?;
Ok(())
}
/// Decrypt `reader` into `writer` and verify the authentication tag.
///
/// Plaintext is written before authentication succeeds. The file-level API therefore uses an atomic temporary output file.
pub fn decrypt_stream<R: Read, W: Write>(
mut reader: R,
mut writer: W,
key: &[u8],
) -> Result<(), FileCryptError> {
check_key(key)?;
let mut header_bytes = [0u8; HEADER_LEN];
read_exact_or(&mut reader, &mut header_bytes, FileCryptError::Truncated)?;
let header = Header::from_bytes(&header_bytes)?;
if header.key_len != key.len() {
return Err(FileCryptError::KeyLengthMismatch {
header: header.key_len,
provided: key.len(),
});
}
let mut aead = KeccakAead::new(&header.iv, key, &header.nonce);
aead.associated_data_processing(&header_bytes);
let mut buf = vec![0u8; BUF_SIZE];
let mut remaining = header.plaintext_len;
while remaining > 0 {
let n = remaining.min(BUF_SIZE as u64) as usize;
read_exact_or(&mut reader, &mut buf[..n], FileCryptError::Truncated)?;
let plaintext = aead.ciphertext_processing(&buf[..n]);
writer.write_all(&plaintext)?;
remaining -= n as u64;
}
let mut stored_tag = vec![0u8; header.key_len];
read_exact_or(&mut reader, &mut stored_tag, FileCryptError::Truncated)?;
if !at_eof(&mut reader)? {
return Err(FileCryptError::TrailingData);
}
let computed_tag = aead.finalize(key);
let tags_match: bool = computed_tag.as_slice().ct_eq(stored_tag.as_slice()).into();
if !tags_match {
return Err(FileCryptError::AuthenticationFailed);
}
writer.flush()?;
Ok(())
}
+300
View File
@@ -0,0 +1,300 @@
use super::*;
use crate::file_crypt::stream::{decrypt_stream, encrypt_stream};
use std::fs;
use std::io::Cursor;
const KEY: [u8; 16] = [0x42; 16];
fn sample(n: usize) -> Vec<u8> {
(0..n)
.map(|i| (i.wrapping_mul(31).wrapping_add(7)) as u8)
.collect()
}
fn enc(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
encrypt_stream(
Cursor::new(plaintext),
&mut out,
key,
plaintext.len() as u64,
)
.unwrap();
out
}
fn dec(key: &[u8], data: &[u8]) -> Result<Vec<u8>, FileCryptError> {
let mut out = Vec::new();
decrypt_stream(Cursor::new(data), &mut out, key)?;
Ok(out)
}
fn boundary_sizes() -> Vec<usize> {
vec![
0,
1,
15,
16,
17,
31,
32,
33,
BUF_SIZE - 1,
BUF_SIZE,
BUF_SIZE + 1,
3 * BUF_SIZE + 5,
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keccak_aead::KeccakAead;
#[test]
fn roundtrip_various_sizes() {
for n in boundary_sizes() {
let plaintext = sample(n);
let encrypted = enc(&KEY, &plaintext);
assert_eq!(encrypted.len(), HEADER_LEN + n + KEY.len(), "size {n}");
assert_eq!(dec(&KEY, &encrypted).unwrap(), plaintext, "size {n}");
}
}
#[test]
fn roundtrip_all_key_lengths() {
for key_len in MIN_KEY_LEN..=MAX_KEY_LEN {
let key = vec![0xA5u8; key_len];
let plaintext = sample(100);
let encrypted = enc(&key, &plaintext);
assert_eq!(encrypted[5] as usize, key_len);
assert_eq!(encrypted.len(), HEADER_LEN + 100 + key_len);
assert_eq!(dec(&key, &encrypted).unwrap(), plaintext);
}
}
/// Chunked streaming must be byte-identical to the one-shot AEAD.
#[test]
fn matches_one_shot_aead() {
for n in boundary_sizes() {
let plaintext = sample(n);
let encrypted = enc(&KEY, &plaintext);
let header = &encrypted[..HEADER_LEN];
let iv = &header[8..24];
let nonce = &header[24..40];
let expected = KeccakAead::encrypt(&KEY, &plaintext, iv, header, nonce);
assert_eq!(
&encrypted[HEADER_LEN..HEADER_LEN + n],
&expected.cipher[..],
"size {n}"
);
assert_eq!(&encrypted[HEADER_LEN + n..], &expected.tag[..], "size {n}");
}
}
#[test]
fn iv_and_nonce_are_random() {
let a = enc(&KEY, b"same plaintext");
let b = enc(&KEY, b"same plaintext");
assert_ne!(a[8..24], b[8..24], "IV repeated");
assert_ne!(a[24..40], b[24..40], "nonce repeated");
assert_ne!(a[HEADER_LEN..], b[HEADER_LEN..]);
}
#[test]
fn every_single_bit_flip_is_rejected() {
let plaintext = sample(100);
let encrypted = enc(&KEY, &plaintext);
for pos in 0..encrypted.len() {
for bit in [0x01u8, 0x80] {
let mut tampered = encrypted.clone();
tampered[pos] ^= bit;
let result = dec(&KEY, &tampered);
assert!(result.is_err(), "flip at byte {pos} bit {bit:#x} accepted");
// Everything after the header must fail specifically on the tag.
if pos >= HEADER_LEN {
assert!(
matches!(result, Err(FileCryptError::AuthenticationFailed)),
"byte {pos}"
);
}
}
}
}
/// The AEAD absorbs a partial final ciphertext block zero-padded and
/// without a length, so `C || 00` yields the same tag as `C`. The
/// authenticated length in the header must stop that.
#[test]
fn trailing_zero_byte_forgery_is_rejected() {
let plaintext = sample(20); // final chunk has 4 bytes
let encrypted = enc(&KEY, &plaintext);
let mut forged = encrypted[..HEADER_LEN + 20].to_vec();
forged.push(0x00);
forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]);
// Without touching the header: structural error.
assert!(dec(&KEY, &forged).is_err());
// Also bumping the length field: the header is associated data.
forged[40..48].copy_from_slice(&21u64.to_le_bytes());
assert!(matches!(
dec(&KEY, &forged),
Err(FileCryptError::AuthenticationFailed)
));
}
#[test]
fn truncation_and_trailing_data() {
let encrypted = enc(&KEY, &sample(100));
for cut in [
0,
1,
HEADER_LEN - 1,
HEADER_LEN,
HEADER_LEN + 50,
encrypted.len() - 1,
] {
assert!(
matches!(dec(&KEY, &encrypted[..cut]), Err(FileCryptError::Truncated)),
"cut at {cut}"
);
}
let mut extended = encrypted.clone();
extended.push(0);
assert!(matches!(
dec(&KEY, &extended),
Err(FileCryptError::TrailingData)
));
}
#[test]
fn wrong_key_and_key_length() {
let encrypted = enc(&KEY, &sample(50));
let mut other = KEY;
other[0] ^= 1;
assert!(matches!(
dec(&other, &encrypted),
Err(FileCryptError::AuthenticationFailed)
));
assert!(matches!(
dec(&[0x42u8; 24], &encrypted),
Err(FileCryptError::KeyLengthMismatch {
header: 16,
provided: 24
})
));
assert!(matches!(
dec(&[0x42u8; 8], &encrypted),
Err(FileCryptError::InvalidKeyLength(8))
));
assert!(matches!(
encrypt_stream(Cursor::new(b"x"), Vec::new(), &[0u8; 25], 1),
Err(FileCryptError::InvalidKeyLength(25))
));
}
#[test]
fn encrypt_detects_length_change() {
let too_short = encrypt_stream(Cursor::new(vec![0u8; 10]), Vec::new(), &KEY, 11);
assert!(matches!(too_short, Err(FileCryptError::InputLengthChanged)));
let too_long = encrypt_stream(Cursor::new(vec![0u8; 12]), Vec::new(), &KEY, 11);
assert!(matches!(too_long, Err(FileCryptError::InputLengthChanged)));
}
#[test]
fn garbage_headers() {
assert!(matches!(
dec(&KEY, &[0u8; HEADER_LEN]),
Err(FileCryptError::BadMagic)
));
let mut bad_version = enc(&KEY, b"x");
bad_version[4] = 2;
assert!(matches!(
dec(&KEY, &bad_version),
Err(FileCryptError::UnsupportedVersion(2))
));
// Absurd declared length on a tiny file: must fail, not allocate.
let mut huge = enc(&KEY, b"x");
huge[40..48].copy_from_slice(&u64::MAX.to_le_bytes());
assert!(matches!(dec(&KEY, &huge), Err(FileCryptError::Truncated)));
}
#[test]
fn file_roundtrip_and_atomic_failure() {
let dir = tempfile::tempdir().unwrap();
let plain = dir.path().join("plain.bin");
let encrypted = dir.path().join("plain.bin.kaef");
let decrypted = dir.path().join("decrypted.bin");
let data = sample(3 * BUF_SIZE + 5);
fs::write(&plain, &data).unwrap();
encrypt_file(&plain, &encrypted, &KEY).unwrap();
decrypt_file(&encrypted, &decrypted, &KEY).unwrap();
assert_eq!(fs::read(&decrypted).unwrap(), data);
// Corrupt one ciphertext byte.
let mut bytes = fs::read(&encrypted).unwrap();
bytes[HEADER_LEN + 1000] ^= 1;
let corrupted = dir.path().join("corrupted.kaef");
fs::write(&corrupted, &bytes).unwrap();
// A failed decryption must not create the output
let out_new = dir.path().join("out_new.bin");
assert!(matches!(
decrypt_file(&corrupted, &out_new, &KEY),
Err(FileCryptError::AuthenticationFailed)
));
assert!(!out_new.exists());
// nor clobber an existing file.
let out_existing = dir.path().join("out_existing.bin");
fs::write(&out_existing, b"precious").unwrap();
assert!(decrypt_file(&corrupted, &out_existing, &KEY).is_err());
assert_eq!(fs::read(&out_existing).unwrap(), b"precious");
// No temporary files are left behind.
let leftovers: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".tmp"))
.collect();
assert!(leftovers.is_empty(), "leftover temp files: {leftovers:?}");
}
#[test]
fn empty_file_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let plain = dir.path().join("empty");
let encrypted = dir.path().join("empty.kaef");
let decrypted = dir.path().join("empty.out");
fs::write(&plain, b"").unwrap();
encrypt_file(&plain, &encrypted, &KEY).unwrap();
assert_eq!(
fs::metadata(&encrypted).unwrap().len() as usize,
HEADER_LEN + KEY.len()
);
decrypt_file(&encrypted, &decrypted, &KEY).unwrap();
assert_eq!(fs::read(&decrypted).unwrap(), b"");
}
}
+8 -8
View File
@@ -56,8 +56,8 @@ impl KeccakAead {
let mut input = [0u8; STATE_SIZE];
// r = chunk XOR state[0..16]
for i in 0..chunk.len() {
input[i] = chunk[i] ^ self.state[i];
for (i, &byte) in chunk.iter().enumerate() {
input[i] = byte ^ self.state[i];
}
// c = state[16..40]
@@ -78,8 +78,8 @@ impl KeccakAead {
let mut input = [0u8; STATE_SIZE];
// r = chunk XOR state[0..16]
for i in 0..chunk.len() {
ciphertext.push(chunk[i] ^ self.state[i]);
for (i, &byte) in chunk.iter().enumerate() {
ciphertext.push(byte ^ self.state[i]);
}
// input = r || c
@@ -105,8 +105,8 @@ impl KeccakAead {
let mut input = [0u8; STATE_SIZE];
// plaintext = ciphertext XOR state[0..16]
for i in 0..chunk.len() {
plaintext.push(chunk[i] ^ self.state[i]);
for (i, &byte) in chunk.iter().enumerate() {
plaintext.push(byte ^ self.state[i]);
}
// input = ciphertext || c
@@ -137,8 +137,8 @@ impl KeccakAead {
padded_key[padding_len..].copy_from_slice(key);
// XOR padded key into c.
for i in 0..CAPACITY_SIZE {
self.state[RATE_SIZE + i] ^= padded_key[i];
for (i, &byte) in padded_key.iter().enumerate().take(CAPACITY_SIZE) {
self.state[RATE_SIZE + i] ^= byte;
}
// SHAKE256(state, 40)
+95 -2
View File
@@ -1,6 +1,99 @@
use std::error::Error;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
mod file_crypt;
mod keccak;
mod keccak_aead;
fn main() {
println!("Hello, world!");
use file_crypt::{MAX_KEY_LEN, decrypt_file, encrypt_file};
/// File encryption utility using KeccakAead.
#[derive(Parser, Debug)]
#[command(
name = "filecrypt-rs",
version,
about = "Encrypt and decrypt files using KeccakAead"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Encrypt a file.
Encrypt {
/// Input plaintext file.
#[arg(short, long, value_name = "FILE")]
input: PathBuf,
/// Output encrypted file.
#[arg(short, long, value_name = "FILE")]
output: PathBuf,
/// Password used to derive the encryption key.
#[arg(short, long, value_name = "PASSWORD")]
password: String,
},
/// Decrypt a file.
Decrypt {
/// Input encrypted file.
#[arg(short, long, value_name = "FILE")]
input: PathBuf,
/// Output plaintext file.
#[arg(short, long, value_name = "FILE")]
output: PathBuf,
/// Password used to derive the encryption key.
#[arg(short, long, value_name = "PASSWORD")]
password: String,
},
}
/// Derive the fixed-length AEAD key from a password.
///
/// SHA-3-256 produces 32 bytes, while `KeccakAead` currently accepts keys from 16 through 24 bytes. Therefore we use the first 24 bytes.
fn derive_key(password: &str) -> [u8; MAX_KEY_LEN] {
let digest = keccak::sha3_256(password.as_bytes());
let mut key = [0u8; MAX_KEY_LEN];
key.copy_from_slice(&digest[..MAX_KEY_LEN]);
key
}
fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
match cli.command {
Command::Encrypt {
input,
output,
password,
} => {
let key = derive_key(&password);
encrypt_file(&input, &output, &key)?;
println!("Encrypted '{}' -> '{}'", input.display(), output.display());
}
Command::Decrypt {
input,
output,
password,
} => {
let key = derive_key(&password);
decrypt_file(&input, &output, &key)?;
println!("Decrypted '{}' -> '{}'", input.display(), output.display());
}
}
Ok(())
}