1use std::borrow::Cow;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
4use std::path::{Path, PathBuf};
5
6use data_encoding::BASE64_MIME;
7use hmac::{Hmac, Mac};
8use log::debug;
9use sha1::Sha1;
10
11use crate::keys::Error;
12
13pub fn check_known_hosts(
15 host: &str,
16 port: u16,
17 pubkey: &ssh_key::PublicKey,
18) -> Result<bool, Error> {
19 check_known_hosts_path(host, port, pubkey, known_hosts_path()?)
20}
21
22pub fn check_known_hosts_path<P: AsRef<Path>>(
24 host: &str,
25 port: u16,
26 pubkey: &ssh_key::PublicKey,
27 path: P,
28) -> Result<bool, Error> {
29 let check = known_host_keys_path(host, port, path)?
30 .into_iter()
31 .map(|(line, recorded)| {
32 match (
33 pubkey.algorithm() == recorded.algorithm(),
34 *pubkey == recorded,
35 ) {
36 (true, true) => Ok(true),
37 (true, false) => Err(Error::KeyChanged { line }),
38 _ => Ok(false),
39 }
40 })
41 .collect::<Result<Vec<bool>, Error>>()?
43 .into_iter()
44 .any(|x| x);
46
47 Ok(check)
48}
49
50#[cfg(target_os = "windows")]
51fn known_hosts_path() -> Result<PathBuf, Error> {
52 if let Some(home_dir) = home::home_dir() {
53 Ok(home_dir.join("ssh").join("known_hosts"))
54 } else {
55 Err(Error::NoHomeDir)
56 }
57}
58
59#[cfg(not(target_os = "windows"))]
60fn known_hosts_path() -> Result<PathBuf, Error> {
61 if let Some(home_dir) = home::home_dir() {
62 Ok(home_dir.join(".ssh").join("known_hosts"))
63 } else {
64 Err(Error::NoHomeDir)
65 }
66}
67
68pub fn known_host_keys(host: &str, port: u16) -> Result<Vec<(usize, ssh_key::PublicKey)>, Error> {
70 known_host_keys_path(host, port, known_hosts_path()?)
71}
72
73pub fn known_host_keys_path<P: AsRef<Path>>(
75 host: &str,
76 port: u16,
77 path: P,
78) -> Result<Vec<(usize, ssh_key::PublicKey)>, Error> {
79 use crate::keys::parse_public_key_base64;
80
81 let mut f = if let Ok(f) = File::open(path) {
82 BufReader::new(f)
83 } else {
84 return Ok(vec![]);
85 };
86 let mut buffer = String::new();
87
88 let host_port = if port == 22 {
89 Cow::Borrowed(host)
90 } else {
91 Cow::Owned(format!("[{}]:{}", host, port))
92 };
93 debug!("host_port = {:?}", host_port);
94 let mut line = 1;
95 let mut matches = vec![];
96 while f.read_line(&mut buffer)? > 0 {
97 {
98 if buffer.as_bytes().first() == Some(&b'#') {
99 buffer.clear();
100 continue;
101 }
102 debug!("line = {:?}", buffer);
103 let mut s = buffer.split(' ');
104 let hosts = s.next();
105 let _ = s.next();
106 let key = s.next();
107 if let (Some(h), Some(k)) = (hosts, key) {
108 debug!("{:?} {:?}", h, k);
109 if match_hostname(&host_port, h) {
110 matches.push((line, parse_public_key_base64(k)?));
111 }
112 }
113 }
114 buffer.clear();
115 line += 1;
116 }
117 Ok(matches)
118}
119
120fn match_hostname(host: &str, pattern: &str) -> bool {
121 for entry in pattern.split(',') {
122 if entry.starts_with("|1|") {
123 let mut parts = entry.split('|').skip(2);
124 let Some(Ok(salt)) = parts.next().map(|p| BASE64_MIME.decode(p.as_bytes())) else {
125 continue;
126 };
127 let Some(Ok(hash)) = parts.next().map(|p| BASE64_MIME.decode(p.as_bytes())) else {
128 continue;
129 };
130 if let Ok(hmac) = Hmac::<Sha1>::new_from_slice(&salt) {
131 if hmac.chain_update(host).verify_slice(&hash).is_ok() {
132 return true;
133 }
134 }
135 } else if host == entry {
136 return true;
137 }
138 }
139 false
140}
141
142pub fn learn_known_hosts(host: &str, port: u16, pubkey: &ssh_key::PublicKey) -> Result<(), Error> {
144 learn_known_hosts_path(host, port, pubkey, known_hosts_path()?)
145}
146
147pub fn learn_known_hosts_path<P: AsRef<Path>>(
149 host: &str,
150 port: u16,
151 pubkey: &ssh_key::PublicKey,
152 path: P,
153) -> Result<(), Error> {
154 if let Some(parent) = path.as_ref().parent() {
155 std::fs::create_dir_all(parent)?
156 }
157 let mut file = OpenOptions::new()
158 .read(true)
159 .append(true)
160 .create(true)
161 .open(path)?;
162
163 let mut buf = [0; 1];
165 let mut ends_in_newline = false;
166 if file.seek(SeekFrom::End(-1)).is_ok() {
167 file.read_exact(&mut buf)?;
168 ends_in_newline = buf[0] == b'\n';
169 }
170
171 file.seek(SeekFrom::End(0))?;
173 let mut file = std::io::BufWriter::new(file);
174 if !ends_in_newline {
175 file.write_all(b"\n")?;
176 }
177 if port != 22 {
178 write!(file, "[{}]:{} ", host, port)?
179 } else {
180 write!(file, "{} ", host)?
181 }
182 file.write_all(pubkey.to_openssh()?.as_bytes())?;
183 file.write_all(b"\n")?;
184 Ok(())
185}
186
187#[cfg(test)]
188mod test {
189 use std::fs::File;
190
191 use super::*;
192 use crate::keys::parse_public_key_base64;
193
194 #[test]
195 fn test_check_known_hosts() {
196 env_logger::try_init().unwrap_or(());
197 let dir = tempfile::tempdir().unwrap();
198 let path = dir.path().join("known_hosts");
199 {
200 let mut f = File::create(&path).unwrap();
201 f.write_all(b"[localhost]:13265 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ\n").unwrap();
202 f.write_all(b"#pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap();
203 f.write_all(b"pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap();
204 f.write_all(b"|1|O33ESRMWPVkMYIwJ1Uw+n877jTo=|nuuC5vEqXlEZ/8BXQR7m619W6Ak= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILIG2T/B0l0gaqj3puu510tu9N1OkQ4znY3LYuEm5zCF\n").unwrap();
205 }
206
207 let host = "localhost";
209 let port = 13265;
210 let hostkey = parse_public_key_base64(
211 "AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ",
212 )
213 .unwrap();
214 assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap());
215
216 let host = "example.com";
218 let port = 22;
219 let hostkey = parse_public_key_base64(
220 "AAAAC3NzaC1lZDI1NTE5AAAAILIG2T/B0l0gaqj3puu510tu9N1OkQ4znY3LYuEm5zCF",
221 )
222 .unwrap();
223 assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap());
224
225 let host = "pijul.org";
227 let port = 22;
228 let hostkey = parse_public_key_base64(
229 "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X",
230 )
231 .unwrap();
232 assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap());
233
234 let host = "pijul.org";
236 let port = 22;
237 let hostkey = parse_public_key_base64(
238 "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X",
239 )
240 .unwrap();
241 assert!(check_known_hosts_path(host, port, &hostkey, &path).is_err());
242 }
243}