libwebauthn 0.5.0

FIDO2 (WebAuthn) and FIDO U2F platform library for Linux written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
use std::time::Duration;

use aes::cipher::{block_padding::NoPadding, BlockDecryptMut};
use async_trait::async_trait;
use cbc::cipher::{BlockEncryptMut, KeyIvInit};
use cosey as cose;
use hkdf::Hkdf;
use hmac::Mac;
use p256::{
    ecdh::EphemeralSecret, elliptic_curve::sec1::FromEncodedPoint, EncodedPoint,
    PublicKey as P256PublicKey,
};
use rand::{rngs::OsRng, thread_rng, Rng, SeedableRng};
use sha2::{Digest, Sha256};
use tracing::{error, instrument, warn};
use x509_parser::nom::AsBytes;

use crate::{
    proto::{
        ctap2::{Ctap2, Ctap2ClientPinRequest, Ctap2GetInfoResponse, Ctap2PinUvAuthProtocol},
        CtapError,
    },
    transport::Channel,
    webauthn::{
        error::{Error, PlatformError},
        pin_uv_auth_token::{obtain_pin, obtain_shared_secret, select_uv_proto},
    },
};

type Aes256CbcEncryptor = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDecryptor = cbc::Decryptor<aes::Aes256>;
type HmacSha256 = hmac::Hmac<Sha256>;

