Skip to main content

passless_rs/storage/local/
mod.rs

1//! Local file system storage adapter
2
3pub mod init;
4
5use crate::storage::credential::Credential;
6use crate::storage::index::{
7    CredentialIndexes, CredentialPathInfo, load_credential_paths, update_indexes_on_delete,
8    update_indexes_on_write,
9};
10use crate::storage::rp_id::validate_rp_id_for_storage;
11use crate::storage::{CredentialFilter, CredentialStorage};
12use crate::util::{atomic_write_in_dir, create_secure_dir_all};
13
14use soft_fido2::Result;
15
16use std::fs;
17use std::io::Read;
18use std::path::{Path, PathBuf};
19
20use log::{debug, error, info, warn};
21use zeroize::Zeroizing;
22
23pub struct LocalStorageAdapter {
24    storage_dir: PathBuf,
25    indexes: CredentialIndexes,
26    iteration_index: usize,
27    iteration_files: Vec<PathBuf>,
28}
29
30impl LocalStorageAdapter {
31    #[cfg(test)]
32    pub fn new(storage_dir: PathBuf) -> Result<Self> {
33        Self::new_with_options(storage_dir, true)
34    }
35
36    pub fn new_with_options(
37        storage_dir: PathBuf,
38        allow_create_without_prompt: bool,
39    ) -> Result<Self> {
40        let allow_create_without_prompt = cfg!(debug_assertions) && allow_create_without_prompt;
41        info!("Using local file system backend");
42        info!("Storage path: {}", storage_dir.display());
43
44        if storage_dir.is_absolute() {
45            warn!("Storage path is absolute: {}", storage_dir.display());
46        } else {
47            debug!("Storage path is relative: {}", storage_dir.display());
48        }
49
50        self::init::ensure_initialized(&storage_dir, allow_create_without_prompt)
51            .map_err(|_| soft_fido2::Error::Other)?;
52
53        let indexes =
54            load_credential_paths(&storage_dir, "bin").map_err(|_| soft_fido2::Error::Other)?;
55
56        debug!("Loaded {} credentials into indexes", indexes.id.len());
57
58        Ok(Self {
59            storage_dir,
60            indexes,
61            iteration_index: 0,
62            iteration_files: Vec::new(),
63        })
64    }
65
66    fn load_credential_from_path(&self, path: &Path) -> Result<soft_fido2::Credential> {
67        debug!("Loading credential from: {:?}", path);
68        let mut file = fs::File::open(path).map_err(|_| soft_fido2::Error::DoesNotExist)?;
69        let mut contents = Vec::new();
70        file.read_to_end(&mut contents)
71            .map_err(|_| soft_fido2::Error::Other)?;
72
73        Credential::from_bytes(&contents).map(|cred| cred.to_soft_fido2())
74    }
75
76    fn save_credential(&mut self, cred: &soft_fido2::Credential) -> Result<()> {
77        let our_cred = Credential::from_soft_fido2(cred);
78
79        let rp_id = validate_rp_id_for_storage(cred.rp.id.as_str())
80            .map_err(|_| soft_fido2::Error::Other)?;
81
82        let path_info = CredentialPathInfo::new(rp_id, cred.id.clone(), "bin".to_string());
83
84        let path = path_info.to_path(&self.storage_dir);
85
86        let parent = path.parent().ok_or(soft_fido2::Error::Other)?;
87        create_secure_dir_all(parent).map_err(|e| {
88            error!(
89                "Failed to create credential directory {}: {}",
90                parent.display(),
91                e
92            );
93            soft_fido2::Error::Other
94        })?;
95
96        let bytes = Zeroizing::new(our_cred.to_bytes()?);
97
98        let filename = path
99            .file_name()
100            .and_then(|n| n.to_str())
101            .ok_or(soft_fido2::Error::Other)?;
102        atomic_write_in_dir(parent, filename, &bytes).map_err(|e| {
103            error!("Failed to persist credential {}: {}", path.display(), e);
104            soft_fido2::Error::Other
105        })?;
106
107        update_indexes_on_write(&mut self.indexes, path_info);
108
109        debug!("Saved credential for RP: {}", cred.rp.id);
110        Ok(())
111    }
112
113    fn find_next(&mut self) -> Result<soft_fido2::Credential> {
114        debug!(
115            "Finding next credential (index: {}/{})",
116            self.iteration_index,
117            self.iteration_files.len()
118        );
119
120        if self.iteration_index >= self.iteration_files.len() {
121            debug!("No more credentials matching filter");
122            return Err(soft_fido2::Error::DoesNotExist);
123        }
124
125        let path = &self.iteration_files[self.iteration_index];
126        self.iteration_index += 1;
127
128        self.load_credential_from_path(path)
129    }
130}
131
132impl CredentialStorage for LocalStorageAdapter {
133    fn read_first(&mut self, filter: CredentialFilter) -> Result<soft_fido2::Credential> {
134        debug!("read_first called with filter: {:?}", filter);
135
136        self.iteration_index = 0;
137        self.iteration_files = self.indexes.resolve_filter(&filter, &self.storage_dir);
138
139        debug!("Found {} matching paths", self.iteration_files.len());
140
141        self.find_next()
142    }
143
144    fn read_next(&mut self) -> Result<soft_fido2::Credential> {
145        self.find_next()
146    }
147
148    fn read(&mut self, id: &[u8]) -> Result<soft_fido2::Credential> {
149        debug!("read called for credential ID");
150
151        let path_info = self
152            .indexes
153            .id
154            .get(id)
155            .ok_or(soft_fido2::Error::DoesNotExist)?;
156
157        let path = path_info.to_path(&self.storage_dir);
158        self.load_credential_from_path(&path)
159    }
160
161    fn write(&mut self, cred_ref: soft_fido2::CredentialRef) -> Result<()> {
162        let cred = cred_ref.to_owned();
163        self.save_credential(&cred)
164    }
165
166    fn delete(&mut self, id: &[u8]) -> Result<()> {
167        debug!("delete called for credential ID");
168
169        let path_info = self
170            .indexes
171            .id
172            .get(id)
173            .ok_or(soft_fido2::Error::DoesNotExist)?;
174
175        let path = path_info.to_path(&self.storage_dir);
176        let rp_id = path_info.rp_id.clone();
177
178        fs::remove_file(&path).map_err(|_| soft_fido2::Error::Other)?;
179
180        update_indexes_on_delete(&mut self.indexes, id);
181
182        debug!("Deleted credential for RP: {}", rp_id);
183        Ok(())
184    }
185
186    fn count_credentials(&self) -> usize {
187        let count = self.indexes.id.len();
188        debug!("count_credentials: {}", count);
189        count
190    }
191}