soft_fido2/
pin.rs

1//! CTAP2 PIN Protocol Support
2//!
3//! Provides PIN/UV authentication protocol implementation for CTAP2.
4
5use crate::error::{Error, Result};
6use crate::request::PinUvAuthProtocol;
7use crate::transport::Transport;
8
9use soft_fido2_crypto::pin_protocol;
10use soft_fido2_ctap::SecBytes;
11use soft_fido2_ctap::cbor::{MapBuilder, Value};
12
13use p256::elliptic_curve::sec1::ToEncodedPoint;
14use p256::{PublicKey as P256PublicKey, SecretKey as P256SecretKey};
15use rand::rngs::OsRng;
16use zeroize::Zeroizing;
17
18/// PIN protocol version
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PinProtocol {
21    /// PIN protocol version 1 (AES-256-CBC + HMAC-SHA-256)
22    V1,
23    /// PIN protocol version 2 (HMAC-SECRET)
24    V2,
25}
26
27impl From<PinProtocol> for PinUvAuthProtocol {
28    fn from(protocol: PinProtocol) -> Self {
29        match protocol {
30            PinProtocol::V1 => PinUvAuthProtocol::V1,
31            PinProtocol::V2 => PinUvAuthProtocol::V2,
32        }
33    }
34}
35
36impl From<PinUvAuthProtocol> for PinProtocol {
37    fn from(protocol: PinUvAuthProtocol) -> Self {
38        match protocol {
39            PinUvAuthProtocol::V1 => PinProtocol::V1,
40            PinUvAuthProtocol::V2 => PinProtocol::V2,
41        }
42    }
43}
44
45/// PIN/UV authentication encapsulation
46pub struct PinUvAuthEncapsulation {
47    protocol: PinProtocol,
48    /// Platform's ECDH secret key (32 bytes, memory-protected)
49    platform_secret: Option<SecBytes>,
50    /// Platform's public key (65 bytes uncompressed, not secret)
51    platform_public: Option<[u8; 65]>,
52    /// Authenticator's public key (from getKeyAgreement)
53    authenticator_key: Option<P256PublicKey>,
54    /// Shared secret derived from ECDH (32 bytes, memory-protected)
55    shared_secret: Option<SecBytes>,
56    /// PIN token (32 bytes, memory-protected)
57    pin_token: Option<SecBytes>,
58}
59
60impl PinUvAuthEncapsulation {
61    /// Create a new PIN/UV authentication encapsulation
62    ///
63    /// # Arguments
64    ///
65    /// * `transport` - The transport to use for initialization (performs key agreement)
66    /// * `protocol` - The PIN protocol version to use
67    pub fn new(transport: &mut Transport, protocol: PinProtocol) -> Result<Self> {
68        let mut encap = Self {
69            protocol,
70            platform_secret: None,
71            platform_public: None,
72            authenticator_key: None,
73            shared_secret: None,
74            pin_token: None,
75        };
76
77        // Perform key agreement immediately
78        encap.initialize(transport)?;
79
80        Ok(encap)
81    }
82
83    /// Initialize the encapsulation by performing key agreement
84    ///
85    /// This sends clientPin subcommand 0x02 (getKeyAgreement) to the authenticator.
86    pub fn initialize(&mut self, transport: &mut Transport) -> Result<()> {
87        // Generate platform key pair (using SecretKey for persistence)
88        let platform_secret_key = P256SecretKey::random(&mut OsRng);
89        let platform_public_key = platform_secret_key.public_key();
90        let platform_public_point = platform_public_key.to_encoded_point(false);
91
92        // Store platform secret key (memory-protected)
93        let secret_bytes: [u8; 32] = *platform_secret_key.to_bytes().as_ref();
94        self.platform_secret = Some(SecBytes::from_slice(&secret_bytes));
95
96        let public_bytes = platform_public_point.as_bytes();
97        let mut public_array = [0u8; 65];
98        public_array.copy_from_slice(public_bytes);
99        self.platform_public = Some(public_array);
100
101        // Build getKeyAgreement request using MapBuilder
102        let protocol_version = match self.protocol {
103            PinProtocol::V1 => 1u8,
104            PinProtocol::V2 => 2u8,
105        };
106
107        let request_bytes = MapBuilder::new()
108            .insert(1, protocol_version) // pinUvAuthProtocol
109            .map_err(|_| Error::Other)?
110            .insert(2, 0x02u8) // subCommand (getKeyAgreement = 0x02)
111            .map_err(|_| Error::Other)?
112            .build()
113            .map_err(|_| Error::Other)?;
114
115        // Send clientPin command (0x06) with 30s timeout
116        let response = transport.send_ctap_command(0x06, &request_bytes, 30000)?;
117
118        // Transport layer already checked status byte and returns only CBOR data for success
119        if response.is_empty() {
120            return Err(Error::Other);
121        }
122
123        // Parse CBOR response (entire response is CBOR data)
124        let response_value: Value =
125            soft_fido2_ctap::cbor::decode(&response).map_err(|_| Error::Other)?;
126
127        // Extract keyAgreement from response (should be in response[0x01])
128        let authenticator_cose_key = match response_value {
129            Value::Map(map) => map
130                .iter()
131                .find(|(k, _)| matches!(k, Value::Integer(i) if *i == 1.into()))
132                .map(|(_, v)| v.clone())
133                .ok_or(Error::Other)?,
134            _ => return Err(Error::Other),
135        };
136
137        // Parse COSE key to get P-256 public key
138        let authenticator_public_key = Self::parse_cose_key(&authenticator_cose_key)?;
139
140        // Perform ECDH to derive shared secret
141        use p256::ecdh::diffie_hellman;
142        let shared_secret = diffie_hellman(
143            platform_secret_key.to_nonzero_scalar(),
144            authenticator_public_key.as_affine(),
145        );
146
147        // Store authenticator key and shared secret (memory-protected)
148        self.authenticator_key = Some(authenticator_public_key);
149        let shared_secret_bytes = shared_secret.raw_secret_bytes();
150        self.shared_secret = Some(SecBytes::from_slice(shared_secret_bytes.as_slice()));
151
152        Ok(())
153    }
154
155    /// Get a PIN/UV auth token using PIN with permissions
156    ///
157    /// This is the recommended way to get a PIN token in CTAP 2.1.
158    ///
159    /// # Arguments
160    ///
161    /// * `transport` - The transport to communicate with the authenticator
162    /// * `pin` - The user's PIN
163    /// * `permissions` - Permission flags (0x01 = makeCredential, 0x02 = getAssertion, etc.)
164    /// * `rp_id` - Optional RP ID to scope the permission
165    pub fn get_pin_uv_auth_token_using_pin_with_permissions(
166        &mut self,
167        transport: &mut Transport,
168        pin: &str,
169        permissions: u8,
170        rp_id: Option<&str>,
171    ) -> Result<Vec<u8>> {
172        let shared_secret = self.shared_secret.as_ref().ok_or(Error::Other)?;
173
174        // Compute PIN hash (zeroized on drop)
175        let pin_hash = Zeroizing::new({
176            use sha2::{Digest, Sha256};
177            let hash: [u8; 32] = Sha256::digest(pin.as_bytes()).into();
178            hash
179        });
180
181        // Derive keys (zeroized on drop)
182        let (enc_key, _hmac_key) = self.derive_keys_zeroized(shared_secret.as_slice())?;
183
184        let pin_hash_enc =
185            match self.protocol {
186                PinProtocol::V1 => pin_protocol::v1::encrypt(&enc_key, &pin_hash[..16])
187                    .map_err(|_| Error::Other)?,
188                PinProtocol::V2 => pin_protocol::v2::encrypt(&enc_key, &pin_hash[..16])
189                    .map_err(|_| Error::Other)?,
190            };
191
192        // Get platform key agreement parameter
193        let platform_key_agreement = self.get_key_agreement_cose()?;
194
195        let protocol_version = match self.protocol {
196            PinProtocol::V1 => 1u8,
197            PinProtocol::V2 => 2u8,
198        };
199
200        let mut builder = MapBuilder::new();
201        builder = builder
202            .insert(1, protocol_version)
203            .map_err(|_| Error::Other)?;
204        builder = builder
205            .insert(2, 0x09u8) // subCommand (getPinUvAuthTokenUsingPinWithPermissions)
206            .map_err(|_| Error::Other)?;
207        builder = builder
208            .insert(3, &platform_key_agreement)
209            .map_err(|_| Error::Other)?;
210        builder = builder
211            .insert_bytes(6, &pin_hash_enc)
212            .map_err(|_| Error::Other)?;
213        builder = builder.insert(9, permissions).map_err(|_| Error::Other)?;
214
215        if let Some(rp_id_str) = rp_id {
216            builder = builder.insert(10, rp_id_str).map_err(|_| Error::Other)?;
217        }
218
219        let request_bytes = builder.build().map_err(|_| Error::Other)?;
220
221        let response = transport.send_ctap_command(0x06, &request_bytes, 30000)?;
222
223        if response.is_empty() {
224            return Err(Error::Other);
225        }
226
227        let response_value: Value =
228            soft_fido2_ctap::cbor::decode(&response).map_err(|_| Error::Other)?;
229
230        let pin_token_enc = match response_value {
231            Value::Map(map) => map
232                .iter()
233                .find(|(k, _)| matches!(k, Value::Integer(i) if *i == 2.into()))
234                .and_then(|(_, v)| match v {
235                    Value::Bytes(b) => Some(b.clone()),
236                    _ => None,
237                })
238                .ok_or(Error::Other)?,
239            _ => return Err(Error::Other),
240        };
241
242        // Decrypt PIN token
243        let pin_token = Zeroizing::new(match self.protocol {
244            PinProtocol::V1 => {
245                let decrypted = pin_protocol::v1::decrypt(&enc_key, &pin_token_enc)
246                    .map_err(|_| Error::Other)?;
247                let mut token = [0u8; 32];
248                token.copy_from_slice(&decrypted[..32]);
249                token
250            }
251            PinProtocol::V2 => {
252                let decrypted = pin_protocol::v2::decrypt(&enc_key, &pin_token_enc)
253                    .map_err(|_| Error::Other)?;
254                let mut token = [0u8; 32];
255                token.copy_from_slice(&decrypted[..32]);
256                token
257            }
258        });
259
260        // Store PIN token (memory-protected)
261        self.pin_token = Some(SecBytes::from_slice(&*pin_token));
262
263        Ok(pin_token.to_vec())
264    }
265
266    /// Get PIN/UV auth token using built-in user verification (UV)
267    ///
268    /// This method uses the authenticator's built-in user verification (biometric/fingerprint)
269    /// instead of PIN authentication. This is useful when:
270    /// - No PIN is set on the authenticator
271    /// - The authenticator supports biometric UV
272    /// - UV is preferred over PIN
273    ///
274    /// # Arguments
275    ///
276    /// * `transport` - The transport to communicate with the authenticator
277    /// * `permissions` - Permission flags (0x01 = makeCredential, 0x02 = getAssertion, etc.)
278    /// * `rp_id` - Optional RP ID to scope the permission
279    pub fn get_pin_uv_auth_token_using_uv_with_permissions(
280        &mut self,
281        transport: &mut Transport,
282        permissions: u8,
283        rp_id: Option<&str>,
284    ) -> Result<Vec<u8>> {
285        // Get platform key agreement parameter
286        let platform_key_agreement = self.get_key_agreement_cose()?;
287
288        // Build getPinUvAuthTokenUsingUvWithPermissions request
289        let protocol_version = match self.protocol {
290            PinProtocol::V1 => 1u8,
291            PinProtocol::V2 => 2u8,
292        };
293
294        let mut builder = MapBuilder::new();
295        builder = builder
296            .insert(1, protocol_version) // pinUvAuthProtocol
297            .map_err(|_| Error::Other)?;
298        builder = builder
299            .insert(2, 0x06u8) // subCommand (getPinUvAuthTokenUsingUvWithPermissions = 0x06)
300            .map_err(|_| Error::Other)?;
301        builder = builder
302            .insert(3, &platform_key_agreement) // keyAgreement
303            .map_err(|_| Error::Other)?;
304        builder = builder
305            .insert(9, permissions) // permissions
306            .map_err(|_| Error::Other)?;
307
308        if let Some(rp_id_str) = rp_id {
309            builder = builder
310                .insert(10, rp_id_str) // rpId
311                .map_err(|_| Error::Other)?;
312        }
313
314        let request_bytes = builder.build().map_err(|_| Error::Other)?;
315
316        // Send clientPin command (0x06) with 30s timeout
317        let response = transport.send_ctap_command(0x06, &request_bytes, 30000)?;
318
319        // Transport layer already checked status byte and returns only CBOR data for success
320        if response.is_empty() {
321            return Err(Error::Other);
322        }
323
324        // Parse CBOR response (entire response is CBOR data)
325        let response_value: Value =
326            soft_fido2_ctap::cbor::decode(&response).map_err(|_| Error::Other)?;
327
328        let pin_token_enc = match response_value {
329            Value::Map(map) => map
330                .iter()
331                .find(|(k, _)| matches!(k, Value::Integer(i) if *i == 2.into()))
332                .and_then(|(_, v)| match v {
333                    Value::Bytes(b) => Some(b.clone()),
334                    _ => None,
335                })
336                .ok_or(Error::Other)?,
337            _ => return Err(Error::Other),
338        };
339
340        // Get shared secret and derive keys (zeroized on drop)
341        let shared_secret = self.shared_secret.as_ref().ok_or(Error::Other)?;
342        let (enc_key, _hmac_key) = self.derive_keys_zeroized(shared_secret.as_slice())?;
343
344        // Decrypt PIN token (zeroized on drop)
345        let pin_token = Zeroizing::new(match self.protocol {
346            PinProtocol::V1 => {
347                let decrypted = pin_protocol::v1::decrypt(&enc_key, &pin_token_enc)
348                    .map_err(|_| Error::Other)?;
349                let mut token = [0u8; 32];
350                token.copy_from_slice(&decrypted[..32]);
351                token
352            }
353            PinProtocol::V2 => {
354                let decrypted = pin_protocol::v2::decrypt(&enc_key, &pin_token_enc)
355                    .map_err(|_| Error::Other)?;
356                let mut token = [0u8; 32];
357                token.copy_from_slice(&decrypted[..32]);
358                token
359            }
360        });
361
362        // Store PIN token (memory-protected)
363        self.pin_token = Some(SecBytes::from_slice(&*pin_token));
364
365        Ok(pin_token.to_vec())
366    }
367
368    /// Calculate pinUvAuthParam for a request
369    ///
370    /// # Arguments
371    ///
372    /// * `data` - The data to authenticate (e.g., clientDataHash || rpIdHash for makeCredential)
373    /// * `pin_token` - The PIN token obtained from get_pin_uv_auth_token_using_pin_with_permissions
374    pub fn authenticate(&self, data: &[u8], pin_token: &[u8]) -> Result<Vec<u8>> {
375        let pin_token_array: &[u8; 32] = pin_token.try_into().map_err(|_| Error::Other)?;
376        let result = match self.protocol {
377            PinProtocol::V1 => pin_protocol::v1::authenticate(pin_token_array, data).to_vec(),
378            PinProtocol::V2 => pin_protocol::v2::authenticate(pin_token_array, data).to_vec(),
379        };
380
381        Ok(result)
382    }
383
384    /// Get the platform's key agreement parameter in COSE format
385    fn get_key_agreement_cose(&self) -> Result<Value> {
386        let secret_bytes = self.platform_secret.as_ref().ok_or(Error::Other)?;
387        let secret_arr = secret_bytes.to_array::<32>().ok_or(Error::Other)?;
388        let secret_key =
389            P256SecretKey::from_bytes((&*secret_arr).into()).map_err(|_| Error::Other)?;
390        let public_key = secret_key.public_key();
391        let point = public_key.to_encoded_point(false);
392
393        let key_map = vec![
394            (Value::Integer(1.into()), Value::Integer(2.into())), // kty: EC2
395            (Value::Integer(3.into()), Value::Integer((-25).into())), // alg: ECDH-ES+HKDF-256
396            (Value::Integer((-1).into()), Value::Integer(1.into())), // crv: P-256
397            (
398                Value::Integer((-2).into()),
399                Value::Bytes(point.x().ok_or(Error::Other)?.to_vec()),
400            ), // x
401            (
402                Value::Integer((-3).into()),
403                Value::Bytes(point.y().ok_or(Error::Other)?.to_vec()),
404            ), // y
405        ];
406
407        Ok(Value::Map(key_map))
408    }
409
410    /// Derive encryption and HMAC keys from shared secret (zeroized on drop)
411    #[allow(clippy::type_complexity)]
412    fn derive_keys_zeroized(
413        &self,
414        shared_secret: &[u8],
415    ) -> Result<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>)> {
416        // Convert slice to fixed-size array
417        let secret_arr: &[u8; 32] = shared_secret.try_into().map_err(|_| Error::Other)?;
418
419        match self.protocol {
420            PinProtocol::V1 => {
421                let (enc, hmac) = pin_protocol::v1::derive_keys(secret_arr);
422                Ok((enc, hmac))
423            }
424            PinProtocol::V2 => {
425                let enc = pin_protocol::v2::derive_encryption_key(secret_arr);
426                let hmac = pin_protocol::v2::derive_hmac_key(secret_arr);
427                Ok((enc, hmac))
428            }
429        }
430    }
431
432    /// Parse a COSE key to extract P-256 public key
433    fn parse_cose_key(cose_key: &Value) -> Result<P256PublicKey> {
434        let map = match cose_key {
435            Value::Map(m) => m,
436            _ => return Err(Error::Other),
437        };
438
439        // Extract x and y coordinates
440        let x = map
441            .iter()
442            .find(|(k, _)| matches!(k, Value::Integer(i) if *i == (-2).into()))
443            .and_then(|(_, v)| match v {
444                Value::Bytes(b) => Some(b.clone()),
445                _ => None,
446            })
447            .ok_or(Error::Other)?;
448
449        let y = map
450            .iter()
451            .find(|(k, _)| matches!(k, Value::Integer(i) if *i == (-3).into()))
452            .and_then(|(_, v)| match v {
453                Value::Bytes(b) => Some(b.clone()),
454                _ => None,
455            })
456            .ok_or(Error::Other)?;
457
458        // Create uncompressed SEC1 encoding: 0x04 || x || y
459        let mut uncompressed = vec![0x04];
460        uncompressed.extend_from_slice(&x);
461        uncompressed.extend_from_slice(&y);
462
463        // Parse as P-256 public key
464        use p256::elliptic_curve::sec1::FromEncodedPoint;
465        let point = p256::EncodedPoint::from_bytes(&uncompressed).map_err(|_| Error::Other)?;
466
467        // CtOption::into() returns Option
468        let public_key: Option<P256PublicKey> = P256PublicKey::from_encoded_point(&point).into();
469        public_key.ok_or(Error::Other)
470    }
471
472    /// Set a new PIN on the authenticator
473    ///
474    /// This method sets the initial PIN on an authenticator that doesn't have one set.
475    /// The PIN must be 4-63 UTF-8 characters.
476    ///
477    /// # Arguments
478    ///
479    /// * `transport` - The transport to communicate with the authenticator
480    /// * `new_pin` - The new PIN to set (4-63 UTF-8 characters)
481    ///
482    /// # Errors
483    ///
484    /// Returns an error if:
485    /// - A PIN is already set on the authenticator
486    /// - The PIN length is invalid (must be 4-63 characters)
487    /// - The shared secret hasn't been established (call initialize first)
488    /// - Communication with the authenticator fails
489    pub fn set_pin(&mut self, transport: &mut Transport, new_pin: &str) -> Result<()> {
490        let shared_secret = self.shared_secret.as_ref().ok_or(Error::Other)?;
491
492        // Validate PIN length (CTAP spec: 4-63 Unicode code points)
493        let pin_len = new_pin.chars().count();
494        if !(4..=63).contains(&pin_len) {
495            return Err(Error::InvalidPinLength);
496        }
497
498        // Pad PIN to 64 bytes with zeros (CTAP spec requirement, zeroized on drop)
499        let padded_pin = Zeroizing::new({
500            let mut buf = [0u8; 64];
501            let pin_bytes = new_pin.as_bytes();
502            if pin_bytes.len() > 64 {
503                return Err(Error::InvalidPinLength);
504            }
505            buf[..pin_bytes.len()].copy_from_slice(pin_bytes);
506            buf
507        });
508
509        // Derive keys (zeroized on drop)
510        let (enc_key, hmac_key) = self.derive_keys_zeroized(shared_secret.as_slice())?;
511
512        // Encrypt padded PIN
513        let new_pin_enc = match self.protocol {
514            PinProtocol::V1 => {
515                pin_protocol::v1::encrypt(&enc_key, &*padded_pin).map_err(|_| Error::Other)?
516            }
517            PinProtocol::V2 => {
518                pin_protocol::v2::encrypt(&enc_key, &*padded_pin).map_err(|_| Error::Other)?
519            }
520        };
521
522        // Compute pinUvAuthParam = HMAC(hmac_key, newPinEnc)
523        let pin_uv_auth_param = match self.protocol {
524            PinProtocol::V1 => pin_protocol::v1::authenticate(&hmac_key, &new_pin_enc).to_vec(),
525            PinProtocol::V2 => pin_protocol::v2::authenticate(&hmac_key, &new_pin_enc).to_vec(),
526        };
527
528        // Get platform key agreement parameter
529        let platform_key_agreement = self.get_key_agreement_cose()?;
530
531        // Build setPin request
532        let protocol_version = match self.protocol {
533            PinProtocol::V1 => 1u8,
534            PinProtocol::V2 => 2u8,
535        };
536
537        let request_bytes = MapBuilder::new()
538            .insert(1, protocol_version) // pinUvAuthProtocol
539            .map_err(|_| Error::Other)?
540            .insert(2, 0x03u8) // subCommand (setPin = 0x03)
541            .map_err(|_| Error::Other)?
542            .insert(3, &platform_key_agreement) // keyAgreement
543            .map_err(|_| Error::Other)?
544            .insert_bytes(4, &pin_uv_auth_param) // pinUvAuthParam
545            .map_err(|_| Error::Other)?
546            .insert_bytes(5, &new_pin_enc) // newPinEnc
547            .map_err(|_| Error::Other)?
548            .build()
549            .map_err(|_| Error::Other)?;
550
551        // Send clientPin command (0x06) with 30s timeout
552        let _response = transport.send_ctap_command(0x06, &request_bytes, 30000)?;
553
554        // Success - empty response means PIN was set
555        Ok(())
556    }
557
558    /// Change the PIN on the authenticator
559    ///
560    /// This method changes an existing PIN to a new one.
561    /// Both PINs must be 4-63 UTF-8 characters.
562    ///
563    /// # Arguments
564    ///
565    /// * `transport` - The transport to communicate with the authenticator
566    /// * `current_pin` - The current PIN
567    /// * `new_pin` - The new PIN to set (4-63 UTF-8 characters)
568    ///
569    /// # Errors
570    ///
571    /// Returns an error if:
572    /// - No PIN is set on the authenticator
573    /// - The current PIN is incorrect
574    /// - The new PIN length is invalid (must be 4-63 characters)
575    /// - The shared secret hasn't been established (call initialize first)
576    /// - Communication with the authenticator fails
577    pub fn change_pin(
578        &mut self,
579        transport: &mut Transport,
580        current_pin: &str,
581        new_pin: &str,
582    ) -> Result<()> {
583        let shared_secret = self.shared_secret.as_ref().ok_or(Error::Other)?;
584
585        // Validate new PIN length (CTAP spec: 4-63 Unicode code points)
586        let pin_len = new_pin.chars().count();
587        if !(4..=63).contains(&pin_len) {
588            return Err(Error::InvalidPinLength);
589        }
590
591        // Compute current PIN hash (SHA-256, zeroized on drop)
592        let current_pin_hash = Zeroizing::new({
593            use sha2::{Digest, Sha256};
594            let hash: [u8; 32] = Sha256::digest(current_pin.as_bytes()).into();
595            hash
596        });
597
598        // Pad new PIN to 64 bytes with zeros (zeroized on drop)
599        let padded_new_pin = Zeroizing::new({
600            let mut buf = [0u8; 64];
601            let new_pin_bytes = new_pin.as_bytes();
602            if new_pin_bytes.len() > 64 {
603                return Err(Error::InvalidPinLength);
604            }
605            buf[..new_pin_bytes.len()].copy_from_slice(new_pin_bytes);
606            buf
607        });
608
609        // Derive keys (zeroized on drop)
610        let (enc_key, hmac_key) = self.derive_keys_zeroized(shared_secret.as_slice())?;
611
612        // Encrypt current PIN hash (first 16 bytes per CTAP spec)
613        let pin_hash_enc = match self.protocol {
614            PinProtocol::V1 => pin_protocol::v1::encrypt(&enc_key, &current_pin_hash[..16])
615                .map_err(|_| Error::Other)?,
616            PinProtocol::V2 => pin_protocol::v2::encrypt(&enc_key, &current_pin_hash[..16])
617                .map_err(|_| Error::Other)?,
618        };
619
620        // Encrypt new padded PIN
621        let new_pin_enc =
622            match self.protocol {
623                PinProtocol::V1 => pin_protocol::v1::encrypt(&enc_key, &*padded_new_pin)
624                    .map_err(|_| Error::Other)?,
625                PinProtocol::V2 => pin_protocol::v2::encrypt(&enc_key, &*padded_new_pin)
626                    .map_err(|_| Error::Other)?,
627            };
628
629        // Compute pinUvAuthParam = HMAC(hmac_key, newPinEnc || pinHashEnc)
630        let mut verify_data = new_pin_enc.clone();
631        verify_data.extend_from_slice(&pin_hash_enc);
632
633        let pin_uv_auth_param = match self.protocol {
634            PinProtocol::V1 => pin_protocol::v1::authenticate(&hmac_key, &verify_data).to_vec(),
635            PinProtocol::V2 => pin_protocol::v2::authenticate(&hmac_key, &verify_data).to_vec(),
636        };
637
638        // Get platform key agreement parameter
639        let platform_key_agreement = self.get_key_agreement_cose()?;
640
641        // Build changePin request
642        let protocol_version = match self.protocol {
643            PinProtocol::V1 => 1u8,
644            PinProtocol::V2 => 2u8,
645        };
646
647        let request_bytes = MapBuilder::new()
648            .insert(1, protocol_version) // pinUvAuthProtocol
649            .map_err(|_| Error::Other)?
650            .insert(2, 0x04u8) // subCommand (changePin = 0x04)
651            .map_err(|_| Error::Other)?
652            .insert(3, &platform_key_agreement) // keyAgreement
653            .map_err(|_| Error::Other)?
654            .insert_bytes(4, &pin_uv_auth_param) // pinUvAuthParam
655            .map_err(|_| Error::Other)?
656            .insert_bytes(5, &new_pin_enc) // newPinEnc
657            .map_err(|_| Error::Other)?
658            .insert_bytes(6, &pin_hash_enc) // pinHashEnc
659            .map_err(|_| Error::Other)?
660            .build()
661            .map_err(|_| Error::Other)?;
662
663        // Send clientPin command (0x06) with 30s timeout
664        let _response = transport.send_ctap_command(0x06, &request_bytes, 30000)?;
665
666        // Success - empty response means PIN was changed
667        Ok(())
668    }
669
670    /// Get PIN retries remaining
671    ///
672    /// Returns the number of PIN attempts remaining before the authenticator is blocked.
673    ///
674    /// # Arguments
675    ///
676    /// * `transport` - The transport to communicate with the authenticator
677    ///
678    /// # Returns
679    ///
680    /// The number of PIN retries remaining (typically 0-8)
681    pub fn get_pin_retries(&self, transport: &mut Transport) -> Result<u8> {
682        // Build getPinRetries request
683        let request_bytes = MapBuilder::new()
684            .insert(2, 0x01u8) // subCommand (getPinRetries = 0x01)
685            .map_err(|_| Error::Other)?
686            .build()
687            .map_err(|_| Error::Other)?;
688
689        // Send clientPin command (0x06) with 30s timeout
690        let response = transport.send_ctap_command(0x06, &request_bytes, 30000)?;
691
692        if response.is_empty() {
693            return Err(Error::Other);
694        }
695
696        // Parse CBOR response
697        let response_value: Value =
698            soft_fido2_ctap::cbor::decode(&response).map_err(|_| Error::Other)?;
699
700        // Extract pinRetries from response (key 0x03)
701        let retries = match response_value {
702            Value::Map(map) => map
703                .iter()
704                .find(|(k, _)| matches!(k, Value::Integer(i) if *i == 3.into()))
705                .and_then(|(_, v)| match v {
706                    Value::Integer(i) => {
707                        let val: i128 = *i;
708                        u8::try_from(val).ok()
709                    }
710                    _ => None,
711                })
712                .ok_or(Error::Other)?,
713            _ => return Err(Error::Other),
714        };
715
716        Ok(retries)
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723
724    #[test]
725    fn test_pin_protocol_conversion() {
726        assert_eq!(PinProtocol::from(PinUvAuthProtocol::V1), PinProtocol::V1);
727        assert_eq!(PinProtocol::from(PinUvAuthProtocol::V2), PinProtocol::V2);
728    }
729}