use crate::{Error, PublicKey, Result};
use core::str;
use encoding::base64::{Base64, Encoding};
use {
alloc::string::{String, ToString},
alloc::vec::Vec,
core::fmt,
};
#[cfg(feature = "std")]
use std::{fs, path::Path};
const COMMENT_DELIMITER: char = '#';
const MAGIC_HASH_PREFIX: &str = "|1|";
pub struct KnownHosts<'a> {
lines: str::Lines<'a>,
}
impl<'a> KnownHosts<'a> {
pub fn new(input: &'a str) -> Self {
Self {
lines: input.lines(),
}
}
#[cfg(feature = "std")]
pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
let input = fs::read_to_string(path)?;
KnownHosts::new(&input).collect()
}
fn next_line_trimmed(&mut self) -> Option<&'a str> {
loop {
let mut line = self.lines.next()?;
if let Some((l, _)) = line.split_once(COMMENT_DELIMITER) {
line = l;
}
line = line.trim_end();
if !line.is_empty() {
return Some(line);
}
}
}
}
impl Iterator for KnownHosts<'_> {
type Item = Result<Entry>;
fn next(&mut self) -> Option<Result<Entry>> {
self.next_line_trimmed().map(|line| line.parse())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Entry {
marker: Option<Marker>,
host_patterns: HostPatterns,
public_key: PublicKey,
}
impl Entry {
pub fn marker(&self) -> Option<&Marker> {
self.marker.as_ref()
}
pub fn host_patterns(&self) -> &HostPatterns {
&self.host_patterns
}
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
}
impl From<Entry> for Option<Marker> {
fn from(entry: Entry) -> Option<Marker> {
entry.marker
}
}
impl From<Entry> for HostPatterns {
fn from(entry: Entry) -> HostPatterns {
entry.host_patterns
}
}
impl From<Entry> for PublicKey {
fn from(entry: Entry) -> PublicKey {
entry.public_key
}
}
impl str::FromStr for Entry {
type Err = Error;
fn from_str(line: &str) -> Result<Self> {
let (marker, line) = if line.starts_with('@') {
let (marker_str, line) = line.split_once(' ').ok_or(Error::FormatEncoding)?;
(Some(marker_str.parse()?), line)
} else {
(None, line)
};
let (hosts_str, public_key_str) = line.split_once(' ').ok_or(Error::FormatEncoding)?;
let host_patterns = hosts_str.parse()?;
let public_key = public_key_str.parse()?;
Ok(Self {
marker,
host_patterns,
public_key,
})
}
}
impl ToString for Entry {
fn to_string(&self) -> String {
let mut s = String::new();
if let Some(marker) = &self.marker {
s.push_str(marker.as_str());
s.push(' ');
}
s.push_str(&self.host_patterns.to_string());
s.push(' ');
s.push_str(&self.public_key.to_string());
s
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Marker {
CertAuthority,
Revoked,
}
impl Marker {
pub fn as_str(&self) -> &str {
match self {
Self::CertAuthority => "@cert-authority",
Self::Revoked => "@revoked",
}
}
}
impl AsRef<str> for Marker {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl str::FromStr for Marker {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Ok(match s {
"@cert-authority" => Marker::CertAuthority,
"@revoked" => Marker::Revoked,
_ => return Err(Error::FormatEncoding),
})
}
}
impl fmt::Display for Marker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HostPatterns {
Patterns(Vec<String>),
HashedName {
salt: Vec<u8>,
hash: [u8; 20],
},
}
impl str::FromStr for HostPatterns {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if let Some(s) = s.strip_prefix(MAGIC_HASH_PREFIX) {
let mut hash = [0; 20];
let (salt, hash_str) = s.split_once('|').ok_or(Error::FormatEncoding)?;
let salt = Base64::decode_vec(salt)?;
Base64::decode(hash_str, &mut hash)?;
Ok(HostPatterns::HashedName { salt, hash })
} else if !s.is_empty() {
Ok(HostPatterns::Patterns(
s.split_terminator(',').map(str::to_string).collect(),
))
} else {
Err(Error::FormatEncoding)
}
}
}
impl ToString for HostPatterns {
fn to_string(&self) -> String {
match &self {
HostPatterns::Patterns(patterns) => patterns.join(","),
HostPatterns::HashedName { salt, hash } => {
let salt = Base64::encode_string(salt);
let hash = Base64::encode_string(hash);
format!("|1|{salt}|{hash}")
}
}
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use core::str::FromStr;
use super::Entry;
use super::HostPatterns;
use super::Marker;
#[test]
fn simple_markers() {
assert_eq!(Ok(Marker::CertAuthority), "@cert-authority".parse());
assert_eq!(Ok(Marker::Revoked), "@revoked".parse());
assert!(Marker::from_str("@gibberish").is_err());
}
#[test]
fn empty_host_patterns() {
assert!(HostPatterns::from_str("").is_err());
}
#[test]
fn single_host_pattern() {
assert_eq!(
Ok(HostPatterns::Patterns(vec!["cvs.example.net".to_string()])),
"cvs.example.net".parse()
);
}
#[test]
fn multiple_host_patterns() {
assert_eq!(
Ok(HostPatterns::Patterns(vec![
"cvs.example.net".to_string(),
"!test.example.???".to_string(),
"[*.example.net]:999".to_string(),
])),
"cvs.example.net,!test.example.???,[*.example.net]:999".parse()
);
}
#[test]
fn single_hashed_host() {
assert_eq!(
Ok(HostPatterns::HashedName {
salt: vec![
37, 242, 147, 116, 24, 123, 172, 214, 215, 145, 80, 16, 9, 26, 120, 57, 10, 15,
126, 98
],
hash: [
81, 33, 2, 175, 116, 150, 127, 82, 84, 62, 201, 172, 228, 10, 159, 15, 148, 31,
198, 67
],
}),
"|1|JfKTdBh7rNbXkVAQCRp4OQoPfmI=|USECr3SWf1JUPsms5AqfD5QfxkM=".parse()
);
}
#[test]
fn full_line_hashed() {
let line = "@revoked |1|lcY/In3lsGnkJikLENb0DM70B/I=|Qs4e9Nr7mM6avuEv02fw2uFnwQo= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9dG4kjRhQTtWTVzd2t27+t0DEHBPW7iOD23TUiYLio comment";
let entry = Entry::from_str(line).expect("Valid entry");
assert_eq!(entry.marker(), Some(&Marker::Revoked));
assert_eq!(
entry.host_patterns(),
&HostPatterns::HashedName {
salt: vec![
149, 198, 63, 34, 125, 229, 176, 105, 228, 38, 41, 11, 16, 214, 244, 12, 206,
244, 7, 242
],
hash: [
66, 206, 30, 244, 218, 251, 152, 206, 154, 190, 225, 47, 211, 103, 240, 218,
225, 103, 193, 10
],
}
);
}
}