diff --git a/Cargo.lock b/Cargo.lock index 846e988..0720e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,7 +209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08cee7a0952628fde958e149507c2bb321ab4fccfafd225da0b20adc956ef88a" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "devd-rs", "libc", "libudev", @@ -228,7 +228,7 @@ dependencies = [ "base64 0.21.7", "bitflags 1.3.2", "cfg-if 1.0.0", - "core-foundation", + "core-foundation 0.9.4", "devd-rs", "libc", "libudev", @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.11.6" +version = "1.11.7" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", @@ -537,6 +537,7 @@ dependencies = [ "rpassword", "rust_util", "secrecy", + "security-framework 3.2.0", "sequoia-openpgp", "serde", "serde_json", @@ -678,6 +679,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2159,7 +2170,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3316,7 +3327,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3792,7 +3816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] diff --git a/Cargo.toml b/Cargo.toml index a9a3d91..07f73a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.11.6" +version = "1.11.7" authors = ["Hatter Jiang "] edition = "2018" @@ -56,6 +56,7 @@ regex = "1.4.6" aes-gcm-stream = "0.2" swift-secure-enclave-tool-rs = "0.1" u2f-hatter-fork = "0.2" +security-framework = { version = "3.0", features = ["OSX_10_15"] } #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_generatekeypair.rs b/src/cmd_generatekeypair.rs index 5ec60e3..8fccd2f 100644 --- a/src/cmd_generatekeypair.rs +++ b/src/cmd_generatekeypair.rs @@ -1,3 +1,4 @@ +use crate::keychain::{KeychainKey, KeychainKeyValue}; use crate::{ecdsautil, hmacutil}; use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; @@ -26,12 +27,46 @@ impl Command for CommandImpl { .long("with-hmac-encrypt") .help("With HMAC encrypt"), ) + .arg( + Arg::with_name("keychain-name") + .long("keychain-name") + .takes_value(true) + .help("Key chain name"), + ) + .arg( + Arg::with_name("import-key-value") + .long("import-key-value") + .takes_value(true) + .help("Import key value"), + ) .arg(Arg::with_name("json").long("json").help("JSON output")) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { let with_hmac_encrypt = sub_arg_matches.is_present("with-hmac-encrypt"); let key_type = sub_arg_matches.value_of("type").unwrap().to_lowercase(); + let keychain_name = sub_arg_matches.value_of("keychain-name"); + let import_key_value = sub_arg_matches.value_of("import-key-value"); + + if let Some(keychain_name) = keychain_name { + let keychain_key = KeychainKey::from_key_name_default(keychain_name); + if let Some(keychain_key_value_bytes) = keychain_key.get_password()? { + let keychain_key_value: KeychainKeyValue = + serde_json::from_slice(&keychain_key_value_bytes)?; + util_msg::set_logger_std_out(false); + information!("Keychain key URI: {}", keychain_key.to_key_uri()); + println!( + "{}", + serde_json::to_string_pretty(&keychain_key_value).unwrap() + ); + return simple_error!("Keychain key URI: {} exists", keychain_key.to_key_uri()); + } + + if let Some(import_key_value) = import_key_value { + keychain_key.set_password(import_key_value.as_bytes())?; + return Ok(None); + } + } let json_output = sub_arg_matches.is_present("json"); if json_output { @@ -54,17 +89,48 @@ impl Command for CommandImpl { (pkcs8_base64, secret_key_pem) }; + let keychain_key_uri = if let Some(keychain_name) = keychain_name { + let keychain_key_value = KeychainKeyValue { + keychain_name: keychain_name.to_string(), + pkcs8_base64: pkcs8_base64.clone(), + secret_key_pem: secret_key_pem.clone(), + public_key_pem: public_key_pem.clone(), + public_key_jwk: jwk_ec_key.to_string(), + }; + let keychain_key_value_json = serde_json::to_string(&keychain_key_value)?; + + let keychain_key = KeychainKey::from_key_name_default(keychain_name); + keychain_key.set_password(keychain_key_value_json.as_bytes())?; + Some(keychain_key.to_key_uri()) + } else { + None + }; + if json_output { let mut json = BTreeMap::<&'_ str, String>::new(); - json.insert("private_key_base64", pkcs8_base64); - json.insert("private_key_pem", secret_key_pem); + match keychain_key_uri { + None => { + json.insert("private_key_base64", pkcs8_base64); + json.insert("private_key_pem", secret_key_pem); + } + Some(keychain_key_uri) => { + json.insert("keychain_key_uri", keychain_key_uri); + } + } json.insert("public_key_pem", public_key_pem); json.insert("public_key_jwk", jwk_ec_key.to_string()); println!("{}", serde_json::to_string_pretty(&json).unwrap()); } else { - information!("Private key base64:\n{}\n", pkcs8_base64); - information!("Private key PEM:\n{}\n", secret_key_pem); + match keychain_key_uri { + None => { + information!("Private key base64:\n{}\n", pkcs8_base64); + information!("Private key PEM:\n{}\n", secret_key_pem); + } + Some(keychain_key_uri) => { + information!("Keychain key URI:\n{}\n", keychain_key_uri); + } + } information!("Public key PEM:\n{}", public_key_pem); information!("Public key JWK:\n{}", jwk_ec_key.to_string()); } diff --git a/src/cmd_signjwtsoft.rs b/src/cmd_signjwtsoft.rs index e913332..de3488f 100644 --- a/src/cmd_signjwtsoft.rs +++ b/src/cmd_signjwtsoft.rs @@ -7,7 +7,8 @@ use rust_util::{util_msg, XResult}; use serde_json::{Map, Value}; use crate::cmd_signjwt::{build_jwt_parts, merge_header_claims, merge_payload_claims}; -use crate::{digest, ecdsautil, hmacutil, rsautil, util}; +use crate::keychain::{KeychainKey, KeychainKeyValue}; +use crate::{digest, ecdsautil, hmacutil, keychain, rsautil, util}; const SEPARATOR: &str = "."; @@ -41,8 +42,25 @@ impl Command for CommandImpl { sub_arg_matches.value_of("private-key"), "Private key PKCS#8 DER base64 encoded or PEM" ); + let private_key = hmacutil::try_hmac_decrypt_to_string(private_key)?; + let private_key = if keychain::is_keychain_key_uri(&private_key) { + debugging!("Private key keychain key URI: {}", &private_key); + let keychain_key = KeychainKey::parse_key_uri(&private_key)?; + let keychain_key_value_bytes = opt_value_result!( + keychain_key.get_password()?, + "Keychain key URI: {} not found", + &private_key + ); + let keychain_key_value: KeychainKeyValue = + serde_json::from_slice(&keychain_key_value_bytes)?; + debugging!("Keychain key value {:?}", &keychain_key_value); + keychain_key_value.pkcs8_base64 + } else { + private_key + }; + let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?; let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?; diff --git a/src/keychain.rs b/src/keychain.rs new file mode 100644 index 0000000..a30ba00 --- /dev/null +++ b/src/keychain.rs @@ -0,0 +1,168 @@ +use rust_util::{util_file, XResult}; +use security_framework::os::macos::keychain::{CreateOptions, SecKeychain}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +const KEYCHAIN_KEY_PREFIX: &str = "keychain:"; +const DEFAULT_SERVICE_NAME: &str = "card-cli"; + +pub struct KeychainKey { + pub keychain_name: String, + pub service_name: String, + pub key_name: String, +} +#[derive(Debug, Serialize, Deserialize)] +pub struct KeychainKeyValue { + pub keychain_name: String, + pub pkcs8_base64: String, + pub secret_key_pem: String, + pub public_key_pem: String, + pub public_key_jwk: String, +} + +pub fn is_keychain_key_uri(name: &str) -> bool { + name.starts_with(KEYCHAIN_KEY_PREFIX) +} + +impl KeychainKey { + pub fn from_key_name_default(key_name: &str) -> Self { + Self::from("", DEFAULT_SERVICE_NAME, key_name) + } + + pub fn from(keychain_name: &str, service_name: &str, key_name: &str) -> Self { + debugging!( + "Keychain key: {} - {} - {}", + keychain_name, + service_name, + key_name + ); + Self { + keychain_name: keychain_name.to_string(), + service_name: service_name.to_string(), + key_name: key_name.to_string(), + } + } + + pub fn parse_key_uri(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_key_uri(&self) -> String { + let mut s = String::new(); + s.push_str(KEYCHAIN_KEY_PREFIX); + s.push_str(&self.keychain_name); + s.push(':'); + s.push_str(&self.service_name); + s.push(':'); + s.push_str(&self.key_name); + s + } + + pub fn get_password(&self) -> XResult>> { + let sec_keychain = self.get_keychain()?; + debugging!( + "Try find generic password: {}.{}", + &self.service_name, + &self.key_name + ); + 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_key_uri(), e); + Ok(None) + } + } + } + + pub fn set_password(&self, password: &[u8]) -> XResult<()> { + let sec_keychain = self.get_keychain()?; + if sec_keychain + .find_generic_password(&self.service_name, &self.key_name) + .is_ok() + { + 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() { + let keychain_file_name = format!("{}.keychain", &self.keychain_name); + debugging!("Open or create keychain: {}", &keychain_file_name); + let keychain_exists = check_keychain_exists(&keychain_file_name); + if keychain_exists { + Ok(opt_result!( + SecKeychain::open(&keychain_file_name), + "Open keychain: {}, failed: {}", + &keychain_file_name + )) + } else { + match CreateOptions::new() + .prompt_user(true) + .create(&keychain_file_name) + { + Ok(sec_keychain) => Ok(sec_keychain), + Err(ce) => match SecKeychain::open(&keychain_file_name) { + Ok(sec_keychain) => Ok(sec_keychain), + Err(oe) => simple_error!( + "Create keychain: {}, failed: {}, open also failed: {}", + &self.keychain_name, + ce, + oe + ), + }, + } + } + } else { + Ok(opt_result!( + SecKeychain::default(), + "Get keychain failed: {}" + )) + } + } +} + +fn check_keychain_exists(keychain_file_name: &str) -> bool { + let keychain_path = PathBuf::from(util_file::resolve_file_path("~/Library/Keychains/")); + match keychain_path.read_dir() { + Ok(read_dir) => { + for dir in read_dir { + match dir { + Ok(dir) => { + if let Some(file_name) = dir.file_name().to_str() { + if file_name.starts_with(keychain_file_name) { + debugging!("Found key chain file: {:?}", dir); + return true; + } + } + } + Err(e) => { + debugging!("Read path sub dir: {:?} failed: {}", keychain_path, e); + } + } + } + } + Err(e) => { + debugging!("Read path: {:?} failed: {}", keychain_path, e); + } + } + false +} diff --git a/src/main.rs b/src/main.rs index 57fceb9..44391f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,7 @@ mod seutil; mod signfile; mod sshutil; mod util; +mod keychain; pub struct DefaultCommandImpl;