1use crate::pin_storage::{
4 PinStorage, SerializablePinConfig, SerializablePinRetries, SerializablePinState,
5};
6use crate::storage::pass::GpgBackend;
7use crate::storage::pass::gpg_id;
8use crate::util::create_secure_dir_all;
9
10use passless_core::error::{Error, Result};
11
12use std::path::{Path, PathBuf};
13use std::sync::RwLock;
14
15use log::{debug, info, warn};
16use prs_lib::crypto::IsContext;
17use prs_lib::{Ciphertext, Plaintext, Store};
18
19const PIN_CONFIG_ENTRY: &str = "pin_state";
20const PIN_RETRIES_ENTRY: &str = "pin_retries";
21
22#[derive(Debug, Clone)]
24struct SyncedConfig {
25 version: u64,
26 #[allow(dead_code)]
27 modified_at: Option<u64>,
28 pin_hash: Option<Vec<u8>>,
29}
30
31pub struct PassPinStorage {
37 store_path: PathBuf,
38 fido2_path: PathBuf,
39 gpg_backend: GpgBackend,
40 last_config: RwLock<Option<SerializablePinConfig>>,
42 last_synced: RwLock<Option<SyncedConfig>>,
44}
45
46impl PassPinStorage {
47 pub fn new(store_path: PathBuf, fido2_path: PathBuf, gpg_backend: GpgBackend) -> Self {
49 debug!(
50 "Pass PIN storage: store={}, path={}",
51 store_path.display(),
52 fido2_path.display()
53 );
54 Self {
55 store_path,
56 fido2_path,
57 gpg_backend,
58 last_config: RwLock::new(None),
59 last_synced: RwLock::new(None),
60 }
61 }
62
63 fn get_config_path(&self) -> PathBuf {
64 self.store_path
65 .join(&self.fido2_path)
66 .join(format!("{}.gpg", PIN_CONFIG_ENTRY))
67 }
68
69 fn get_retries_path(&self) -> PathBuf {
70 self.store_path
71 .join(&self.fido2_path)
72 .join(format!("{}.gpg", PIN_RETRIES_ENTRY))
73 }
74
75 fn create_crypto_context(&self) -> Result<prs_lib::crypto::Context> {
76 let proto = match self.gpg_backend {
77 GpgBackend::Gpgme | GpgBackend::GnupgBin => prs_lib::crypto::Proto::Gpg,
78 };
79
80 let config = prs_lib::crypto::Config::from(proto);
81 prs_lib::crypto::context(&config).map_err(|e| {
82 warn!("Failed to create crypto context: {:?}", e);
83 Error::Storage(format!("Failed to create crypto context: {:?}", e))
84 })
85 }
86
87 fn resolve_recipients_for_target(&self, target: &Path) -> Result<prs_lib::Recipients> {
88 gpg_id::resolve_recipients_for_target(&self.store_path, target)
89 }
90
91 fn sync_prepare(&self) -> Result<()> {
92 debug!("Preparing password store sync for PIN config");
93
94 let store = Store::open(self.store_path.to_string_lossy().as_ref()).map_err(|e| {
95 debug!("Failed to open store for sync: {:?}", e);
96 Error::Storage(format!("Failed to open store for sync: {:?}", e))
97 })?;
98
99 let sync = store.sync();
100
101 match sync.prepare() {
102 Ok(()) => {
103 debug!("Successfully prepared store sync (pulled if remote configured)");
104 Ok(())
105 }
106 Err(e) => {
107 warn!("Failed to prepare store sync: {:?}", e);
108 Ok(())
109 }
110 }
111 }
112
113 fn sync_finalize(&self, message: &str) -> Result<()> {
114 debug!("Finalizing password store sync: {}", message);
115
116 let store = Store::open(self.store_path.to_string_lossy().as_ref()).map_err(|e| {
117 debug!("Failed to open store for sync: {:?}", e);
118 Error::Storage(format!("Failed to open store for sync: {:?}", e))
119 })?;
120
121 let sync = store.sync();
122
123 match sync.finalize(message) {
124 Ok(()) => {
125 debug!(
126 "Successfully finalized store sync (committed and pushed if remote configured)"
127 );
128 Ok(())
129 }
130 Err(e) => {
131 debug!("Failed to finalize store sync: {:?}", e);
132 warn!("Failed to finalize store sync: {:?}", e);
133 Ok(())
134 }
135 }
136 }
137
138 fn load_config(&self) -> std::result::Result<SerializablePinConfig, soft_fido2::StatusCode> {
139 debug!("Loading PIN config from pass store");
140
141 let path = self.get_config_path();
142
143 if !path.exists() {
144 debug!("PIN config entry does not exist, returning default state");
145 return Ok(SerializablePinConfig::default());
146 }
147
148 let encrypted_data = std::fs::read(&path).map_err(|e| {
149 warn!("Failed to read PIN config entry: {}", e);
150 soft_fido2::StatusCode::Other
151 })?;
152
153 let mut context = self.create_crypto_context().map_err(|e| {
154 warn!("Failed to create crypto context: {:?}", e);
155 soft_fido2::StatusCode::Other
156 })?;
157
158 let ciphertext = Ciphertext::from(encrypted_data);
159 let plaintext = context.decrypt(ciphertext).map_err(|e| {
160 warn!("Failed to decrypt PIN config: {:?}", e);
161 soft_fido2::StatusCode::Other
162 })?;
163
164 SerializablePinConfig::from_json_bytes(plaintext.unsecure_ref()).map_err(|e| {
165 warn!("Failed to parse PIN config: {:?}", e);
166 soft_fido2::StatusCode::InvalidParameter
167 })
168 }
169
170 fn load_retries(&self) -> std::result::Result<SerializablePinRetries, soft_fido2::StatusCode> {
171 debug!("Loading PIN retries from pass store");
172
173 let path = self.get_retries_path();
174
175 if !path.exists() {
176 debug!("PIN retries entry does not exist, returning default state");
177 return Ok(SerializablePinRetries::default());
178 }
179
180 let encrypted_data = std::fs::read(&path).map_err(|e| {
181 warn!("Failed to read PIN retries entry: {}", e);
182 soft_fido2::StatusCode::Other
183 })?;
184
185 let mut context = self.create_crypto_context().map_err(|e| {
186 warn!("Failed to create crypto context: {:?}", e);
187 soft_fido2::StatusCode::Other
188 })?;
189
190 let ciphertext = Ciphertext::from(encrypted_data);
191 let plaintext = context.decrypt(ciphertext).map_err(|e| {
192 warn!("Failed to decrypt PIN retries: {:?}", e);
193 soft_fido2::StatusCode::Other
194 })?;
195
196 SerializablePinRetries::from_json_bytes(plaintext.unsecure_ref()).map_err(|e| {
197 warn!("Failed to parse PIN retries: {:?}", e);
198 soft_fido2::StatusCode::InvalidParameter
199 })
200 }
201
202 fn save_config(
203 &self,
204 config: &SerializablePinConfig,
205 ) -> std::result::Result<(), soft_fido2::StatusCode> {
206 debug!("Saving PIN config to pass store");
207
208 let bytes = config.to_json_bytes()?;
209
210 let path = self.get_config_path();
211 let recipients = self.resolve_recipients_for_target(&path).map_err(|e| {
212 warn!("Failed to load GPG recipients: {:?}", e);
213 soft_fido2::StatusCode::Other
214 })?;
215
216 let mut context = self.create_crypto_context().map_err(|e| {
217 warn!("Failed to create crypto context: {:?}", e);
218 soft_fido2::StatusCode::Other
219 })?;
220
221 if let Some(parent) = path.parent() {
222 create_secure_dir_all(parent).map_err(|e| {
223 warn!("Failed to create directory: {}", e);
224 soft_fido2::StatusCode::Other
225 })?;
226 }
227
228 let plaintext = Plaintext::from(bytes);
229 context
230 .encrypt_file(&recipients, plaintext, &path)
231 .map_err(|e| {
232 warn!("Failed to encrypt PIN config: {:?}", e);
233 soft_fido2::StatusCode::Other
234 })?;
235
236 let relative_path = path
237 .strip_prefix(&self.store_path)
238 .unwrap_or(&path)
239 .display();
240 let commit_message = format!("Update PIN configuration: {}.", relative_path);
241 self.sync_finalize(&commit_message).map_err(|e| {
242 warn!("Failed to sync PIN config: {:?}", e);
243 soft_fido2::StatusCode::Other
244 })?;
245
246 *self.last_config.write().map_err(|e| {
247 warn!("Failed to acquire config lock: {:?}", e);
248 soft_fido2::StatusCode::Other
249 })? = Some(config.clone());
250 debug!("PIN config saved and synced successfully");
251 Ok(())
252 }
253
254 fn save_retries(
255 &self,
256 retries: &SerializablePinRetries,
257 ) -> std::result::Result<(), soft_fido2::StatusCode> {
258 debug!("Saving PIN retries to pass store (local only, no sync)");
259
260 let bytes = retries.to_json_bytes()?;
261
262 let path = self.get_retries_path();
263 let recipients = self.resolve_recipients_for_target(&path).map_err(|e| {
264 warn!("Failed to load GPG recipients: {:?}", e);
265 soft_fido2::StatusCode::Other
266 })?;
267
268 let mut context = self.create_crypto_context().map_err(|e| {
269 warn!("Failed to create crypto context: {:?}", e);
270 soft_fido2::StatusCode::Other
271 })?;
272
273 if let Some(parent) = path.parent() {
274 create_secure_dir_all(parent).map_err(|e| {
275 warn!("Failed to create directory: {}", e);
276 soft_fido2::StatusCode::Other
277 })?;
278 }
279
280 let plaintext = Plaintext::from(bytes);
281 context
282 .encrypt_file(&recipients, plaintext, &path)
283 .map_err(|e| {
284 warn!("Failed to encrypt PIN retries: {:?}", e);
285 soft_fido2::StatusCode::Other
286 })?;
287
288 debug!("PIN retries saved successfully (no git sync)");
289 Ok(())
290 }
291
292 fn config_changed(&self, new_config: &SerializablePinConfig) -> bool {
293 let last = match self.last_config.read() {
294 Ok(l) => l,
295 Err(_) => return true,
296 };
297 match last.as_ref() {
298 Some(old) => old != new_config,
299 None => true,
300 }
301 }
302
303 fn check_and_resolve_conflict(
306 &self,
307 local_config: &SerializablePinConfig,
308 ) -> std::result::Result<Option<SerializablePinConfig>, soft_fido2::StatusCode> {
309 let last_synced = match self.last_synced.read() {
310 Ok(l) => l.clone(),
311 Err(_) => return Ok(None),
312 };
313
314 let Some(synced) = last_synced else {
315 debug!("No previous synced state, no conflict check needed");
316 return Ok(None);
317 };
318
319 if let Err(e) = self.sync_prepare() {
321 warn!("Failed to prepare sync for conflict check: {:?}", e);
322 }
323
324 let remote_config = self.load_config()?;
326
327 let remote_changed =
329 remote_config.version != synced.version || remote_config.pin_hash != synced.pin_hash;
330
331 if !remote_changed {
332 debug!("Remote config unchanged since last sync, no conflict");
333 return Ok(None);
334 }
335
336 let local_changed =
338 local_config.version != synced.version || local_config.pin_hash != synced.pin_hash;
339
340 if !local_changed {
341 debug!("Remote changed but local unchanged, using remote config");
342 return Ok(Some(remote_config));
343 }
344
345 warn!("PIN config conflict detected: both local and remote changed since last sync");
347 warn!(
348 "Local: version={}, modified_at={:?}",
349 local_config.version, local_config.modified_at
350 );
351 warn!(
352 "Remote: version={}, modified_at={:?}",
353 remote_config.version, remote_config.modified_at
354 );
355
356 let local_time = local_config.modified_at.unwrap_or(0);
358 let remote_time = remote_config.modified_at.unwrap_or(0);
359
360 if remote_time > local_time {
361 info!(
362 "Conflict resolved: remote config is newer ({} > {}), using remote",
363 remote_time, local_time
364 );
365 return Ok(Some(remote_config));
366 } else if local_time > remote_time {
367 info!(
368 "Conflict resolved: local config is newer ({} > {}), keeping local",
369 local_time, remote_time
370 );
371 return Ok(None);
372 }
373
374 if remote_config.version > local_config.version {
376 info!(
377 "Conflict resolved: remote has higher version ({} > {})",
378 remote_config.version, local_config.version
379 );
380 return Ok(Some(remote_config));
381 }
382
383 info!(
384 "Conflict resolved: local has higher or equal version ({} >= {})",
385 local_config.version, remote_config.version
386 );
387 Ok(None)
388 }
389}
390
391impl PinStorage for PassPinStorage {
392 fn load_pin_state(&self) -> std::result::Result<soft_fido2::PinState, soft_fido2::StatusCode> {
393 if let Err(e) = self.sync_prepare() {
395 warn!("Failed to prepare sync for PIN config: {:?}", e);
396 }
397
398 let config = self.load_config()?;
399 let retries = self.load_retries()?;
400
401 *self.last_synced.write().map_err(|e| {
403 warn!("Failed to acquire synced lock: {:?}", e);
404 soft_fido2::StatusCode::Other
405 })? = Some(SyncedConfig {
406 version: config.version,
407 modified_at: config.modified_at,
408 pin_hash: config.pin_hash.clone(),
409 });
410
411 *self.last_config.write().map_err(|e| {
412 warn!("Failed to acquire config lock: {:?}", e);
413 soft_fido2::StatusCode::Other
414 })? = Some(config.clone());
415
416 let state = SerializablePinState::from_parts(&config, &retries);
417 Ok(state.into())
418 }
419
420 fn save_pin_state(
421 &self,
422 state: &soft_fido2::PinState,
423 ) -> std::result::Result<(), soft_fido2::StatusCode> {
424 let new_config = SerializablePinConfig::from(state);
425 let new_retries = SerializablePinRetries::from(state);
426
427 if state.is_pin_set() {
428 info!(
429 "Saving PIN state (PIN is set, {} retries remaining)",
430 state.retries
431 );
432 } else {
433 info!("Saving PIN state (no PIN set)");
434 }
435
436 if self.config_changed(&new_config) {
437 debug!("PIN config changed, checking for conflicts...");
438
439 match self.check_and_resolve_conflict(&new_config)? {
441 Some(resolved_config) => {
442 warn!("Conflict resolved by using remote config, updating local state");
443 let resolved_state =
445 SerializablePinState::from_parts(&resolved_config, &new_retries);
446 let _resolved_pin_state: soft_fido2::PinState = resolved_state.into();
447
448 *self.last_synced.write().map_err(|e| {
450 warn!("Failed to acquire synced lock: {:?}", e);
451 soft_fido2::StatusCode::Other
452 })? = Some(SyncedConfig {
453 version: resolved_config.version,
454 modified_at: resolved_config.modified_at,
455 pin_hash: resolved_config.pin_hash.clone(),
456 });
457
458 *self.last_config.write().map_err(|e| {
459 warn!("Failed to acquire config lock: {:?}", e);
460 soft_fido2::StatusCode::Other
461 })? = Some(resolved_config);
462
463 self.save_retries(&new_retries)?;
465
466 return Err(soft_fido2::StatusCode::Other);
468 }
469 None => {
470 debug!("No conflict, proceeding with save");
471 }
472 }
473
474 debug!("Saving PIN config and syncing");
475 self.save_config(&new_config)?;
476
477 *self.last_synced.write().map_err(|e| {
479 warn!("Failed to acquire synced lock: {:?}", e);
480 soft_fido2::StatusCode::Other
481 })? = Some(SyncedConfig {
482 version: new_config.version,
483 modified_at: new_config.modified_at,
484 pin_hash: new_config.pin_hash.clone(),
485 });
486 } else {
487 debug!("PIN config unchanged, skipping save");
488 }
489
490 self.save_retries(&new_retries)?;
491
492 Ok(())
493 }
494}