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