feat: v1.0.0, suport exec-env

This commit is contained in:
2023-12-02 15:26:15 +08:00
parent 0bf4ef2e6b
commit 76b8f93ddd
9 changed files with 270 additions and 21 deletions

37
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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<PathBuf
Ok(temp_file)
}
fn decrypt_limited_content_to_vec(mut file_in: &mut File,
meta: &TinyEncryptMeta, cryptor: Cryptor, key_nonce: &KeyNonce) -> XResult<Option<String>> {
pub fn decrypt_limited_content_to_vec(mut file_in: &mut File,
meta: &TinyEncryptMeta, cryptor: Cryptor, key_nonce: &KeyNonce) -> XResult<Option<String>> {
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<TinyEncryptConfig>,
envelop: &TinyEncryptEnvelop,
pin: &Option<String>,
slot: &Option<String>) -> XResult<Vec<u8>> {
pub fn try_decrypt_key(config: &Option<TinyEncryptConfig>,
envelop: &TinyEncryptEnvelop,
pin: &Option<String>,
slot: &Option<String>) -> XResult<Vec<u8>> {
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<String>) -> XR
Ok(key)
}
fn select_envelop<'a>(meta: &'a TinyEncryptMeta, key_id: &Option<String>, config: &Option<TinyEncryptConfig>) -> XResult<&'a TinyEncryptEnvelop> {
pub fn select_envelop<'a>(meta: &'a TinyEncryptMeta, key_id: &Option<String>, config: &Option<TinyEncryptConfig>) -> XResult<&'a TinyEncryptEnvelop> {
let envelops = match &meta.envelops {
None => return simple_error!("No envelops found"),
Some(envelops) => if envelops.is_empty() {

154
src/cmd_execenv.rs Normal file
View File

@@ -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<String>,
/// KeyID
#[arg(long, short = 'k')]
pub key_id: Option<String>,
/// Slot
#[arg(long, short = 's')]
pub slot: Option<String>,
// Tiny encrypt file name
pub file_name: String,
// Arguments
pub arguments: Vec<String>,
}
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::<Value>(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::<String>();
let v = ln.chars().skip_while(|c| c != &'=').skip(1).collect::<String>();
env.push((k, v));
} else {
env.push((ln.to_string(), "".to_string()));
}
});
}
debugging!("Parsed env: {:?}", env);
env
}

View File

@@ -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<String>,
/// Service name, or tiny-encrypt
#[arg(long, short = 's')]
pub server_name: Option<String>,
/// Key type, or default x25519
#[arg(long, short = 't')]
pub key_type: Option<String>,
/// 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(())
}

View File

@@ -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{}",

View File

@@ -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;

View File

@@ -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),
}