From 76b8f93ddd88931674fd17d3b431847683b2c3e6 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 2 Dec 2023 15:26:15 +0800 Subject: [PATCH] feat: v1.0.0, suport exec-env --- Cargo.lock | 37 ++++++++- Cargo.toml | 6 +- justfile | 4 +- src/cmd_decrypt.rs | 24 +++--- src/cmd_execenv.rs | 154 +++++++++++++++++++++++++++++++++++++ src/cmd_initkeychainkey.rs | 40 ++++++++++ src/cmd_version.rs | 2 + src/lib.rs | 10 +++ src/main.rs | 14 +++- 9 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 src/cmd_execenv.rs create mode 100644 src/cmd_initkeychainkey.rs diff --git a/Cargo.lock b/Cargo.lock index a5f7ca0..f05f19d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -1403,6 +1413,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "num-bigint", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.20" @@ -1657,7 +1691,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "0.9.1" +version = "1.0.0" dependencies = [ "aes-gcm-stream", "base64", @@ -1677,6 +1711,7 @@ dependencies = [ "rsa", "rust-crypto", "rust_util", + "security-framework", "serde", "serde_json", "simpledateformat", diff --git a/Cargo.toml b/Cargo.toml index ced87cb..25612ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "0.9.1" +version = "1.0.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" @@ -9,8 +9,9 @@ repository = "https://git.hatter.ink/hatter/tiny-encrypt-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["smartcard"] +default = ["smartcard", "macos"] smartcard = ["openpgp-card", "openpgp-card-pcsc", "yubikey"] +macos = ["security-framework"] [dependencies] aes-gcm-stream = "0.2" @@ -32,6 +33,7 @@ rpassword = "7.3" rsa = { version = "0.9", features = ["pem"] } rust-crypto = "0.2" rust_util = "0.6" +security-framework = { version = "2.9.2", features = ["OSX_10_15"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simpledateformat = "0.1" diff --git a/justfile b/justfile index 7187b0e..76f4f84 100644 --- a/justfile +++ b/justfile @@ -5,7 +5,7 @@ _: build: cargo build --release -# Build release without smartcard -build-no-smartcard: +# Build release without features +build-no-features: cargo build --release --no-default-features diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index 6fe4b76..aef9e29 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -87,14 +87,8 @@ pub fn decrypt(cmd_decrypt: CmdDecrypt) -> XResult<()> { if cmd_decrypt.edit_file && (cmd_decrypt.paths.len() != 1) { return simple_error!("Edit mode only allows one file assigned."); } - let pin = match &cmd_decrypt.pin { - Some(pin) => Some(pin.clone()), - None => util_env::get_pin(), - }; - let key_id = match &cmd_decrypt.key_id { - Some(key_id) => Some(key_id.clone()), - None => util_env::get_key_id(), - }; + let pin = cmd_decrypt.pin.clone().or_else(util_env::get_pin); + let key_id = cmd_decrypt.key_id.clone().or_else(util_env::get_key_id); for path in &cmd_decrypt.paths { let start_decrypt_single = Instant::now(); @@ -322,8 +316,8 @@ fn create_edit_temp_file(file_content: &[u8], path_out: &str) -> XResult XResult> { +pub fn decrypt_limited_content_to_vec(mut file_in: &mut File, + meta: &TinyEncryptMeta, cryptor: Cryptor, key_nonce: &KeyNonce) -> XResult> { if meta.file_length > 100 * 1024 { failure!("File too large(more than 100K) cannot direct print on console."); return Ok(None); @@ -420,10 +414,10 @@ fn parse_encrypted_meta(meta: &TinyEncryptMeta, cryptor: Cryptor, key_nonce: &Ke Ok(Some(enc_meta)) } -fn try_decrypt_key(config: &Option, - envelop: &TinyEncryptEnvelop, - pin: &Option, - slot: &Option) -> XResult> { +pub fn try_decrypt_key(config: &Option, + envelop: &TinyEncryptEnvelop, + pin: &Option, + slot: &Option) -> XResult> { match envelop.r#type { TinyEncryptEnvelopType::Pgp => try_decrypt_key_pgp(envelop, pin), TinyEncryptEnvelopType::PgpX25519 => try_decrypt_key_ecdh_pgp_x25519(envelop, pin), @@ -511,7 +505,7 @@ fn try_decrypt_key_pgp(envelop: &TinyEncryptEnvelop, pin: &Option) -> XR Ok(key) } -fn select_envelop<'a>(meta: &'a TinyEncryptMeta, key_id: &Option, config: &Option) -> XResult<&'a TinyEncryptEnvelop> { +pub fn select_envelop<'a>(meta: &'a TinyEncryptMeta, key_id: &Option, config: &Option) -> XResult<&'a TinyEncryptEnvelop> { let envelops = match &meta.envelops { None => return simple_error!("No envelops found"), Some(envelops) => if envelops.is_empty() { diff --git a/src/cmd_execenv.rs b/src/cmd_execenv.rs new file mode 100644 index 0000000..9a7f9aa --- /dev/null +++ b/src/cmd_execenv.rs @@ -0,0 +1,154 @@ +use std::fs::File; +use std::path::PathBuf; +use std::process::Command; +use std::time::Instant; + +use clap::Args; +use rust_util::{debugging, iff, information, opt_result, simple_error, util_cmd, util_msg, warning, XResult}; +use serde_json::Value; +use zeroize::Zeroize; + +use crate::{consts, util, util_env}; +use crate::cmd_decrypt::{decrypt_limited_content_to_vec, select_envelop, try_decrypt_key}; +use crate::config::TinyEncryptConfig; +use crate::consts::TINY_ENC_CONFIG_FILE; +use crate::crypto_cryptor::{Cryptor, KeyNonce}; +use crate::util::SecVec; +use crate::util_enc_file; + +#[derive(Debug, Args)] +pub struct CmdExecEnv { + /// PIN + #[arg(long, short = 'p')] + pub pin: Option, + /// KeyID + #[arg(long, short = 'k')] + pub key_id: Option, + /// Slot + #[arg(long, short = 's')] + pub slot: Option, + // Tiny encrypt file name + pub file_name: String, + // Arguments + pub arguments: Vec, +} + +impl Drop for CmdExecEnv { + fn drop(&mut self) { + if let Some(p) = self.pin.as_mut() { p.zeroize(); } + } +} + +pub fn exec_env(cmd_exec_env: CmdExecEnv) -> XResult<()> { + debugging!("Cmd exec env: {:?}", cmd_exec_env); + let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE).ok(); + if cmd_exec_env.arguments.is_empty() { + return simple_error!("No commands assigned."); + } + + let start = Instant::now(); + let pin = cmd_exec_env.pin.clone().or_else(util_env::get_pin); + let key_id = cmd_exec_env.key_id.clone().or_else(util_env::get_key_id); + + let path = PathBuf::from(&cmd_exec_env.file_name); + let path_display = format!("{}", &path.display()); + util::require_tiny_enc_file_and_exists(&path)?; + + let mut file_in = opt_result!(File::open(&path), "Open file: {} failed: {}", &path_display); + let (_, _, meta) = opt_result!( + util_enc_file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display); + util_msg::when_debug(|| { + debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap()); + }); + + let encryption_algorithm = meta.encryption_algorithm.as_deref() + .unwrap_or(consts::TINY_ENC_AES_GCM); + let cryptor = Cryptor::from(encryption_algorithm)?; + + let selected_envelop = select_envelop(&meta, &key_id, &config)?; + + let key = SecVec(try_decrypt_key(&config, selected_envelop, &pin, &cmd_exec_env.slot)?); + let nonce = SecVec(opt_result!(util::decode_base64(&meta.nonce), "Decode nonce failed: {}")); + let key_nonce = KeyNonce { k: key.as_ref(), n: nonce.as_ref() }; + + let decrypted_content = decrypt_limited_content_to_vec(&mut file_in, &meta, cryptor, &key_nonce)?; + let exit_code = if let Some(output) = decrypted_content { + debugging!("Outputs: {}", output); + let arguments = &cmd_exec_env.arguments; + let envs = parse_output_to_env(&output); + + let mut command = Command::new(&arguments[0]); + arguments.iter().skip(1).for_each(|a| { command.arg(a); }); + envs.iter().for_each(|(k, v)| { command.env(k, v); }); + + debugging!("Run cmd: {:?}", command); + let run_cmd_result = util_cmd::run_command_and_wait(&mut command)?; + debugging!("Run cmd result: {}", run_cmd_result); + iff!(run_cmd_result.success(), 0, run_cmd_result.code().unwrap_or(-2)) + } else { + -1 + }; + + information!("Finished, cost: {}ms", start.elapsed().as_millis()); + std::process::exit(exit_code); +} + +// supports format: +// JSON: +// { +// "KEY": "value", +// "KEY2": "value2" +// } +// ----OR---- +// [ +// "KEY": "value", +// "KEY2": "value2" +// ] +// ENV: +// KEY=value +// KEY2=value2 +fn parse_output_to_env(output: &str) -> Vec<(String, String)> { + let mut env = vec![]; + if let Ok(json) = serde_json::from_str::(output) { + match &json { + Value::Array(array) => { + for a in array { + match a { + Value::String(s) => { env.push((s.to_string(), "".to_string())); } + Value::Array(a2) => if a2.len() == 2 { + env.push((a2[0].to_string(), a2[1].to_string())); + } else { + warning!("Invalid array object: {:?}", a2); + } + Value::Object(object) => { + object.iter().for_each(|(k, v)| { + env.push((k.to_string(), v.to_string())); + }); + } + _ => { warning!("Invalid array object: {}", a); } + } + } + } + Value::Object(object) => { + object.iter().for_each(|(k, v)| { + env.push((k.to_string(), v.to_string())); + }); + } + _ => { warning!("Parse to env failed: {}", json); } + } + } else { + let lines = output.split('\n'); + lines.filter(|ln| !ln.trim().is_empty()).for_each(|ln| { + if ln.contains('=') { + let k = ln.chars().take_while(|c| c != &'=').collect::(); + let v = ln.chars().skip_while(|c| c != &'=').skip(1).collect::(); + env.push((k, v)); + } else { + env.push((ln.to_string(), "".to_string())); + } + }); + } + + debugging!("Parsed env: {:?}", env); + env +} diff --git a/src/cmd_initkeychainkey.rs b/src/cmd_initkeychainkey.rs new file mode 100644 index 0000000..c14a1d3 --- /dev/null +++ b/src/cmd_initkeychainkey.rs @@ -0,0 +1,40 @@ +use clap::Args; +use rust_util::XResult; + +#[derive(Debug, Args)] +pub struct CmdKeychainKey { + /// Keychain name, or default + #[arg(long, short = 'c')] + pub keychain_name: Option, + /// Service name, or tiny-encrypt + #[arg(long, short = 's')] + pub server_name: Option, + /// Key type, or default x25519 + #[arg(long, short = 't')] + pub key_type: Option, + /// Key name + #[arg(long, short = 'n')] + pub key_name: String, +} + +#[allow(dead_code)] +const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt"; + +#[allow(dead_code)] +pub enum KeyType { + P256, + P384, + X25519, +} + +// TODO Under developing +// keychain://keychain_name?sn=service_name&kt=kp-p256&kn=key_name&fp=fingerprint +// keychain_name -> default +// service_name -> tiny-encrypt +// kt=kp-p256|kp-p384|kp-x25519 -> keypair P256, P385 or X25519 +// key_name -> key name in keychain +// fingerprint -> hex(SHA256(public_key)[0..4]) +pub fn keychain_key(_cmd_keychain_key: CmdKeychainKey) -> XResult<()> { + println!(); + Ok(()) +} \ No newline at end of file diff --git a/src/cmd_version.rs b/src/cmd_version.rs index 500a3d0..f6a1d1f 100644 --- a/src/cmd_version.rs +++ b/src/cmd_version.rs @@ -10,6 +10,8 @@ pub fn version(_cmd_version: CmdVersion) -> XResult<()> { let mut features: Vec<&str> = vec![]; #[cfg(feature = "smartcard")] features.push("smartcard"); + #[cfg(feature = "macos")] + features.push("macos"); if features.is_empty() { features.push("-"); } println!( "User-Agent: {} [ with features: {} ]\n{}", diff --git a/src/lib.rs b/src/lib.rs index 6d313a1..b3447aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,13 @@ pub use cmd_info::info; pub use cmd_info::info_single; pub use cmd_version::CmdVersion; pub use cmd_version::version; +#[cfg(feature = "macos")] +pub use cmd_initkeychainkey::CmdKeychainKey; +#[cfg(feature = "macos")] +pub use cmd_initkeychainkey::keychain_key; +pub use cmd_execenv::CmdExecEnv; +pub use cmd_execenv::exec_env; + mod consts; mod util; @@ -47,4 +54,7 @@ mod cmd_info; mod cmd_decrypt; mod cmd_encrypt; mod cmd_directdecrypt; +#[cfg(feature = "macos")] +mod cmd_initkeychainkey; +mod cmd_execenv; diff --git a/src/main.rs b/src/main.rs index 7438e7c..57290d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,11 @@ extern crate core; use clap::{Parser, Subcommand}; use rust_util::XResult; -use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdVersion}; +use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdExecEnv, CmdInfo, CmdVersion}; #[cfg(feature = "smartcard")] use tiny_encrypt::CmdDecrypt; +#[cfg(feature = "macos")] +use tiny_encrypt::CmdKeychainKey; #[derive(Debug, Parser)] #[command(name = "tiny-encrypt-rs")] @@ -30,6 +32,13 @@ enum Commands { /// Show file info #[command(arg_required_else_help = true, short_flag = 'I')] Info(CmdInfo), + #[cfg(feature = "macos")] + /// Keychain Key [pending implementation] + #[command(arg_required_else_help = true, short_flag = 'k')] + KeychainKey(CmdKeychainKey), + /// Execute env + #[command(arg_required_else_help = true, short_flag = 'X')] + CmdExecEnv(CmdExecEnv), /// Show version #[command(short_flag = 'v')] Version(CmdVersion), @@ -46,6 +55,9 @@ fn main() -> XResult<()> { 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), + #[cfg(feature = "macos")] + Commands::KeychainKey(cmd_keychain_key) => tiny_encrypt::keychain_key(cmd_keychain_key), + Commands::CmdExecEnv(cmd_exec_env) => tiny_encrypt::exec_env(cmd_exec_env), Commands::Version(cmd_version) => tiny_encrypt::version(cmd_version), Commands::Config(cmd_config) => tiny_encrypt::config(cmd_config), }