sunset_stdasync/
knownhosts.rs

1#[allow(unused_imports)]
2use log::{debug, error, info, log, trace, warn};
3
4use std::fs::{File, OpenOptions};
5use std::io;
6use std::io::{BufRead, Read, Write};
7use std::path::{Path, PathBuf};
8
9use crate::*;
10use sunset::packets::PubKey;
11
12type OpenSSHKey = ssh_key::PublicKey;
13
14#[derive(Debug)]
15pub enum KnownHostsError {
16    /// Host Key Mismatch
17    Mismatch {
18        path: PathBuf,
19        line: usize,
20        existing: OpenSSHKey,
21    },
22
23    /// User didn't accept new key
24    NotAccepted,
25
26    /// Failure
27    Failure {
28        source: Box<dyn std::error::Error>,
29    },
30
31    Other {
32        msg: String,
33    },
34}
35
36impl<E> From<E> for KnownHostsError
37where
38    E: std::error::Error + 'static,
39{
40    fn from(e: E) -> Self {
41        KnownHostsError::Failure { source: Box::new(e) }
42    }
43}
44
45const USER_KNOWN_HOSTS: &str = ".ssh/known_hosts";
46
47fn user_known_hosts() -> Result<PathBuf, KnownHostsError> {
48    // home_dir() works fine on linux.
49    #[allow(deprecated)]
50    let p = std::env::home_dir().ok_or_else(|| KnownHostsError::Other {
51        msg: "Failed getting home directory".into(),
52    })?;
53    Ok(p.join(USER_KNOWN_HOSTS))
54}
55
56pub fn check_known_hosts(
57    host: &str,
58    port: u16,
59    key: &PubKey,
60) -> Result<(), KnownHostsError> {
61    let p = user_known_hosts()?;
62    check_known_hosts_file(host, port, key, &p)
63}
64
65/// Returns a `(host, key)` entry from a known_hosts line, or `None` if not matching
66fn line_entry(line: &str) -> Option<(String, String)> {
67    line.split_once(' ').map(|(h, k)| (h.into(), k.into()))
68}
69
70/// Returns the host string. Non-22 ports are appended.
71fn host_part(host: &str, port: u16) -> String {
72    let mut host = host.to_lowercase();
73    if port != sunset::sshnames::SSH_PORT {
74        host = format!("[{host}]:{port}");
75    }
76    host
77}
78
79pub fn check_known_hosts_file(
80    host: &str,
81    port: u16,
82    key: &PubKey,
83    p: &Path,
84) -> Result<(), KnownHostsError> {
85    let f = File::open(p)?;
86    let f = io::BufReader::new(f);
87
88    let match_host = host_part(host, port);
89
90    let pubk: OpenSSHKey = key.try_into()?;
91
92    for (line, (lh, lk)) in f.lines().enumerate().filter_map(|(num, l)| {
93        if let Ok(l) = l {
94            line_entry(&l).map(|entry| (num, entry))
95        } else {
96            None
97        }
98    }) {
99        let line = line + 1;
100
101        if lh != match_host {
102            continue;
103        }
104
105        let known_key = match OpenSSHKey::from_openssh(&lk) {
106            Ok(k) => k,
107            Err(e) => {
108                warn!(
109                    "Unparsed key for \"{}\" on line {}:{}",
110                    host,
111                    p.display(),
112                    line
113                );
114                trace!("{e:?}");
115                continue;
116            }
117        };
118
119        if pubk.algorithm() != known_key.algorithm() {
120            debug!("Line {line}, Ignoring other-format existing key {known_key:?}")
121        } else {
122            if pubk.key_data() == known_key.key_data() {
123                debug!("Line {line}, found matching key");
124                return Ok(());
125            } else {
126                let fp = known_key.fingerprint(Default::default());
127                println!("\nHost key mismatch for {match_host} in ~/.ssh/known_hosts line {line}\n\
128                    Existing key has fingerprint {fp}\n");
129                return Err(KnownHostsError::Mismatch {
130                    path: p.to_path_buf(),
131                    line,
132                    existing: known_key,
133                });
134            }
135        }
136    }
137
138    // no match, maybe add it
139    ask_to_confirm(host, port, key, p)
140}
141
142fn read_tty_response() -> Result<String, std::io::Error> {
143    let mut s;
144    let mut f = File::open("/dev/tty");
145    let f: &mut dyn Read = match f.as_mut() {
146        Ok(f) => f,
147        Err(_) => {
148            s = io::stdin();
149            &mut s
150        }
151    };
152
153    let mut f = io::BufReader::new(f);
154    let mut resp = String::new();
155    f.read_line(&mut resp)?;
156    Ok(resp)
157}
158
159fn ask_to_confirm(
160    host: &str,
161    port: u16,
162    key: &PubKey,
163    p: &Path,
164) -> Result<(), KnownHostsError> {
165    let k: OpenSSHKey = key.try_into()?;
166    let fp = k.fingerprint(Default::default());
167    let h = host_part(host, port);
168    let _ = writeln!(io::stderr(), "\nHost {h} is not in ~/.ssh/known_hosts\nFingerprint {fp}\nDo you want to continue connecting? (y/n)");
169
170    let mut resp = read_tty_response()?;
171    resp.make_ascii_lowercase();
172    if resp.starts_with('y') {
173        add_key(host, port, key, p)
174    } else {
175        Err(KnownHostsError::NotAccepted)
176    }
177}
178
179fn add_key(
180    host: &str,
181    port: u16,
182    key: &PubKey,
183    p: &Path,
184) -> Result<(), KnownHostsError> {
185    let k: OpenSSHKey = key.try_into()?;
186    // encode it
187    let k = k.to_openssh()?;
188
189    let h = host_part(host, port);
190
191    let entry = format!("{h} {k}\n");
192
193    let mut f = std::fs::OpenOptions::new().append(true).open(p)?;
194
195    f.write_all(entry.as_bytes())?;
196
197    Ok(())
198}