#[derive(Default, Debug)]
pub struct PinUvAuthToken {
    pub rpid: Option<String>,
    pub user_verified: bool,
    pub user_present: bool,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum PinRequestReason {
    /// The RP required a PIN
    RelyingPartyRequest,
    /// The device is configured to require a PIN
    AuthenticatorPolicy,
    /// Buitin UV failed and is temporarily blocked, and we have to enter a valid PIN to unblock it
    FallbackFromUV,
    // Passkey
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum PinNotSetReason {
    /// Device does not have a PIN set, but needs one
    PinNotSet,
    /// PIN too short
    PinTooShort,
    /// PIN too long
    PinTooLong,
    /// PIN violates PinPolicy
    PinPolicyViolation,
}

pub trait PinUvAuthProtocol: Send + Sync {
    fn version(&self) -> Ctap2PinUvAuthProtocol;

    /// encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
    ///   Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the
    ///   shared secret.
    fn encapsulate(
        &self,
        peer_public_key: &cose::PublicKey,
    ) -> Result<(cose::PublicKey, Vec<u8>), Error>;

    // encrypt(key, demPlaintext) → ciphertext
    //   Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
    //   The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
    fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error>;

    // decrypt(key, ciphertext) → plaintext | error
    //   Decrypts a ciphertext and returns the plaintext.
    fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error>;

    // authenticate(key, message) → signature
    //   Computes a MAC of the given message.
    fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
}

trait ECPrivateKeyPinUvAuthProtocol {
    fn private_key(&self) -> &EphemeralSecret;
    fn public_key(&self) -> &P256PublicKey;
    fn kdf(&self, bytes: &[u8]) -> Result<Vec<u8>, Error>;
}

/// Common functionality between ECDH-based PIN/UV auth protocols (1 & 2)
trait ECDHPinUvAuthProtocol {
    fn ecdh(&self, peer_public_key: &cose::PublicKey) -> Result<Vec<u8>, Error>;
    fn encapsulate(
        &self,
        peer_public_key: &cose::PublicKey,
    ) -> Result<(cose::PublicKey, Vec<u8>), Error>;
    fn get_public_key(&self) -> Result<cose::PublicKey, Error>;
}

pub struct PinUvAuthProtocolOne {
    private_key: EphemeralSecret,
    public_key: P256PublicKey,
}

impl Default for PinUvAuthProtocolOne {
    fn default() -> Self {
        Self::new()
    }
}

impl PinUvAuthProtocolOne {
    pub fn new() -> Self {
        let private_key = if cfg!(test) {
            // For testing only!!
            // We need a deterministic, seedable RNG to be able to "predict" crypto-operations
            let mut rng = rand::rngs::StdRng::seed_from_u64(42);
            EphemeralSecret::random(&mut rng)
        } else {
            EphemeralSecret::random(&mut OsRng)
        };
        let public_key = private_key.public_key();
        Self {
            private_key,
            public_key,
        }
    }
}

impl ECPrivateKeyPinUvAuthProtocol for PinUvAuthProtocolOne {
    fn private_key(&self) -> &EphemeralSecret {
        &self.private_key
    }

    fn public_key(&self) -> &P256PublicKey {
        &self.public_key
    }

    /// kdf(Z) → sharedSecret
    fn kdf(&self, bytes: &[u8]) -> Result<Vec<u8>, Error> {
        let mut hasher = Sha256::default();
        hasher.update(bytes);
        Ok(hasher.finalize().to_vec())
    }
}

impl<P> ECDHPinUvAuthProtocol for P
where
    P: ECPrivateKeyPinUvAuthProtocol,
{
    #[instrument(skip_all)]
    fn encapsulate(
        &self,
        peer_public_key: &cose::PublicKey,
    ) -> Result<(cose::PublicKey, Vec<u8>), Error> {
        // Let sharedSecret be the result of calling ecdh(peerCoseKey). Return any resulting error.
        let shared_secret = self.ecdh(peer_public_key)?;

        // Return(getPublicKey(), sharedSecret)
        Ok((self.get_public_key()?, shared_secret))
    }

    /// ecdh(peerCoseKey) → sharedSecret | error
    fn ecdh(&self, peer_public_key: &cose::PublicKey) -> Result<Vec<u8>, Error> {
        // Parse peerCoseKey as specified for getPublicKey, below, and produce a P-256 point, Y.
        // If unsuccessful, or if the resulting point is not on the curve, return error.
        let cose::PublicKey::EcdhEsHkdf256Key(peer_public_key) = peer_public_key else {
            error!(
                ?peer_public_key,
                "Unsupported peerCoseKey format. Only EcdhEsHkdf256Key is supported."
            );
            return Err(Error::Ctap(CtapError::Other));
        };
        // x and y must be exactly 32 bytes (P-256 field size). `cosey` accepts
        // any length up to 32; validate before converting to `&FieldBytes`.
        let x: &[u8; 32] = peer_public_key.x.as_bytes().try_into().map_err(|_| {
            error!(
                x_len = peer_public_key.x.as_bytes().len(),
                "Peer public key x coordinate is not 32 bytes"
            );
            Error::Ctap(CtapError::Other)
        })?;
        let y: &[u8; 32] = peer_public_key.y.as_bytes().try_into().map_err(|_| {
            error!(
                y_len = peer_public_key.y.as_bytes().len(),
                "Peer public key y coordinate is not 32 bytes"
            );
            Error::Ctap(CtapError::Other)
        })?;
        let encoded_point = EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
        let Some(peer_public_key) = P256PublicKey::from_encoded_point(&encoded_point).into() else {
            error!("Failed to parse public key.");
            return Err(Error::Ctap(CtapError::Other));
        };

        // Calculate xY, the shared point. (I.e. the scalar-multiplication of the peer’s point, Y, with the
        // local private key agreement key.)
        let shared = self.private_key().diffie_hellman(&peer_public_key);

        // Return kdf(Z).
        self.kdf(shared.raw_secret_bytes().as_bytes())
    }

    /// getPublicKey()
    fn get_public_key(&self) -> Result<cose::PublicKey, Error> {
        let point = EncodedPoint::from(self.public_key());
        let x_bytes = point.x().ok_or_else(|| {
            error!("Public key is the identity point");
            Error::Platform(PlatformError::CryptoError(
                "public key is the identity point".into(),
            ))
        })?;
        let y_bytes = point.y().ok_or_else(|| {
            error!("Public key is identity or compressed");
            Error::Platform(PlatformError::CryptoError(
                "public key is identity or compressed".into(),
            ))
        })?;
        let x: heapless::Vec<u8, 32> =
            heapless::Vec::from_slice(x_bytes.as_bytes()).map_err(|_| {
                Error::Platform(PlatformError::CryptoError(
                    "x coordinate exceeds 32 bytes".into(),
                ))
            })?;
        let y: heapless::Vec<u8, 32> =
            heapless::Vec::from_slice(y_bytes.as_bytes()).map_err(|_| {
                Error::Platform(PlatformError::CryptoError(
                    "y coordinate exceeds 32 bytes".into(),
                ))
            })?;
        Ok(cose::PublicKey::EcdhEsHkdf256Key(
            cose::EcdhEsHkdf256PublicKey {
                x: x.into(),
                y: y.into(),
            },
        ))
    }
}

impl PinUvAuthProtocol for PinUvAuthProtocolOne {
    fn version(&self) -> Ctap2PinUvAuthProtocol {
        Ctap2PinUvAuthProtocol::One
    }

    #[instrument(skip_all)]
    fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
        // Return the AES-256-CBC encryption of demPlaintext using an all-zero IV.
        // (No padding is performed as the size of demPlaintext is required to be a multiple of the AES block length.)
        let iv: &[u8] = &[0; 16];
        let Ok(enc) = Aes256CbcEncryptor::new_from_slices(key, iv) else {
            error!(?key, "Invalid key for AES-256 encryption");
            return Err(Error::Ctap(CtapError::Other));
        };
        Ok(enc.encrypt_padded_vec_mut::<NoPadding>(plaintext))
    }

    #[instrument(skip_all)]
    fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
        // Return the first 16 bytes of the result of computing HMAC-SHA-256 with the given key and message.
        let hmac = hmac_sha256(key, message)?;
        // HMAC-SHA-256 produces 32 bytes, so this slice is always valid.
        let truncated = hmac.get(..16).ok_or_else(|| {
            error!(len = hmac.len(), "HMAC output shorter than 16 bytes");
            Error::Platform(PlatformError::CryptoError(
                "HMAC output shorter than 16 bytes".into(),
            ))
        })?;
        Ok(Vec::from(truncated))
    }

