@@ -6,9 +6,11 @@ use aes::cipher::{block_padding::NoPadding, BlockDecryptMut};
66use async_trait:: async_trait;
77use cbc:: cipher:: KeyIvInit ;
88use hkdf:: Hkdf ;
9+ use rand:: rngs:: OsRng ;
10+ use rand:: RngCore ;
911use sha2:: Sha256 ;
1012use tokio:: sync:: Mutex ;
11- use tracing:: { debug, error, trace} ;
13+ use tracing:: { debug, error, trace, warn } ;
1214use zeroize:: ZeroizeOnDrop ;
1315
1416use crate :: proto:: ctap2:: { Ctap2GetInfoResponse , Ctap2PinUvAuthProtocol } ;
@@ -124,7 +126,6 @@ impl PersistentTokenStore for MemoryPersistentTokenStore {
124126
125127/// Derive the 16-byte AES-128 key for `encIdentifier` from a persistent token, per
126128/// CTAP 2.3-PS 6.4: `HKDF-SHA-256(salt = 32 zero bytes, IKM = token, L = 16, info = "encIdentifier")`.
127- #[ allow( dead_code) ] // wired into the acquisition/reuse flow in a later change
128129fn enc_identifier_key ( token : & [ u8 ] ) -> Result < [ u8 ; 16 ] , Error > {
129130 let hkdf = Hkdf :: < Sha256 > :: new ( Some ( & ENC_IDENTIFIER_HKDF_SALT ) , token) ;
130131 let mut key = [ 0u8 ; 16 ] ;
@@ -140,7 +141,6 @@ fn enc_identifier_key(token: &[u8]) -> Result<[u8; 16], Error> {
140141
141142/// Recover the 128-bit device identifier from an `encIdentifier` (`iv || ct`) using a
142143/// persistent token. `ct` is exactly one AES block, so decryption uses no padding.
143- #[ allow( dead_code) ] // wired into the acquisition/reuse flow in a later change
144144pub ( crate ) fn decrypt_enc_identifier (
145145 token : & [ u8 ] ,
146146 enc_identifier : & [ u8 ] ,
@@ -172,7 +172,6 @@ pub(crate) fn decrypt_enc_identifier(
172172/// `encIdentifier`. The IV is fresh on every getInfo, so raw bytes never compare equal
173173/// across connections; recognition is decrypt-and-compare against each record's stored
174174/// device identifier. Returns the first match, or `None` if no stored token fits.
175- #[ allow( dead_code) ] // wired into the acquisition/reuse flow in a later change
176175pub ( crate ) async fn recognize_authenticator (
177176 store : & dyn PersistentTokenStore ,
178177 info : & Ctap2GetInfoResponse ,
@@ -190,18 +189,71 @@ pub(crate) async fn recognize_authenticator(
190189 None
191190}
192191
192+ /// A fresh, opaque record id: 16 random bytes, hex-encoded. Random rather than derived
193+ /// from the device, so a record survives device-identifier changes only via reaping.
194+ fn new_record_id ( ) -> PersistentTokenRecordId {
195+ let mut bytes = [ 0u8 ; 16 ] ;
196+ OsRng . fill_bytes ( & mut bytes) ;
197+ hex:: encode ( bytes)
198+ }
199+
200+ /// Capture a freshly minted pcmr token for cross-session reuse: recover this device's
201+ /// identifier from `encIdentifier`, then store a new record under a fresh id. Returns the
202+ /// id. Callers treat failures as best-effort (the current operation still proceeds with
203+ /// the minted token).
204+ pub ( crate ) async fn store_minted_token (
205+ store : & dyn PersistentTokenStore ,
206+ info : & Ctap2GetInfoResponse ,
207+ token : & [ u8 ] ,
208+ pin_uv_auth_protocol : Ctap2PinUvAuthProtocol ,
209+ ) -> Result < PersistentTokenRecordId , Error > {
210+ let Some ( enc_identifier) = info. enc_identifier . as_ref ( ) else {
211+ warn ! ( "perCredMgmtRO advertised but no encIdentifier returned; cannot persist token" ) ;
212+ return Err ( Error :: Ctap ( CtapError :: Other ) ) ;
213+ } ;
214+ let device_identifier = decrypt_enc_identifier ( token, enc_identifier) ?;
215+ let aaguid: [ u8 ; 16 ] = info. aaguid [ ..] . try_into ( ) . map_err ( |_| {
216+ error ! ( len = info. aaguid. len( ) , "AAGUID was not 16 bytes" ) ;
217+ Error :: Ctap ( CtapError :: Other )
218+ } ) ?;
219+ let id = new_record_id ( ) ;
220+ let record = PersistentTokenRecord {
221+ persistent_token : token. to_vec ( ) ,
222+ pin_uv_auth_protocol,
223+ device_identifier,
224+ aaguid,
225+ } ;
226+ store. put ( & id, & record) . await ;
227+ debug ! ( ?id, "Stored freshly minted persistent token" ) ;
228+ Ok ( id)
229+ }
230+
231+ /// Test-only: build an `encIdentifier` (`iv || ct`) for a device identifier under a
232+ /// token, using the production key derivation. Shared across test modules.
233+ #[ cfg( test) ]
234+ pub ( crate ) fn build_enc_identifier (
235+ token : & [ u8 ] ,
236+ device_identifier : & [ u8 ; 16 ] ,
237+ iv : & [ u8 ; 16 ] ,
238+ ) -> Vec < u8 > {
239+ use aes:: cipher:: BlockEncryptMut ;
240+ type Aes128CbcEncryptor = cbc:: Encryptor < aes:: Aes128 > ;
241+ let key = enc_identifier_key ( token) . expect ( "encIdentifier key derivation" ) ;
242+ let encryptor = Aes128CbcEncryptor :: new_from_slices ( & key, iv) . expect ( "valid key/iv" ) ;
243+ let ciphertext = encryptor. encrypt_padded_vec_mut :: < NoPadding > ( device_identifier) ;
244+ let mut enc = iv. to_vec ( ) ;
245+ enc. extend_from_slice ( & ciphertext) ;
246+ enc
247+ }
248+
193249#[ cfg( test) ]
194250mod test {
195251 use super :: * ;
196252
197- use aes:: cipher:: { block_padding:: NoPadding as TestNoPadding , BlockEncryptMut } ;
198- use cbc:: cipher:: KeyIvInit as TestKeyIvInit ;
199253 use serde_bytes:: ByteBuf ;
200254
201255 use crate :: proto:: ctap2:: Ctap2GetInfoResponse ;
202256
203- type Aes128CbcEncryptor = cbc:: Encryptor < aes:: Aes128 > ;
204-
205257 fn sample_record ( ) -> PersistentTokenRecord {
206258 PersistentTokenRecord {
207259 persistent_token : vec ! [ 0xAB ; 32 ] ,
@@ -211,17 +263,6 @@ mod test {
211263 }
212264 }
213265
214- /// The authenticator side: produce `iv || ct` for a device identifier under a token,
215- /// using the same key derivation the recognition path uses.
216- fn build_enc_identifier ( token : & [ u8 ] , device_identifier : & [ u8 ; 16 ] , iv : & [ u8 ; 16 ] ) -> Vec < u8 > {
217- let key = enc_identifier_key ( token) . unwrap ( ) ;
218- let encryptor = Aes128CbcEncryptor :: new_from_slices ( & key, iv) . unwrap ( ) ;
219- let ciphertext = encryptor. encrypt_padded_vec_mut :: < TestNoPadding > ( device_identifier) ;
220- let mut enc = iv. to_vec ( ) ;
221- enc. extend_from_slice ( & ciphertext) ;
222- enc
223- }
224-
225266 fn record_with ( token : Vec < u8 > , device_identifier : [ u8 ; 16 ] ) -> PersistentTokenRecord {
226267 PersistentTokenRecord {
227268 persistent_token : token,
0 commit comments