feat: v1.11.7, support keychain store
This commit is contained in:
36
Cargo.lock
generated
36
Cargo.lock
generated
@@ -209,7 +209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "08cee7a0952628fde958e149507c2bb321ab4fccfafd225da0b20adc956ef88a"
|
checksum = "08cee7a0952628fde958e149507c2bb321ab4fccfafd225da0b20adc956ef88a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation",
|
"core-foundation 0.9.4",
|
||||||
"devd-rs",
|
"devd-rs",
|
||||||
"libc",
|
"libc",
|
||||||
"libudev",
|
"libudev",
|
||||||
@@ -228,7 +228,7 @@ dependencies = [
|
|||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"core-foundation",
|
"core-foundation 0.9.4",
|
||||||
"devd-rs",
|
"devd-rs",
|
||||||
"libc",
|
"libc",
|
||||||
"libudev",
|
"libudev",
|
||||||
@@ -508,7 +508,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "card-cli"
|
name = "card-cli"
|
||||||
version = "1.11.6"
|
version = "1.11.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm-stream",
|
"aes-gcm-stream",
|
||||||
"authenticator 0.3.1",
|
"authenticator 0.3.1",
|
||||||
@@ -537,6 +537,7 @@ dependencies = [
|
|||||||
"rpassword",
|
"rpassword",
|
||||||
"rust_util",
|
"rust_util",
|
||||||
"secrecy",
|
"secrecy",
|
||||||
|
"security-framework 3.2.0",
|
||||||
"sequoia-openpgp",
|
"sequoia-openpgp",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -678,6 +679,16 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -2159,7 +2170,7 @@ dependencies = [
|
|||||||
"openssl-probe",
|
"openssl-probe",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"schannel",
|
"schannel",
|
||||||
"security-framework",
|
"security-framework 2.11.1",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
@@ -3316,7 +3327,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.0",
|
"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",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
@@ -3792,7 +3816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation",
|
"core-foundation 0.9.4",
|
||||||
"system-configuration-sys",
|
"system-configuration-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "card-cli"
|
name = "card-cli"
|
||||||
version = "1.11.6"
|
version = "1.11.7"
|
||||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ regex = "1.4.6"
|
|||||||
aes-gcm-stream = "0.2"
|
aes-gcm-stream = "0.2"
|
||||||
swift-secure-enclave-tool-rs = "0.1"
|
swift-secure-enclave-tool-rs = "0.1"
|
||||||
u2f-hatter-fork = "0.2"
|
u2f-hatter-fork = "0.2"
|
||||||
|
security-framework = { version = "3.0", features = ["OSX_10_15"] }
|
||||||
#lazy_static = "1.4.0"
|
#lazy_static = "1.4.0"
|
||||||
#ssh-key = "0.4.0"
|
#ssh-key = "0.4.0"
|
||||||
#ctap-hid-fido2 = "2.1.3"
|
#ctap-hid-fido2 = "2.1.3"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::keychain::{KeychainKey, KeychainKeyValue};
|
||||||
use crate::{ecdsautil, hmacutil};
|
use crate::{ecdsautil, hmacutil};
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
@@ -26,12 +27,46 @@ impl Command for CommandImpl {
|
|||||||
.long("with-hmac-encrypt")
|
.long("with-hmac-encrypt")
|
||||||
.help("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"))
|
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||||
let with_hmac_encrypt = sub_arg_matches.is_present("with-hmac-encrypt");
|
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 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");
|
let json_output = sub_arg_matches.is_present("json");
|
||||||
if json_output {
|
if json_output {
|
||||||
@@ -54,17 +89,48 @@ impl Command for CommandImpl {
|
|||||||
(pkcs8_base64, secret_key_pem)
|
(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 {
|
if json_output {
|
||||||
let mut json = BTreeMap::<&'_ str, String>::new();
|
let mut json = BTreeMap::<&'_ str, String>::new();
|
||||||
json.insert("private_key_base64", pkcs8_base64);
|
match keychain_key_uri {
|
||||||
json.insert("private_key_pem", secret_key_pem);
|
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_pem", public_key_pem);
|
||||||
json.insert("public_key_jwk", jwk_ec_key.to_string());
|
json.insert("public_key_jwk", jwk_ec_key.to_string());
|
||||||
|
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||||
} else {
|
} else {
|
||||||
information!("Private key base64:\n{}\n", pkcs8_base64);
|
match keychain_key_uri {
|
||||||
information!("Private key PEM:\n{}\n", secret_key_pem);
|
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 PEM:\n{}", public_key_pem);
|
||||||
information!("Public key JWK:\n{}", jwk_ec_key.to_string());
|
information!("Public key JWK:\n{}", jwk_ec_key.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use rust_util::{util_msg, XResult};
|
|||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
use crate::cmd_signjwt::{build_jwt_parts, merge_header_claims, merge_payload_claims};
|
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 = ".";
|
const SEPARATOR: &str = ".";
|
||||||
|
|
||||||
@@ -41,8 +42,25 @@ impl Command for CommandImpl {
|
|||||||
sub_arg_matches.value_of("private-key"),
|
sub_arg_matches.value_of("private-key"),
|
||||||
"Private key PKCS#8 DER base64 encoded or PEM"
|
"Private key PKCS#8 DER base64 encoded or PEM"
|
||||||
);
|
);
|
||||||
|
|
||||||
let private_key = hmacutil::try_hmac_decrypt_to_string(private_key)?;
|
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 (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
|
||||||
|
|
||||||
let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?;
|
let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?;
|
||||||
|
|||||||
168
src/keychain.rs
Normal file
168
src/keychain.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ mod seutil;
|
|||||||
mod signfile;
|
mod signfile;
|
||||||
mod sshutil;
|
mod sshutil;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod keychain;
|
||||||
|
|
||||||
pub struct DefaultCommandImpl;
|
pub struct DefaultCommandImpl;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user