Skip to main content

passless_rs/pin_storage/
mod.rs

1//! PIN storage backends for FIDO2 authenticator
2//!
3//! This module provides storage backends for persisting PIN state.
4
5pub mod local;
6pub mod pass;
7#[cfg(feature = "tpm")]
8pub mod tpm;
9
10pub use local::LocalPinStorage;
11pub use pass::PassPinStorage;
12#[cfg(feature = "tpm")]
13pub use tpm::TpmPinStorage;
14
15use serde::{Deserialize, Serialize};
16use soft_fido2::{PinState, StatusCode};
17
18/// Serializable PIN config for storage (synced across machines)
19///
20/// Contains PIN configuration that should be synchronized across all machines
21/// using the same password-store. Changes to this file are committed to git.
22#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
23pub struct SerializablePinConfig {
24    /// PIN hash as raw bytes (None if no PIN set)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub pin_hash: Option<Vec<u8>>,
27    /// Minimum PIN length (4-63)
28    #[serde(default = "default_min_pin_length")]
29    pub min_pin_length: u8,
30    /// State version for rollback detection
31    #[serde(default)]
32    pub version: u64,
33    /// Force PIN change flag
34    #[serde(default)]
35    pub force_pin_change: bool,
36    /// Credential wrapping generation, incremented on reset to invalidate wrapped credentials
37    #[serde(default)]
38    pub credential_wrapping_generation: u64,
39    /// Modification timestamp in milliseconds since Unix epoch
40    /// Used for conflict resolution when syncing across machines
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub modified_at: Option<u64>,
43}
44
45fn default_min_pin_length() -> u8 {
46    4
47}
48
49impl SerializablePinConfig {
50    /// Convert to JSON bytes
51    pub fn to_json_bytes(&self) -> Result<Vec<u8>, StatusCode> {
52        serde_json::to_vec(self).map_err(|_| StatusCode::Other)
53    }
54
55    /// Convert from JSON bytes
56    pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, StatusCode> {
57        serde_json::from_slice(bytes).map_err(|_| StatusCode::InvalidParameter)
58    }
59
60    /// Check if PIN is set
61    pub fn is_pin_set(&self) -> bool {
62        self.pin_hash.is_some()
63    }
64}
65
66impl From<&PinState> for SerializablePinConfig {
67    fn from(state: &PinState) -> Self {
68        let modified_at = std::time::SystemTime::now()
69            .duration_since(std::time::UNIX_EPOCH)
70            .map(|d| d.as_millis() as u64)
71            .ok();
72        Self {
73            pin_hash: state.pin_hash.as_ref().map(|h| h.as_array().to_vec()),
74            min_pin_length: state.min_pin_length,
75            version: state.version,
76            force_pin_change: state.force_pin_change,
77            credential_wrapping_generation: state.credential_wrapping_generation,
78            modified_at,
79        }
80    }
81}
82
83/// Serializable PIN retry state for storage (local-only, not synced)
84///
85/// Contains retry counters and lock state that are machine-specific.
86/// These should NOT be synced to git to avoid conflicts and commit spam.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SerializablePinRetries {
89    /// Remaining PIN retry attempts (0-8)
90    #[serde(default = "default_retries")]
91    pub retries: u8,
92    /// Remaining UV retry attempts
93    ///
94    /// The serde default of 3 maintains backward compatibility with existing stored state.
95    /// The actual maximum is enforced by the configured `pin.max_uv_retries` value,
96    /// which is applied by the PinStorageWrapper during load/save operations.
97    #[serde(default = "default_uv_retries")]
98    pub uv_retries: u8,
99    /// Auto-lock timestamp in milliseconds since Unix epoch (None = not locked)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub locked_until: Option<u64>,
102}
103
104fn default_retries() -> u8 {
105    8
106}
107
108/// Serde default for UV retries - preserves backward compatibility with existing stored state.
109/// The configured maximum is applied separately by PinStorageWrapper.
110fn default_uv_retries() -> u8 {
111    3
112}
113
114impl Default for SerializablePinRetries {
115    fn default() -> Self {
116        Self {
117            retries: 8,
118            uv_retries: 8,
119            locked_until: None,
120        }
121    }
122}
123
124impl SerializablePinRetries {
125    /// Create new retry state with configured maximums
126    pub fn new(max_retries: u8, max_uv_retries: u8) -> Self {
127        Self {
128            retries: max_retries,
129            uv_retries: max_uv_retries,
130            locked_until: None,
131        }
132    }
133
134    /// Convert to JSON bytes
135    pub fn to_json_bytes(&self) -> Result<Vec<u8>, StatusCode> {
136        serde_json::to_vec(self).map_err(|_| StatusCode::Other)
137    }
138
139    /// Convert from JSON bytes
140    pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, StatusCode> {
141        serde_json::from_slice(bytes).map_err(|_| StatusCode::InvalidParameter)
142    }
143}
144
145impl From<&PinState> for SerializablePinRetries {
146    fn from(state: &PinState) -> Self {
147        Self {
148            retries: state.retries,
149            uv_retries: state.uv_retries,
150            locked_until: state.locked_until,
151        }
152    }
153}
154
155/// Serializable PIN state for storage (legacy, single-file format)
156///
157/// This struct represents the PIN state in a format suitable for serialization
158/// to JSON or other formats. It contains the PIN hash as raw bytes (if set),
159/// retry counters, and configuration.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SerializablePinState {
162    /// PIN hash as raw bytes (None if no PIN set)
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub pin_hash: Option<Vec<u8>>,
165    /// Remaining PIN retry attempts (0-8)
166    pub retries: u8,
167    /// Remaining UV retry attempts
168    ///
169    /// The serde default of 3 maintains backward compatibility with existing stored state.
170    /// The actual maximum is enforced by the configured `pin.max_uv_retries` value.
171    #[serde(default = "default_uv_retries")]
172    pub uv_retries: u8,
173    /// Minimum PIN length (4-63)
174    pub min_pin_length: u8,
175    /// State version for rollback detection
176    pub version: u64,
177    /// Force PIN change flag
178    #[serde(default)]
179    pub force_pin_change: bool,
180    /// Credential wrapping generation, incremented on reset to invalidate wrapped credentials
181    #[serde(default)]
182    pub credential_wrapping_generation: u64,
183    /// Auto-lock timestamp in milliseconds since Unix epoch (None = not locked)
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub locked_until: Option<u64>,
186}
187
188impl Default for SerializablePinState {
189    fn default() -> Self {
190        Self {
191            pin_hash: None,
192            retries: 8,
193            uv_retries: 8,
194            min_pin_length: 4,
195            version: 0,
196            force_pin_change: false,
197            credential_wrapping_generation: 0,
198            locked_until: None,
199        }
200    }
201}
202
203impl From<&PinState> for SerializablePinState {
204    fn from(state: &PinState) -> Self {
205        Self {
206            pin_hash: state.pin_hash.as_ref().map(|h| h.as_array().to_vec()),
207            retries: state.retries,
208            uv_retries: state.uv_retries,
209            min_pin_length: state.min_pin_length,
210            version: state.version,
211            force_pin_change: state.force_pin_change,
212            locked_until: state.locked_until,
213            credential_wrapping_generation: state.credential_wrapping_generation,
214        }
215    }
216}
217
218impl From<SerializablePinState> for PinState {
219    fn from(state: SerializablePinState) -> Self {
220        use soft_fido2_ctap::SecPinHash;
221        Self {
222            pin_hash: state.pin_hash.map(|h| {
223                let arr: [u8; 32] = h.as_slice().try_into().unwrap_or([0u8; 32]);
224                SecPinHash::new(arr)
225            }),
226            retries: state.retries,
227            uv_retries: state.uv_retries,
228            min_pin_length: state.min_pin_length,
229            version: state.version,
230            force_pin_change: state.force_pin_change,
231            locked_until: state.locked_until,
232            credential_wrapping_generation: state.credential_wrapping_generation,
233        }
234    }
235}
236
237impl SerializablePinState {
238    /// Convert to JSON bytes
239    pub fn to_json_bytes(&self) -> Result<Vec<u8>, StatusCode> {
240        serde_json::to_vec(self).map_err(|_| StatusCode::Other)
241    }
242
243    /// Convert from JSON bytes
244    pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, StatusCode> {
245        serde_json::from_slice(bytes).map_err(|_| StatusCode::InvalidParameter)
246    }
247
248    /// Create from config and retries parts
249    pub fn from_parts(config: &SerializablePinConfig, retries: &SerializablePinRetries) -> Self {
250        Self {
251            pin_hash: config.pin_hash.clone(),
252            retries: retries.retries,
253            uv_retries: retries.uv_retries,
254            min_pin_length: config.min_pin_length,
255            version: config.version,
256            force_pin_change: config.force_pin_change,
257            locked_until: retries.locked_until,
258            credential_wrapping_generation: config.credential_wrapping_generation,
259        }
260    }
261}
262
263/// Trait for PIN storage backends
264///
265/// This trait is implemented by storage backends that can persist PIN state.
266/// The soft-fido2 library calls these methods when PIN state changes.
267pub trait PinStorage: Send + Sync {
268    /// Load PIN state from storage
269    ///
270    /// Returns default state if no state is stored.
271    fn load_pin_state(&self) -> Result<PinState, StatusCode>;
272
273    /// Save PIN state to storage
274    fn save_pin_state(&self, state: &PinState) -> Result<(), StatusCode>;
275}
276
277/// No-op PIN storage implementation
278impl PinStorage for () {
279    fn load_pin_state(&self) -> Result<PinState, StatusCode> {
280        Ok(PinState::new())
281    }
282
283    fn save_pin_state(&self, _state: &PinState) -> Result<(), StatusCode> {
284        Ok(())
285    }
286}
287
288impl PinStorage for Box<dyn PinStorage> {
289    fn load_pin_state(&self) -> Result<PinState, StatusCode> {
290        (**self).load_pin_state()
291    }
292
293    fn save_pin_state(&self, state: &PinState) -> Result<(), StatusCode> {
294        (**self).save_pin_state(state)
295    }
296}