fcrypt 0.3.2

Cross-platform CLI for password-based file encryption and decryption
Documentation
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;

use crate::error::{AppError, Result};
use crate::sym::crypto::{self, CryptoConfig};

pub fn encrypt_file<F>(
    input_path: &Path,
    output_path: &Path,
    password: &str,
    config: &CryptoConfig,
    allow_overwrite: bool,
    on_progress: F,
) -> Result<()>
where
    F: FnMut(u64),
{
    if output_path.exists() && !allow_overwrite {
        return Err(AppError::OutputExists(output_path.to_path_buf()));
    }

    let input_file = File::open(input_path)?;
    let input_len = input_file.metadata()?.len();
    let reader_capacity = config.chunk_size.max(64 * 1024);
    let mut reader = BufReader::with_capacity(reader_capacity, input_file);

    let output_dir = output_parent_dir(output_path);
    let mut temp_output = NamedTempFile::new_in(output_dir)?;
    {
        let writer_capacity = config
            .chunk_size
            .checked_add(crypto::TAG_LEN)
            .ok_or(AppError::InputTooLarge)?
            .max(64 * 1024);
        let mut writer = BufWriter::with_capacity(writer_capacity, temp_output.as_file_mut());
        crypto::encrypt_stream(
            &mut reader,
            &mut writer,
            input_len,
            password,
            config,
            on_progress,
        )?;
        writer.flush()?;
    }

    temp_output.as_file_mut().sync_all()?;
    persist_temp_file(temp_output, output_path, allow_overwrite)
}

pub fn decrypt_file<F>(
    input_path: &Path,
    output_path: &Path,
    password: &str,
    config: &CryptoConfig,
    allow_overwrite: bool,
    mut on_progress: F,
) -> Result<()>
where
    F: FnMut(u64),
{
    if output_path.exists() && !allow_overwrite {
        return Err(AppError::OutputExists(output_path.to_path_buf()));
    }

    let input_file = File::open(input_path)?;
    let input_len = input_file.metadata()?.len();
    let reader_capacity = config.chunk_size.max(64 * 1024);
    let mut reader = BufReader::with_capacity(reader_capacity, input_file);

    let output_dir = output_parent_dir(output_path);
    let mut temp_output = NamedTempFile::new_in(output_dir)?;
    {
        let writer_capacity = config
            .chunk_size
            .checked_add(crypto::TAG_LEN)
            .ok_or(AppError::InputTooLarge)?
            .max(64 * 1024);
        let mut writer = BufWriter::with_capacity(writer_capacity, temp_output.as_file_mut());
        crypto::decrypt_stream(&mut reader, &mut writer, input_len, password, config, |n| {
            on_progress(n)
        })?;
        writer.flush()?;
    }

    temp_output.as_file_mut().sync_all()?;
    persist_temp_file(temp_output, output_path, allow_overwrite)
}

fn persist_temp_file(
    temp_file: NamedTempFile,
    output_path: &Path,
    allow_overwrite: bool,
) -> Result<()> {
    let result = if allow_overwrite {
        temp_file.persist(output_path)
    } else {
        temp_file.persist_noclobber(output_path)
    };

    result.map(|_| ()).map_err(|e| {
        if e.error.kind() == std::io::ErrorKind::AlreadyExists {
            AppError::OutputExists(output_path.to_path_buf())
        } else {
            AppError::Io(e.error)
        }
    })
}

fn output_parent_dir(output_path: &Path) -> PathBuf {
    output_path
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| PathBuf::from("."))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn persist_temp_file_does_not_clobber_without_overwrite() {
        let dir = tempdir().expect("tempdir must be created");
        let output = dir.path().join("output.bin");
        std::fs::write(&output, b"old").expect("existing output must be written");

        let mut temp_output =
            NamedTempFile::new_in(dir.path()).expect("temp output must be created");
        temp_output
            .write_all(b"new")
            .expect("temp output must be written");

        let err = persist_temp_file(temp_output, &output, false)
            .expect_err("persist must fail when output exists");

        assert!(matches!(err, AppError::OutputExists(path) if path == output));
        assert_eq!(
            std::fs::read(&output).expect("output must remain readable"),
            b"old"
        );
    }

    #[test]
    fn persist_temp_file_replaces_when_overwrite_allowed() {
        let dir = tempdir().expect("tempdir must be created");
        let output = dir.path().join("output.bin");
        std::fs::write(&output, b"old").expect("existing output must be written");

        let mut temp_output =
            NamedTempFile::new_in(dir.path()).expect("temp output must be created");
        temp_output
            .write_all(b"new")
            .expect("temp output must be written");

        persist_temp_file(temp_output, &output, true).expect("persist must replace output");

        assert_eq!(
            std::fs::read(&output).expect("output must remain readable"),
            b"new"
        );
    }
}