feat: v1.11.7, support keychain store

This commit is contained in:
2025-03-26 22:51:52 +08:00
parent af20f4c4a0
commit 755d61fa86
6 changed files with 290 additions and 12 deletions

View File

@@ -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());
}

View File

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

168
src/keychain.rs Normal file
View File

@@ -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<Self> {
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::<Vec<_>>();
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<Option<Vec<u8>>> {
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<SecKeychain> {
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
}

View File

@@ -68,6 +68,7 @@ mod seutil;
mod signfile;
mod sshutil;
mod util;
mod keychain;
pub struct DefaultCommandImpl;