1use crate::{Error, PublicKey, Result};
4use core::str;
5use encoding::base64::{Base64, Encoding};
6
7use {
8 alloc::string::{String, ToString},
9 alloc::vec::Vec,
10 core::fmt,
11};
12
13#[cfg(feature = "std")]
14use std::{fs, path::Path};
15
16const COMMENT_DELIMITER: char = '#';
18const MAGIC_HASH_PREFIX: &str = "|1|";
20
21pub struct KnownHosts<'a> {
48 lines: str::Lines<'a>,
50}
51
52impl<'a> KnownHosts<'a> {
53 pub fn new(input: &'a str) -> Self {
55 Self {
56 lines: input.lines(),
57 }
58 }
59
60 #[cfg(feature = "std")]
63 pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
64 let input = fs::read_to_string(path)?;
66 KnownHosts::new(&input).collect()
67 }
68
69 fn next_line_trimmed(&mut self) -> Option<&'a str> {
73 loop {
74 let mut line = self.lines.next()?;
75
76 if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
78 line = l;
79 }
80
81 line = line.trim_end();
83
84 if !line.is_empty() {
85 return Some(line);
86 }
87 }
88 }
89}
90
91impl Iterator for KnownHosts<'_> {
92 type Item = Result<Entry>;
93
94 fn next(&mut self) -> Option<Result<Entry>> {
95 self.next_line_trimmed().map(|line| line.parse())
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct Entry {
102 marker: Option<Marker>,
104
105 host_patterns: HostPatterns,
107
108 public_key: PublicKey,
110}
111
112impl Entry {
113 pub fn marker(&self) -> Option<&Marker> {
115 self.marker.as_ref()
116 }
117
118 pub fn host_patterns(&self) -> &HostPatterns {
120 &self.host_patterns
121 }
122
123 pub fn public_key(&self) -> &PublicKey {
125 &self.public_key
126 }
127}
128impl From<Entry> for Option<Marker> {
129 fn from(entry: Entry) -> Option<Marker> {
130 entry.marker
131 }
132}
133impl From<Entry> for HostPatterns {
134 fn from(entry: Entry) -> HostPatterns {
135 entry.host_patterns
136 }
137}
138impl From<Entry> for PublicKey {
139 fn from(entry: Entry) -> PublicKey {
140 entry.public_key
141 }
142}
143
144impl str::FromStr for Entry {
145 type Err = Error;
146
147 fn from_str(line: &str) -> Result<Self> {
148 let (marker, line) = if line.starts_with('@') {
154 let (marker_str, line) = line.split_once(' ').ok_or(Error::FormatEncoding)?;
155 (Some(marker_str.parse()?), line)
156 } else {
157 (None, line)
158 };
159 let (hosts_str, public_key_str) = line.split_once(' ').ok_or(Error::FormatEncoding)?;
160
161 let host_patterns = hosts_str.parse()?;
162 let public_key = public_key_str.parse()?;
163
164 Ok(Self {
165 marker,
166 host_patterns,
167 public_key,
168 })
169 }
170}
171
172#[allow(clippy::to_string_trait_impl)]
173impl ToString for Entry {
174 fn to_string(&self) -> String {
175 let mut s = String::new();
176
177 if let Some(marker) = &self.marker {
178 s.push_str(marker.as_str());
179 s.push(' ');
180 }
181
182 s.push_str(&self.host_patterns.to_string());
183 s.push(' ');
184
185 s.push_str(&self.public_key.to_string());
186 s
187 }
188}
189
190#[derive(Clone, Debug, Eq, PartialEq)]
194pub enum Marker {
195 CertAuthority,
197 Revoked,
200}
201
202impl Marker {
203 pub fn as_str(&self) -> &str {
205 match self {
206 Self::CertAuthority => "@cert-authority",
207 Self::Revoked => "@revoked",
208 }
209 }
210}
211
212impl AsRef<str> for Marker {
213 fn as_ref(&self) -> &str {
214 self.as_str()
215 }
216}
217
218impl str::FromStr for Marker {
219 type Err = Error;
220
221 fn from_str(s: &str) -> Result<Self> {
222 Ok(match s {
223 "@cert-authority" => Marker::CertAuthority,
224 "@revoked" => Marker::Revoked,
225 _ => return Err(Error::FormatEncoding),
226 })
227 }
228}
229
230impl fmt::Display for Marker {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 f.write_str(self.as_str())
233 }
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
243pub enum HostPatterns {
244 Patterns(Vec<String>),
246 HashedName {
248 salt: Vec<u8>,
250 hash: [u8; 20],
252 },
253}
254
255impl str::FromStr for HostPatterns {
256 type Err = Error;
257
258 fn from_str(s: &str) -> Result<Self> {
259 if let Some(s) = s.strip_prefix(MAGIC_HASH_PREFIX) {
260 let mut hash = [0; 20];
261 let (salt, hash_str) = s.split_once('|').ok_or(Error::FormatEncoding)?;
262
263 let salt = Base64::decode_vec(salt)?;
264 Base64::decode(hash_str, &mut hash)?;
265
266 Ok(HostPatterns::HashedName { salt, hash })
267 } else if !s.is_empty() {
268 Ok(HostPatterns::Patterns(
269 s.split_terminator(',').map(str::to_string).collect(),
270 ))
271 } else {
272 Err(Error::FormatEncoding)
273 }
274 }
275}
276
277#[allow(clippy::to_string_trait_impl)]
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}