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 if !username
37 .chars()
38 .all(|c| c.is_ascii_alphanumeric() || c == '-')
39 || username.is_empty()
40 {
41 return Err(GitHubError::Fetch(format!(
42 "invalid GitHub username: {username}"
43 )));
44 }
45
46 let url = format!("https://github.com/{username}.keys");
47
48 let body = ureq::get(&url)
49 .call()
50 .map_err(|e| GitHubError::Fetch(format!("{url}: {e}")))?
51 .into_body()
52 .read_to_string()
53 .map_err(|e| GitHubError::Fetch(format!("reading response: {e}")))?;
54
55 if body.trim().is_empty() {
56 return Err(GitHubError::NoKeys(username.into()));
57 }
58
59 let mut keys = Vec::new();
60 for line in body.lines() {
61 let line = line.trim();
62 if line.is_empty() {
63 continue;
64 }
65
66 let key_type = line.split_whitespace().next().unwrap_or("");
68
69 if key_type != "ssh-ed25519" && key_type != "ssh-rsa" {
71 continue;
72 }
73
74 if let Ok(recipient) = crypto::parse_recipient(line) {
76 let normalized = match &recipient {
78 MurkRecipient::Ssh(r) => r.to_string(),
79 MurkRecipient::Age(_) => unreachable!("SSH key parsed as age key"),
80 };
81 keys.push((recipient, normalized));
82 }
83 }
84
85 if keys.is_empty() {
86 return Err(GitHubError::NoKeys(username.into()));
87 }
88
89 Ok(keys)
90}
91
92pub fn key_type_label(key_string: &str) -> &str {
97 key_string.split_whitespace().next().unwrap_or("ssh")
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn key_type_label_ed25519() {
106 assert_eq!(
107 key_type_label("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."),
108 "ssh-ed25519"
109 );
110 }
111
112 #[test]
113 fn key_type_label_rsa() {
114 assert_eq!(key_type_label("ssh-rsa AAAAB3NzaC1yc2EAAAA..."), "ssh-rsa");
115 }
116
117 #[test]
118 fn key_type_label_empty() {
119 assert_eq!(key_type_label(""), "ssh");
120 }
121}