From fd7e8d35a68a862cb38df7c9cc82eb47f5004d17 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Fri, 8 Dec 2023 21:36:03 +0800 Subject: [PATCH] feat: v1.1.0, add static x25519 support --- Cargo.lock | 2 +- Cargo.toml | 8 ++-- src/cmd_decrypt.rs | 49 ++++++++++++++++++++++-- src/cmd_encrypt.rs | 2 +- src/cmd_execenv.rs | 1 + src/cmd_initkeychainkey.rs | 60 +++++++++++++++++++---------- src/config.rs | 3 ++ src/lib.rs | 17 ++++++--- src/main.rs | 12 ++++-- src/spec.rs | 17 +++++++++ src/util_envelop.rs | 2 +- src/util_keychainpasskey.rs | 75 +++++++++++++++++++++++++++++++++++++ 12 files changed, 209 insertions(+), 39 deletions(-) create mode 100644 src/util_keychainpasskey.rs diff --git a/Cargo.lock b/Cargo.lock index 1a27b07..31c31aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1691,7 +1691,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "1.0.2" +version = "1.1.0" dependencies = [ "aes-gcm-stream", "base64", diff --git a/Cargo.toml b/Cargo.toml index c466f26..09f8a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "1.0.2" +version = "1.1.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" @@ -9,8 +9,8 @@ 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", "macos"] -smartcard = ["openpgp-card", "openpgp-card-pcsc", "yubikey"] +default = ["decrypt", "macos"] +decrypt = ["openpgp-card", "openpgp-card-pcsc", "yubikey"] macos = ["security-framework"] [dependencies] @@ -38,7 +38,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simpledateformat = "0.1" tabled = "0.14" -x25519-dalek = "2.0" +x25519-dalek = { version = "2.0", features = ["static_secrets", "getrandom"] } x509-parser = "0.15" yubikey = { version = "0.8", features = ["untested"], optional = true } zeroize = "1.7" diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index aef9e29..366f28a 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -9,7 +9,7 @@ use std::time::{Instant, SystemTime}; use clap::Args; use flate2::Compression; use openpgp_card::crypto_data::Cryptogram; -use rust_util::{debugging, failure, iff, information, opt_result, println_ex, simple_error, success, util_cmd, util_msg, util_size, util_time, warning, XResult}; +use rust_util::{debugging, failure, iff, information, opt_result, opt_value_result, println_ex, simple_error, success, util_cmd, util_msg, util_size, util_time, warning, XResult}; use rust_util::util_time::UnixEpochTime; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; @@ -18,6 +18,8 @@ use yubikey::YubiKey; use zeroize::Zeroize; use crate::{cmd_encrypt, consts, crypto_simple, util, util_enc_file, util_env, util_envelop, util_file, util_pgp, util_piv}; +#[cfg(feature = "macos")] +use crate::util_keychainpasskey; use crate::compress::GzStreamDecoder; use crate::config::TinyEncryptConfig; use crate::consts::{ @@ -61,9 +63,12 @@ pub struct CmdDecrypt { /// Digest file #[arg(long, short = 'D')] pub digest_file: bool, - // Edit file + /// Edit file #[arg(long, short = 'E')] pub edit_file: bool, + // Readonly + #[arg(long)] + pub readonly: bool, /// Digest algorithm (sha1, sha256[default], sha384, sha512 ...) #[arg(long, short = 'A')] pub digest_algorithm: Option, @@ -196,6 +201,10 @@ pub fn decrypt_single(config: &Option, let do_edit_file = || -> XResult<()> { let temp_file_content_bytes = run_file_editor_and_wait_content(&editor, &temp_file, secure_editor, &temp_encryption_key_nonce)?; + if cmd_decrypt.readonly { + information!("Readonly, do not check temp file is changed."); + return Ok(()); + } let temp_file_content_bytes = if secure_editor { let mut decryptor = temp_cryptor.decryptor(&temp_key_nonce)?; decryptor.decrypt(&temp_file_content_bytes)? @@ -421,6 +430,8 @@ pub fn try_decrypt_key(config: &Option, match envelop.r#type { TinyEncryptEnvelopType::Pgp => try_decrypt_key_pgp(envelop, pin), TinyEncryptEnvelopType::PgpX25519 => try_decrypt_key_ecdh_pgp_x25519(envelop, pin), + #[cfg(feature = "macos")] + TinyEncryptEnvelopType::StaticX25519 => try_decrypt_key_ecdh_static_x25519(config, envelop), TinyEncryptEnvelopType::Ecdh | TinyEncryptEnvelopType::EcdhP384 => try_decrypt_key_ecdh(config, envelop, pin, slot), unknown_type => simple_error!("Unknown or unsupported type: {}", unknown_type.get_name()), } @@ -491,6 +502,36 @@ fn try_decrypt_key_ecdh_pgp_x25519(envelop: &TinyEncryptEnvelop, pin: &Option, envelop: &TinyEncryptEnvelop) -> XResult> { + let wrap_key = WrapKey::parse(&envelop.encrypted_key)?; + let cryptor = match wrap_key.header.enc.as_str() { + ENC_AES256_GCM_X25519 => Cryptor::Aes256Gcm, + ENC_CHACHA20_POLY1305_X25519 => Cryptor::ChaCha20Poly1305, + _ => return simple_error!("Unsupported header enc: {}", &wrap_key.header.enc), + }; + let e_pub_key_bytes = wrap_key.header.get_e_pub_key_bytes()?; + let config = opt_value_result!(config, "Tiny encrypt config is not found"); + let config_envelop = opt_value_result!( + config.find_by_kid(&envelop.kid), "Cannot find config for: {}", &envelop.kid); + let config_envelop_args = opt_value_result!(&config_envelop.args, "No arguments found for: {}", &envelop.kid); + if config_envelop_args.len() < 3 { + return simple_error!("Not enough arguments for: {}", &envelop.kid); + } + let service_name = &config_envelop_args[1]; + let key_name = &config_envelop_args[2]; + let shared_secret = opt_result!( + util_keychainpasskey::decrypt_data(service_name, key_name, &e_pub_key_bytes), "Decrypt static x25519 failed: {}"); + + let key = util::simple_kdf(shared_secret.as_slice()); + let key_nonce = KeyNonce { k: &key, n: &wrap_key.nonce }; + let decrypted_key = crypto_simple::decrypt( + cryptor, &key_nonce, &wrap_key.encrypted_data)?; + util::zeroize(key); + util::zeroize(shared_secret); + Ok(decrypted_key) +} + fn try_decrypt_key_pgp(envelop: &TinyEncryptEnvelop, pin: &Option) -> XResult> { let mut pgp = util_pgp::get_openpgp()?; let mut trans = opt_result!(pgp.transaction(), "Connect OpenPGP card failed: {}"); @@ -523,7 +564,9 @@ pub fn select_envelop<'a>(meta: &'a TinyEncryptMeta, key_id: &Option, co if envelops.len() == 1 { let selected_envelop = &envelops[0]; success!("Auto selected envelop: #{} {}", 1, util_envelop::format_envelop(selected_envelop, config)); - util::read_line("Press enter to continue: "); + if !selected_envelop.r#type.auto_select() { + util::read_line("Press enter to continue: "); + } return Ok(selected_envelop); } diff --git a/src/cmd_encrypt.rs b/src/cmd_encrypt.rs index 216aece..ac1d5ce 100644 --- a/src/cmd_encrypt.rs +++ b/src/cmd_encrypt.rs @@ -268,7 +268,7 @@ fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfig TinyEncryptEnvelopType::Pgp => { encrypted_envelops.push(encrypt_envelop_pgp(key, envelop)?); } - TinyEncryptEnvelopType::PgpX25519 => { + TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => { encrypted_envelops.push(encrypt_envelop_ecdh_x25519(cryptor, key, envelop)?); } TinyEncryptEnvelopType::Ecdh => { diff --git a/src/cmd_execenv.rs b/src/cmd_execenv.rs index 9a7f9aa..c3683f9 100644 --- a/src/cmd_execenv.rs +++ b/src/cmd_execenv.rs @@ -40,6 +40,7 @@ impl Drop for CmdExecEnv { } pub fn exec_env(cmd_exec_env: CmdExecEnv) -> XResult<()> { + util_msg::set_logger_std_out(false); debugging!("Cmd exec env: {:?}", cmd_exec_env); let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE).ok(); if cmd_exec_env.arguments.is_empty() { diff --git a/src/cmd_initkeychainkey.rs b/src/cmd_initkeychainkey.rs index c14a1d3..e71cc66 100644 --- a/src/cmd_initkeychainkey.rs +++ b/src/cmd_initkeychainkey.rs @@ -1,11 +1,16 @@ use clap::Args; -use rust_util::XResult; +use rust_util::{debugging, information, opt_result, simple_error, success, XResult}; +use security_framework::os::macos::keychain::SecKeychain; + +use crate::config::TinyEncryptConfigEnvelop; +use crate::spec::TinyEncryptEnvelopType; +use crate::util_keychainpasskey; #[derive(Debug, Args)] pub struct CmdKeychainKey { - /// Keychain name, or default - #[arg(long, short = 'c')] - pub keychain_name: Option, + // /// 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, @@ -20,21 +25,38 @@ pub struct CmdKeychainKey { #[allow(dead_code)] const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt"; -#[allow(dead_code)] -pub enum KeyType { - P256, - P384, - X25519, -} +pub fn keychain_key(cmd_keychain_key: CmdKeychainKey) -> XResult<()> { + let service_name = cmd_keychain_key.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME); + let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}"); + if sec_keychain.find_generic_password(service_name, &cmd_keychain_key.key_name).is_ok() { + return simple_error!("Static x25519 exists: {}.{}", service_name, &cmd_keychain_key.key_name); + } + + let (keychain_key, public_key) = util_keychainpasskey::generate_pass_x25519_static_secret(); + opt_result!( + sec_keychain.set_generic_password(service_name, &cmd_keychain_key.key_name, keychain_key.as_bytes()), + "Write static x25519 failed: {}" + ); + + let public_key_hex = hex::encode(public_key.as_bytes()); + debugging!("Keychain key : {}", keychain_key); + success!("Keychain name: {}", &cmd_keychain_key.key_name); + success!("Public key : {}", &public_key_hex); + + let config_envelop = TinyEncryptConfigEnvelop { + r#type: TinyEncryptEnvelopType::StaticX25519, + sid: Some(cmd_keychain_key.key_name.clone()), + kid: format!("keychain:{}", &public_key_hex), + desc: Some("Keychain static".to_string()), + args: Some(vec![ + "".to_string(), + service_name.to_string(), + cmd_keychain_key.key_name.clone(), + ]), + public_part: public_key_hex, + }; + + information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap()); -// 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/config.rs b/src/config.rs index 8727048..1b19529 100644 --- a/src/config.rs +++ b/src/config.rs @@ -42,9 +42,12 @@ pub struct TinyEncryptConfig { #[serde(rename_all = "camelCase")] pub struct TinyEncryptConfigEnvelop { pub r#type: TinyEncryptEnvelopType, + #[serde(skip_serializing_if = "Option::is_none")] pub sid: Option, pub kid: String, + #[serde(skip_serializing_if = "Option::is_none")] pub desc: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub args: Option>, pub public_part: String, } diff --git a/src/lib.rs b/src/lib.rs index b3447aa..77a1832 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ pub use cmd_config::CmdConfig; pub use cmd_config::config; -#[cfg(feature = "smartcard")] +#[cfg(feature = "decrypt")] pub use cmd_decrypt::CmdDecrypt; -#[cfg(feature = "smartcard")] +#[cfg(feature = "decrypt")] pub use cmd_decrypt::decrypt; -#[cfg(feature = "smartcard")] +#[cfg(feature = "decrypt")] pub use cmd_decrypt::decrypt_single; pub use cmd_directdecrypt::CmdDirectDecrypt; pub use cmd_directdecrypt::direct_decrypt; @@ -21,7 +21,9 @@ pub use cmd_version::version; pub use cmd_initkeychainkey::CmdKeychainKey; #[cfg(feature = "macos")] pub use cmd_initkeychainkey::keychain_key; +#[cfg(feature = "decrypt")] pub use cmd_execenv::CmdExecEnv; +#[cfg(feature = "decrypt")] pub use cmd_execenv::exec_env; @@ -30,9 +32,9 @@ mod util; mod util_env; mod util_digest; mod util_progress; -#[cfg(feature = "smartcard")] +#[cfg(feature = "decrypt")] mod util_piv; -#[cfg(feature = "smartcard")] +#[cfg(feature = "decrypt")] mod util_pgp; mod util_p256; mod util_p384; @@ -50,11 +52,14 @@ mod util_enc_file; mod cmd_version; mod cmd_config; mod cmd_info; -#[cfg(feature = "smartcard")] +#[cfg(feature = "decrypt")] mod cmd_decrypt; mod cmd_encrypt; mod cmd_directdecrypt; #[cfg(feature = "macos")] mod cmd_initkeychainkey; +#[cfg(feature = "macos")] +mod util_keychainpasskey; +#[cfg(feature = "decrypt")] mod cmd_execenv; diff --git a/src/main.rs b/src/main.rs index 01f441c..68d982b 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, CmdExecEnv, CmdInfo, CmdVersion}; -#[cfg(feature = "smartcard")] +use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdVersion}; +#[cfg(feature = "decrypt")] use tiny_encrypt::CmdDecrypt; +#[cfg(feature = "decrypt")] +use tiny_encrypt::CmdExecEnv; #[cfg(feature = "macos")] use tiny_encrypt::CmdKeychainKey; @@ -22,7 +24,7 @@ enum Commands { /// Encrypt file(s) #[command(arg_required_else_help = true, short_flag = 'e')] Encrypt(CmdEncrypt), - #[cfg(feature = "smartcard")] + #[cfg(feature = "decrypt")] /// Decrypt file(s) #[command(arg_required_else_help = true, short_flag = 'd')] Decrypt(CmdDecrypt), @@ -36,6 +38,7 @@ enum Commands { /// Keychain Key [pending implementation] #[command(arg_required_else_help = true, short_flag = 'k')] KeychainKey(CmdKeychainKey), + #[cfg(feature = "decrypt")] /// Execute env #[command(arg_required_else_help = true, short_flag = 'X')] ExecEnv(CmdExecEnv), @@ -51,12 +54,13 @@ fn main() -> XResult<()> { let args = Cli::parse(); match args.command { Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt), - #[cfg(feature = "smartcard")] + #[cfg(feature = "decrypt")] 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), + #[cfg(feature = "decrypt")] Commands::ExecEnv(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), diff --git a/src/spec.rs b/src/spec.rs index 288c30d..697fdb2 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -71,6 +71,9 @@ pub enum TinyEncryptEnvelopType { // OpenPGP X25519 #[serde(rename = "pgp-x25519")] PgpX25519, + // Static X25519 (less secure) + #[serde(rename = "static-x25519")] + StaticX25519, // Age, tiny-encrypt-rs is not supported #[serde(rename = "age")] Age, @@ -89,16 +92,30 @@ impl TinyEncryptEnvelopType { pub fn get_upper_name(&self) -> String { self.get_name().to_uppercase() } + pub fn get_name(&self) -> &'static str { match self { TinyEncryptEnvelopType::Pgp => "pgp", TinyEncryptEnvelopType::PgpX25519 => "pgp-x25519", + TinyEncryptEnvelopType::StaticX25519 => "static-x25519", TinyEncryptEnvelopType::Age => "age", TinyEncryptEnvelopType::Ecdh => "ecdh", TinyEncryptEnvelopType::EcdhP384 => "ecdh-p384", TinyEncryptEnvelopType::Kms => "kms", } } + + pub fn auto_select(&self) -> bool { + match self { + TinyEncryptEnvelopType::Pgp => false, + TinyEncryptEnvelopType::PgpX25519 => false, + TinyEncryptEnvelopType::StaticX25519 => true, + TinyEncryptEnvelopType::Age => false, + TinyEncryptEnvelopType::Ecdh => false, + TinyEncryptEnvelopType::EcdhP384 => false, + TinyEncryptEnvelopType::Kms => true, + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/src/util_envelop.rs b/src/util_envelop.rs index 670a455..cd2088a 100644 --- a/src/util_envelop.rs +++ b/src/util_envelop.rs @@ -26,7 +26,7 @@ fn get_envelop_desc(envelop: &TinyEncryptEnvelop, config_envelop: &Option<&TinyE } pub fn with_width_type(s: &str) -> String { - with_width(s, 10) + with_width(s, 13) } pub fn with_width(s: &str, width: usize) -> String { diff --git a/src/util_keychainpasskey.rs b/src/util_keychainpasskey.rs new file mode 100644 index 0000000..cf4696f --- /dev/null +++ b/src/util_keychainpasskey.rs @@ -0,0 +1,75 @@ +use rust_util::{opt_result, simple_error, XResult}; +use security_framework::os::macos::keychain::SecKeychain; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroize; + +const X2559_PLAIN_PREFIX: &str = "x25519-plain:"; + +pub struct X25519StaticSecret { + pub secret: Vec, +} + +impl Zeroize for X25519StaticSecret { + fn zeroize(&mut self) { + self.secret.zeroize(); + } +} + +impl X25519StaticSecret { + pub fn parse(key: &str) -> XResult { + if !key.starts_with(X2559_PLAIN_PREFIX) { + return simple_error!("Not X25519 plain key"); + } + let extract_key_hex = &key[X2559_PLAIN_PREFIX.len()..]; + let extract_key = opt_result!(hex::decode(extract_key_hex), "Decode X25519 plain key failed: {}"); + Ok(Self { + secret: extract_key, + }) + } + + pub fn to_str(&self) -> String { + let mut v = String::new(); + v.push_str(X2559_PLAIN_PREFIX); + v.push_str(&hex::encode(&self.secret)); + v + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { + secret: bytes.to_vec(), + } + } + + pub fn to_static_secret(&self) -> XResult { + let secret_slice = self.secret.as_slice(); + let mut inner_secret: [u8; 32] = opt_result!(secret_slice.try_into(), "X25519 secret key error: {}"); + let static_secret = StaticSecret::from(inner_secret); + inner_secret.zeroize(); + Ok(static_secret) + } +} + +pub fn decrypt_data(service_name: &str, key_name: &str, ephemeral_public_key_bytes: &[u8]) -> XResult> { + let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}"); + let (static_x25519, _) = opt_result!(sec_keychain.find_generic_password(service_name, key_name), + "Cannot find static x25519 {}.{}: {}", service_name, key_name); + let static_x25519_bytes = static_x25519.as_ref(); + let static_x25519_str = opt_result!(String::from_utf8(static_x25519_bytes.to_vec()), "Parse static x25519 failed: {}"); + + let x25519_static_secret = X25519StaticSecret::parse(&static_x25519_str)?; + let static_secret = x25519_static_secret.to_static_secret()?; + let inner_ephemeral_public_key: [u8; 32] = opt_result!( + ephemeral_public_key_bytes.try_into(), "X25519 public key error: {}"); + let ephemeral_public_key = PublicKey::from(inner_ephemeral_public_key); + let shared_secret = static_secret.diffie_hellman(&ephemeral_public_key); + + Ok(shared_secret.as_bytes().to_vec()) +} + +pub fn generate_pass_x25519_static_secret() -> (String, PublicKey) { + let static_secret = StaticSecret::random(); + let public_key: PublicKey = (&static_secret).into(); + let static_secret_bytes = static_secret.as_bytes(); + let x25519_static_secret = X25519StaticSecret::from_bytes(static_secret_bytes); + (x25519_static_secret.to_str(), public_key) +} \ No newline at end of file