@@ -4,7 +4,10 @@ use std::{
44} ;
55
66use authenticator:: ctap:: CtapApp ;
7- use transport_core:: state:: { IdentityConfig , PersistentStore } ;
7+ use sha2:: { Digest , Sha256 } ;
8+ use transport_core:: state:: {
9+ reset_state_dir, IdentityConfig , PersistentStore , StoredPinState , DEFAULT_PIN_RETRIES ,
10+ } ;
811use transport_core:: { set_waiting, Apps as TrussedApps , Builder , Options , Platform , Syscall } ;
912use trussed:: {
1013 backend:: { CoreOnly , NoId } ,
@@ -163,3 +166,137 @@ pub fn default_identity() -> IdentityStrings {
163166pub fn ensure_state_dir ( path : & Path ) -> io:: Result < ( ) > {
164167 transport_core:: state:: ensure_state_dir ( path)
165168}
169+
170+ /// Minimum PIN length enforced by the CTAP layer.
171+ const MIN_PIN_LENGTH : usize = 4 ;
172+ /// Maximum PIN length permitted by CTAP (63 byte UTF-8 string max).
173+ const MAX_PIN_LENGTH : usize = 63 ;
174+
175+ fn validate_pin ( pin : & str ) -> io:: Result < ( ) > {
176+ let bytes = pin. as_bytes ( ) ;
177+ if bytes. len ( ) < MIN_PIN_LENGTH {
178+ return Err ( io:: Error :: new (
179+ io:: ErrorKind :: InvalidInput ,
180+ format ! ( "PIN must be at least {MIN_PIN_LENGTH} bytes" ) ,
181+ ) ) ;
182+ }
183+ if bytes. len ( ) > MAX_PIN_LENGTH {
184+ return Err ( io:: Error :: new (
185+ io:: ErrorKind :: InvalidInput ,
186+ format ! ( "PIN must be at most {MAX_PIN_LENGTH} bytes" ) ,
187+ ) ) ;
188+ }
189+ Ok ( ( ) )
190+ }
191+
192+ fn hash_pin ( pin : & str ) -> [ u8 ; 16 ] {
193+ let digest = Sha256 :: digest ( pin. as_bytes ( ) ) ;
194+ let mut out = [ 0u8 ; 16 ] ;
195+ out. copy_from_slice ( & digest[ ..16 ] ) ;
196+ out
197+ }
198+
199+ /// Summary view of the persistent PIN state for the `pin status` CLI.
200+ pub struct PinInfo {
201+ pub is_set : bool ,
202+ pub retries : u8 ,
203+ pub blocked : bool ,
204+ }
205+
206+ /// Read the persistent PIN summary for the CLI.
207+ pub fn pin_info ( state_dir : & Path ) -> io:: Result < PinInfo > {
208+ let store = PersistentStore :: new ( state_dir) ?;
209+ let state = store. read_pin_state ( ) ?;
210+ Ok ( PinInfo {
211+ is_set : state. pin_hash . is_some ( ) ,
212+ retries : state. pin_retries ,
213+ blocked : state. pin_auth_blocked ,
214+ } )
215+ }
216+
217+ /// Persist a brand-new PIN. Fails if a PIN is already set.
218+ pub fn pin_set ( state_dir : & Path , new_pin : & str ) -> io:: Result < ( ) > {
219+ validate_pin ( new_pin) ?;
220+ let store = PersistentStore :: new ( state_dir) ?;
221+ let existing = store. read_pin_state ( ) ?;
222+ if existing. pin_hash . is_some ( ) {
223+ return Err ( io:: Error :: new (
224+ io:: ErrorKind :: AlreadyExists ,
225+ "a PIN is already set; use 'pin change' instead" ,
226+ ) ) ;
227+ }
228+ let updated = StoredPinState {
229+ pin_hash : Some ( hash_pin ( new_pin) ) ,
230+ pin_retries : DEFAULT_PIN_RETRIES ,
231+ consecutive_failures : 0 ,
232+ pin_auth_blocked : false ,
233+ } ;
234+ store. write_pin_state ( & updated)
235+ }
236+
237+ /// Persist a replacement PIN after verifying the current one.
238+ pub fn pin_change ( state_dir : & Path , current_pin : & str , new_pin : & str ) -> io:: Result < ( ) > {
239+ validate_pin ( new_pin) ?;
240+ let store = PersistentStore :: new ( state_dir) ?;
241+ let mut state = store. read_pin_state ( ) ?;
242+ verify_current_pin ( & mut state, current_pin) ?;
243+ state. pin_hash = Some ( hash_pin ( new_pin) ) ;
244+ state. pin_retries = DEFAULT_PIN_RETRIES ;
245+ state. consecutive_failures = 0 ;
246+ state. pin_auth_blocked = false ;
247+ store. write_pin_state ( & state)
248+ }
249+
250+ /// Clear the PIN after verifying the current one.
251+ pub fn pin_remove ( state_dir : & Path , current_pin : & str ) -> io:: Result < ( ) > {
252+ let store = PersistentStore :: new ( state_dir) ?;
253+ let mut state = store. read_pin_state ( ) ?;
254+ verify_current_pin ( & mut state, current_pin) ?;
255+ state. pin_hash = None ;
256+ state. pin_retries = DEFAULT_PIN_RETRIES ;
257+ state. consecutive_failures = 0 ;
258+ state. pin_auth_blocked = false ;
259+ store. write_pin_state ( & state)
260+ }
261+
262+ /// Wipe credentials and reset PIN state. Equivalent to a factory reset.
263+ pub fn reset_state ( state_dir : & Path ) -> io:: Result < ( ) > {
264+ reset_state_dir ( state_dir)
265+ }
266+
267+ fn verify_current_pin ( state : & mut StoredPinState , candidate : & str ) -> io:: Result < ( ) > {
268+ let stored = state. pin_hash . ok_or_else ( || {
269+ io:: Error :: new ( io:: ErrorKind :: PermissionDenied , "no PIN is currently set" )
270+ } ) ?;
271+ if state. pin_auth_blocked {
272+ return Err ( io:: Error :: new (
273+ io:: ErrorKind :: PermissionDenied ,
274+ "PIN is blocked until the authenticator is reset" ,
275+ ) ) ;
276+ }
277+ let provided = hash_pin ( candidate) ;
278+ let mut diff = 0u8 ;
279+ for ( a, b) in stored. iter ( ) . zip ( provided. iter ( ) ) {
280+ diff |= a ^ b;
281+ }
282+ if diff == 0 {
283+ state. pin_retries = DEFAULT_PIN_RETRIES ;
284+ state. consecutive_failures = 0 ;
285+ Ok ( ( ) )
286+ } else {
287+ if state. pin_retries > 0 {
288+ state. pin_retries -= 1 ;
289+ }
290+ state. consecutive_failures = state. consecutive_failures . saturating_add ( 1 ) ;
291+ if state. pin_retries == 0 {
292+ state. pin_auth_blocked = true ;
293+ }
294+ Err ( io:: Error :: new (
295+ io:: ErrorKind :: PermissionDenied ,
296+ format ! (
297+ "PIN is incorrect ({} retries remaining)" ,
298+ state. pin_retries
299+ ) ,
300+ ) )
301+ }
302+ }
0 commit comments