From 6a07360dc1f9514a0d8b94e728a820cf68277be5 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 16 Nov 2024 21:59:36 +0800 Subject: [PATCH 1/2] feat: pendig simple encrypt/decrypt --- src/cmd_simple_encrypt_decrypt.rs | 78 +++++++++++++++++++++++++++++++ src/lib.rs | 3 ++ src/main.rs | 6 ++- 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/cmd_simple_encrypt_decrypt.rs diff --git a/src/cmd_simple_encrypt_decrypt.rs b/src/cmd_simple_encrypt_decrypt.rs new file mode 100644 index 0000000..0378bba --- /dev/null +++ b/src/cmd_simple_encrypt_decrypt.rs @@ -0,0 +1,78 @@ +use crate::config::TinyEncryptConfig; +use crate::consts::TINY_ENC_CONFIG_FILE; +use clap::Args; +use rust_util::{debugging, failure, iff, information, opt_result, simple_error, success, util_size, warning, XResult}; +use serde::Serialize; +use std::process::exit; + +#[derive(Debug, Args)] +pub struct CmdSimpleEncrypt { + /// Encryption profile (use default when --key-filter is assigned) + #[arg(long, short = 'p')] + pub profile: Option, + + /// Encryption key filter (key_id or type:TYPE(e.g. ecdh, pgp, ecdh-p384, pgp-ed25519), multiple joined by ',', ALL for all) + #[arg(long, short = 'k')] + pub key_filter: Option, +} + +#[derive(Serialize)] +pub struct CmdResult { + pub code: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +impl CmdResult { + pub fn fail(code: i32, message: &str) -> Self { + Self { + code, + message: Some(message.to_string()), + result: None, + } + } + + pub fn success(result: &str) -> Self { + Self { + code: 0, + message: None, + result: Some(result.to_string()), + } + } + + pub fn print_exit(&self) { + let result = serde_json::to_string_pretty(self).unwrap(); + println!("{}", result); + exit(self.code); + } +} + +pub fn simple_encrypt(cmd_encrypt: CmdSimpleEncrypt) -> XResult<()> { + if let Err(inner_result_error) = inner_simple_encrypt(cmd_encrypt) { + CmdResult::fail(-1, &format!("{}", inner_result_error)).print_exit(); + } + Ok(()) +} + +pub fn inner_simple_encrypt(cmd_encrypt: CmdSimpleEncrypt) -> XResult<()> { + let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?; + debugging!("Found tiny encrypt config: {:?}", config); + let envelops = config.find_envelops(&cmd_encrypt.profile, &cmd_encrypt.key_filter)?; + if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); } + debugging!("Found envelops: {:?}", envelops); + let envelop_tkids: Vec<_> = envelops.iter() + .map(|e| format!("{}:{}", e.r#type.get_name(), e.kid)) + .collect(); + debugging!("Matched {} envelop(s): \n- {}", envelops.len(), envelop_tkids.join("\n- ")); + + if envelop_tkids.is_empty() { + return simple_error!("no matched envelops found"); + } + // TODO ... + + + Ok(()) +} + diff --git a/src/lib.rs b/src/lib.rs index 18d13ab..84ca37e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,9 +9,11 @@ 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_simple_encrypt_decrypt::CmdSimpleEncrypt; pub use cmd_encrypt::encrypt; pub use cmd_encrypt::encrypt_single; pub use cmd_encrypt::encrypt_single_file_out; +pub use cmd_simple_encrypt_decrypt::simple_encrypt; #[cfg(feature = "decrypt")] pub use cmd_execenv::CmdExecEnv; #[cfg(feature = "decrypt")] @@ -57,6 +59,7 @@ mod cmd_info; #[cfg(feature = "decrypt")] mod cmd_decrypt; mod cmd_encrypt; +mod cmd_simple_encrypt_decrypt; mod cmd_directdecrypt; #[cfg(feature = "macos")] mod cmd_initkeychain; diff --git a/src/main.rs b/src/main.rs index 4120f28..d5edee1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ extern crate core; use clap::{Parser, Subcommand}; use rust_util::XResult; -use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdVersion}; #[cfg(feature = "decrypt")] use tiny_encrypt::CmdDecrypt; #[cfg(feature = "decrypt")] @@ -12,6 +11,7 @@ use tiny_encrypt::CmdExecEnv; use tiny_encrypt::CmdInitKeychain; #[cfg(feature = "smartcard")] use tiny_encrypt::CmdInitPiv; +use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdSimpleEncrypt, CmdVersion}; #[derive(Debug, Parser)] #[command(name = "tiny-encrypt-rs")] @@ -26,6 +26,9 @@ enum Commands { /// Encrypt file(s) #[command(arg_required_else_help = true, short_flag = 'e')] Encrypt(CmdEncrypt), + /// Simple encrypt file(s) + #[command(arg_required_else_help = true)] + SimpleEncrypt(CmdSimpleEncrypt), #[cfg(feature = "decrypt")] /// Decrypt file(s) #[command(arg_required_else_help = true, short_flag = 'd')] @@ -60,6 +63,7 @@ fn main() -> XResult<()> { let args = Cli::parse(); match args.command { Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt), + Commands::SimpleEncrypt(cmd_simple_encrypt) => tiny_encrypt::simple_encrypt(cmd_simple_encrypt), #[cfg(feature = "decrypt")] Commands::Decrypt(cmd_decrypt) => tiny_encrypt::decrypt(cmd_decrypt), Commands::DirectDecrypt(cmd_direct_decrypt) => tiny_encrypt::direct_decrypt(cmd_direct_decrypt), From 8e6e708c731f0a24b4c859ce6d88269a883f381c Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 17 Nov 2024 10:30:39 +0800 Subject: [PATCH 2/2] feat: v1.8.0, simple-encrypt and simple-decrypt --- Cargo.lock | 10 +- Cargo.toml | 2 +- src/cmd_encrypt.rs | 2 +- src/cmd_simple_encrypt_decrypt.rs | 166 ++++++++++++++++++++++++++++-- src/lib.rs | 2 + src/main.rs | 8 +- src/util.rs | 9 +- 7 files changed, 180 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06c771b..1bd1c95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,9 +885,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libm" @@ -1609,9 +1609,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1870,7 +1870,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "1.7.15" +version = "1.8.0" dependencies = [ "aes-gcm-stream", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 66f19cb..90b09f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "1.7.15" +version = "1.8.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" diff --git a/src/cmd_encrypt.rs b/src/cmd_encrypt.rs index 0030ab1..c2a851a 100644 --- a/src/cmd_encrypt.rs +++ b/src/cmd_encrypt.rs @@ -295,7 +295,7 @@ pub(crate) fn encrypt_file(file_in: &mut impl Read, file_len: u64, file_out: &mu Ok(total_len) } -fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResult> { +pub fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResult> { let mut encrypted_envelops = vec![]; for envelop in envelops { match envelop.r#type { diff --git a/src/cmd_simple_encrypt_decrypt.rs b/src/cmd_simple_encrypt_decrypt.rs index 0378bba..c8d8792 100644 --- a/src/cmd_simple_encrypt_decrypt.rs +++ b/src/cmd_simple_encrypt_decrypt.rs @@ -1,10 +1,19 @@ +use crate::cmd_decrypt::try_decrypt_key; use crate::config::TinyEncryptConfig; use crate::consts::TINY_ENC_CONFIG_FILE; +use crate::spec::TinyEncryptEnvelop; +use crate::{cmd_encrypt, crypto_cryptor, util, util_env}; +use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; +use base64::Engine; use clap::Args; -use rust_util::{debugging, failure, iff, information, opt_result, simple_error, success, util_size, warning, XResult}; +use rust_util::{debugging, opt_result, simple_error, XResult}; use serde::Serialize; use std::process::exit; +// Reference: https://git.hatter.ink/hatter/tiny-encrypt-rs/issues/3 +const SIMPLE_ENCRYPTION_HEADER: &str = "tinyencrypt-dir"; +const SIMPLE_ENCRYPTION_DOT: &str = "."; + #[derive(Debug, Args)] pub struct CmdSimpleEncrypt { /// Encryption profile (use default when --key-filter is assigned) @@ -14,6 +23,76 @@ pub struct CmdSimpleEncrypt { /// Encryption key filter (key_id or type:TYPE(e.g. ecdh, pgp, ecdh-p384, pgp-ed25519), multiple joined by ',', ALL for all) #[arg(long, short = 'k')] pub key_filter: Option, + + /// Encrypt value from stdin + #[arg(long)] + pub value_stdin: bool, + + /// Encrypt value + #[arg(long, short = 'v')] + pub value: Option, + + /// Encrypt value in bse64 + #[arg(long)] + pub value_base64: Option, + + /// Encrypt value in hex + #[arg(long)] + pub value_hex: Option, +} + +#[derive(Debug, Args)] +pub struct CmdSimpleDecrypt { + /// PGP or PIV PIN + #[arg(long, short = 'p')] + pub pin: Option, + + /// Decrypt key ID + #[arg(long, short = 'k')] + pub key_id: Option, + + /// PIV slot + #[arg(long, short = 's')] + pub slot: Option, + + /// Decrypt value from stdin + #[arg(long)] + pub value_stdin: bool, + + /// Decrypt value + #[arg(long, short = 'v')] + pub value: Option, + + /// Decrypt result output type (plain, hex, bse64) + #[arg(long, short = 'o')] + pub output_type: Option, +} + +impl CmdSimpleEncrypt { + pub fn get_value(&self) -> XResult>> { + if self.value_stdin { + return Ok(Some(util::read_stdin()?)); + } + if let Some(value) = &self.value { + return Ok(Some(value.as_bytes().to_vec())); + } + if let Some(value_base64) = &self.value_base64 { + return Ok(Some(opt_result!(STANDARD.decode(value_base64), "Parse value base64 failed: {}"))); + } + if let Some(value_hex) = &self.value_hex { + return Ok(Some(opt_result!(hex::decode(value_hex), "Parse value hex failed: {}"))); + } + Ok(None) + } +} + +impl CmdSimpleDecrypt { + pub fn get_value(&self) -> XResult> { + if self.value_stdin { + return Ok(Some(opt_result!(String::from_utf8(util::read_stdin()?), "Read stdin value failed: {}"))); + } + Ok(self.value.clone()) + } } #[derive(Serialize)] @@ -42,24 +121,31 @@ impl CmdResult { } } - pub fn print_exit(&self) { + pub fn print_exit(&self) -> ! { let result = serde_json::to_string_pretty(self).unwrap(); println!("{}", result); - exit(self.code); + exit(self.code) } } -pub fn simple_encrypt(cmd_encrypt: CmdSimpleEncrypt) -> XResult<()> { - if let Err(inner_result_error) = inner_simple_encrypt(cmd_encrypt) { +pub fn simple_encrypt(cmd_simple_encrypt: CmdSimpleEncrypt) -> XResult<()> { + if let Err(inner_result_error) = inner_simple_encrypt(cmd_simple_encrypt) { CmdResult::fail(-1, &format!("{}", inner_result_error)).print_exit(); } Ok(()) } -pub fn inner_simple_encrypt(cmd_encrypt: CmdSimpleEncrypt) -> XResult<()> { +pub fn simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()> { + if let Err(inner_result_error) = inner_simple_decrypt(cmd_simple_decrypt) { + CmdResult::fail(-1, &format!("{}", inner_result_error)).print_exit(); + } + Ok(()) +} + +pub fn inner_simple_encrypt(cmd_simple_encrypt: CmdSimpleEncrypt) -> XResult<()> { let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?; debugging!("Found tiny encrypt config: {:?}", config); - let envelops = config.find_envelops(&cmd_encrypt.profile, &cmd_encrypt.key_filter)?; + let envelops = config.find_envelops(&cmd_simple_encrypt.profile, &cmd_simple_encrypt.key_filter)?; if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); } debugging!("Found envelops: {:?}", envelops); let envelop_tkids: Vec<_> = envelops.iter() @@ -70,9 +156,71 @@ pub fn inner_simple_encrypt(cmd_encrypt: CmdSimpleEncrypt) -> XResult<()> { if envelop_tkids.is_empty() { return simple_error!("no matched envelops found"); } - // TODO ... + let value = match cmd_simple_encrypt.get_value()? { + None => return simple_error!("--value-stdin/value/value-base64/value-hex must assign one"), + Some(value) => value, + }; - Ok(()) + let cryptor = crypto_cryptor::get_cryptor_by_encryption_algorithm(&None)?; + let envelops = cmd_encrypt::encrypt_envelops(cryptor, &value, &envelops)?; + + let envelops_json = serde_json::to_string(&envelops)?; + let simple_encrypt_result = format!("{}.{}", + SIMPLE_ENCRYPTION_HEADER, + URL_SAFE_NO_PAD.encode(envelops_json.as_bytes()) + ); + + CmdResult::success(&simple_encrypt_result).print_exit(); } +pub fn inner_simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()> { + let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE).ok(); + + let pin = cmd_simple_decrypt.pin.clone().or_else(util_env::get_pin); + let slot = cmd_simple_decrypt.slot.clone(); + + let output_type = cmd_simple_decrypt.output_type.as_deref().unwrap_or("plain"); + match output_type { + "plain" | "hex" | "base64" => (), + _ => return simple_error!("not supported output type: {}", output_type), + }; + + let value = match cmd_simple_decrypt.get_value()? { + None => return simple_error!("--value-stdin/value must assign one"), + Some(value) => value, + }; + let value_parts = value.trim().split(SIMPLE_ENCRYPTION_DOT).collect::>(); + if value_parts.len() != 2 { + return simple_error!("bad value format: {}", value); + } + if value_parts[0] != SIMPLE_ENCRYPTION_HEADER { + return simple_error!("bad value format: {}", value); + } + let envelopes_json = opt_result!(URL_SAFE_NO_PAD.decode(&value_parts[1]), "bad value format: {}"); + let envelops: Vec = match serde_json::from_slice(&envelopes_json) { + Err(_) => return simple_error!("bad value format: {}", value), + Ok(value) => value, + }; + + let filter_envelops = envelops.iter().filter(|e| { + match &cmd_simple_decrypt.key_id { + None => true, + Some(key_id) => &e.kid == key_id, + } + }).collect::>(); + if filter_envelops.is_empty() { + return simple_error!("no envelops found: {:?}", cmd_simple_decrypt.key_id); + } + if filter_envelops.len() > 1 { + return simple_error!("too many envelops: {:?}, len: {}", cmd_simple_decrypt.key_id, filter_envelops.len()); + } + let value = try_decrypt_key(&config, filter_envelops[0], &pin, &slot, false)?; + let value = match output_type { + "plain" => opt_result!(String::from_utf8(value), "bad value encoding: {}"), + "hex" => hex::encode(&value), + "base64" => STANDARD.encode(&value), + _ => return simple_error!("not supported output type: {}", output_type), + }; + CmdResult::success(&value).print_exit(); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 84ca37e..12789f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,12 @@ pub use cmd_directdecrypt::CmdDirectDecrypt; pub use cmd_directdecrypt::direct_decrypt; pub use cmd_encrypt::CmdEncrypt; pub use cmd_simple_encrypt_decrypt::CmdSimpleEncrypt; +pub use cmd_simple_encrypt_decrypt::CmdSimpleDecrypt; pub use cmd_encrypt::encrypt; pub use cmd_encrypt::encrypt_single; pub use cmd_encrypt::encrypt_single_file_out; pub use cmd_simple_encrypt_decrypt::simple_encrypt; +pub use cmd_simple_encrypt_decrypt::simple_decrypt; #[cfg(feature = "decrypt")] pub use cmd_execenv::CmdExecEnv; #[cfg(feature = "decrypt")] diff --git a/src/main.rs b/src/main.rs index d5edee1..31fee19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use tiny_encrypt::CmdExecEnv; use tiny_encrypt::CmdInitKeychain; #[cfg(feature = "smartcard")] use tiny_encrypt::CmdInitPiv; -use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdSimpleEncrypt, CmdVersion}; +use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdSimpleDecrypt, CmdSimpleEncrypt, CmdVersion}; #[derive(Debug, Parser)] #[command(name = "tiny-encrypt-rs")] @@ -26,9 +26,12 @@ enum Commands { /// Encrypt file(s) #[command(arg_required_else_help = true, short_flag = 'e')] Encrypt(CmdEncrypt), - /// Simple encrypt file(s) + /// Simple encrypt message #[command(arg_required_else_help = true)] SimpleEncrypt(CmdSimpleEncrypt), + /// Simple decrypt message + #[command(arg_required_else_help = true)] + SimpleDecrypt(CmdSimpleDecrypt), #[cfg(feature = "decrypt")] /// Decrypt file(s) #[command(arg_required_else_help = true, short_flag = 'd')] @@ -64,6 +67,7 @@ fn main() -> XResult<()> { match args.command { Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt), Commands::SimpleEncrypt(cmd_simple_encrypt) => tiny_encrypt::simple_encrypt(cmd_simple_encrypt), + Commands::SimpleDecrypt(cmd_simple_decrypt) => tiny_encrypt::simple_decrypt(cmd_simple_decrypt), #[cfg(feature = "decrypt")] Commands::Decrypt(cmd_decrypt) => tiny_encrypt::decrypt(cmd_decrypt), Commands::DirectDecrypt(cmd_direct_decrypt) => tiny_encrypt::direct_decrypt(cmd_direct_decrypt), diff --git a/src/util.rs b/src/util.rs index 5048cd5..b048ac3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::{fs, io}; @@ -32,6 +32,13 @@ impl AsRef<[u8]> for SecVec { } } +pub fn read_stdin() -> XResult> { + let mut buffer = vec![]; + let mut stdin = io::stdin(); + opt_result!(stdin.read_to_end(&mut buffer), "Read stdin failed: {}"); + Ok(buffer) +} + pub fn read_pin(pin: &Option) -> XResult { let rpin = match pin { Some(pin) => pin.to_string(),