@@ -20,26 +20,31 @@ use russh::client::Msg;
2020use russh:: Channel ;
2121use std:: io:: { self , Write } ;
2222use tokio:: time:: { timeout, Duration } ;
23+ use zeroize:: Zeroizing ;
2324
2425use crate :: jump:: { parse_jump_hosts, JumpHostChain } ;
2526use crate :: node:: Node ;
2627use crate :: ssh:: {
2728 known_hosts:: get_check_method,
28- tokio_client:: { AuthMethod , Client , ServerCheckMethod } ,
29+ tokio_client:: { AuthMethod , Client , Error as SshError , ServerCheckMethod } ,
2930} ;
3031
3132use super :: types:: { InteractiveCommand , NodeSession } ;
3233
3334impl InteractiveCommand {
3435 /// Helper function to establish SSH connection with proper error handling and rate limiting
3536 /// This eliminates code duplication across different connection paths and prevents brute-force attacks
37+ ///
38+ /// If `allow_password_fallback` is true and key authentication fails, it will prompt for password
39+ /// and retry with password authentication (matching OpenSSH behavior).
3640 async fn establish_connection (
3741 addr : ( & str , u16 ) ,
3842 username : & str ,
3943 auth_method : AuthMethod ,
4044 check_method : ServerCheckMethod ,
4145 host : & str ,
4246 port : u16 ,
47+ allow_password_fallback : bool ,
4348 ) -> Result < Client > {
4449 const SSH_CONNECT_TIMEOUT_SECS : u64 = 30 ;
4550 let connect_timeout = Duration :: from_secs ( SSH_CONNECT_TIMEOUT_SECS ) ;
@@ -56,15 +61,47 @@ impl InteractiveCommand {
5661
5762 let result = timeout (
5863 connect_timeout,
59- Client :: connect ( addr, username, auth_method, check_method) ,
64+ Client :: connect ( addr, username, auth_method, check_method. clone ( ) ) ,
6065 )
6166 . await
6267 . with_context ( || {
6368 format ! (
6469 "Connection timeout: Failed to connect to {host}:{port} after {SSH_CONNECT_TIMEOUT_SECS} seconds"
6570 )
66- } ) ?
67- . with_context ( || format ! ( "SSH connection failed to {host}:{port}" ) ) ;
71+ } ) ?;
72+
73+ // Check if key authentication failed and password fallback is allowed
74+ let result = match result {
75+ Err ( SshError :: KeyAuthFailed )
76+ if allow_password_fallback && atty:: is ( atty:: Stream :: Stdin ) =>
77+ {
78+ tracing:: debug!(
79+ "SSH key authentication failed for {username}@{host}:{port}, attempting password fallback"
80+ ) ;
81+
82+ // Prompt for password (matching OpenSSH behavior)
83+ let password = Self :: prompt_password ( username, host) . await ?;
84+
85+ // Retry with password authentication
86+ let password_auth = AuthMethod :: with_password ( & password) ;
87+
88+ // Small delay before retry to prevent rapid attempts
89+ tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
90+
91+ timeout (
92+ connect_timeout,
93+ Client :: connect ( addr, username, password_auth, check_method) ,
94+ )
95+ . await
96+ . with_context ( || {
97+ format ! (
98+ "Connection timeout: Failed to connect to {host}:{port} after {SSH_CONNECT_TIMEOUT_SECS} seconds"
99+ )
100+ } ) ?
101+ . with_context ( || format ! ( "SSH connection failed to {host}:{port}" ) )
102+ }
103+ other => other. with_context ( || format ! ( "SSH connection failed to {host}:{port}" ) ) ,
104+ } ;
68105
69106 // SECURITY: Normalize timing to prevent timing attacks
70107 // Ensure all authentication attempts take at least 500ms to complete
@@ -79,6 +116,22 @@ impl InteractiveCommand {
79116 result
80117 }
81118
119+ /// Prompt for password with secure handling
120+ async fn prompt_password ( username : & str , host : & str ) -> Result < Zeroizing < String > > {
121+ let username = username. to_string ( ) ;
122+ let host = host. to_string ( ) ;
123+
124+ tokio:: task:: spawn_blocking ( move || {
125+ let password = Zeroizing :: new (
126+ rpassword:: prompt_password ( format ! ( "{username}@{host}'s password: " ) )
127+ . with_context ( || "Failed to read password" ) ?,
128+ ) ;
129+ Ok ( password)
130+ } )
131+ . await
132+ . with_context ( || "Password prompt task failed" ) ?
133+ }
134+
82135 /// Determine authentication method based on node and config (same logic as exec mode)
83136 pub ( super ) async fn determine_auth_method ( & self , node : & Node ) -> Result < AuthMethod > {
84137 // Use centralized authentication logic from auth module
@@ -164,13 +217,15 @@ impl InteractiveCommand {
164217 tracing:: debug!( "No valid jump hosts found, using direct connection" ) ;
165218
166219 // Use the helper function to establish connection
220+ // Enable password fallback for interactive mode (matches OpenSSH behavior)
167221 Self :: establish_connection (
168222 addr,
169223 & node. username ,
170224 auth_method. clone ( ) ,
171225 check_method. clone ( ) ,
172226 & node. host ,
173227 node. port ,
228+ !self . use_password , // Allow fallback unless explicit password mode
174229 )
175230 . await ?
176231 } else {
@@ -239,13 +294,15 @@ impl InteractiveCommand {
239294 tracing:: debug!( "Using direct connection (no jump hosts)" ) ;
240295
241296 // Use the helper function to establish connection
297+ // Enable password fallback for interactive mode (matches OpenSSH behavior)
242298 Self :: establish_connection (
243299 addr,
244300 & node. username ,
245301 auth_method,
246302 check_method,
247303 & node. host ,
248304 node. port ,
305+ !self . use_password , // Allow fallback unless explicit password mode
249306 )
250307 . await ?
251308 } ;
@@ -300,13 +357,15 @@ impl InteractiveCommand {
300357 tracing:: debug!( "No valid jump hosts found, using direct connection for PTY" ) ;
301358
302359 // Use the helper function to establish connection
360+ // Enable password fallback for interactive mode (matches OpenSSH behavior)
303361 Self :: establish_connection (
304362 addr,
305363 & node. username ,
306364 auth_method. clone ( ) ,
307365 check_method. clone ( ) ,
308366 & node. host ,
309367 node. port ,
368+ !self . use_password , // Allow fallback unless explicit password mode
310369 )
311370 . await ?
312371 } else {
@@ -375,13 +434,15 @@ impl InteractiveCommand {
375434 tracing:: debug!( "Using direct connection for PTY (no jump hosts)" ) ;
376435
377436 // Use the helper function to establish connection
437+ // Enable password fallback for interactive mode (matches OpenSSH behavior)
378438 Self :: establish_connection (
379439 addr,
380440 & node. username ,
381441 auth_method,
382442 check_method,
383443 & node. host ,
384444 node. port ,
445+ !self . use_password , // Allow fallback unless explicit password mode
385446 )
386447 . await ?
387448 } ;
0 commit comments