@@ -192,6 +192,8 @@ const COSE_ALG_ES256: i32 = -7;
192192const CREDENTIAL_STORE_PATH : & str = "credentials.cbor" ;
193193#[ cfg( not( test) ) ]
194194const ATTESTATION_STORE_PATH : & str = "attestation.cbor" ;
195+ #[ cfg( not( test) ) ]
196+ const PIN_STATE_STORE_PATH : & str = "pin-state.cbor" ;
195197pub ( crate ) const PIN_UV_AUTH_PROTOCOL_CLASSIC_V1 : i32 = 1 ;
196198pub ( 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+
245269impl 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