1use base64ct::{Base64, Encoding};
4
5use crate::{Error, PublicKey, Result};
6use core::str;
7
8use {
9 alloc::string::{String, ToString},
10 alloc::vec::Vec,
11 core::fmt,
12};
13
14#[cfg(feature = "std")]
15use std::{fs, path::Path};
16
17const COMMENT_DELIMITER: char = '#';
19const MAGIC_HASH_PREFIX: &str = "|1|";
21
22pub struct KnownHosts<'a> {
49 lines: core::str::Lines<'a>,
51}
52
53impl<'a> KnownHosts<'a> {
54 pub fn new(input: &'a str) -> Self {
56 Self {
57 lines: input.lines(),
58 }
59 }
60
61 #[cfg(feature = "std")]
64 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
65 pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
66 let input = fs::read_to_string(path)?;
68 KnownHosts::new(&input).collect()
69 }
70
71 fn next_line_trimmed(&mut self) -> Option<&'a str> {
75 loop {
76 let mut line = self.lines.next()?;
77
78 if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
80 line = l;
81 }
82
83 line = line.trim_end();
85
86 if !line.is_empty() {
87 return Some(line);
88 }
89 }
90 }
91}
92
93impl Iterator for KnownHosts<'_> {
94 type Item = Result<Entry>;
95
96 fn next(&mut self) -> Option<Result<Entry>> {
97 self.next_line_trimmed().map(|line| line.parse())
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct Entry {
104 marker: Option<Marker>,
106
107 host_patterns: HostPatterns,
109
110 public_key: PublicKey,
112}
113
114impl Entry {
115 pub fn marker(&self) -> Option<&Marker> {
117 self.marker.as_ref()
118 }
119
120 pub fn host_patterns(&self) -> &HostPatterns {
122 &self.host_patterns
123 }
124
125 pub fn public_key(&self) -> &PublicKey {
127 &self.public_key
128 }
129}
130impl From<Entry> for Option<Marker> {
131 fn from(entry: Entry) -> Option<Marker> {
132 entry.marker
133 }
134}
135impl From<Entry> for HostPatterns {
136 fn from(entry: Entry) -> HostPatterns {
137 entry.host_patterns
138 }
139}
140impl From<Entry> for PublicKey {
141 fn from(entry: Entry) -> PublicKey {
142 entry.public_key
143 }
144}
145
146impl str::FromStr for Entry {
147 type Err = Error;
148
149 fn from_str(line: &str) -> Result<Self> {
150 let (marker, line) = if line.starts_with('@') {
156 let (marker_str, line) = line.split_once(' ').ok_or(Error::FormatEncoding)?;
157 (Some(marker_str.parse()?), line)
158 } else {
159 (None, line)
160 };
161 let (hosts_str, public_key_str) = line.split_once(' ').ok_or(Error::FormatEncoding)?;
162
163 let host_patterns = hosts_str.parse()?;
164 let public_key = public_key_str.parse()?;
165
166 Ok(Self {
167 marker,
168 host_patterns,
169 public_key,
170 })
171 }
172}
173
174impl ToString for Entry {
175 fn to_string(&self) -> String {
176 let mut s = String::new();
177
178 if let Some(marker) = &self.marker {
179 s.push_str(marker.as_str());
180 s.push(' ');
181 }
182
183 s.push_str(&self.host_patterns.to_string());
184 s.push(' ');
185
186 s.push_str(&self.public_key.to_string());
187 s
188 }
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
195pub enum Marker {
196 CertAuthority,
198 Revoked,
201}
202
203impl Marker {
204 pub fn as_str(&self) -> &str {
206 match self {
207 Self::CertAuthority => "@cert-authority",
208 Self::Revoked => "@revoked",
209 }
210 }
211}
212
213impl AsRef<str> for Marker {
214 fn as_ref(&self) -> &str {
215 self.as_str()
216 }
217}
218
219impl str::FromStr for Marker {
220 type Err = Error;
221
222 fn from_str(s: &str) -> Result<Self> {
223 Ok(match s {
224 "@cert-authority" => Marker::CertAuthority,
225 "@revoked" => Marker::Revoked,
226 _ => return Err(Error::FormatEncoding),
227 })
228 }
229}
230
231impl fmt::Display for Marker {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 f.write_str(self.as_str())
234 }
235}
236
237#[derive(Clone, Debug, Eq, PartialEq)]
244pub enum HostPatterns {
245 Patterns(Vec<String>),
247 HashedName {
249 salt: Vec<u8>,
251 hash: [u8; 20],
253 },
254}
255
256impl str::FromStr for HostPatterns {
257 type Err = Error;
258
259 fn from_str(s: &str) -> Result<Self> {
260 if let Some(s) = s.strip_prefix(MAGIC_HASH_PREFIX) {
261 let mut hash = [0; 20];
262 let (salt, hash_str) = s.split_once('|').ok_or(Error::FormatEncoding)?;
263
264 let salt = Base64::decode_vec(salt)?;
265 Base64::decode(hash_str, &mut hash)?;
266
267 Ok(HostPatterns::HashedName { salt, hash })
268 } else if !s.is_empty() {
269 Ok(HostPatterns::Patterns(
270 s.split_terminator(',').map(str::to_string).collect(),
271 ))
272 } else {
273 Err(Error::FormatEncoding)
274 }
275 }
276}
277
278impl ToString for HostPatterns {
279 fn to_string(&self) -> String {
280 match &self {
281 HostPatterns::Patterns(patterns) => patterns.join(","),
282 HostPatterns::HashedName { salt, hash } => {
283 let salt = Base64::encode_string(salt);
284 let hash = Base64::encode_string(hash);
285 format!("|1|{}|{}", salt, hash)
286 }
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use alloc::string::ToString;
294 use core::str::FromStr;
295
296 use super::Entry;
297 use super::HostPatterns;
298 use super::Marker;
299
300 #[test]
301 fn simple_markers() {
302 assert_eq!(Ok(Marker::CertAuthority), "@cert-authority".parse());
303 assert_eq!(Ok(Marker::Revoked), "@revoked".parse());
304 assert!(Marker::from_str("@gibberish").is_err());
305 }
306
307 #[test]
308 fn empty_host_patterns() {
309 assert!(HostPatterns::from_str("").is_err());
310 }
311
312 #[test]
317 fn single_host_pattern() {
318 assert_eq!(
319 Ok(HostPatterns::Patterns(vec!["cvs.example.net".to_string()])),
320 "cvs.example.net".parse()
321 );
322 }
323 #[test]
324 fn multiple_host_patterns() {
325 assert_eq!(
326 Ok(HostPatterns::Patterns(vec![
327 "cvs.example.net".to_string(),
328 "!test.example.???".to_string(),
329 "[*.example.net]:999".to_string(),
330 ])),
331 "cvs.example.net,!test.example.???,[*.example.net]:999".parse()
332 );
333 }
334 #[test]
335 fn single_hashed_host() {
336 assert_eq!(
337 Ok(HostPatterns::HashedName {
338 salt: vec![
339 37, 242, 147, 116, 24, 123, 172, 214, 215, 145, 80, 16, 9, 26, 120, 57, 10, 15,
340 126, 98
341 ],
342 hash: [
343 81, 33, 2, 175, 116, 150, 127, 82, 84, 62, 201, 172, 228, 10, 159, 15, 148, 31,
344 198, 67
345 ],
346 }),
347 "|1|JfKTdBh7rNbXkVAQCRp4OQoPfmI=|USECr3SWf1JUPsms5AqfD5QfxkM=".parse()
348 );
349 }
350
351 #[test]
352 fn full_line_hashed() {
353 let line = "@revoked |1|lcY/In3lsGnkJikLENb0DM70B/I=|Qs4e9Nr7mM6avuEv02fw2uFnwQo= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9dG4kjRhQTtWTVzd2t27+t0DEHBPW7iOD23TUiYLio comment";
354 let entry = Entry::from_str(line).expect("Valid entry");
355 assert_eq!(entry.marker(), Some(&Marker::Revoked));
356 assert_eq!(
357 entry.host_patterns(),
358 &HostPatterns::HashedName {
359 salt: vec![
360 149, 198, 63, 34, 125, 229, 176, 105, 228, 38, 41, 11, 16, 214, 244, 12, 206,
361 244, 7, 242
362 ],
363 hash: [
364 66, 206, 30, 244, 218, 251, 152, 206, 154, 190, 225, 47, 211, 103, 240, 218,
365 225, 103, 193, 10
366 ],
367 }
368 );
369 }
371}