Skip to main content

aven_core/sync/client/keys/
storage.rs

1//! Host persistence for protected local keys.
2use std::collections::HashSet;
3use std::fs::{self, File, OpenOptions};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6
7use zeroize::Zeroizing;
8
9use super::{ProtectedLocalKeyStoreErrorKind, StoreResult, error};
10
11/// Names the package keyring among secret items. Every other secret item is
12/// named by its kind, such as `seed` or `peer-identity`.
13pub const KEYRING_ITEM: &str = "keyring";
14
15/// Holds a namespace's exclusive lock until dropped.
16pub struct ProtectedStorageLock {
17    _guard: Box<dyn Send + Sync>,
18}
19
20impl ProtectedStorageLock {
21    pub fn new(guard: impl Send + Sync + 'static) -> Self {
22        Self {
23            _guard: Box::new(guard),
24        }
25    }
26}
27
28/// Host-owned storage for protected local keys.
29///
30/// Every call names a namespace: a lowercase hex digest of the database's
31/// location and random `client_id`, so each database has its own items.
32/// Secret items hold key material and must stay out of database exports,
33/// backups and synchronizing credential stores. Records are nonsecret
34/// markers and public evidence. Both are opaque byte strings.
35///
36/// Creating an item or record never replaces an existing one. Absence is
37/// `Ok(None)`, never an error. Failures map to the stable error classes:
38/// `Unavailable` while storage can't be reached (for example, locked),
39/// `Corrupt` for unsafe or malformed contents and `WriteFailed` when a write
40/// or delete fails. Messages never contain key material or paths.
41pub trait ProtectedStorage: Send + Sync {
42    /// Stable bytes naming where the database lives. With its `client_id`
43    /// they name the namespace, so they must not change while the database
44    /// stays in place. The default is the canonical path, which suits hosts
45    /// whose paths are stable across launches and updates.
46    fn database_location(&self, database_path: &Path) -> StoreResult<Vec<u8>> {
47        canonical_location(database_path)
48    }
49
50    /// Makes the namespace ready before first use.
51    fn prepare(&self, namespace: &str) -> StoreResult<()>;
52
53    /// Excludes every other process and store using this namespace until the
54    /// returned lock is dropped.
55    fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock>;
56
57    /// The secret item, which callers expect to be `expected_len` bytes.
58    fn load_secret(
59        &self,
60        namespace: &str,
61        item: &str,
62        expected_len: usize,
63    ) -> StoreResult<Option<Zeroizing<Vec<u8>>>>;
64
65    /// Durably saves a secret item that the caller observed absent.
66    fn create_secret(&self, namespace: &str, item: &str, bytes: &[u8]) -> StoreResult<()>;
67
68    /// Deletes a secret item; an absent item is already deleted.
69    fn delete_secret(&self, namespace: &str, item: &str) -> StoreResult<()>;
70
71    /// The record, which callers expect to be `expected_len` bytes.
72    fn read_record(
73        &self,
74        namespace: &str,
75        name: &str,
76        expected_len: usize,
77    ) -> StoreResult<Option<Vec<u8>>>;
78
79    /// Durably saves a new record; an existing record is a failure.
80    fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()>;
81
82    /// Removes a record; an absent record is already removed.
83    fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()>;
84
85    /// Names of every secret item in the namespace. Extra names are allowed
86    /// and only cost a load; a missing name makes its item read as absent.
87    /// Called under the namespace lock, once per lock.
88    fn list_secrets(&self, namespace: &str) -> StoreResult<HashSet<String>>;
89
90    /// Names of every record in the namespace, with the same contract as
91    /// [`Self::list_secrets`].
92    fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>>;
93}
94
95/// The canonical database path as bytes.
96pub fn canonical_location(database_path: &Path) -> StoreResult<Vec<u8>> {
97    let canonical = database_path
98        .canonicalize()
99        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
100    #[cfg(unix)]
101    {
102        use std::os::unix::ffi::OsStrExt;
103        Ok(canonical.as_os_str().as_bytes().to_vec())
104    }
105    #[cfg(not(unix))]
106    {
107        Ok(canonical.to_string_lossy().as_bytes().to_vec())
108    }
109}
110
111/// Owner-only files in one directory. Secret item `kind` of namespace `ns`
112/// is `ns.kind`, the keyring is `ns.keyring`, record `name` is `ns.name` and
113/// the lock is `ns.lock`.
114pub struct FileProtectedStorage {
115    directory: PathBuf,
116}
117
118impl FileProtectedStorage {
119    pub fn new(directory: PathBuf) -> Self {
120        Self { directory }
121    }
122
123    pub fn directory(&self) -> &Path {
124        &self.directory
125    }
126
127    pub fn path(&self, namespace: &str, name: &str) -> PathBuf {
128        self.directory.join(format!("{namespace}.{name}"))
129    }
130}
131
132impl ProtectedStorage for FileProtectedStorage {
133    fn prepare(&self, _namespace: &str) -> StoreResult<()> {
134        prepare_directory(&self.directory)
135    }
136
137    fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock> {
138        let path = self.path(namespace, "lock");
139        let mut options = OpenOptions::new();
140        options.read(true).write(true).create(true).truncate(false);
141        #[cfg(unix)]
142        {
143            use std::os::unix::fs::OpenOptionsExt;
144            options.mode(0o600);
145        }
146        let file = options
147            .open(path)
148            .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
149        file.lock()
150            .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
151        Ok(ProtectedStorageLock::new(file))
152    }
153
154    fn load_secret(
155        &self,
156        namespace: &str,
157        item: &str,
158        expected_len: usize,
159    ) -> StoreResult<Option<Zeroizing<Vec<u8>>>> {
160        Ok(read_restricted_file(&self.path(namespace, item), expected_len)?.map(Zeroizing::new))
161    }
162
163    fn create_secret(&self, namespace: &str, item: &str, bytes: &[u8]) -> StoreResult<()> {
164        write_restricted_new(&self.path(namespace, item), bytes)
165    }
166
167    fn delete_secret(&self, namespace: &str, item: &str) -> StoreResult<()> {
168        remove_restricted_file(&self.path(namespace, item))
169    }
170
171    fn read_record(
172        &self,
173        namespace: &str,
174        name: &str,
175        expected_len: usize,
176    ) -> StoreResult<Option<Vec<u8>>> {
177        read_restricted_file(&self.path(namespace, name), expected_len)
178    }
179
180    fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()> {
181        write_restricted_new(&self.path(namespace, name), bytes)
182    }
183
184    fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()> {
185        remove_restricted_file(&self.path(namespace, name))
186    }
187
188    /// Secret items and records share the directory, so both listings name
189    /// every file of the namespace except its lock.
190    fn list_secrets(&self, namespace: &str) -> StoreResult<HashSet<String>> {
191        self.list(namespace)
192    }
193
194    fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>> {
195        self.list(namespace)
196    }
197}
198
199impl FileProtectedStorage {
200    fn list(&self, namespace: &str) -> StoreResult<HashSet<String>> {
201        let prefix = format!("{namespace}.");
202        let entries = match fs::read_dir(&self.directory) {
203            Ok(entries) => entries,
204            Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
205                return Ok(HashSet::new());
206            }
207            Err(_) => return Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
208        };
209        let mut names = HashSet::new();
210        for entry in entries {
211            let entry = entry.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
212            if let Some(name) = entry
213                .file_name()
214                .to_str()
215                .and_then(|name| name.strip_prefix(&prefix))
216                .filter(|name| *name != "lock")
217            {
218                names.insert(name.to_string());
219            }
220        }
221        Ok(names)
222    }
223}
224
225fn prepare_directory(path: &Path) -> StoreResult<()> {
226    fs::create_dir_all(path).map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
227    #[cfg(unix)]
228    {
229        use std::os::unix::fs::PermissionsExt;
230        fs::set_permissions(path, fs::Permissions::from_mode(0o700))
231            .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
232    }
233    Ok(())
234}
235
236fn read_restricted_file(path: &Path, expected_len: usize) -> StoreResult<Option<Vec<u8>>> {
237    let mut file = match File::open(path) {
238        Ok(file) => file,
239        Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None),
240        Err(_) => return Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
241    };
242    let metadata = file
243        .metadata()
244        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
245    if !metadata.is_file() || metadata.len() != expected_len as u64 {
246        return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
247    }
248    #[cfg(unix)]
249    {
250        use std::os::unix::fs::PermissionsExt;
251        if metadata.permissions().mode() & 0o077 != 0 {
252            return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
253        }
254    }
255    let mut bytes = vec![0_u8; expected_len];
256    file.read_exact(&mut bytes)
257        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Corrupt))?;
258    Ok(Some(bytes))
259}
260
261fn sync_parent(path: &Path) -> StoreResult<()> {
262    let parent = path
263        .parent()
264        .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
265    File::open(parent)
266        .and_then(|directory| directory.sync_all())
267        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))
268}
269
270fn remove_restricted_file(path: &Path) -> StoreResult<()> {
271    match fs::remove_file(path) {
272        Ok(()) => sync_parent(path),
273        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
274        Err(_) => Err(super::error(ProtectedLocalKeyStoreErrorKind::WriteFailed)),
275    }
276}
277
278fn write_restricted_new(path: &Path, bytes: &[u8]) -> StoreResult<()> {
279    let parent = path
280        .parent()
281        .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
282    let mut staged = tempfile::NamedTempFile::new_in(parent)
283        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
284    #[cfg(unix)]
285    {
286        use std::os::unix::fs::PermissionsExt;
287        staged
288            .as_file()
289            .set_permissions(fs::Permissions::from_mode(0o600))
290            .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
291    }
292    staged
293        .as_file_mut()
294        .write_all(bytes)
295        .and_then(|()| staged.as_file().sync_all())
296        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
297    staged
298        .persist_noclobber(path)
299        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
300    File::open(parent)
301        .and_then(|directory| directory.sync_all())
302        .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))
303}