@@ -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 } ;
@@ -128,7 +130,6 @@ impl PersistentTokenStore for EphemeralPersistentTokenStore {
128130
129131/// Derive the 16-byte AES-128 key for `encIdentifier` from a persistent token, per
130132/// CTAP 2.3-PS 6.4: `HKDF-SHA-256(salt = 32 zero bytes, IKM = token, L = 16, info = "encIdentifier")`.
131- #[ allow( dead_code) ] // wired into the acquisition/reuse flow in a later change
132133fn enc_identifier_key ( token : & [ u8 ] ) -> Result < [ u8 ; 16 ] , Error > {
133134 let hkdf = Hkdf :: < Sha256 > :: new ( Some ( & ENC_IDENTIFIER_HKDF_SALT ) , token) ;
134135 let mut key = [ 0u8 ; 16 ] ;
@@ -144,7 +145,6 @@ fn enc_identifier_key(token: &[u8]) -> Result<[u8; 16], Error> {
144145
145146/// Recover the 128-bit device identifier from an `encIdentifier` (`iv || ct`) using a
146147/// persistent token. `ct` is exactly one AES block, so decryption uses no padding.
147- #[ allow( dead_code) ] // wired into the acquisition/reuse flow in a later change
148148pub ( crate ) fn decrypt_enc_identifier (
149149 token : & [ u8 ] ,
150150 enc_identifier : & [ u8 ] ,
@@ -176,7 +176,6 @@ pub(crate) fn decrypt_enc_identifier(
176176/// `encIdentifier`. The IV is fresh on every getInfo, so raw bytes never compare equal
177177/// across connections; recognition is decrypt-and-compare against each record's stored
178178/// device identifier. Returns the first match, or `None` if no stored token fits.
179- #[ allow( dead_code) ] // wired into the acquisition/reuse flow in a later change
180179pub ( crate ) async fn recognize_authenticator (
181180 store : & dyn PersistentTokenStore ,
182181 info : & Ctap2GetInfoResponse ,
@@ -194,18 +193,71 @@ pub(crate) async fn recognize_authenticator(
194193 None
195194}
196195
196+ /// A fresh, opaque record id: 16 random bytes, hex-encoded. Random rather than derived
197+ /// from the device, so a record survives device-identifier changes only via reaping.
198+ fn new_record_id ( ) -> PersistentTokenRecordId {
199+ let mut bytes = [ 0u8 ; 16 ] ;
200+ OsRng . fill_bytes ( & mut bytes) ;
201+ hex:: encode ( bytes)
202+ }
203+
204+ /// Capture a freshly minted pcmr token for cross-session reuse: recover this device's
205+ /// identifier from `encIdentifier`, then store a new record under a fresh id. Returns the
206+ /// id. Callers treat failures as best-effort (the current operation still proceeds with
207+ /// the minted token).
208+ pub ( crate ) async fn store_minted_token (
209+ store : & dyn PersistentTokenStore ,
210+ info : & Ctap2GetInfoResponse ,
211+ token : & [ u8 ] ,
212+ pin_uv_auth_protocol : Ctap2PinUvAuthProtocol ,
213+ ) -> Result < PersistentTokenRecordId , Error > {
214+ let Some ( enc_identifier) = info. enc_identifier . as_ref ( ) else {
215+ warn ! ( "perCredMgmtRO advertised but no encIdentifier returned; cannot persist token" ) ;
216+ return Err ( Error :: Ctap ( CtapError :: Other ) ) ;
217+ } ;
218+ let device_identifier = decrypt_enc_identifier ( token, enc_identifier) ?;
219+ let aaguid: [ u8 ; 16 ] = info. aaguid [ ..] . try_into ( ) . map_err ( |_| {
220+ error ! ( len = info. aaguid. len( ) , "AAGUID was not 16 bytes" ) ;
221+ Error :: Ctap ( CtapError :: Other )
222+ } ) ?;
223+ let id = new_record_id ( ) ;
224+ let record = PersistentTokenRecord {
225+ persistent_token : token. to_vec ( ) ,
226+ pin_uv_auth_protocol,
227+ device_identifier,
228+ aaguid,
229+ } ;
230+ store. put ( & id, & record) . await ;
231+ debug ! ( ?id, "Stored freshly minted persistent token" ) ;
232+ Ok ( id)
233+ }
234+
235+ /// Test-only: build an `encIdentifier` (`iv || ct`) for a device identifier under a
236+ /// token, using the production key derivation. Shared across test modules.
237+ #[ cfg( test) ]
238+ pub ( crate ) fn build_enc_identifier (
239+ token : & [ u8 ] ,
240+ device_identifier : & [ u8 ; 16 ] ,
241+ iv : & [ u8 ; 16 ] ,
242+ ) -> Vec < u8 > {
243+ use aes:: cipher:: BlockEncryptMut ;
244+ type Aes128CbcEncryptor = cbc:: Encryptor < aes:: Aes128 > ;
245+ let key = enc_identifier_key ( token) . expect ( "encIdentifier key derivation" ) ;
246+ let encryptor = Aes128CbcEncryptor :: new_from_slices ( & key, iv) . expect ( "valid key/iv" ) ;
247+ let ciphertext = encryptor. encrypt_padded_vec_mut :: < NoPadding > ( device_identifier) ;
248+ let mut enc = iv. to_vec ( ) ;
249+ enc. extend_from_slice ( & ciphertext) ;
250+ enc
251+ }
252+
197253#[ cfg( test) ]
198254mod test {
199255 use super :: * ;
200256
201- use aes:: cipher:: { block_padding:: NoPadding as TestNoPadding , BlockEncryptMut } ;
202- use cbc:: cipher:: KeyIvInit as TestKeyIvInit ;
203257 use serde_bytes:: ByteBuf ;
204258
205259 use crate :: proto:: ctap2:: Ctap2GetInfoResponse ;
206260
207- type Aes128CbcEncryptor = cbc:: Encryptor < aes:: Aes128 > ;
208-
209261 fn sample_record ( ) -> PersistentTokenRecord {
210262 PersistentTokenRecord {
211263 persistent_token : vec ! [ 0xAB ; 32 ] ,
@@ -215,17 +267,6 @@ mod test {
215267 }
216268 }
217269
218- /// The authenticator side: produce `iv || ct` for a device identifier under a token,
219- /// using the same key derivation the recognition path uses.
220- fn build_enc_identifier ( token : & [ u8 ] , device_identifier : & [ u8 ; 16 ] , iv : & [ u8 ; 16 ] ) -> Vec < u8 > {
221- let key = enc_identifier_key ( token) . unwrap ( ) ;
222- let encryptor = Aes128CbcEncryptor :: new_from_slices ( & key, iv) . unwrap ( ) ;
223- let ciphertext = encryptor. encrypt_padded_vec_mut :: < TestNoPadding > ( device_identifier) ;
224- let mut enc = iv. to_vec ( ) ;
225- enc. extend_from_slice ( & ciphertext) ;
226- enc
227- }
228-
229270 fn record_with ( token : Vec < u8 > , device_identifier : [ u8 ; 16 ] ) -> PersistentTokenRecord {
230271 PersistentTokenRecord {
231272 persistent_token : token,
0 commit comments