diff --git a/Cargo.lock b/Cargo.lock index d4b7aab..d4a5225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1237,6 +1237,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rpassword" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rusb" version = "0.8.1" @@ -1586,6 +1607,7 @@ dependencies = [ "openpgp-card", "openpgp-card-pcsc", "reqwest", + "rpassword", "rust_util", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 972a97f..7a9547e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ hex = "0.4.3" openpgp-card = "0.3.3" openpgp-card-pcsc = "0.3.0" reqwest = { version = "0.11.14", features = ["blocking", "rustls", "rustls-tls"] } +rpassword = "7.2.0" rust_util = "0.6.41" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index b62fd63..a481c6f 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -1,22 +1,81 @@ -use std::path::PathBuf; +use std::fs; use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; + use base64::Engine; -use base64::engine::general_purpose; use openpgp_card::crypto_data::Cryptogram; use openpgp_card::OpenPgp; -use rust_util::{debugging, failure, opt_result, simple_error, success, XResult}; -use crate::card::get_card; -use crate::file; +use rust_util::{debugging, failure, opt_result, simple_error, success, util_term, XResult}; -pub fn decrypt(path: PathBuf, pin: &Option) -> XResult<()> { +use crate::{file, util}; +use crate::card::get_card; +use crate::spec::{TinyEncryptEnvelop, TinyEncryptMeta}; +use crate::util::{decode_base64, TINY_ENC_FILE_EXT}; + +pub fn decrypt(path: &PathBuf, pin: &Option) -> XResult<()> { let path_display = format!("{}", path.display()); + if !path_display.ends_with(TINY_ENC_FILE_EXT) { + return simple_error!("File is not tiny encrypt file: {}", &path_display); + } let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display); let mut meta = opt_result!(file::read_tiny_encrypt_meta(&mut file_in), "Read file: {}, failed: {}", &path_display); meta.normalize(); - debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap()); + let path_out = &path_display[0..path_display.len() - TINY_ENC_FILE_EXT.len()]; + if let Ok(_) = fs::metadata(path_out) { + return simple_error!("Output file: {} exists", path_out); + } - let mut card = match get_card() { + debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap()); + let selected_envelop = select_envelop(&meta)?; + + let key = try_decrypt_key(selected_envelop, pin)?; + let nonce = opt_result!( decode_base64(&meta.nonce), "Decode nonce failed: {}"); + + debugging!("Decrypt key: {}", hex::encode(&key)); + debugging!("Decrypt nonce: {}", hex::encode(&nonce)); + + let mut file_out = File::create(path_out)?; + let _ = decrypt_file(&mut file_in, &mut file_out, &key, &nonce)?; + + Ok(()) +} + +fn decrypt_file(file_in: &mut File, file_out: &mut File, key: &[u8], nonce: &[u8]) -> XResult { + let mut total_len = 0; + let mut buffer = [0u8; 1024 * 8]; + let key = opt_result!(key.try_into(), "Key is not 32 bytes: {}"); + let mut decryptor = aes_gcm_stream::Aes256GcmStreamDecryptor::new(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: {}"); + if !last_block.is_empty() { + opt_result!(file_out.write_all(&last_block), "Write file failed: {}"); + } + success!("Decrypt finished, total bytes: {}", total_len); + break; + } else { + total_len += len; + let decrypted = decryptor.update(&buffer[0..len]); + if !decrypted.is_empty() { + opt_result!(file_out.write_all(&decrypted), "Write file failed: {}"); + } + } + } + Ok(total_len) +} + +fn try_decrypt_key(envelop: &TinyEncryptEnvelop, pin: &Option) -> XResult> { + match envelop.r#type.to_lowercase().as_str() { + "pgp" => try_decrypt_key_pgp(envelop, pin), + unknown_type => return simple_error!("Unknown or not supported type: {}", unknown_type) + } +} + +fn try_decrypt_key_pgp(envelop: &TinyEncryptEnvelop, pin: &Option) -> XResult> { + let card = match get_card() { Err(e) => { failure!("Get PGP card failed: {}", e); return simple_error!("Get card failed: {}", e); @@ -26,20 +85,62 @@ pub fn decrypt(path: PathBuf, pin: &Option) -> XResult<()> { let mut pgp = OpenPgp::new(card); let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}"); - let pin = pin.as_ref().map(|s| s.as_str()).unwrap_or_else(|| "123456"); + let pin = read_pin(pin); if let Err(e) = trans.verify_pw1_user(pin.as_ref()) { failure!("Verify user pin failed: {}", e); return simple_error!("User pin verify failed: {}", e); } success!("User pin verify success!"); - let pgp_envelop = meta.pgp_envelop.unwrap(); + let pgp_envelop = &envelop.encrypted_key; debugging!("PGP envelop: {}", &pgp_envelop); - let pgp_envelop_bytes = opt_result!(general_purpose::STANDARD.decode(&pgp_envelop), "Decode PGP envelop failed: {}"); + let pgp_envelop_bytes = opt_result!(decode_base64(&pgp_envelop), "Decode PGP envelop failed: {}"); let key = trans.decipher(Cryptogram::RSA(&pgp_envelop_bytes))?; + Ok(key) +} - success!("{}", hex::encode(&key)); +fn read_pin(pin: &Option) -> String { + match pin { + Some(pin) => pin.to_string(), + None => if util_term::read_yes_no("Use default PIN 123456, please confirm") { + "123456".into() + } else { + rpassword::prompt_password("Please input PIN: ").expect("Read PIN failed") + } + } +} - Ok(()) +fn select_envelop(meta: &TinyEncryptMeta) -> XResult<&TinyEncryptEnvelop> { + let envelops = match &meta.envelops { + None => return simple_error!("No envelops found"), + Some(envelops) => if envelops.is_empty() { + return simple_error!("No envelops found"); + } else { + envelops + }, + }; + + success!("Found {} envelops:", envelops.len()); + if envelops.len() == 1 { + let selected_envelop = &envelops[0]; + success!("Auto selected envelop: #{} {}", 1, selected_envelop.r#type.to_uppercase()); + return Ok(selected_envelop); + } + + envelops.iter().enumerate().for_each(|(i, envelop)| { + println!("#{} {}{}", i + 1, + envelop.r#type.to_uppercase(), + if envelop.kid.is_empty() { + "".into() + } else { + format!(", Kid: {}", envelop.kid) + } + ); + }); + + let envelop_number = util::read_number("Please select an envelop:", 1, envelops.len()); + let selected_envelop = &envelops[envelop_number - 1]; + success!("Selected envelop: #{} {}", envelop_number, selected_envelop.r#type.to_uppercase()); + Ok(selected_envelop) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3a52f4f..c66179b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate core; use std::path::PathBuf; use clap::{Parser, Subcommand}; -use rust_util::{information, XResult}; +use rust_util::{failure, information, success, XResult}; mod util; mod config; @@ -35,6 +35,8 @@ enum Commands { Decrypt { /// Files need to be decrypted paths: Vec, + #[arg(long)] + pin: Option, }, /// Show file info #[command(arg_required_else_help = true, short_flag = 'I')] @@ -54,9 +56,12 @@ fn main() -> XResult<()> { paths.iter().for_each(|f| information!("{:?}", f)); Ok(()) } - Commands::Decrypt { mut paths } => { - for path in paths { - cmd_decrypt::decrypt(path, &Some("123456".to_string())).unwrap(); + Commands::Decrypt { mut paths, pin } => { + for path in &paths { + match cmd_decrypt::decrypt(path, &pin) { + Ok(_) => success!("Decrypt {} succeed", path.to_str().unwrap_or("N/A")), + Err(e) => failure!("Decrypt {} failed: {}", path.to_str().unwrap_or("N/A"), e), + } } Ok(()) } diff --git a/src/util.rs b/src/util.rs index 86b9796..431a6ca 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,33 @@ +use std::io; +use std::io::Write; + +use base64::Engine; +use base64::engine::general_purpose; +use rust_util::{warning, XResult}; + +pub const TINY_ENC_FILE_EXT: &str = ".tinyenc"; + +pub fn decode_base64(input: &str) -> XResult> { + Ok(general_purpose::STANDARD.decode(input)?) +} + +pub fn read_number(hint: &str, from: usize, to: usize) -> usize { + loop { + print!("{} ({}-{}): ", hint, from, to); + io::stdout().flush().ok(); + let mut buff = String::new(); + let _ = io::stdin().read_line(&mut buff).expect("Read line from stdin"); + let buff = buff.trim(); + match buff.parse() { + Err(_) => warning!("Input number error!"), + Ok(number) => if number < from || number > to { + warning!("Input number is not in range."); + } else { + return number; + }, + } + } +} pub fn get_user_agent() -> String { format!("TinyEncrypt-rs v{}@{}", env!("CARGO_PKG_VERSION"),