    #[instrument(skip_all)]
    fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
        // If the size of demCiphertext is not a multiple of the AES block length, return error.
        // Otherwise return the AES-256-CBC decryption of demCiphertext using an all-zero IV.
        if !ciphertext.len().is_multiple_of(16) {
            error!(
                ?ciphertext,
                "Ciphertext length is not a multiple of AES block length"
            );
            return Err(Error::Ctap(CtapError::Other));
        }

        let iv: &[u8] = &[0; 16];
        let Ok(dec) = Aes256CbcDecryptor::new_from_slices(key, iv) else {
            error!(?key, "Invalid key for AES-256 decryption");
            return Err(Error::Ctap(CtapError::Other));
        };
        let Ok(plaintext) = dec.decrypt_padded_vec_mut::<NoPadding>(ciphertext) else {
            error!("Unpad error while decrypting");
            return Err(Error::Ctap(CtapError::Other));
        };
        Ok(plaintext)
    }

    fn encapsulate(
        &self,
        peer_public_key: &cose::PublicKey,
    ) -> Result<(cose::PublicKey, Vec<u8>), Error> {
        <Self as ECDHPinUvAuthProtocol>::encapsulate(self, peer_public_key)
    }
}

pub struct PinUvAuthProtocolTwo {
    private_key: EphemeralSecret,
    public_key: P256PublicKey,
}

impl Default for PinUvAuthProtocolTwo {
    fn default() -> Self {
        Self::new()
    }
}

impl PinUvAuthProtocolTwo {
    pub fn new() -> Self {
        let private_key = if cfg!(test) {
            // For testing only!!
            // We need a deterministic, seedable RNG to be able to "predict" crypto-operations
            let mut rng = rand::rngs::StdRng::seed_from_u64(42);
            EphemeralSecret::random(&mut rng)
        } else {
            EphemeralSecret::random(&mut OsRng)
        };
        let public_key = private_key.public_key();
        Self {
            private_key,
            public_key,
        }
    }
}

impl ECPrivateKeyPinUvAuthProtocol for PinUvAuthProtocolTwo {
    fn private_key(&self) -> &EphemeralSecret {
        &self.private_key
    }

    fn public_key(&self) -> &P256PublicKey {
        &self.public_key
    }

