1use 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 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 set_permissions_keys(p)?;
43 Ok(KeyDir(p.canonicalize()?))
45 }
46
47 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
120pub 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)]
128fn 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 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}