use std::collections::HashSet;
use anyhow::Result;
use sshenv_shims::{default_bindings_path, load_bindings, resolve_shim_dir};
use sshenv_vault::Vault;
use crate::commands::Context as CmdContext;
use crate::identity::{
discover_private_key_paths, load_identities, load_identities_for_vault,
public_fingerprint_for_private_key,
};
pub fn run(ctx: &CmdContext) -> Result<()> {
let mut ok = true;
println!("sshenv doctor");
println!("=============");
println!();
println!("Vault path: {}", ctx.vault_path.display());
let mut vault_recipient_fps: Option<HashSet<String>> = None;
if ctx.vault_path.exists() {
match Vault::load_ciphertext(&ctx.vault_path) {
Ok(ct) => {
println!(" - parses OK");
println!(" - recipients: {}", ct.recipients.len());
let mut fps = HashSet::new();
for r in &ct.recipients {
println!(" {}", r.fingerprint);
fps.insert(r.fingerprint.clone());
}
vault_recipient_fps = Some(fps);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(&ctx.vault_path) {
let mode = meta.permissions().mode() & 0o777;
if mode == 0o600 {
println!(" - permissions: 0600 (ok)");
} else {
println!(" - permissions: {mode:04o} (expected 0600)");
}
}
}
}
Err(err) => {
println!(" - failed to parse: {err}");
ok = false;
}
}
} else {
println!(" - vault file does not exist; run `sshenv init` first.");
ok = false;
}
println!();
println!("Available SSH private keys:");
let key_paths = discover_private_key_paths();
if key_paths.is_empty() {
println!(" (none found in ~/.ssh/ or ~/.ssh/config)");
} else {
for path in &key_paths {
let annotation = match (
&vault_recipient_fps,
public_fingerprint_for_private_key(path),
) {
(Some(recipients), Some(fp)) => {
if recipients.contains(&fp) {
format!(" {fp} (authorized)")
} else {
format!(" {fp} (not a recipient)")
}
}
(_, Some(fp)) => format!(" {fp}"),
(_, None) => " (no .pub sibling)".to_string(),
};
println!(" {}{annotation}", path.display());
}
}
if let Some(fps) = &vault_recipient_fps {
println!();
print!("Unlock check: ");
let id_result = load_identities_for_vault(fps);
match id_result {
Ok(ids) if ids.is_empty() => {
println!("no SSH key on this host matches a vault recipient");
ok = false;
}
Ok(ids) => match Vault::load_ciphertext(&ctx.vault_path) {
Ok(ct) => match Vault::unlock(ct, &ids) {
Ok(_) => println!("ok"),
Err(_) => {
println!("a matching key was found but decryption failed");
ok = false;
}
},
Err(err) => {
println!("vault not loadable: {err}");
ok = false;
}
},
Err(err) => {
println!("error: {err}");
ok = false;
}
}
} else if ctx.vault_path.exists() {
println!();
print!("Unlock check: ");
match load_identities() {
Ok(ids) if ids.is_empty() => {
println!("no SSH identities available");
ok = false;
}
Ok(_) => println!("(skipped; vault not parseable)"),
Err(err) => {
println!("error: {err}");
ok = false;
}
}
}
println!();
let bindings_path = default_bindings_path();
println!("Bindings file: {}", bindings_path.display());
let bindings = load_bindings(&bindings_path).unwrap_or_default();
let shim_dir = resolve_shim_dir(&bindings);
println!("Shim dir: {}", shim_dir.display());
println!("Bindings: {}", bindings.bindings.len());
let path_env = std::env::var("PATH").unwrap_or_default();
let shim_dir_str = shim_dir.display().to_string();
let on_path = path_env.split(':').any(|p| {
p == shim_dir_str || p.trim_end_matches('/') == shim_dir_str.trim_end_matches('/')
});
println!(
"PATH: {}",
if on_path {
"shim dir is on PATH"
} else {
"shim dir NOT on PATH (shims will not activate)"
}
);
if !bindings.bindings.is_empty() && !on_path {
ok = false;
}
println!();
println!("Result: {}", if ok { "ok" } else { "problems detected" });
if !ok {
std::process::exit(1);
}
Ok(())
}