    /// kdf(Z) → sharedSecret
    fn kdf(&self, ikm: &[u8]) -> Result<Vec<u8>, Error> {
        // Returns:
        //   HKDF-SHA-256(salt = 32 zero bytes, IKM = Z, L = 32, info = "CTAP2 HMAC key") ||
        //   HKDF-SHA-256(salt = 32 zero bytes, IKM = Z, L = 32, info = "CTAP2 AES key")
        let salt: &[u8] = &[0u8; 32];
        let mut output = hkdf_sha256(Some(salt), ikm, "CTAP2 HMAC key".as_bytes())?;
        output.extend(hkdf_sha256(Some(salt), ikm, "CTAP2 AES key".as_bytes())?);
        Ok(output)
    }
}

impl PinUvAuthProtocol for PinUvAuthProtocolTwo {
    fn version(&self) -> Ctap2PinUvAuthProtocol {
        Ctap2PinUvAuthProtocol::Two
    }

    #[instrument(skip_all)]
    fn encapsulate(
        &self,
        peer_public_key: &cose::PublicKey,
    ) -> Result<(cose::PublicKey, Vec<u8>), Error> {
        <Self as ECDHPinUvAuthProtocol>::encapsulate(self, peer_public_key)
    }

    fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
        // Discard the first 32 bytes of key. (This selects the AES-key portion of the shared secret.)
        let key = key.get(32..).ok_or_else(|| {
            error!(
                key_len = key.len(),
                "key shorter than 32 bytes; cannot select AES-key portion"
            );
            Error::Ctap(CtapError::Other)
        })?;

        // Let iv be a 16-byte, random bytestring.
        let iv: [u8; 16] = thread_rng().gen();

        // Let ct be the AES-256-CBC encryption of demPlaintext using key and iv.
        // (No padding is performed as the size of demPlaintext is required to be a multiple of the AES block length.)
        let Ok(enc) = Aes256CbcEncryptor::new_from_slices(key, &iv) else {
            error!(?key, "Invalid key for AES-256 encryption");
            return Err(Error::Ctap(CtapError::Other));
        };
        let ct = enc.encrypt_padded_vec_mut::<NoPadding>(plaintext);

        // Return iv || ct.
        let mut out = Vec::from(iv);
        out.extend(ct);
        Ok(out)
    }

    fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
        // Discard the first 32 bytes of key. (This selects the AES-key portion of the shared secret.)
        let key = key.get(32..).ok_or_else(|| {
            error!(
                key_len = key.len(),
                "key shorter than 32 bytes; cannot select AES-key portion"
            );
            Error::Ctap(CtapError::Other)
        })?;

        // If demPlaintext is less than 16 bytes in length, return an error
        if ciphertext.len() < 16 {
            error!({ len = ciphertext.len() }, "Invalid length for ciphertext");
            return Err(Error::Ctap(CtapError::Other));
        };

        // Split demPlaintext after the 16th byte to produce two subspans, iv and ct.
        let (iv, ciphertext) = ciphertext.split_at(16);

        // Return the AES-256-CBC decryption of ct using key and iv.
        let Ok(dec) = Aes256CbcDecryptor::new_from_slices(key, iv) else {
            error!(?key, "Invalid key for AES-256 decryption");
            return Err(Error::Ctap(CtapError::Other));
        };
        let Ok(plaintext) = dec.decrypt_padded_vec_mut::<NoPadding>(ciphertext) else {
            error!("Unpad error while decrypting");
            return Err(Error::Ctap(CtapError::Other));
        };
        Ok(plaintext)
    }

    fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
        // If key is longer than 32 bytes, discard the excess. (This selects the HMAC-key portion of the shared secret.
        // When key is the pinUvAuthToken, it is exactly 32 bytes long and thus this step has no effect.)
        let key = key.get(..32).ok_or_else(|| {
            error!(
                key_len = key.len(),
                "key shorter than 32 bytes; cannot select HMAC-key portion"
            );
            Error::Ctap(CtapError::Other)
        })?;

        // Return the result of computing HMAC-SHA-256 on key and message.
        hmac_sha256(key, message)
    }
}

/// hash(pin) -> LEFT(SHA-256(pin), 16)
pub fn pin_hash(pin: &[u8]) -> Vec<u8> {
    let mut hasher = Sha256::default();
    hasher.update(pin);
    let hashed = hasher.finalize();
    // SHA-256 output is fixed at 32 bytes; keep only the first 16 per the spec.
    hashed.into_iter().take(16).collect()
}

