1use crate::{cache, config, tools, vault::fingerprint};
2use anyhow::{Result, anyhow};
3use reqwest::header::HeaderMap;
4use rsa::RsaPublicKey;
5use ssh_key::{HashAlg, PublicKey};
6use std::collections::HashMap;
7use url::Url;
8
9const GITHUB_BASE_URL: &str = "https://github.com";
10const SSHKEYS_ONLINE: &str = "https://ssh-keys.online/new";
11
12pub fn get_keys(user: &str) -> Result<String> {
18 let mut cache = true;
19
20 let url = if user.starts_with("http://") || user.starts_with("https://") {
21 Url::parse(user)?
22 } else if user == "new" {
23 cache = false;
24
25 let config = config::get()?;
27
28 Url::parse(
29 &config
30 .get_string("sshkeys_online")
31 .unwrap_or_else(|_| String::from(SSHKEYS_ONLINE)),
32 )?
33 } else {
34 Url::parse(&format!("{GITHUB_BASE_URL}/{user}.keys"))?
35 };
36
37 request(url.as_str(), cache)
38}
39
40pub fn request(url: &str, cache: bool) -> Result<String> {
47 let url = Url::parse(url)?;
48
49 let cache_key = format!("{:x}", md5::compute(url.as_str().as_bytes()));
50
51 if let Ok(key) = cache::get(&cache_key) {
53 Ok(key)
54 } else {
55 let headers: HeaderMap = get_headers()?;
57
58 let client = reqwest::blocking::Client::builder()
60 .user_agent("ssh-vault")
61 .default_headers(headers)
62 .build()?;
63
64 let res = client.get(url).send()?;
66
67 if res.status().is_success() {
68 let body = res.text()?;
70
71 if cache {
72 cache::put(&cache_key, &body)?;
73 }
74 Ok(body)
75 } else {
76 Err(anyhow!("Request failed with status: {}", res.status()))
77 }
78 }
79}
80
81fn get_headers() -> Result<HeaderMap> {
83 let mut config_headers: HashMap<String, String> = HashMap::new();
84
85 let config = config::get()?;
87
88 if let Ok(http_headers) = config.get_table("http_headers") {
89 for (key, value) in &http_headers {
90 config_headers.insert(key.clone(), value.to_string());
91 }
92 }
93
94 let headers: HeaderMap = (&config_headers).try_into().unwrap_or_default();
95
96 Ok(headers)
97}
98
99pub fn get_user_key(
105 keys: &str,
106 key: Option<u32>,
107 fingerprint: &Option<String>,
108) -> Result<PublicKey> {
109 let keys = tools::filter_fetched_keys(keys)?;
111
112 let key = key.map_or(0, |mut key| {
113 key = key.saturating_sub(1);
114 key
115 });
116
117 for (id, line) in keys.lines().enumerate() {
118 let u32_id = u32::try_from(id)?;
119 if key >= u32::try_from(keys.lines().count())? {
120 Err(anyhow!(
121 "key index not found, try -k with a value between 1 and {}",
122 keys.lines().count()
123 ))?;
124 }
125
126 if let Ok(public_key) = PublicKey::from_openssh(line) {
128 if let Some(f) = &fingerprint {
130 if public_key.fingerprint(HashAlg::Sha256).to_string() == *f {
131 return Ok(public_key);
132 }
133
134 if let Some(key_data) = public_key.key_data().rsa() {
136 let rsa_public_key = RsaPublicKey::try_from(key_data)?;
137 if fingerprint::md5_fingerprint(&rsa_public_key)?.as_bytes() == f.as_bytes() {
138 return Ok(public_key);
139 }
140 }
141 } else if u32_id == key {
142 return Ok(public_key);
143 }
144 }
145 }
146
147 Err(anyhow!("key not found"))
148}
149#[cfg(test)]
150#[allow(
151 clippy::unwrap_used,
152 clippy::indexing_slicing,
153 clippy::cast_possible_truncation
154)]
155mod tests {
156 use super::*;
157 use crate::vault::fingerprint::Fingerprint;
158 use crate::vault::fingerprint::get_remote_fingerprints;
159
160 const KEYS: &str = "
161# random comment
162ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw==
163# another random comment
164space
165
166# another random comment
167ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXsxWj7gvLUHbkUDzB6g+DfTdJbIcjH5Ge8ZZcYrTFeZ3hFL/pEfsuDf0Ut87QR0QpTFwM8SHyjKAX1rnF10Y+9ezG3Z4btHFk7SVPW0qqBwoTHFYiRqjgOcQrfQoDAhn9p/h93RCHR6gQPwj5CmDMRmnUcPV9mzjiLyqaqecAjGZj6q6O99Z5/lY2It/fCUcNW0JXBc31SiquvkkYhNjQsQgJxI5KnBMUEdVhk3ItJp8XeDbk2Kq03w0L8XcAqS2BUl4nNF4a5eMgME/tCUjSVYMvqcFIpOUsZhYNE+rt0ElbsMuehdvdLCbb2EBt+n75JgfGOsZCd96JrZiPlq55e0r5uDPz0rVtqnAWQawTtmSwa/VY7GZCf/xB2FvuqoXozWpAgzM7pypVx3JTBZwHx0xe/a0m1RA6+laQ4cCKV6FZWPV8WwUcvvxPknbDsjCeXgVQAxlXMk3pYrcGl61IPv/GaOr1QNPtUFRUuQXfgWh0F5SaU5MeI6HSGvuzooM= vault@ssh-vault.online
168---
169ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W
170+++
171ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault
172
173Fin
174";
175
176 fn get_expected() -> Vec<Fingerprint> {
177 vec![
178 Fingerprint {
179 key: "ID: 1".to_string(),
180 fingerprints: vec![
181 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string(),
182 "MD5 55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15".to_string(),
183 ],
184 comment: String::new(),
185 algorithm: "ssh-rsa".to_string(),
186 },
187 Fingerprint {
188 key: "ID: 2".to_string(),
189 fingerprints: vec![
190 "SHA256:O09r+CSX4Ub8S3klaRp86ahCLbBkxhbaXW7v8y/ANCI".to_string(),
191 "MD5 19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58".to_string(),
192 ],
193 comment: "vault@ssh-vault.online".to_string(),
194 algorithm: "ssh-rsa".to_string(),
195 },
196 Fingerprint {
197 key: "ID: 3".to_string(),
198 fingerprints: vec![
199 "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM".to_string(),
200 ],
201 comment: String::new(),
202 algorithm: "ssh-ed25519".to_string(),
203 },
204 Fingerprint {
205 key: "ID: 4".to_string(),
206 fingerprints: vec![
207 "SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8".to_string(),
208 ],
209 comment: "ssh-vault".to_string(),
210 algorithm: "ssh-ed25519".to_string(),
211 },
212 ]
213 }
214
215 #[test]
216 fn test_get_remote_fingerprints() {
217 let f = get_remote_fingerprints(KEYS, None).unwrap();
218 assert_eq!(f, get_expected());
219 }
220
221 #[test]
222 fn test_get_remote_fingerprints_with_key() {
223 for i in 1..=4 {
224 assert_eq!(
225 get_expected()[i - 1],
226 get_remote_fingerprints(KEYS, Some(i as u32)).unwrap()[0]
227 );
228 }
229 }
230
231 #[test]
232 fn test_get_remote_fingerprints_with_key_0_1() {
233 assert_eq!(
235 get_expected()[0],
236 get_remote_fingerprints(KEYS, Some(0)).unwrap()[0]
237 );
238
239 assert_eq!(
240 get_expected()[0],
241 get_remote_fingerprints(KEYS, Some(1)).unwrap()[0]
242 );
243
244 assert_ne!(
246 get_expected()[0],
247 get_remote_fingerprints(KEYS, Some(2)).unwrap()[0]
248 );
249 }
250
251 #[test]
252 fn test_get_remote_fingerprints_with_empty_keys() {
253 assert!(get_remote_fingerprints(KEYS, Some(10)).is_err());
254 assert!(get_remote_fingerprints("", None).is_err());
255 assert!(get_remote_fingerprints("", Some(1)).is_err());
256 }
257
258 #[test]
259 fn test_get_user_key() {
260 let key = get_user_key(KEYS, Some(1), &None).unwrap();
261 assert_eq!(
262 key.fingerprint(HashAlg::Sha256).to_string(),
263 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
264 );
265 }
266
267 #[test]
268 fn test_get_user_key_3() {
269 let key = get_user_key(KEYS, Some(3), &None).unwrap();
270 assert_eq!(
271 key.fingerprint(HashAlg::Sha256).to_string(),
272 "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM".to_string()
273 );
274 }
275
276 #[test]
277 fn test_get_user_key_0_1() {
278 let key = get_user_key(KEYS, None, &None).unwrap();
280 assert_eq!(
281 key.fingerprint(HashAlg::Sha256).to_string(),
282 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
283 );
284 let key = get_user_key(KEYS, Some(0), &None).unwrap();
285 assert_eq!(
286 key.fingerprint(HashAlg::Sha256).to_string(),
287 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
288 );
289 let key = get_user_key(KEYS, Some(1), &None).unwrap();
290 assert_eq!(
291 key.fingerprint(HashAlg::Sha256).to_string(),
292 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
293 );
294 }
295
296 #[test]
297 fn test_get_user_key_with_fingerprint() {
298 let key = get_user_key(
299 KEYS,
300 None,
301 &Some("SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()),
302 )
303 .unwrap();
304 assert_eq!(
305 key.fingerprint(HashAlg::Sha256).to_string(),
306 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
307 );
308 }
309
310 #[test]
311 fn test_get_user_key_with_fingerprint_md5_rsa() {
312 let key = get_user_key(
313 KEYS,
314 None,
315 &Some("55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15".to_string()),
316 )
317 .unwrap();
318 assert_eq!(
319 key.fingerprint(HashAlg::Sha256).to_string(),
320 "SHA256:12mLJQInCFoL9JOPJwPGb/FUEe459PY1yZEZqNGVZtA".to_string()
321 );
322
323 let key = get_user_key(
324 KEYS,
325 None,
326 &Some("19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58".to_string()),
327 )
328 .unwrap();
329 assert_eq!(
330 key.fingerprint(HashAlg::Sha256).to_string(),
331 "SHA256:O09r+CSX4Ub8S3klaRp86ahCLbBkxhbaXW7v8y/ANCI".to_string()
332 );
333 }
334
335 #[test]
336 fn test_get_user_key_with_empty_keys() {
337 assert!(get_user_key("", Some(10), &None).is_err());
338 assert!(get_user_key("", None, &None).is_err());
339 assert!(get_user_key("", Some(1), &None).is_err());
340 }
341
342 #[test]
343 fn test_get_user_key_with_key_out_of_range() {
344 assert!(get_user_key(KEYS, Some(10), &None).is_err());
345 }
346
347 #[test]
348 fn test_get_headers() {
349 let headers = get_headers().unwrap();
350 assert!(headers.is_empty());
351 }
352}