wash_lib/keys/
fs.rs

1//! A filesystem directory based implementation of a `KeyManager`
2
3use std::{
4    ops::Deref,
5    path::{Path, PathBuf},
6};
7
8use anyhow::Result;
9use nkeys::KeyPair;
10
11use super::KeyManager;
12
13pub const KEY_FILE_EXTENSION: &str = "nk";
14
15pub struct KeyDir(PathBuf);
16
17impl AsRef<Path> for KeyDir {
18    fn as_ref(&self) -> &Path {
19        &self.0
20    }
21}
22
23impl Deref for KeyDir {
24    type Target = Path;
25
26    fn deref(&self) -> &Self::Target {
27        &self.0
28    }
29}
30
31impl KeyDir {
32    /// Creates a new `KeyDir`, erroring if it is unable to access or create the given directory.
33    pub fn new(path: impl AsRef<Path>) -> Result<KeyDir> {
34        let p = path.as_ref();
35        let exists = p.exists();
36        if exists && !p.is_dir() {
37            anyhow::bail!("{} is not a directory (or cannot be accessed)", p.display())
38        } else if !exists {
39            std::fs::create_dir_all(p)?;
40        }
41        // Always ensure the directory has the proper permissions, even if it exists
42        set_permissions_keys(p)?;
43        // Make sure we have the fully qualified path at this point
44        Ok(KeyDir(p.canonicalize()?))
45    }
46
47    /// Returns a list of paths to all keyfiles in the directory
48    pub fn list_paths(&self) -> Result<Vec<PathBuf>> {
49        let paths = std::fs::read_dir(&self.0)?;
50
51        Ok(paths
52            .filter_map(|p| {
53                if let Ok(entry) = p {
54                    let path = entry.path();
55                    match path.extension().map(|os| os.to_str()).unwrap_or_default() {
56                        Some(KEY_FILE_EXTENSION) => Some(path),
57                        _ => None,
58                    }
59                } else {
60                    None
61                }
62            })
63            .collect())
64    }
65
66    fn generate_file_path(&self, name: &str) -> PathBuf {
67        self.0.join(format!("{name}.{KEY_FILE_EXTENSION}"))
68    }
69}
70
71impl KeyManager for KeyDir {
72    fn get(&self, name: &str) -> Result<Option<KeyPair>> {
73        let path = self.generate_file_path(name);
74        match read_key(path) {
75            Ok(k) => Ok(Some(k)),
76            Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => Ok(None),
77            Err(e) => Err(anyhow::anyhow!("Unable to load key from disk: {}", e)),
78        }
79    }
80
81    fn list_names(&self) -> Result<Vec<String>> {
82        Ok(self
83            .list_paths()?
84            .into_iter()
85            .filter_map(|p| {
86                p.file_stem()
87                    .unwrap_or_default()
88                    .to_os_string()
89                    .into_string()
90                    .ok()
91            })
92            .collect())
93    }
94
95    fn list(&self) -> Result<Vec<KeyPair>> {
96        self.list_paths()?
97            .into_iter()
98            .map(|p| {
99                read_key(p).map_err(|e| anyhow::anyhow!("Unable to load key from disk: {}", e))
100            })
101            .collect()
102    }
103
104    fn delete(&self, name: &str) -> Result<()> {
105        match std::fs::remove_file(self.generate_file_path(name)) {
106            Ok(()) => Ok(()),
107            Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => Ok(()),
108            Err(e) => Err(anyhow::anyhow!("Unable to delete key from disk: {}", e)),
109        }
110    }
111
112    fn save(&self, name: &str, key: &KeyPair) -> Result<()> {
113        let path = self.generate_file_path(name);
114        std::fs::write(&path, key.seed()?.as_bytes())
115            .map_err(|e| anyhow::anyhow!("Unable to write key to disk: {}", e))?;
116        set_permissions_keys(path)
117    }
118}
119
120/// Helper function for reading a key from disk
121pub fn read_key(p: impl AsRef<Path>) -> std::io::Result<KeyPair> {
122    let raw = std::fs::read_to_string(p)?;
123
124    KeyPair::from_seed(&raw).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
125}
126
127#[cfg(unix)]
128/// Set file and folder permissions for keys.
129fn set_permissions_keys(path: impl AsRef<Path>) -> Result<()> {
130    use std::os::unix::fs::PermissionsExt;
131
132    let metadata = path.as_ref().metadata()?;
133    match metadata.file_type().is_dir() {
134        true => std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?,
135        false => std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?,
136    };
137    Ok(())
138}
139
140#[cfg(target_os = "windows")]
141fn set_permissions_keys(_path: impl AsRef<Path>) -> Result<()> {
142    Ok(())
143}
144
145#[cfg(test)]
146mod test {
147    use nkeys::KeyPairType;
148
149    use super::*;
150
151    const TEST_KEY: &str = "SMAAGJ4DY4FNV4VJWA6QU7UQIL7DKJR4Z3UH7NBMNTH22V6VEIJGJUBQN4";
152
153    #[test]
154    fn round_trip_happy_path() {
155        let tempdir = tempfile::tempdir().expect("Unable to create temp dir");
156        let key_dir = KeyDir::new(&tempdir).expect("Should be able to create key dir");
157
158        let key1 = KeyPair::new(KeyPairType::Account);
159        let key2 = KeyPair::new(KeyPairType::Module);
160
161        key_dir
162            .save("foobar_account", &key1)
163            .expect("Should be able to save key");
164        key_dir
165            .save("foobar_module", &key2)
166            .expect("Should be able to save key");
167
168        assert_eq!(
169            tempdir.path().read_dir().unwrap().count(),
170            2,
171            "Directory should have 2 entries"
172        );
173
174        let names = key_dir.list_names().expect("Should be able to list names");
175        assert_eq!(names.len(), 2, "Should have listed 2 names");
176        for name in names {
177            assert!(
178                name == "foobar_account" || name == "foobar_module",
179                "Should only have the newly created keys in the list"
180            );
181        }
182
183        let key = key_dir
184            .get("foobar_module")
185            .expect("Shouldn't error while reading key")
186            .expect("Key should exist");
187        assert_eq!(
188            key.public_key(),
189            key2.public_key(),
190            "Should have fetched the right key from disk"
191        );
192
193        assert_eq!(
194            key_dir
195                .list()
196                .expect("Should be able to load all keys")
197                .len(),
198            2,
199            "Should have loaded 2 keys from disk"
200        );
201
202        key_dir
203            .delete("foobar_account")
204            .expect("Should be able to delete key");
205        assert_eq!(
206            tempdir.path().read_dir().unwrap().count(),
207            1,
208            "Directory should have 1 entry"
209        );
210    }
211
212    #[test]
213    fn can_read_existing() {
214        let tempdir = tempfile::tempdir().expect("Unable to create temp dir");
215        std::fs::write(tempdir.path().join("foobar_module.nk"), TEST_KEY)
216            .expect("Unable to write test file");
217        // Write a file that should be skipped
218        std::fs::write(tempdir.path().join("blah"), TEST_KEY).expect("Unable to write test file");
219
220        let key_dir = KeyDir::new(&tempdir).expect("Should be able to create key dir");
221
222        assert_eq!(
223            key_dir
224                .list_names()
225                .expect("Should be able to list existing keys")
226                .len(),
227            1,
228            "Should only have 1 key on disk"
229        );
230
231        let key = key_dir
232            .get("foobar_module")
233            .expect("Should be able to load key from disk")
234            .expect("Key should exist");
235        assert_eq!(
236            key.seed().unwrap(),
237            TEST_KEY,
238            "Should load the correct key from disk"
239        );
240    }
241
242    #[test]
243    fn delete_of_nonexistent_key_should_succeed() {
244        let tempdir = tempfile::tempdir().expect("Unable to create temp dir");
245        let key_dir = KeyDir::new(&tempdir).expect("Should be able to create key dir");
246
247        key_dir
248            .delete("foobar")
249            .expect("Non-existent key shouldn't error");
250    }
251}