pub fn hmac_sha256(key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
    let mut hmac = HmacSha256::new_from_slice(key).map_err(|e| {
        error!("HMAC key error: {e}");
        Error::Platform(PlatformError::CryptoError(format!("HMAC key error: {e}")))
    })?;
    hmac.update(message);
    Ok(hmac.finalize().into_bytes().to_vec())
}

pub fn hkdf_sha256(salt: Option<&[u8]>, ikm: &[u8], info: &[u8]) -> Result<Vec<u8>, Error> {
    let hk = Hkdf::<Sha256>::new(salt, ikm);
    let mut okm = [0u8; 32]; // fixed L = 32
    hk.expand(info, &mut okm).map_err(|e| {
        error!("HKDF expand error: {e}");
        Error::Platform(PlatformError::CryptoError(format!(
            "HKDF expand error: {e}"
        )))
    })?;
    Ok(Vec::from(okm))
}

// Defining a sealed trait for the internal API that is only visible within the crate
// while not touching/extending the external API
pub(crate) mod internal {
    use super::*;

    #[async_trait]
    pub trait PinManagementInternal {
        async fn change_pin_internal(
            &mut self,
            get_info_response: &Ctap2GetInfoResponse,
            new_pin: String,
            timeout: Duration,
        ) -> Result<(), Error>;
    }

