#![allow(dead_code)]
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[cfg(unix)]
use puressh::agent::{Agent, AgentHostKey};
use puressh::auth::ClientCredential;
use puressh::client::{HostKeyPolicy, KnownHostsPolicy, TofuAction};
use puressh::key::PrivateKey;
use puressh::known_hosts::KnownHosts;
use zeroize::Zeroizing;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StrictMode {
Yes,
No,
AcceptNew,
Ask,
}
pub fn resolve_user(cli_user: Option<&str>, user_in_host: Option<&str>) -> Result<String, String> {
if let Some(u) = cli_user {
return Ok(u.to_string());
}
if let Some(u) = user_in_host {
return Ok(u.to_string());
}
std::env::var("USER").map_err(|_| "no user specified and $USER is unset".into())
}
pub fn parse_userhost(target: &str) -> (Option<String>, String) {
match target.split_once('@') {
Some((u, h)) => (Some(u.to_string()), h.to_string()),
None => (None, target.to_string()),
}
}
pub fn parse_userhost_path(target: &str) -> Option<(Option<String>, String, String)> {
if target.starts_with('/') {
return None;
}
let (head, path) = target.split_once(':')?;
let (user, host) = parse_userhost(head);
if host.is_empty() {
return None;
}
Some((user, host, path.to_string()))
}
pub fn read_password_from_stdin() -> std::io::Result<Zeroizing<String>> {
if let Some(out) = try_ssh_askpass()? {
return Ok(out);
}
eprint!("password: ");
std::io::stderr().flush()?;
#[cfg(unix)]
{
if let Some(out) = read_password_no_echo_unix()? {
return Ok(out);
}
}
eprintln!();
eprintln!("(warning: terminal echo could not be disabled; password will be visible)");
let mut buf = String::new();
read_one_line(&mut buf, 4096)?;
Ok(Zeroizing::new(buf))
}
fn read_one_line(buf: &mut String, max_len: usize) -> std::io::Result<()> {
let mut byte = [0u8; 1];
let mut stdin = std::io::stdin();
loop {
let n = stdin.read(&mut byte)?;
if n == 0 || byte[0] == b'\n' {
break;
}
if byte[0] == b'\r' {
continue;
}
buf.push(byte[0] as char);
if buf.len() > max_len {
break;
}
}
Ok(())
}
#[cfg(unix)]
fn read_password_no_echo_unix() -> std::io::Result<Option<Zeroizing<String>>> {
use std::os::unix::io::AsRawFd;
let fd = std::io::stdin().as_raw_fd();
let mut term: libc::termios = unsafe { core::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut term as *mut _) } != 0 {
return Ok(None);
}
let original = term;
struct EchoGuard {
fd: libc::c_int,
original: libc::termios,
}
impl Drop for EchoGuard {
fn drop(&mut self) {
unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &self.original) };
}
}
term.c_lflag &= !libc::ECHO;
if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &term) } != 0 {
return Ok(None);
}
let _guard = EchoGuard { fd, original };
let mut buf = String::new();
let res = read_one_line(&mut buf, 4096);
eprintln!();
res?;
Ok(Some(Zeroizing::new(buf)))
}
fn try_ssh_askpass() -> std::io::Result<Option<Zeroizing<String>>> {
let askpass = match std::env::var_os("SSH_ASKPASS") {
Some(v) if !v.is_empty() => v,
_ => return Ok(None),
};
if let Some(req) = std::env::var_os("SSH_ASKPASS_REQUIRE") {
if req == "never" {
return Ok(None);
}
}
let mut cmd = std::process::Command::new(askpass);
cmd.arg("password: ");
cmd.stdin(std::process::Stdio::null());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::inherit());
let out = match cmd.output() {
Ok(o) => o,
Err(_) => return Ok(None),
};
if !out.status.success() {
return Ok(None);
}
let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
if let Some(idx) = s.find('\n') {
s.truncate(idx);
}
if s.ends_with('\r') {
s.pop();
}
Ok(Some(Zeroizing::new(s)))
}
pub fn load_identity(path: &str) -> Result<PrivateKey, String> {
let pem = std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))?;
PrivateKey::parse_openssh_pem(&pem, None)
.map_err(|e| format!("parse {path}: {e} (passphrase-protected keys not supported here)"))
}
#[cfg(unix)]
pub fn connect_agent_credentials() -> Result<Vec<ClientCredential>, String> {
let agent = match Agent::connect_env().map_err(|e| format!("connect: {e}"))? {
Some(a) => a,
None => return Ok(Vec::new()),
};
let agent = Arc::new(Mutex::new(agent));
let identities = {
let mut a = agent
.lock()
.map_err(|_| "agent mutex poisoned".to_string())?;
a.identities().map_err(|e| format!("identities: {e}"))?
};
let mut creds: Vec<ClientCredential> = Vec::with_capacity(identities.len());
for ident in identities {
match AgentHostKey::from_identity(Arc::clone(&agent), ident.key_blob.clone()) {
Ok(hk) => creds.push(ClientCredential::PublicKey(Box::new(hk))),
Err(e) => eprintln!(
"warning: agent identity {:?}: skipping: {e}",
ident.comment()
),
}
}
Ok(creds)
}
#[cfg(not(unix))]
pub fn connect_agent_credentials() -> Result<Vec<ClientCredential>, String> {
Ok(Vec::new())
}
pub fn default_known_hosts_path() -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
Some(PathBuf::from(home).join(".ssh").join("known_hosts"))
}
pub fn fingerprint_b64_sha256(blob: &[u8]) -> String {
use purecrypto::hash::{Digest, Sha256};
let digest = Sha256::digest(blob);
let s = base64_no_pad(digest.as_ref());
format!("SHA256:{s}")
}
pub fn base64_no_pad(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
let mut i = 0;
while i + 3 <= bytes.len() {
let b = ((bytes[i] as u32) << 16) | ((bytes[i + 1] as u32) << 8) | (bytes[i + 2] as u32);
out.push(ALPHABET[((b >> 18) & 0x3F) as usize] as char);
out.push(ALPHABET[((b >> 12) & 0x3F) as usize] as char);
out.push(ALPHABET[((b >> 6) & 0x3F) as usize] as char);
out.push(ALPHABET[(b & 0x3F) as usize] as char);
i += 3;
}
let rem = bytes.len() - i;
if rem == 1 {
let b = (bytes[i] as u32) << 16;
out.push(ALPHABET[((b >> 18) & 0x3F) as usize] as char);
out.push(ALPHABET[((b >> 12) & 0x3F) as usize] as char);
} else if rem == 2 {
let b = ((bytes[i] as u32) << 16) | ((bytes[i + 1] as u32) << 8);
out.push(ALPHABET[((b >> 18) & 0x3F) as usize] as char);
out.push(ALPHABET[((b >> 12) & 0x3F) as usize] as char);
out.push(ALPHABET[((b >> 6) & 0x3F) as usize] as char);
}
out
}
pub fn tofu_prompt(host: &str, port: u16, key_type: &str, key_blob: &[u8]) -> bool {
let fp = fingerprint_b64_sha256(key_blob);
let target = if port == 22 {
host.to_string()
} else {
format!("[{host}]:{port}")
};
eprintln!("The authenticity of host '{target}' can't be established.");
eprintln!("{key_type} key fingerprint is {fp}.");
eprint!("Are you sure you want to continue connecting (yes/no)? ");
let _ = std::io::stderr().flush();
let mut line = String::new();
let mut byte = [0u8; 1];
let mut stdin = std::io::stdin();
while let Ok(n) = stdin.read(&mut byte) {
if n == 0 || byte[0] == b'\n' {
break;
}
if byte[0] == b'\r' {
continue;
}
line.push(byte[0] as char);
if line.len() > 16 {
break;
}
}
matches!(line.trim().to_ascii_lowercase().as_str(), "yes" | "y")
}
pub fn build_host_key_policy(
strict: StrictMode,
explicit_path: Option<PathBuf>,
hash_known_hosts: bool,
) -> Result<HostKeyPolicy, String> {
let path = match explicit_path {
Some(p) => p,
None => default_known_hosts_path()
.ok_or_else(|| "no $HOME, cannot locate default known_hosts".to_string())?,
};
let store = KnownHosts::load(&path).map_err(|e| format!("load {}: {e}", path.display()))?;
let (on_unknown, on_mismatch) = match strict {
StrictMode::Yes => (TofuAction::Reject, TofuAction::Reject),
StrictMode::AcceptNew => (TofuAction::Accept, TofuAction::Reject),
StrictMode::Ask => (
TofuAction::Prompt(Arc::new(tofu_prompt)),
TofuAction::Reject,
),
StrictMode::No => (TofuAction::Accept, TofuAction::AcceptWithWarning),
};
Ok(HostKeyPolicy::KnownHosts(KnownHostsPolicy {
store: Arc::new(Mutex::new(store)),
save_path: Some(path),
hash_new: hash_known_hosts,
on_unknown,
on_mismatch,
}))
}