a73x

src/server/ssh/auth.rs

Ref:   Size: 3.3 KiB

use std::path::Path;

#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct AuthorizedKey {
    pub key_type: String,
    pub key_data: String,
    pub comment: Option<String>,
}

pub fn parse_authorized_keys(content: &str) -> Vec<AuthorizedKey> {
    content
        .lines()
        .filter_map(|line| {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                return None;
            }
            let mut parts = line.splitn(3, ' ');
            let key_type = parts.next()?.to_string();
            let key_data = parts.next()?.to_string();
            let comment = parts.next().map(|s| s.to_string());
            Some(AuthorizedKey { key_type, key_data, comment })
        })
        .collect()
}

pub fn load_authorized_keys(path: &Path) -> Result<Vec<AuthorizedKey>, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    Ok(parse_authorized_keys(&content))
}

fn is_rsa_key_type(key_type: &str) -> bool {
    matches!(key_type, "ssh-rsa" | "rsa-sha2-256" | "rsa-sha2-512")
}

pub fn is_authorized(keys: &[AuthorizedKey], key_type: &str, key_data: &str) -> bool {
    keys.iter().any(|k| {
        let type_matches = k.key_type == key_type
            || (is_rsa_key_type(&k.key_type) && is_rsa_key_type(key_type));
        type_matches && k.key_data == key_data
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_single_key() {
        let content = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest user@host\n";
        let keys = parse_authorized_keys(content);
        assert_eq!(keys.len(), 1);
        assert_eq!(keys[0].key_type, "ssh-ed25519");
        assert_eq!(keys[0].key_data, "AAAAC3NzaC1lZDI1NTE5AAAAITest");
        assert_eq!(keys[0].comment.as_deref(), Some("user@host"));
    }

    #[test]
    fn parse_multiple_keys() {
        let content = "# alice\nssh-ed25519 AAAA1111 alice@example.com\n\n# bob\nssh-ed25519 AAAA2222 bob@work\n";
        let keys = parse_authorized_keys(content);
        assert_eq!(keys.len(), 2);
        assert_eq!(keys[0].key_data, "AAAA1111");
        assert_eq!(keys[1].key_data, "AAAA2222");
    }

    #[test]
    fn skip_comments_and_blanks() {
        let content = "# this is a comment\n\n   # indented comment\n\nssh-ed25519 AAAAkey user\n";
        let keys = parse_authorized_keys(content);
        assert_eq!(keys.len(), 1);
    }

    #[test]
    fn key_without_comment() {
        let content = "ssh-ed25519 AAAAnocomment\n";
        let keys = parse_authorized_keys(content);
        assert_eq!(keys.len(), 1);
        assert!(keys[0].comment.is_none());
    }

    #[test]
    fn is_authorized_matches() {
        let keys = parse_authorized_keys("ssh-ed25519 AAAA1111 alice\nssh-ed25519 AAAA2222 bob\n");
        assert!(is_authorized(&keys, "ssh-ed25519", "AAAA1111"));
        assert!(is_authorized(&keys, "ssh-ed25519", "AAAA2222"));
        assert!(!is_authorized(&keys, "ssh-ed25519", "AAAA9999"));
        assert!(!is_authorized(&keys, "ssh-rsa", "AAAA1111"));
    }

    #[test]
    fn rsa_key_type_variants_match() {
        let keys = parse_authorized_keys("ssh-rsa AAAARSA user\n");
        assert!(is_authorized(&keys, "ssh-rsa", "AAAARSA"));
        assert!(is_authorized(&keys, "rsa-sha2-256", "AAAARSA"));
        assert!(is_authorized(&keys, "rsa-sha2-512", "AAAARSA"));
        assert!(!is_authorized(&keys, "rsa-sha2-256", "WRONG"));
    }
}