wezterm_ssh/
auth.rs

1use crate::session::SessionEvent;
2use anyhow::Context;
3use smol::channel::{bounded, Sender};
4
5#[derive(Debug)]
6pub struct AuthenticationPrompt {
7    pub prompt: String,
8    pub echo: bool,
9}
10
11#[derive(Debug)]
12pub struct AuthenticationEvent {
13    pub username: String,
14    pub instructions: String,
15    pub prompts: Vec<AuthenticationPrompt>,
16    pub(crate) reply: Sender<Vec<String>>,
17}
18
19impl AuthenticationEvent {
20    pub async fn answer(self, answers: Vec<String>) -> anyhow::Result<()> {
21        Ok(self.reply.send(answers).await?)
22    }
23
24    pub fn try_answer(self, answers: Vec<String>) -> anyhow::Result<()> {
25        Ok(self.reply.try_send(answers)?)
26    }
27}
28
29impl crate::sessioninner::SessionInner {
30    #[cfg(feature = "ssh2")]
31    fn agent_auth(&mut self, sess: &ssh2::Session, user: &str) -> anyhow::Result<bool> {
32        if let Some(only) = self.config.get("identitiesonly") {
33            if only == "yes" {
34                log::trace!("Skipping agent auth because identitiesonly=yes");
35                return Ok(false);
36            }
37        }
38
39        let mut agent = sess.agent()?;
40        if agent.connect().is_err() {
41            // If the agent is around, we can proceed with other methods
42            return Ok(false);
43        }
44
45        agent.list_identities()?;
46        let identities = agent.identities()?;
47        for identity in identities {
48            if agent.userauth(user, &identity).is_ok() {
49                return Ok(true);
50            }
51        }
52
53        Ok(false)
54    }
55
56    #[cfg(feature = "ssh2")]
57    fn pubkey_auth(
58        &mut self,
59        sess: &ssh2::Session,
60        user: &str,
61        host: &str,
62    ) -> anyhow::Result<bool> {
63        use std::path::{Path, PathBuf};
64
65        if let Some(files) = self.config.get("identityfile") {
66            for file in files.split_whitespace() {
67                let pubkey: PathBuf = format!("{}.pub", file).into();
68                let file = Path::new(file);
69
70                if !file.exists() {
71                    continue;
72                }
73
74                let pubkey = if pubkey.exists() {
75                    Some(pubkey.as_ref())
76                } else {
77                    None
78                };
79
80                // We try with no passphrase first, in case the key is unencrypted
81                match sess.userauth_pubkey_file(user, pubkey, &file, None) {
82                    Ok(_) => {
83                        log::info!("pubkey_file immediately ok for {}", file.display());
84                        return Ok(true);
85                    }
86                    Err(_) => {
87                        // Most likely cause of error is that we need a passphrase
88                        // to decrypt the key, so let's prompt the user for one.
89                        let (reply, answers) = bounded(1);
90                        self.tx_event
91                            .try_send(SessionEvent::Authenticate(AuthenticationEvent {
92                                username: "".to_string(),
93                                instructions: "".to_string(),
94                                prompts: vec![AuthenticationPrompt {
95                                    prompt: format!(
96                                        "Passphrase to decrypt {} for {}@{}:\n> ",
97                                        file.display(),
98                                        user,
99                                        host
100                                    ),
101                                    echo: false,
102                                }],
103                                reply,
104                            }))
105                            .context("sending Authenticate request to user")?;
106
107                        let answers = smol::block_on(answers.recv())
108                            .context("waiting for authentication answers from user")?;
109
110                        if answers.is_empty() {
111                            anyhow::bail!("user cancelled authentication");
112                        }
113
114                        let passphrase = &answers[0];
115
116                        match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) {
117                            Ok(_) => {
118                                return Ok(true);
119                            }
120                            Err(err) => {
121                                log::warn!("pubkey auth: {:#}", err);
122                            }
123                        }
124                    }
125                }
126            }
127        }
128        Ok(false)
129    }
130
131    #[cfg(feature = "libssh-rs")]
132    pub fn authenticate_libssh(&mut self, sess: &libssh_rs::Session) -> anyhow::Result<()> {
133        use std::collections::HashMap;
134        let tx = self.tx_event.clone();
135
136        // Set the callback for pubkey auth
137        sess.set_auth_callback(move |prompt, echo, _verify, identity| {
138            let (reply, answers) = bounded(1);
139            tx.try_send(SessionEvent::Authenticate(AuthenticationEvent {
140                username: "".to_string(),
141                instructions: "".to_string(),
142                prompts: vec![AuthenticationPrompt {
143                    prompt: match identity {
144                        Some(ident) => format!("{} ({}): ", prompt, ident),
145                        None => prompt.to_string(),
146                    },
147                    echo,
148                }],
149                reply,
150            }))
151            .unwrap();
152
153            let mut answers = smol::block_on(answers.recv())
154                .context("waiting for authentication answers from user")
155                .unwrap();
156            Ok(answers.remove(0))
157        });
158
159        use libssh_rs::{AuthMethods, AuthStatus};
160        match sess.userauth_none(None)? {
161            AuthStatus::Success => return Ok(()),
162            _ => {}
163        }
164
165        loop {
166            let auth_methods = sess.userauth_list(None)?;
167            let mut status_by_method = HashMap::new();
168
169            if auth_methods.contains(AuthMethods::PUBLIC_KEY) {
170                match sess.userauth_public_key_auto(None, None)? {
171                    AuthStatus::Success => return Ok(()),
172                    AuthStatus::Partial => continue,
173                    status => {
174                        status_by_method.insert(AuthMethods::PUBLIC_KEY, status);
175                    }
176                }
177            }
178
179            if auth_methods.contains(AuthMethods::INTERACTIVE) {
180                loop {
181                    match sess.userauth_keyboard_interactive(None, None)? {
182                        AuthStatus::Success => return Ok(()),
183                        AuthStatus::Info => {
184                            let info = sess.userauth_keyboard_interactive_info()?;
185
186                            let (reply, answers) = bounded(1);
187                            self.tx_event
188                                .try_send(SessionEvent::Authenticate(AuthenticationEvent {
189                                    username: sess.get_user_name()?,
190                                    instructions: info.instruction,
191                                    prompts: info
192                                        .prompts
193                                        .into_iter()
194                                        .map(|p| AuthenticationPrompt {
195                                            prompt: p.prompt,
196                                            echo: p.echo,
197                                        })
198                                        .collect(),
199                                    reply,
200                                }))
201                                .context("sending Authenticate request to user")?;
202
203                            let answers = smol::block_on(answers.recv())
204                                .context("waiting for authentication answers from user")?;
205
206                            sess.userauth_keyboard_interactive_set_answers(&answers)?;
207
208                            continue;
209                        }
210                        AuthStatus::Denied => {
211                            break;
212                        }
213                        AuthStatus::Partial => continue,
214                        status => {
215                            anyhow::bail!("interactive auth status: {:?}", status);
216                        }
217                    }
218                }
219            }
220
221            if auth_methods.contains(AuthMethods::PASSWORD) {
222                let (reply, answers) = bounded(1);
223                self.tx_event
224                    .try_send(SessionEvent::Authenticate(AuthenticationEvent {
225                        username: "".to_string(),
226                        instructions: "".to_string(),
227                        prompts: vec![AuthenticationPrompt {
228                            prompt: "Password: ".to_string(),
229                            echo: false,
230                        }],
231                        reply,
232                    }))
233                    .unwrap();
234
235                let mut answers = smol::block_on(answers.recv())
236                    .context("waiting for authentication answers from user")
237                    .unwrap();
238                let pw = answers.remove(0);
239
240                match sess.userauth_password(None, Some(&pw))? {
241                    AuthStatus::Success => return Ok(()),
242                    AuthStatus::Partial => continue,
243                    status => anyhow::bail!("password auth status: {:?}", status),
244                }
245            }
246
247            anyhow::bail!(
248                "unhandled auth case; methods={:?}, status={:?}",
249                auth_methods,
250                status_by_method
251            );
252        }
253    }
254
255    #[cfg(feature = "ssh2")]
256    pub fn authenticate(
257        &mut self,
258        sess: &ssh2::Session,
259        user: &str,
260        host: &str,
261    ) -> anyhow::Result<()> {
262        use std::collections::HashSet;
263
264        loop {
265            if sess.authenticated() {
266                return Ok(());
267            }
268
269            // Re-query the auth methods on each loop as a successful method
270            // may unlock a new method on a subsequent iteration (eg: password
271            // auth may then unlock 2fac)
272            let methods: HashSet<&str> = sess.auth_methods(&user)?.split(',').collect();
273            log::trace!("ssh auth methods: {:?}", methods);
274
275            if !sess.authenticated() && methods.contains("publickey") {
276                if self.agent_auth(sess, user)? {
277                    continue;
278                }
279
280                if self.pubkey_auth(sess, user, host)? {
281                    continue;
282                }
283            }
284
285            if !sess.authenticated() && methods.contains("password") {
286                let (reply, answers) = bounded(1);
287                self.tx_event
288                    .try_send(SessionEvent::Authenticate(AuthenticationEvent {
289                        username: user.to_string(),
290                        instructions: "".to_string(),
291                        prompts: vec![AuthenticationPrompt {
292                            prompt: format!("Password for {}@{}: ", user, host),
293                            echo: false,
294                        }],
295                        reply,
296                    }))
297                    .context("sending Authenticate request to user")?;
298
299                let answers = smol::block_on(answers.recv())
300                    .context("waiting for authentication answers from user")?;
301
302                if answers.is_empty() {
303                    anyhow::bail!("user cancelled authentication");
304                }
305
306                if let Err(err) = sess.userauth_password(user, &answers[0]) {
307                    log::error!("while attempting password auth: {}", err);
308                }
309            }
310
311            if !sess.authenticated() && methods.contains("keyboard-interactive") {
312                struct Helper<'a> {
313                    tx_event: &'a Sender<SessionEvent>,
314                }
315
316                impl<'a> ssh2::KeyboardInteractivePrompt for Helper<'a> {
317                    fn prompt<'b>(
318                        &mut self,
319                        username: &str,
320                        instructions: &str,
321                        prompts: &[ssh2::Prompt<'b>],
322                    ) -> Vec<String> {
323                        let (reply, answers) = bounded(1);
324                        if let Err(err) = self.tx_event.try_send(SessionEvent::Authenticate(
325                            AuthenticationEvent {
326                                username: username.to_string(),
327                                instructions: instructions.to_string(),
328                                prompts: prompts
329                                    .iter()
330                                    .map(|p| AuthenticationPrompt {
331                                        prompt: p.text.to_string(),
332                                        echo: p.echo,
333                                    })
334                                    .collect(),
335                                reply,
336                            },
337                        )) {
338                            log::error!("sending Authenticate request to user: {:#}", err);
339                            return vec![];
340                        }
341
342                        match smol::block_on(answers.recv()) {
343                            Err(err) => {
344                                log::error!(
345                                    "waiting for authentication answers from user: {:#}",
346                                    err
347                                );
348                                return vec![];
349                            }
350                            Ok(answers) => answers,
351                        }
352                    }
353                }
354
355                let mut helper = Helper {
356                    tx_event: &self.tx_event,
357                };
358
359                if let Err(err) = sess.userauth_keyboard_interactive(user, &mut helper) {
360                    log::error!("while attempting keyboard-interactive auth: {}", err);
361                }
362            }
363        }
364    }
365}