1use crate::crypto::{self, MurkRecipient};
4
5#[derive(Debug)]
7pub enum GitHubError {
8 Fetch(String),
10 NoKeys(String),
12}
13
14impl std::fmt::Display for GitHubError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 GitHubError::Fetch(msg) => write!(f, "GitHub key fetch failed: {msg}"),
18 GitHubError::NoKeys(user) => write!(
19 f,
20 "no supported SSH keys found for GitHub user {user} (need ed25519 or rsa)"
21 ),
22 }
23 }
24}
25
26pub fn fetch_keys(username: &str) -> Result<Vec<(MurkRecipient, String)>, GitHubError> {
35 let url = format!("https://github.com/{username}.keys");
36
37 let body = ureq::get(&url)
38 .call()
39 .map_err(|e| GitHubError::Fetch(format!("{url}: {e}")))?
40 .into_body()
41 .read_to_string()
42 .map_err(|e| GitHubError::Fetch(format!("reading response: {e}")))?;
43
44 if body.trim().is_empty() {
45 return Err(GitHubError::NoKeys(username.into()));
46 }
47
48 let mut keys = Vec::new();
49 for line in body.lines() {
50 let line = line.trim();
51 if line.is_empty() {
52 continue;
53 }
54
55 let key_type = line.split_whitespace().next().unwrap_or("");
57
58 if key_type != "ssh-ed25519" && key_type != "ssh-rsa" {
60 continue;
61 }
62
63 if let Ok(recipient) = crypto::parse_recipient(line) {
65 let normalized = match &recipient {
67 MurkRecipient::Ssh(r) => r.to_string(),
68 MurkRecipient::Age(_) => unreachable!("SSH key parsed as age key"),
69 };
70 keys.push((recipient, normalized));
71 }
72 }
73
74 if keys.is_empty() {
75 return Err(GitHubError::NoKeys(username.into()));
76 }
77
78 Ok(keys)
79}
80
81pub fn key_type_label(key_string: &str) -> &str {
86 key_string.split_whitespace().next().unwrap_or("ssh")
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn key_type_label_ed25519() {
95 assert_eq!(
96 key_type_label("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."),
97 "ssh-ed25519"
98 );
99 }
100
101 #[test]
102 fn key_type_label_rsa() {
103 assert_eq!(key_type_label("ssh-rsa AAAAB3NzaC1yc2EAAAA..."), "ssh-rsa");
104 }
105
106 #[test]
107 fn key_type_label_empty() {
108 assert_eq!(key_type_label(""), "ssh");
109 }
110}