ssh_vault/vault/
fingerprint.rs1use crate::tools;
2use anyhow::{Context, Result};
3use rsa::{RsaPublicKey, pkcs8::EncodePublicKey};
4use ssh_key::{HashAlg, PublicKey};
5use std::{fmt, fs, path::Path};
6
7#[derive(Debug, Default, Clone, PartialEq, Eq)]
8pub struct Fingerprint {
9 pub key: String,
10 pub fingerprints: Vec<String>,
11 pub comment: String,
12 pub algorithm: String,
13}
14
15impl fmt::Display for Fingerprint {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 fn format_fingerprint(fp: &str, width: usize) -> String {
18 format!("{:>width$} {}", "", fp, width = width)
19 }
20
21 let custom_width = f.width().unwrap_or(self.key.len());
23
24 writeln!(
25 f,
26 "{:>width$} Type: {} Comment: {}",
27 self.key,
28 self.algorithm,
29 self.comment,
30 width = custom_width
31 )?;
32
33 for fp in &self.fingerprints {
34 writeln!(f, "{}", format_fingerprint(fp, custom_width))?;
35 }
36
37 Ok(())
38 }
39}
40
41pub fn fingerprints() -> Result<Vec<Fingerprint>> {
47 let mut fingerprints: Vec<Fingerprint> = Vec::new();
49
50 let home = tools::get_home()?;
51 let ssh_home = Path::new(&home).join(".ssh");
52 if let Ok(entries) = fs::read_dir(ssh_home) {
53 for entry in entries.flatten() {
54 let path = entry.path();
55 if let Some(ext) = path.extension()
56 && ext == "pub"
57 && let Ok(key) = PublicKey::read_openssh_file(&path)
58 {
59 let mut fingerprint = Fingerprint {
61 key: path
62 .file_name()
63 .unwrap_or_default()
64 .to_string_lossy()
65 .to_string(),
66 comment: key.comment().to_string(),
67 algorithm: key.algorithm().to_string(),
68 ..Default::default()
69 };
70
71 fingerprint
72 .fingerprints
73 .push(key.fingerprint(HashAlg::Sha256).to_string());
74
75 if let Some(key_data) = key.key_data().rsa() {
76 let rsa_public_key = RsaPublicKey::try_from(key_data)?;
77 fingerprint
78 .fingerprints
79 .push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
80 }
81
82 fingerprints.push(fingerprint);
83 }
84 }
85 }
86 Ok(fingerprints)
87}
88
89pub fn fingerprint(key: &str) -> Result<Fingerprint> {
95 let path = Path::new(&key);
96 let key = PublicKey::read_openssh_file(path)
97 .context("Ensure you are passing a valid openssh public key")?;
98 let mut fingerprint = Fingerprint {
99 key: path
100 .file_name()
101 .unwrap_or_default()
102 .to_string_lossy()
103 .to_string(),
104 comment: key.comment().to_string(),
105 algorithm: key.algorithm().to_string(),
106 ..Default::default()
107 };
108
109 fingerprint
110 .fingerprints
111 .push(key.fingerprint(HashAlg::Sha256).to_string());
112
113 if let Some(key_data) = key.key_data().rsa() {
114 let rsa_public_key = RsaPublicKey::try_from(key_data)?;
115 fingerprint
116 .fingerprints
117 .push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
118 }
119
120 Ok(fingerprint)
121}
122
123pub fn get_remote_fingerprints(keys: &str, key: Option<u32>) -> Result<Vec<Fingerprint>> {
129 let keys = tools::filter_fetched_keys(keys)?;
131
132 let mut fingerprints: Vec<Fingerprint> = Vec::new();
134
135 for (id, line) in keys.lines().enumerate() {
136 let u32_id = u32::try_from(id)?;
137
138 if let Some(mut key) = key {
139 key = key.saturating_sub(1);
140
141 if key >= u32::try_from(keys.lines().count())? {
142 Err(anyhow::anyhow!(
143 "key index not found, try -k with a value between 1 and {}",
144 keys.lines().count()
145 ))?;
146 }
147 if u32_id != key {
148 continue;
149 }
150 }
151
152 if let Ok(key) = PublicKey::from_openssh(line) {
153 let mut fingerprint = Fingerprint {
155 key: format!("ID: {}", id + 1),
156 comment: key.comment().to_string(),
157 algorithm: key.algorithm().to_string(),
158 ..Default::default()
159 };
160
161 fingerprint
162 .fingerprints
163 .push(key.fingerprint(HashAlg::Sha256).to_string());
164
165 if let Some(key_data) = key.key_data().rsa() {
166 let rsa_public_key = RsaPublicKey::try_from(key_data)?;
167 fingerprint
168 .fingerprints
169 .push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
170 }
171
172 fingerprints.push(fingerprint);
173 }
174 }
175
176 Ok(fingerprints)
177}
178
179pub fn md5_fingerprint(public_key: &RsaPublicKey) -> Result<String> {
186 let public_key_der = public_key.to_public_key_der()?;
187 let md5_fingerprint = md5::compute(public_key_der.as_bytes());
188 let formatted_fingerprint = format!("{md5_fingerprint:x}")
189 .chars()
190 .collect::<Vec<char>>()
191 .chunks(2)
192 .map(|chunk| chunk.iter().collect::<String>())
193 .collect::<Vec<String>>()
194 .join(":");
195 Ok(formatted_fingerprint)
196}
197
198#[cfg(test)]
199#[allow(clippy::unwrap_used)]
200mod tests {
201 use super::*;
202
203 struct Test {
204 key: &'static str,
205 fingerprint: &'static str,
206 }
207
208 #[test]
209 fn test_fingerprints() {
210 let tests = [
211 Test {
212 key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw== test",
213 fingerprint: "55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15",
214 },
215 Test {
216 key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXsxWj7gvLUHbkUDzB6g+DfTdJbIcjH5Ge8ZZcYrTFeZ3hFL/pEfsuDf0Ut87QR0QpTFwM8SHyjKAX1rnF10Y+9ezG3Z4btHFk7SVPW0qqBwoTHFYiRqjgOcQrfQoDAhn9p/h93RCHR6gQPwj5CmDMRmnUcPV9mzjiLyqaqecAjGZj6q6O99Z5/lY2It/fCUcNW0JXBc31SiquvkkYhNjQsQgJxI5KnBMUEdVhk3ItJp8XeDbk2Kq03w0L8XcAqS2BUl4nNF4a5eMgME/tCUjSVYMvqcFIpOUsZhYNE+rt0ElbsMuehdvdLCbb2EBt+n75JgfGOsZCd96JrZiPlq55e0r5uDPz0rVtqnAWQawTtmSwa/VY7GZCf/xB2FvuqoXozWpAgzM7pypVx3JTBZwHx0xe/a0m1RA6+laQ4cCKV6FZWPV8WwUcvvxPknbDsjCeXgVQAxlXMk3pYrcGl61IPv/GaOr1QNPtUFRUuQXfgWh0F5SaU5MeI6HSGvuzooM=",
217 fingerprint: "19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58",
218 },
219 Test {
220 key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault",
221 fingerprint: "SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8",
222 },
223 Test {
224 key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W",
225 fingerprint: "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM",
226 },
227 ];
228
229 for test in &tests {
230 let mut fingerprint = String::new();
231 let public_key = PublicKey::from_openssh(test.key).unwrap();
232 if public_key.algorithm().as_str() == "ssh-rsa" {
233 let key_data = public_key.key_data().rsa().unwrap();
234 let rsa_public_key = RsaPublicKey::try_from(key_data).unwrap();
235 fingerprint = md5_fingerprint(&rsa_public_key).unwrap();
236 } else if public_key.algorithm().as_str() == "ssh-ed25519" {
237 fingerprint = public_key.fingerprint(HashAlg::Sha256).to_string();
238 }
239 assert_eq!(fingerprint, test.fingerprint);
240 }
241 }
242}