diff --git a/Cargo.lock b/Cargo.lock index f710fb5..64ad760 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1700,7 +1700,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "1.4.5" +version = "1.5.0" dependencies = [ "aes-gcm-stream", "base64", diff --git a/Cargo.toml b/Cargo.toml index 121322b..fa8457d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "1.4.5" +version = "1.5.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index b5b79c5..0ddde34 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -37,6 +37,7 @@ use crate::util_digest::DigestWrite; use crate::util_keychainkey; #[cfg(feature = "macos")] use crate::util_keychainstatic; +use crate::util_keychainstatic::KeychainKey; use crate::util_progress::Progress; use crate::wrap_key::WrapKey; @@ -542,10 +543,16 @@ fn try_decrypt_se_key_ecdh(config: &Option, if config_envelop_args.is_empty() { return simple_error!("Not enough arguments for: {}", &envelop.kid); } - let private_key_base64 = &config_envelop_args[0]; + + let private_key_base64 = if let Ok(keychain_key) = KeychainKey::parse(&config_envelop_args[0]) { + let key = opt_value_result!(keychain_key.get_password()?, "Key: {} not found", &keychain_key.to_str()); + opt_result!(String::from_utf8(key), "Parse key failed: {}") + } else { + config_envelop_args[0].clone() + }; let shared_secret = opt_result!(util_keychainkey::decrypt_data( - private_key_base64, + &private_key_base64, &e_pub_key_bytes ), "Decrypt via secure enclave failed: {}"); let key = util::simple_kdf(shared_secret.as_slice()); @@ -595,13 +602,18 @@ fn try_decrypt_key_ecdh_static_x25519(config: &Option, envelo 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 { + if config_envelop_args.len() != 1 && 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 keychain_key = if config_envelop_args.len() == 1 { + KeychainKey::parse(&config_envelop_args[0])? + } else { + KeychainKey::from(&config_envelop_args[0], &config_envelop_args[1], &config_envelop_args[2]) + }; + let shared_secret = opt_result!( - util_keychainstatic::decrypt_data(service_name, key_name, &e_pub_key_bytes), "Decrypt static x25519 failed: {}"); + util_keychainstatic::decrypt_data(&keychain_key, &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 }; diff --git a/src/cmd_initkeychain.rs b/src/cmd_initkeychain.rs index 37e63f3..581ffb1 100644 --- a/src/cmd_initkeychain.rs +++ b/src/cmd_initkeychain.rs @@ -1,13 +1,12 @@ use clap::Args; -use rust_util::{debugging, information, opt_result, opt_value_result, simple_error, success, warning, XResult}; -use security_framework::os::macos::keychain::SecKeychain; +use rust_util::{debugging, information, opt_result, simple_error, success, warning, XResult}; use crate::config::TinyEncryptConfigEnvelop; use crate::spec::TinyEncryptEnvelopType; #[cfg(feature = "secure-enclave")] use crate::util_keychainkey; use crate::util_keychainstatic; -use crate::util_keychainstatic::X25519StaticSecret; +use crate::util_keychainstatic::{KeychainKey, X25519StaticSecret}; #[derive(Debug, Args)] pub struct CmdInitKeychain { @@ -22,7 +21,7 @@ pub struct CmdInitKeychain { pub server_name: Option, /// Key name #[arg(long, short = 'n')] - pub key_name: Option, + pub key_name: String, } const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt"; @@ -43,18 +42,23 @@ pub fn keychain_key_se(cmd_init_keychain: CmdInitKeychain) -> XResult<()> { if !util_keychainkey::is_support_se() { return simple_error!("Secure enclave is not supported."); } + + let service_name = cmd_init_keychain.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME); + let key_name = &cmd_init_keychain.key_name; + let keychain_key = KeychainKey::from("", service_name, key_name); + let (public_key_hex, private_key_base64) = util_keychainkey::generate_se_p256_keypair()?; let public_key_compressed_hex = public_key_hex.chars() .skip(2).take(public_key_hex.len() / 2 - 1).collect::(); + keychain_key.set_password(private_key_base64.as_bytes())?; + let config_envelop = TinyEncryptConfigEnvelop { r#type: TinyEncryptEnvelopType::KeyP256, - sid: cmd_init_keychain.key_name.clone(), + sid: Some(cmd_init_keychain.key_name.clone()), kid: format!("keychain:02{}", &public_key_compressed_hex), desc: Some("Keychain Secure Enclave".to_string()), - args: Some(vec![ - private_key_base64 - ]), + args: Some(vec![keychain_key.to_str()]), public_part: public_key_hex, }; @@ -65,20 +69,20 @@ pub fn keychain_key_se(cmd_init_keychain: CmdInitKeychain) -> XResult<()> { pub fn keychain_key_static(cmd_init_keychain: CmdInitKeychain) -> XResult<()> { let service_name = cmd_init_keychain.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME); - let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}"); - let key_name = opt_value_result!(&cmd_init_keychain.key_name, "Key name is required."); + let key_name = &cmd_init_keychain.key_name; + let keychain_key = KeychainKey::from("", service_name, key_name); - let public_key = match sec_keychain.find_generic_password(service_name, key_name) { - Ok((static_x25519, _)) => { + let public_key = match keychain_key.get_password()? { + Some(static_x25519) => { warning!("Key already exists: {}.{}", service_name, key_name); let x25519_static_secret = X25519StaticSecret::parse_bytes(static_x25519.as_ref())?; x25519_static_secret.to_public_key()? } - Err(_) => { - let (keychain_key, public_key) = util_keychainstatic::generate_static_x25519_secret(); - debugging!("Keychain key : {}", keychain_key); + None => { + let (keychain_key_bytes, public_key) = util_keychainstatic::generate_static_x25519_secret(); + debugging!("Keychain key : {}", keychain_key_bytes); opt_result!( - sec_keychain.set_generic_password(service_name, key_name, keychain_key.as_bytes()), + keychain_key.set_password(keychain_key_bytes.as_bytes()), "Write static x25519 failed: {}" ); public_key @@ -94,11 +98,7 @@ pub fn keychain_key_static(cmd_init_keychain: CmdInitKeychain) -> XResult<()> { sid: Some(key_name.clone()), kid: format!("keychain:{}", &public_key_hex), desc: Some("Keychain static".to_string()), - args: Some(vec![ - "".to_string(), - service_name.to_string(), - key_name.clone(), - ]), + args: Some(vec![keychain_key.to_str()]), public_part: public_key_hex, }; diff --git a/src/util_keychainstatic.rs b/src/util_keychainstatic.rs index f8b5d63..e1ae334 100644 --- a/src/util_keychainstatic.rs +++ b/src/util_keychainstatic.rs @@ -1,9 +1,17 @@ -use rust_util::{opt_result, simple_error, XResult}; +use rust_util::{debugging, opt_result, opt_value_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:"; +const KEYCHAIN_KEY_PREFIX: &str = "keychain:"; + +pub struct KeychainKey { + pub keychain_name: String, + pub service_name: String, + pub key_name: String, +} + pub struct X25519StaticSecret { pub secret: Vec, @@ -60,12 +68,79 @@ impl X25519StaticSecret { } } -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); +impl KeychainKey { + pub fn from(keychain_name: &str, service_name: &str, key_name: &str) -> Self { + Self { + keychain_name: keychain_name.to_string(), + service_name: service_name.to_string(), + key_name: key_name.to_string(), + } + } - let x25519_static_secret = X25519StaticSecret::parse_bytes(static_x25519.as_ref())?; + pub fn parse(keychain_key: &str) -> XResult { + if !keychain_key.starts_with(KEYCHAIN_KEY_PREFIX) { + return simple_error!("Not a valid keychain key: {}", keychain_key); + } + //keychain:keychain_name:service_name:key_name + let keychain_key_parts = keychain_key.split(':').collect::>(); + if keychain_key_parts.len() != 4 { + return simple_error!("Not a valid keychain key: {}", keychain_key); + } + Ok(Self { + keychain_name: keychain_key_parts[1].to_string(), + service_name: keychain_key_parts[2].to_string(), + key_name: keychain_key_parts[3].to_string(), + }) + } + + pub fn to_str(&self) -> String { + let mut s = String::new(); + s.push_str(KEYCHAIN_KEY_PREFIX); + s.push_str(&self.keychain_name); + s.push_str(":"); + s.push_str(&self.service_name); + s.push_str(":"); + s.push_str(&self.key_name); + s + } + + pub fn get_password(&self) -> XResult>> { + let sec_keychain = self.get_keychain()?; + match sec_keychain.find_generic_password(&self.service_name, &self.key_name) { + Ok((item_password, _keychain_item)) => { + Ok(Some(item_password.as_ref().to_vec())) + } + Err(e) => { + debugging!("Get password: {} failed: {}", &self.to_str(), e); + Ok(None) + } + } + } + + pub fn set_password(&self, password: &[u8]) -> XResult<()> { + let sec_keychain = self.get_keychain()?; + if let Ok(_) = sec_keychain.find_generic_password(&self.service_name, &self.key_name) { + return simple_error!("Password {}.{} exists", &self.service_name, &self.key_name); + } + opt_result!( + sec_keychain.set_generic_password(&self.service_name, &self.key_name, password), + "Set password {}.{} error: {}", &self.service_name, &self.key_name + ); + Ok(()) + } + + fn get_keychain(&self) -> XResult { + if !self.keychain_name.is_empty() { + return simple_error!("Keychain name must be empty."); + } + Ok(opt_result!(SecKeychain::default(), "Get keychain failed: {}")) + } +} + +pub fn decrypt_data(keychain_key: &KeychainKey, ephemeral_public_key_bytes: &[u8]) -> XResult> { + let static_x25519 = opt_value_result!(keychain_key.get_password()?, "Static X25519 not found: {}", &keychain_key.to_str()); + + let x25519_static_secret = X25519StaticSecret::parse_bytes(&static_x25519)?; 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: {}");