radicle_node/
fingerprint.rs1use thiserror::Error;
23
24use radicle::crypto;
25use radicle::profile::Home;
26
27#[derive(Debug, PartialEq)]
29pub struct Fingerprint(String);
30
31impl std::fmt::Display for Fingerprint {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "{}", self.0)
34 }
35}
36
37#[derive(Debug, PartialEq, Eq)]
38pub enum FingerprintVerification {
39 Match,
40 Mismatch,
41}
42
43#[derive(Error, Debug)]
44pub enum Error {
45 #[error(transparent)]
46 Io(#[from] std::io::Error),
47
48 #[error("fingerprint file is not valid UTF-8: {0}")]
49 Utf8(#[from] std::str::Utf8Error),
50}
51
52impl Fingerprint {
53 pub fn read(home: &Home) -> Result<Option<Fingerprint>, Error> {
55 match std::fs::read(path(home)) {
56 Ok(contents) => Ok(Some(Fingerprint(
57 String::from(std::str::from_utf8(contents.as_ref())?)
58 .trim_end()
59 .to_string(),
60 ))),
61 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
62 Err(err) => Err(Error::Io(err)),
63 }
64 }
65
66 pub fn init(
68 home: &Home,
69 secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
70 ) -> Result<(), Error> {
71 let public_key = crypto::PublicKey(secret_key.deref().public_key());
72 let mut file = std::fs::OpenOptions::new()
73 .create_new(true)
74 .write(true)
75 .open(path(home))?;
76 {
77 use std::io::Write as _;
78 file.write_all(crypto::ssh::fmt::fingerprint(&public_key).as_ref())?;
79 }
80
81 Ok(())
82 }
83
84 pub fn verify(
86 &self,
87 secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
88 ) -> FingerprintVerification {
89 let public_key = crypto::PublicKey(secret_key.deref().public_key());
90 if crypto::ssh::fmt::fingerprint(&public_key) == self.0 {
91 FingerprintVerification::Match
92 } else {
93 FingerprintVerification::Mismatch
94 }
95 }
96}
97
98fn path(home: &Home) -> std::path::PathBuf {
100 home.node().join("fingerprint")
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 use crypto::ssh::Keystore;
108
109 #[test]
110 fn matching() {
111 let tmp = tempfile::tempdir().unwrap();
112 let home = Home::new(tmp.path()).unwrap();
113
114 let store = Keystore::new(&home.keys());
115 store.init("test 1", None, crypto::Seed::default()).unwrap();
116 let secret = store.secret_key(None).unwrap().unwrap();
117
118 assert_eq!(Fingerprint::read(&home).unwrap(), None);
119 Fingerprint::init(&home, &secret).unwrap();
120
121 let fp = Fingerprint::read(&home).unwrap().unwrap();
122 assert_eq!(fp.verify(&secret), FingerprintVerification::Match);
123
124 std::fs::remove_dir_all(home.keys()).unwrap();
127 store.init("test 1", None, crypto::Seed::default()).unwrap();
128 let other_secret = store.secret_key(None).unwrap().unwrap();
129
130 assert_ne!(secret, other_secret);
131 assert_eq!(fp.verify(&other_secret), FingerprintVerification::Mismatch);
133 }
134}