use std::fs::{self, File};
use std::io::{Read, Write};
use crate::cli::EncryptionArgs;
use crate::file_ops::open_private_key;
use crate::utils::{is_dir, is_file};
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::aead::stream;
use aes_gcm::{Aes256Gcm, KeyInit, aead::OsRng};
use anyhow::Result;
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use walkdir::WalkDir;
fn is_excluded_dir(path: &str, exclude_list: &[String]) -> bool {
exclude_list.iter().any(|ex| path.contains(ex))
}
fn encrypt_file_stream(key_bytes: &[u8], source: &str, should_remove: bool) -> Result<()> {
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(key_bytes);
let cipher = Aes256Gcm::new(key);
let mut nonce = [0u8; 7];
OsRng.fill_bytes(&mut nonce);
let mut encryptor = stream::EncryptorBE32::from_aead(cipher, &nonce.into());
let dest = format!("{}.enc", source);
let mut source_file = File::open(source)?;
let mut dest_file = File::create(&dest)?;
dest_file.write_all(&nonce)?;
let mut buffer = vec![0u8; 131072];
loop {
let read_count = source_file.read(&mut buffer)?;
if read_count == buffer.len() {
let ciphertext = encryptor
.encrypt_next(buffer[..].as_ref())
.map_err(|_| anyhow::anyhow!("Encryption error on chunk"))?;
dest_file.write_all(&ciphertext)?;
} else {
let ciphertext = encryptor
.encrypt_last(&buffer[..read_count])
.map_err(|_| anyhow::anyhow!("Encryption error on final chunk"))?;
dest_file.write_all(&ciphertext)?;
break;
}
}
if should_remove {
fs::remove_file(source)?;
}
Ok(())
}
pub fn run(enc_args: EncryptionArgs) -> Result<()> {
let key_bytes = open_private_key(&enc_args.common.private_key_path)?;
if is_dir(&enc_args.common.source) {
let files_to_process: Vec<_> = WalkDir::new(&enc_args.common.source)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter(|e| {
!is_excluded_dir(
&e.path().to_string_lossy(),
enc_args.exclude_dir.as_deref().unwrap_or(&[]),
)
})
.map(|e| e.path().to_string_lossy().to_string())
.filter(|path| !path.ends_with(".enc"))
.collect();
let pb = ProgressBar::new(files_to_process.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} files ({eta})")
.unwrap()
.progress_chars("#>-"));
files_to_process.into_par_iter().for_each(|file_path| {
if let Err(e) = encrypt_file_stream(&key_bytes, &file_path, enc_args.common.remove_file)
{
eprintln!(
"{} Failed to encrypt {}: {}",
style("✗").red().bold(),
style(&file_path).bold(),
e,
);
}
pb.inc(1);
});
pb.finish_with_message("Encryption complete");
} else if is_file(&enc_args.common.source) {
if !enc_args.common.source.ends_with(".enc") {
encrypt_file_stream(
&key_bytes,
&enc_args.common.source,
enc_args.common.remove_file,
)?;
}
} else {
eprintln!(
"{} Path '{}' is not valid.",
style("✗").red().bold(),
style(&enc_args.common.source).bold(),
);
}
Ok(())
}