passless_rs/storage/local/
mod.rs1pub 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}