    #[async_trait]
    impl<C> PinManagementInternal for C
    where
        C: Channel,
    {
        async fn change_pin_internal(
            &mut self,
            get_info_response: &Ctap2GetInfoResponse,
            new_pin: String,
            timeout: Duration,
        ) -> Result<(), Error> {
            // If the minPINLength member of the authenticatorGetInfo response is absent, then let platformMinPINLengthInCodePoints be 4.
            if new_pin.len() < get_info_response.min_pin_length.unwrap_or(4) as usize {
                // If platformCollectedPinLengthInCodePoints is less than platformMinPINLengthInCodePoints then the platform SHOULD display a "PIN too short" error message to the user.
                return Err(Error::Platform(PlatformError::PinTooShort));
            }

            // If the byte length of "newPin" is greater than the max UTF-8 representation limit of 63 bytes, then the platform SHOULD display a "PIN too long" error message to the user.
            if new_pin.len() >= 64 {
                return Err(Error::Platform(PlatformError::PinTooLong));
            }

            let Some(uv_proto) = select_uv_proto(
                #[cfg(feature = "virt")]
                self.get_forced_pin_protocol(),
                get_info_response,
            )
            .await
            else {
                error!("No supported PIN/UV auth protocols found");
                return Err(Error::Ctap(CtapError::Other));
            };

            let current_pin = match get_info_response
                .options
                .as_ref()
                .ok_or(Error::Platform(PlatformError::InvalidDeviceResponse))?
                .get("clientPin")
            {
                // Obtaining the current PIN, if one is set
                Some(true) => Some(
                    obtain_pin(
                        self,
                        get_info_response,
                        uv_proto.version(),
                        PinRequestReason::AuthenticatorPolicy,
                        timeout,
                    )
                    .await?,
                ),

                // No PIN set yet
                Some(false) => None,

                // Device does not support PIN
                None => {
                    return Err(Error::Platform(PlatformError::PinNotSupported));
                }
            };

            // In preparation for obtaining pinUvAuthToken, the platform:
            // * Obtains a shared secret.
            let (public_key, shared_secret) =
                obtain_shared_secret(self, uv_proto.as_ref(), timeout).await?;

            // paddedPin is newPin padded on the right with 0x00 bytes to make it 64 bytes long. (Since the maximum length of newPin is 63 bytes, there is always at least one byte of padding.)
            let mut padded_new_pin = new_pin.as_bytes().to_vec();
            padded_new_pin.resize(64, 0x00);

            // newPinEnc: the result of calling encrypt(shared secret, paddedPin) where
            let new_pin_enc = uv_proto.encrypt(&shared_secret, &padded_new_pin)?;

            let req = match current_pin {
                Some(curr_pin) => {
                    // pinHashEnc: The result of calling encrypt(shared secret, LEFT(SHA-256(curPin), 16)).
                    let pin_hash = pin_hash(&curr_pin);
                    let pin_hash_enc = uv_proto.encrypt(&shared_secret, &pin_hash)?;

                    // pinUvAuthParam: the result of calling authenticate(shared secret, newPinEnc || pinHashEnc)
                    let uv_auth_param = uv_proto.authenticate(
                        &shared_secret,
                        &[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat(),
                    )?;

                    Ctap2ClientPinRequest::new_change_pin(
                        uv_proto.version(),
                        &new_pin_enc,
                        &pin_hash_enc,
                        public_key,
                        &uv_auth_param,
                    )
                }
                None => {
                    // pinUvAuthParam: the result of calling authenticate(shared secret, newPinEnc).
                    let uv_auth_param = uv_proto.authenticate(&shared_secret, &new_pin_enc)?;

                    Ctap2ClientPinRequest::new_set_pin(
                        uv_proto.version(),
                        &new_pin_enc,
                        public_key,
                        &uv_auth_param,
                    )
                }
            };

            // On success, this is an all-empty Ctap2ClientPinResponse
            let _ = self.ctap2_client_pin(&req, timeout).await?;
            Ok(())
        }
    }
}

use internal::PinManagementInternal;

#[async_trait]
pub trait PinManagement: PinManagementInternal {
    async fn change_pin(&mut self, new_pin: String, timeout: Duration) -> Result<(), Error>;
}

#[async_trait]
impl<C> PinManagement for C
where
    C: Channel,
{
    async fn change_pin(&mut self, new_pin: String, timeout: Duration) -> Result<(), Error> {
        let get_info_response = self.ctap2_get_info().await?;
        self.change_pin_internal(&get_info_response, new_pin, timeout)
            .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cosey::{Bytes, EcdhEsHkdf256PublicKey};

    fn make_peer_key(x: &[u8], y: &[u8]) -> cose::PublicKey {
        cose::PublicKey::EcdhEsHkdf256Key(EcdhEsHkdf256PublicKey {
            x: Bytes::from_slice(x).unwrap(),
            y: Bytes::from_slice(y).unwrap(),
        })
    }

    #[test]
    fn ecdh_rejects_short_x() {
        let proto = PinUvAuthProtocolOne::new();
        let x = vec![0x01u8; 31];
        let y = vec![0x02u8; 32];
        let key = make_peer_key(&x, &y);

        let result = PinUvAuthProtocol::encapsulate(&proto, &key);
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }

    #[test]
    fn ecdh_rejects_empty_x() {
        let proto = PinUvAuthProtocolOne::new();
        let x: Vec<u8> = Vec::new();
        let y = vec![0x02u8; 32];
        let key = make_peer_key(&x, &y);

        let result = PinUvAuthProtocol::encapsulate(&proto, &key);
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }

    #[test]
    fn ecdh_rejects_short_y() {
        let proto = PinUvAuthProtocolTwo::new();
        let x = vec![0x01u8; 32];
        let y = vec![0x02u8; 16];
        let key = make_peer_key(&x, &y);

        let result = PinUvAuthProtocol::encapsulate(&proto, &key);
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }

    #[test]
    fn proto_two_authenticate_rejects_empty_key() {
        let proto = PinUvAuthProtocolTwo::new();
        let result = proto.authenticate(&[], b"clientDataHash");
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }

    #[test]
    fn proto_two_authenticate_rejects_short_key() {
        let proto = PinUvAuthProtocolTwo::new();
        let short_key = [0u8; 16];
        let result = proto.authenticate(&short_key, b"hello");
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }

    #[test]
    fn proto_two_encrypt_rejects_short_key() {
        let proto = PinUvAuthProtocolTwo::new();
        let short_key = [0u8; 16];
        let plaintext = [0u8; 16];
        let result = proto.encrypt(&short_key, &plaintext);
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }

    #[test]
    fn proto_two_decrypt_rejects_short_key() {
        let proto = PinUvAuthProtocolTwo::new();
        let short_key = [0u8; 16];
        let ct = [0u8; 32];
        let result = proto.decrypt(&short_key, &ct);
        assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
    }
}