diff --git a/Cargo.lock b/Cargo.lock index 1ad5a22..1133a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,7 +1727,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "0.6.0" +version = "0.7.0" dependencies = [ "aes-gcm-stream", "base64", diff --git a/Cargo.toml b/Cargo.toml index 64e76bc..6f90e0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "0.6.0" +version = "0.7.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index 7015ba9..6b356b6 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -230,8 +230,8 @@ fn decrypt_file(file_in: &mut File, file_len: u64, file_out: &mut impl Write, last_block }; opt_result!(file_out.write_all(&last_block), "Write file failed: {}"); - debugging!("Decrypt finished, total: {} byte(s)", total_len); progress.finish(); + debugging!("Decrypt finished, total: {} byte(s)", total_len); break; } else { total_len += len as u64; diff --git a/src/cmd_directdecrypt.rs b/src/cmd_directdecrypt.rs new file mode 100644 index 0000000..219dd8b --- /dev/null +++ b/src/cmd_directdecrypt.rs @@ -0,0 +1,120 @@ +use std::fs; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::time::Instant; + +use clap::Args; +use rust_util::{debugging, information, opt_result, simple_error, success, warning, XResult}; +use zeroize::Zeroize; + +use crate::crypto_cryptor::{Cryptor, KeyNonce}; +use crate::util; +use crate::util_progress::Progress; + +#[derive(Debug, Args)] +pub struct CmdDirectDecrypt { + /// Files in + #[arg(long, short = 'i')] + pub file_in: PathBuf, + /// Files output + #[arg(long, short = 'o')] + pub file_out: PathBuf, + /// Remove source file + #[arg(long, short = 'R')] + pub remove_file: bool, + /// Key in HEX + #[arg(long, short = 'k')] + pub key: String, +} + +impl Drop for CmdDirectDecrypt { + fn drop(&mut self) { + self.key.zeroize(); + } +} + +const DIRECT_ENCRYPT_MAGIC: &str = "e2c50001"; + +// Format +// [4 bytes] - magic 0xe2c50001 +// [32 bytes] - key digest +// [12 bytes] - nonce +pub fn direct_decrypt(cmd_direct_decrypt: CmdDirectDecrypt) -> XResult<()> { + let key = opt_result!(hex::decode(&cmd_direct_decrypt.key), "Parse key failed: {}"); + if key.len() != 32 { + return simple_error!("Key length error, must be AES256."); + } + + let mut file_in = opt_result!(File::open(&cmd_direct_decrypt.file_in), "Open in file failed: {}"); + let file_in_len = file_in.metadata().map(|m| m.len()).unwrap_or(0); + if fs::metadata(&cmd_direct_decrypt.file_out).is_ok() { + return simple_error!("Out file exists."); + } + + let mut magic = [0_u8; 4]; + opt_result!(file_in.read_exact(&mut magic), "Read magic failed: {}"); + if hex::encode(magic) != DIRECT_ENCRYPT_MAGIC { + return simple_error!("File magic mismatch."); + } + let mut key_digest = [0_u8; 32]; + opt_result!(file_in.read_exact(&mut key_digest), "Read key digest failed: {}"); + if sha256::digest(&key) != hex::encode(key_digest) { + debugging!("Key digest mismatch: {} vs {}", sha256::digest(&key), hex::encode(key_digest)); + return simple_error!("Key digest mismatch."); + } + let mut nonce = [0_u8; 12]; + opt_result!(file_in.read_exact(&mut nonce), "Read nonce failed: {}"); + + let mut file_out = opt_result!(File::create(&cmd_direct_decrypt.file_out), "Create out file failed: {}"); + let key_nonce = KeyNonce { k: &key, n: &nonce }; + let instant = Instant::now(); + let decrypted_len = opt_result!( + decrypt_file(&mut file_in, file_in_len, &mut file_out, Cryptor::Aes256Gcm, &key_nonce), + "Decrypt file {} -> {}, failed: {}", + cmd_direct_decrypt.file_in.display(), + cmd_direct_decrypt.file_out.display() + ); + let elapsed_millis = instant.elapsed().as_millis(); + success!("Decrypt file succeed: {}, file size: {} byte(s), elapsed: {} ms", + cmd_direct_decrypt.file_out.display(), decrypted_len, elapsed_millis); + + util::zeroize(key); + nonce.zeroize(); + drop(file_in); + drop(file_out); + + if cmd_direct_decrypt.remove_file { + information!("Remove in file: {}", cmd_direct_decrypt.file_in.display()); + if let Err(e) = fs::remove_file(&cmd_direct_decrypt.file_in) { + warning!("Remove in file failed: {}", e); + } + } + + Ok(()) +} + + +fn decrypt_file(file_in: &mut File, file_len: u64, file_out: &mut impl Write, + cryptor: Cryptor, key_nonce: &KeyNonce) -> XResult { + let mut total_len = 0_u64; + let mut buffer = [0u8; 1024 * 8]; + let progress = Progress::new(file_len); + let mut decryptor = cryptor.decryptor(key_nonce)?; + loop { + let len = opt_result!(file_in.read(&mut buffer), "Read file failed: {}"); + if len == 0 { + let last_block = opt_result!(decryptor.finalize(), "Decrypt file failed: {}"); + opt_result!(file_out.write_all(&last_block), "Write file failed: {}"); + progress.finish(); + debugging!("Decrypt finished, total: {} byte(s)", total_len); + break; + } else { + total_len += len as u64; + let decrypted = decryptor.update(&buffer[0..len]); + opt_result!(file_out.write_all(&decrypted), "Write file failed: {}"); + progress.position(total_len); + } + } + Ok(total_len) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f1f9ada..d21b10e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ pub use cmd_config::config; pub use cmd_decrypt::CmdDecrypt; pub use cmd_decrypt::decrypt; pub use cmd_decrypt::decrypt_single; +pub use cmd_directdecrypt::CmdDirectDecrypt; +pub use cmd_directdecrypt::direct_decrypt; pub use cmd_encrypt::CmdEncrypt; pub use cmd_encrypt::encrypt; pub use cmd_encrypt::encrypt_single; @@ -38,4 +40,5 @@ mod cmd_config; mod cmd_info; mod cmd_decrypt; mod cmd_encrypt; +mod cmd_directdecrypt; diff --git a/src/main.rs b/src/main.rs index b7889ac..028bf88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate core; use clap::{Parser, Subcommand}; use rust_util::XResult; -use tiny_encrypt::{CmdConfig, CmdDecrypt, CmdEncrypt, CmdInfo, CmdVersion}; +use tiny_encrypt::{CmdConfig, CmdDecrypt, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdVersion}; #[derive(Debug, Parser)] #[command(name = "tiny-encrypt-rs")] @@ -21,6 +21,9 @@ enum Commands { /// Decrypt file(s) #[command(arg_required_else_help = true, short_flag = 'd')] Decrypt(CmdDecrypt), + /// Direct decrypt file(s) + #[command(arg_required_else_help = true)] + DirectDecrypt(CmdDirectDecrypt), /// Show file info #[command(arg_required_else_help = true, short_flag = 'I')] Info(CmdInfo), @@ -37,6 +40,7 @@ fn main() -> XResult<()> { match args.command { Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt), Commands::Decrypt(cmd_decrypt) => tiny_encrypt::decrypt(cmd_decrypt), + Commands::DirectDecrypt(cmd_direct_decrypt) => tiny_encrypt::direct_decrypt(cmd_direct_decrypt), Commands::Info(cmd_info) => tiny_encrypt::info(cmd_info), Commands::Version(cmd_version) => tiny_encrypt::version(cmd_version), Commands::Config(cmd_config) => tiny_encrypt::config(cmd_config),