Skip to content

Commit 444108d

Browse files
committed
Persist authenticator PIN state across daemon restarts
1 parent a91efec commit 444108d

1 file changed

Lines changed: 98 additions & 1 deletion

File tree

authenticator/src/ctap.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ const COSE_ALG_ES256: i32 = -7;
192192
const CREDENTIAL_STORE_PATH: &str = "credentials.cbor";
193193
#[cfg(not(test))]
194194
const ATTESTATION_STORE_PATH: &str = "attestation.cbor";
195+
#[cfg(not(test))]
196+
const PIN_STATE_STORE_PATH: &str = "pin-state.cbor";
195197
pub(crate) const PIN_UV_AUTH_PROTOCOL_CLASSIC_V1: i32 = 1;
196198
pub(crate) const PIN_UV_AUTH_PROTOCOL_CLASSIC_V2: i32 = 2;
197199
#[cfg_attr(not(test), allow(dead_code))]
@@ -242,6 +244,28 @@ struct PinState {
242244
pin_uv_auth_rp_provided: bool,
243245
}
244246

247+
/// On-disk representation of the persistent PIN state. Kept in sync with
248+
/// `transport-core::state::StoredPinState` so the CLI can read and write the
249+
/// same `pin-state.cbor` file the daemon uses.
250+
#[derive(Debug, Clone, Serialize, Deserialize)]
251+
struct StoredPinState {
252+
pin_hash: Option<[u8; 16]>,
253+
pin_retries: u8,
254+
consecutive_failures: u8,
255+
pin_auth_blocked: bool,
256+
}
257+
258+
impl StoredPinState {
259+
fn snapshot(state: &PinState) -> Self {
260+
Self {
261+
pin_hash: state.pin_hash,
262+
pin_retries: state.pin_retries,
263+
consecutive_failures: state.consecutive_failures,
264+
pin_auth_blocked: state.pin_auth_blocked,
265+
}
266+
}
267+
}
268+
245269
impl PinState {
246270
const MIN_PIN_LENGTH: usize = 4;
247271

@@ -258,6 +282,19 @@ impl PinState {
258282
}
259283
}
260284

285+
fn from_stored(stored: StoredPinState) -> Self {
286+
Self {
287+
pin_hash: stored.pin_hash,
288+
pin_retries: stored.pin_retries,
289+
consecutive_failures: stored.consecutive_failures,
290+
pin_auth_blocked: stored.pin_auth_blocked,
291+
pin_uv_auth_token: None,
292+
pin_uv_auth_permissions: 0,
293+
pin_uv_auth_rp_id: None,
294+
pin_uv_auth_rp_provided: false,
295+
}
296+
}
297+
261298
fn is_set(&self) -> bool {
262299
self.pin_hash.is_some()
263300
}
@@ -572,7 +609,7 @@ where
572609
C: TrussedClient + FilesystemClient + CryptoClient,
573610
{
574611
pub fn new(client: C, aaguid: [u8; 16]) -> Self {
575-
let app = Self {
612+
let mut app = Self {
576613
client,
577614
aaguid,
578615
pin_state: PinState::new(),
@@ -589,6 +626,7 @@ where
589626
#[cfg(test)]
590627
stored_credentials: Vec::new(),
591628
};
629+
app.load_persistent_pin_state();
592630
app
593631
}
594632

@@ -677,6 +715,60 @@ where
677715
}
678716
}
679717

718+
/// Load the persistent PIN state from `pin-state.cbor` on the internal
719+
/// filesystem if one exists. Missing or unreadable files leave the
720+
/// in-memory state at its defaults.
721+
#[cfg(not(test))]
722+
fn load_persistent_pin_state(&mut self) {
723+
let path = match PathBuf::try_from(PIN_STATE_STORE_PATH) {
724+
Ok(p) => p,
725+
Err(_) => return,
726+
};
727+
let reply = match try_syscall!(self.client.read_file(Location::Internal, path)) {
728+
Ok(reply) => reply,
729+
Err(_) => return,
730+
};
731+
let data = reply.data.as_slice();
732+
if data.is_empty() {
733+
return;
734+
}
735+
match from_reader::<StoredPinState, _>(data) {
736+
Ok(stored) => {
737+
self.pin_state = PinState::from_stored(stored);
738+
}
739+
Err(_) => {
740+
log::warn!("pin-state.cbor is unreadable; starting with defaults");
741+
}
742+
}
743+
}
744+
745+
#[cfg(test)]
746+
fn load_persistent_pin_state(&mut self) {}
747+
748+
/// Persist the current PIN state. Called after every CTAP handler that
749+
/// can mutate the PIN/retries/lockout fields so the CLI and the next
750+
/// daemon attach observe a consistent view.
751+
#[cfg(not(test))]
752+
fn save_persistent_pin_state(&mut self) {
753+
let path = match PathBuf::try_from(PIN_STATE_STORE_PATH) {
754+
Ok(p) => p,
755+
Err(_) => return,
756+
};
757+
let stored = StoredPinState::snapshot(&self.pin_state);
758+
let mut encoded = Vec::new();
759+
if into_writer(&stored, &mut encoded).is_err() {
760+
return;
761+
}
762+
let message = match Message::from_slice(&encoded) {
763+
Ok(m) => m,
764+
Err(_) => return,
765+
};
766+
let _ = try_syscall!(self.client.write_file(Location::Internal, path, message, None));
767+
}
768+
769+
#[cfg(test)]
770+
fn save_persistent_pin_state(&mut self) {}
771+
680772
fn as_array<const N: usize>(bytes: &[u8]) -> Result<[u8; N], u8> {
681773
if bytes.len() != N {
682774
return Err(CTAP1_ERR_INVALID_PARAMETER);
@@ -845,6 +937,7 @@ where
845937
let hash = Self::hash_pin(&new_pin);
846938
new_pin.zeroize();
847939
self.pin_state.set_pin(hash);
940+
self.save_persistent_pin_state();
848941
Ok(vec![CTAP2_OK])
849942
}
850943

@@ -885,6 +978,7 @@ where
885978
let current_hash = Self::as_array::<16>(&current_plain)?;
886979
current_plain.zeroize();
887980
if let Err(err) = self.pin_state.verify_pin_hash(&current_hash) {
981+
self.save_persistent_pin_state();
888982
return Err(err);
889983
}
890984

@@ -894,6 +988,7 @@ where
894988
let hash = Self::hash_pin(&new_pin);
895989
new_pin.zeroize();
896990
self.pin_state.set_pin(hash);
991+
self.save_persistent_pin_state();
897992
Ok(vec![CTAP2_OK])
898993
}
899994

@@ -932,8 +1027,10 @@ where
9321027
let current_hash = Self::as_array::<16>(&plain)?;
9331028
plain.zeroize();
9341029
if let Err(err) = self.pin_state.verify_pin_hash(&current_hash) {
1030+
self.save_persistent_pin_state();
9351031
return Err(err);
9361032
}
1033+
self.save_persistent_pin_state();
9371034

9381035
let random = syscall!(self.client.random_bytes(32)).bytes;
9391036
if random.len() != 32 {

0 commit comments

Comments
 (0)