Files
ashell/src/session/ssh_keys.rs
T
tg.shi 31218f7a37 feat(ssh): connect via ~/.ssh/config entries
Add AuthMethod::Config: parse ~/.ssh/config and list concrete host
entries in the connection dialog. Selecting an entry pre-fills
host/port/user/identity file and connects.

- Extract shared key utilities into session::ssh_keys
  (private_keys_with_algs, normalize_inline_private_key,
  authenticate_with_default_keys, session_has_explicit_key), reused by
  backend::ssh and sftp to remove duplication.
- Add session::ssh_config parser for ~/.ssh/config (Host/HostName/User/
  Port/IdentityFile), skipping wildcard patterns and Match/Include.
- Key auth now falls back to default keys (~/.ssh/id_*) when no explicit
  key path or inline content is provided.
- Preserve escaped \r\n normalization in normalize_inline_private_key
  when refactoring into the shared module.
2026-07-08 11:50:51 +08:00

97 lines
2.8 KiB
Rust

use std::sync::Arc;
use anyhow::{Result, anyhow};
use directories::BaseDirs;
use russh::{
client::{self, Handler},
keys::{HashAlg, PrivateKey, key::PrivateKeyWithHashAlg, load_secret_key},
};
use crate::session::config::Session;
pub const DEFAULT_KEY_NAMES: &[&str] = &["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"];
pub fn session_has_explicit_key(session: &Session) -> bool {
!session.private_key_path.trim().is_empty()
|| !normalize_inline_private_key(&session.private_key_inline).is_empty()
}
pub fn normalize_inline_private_key(value: &str) -> String {
let mut normalized = value
.trim()
.replace("\\r\\n", "\n")
.replace("\\n", "\n")
.replace("\r\n", "\n");
if normalized.is_empty() {
return String::new();
}
if !normalized.ends_with('\n') {
normalized.push('\n');
}
normalized
}
pub fn private_keys_with_algs(keypair: PrivateKey) -> Result<Vec<PrivateKeyWithHashAlg>> {
let mut algs = Vec::new();
let key_arc = Arc::new(keypair);
if key_arc.algorithm().is_rsa() {
if let Ok(k) = PrivateKeyWithHashAlg::new(key_arc.clone(), Some(HashAlg::Sha512)) {
algs.push(k);
}
if let Ok(k) = PrivateKeyWithHashAlg::new(key_arc.clone(), Some(HashAlg::Sha256)) {
algs.push(k);
}
if let Ok(k) = PrivateKeyWithHashAlg::new(key_arc.clone(), None) {
algs.push(k);
}
} else if let Ok(k) = PrivateKeyWithHashAlg::new(key_arc.clone(), None) {
algs.push(k);
}
if algs.is_empty() {
return Err(anyhow!(
"Failed to construct PrivateKeyWithHashAlg for any supported hash algorithm"
));
}
Ok(algs)
}
pub async fn authenticate_with_default_keys<H>(
handle: &mut client::Handle<H>,
user: &str,
passphrase: Option<&str>,
) -> Result<bool>
where
H: Handler + Send + Sync,
H::Error: Into<anyhow::Error>,
{
let Some(ssh_dir) = BaseDirs::new().map(|d| d.home_dir().join(".ssh")) else {
return Ok(false);
};
for key_name in DEFAULT_KEY_NAMES {
let key_path = ssh_dir.join(key_name);
if !key_path.exists() {
continue;
}
tracing::debug!("[ssh] trying default key {}", key_path.display());
match load_secret_key(&key_path, passphrase) {
Ok(keypair) => {
if let Ok(keys) = private_keys_with_algs(keypair) {
for key in keys {
match handle.authenticate_publickey(user, key).await {
Ok(true) => return Ok(true),
Ok(false) | Err(_) => continue,
}
}
}
}
Err(_) => continue,
}
}
Ok(false)
}