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
172impl ToString for Entry {
173 fn to_string(&self) -> String {
174 let mut s = String::new();
175
176 if let Some(marker) = &self.marker {
177 s.push_str(marker.as_str());
178 s.push(' ');
179 }
180
181 s.push_str(&self.host_patterns.to_string());
182 s.push(' ');
183
184 s.push_str(&self.public_key.to_string());
185 s
186 }
187}
188
189#[derive(Clone, Debug, Eq, PartialEq)]
193pub enum Marker {
194 CertAuthority,
196 Revoked,
199}
200
201impl Marker {
202 pub fn as_str(&self) -> &str {
204 match self {
205 Self::CertAuthority => "@cert-authority",
206 Self::Revoked => "@revoked",
207 }
208 }
209}
210
211impl AsRef<str> for Marker {
212 fn as_ref(&self) -> &str {
213 self.as_str()
214 }
215}
216
217impl str::FromStr for Marker {
218 type Err = Error;
219
220 fn from_str(s: &str) -> Result<Self> {
221 Ok(match s {
222 "@cert-authority" => Marker::CertAuthority,
223 "@revoked" => Marker::Revoked,
224 _ => return Err(Error::FormatEncoding),
225 })
226 }
227}
228
229impl fmt::Display for Marker {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 f.write_str(self.as_str())
232 }
233}
234
235#[derive(Clone, Debug, Eq, PartialEq)]
242pub enum HostPatterns {
243 Patterns(Vec<String>),
245 HashedName {
247 salt: Vec<u8>,
249 hash: [u8; 20],
251 },
252}
253
254impl str::FromStr for HostPatterns {
255 type Err = Error;
256
257 fn from_str(s: &str) -> Result<Self> {
258 if let Some(s) = s.strip_prefix(MAGIC_HASH_PREFIX) {
259 let mut hash = [0; 20];
260 let (salt, hash_str) = s.split_once('|').ok_or(Error::FormatEncoding)?;
261
262 let salt = Base64::decode_vec(salt)?;
263 Base64::decode(hash_str, &mut hash)?;
264
265 Ok(HostPatterns::HashedName { salt, hash })
266 } else if !s.is_empty() {
267 Ok(HostPatterns::Patterns(
268 s.split_terminator(',').map(str::to_string).collect(),
269 ))
270 } else {
271 Err(Error::FormatEncoding)
272 }
273 }
274}
275
276impl ToString for HostPatterns {
277 fn to_string(&self) -> String {
278 match &self {
279 HostPatterns::Patterns(patterns) => patterns.join(","),
280 HostPatterns::HashedName { salt, hash } => {
281 let salt = Base64::encode_string(salt);
282 let hash = Base64::encode_string(hash);
283 format!("|1|{salt}|{hash}")
284 }
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use alloc::string::ToString;
292 use core::str::FromStr;
293
294 use super::Entry;
295 use super::HostPatterns;
296 use super::Marker;
297
298 #[test]
299 fn simple_markers() {
300 assert_eq!(Ok(Marker::CertAuthority), "@cert-authority".parse());
301 assert_eq!(Ok(Marker::Revoked), "@revoked".parse());
302 assert!(Marker::from_str("@gibberish").is_err());
303 }
304
305 #[test]
306 fn empty_host_patterns() {
307 assert!(HostPatterns::from_str("").is_err());
308 }
309
310 #[test]
315 fn single_host_pattern() {
316 assert_eq!(
317 Ok(HostPatterns::Patterns(vec!["cvs.example.net".to_string()])),
318 "cvs.example.net".parse()
319 );
320 }
321 #[test]
322 fn multiple_host_patterns() {
323 assert_eq!(
324 Ok(HostPatterns::Patterns(vec![
325 "cvs.example.net".to_string(),
326 "!test.example.???".to_string(),
327 "[*.example.net]:999".to_string(),
328 ])),
329 "cvs.example.net,!test.example.???,[*.example.net]:999".parse()
330 );
331 }
332 #[test]
333 fn single_hashed_host() {
334 assert_eq!(
335 Ok(HostPatterns::HashedName {
336 salt: vec![
337 37, 242, 147, 116, 24, 123, 172, 214, 215, 145, 80, 16, 9, 26, 120, 57, 10, 15,
338 126, 98
339 ],
340 hash: [
341 81, 33, 2, 175, 116, 150, 127, 82, 84, 62, 201, 172, 228, 10, 159, 15, 148, 31,
342 198, 67
343 ],
344 }),
345 "|1|JfKTdBh7rNbXkVAQCRp4OQoPfmI=|USECr3SWf1JUPsms5AqfD5QfxkM=".parse()
346 );
347 }
348
349 #[test]
350 fn full_line_hashed() {
351 let line = "@revoked |1|lcY/In3lsGnkJikLENb0DM70B/I=|Qs4e9Nr7mM6avuEv02fw2uFnwQo= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9dG4kjRhQTtWTVzd2t27+t0DEHBPW7iOD23TUiYLio comment";
352 let entry = Entry::from_str(line).expect("Valid entry");
353 assert_eq!(entry.marker(), Some(&Marker::Revoked));
354 assert_eq!(
355 entry.host_patterns(),
356 &HostPatterns::HashedName {
357 salt: vec![
358 149, 198, 63, 34, 125, 229, 176, 105, 228, 38, 41, 11, 16, 214, 244, 12, 206,
359 244, 7, 242
360 ],
361 hash: [
362 66, 206, 30, 244, 218, 251, 152, 206, 154, 190, 225, 47, 211, 103, 240, 218,
363 225, 103, 193, 10
364 ],
365 }
366 );
367 }
369}