From 81ae0035c39b927b43b8a6871aa3ddeb5b1b0bfb Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Sun, 8 Jun 2025 18:34:59 +0200 Subject: [PATCH] Fix SSH auth with id keys (#347) I fixed the id_rsa/id_ed25519 SSH auth issue on Mac and now termscp should respect the key-based authentication, just like the regular ssh user@hostname, without the need for any ssh agents. --- Co-authored-by: Lucas Czekaj --- CHANGELOG.md | 1 + dist/build/macos.sh | 4 +-- src/filetransfer/remotefs_builder.rs | 6 ++++- src/system/sshkey_storage.rs | 37 ++++++++++++++++++++++++---- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421fc98..fefc81f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Released on 31/07/2025 - **Updated dependencies** and updated the Rust edition to `2024` +- [Issue 345](https://github.com/veeso/termscp/issues/345): Default keys are used from `~/.ssh` directory if no keys are resolved for the host. ## 0.17.0 diff --git a/dist/build/macos.sh b/dist/build/macos.sh index 632db05..6871f9d 100755 --- a/dist/build/macos.sh +++ b/dist/build/macos.sh @@ -81,7 +81,7 @@ fi # Build release (x86_64) X86_TARGET="" X86_TARGET_DIR="" -if [ "$ARCH" = "aarch64" ]; then +if [ "$ARCH" = "x86_64" ]; then X86_TARGET="--target x86_64-apple-darwin" X86_TARGET_DIR="target/x86_64-apple-darwin/release/" fi @@ -92,7 +92,7 @@ RET_X86_64=$? ARM64_TARGET="" ARM64_TARGET_DIR="" -if [ "$ARCH" = "aarch64" ]; then +if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then ARM64_TARGET="--target aarch64-apple-darwin" ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/" fi diff --git a/src/filetransfer/remotefs_builder.rs b/src/filetransfer/remotefs_builder.rs index 03e75c0..53cf17c 100644 --- a/src/filetransfer/remotefs_builder.rs +++ b/src/filetransfer/remotefs_builder.rs @@ -233,8 +233,12 @@ impl RemoteFsBuilder { debug!("no username was provided, using current username"); opts = opts.username(whoami::username()); } + // For SSH protocols, only set password if explicitly provided and non-empty. + // This allows the SSH library to prioritize key-based and agent authentication. if let Some(password) = params.password { - opts = opts.password(password); + if !password.is_empty() { + opts = opts.password(password); + } } if let Some(config_path) = config_client.get_ssh_config() { opts = opts.config_file( diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index 090c229..259357a 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -50,6 +50,23 @@ impl SshKeyStorage { .and_then(|x| x.first().cloned()) }) } + + /// Get default SSH identity files that SSH would normally try + /// This mirrors the behavior of OpenSSH client + fn get_default_identity_files(&self) -> Vec { + let Some(home_dir) = dirs::home_dir() else { + return Vec::new(); + }; + + let ssh_dir = home_dir.join(".ssh"); + + // Standard SSH identity files in order of preference (matches OpenSSH) + ["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"] + .iter() + .map(|key_name| ssh_dir.join(key_name)) + .filter(|key_path| key_path.exists()) + .collect() + } } impl SshKeyStorageTrait for SshKeyStorage { @@ -63,9 +80,13 @@ impl SshKeyStorageTrait for SshKeyStorage { username, host ); // otherwise search in configuration - let key = self.resolve_host_in_ssh2_configuration(host)?; - debug!("Found key in SSH config for {host}: {}", key.display()); - Some(key) + if let Some(key) = self.resolve_host_in_ssh2_configuration(host) { + debug!("Found key in SSH config for {host}: {}", key.display()); + return Some(key); + } + + // As a final fallback, try default SSH identity files (like regular ssh does) + self.get_default_identity_files().into_iter().next() } } @@ -134,8 +155,14 @@ mod tests { *storage.resolve("192.168.1.31", "pi").unwrap(), exp_key_path ); - // Verify unexisting key - assert!(storage.resolve("deskichup", "veeso").is_none()); + // Verify key is a default key or none + let default_keys: Vec = storage.get_default_identity_files().into_iter().collect(); + + if let Some(key) = storage.resolve("deskichup", "veeso") { + assert!(default_keys.contains(&key)); + } else { + assert!(default_keys.is_empty()); + } } #[test]