sunset_stdasync/
knownhosts.rs1#[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 Mismatch {
18 path: PathBuf,
19 line: usize,
20 existing: OpenSSHKey,
21 },
22
23 NotAccepted,
25
26 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 #[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
65fn line_entry(line: &str) -> Option<(String, String)> {
67 line.split_once(' ').map(|(h, k)| (h.into(), k.into()))
68}
69
70fn 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 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 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}