Skip to main content

bitwarden_core/key_management/
pin_lock_system.rs

1//! Pin-based unlock in Bitwarden works using a `PasswordProtectedKeyEnvelope`, which is sealed with
2//! the PIN and contains the user-key. When unlocking with PIN, the envelope is unsealed with the
3//! PIN and the key is loaded into the key-store.
4//!
5//! There are two modes of PIN-based unlock: Before-first-unlock (BFU) and after-first-unlock (AFU).
6//! In BFU mode, the PIN envelope is persisted to disk. In AFU mode, the PIN envelope is only stored
7//! in memory. The memory copy is always loaded into memory when transitioning from BFU to AFU mode
8//! with an unlock.
9
10use bitwarden_crypto::{
11    Decryptable, KeyId, KeyStore, PrimitiveEncryptable, SymmetricKeyAlgorithm,
12    safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeNamespace},
13};
14use serde::{Deserialize, Serialize};
15use tracing::warn;
16
17use crate::{
18    Client,
19    key_management::{KeySlotIds, SymmetricKeySlotId},
20};
21
22/// Pin unlock can be configured to use one of two modes. Before-first-unlock and
23/// after-first-unlock. In AFU mode, the PIN is available only after unlocking once with the master
24/// password or another unlock method. In BFU mode, PIN unlock is available right after app start.
25/// For this, the PIN-encrypted vault key is stored on disk.
26#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28#[bitwarden_ffi::wasm_record]
29pub enum PinLockType {
30    /// Pin unlock is available after app start
31    BeforeFirstUnlock,
32    /// Pin unlock is available after unlocking with another method at least once during the app
33    /// session
34    AfterFirstUnlock,
35}
36
37#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
38#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
39#[bitwarden_ffi::wasm_record]
40/// Current availability state for PIN-based unlock.
41pub enum PinUnlockStatus {
42    /// A PIN is configured and the PIN envelope is available for decryption, so PIN-based unlock
43    /// can be attempted.
44    Available,
45    /// A PIN is configured, but the vault must be unlocked using another method first.
46    NeedsUnlock,
47    /// No PIN is configured.
48    NotSet,
49}
50
51pub(crate) enum UnlockError {
52    NoPinSet,
53    PinWrong,
54    InternalError,
55}
56
57#[derive(Debug, PartialEq, Eq)]
58pub(crate) enum MigrationFailed {
59    /// Vault is locked
60    Locked,
61    /// The encrypted PIN is under a key other than the user key, which cannot be recovered.
62    /// V2 -> V2 key rotation is not currently supported here.
63    UnrecoverablePinKey,
64    /// The encrypted PIN uses an unknown encryption algorithm.
65    UnsupportedPinEncryption,
66    /// V1 -> V2 migration is required but no V2 upgrade token is stored.
67    MissingV2UpgradeToken,
68    /// A PIN envelope is stored but no encrypted PIN to re-enroll it with.
69    MissingEncryptedPin,
70    /// Re-enrollment could not decrypt the encrypted PIN.
71    PinDecryption,
72    /// Re-sealing the envelope under the new user key failed.
73    Reenrollment,
74}
75
76/// What [`PinLockSystem::migrate_pin_envelope_if_needed`] should do with the PIN enrollment,
77/// decided from the encrypted PIN alone.
78#[derive(Debug, PartialEq, Eq)]
79enum PinMigrationAction {
80    /// The encrypted PIN is under the current user key. Nothing to do.
81    UpToDate,
82    /// The encrypted PIN is under the previous V1 user key. Re-enroll under the current V2 user
83    /// key, recovering the V1 key from the upgrade token.
84    MigrateV1ToV2,
85    /// Terminal failure; no migration is possible.
86    Failed(MigrationFailed),
87}
88
89/// Decides what to do with the PIN enrollment, from the algorithms of the encrypted PIN and the
90/// user key.
91///
92/// - A V1 (AES-CBC-HMAC) PIN under a V2 user key is a V1 -> V2 upgrade.
93/// - Otherwise the PIN must be under the user key itself. A key id, when the PIN carries one, must
94///   match; V1 PINs may carry none.
95fn classify_encrypted_pin(
96    pin_algorithm: Option<SymmetricKeyAlgorithm>,
97    pin_key_id: Option<&KeyId>,
98    user_key_algorithm: SymmetricKeyAlgorithm,
99    user_key_id: &KeyId,
100) -> PinMigrationAction {
101    let Some(pin_algorithm) = pin_algorithm else {
102        return PinMigrationAction::Failed(MigrationFailed::UnsupportedPinEncryption);
103    };
104
105    let pin_is_v1 = pin_algorithm == SymmetricKeyAlgorithm::Aes256CbcHmac;
106    let user_key_is_v1 = user_key_algorithm == SymmetricKeyAlgorithm::Aes256CbcHmac;
107    if pin_is_v1 && !user_key_is_v1 {
108        return PinMigrationAction::MigrateV1ToV2;
109    }
110
111    match pin_key_id {
112        Some(pin_key_id) if pin_key_id == user_key_id => PinMigrationAction::UpToDate,
113        None if pin_is_v1 => PinMigrationAction::UpToDate,
114        _ => PinMigrationAction::Failed(MigrationFailed::UnrecoverablePinKey),
115    }
116}
117
118/// Provides PIN-based unlock functionality. This includes enrolling into PIN-based unlock,
119/// unlocking using the PIN and handling necessary operations (PIN envelope refreshing when
120/// transitioning to after-first-unlock mode).
121pub struct PinLockSystem<'a> {
122    client: &'a Client,
123}
124
125impl PinLockSystem<'_> {
126    fn key_store(&self) -> &KeyStore<KeySlotIds> {
127        self.client.internal.get_key_store()
128    }
129
130    /// Creates a PIN lock system view for a client instance.
131    pub fn with_client(client: &Client) -> PinLockSystem<'_> {
132        PinLockSystem { client }
133    }
134
135    /// Retrieves the currently active PIN envelope.
136    ///
137    /// If both envelopes are present, the ephemeral envelope is preferred.
138    async fn get_active_pin_envelope(&self) -> Option<PasswordProtectedKeyEnvelope> {
139        let mut pin_protected_key_envelope = self
140            .client
141            .km_state_bridge()
142            .get_ephemeral_pin_envelope()
143            .await;
144        if pin_protected_key_envelope.is_none() {
145            pin_protected_key_envelope = self
146                .client
147                .km_state_bridge()
148                .get_persistent_pin_envelope()
149                .await;
150        }
151        pin_protected_key_envelope
152    }
153
154    /// Attempts to unlock the user key using `pin`.
155    ///
156    /// Returns [`UnlockError::NoPinSet`] if no PIN is configured,
157    /// [`UnlockError::PinWrong`] if `pin` is incorrect, and
158    /// [`UnlockError::InternalError`] for other failures.
159    pub(crate) async fn unlock(&self, pin: &str) -> Result<(), UnlockError> {
160        let pin_envelope = Self::get_active_pin_envelope(self)
161            .await
162            .ok_or(UnlockError::NoPinSet)?;
163
164        // Unseal to key ctx
165        let mut ctx = self.key_store().context_mut();
166        let key_slot = pin_envelope
167            .unseal(
168                pin,
169                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
170                &mut ctx,
171            )
172            .map_err(|e| match e {
173                bitwarden_crypto::safe::PasswordProtectedKeyEnvelopeError::WrongPassword => {
174                    UnlockError::PinWrong
175                }
176                _ => UnlockError::InternalError,
177            })?;
178
179        // The key is currently in the local ctx and would be dropped when ctx goes out of scope.
180        // Persist it to the keystore
181        ctx.persist_symmetric_key(key_slot, SymmetricKeySlotId::User)
182            .map_err(|_| UnlockError::InternalError)
183    }
184
185    /// Brings the PIN enrollment in line with the current user key.
186    ///
187    /// After a V2 upgrade, the encrypted PIN is still encrypted with the V1 user key. It is
188    /// decrypted with the V1 key recovered from the upgrade token, and re-enrolled under the
189    /// current user key with the same lock type. Both lock types store the encrypted PIN, so it
190    /// alone decides what to do. See [`classify_encrypted_pin`].
191    async fn migrate_pin_envelope_if_needed(&self) -> Result<(), MigrationFailed> {
192        // No PIN configured
193        let Some(lock_type) = self.get_pin_lock_type().await else {
194            return Ok(());
195        };
196        let encrypted_pin = self
197            .client
198            .km_state_bridge()
199            .get_encrypted_pin()
200            .await
201            .ok_or(MigrationFailed::MissingEncryptedPin)?;
202
203        // Scoped so the context is dropped before the awaits below.
204        let (user_key_id, user_key_algorithm) = {
205            let ctx = self.key_store().context();
206            (
207                ctx.get_symmetric_key_id(SymmetricKeySlotId::User)
208                    .ok_or(MigrationFailed::Locked)?,
209                ctx.get_symmetric_key_algorithm(SymmetricKeySlotId::User)
210                    .map_err(|_| MigrationFailed::Locked)?,
211            )
212        };
213
214        match classify_encrypted_pin(
215            encrypted_pin.algorithm(),
216            encrypted_pin.key_id().as_ref(),
217            user_key_algorithm,
218            &user_key_id,
219        ) {
220            PinMigrationAction::UpToDate => return Ok(()),
221            PinMigrationAction::Failed(error) => return Err(error),
222            PinMigrationAction::MigrateV1ToV2 => {}
223        }
224
225        let token = self
226            .client
227            .km_state_bridge()
228            .get_v2_upgrade_token()
229            .await
230            .ok_or(MigrationFailed::MissingV2UpgradeToken)?;
231
232        // The unwrapped V1 key only lives in this context, so it is scoped with the decryption.
233        let pin: String = {
234            let mut ctx = self.key_store().context_mut();
235            let v1_slot = token
236                .unwrap_v1(SymmetricKeySlotId::User, &mut ctx)
237                .map_err(|_| MigrationFailed::PinDecryption)?;
238            encrypted_pin
239                .decrypt(&mut ctx, v1_slot)
240                .map_err(|_| MigrationFailed::PinDecryption)?
241        };
242
243        // Do a fresh enrollment with the current user-key
244        self.set_pin(pin, lock_type)
245            .await
246            .map_err(|_| MigrationFailed::Reenrollment)
247    }
248
249    /// Refreshes in-memory PIN unlock material after a successful non-PIN unlock.
250    ///
251    /// This recreates the ephemeral PIN envelope from the encrypted PIN, when available.
252    pub(crate) async fn on_unlock(&self) {
253        // Remove once all clients, ios, android implement the state bridge
254        if !self.client.km_state_bridge().is_bridge_registered() {
255            return;
256        }
257
258        if let Err(e) = self.migrate_pin_envelope_if_needed().await {
259            warn!("PIN migration failed: {e:?}, unenrolling PIN");
260            self.unset_pin().await;
261            return;
262        }
263
264        let encrypted_pin = self.client.km_state_bridge().get_encrypted_pin().await;
265
266        // If PIN unlock is not enabled, do nothing
267        let Some(encrypted_pin) = encrypted_pin else {
268            return;
269        };
270
271        // Re-sealing runs a full KDF. Clients that re-initialize crypto for an already unlocked
272        // session (e.g. a browser popup) would pay it on every initialization, so keep an existing
273        // envelope that already holds the current user key.
274        if let Some(existing_envelope) = self
275            .client
276            .km_state_bridge()
277            .get_ephemeral_pin_envelope()
278            .await
279        {
280            let user_key_id = self
281                .key_store()
282                .context()
283                .get_symmetric_key_id(SymmetricKeySlotId::User);
284            if user_key_id.is_some()
285                && existing_envelope.contained_key_id().ok().flatten() == user_key_id
286            {
287                return;
288            }
289        }
290
291        // Make the fresh PIN envelope
292        let Ok(pin_envelope) = (|| -> Result<PasswordProtectedKeyEnvelope, ()> {
293            let mut ctx = self.key_store().context_mut();
294            let pin: String = encrypted_pin
295                .decrypt(&mut ctx, SymmetricKeySlotId::User)
296                .map_err(|_| ())?;
297            PasswordProtectedKeyEnvelope::seal(
298                SymmetricKeySlotId::User,
299                pin.as_str(),
300                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
301                &ctx,
302            )
303            .map_err(|_| ())
304        })() else {
305            warn!("Failed to create PIN envelope");
306            return;
307        };
308
309        // Store it to memory
310        self.client
311            .km_state_bridge()
312            .set_ephemeral_pin_envelope(&pin_envelope)
313            .await;
314    }
315
316    /// Sets the PIN and stores the generated envelope according to the lock type.
317    pub async fn set_pin(&self, pin: String, lock_type: PinLockType) -> Result<(), ()> {
318        // Clear the existing configuration
319        self.client
320            .km_state_bridge()
321            .clear_persistent_pin_envelope()
322            .await;
323        self.client
324            .km_state_bridge()
325            .clear_ephemeral_pin_envelope()
326            .await;
327        self.client.km_state_bridge().clear_encrypted_pin().await;
328
329        let pin_envelope: PasswordProtectedKeyEnvelope = PasswordProtectedKeyEnvelope::seal(
330            SymmetricKeySlotId::User,
331            pin.as_str(),
332            PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
333            &self.key_store().context_mut(),
334        )
335        .map_err(|_| ())?;
336        let encrypted_pin = pin
337            .encrypt(
338                &mut self.key_store().context_mut(),
339                SymmetricKeySlotId::User,
340            )
341            .map_err(|_| ())?;
342
343        self.client
344            .km_state_bridge()
345            .set_encrypted_pin(&encrypted_pin)
346            .await;
347        self.client
348            .km_state_bridge()
349            .set_ephemeral_pin_envelope(&pin_envelope)
350            .await;
351
352        if lock_type == PinLockType::BeforeFirstUnlock {
353            self.client
354                .km_state_bridge()
355                .set_persistent_pin_envelope(&pin_envelope)
356                .await;
357        }
358
359        Ok(())
360    }
361
362    /// Clears both persistent and ephemeral PIN envelopes.
363    pub async fn unset_pin(&self) {
364        self.client
365            .km_state_bridge()
366            .clear_persistent_pin_envelope()
367            .await;
368        self.client
369            .km_state_bridge()
370            .clear_ephemeral_pin_envelope()
371            .await;
372        self.client.km_state_bridge().clear_encrypted_pin().await;
373    }
374
375    /// Returns the lock type for the currently configured PIN.
376    pub async fn get_pin_lock_type(&self) -> Option<PinLockType> {
377        if self
378            .client
379            .km_state_bridge()
380            .get_persistent_pin_envelope()
381            .await
382            .is_some()
383        {
384            return Some(PinLockType::BeforeFirstUnlock);
385        }
386
387        // Encrypted pin is set for either lock type, persistent pin only for BFU. The ephemeral
388        // envelope may not be set after restarting a client, until the client enters AFU
389        // mode.
390        if self
391            .client
392            .km_state_bridge()
393            .get_encrypted_pin()
394            .await
395            .is_some()
396        {
397            return Some(PinLockType::AfterFirstUnlock);
398        }
399
400        None
401    }
402
403    /// Returns the current PIN unlock status.
404    ///
405    /// If a lock type is configured but no ephemeral envelope is currently present,
406    /// the status is [`PinUnlockStatus::NeedsUnlock`].
407    pub async fn get_pin_status(&self) -> PinUnlockStatus {
408        match Self::get_pin_lock_type(self).await {
409            Some(PinLockType::BeforeFirstUnlock) => {
410                if self.get_active_pin_envelope().await.is_some() {
411                    PinUnlockStatus::Available
412                } else {
413                    PinUnlockStatus::NeedsUnlock
414                }
415            }
416            Some(PinLockType::AfterFirstUnlock) => {
417                if self
418                    .client
419                    .km_state_bridge()
420                    .get_ephemeral_pin_envelope()
421                    .await
422                    .is_some()
423                {
424                    PinUnlockStatus::Available
425                } else {
426                    // This should not happen as AFU should always have the ephemeral envelope, but
427                    // we handle it just in case.
428                    PinUnlockStatus::NeedsUnlock
429                }
430            }
431            None => PinUnlockStatus::NotSet,
432        }
433    }
434
435    /// Returns the configured PIN, if an encrypted PIN is available and decryptable.
436    pub async fn get_pin(&self) -> Option<String> {
437        let encrypted_pin = self.client.km_state_bridge().get_encrypted_pin().await?;
438        encrypted_pin
439            .decrypt(
440                &mut self.client.internal.get_key_store().context_mut(),
441                SymmetricKeySlotId::User,
442            )
443            .ok()
444    }
445
446    /// Validates that the provided PIN can decrypt the stored PIN envelope.
447    pub async fn validate_pin(&self, pin: String) -> bool {
448        let pin_envelope = self.get_active_pin_envelope().await;
449        let Some(pin_envelope) = pin_envelope else {
450            return false;
451        };
452
453        pin_envelope
454            .unseal(
455                pin.as_str(),
456                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
457                &mut self.key_store().context_mut(),
458            )
459            .is_ok()
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use bitwarden_crypto::{EncString, KeyId, SymmetricKeyAlgorithm};
466
467    use super::*;
468    use crate::key_management::{V2UpgradeToken, state_bridge::test_support::InMemoryStateBridge};
469
470    fn decrypt_encrypted_pin(client: &Client, encrypted_pin: &EncString) -> String {
471        encrypted_pin
472            .decrypt(
473                &mut client.internal.get_key_store().context_mut(),
474                SymmetricKeySlotId::User,
475            )
476            .expect("encrypted pin should decrypt successfully")
477    }
478
479    /// The PIN [`TESTVECTOR_LEGACY_ENVELOPE`] was sealed with.
480    const TESTVECTOR_LEGACY_ENVELOPE_PIN: &str = "1234";
481    /// A `PinUnlock` envelope sealed under an AES-CBC-HMAC (V1) key by a client predating derived
482    /// key ids, so it carries no contained key id.
483    ///
484    /// The migration never unseals this envelope — it reads the contained key id and then
485    /// re-enrolls from the key store — so the key sealed inside is arbitrary and deliberately
486    /// unrelated to the user key the tests install.
487    ///
488    /// The current seal path always writes a contained key id, so re-recording this requires
489    /// temporarily changing `set_contained_key_id(&mut header, key_to_seal.key_id())` in
490    /// `bitwarden-crypto/src/safe/password_protected_key_envelope.rs` to pass `None`, sealing a
491    /// `PinUnlock` envelope for an `Aes256CbcHmac` key with [`TESTVECTOR_LEGACY_ENVELOPE_PIN`],
492    /// printing `Vec::from(&envelope)`, and then reverting that change.
493    const TESTVECTOR_LEGACY_ENVELOPE: &[u8] = &[
494        132, 88, 52, 164, 1, 3, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47,
495        120, 46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45,
496        107, 101, 121, 58, 0, 1, 56, 129, 1, 58, 0, 1, 56, 128, 1, 161, 5, 76, 148, 49, 223, 205,
497        195, 252, 91, 216, 65, 200, 121, 104, 88, 80, 89, 120, 16, 161, 14, 97, 191, 97, 138, 42,
498        102, 234, 49, 186, 2, 255, 31, 4, 232, 178, 100, 53, 37, 181, 172, 129, 193, 51, 109, 8,
499        160, 29, 254, 181, 242, 102, 73, 229, 89, 150, 227, 252, 120, 156, 71, 202, 200, 241, 74,
500        241, 206, 16, 155, 83, 49, 242, 13, 209, 10, 217, 251, 164, 244, 69, 41, 52, 9, 192, 140,
501        248, 251, 244, 84, 154, 15, 100, 222, 102, 117, 185, 129, 131, 71, 161, 1, 58, 0, 1, 21,
502        87, 165, 1, 58, 0, 1, 21, 87, 58, 0, 1, 21, 89, 3, 58, 0, 1, 21, 90, 26, 0, 1, 0, 0, 58, 0,
503        1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 64, 184, 87, 20, 40, 186, 214, 56, 87, 53, 118, 100, 5,
504        21, 13, 3, 246,
505    ];
506
507    /// Parses [`TESTVECTOR_LEGACY_ENVELOPE`], asserting it really has no contained key id.
508    fn legacy_envelope() -> PasswordProtectedKeyEnvelope {
509        let envelope = PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACY_ENVELOPE.to_vec())
510            .expect("legacy envelope test vector parses");
511        assert_eq!(
512            envelope.contained_key_id().expect("readable"),
513            None,
514            "legacy envelope test vector must have no contained key id",
515        );
516        envelope
517    }
518
519    /// Returns the `KeyId` of the symmetric key currently in `SymmetricKeySlotId::User`.
520    fn user_key_id(client: &Client) -> KeyId {
521        client
522            .internal
523            .get_key_store()
524            .context()
525            .get_symmetric_key_id(SymmetricKeySlotId::User)
526            .expect("user key present")
527    }
528
529    /// Asserts the envelope wraps `expected_key_id` and unseals successfully under `pin`.
530    fn assert_envelope_wraps_user_key(
531        client: &Client,
532        envelope: &PasswordProtectedKeyEnvelope,
533        pin: &str,
534        expected_key_id: &KeyId,
535    ) {
536        assert_eq!(
537            envelope
538                .contained_key_id()
539                .expect("contained key id readable"),
540            Some(expected_key_id.clone()),
541            "envelope wraps a key other than the current user key",
542        );
543        let _ = envelope
544            .unseal(
545                pin,
546                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
547                &mut client.internal.get_key_store().context_mut(),
548            )
549            .expect("envelope unseals with the configured pin");
550    }
551
552    fn client_with_user_key() -> Client {
553        let client = Client::new(None);
554        client
555            .km_state_bridge()
556            .register_bridge(Box::new(InMemoryStateBridge::default()));
557        {
558            let key_store = client.internal.get_key_store();
559            let mut ctx = key_store.context_mut();
560            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
561            ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)
562                .expect("persisting user key should succeed");
563        }
564        client
565    }
566
567    fn seal_envelope(client: &Client, pin: &str) -> PasswordProtectedKeyEnvelope {
568        PasswordProtectedKeyEnvelope::seal(
569            SymmetricKeySlotId::User,
570            pin,
571            PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
572            &client.internal.get_key_store().context_mut(),
573        )
574        .expect("seal succeeds")
575    }
576
577    #[tokio::test]
578    async fn set_pin_bfu_persists_both_envelopes() {
579        let client = client_with_user_key();
580        let user_key_id = user_key_id(&client);
581        let system = PinLockSystem::with_client(&client);
582
583        system
584            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
585            .await
586            .expect("set_pin succeeds");
587
588        let bridge = client.km_state_bridge();
589        let persistent = bridge
590            .get_persistent_pin_envelope()
591            .await
592            .expect("persistent envelope present");
593        let ephemeral = bridge
594            .get_ephemeral_pin_envelope()
595            .await
596            .expect("ephemeral envelope present");
597        let encrypted_pin = bridge
598            .get_encrypted_pin()
599            .await
600            .expect("encrypted pin present");
601
602        assert_envelope_wraps_user_key(&client, &persistent, "1234", &user_key_id);
603        assert_envelope_wraps_user_key(&client, &ephemeral, "1234", &user_key_id);
604        assert_eq!(decrypt_encrypted_pin(&client, &encrypted_pin), "1234");
605
606        assert_eq!(
607            system.get_pin_lock_type().await,
608            Some(PinLockType::BeforeFirstUnlock)
609        );
610        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
611    }
612
613    #[tokio::test]
614    async fn set_pin_afu_persists_only_ephemeral() {
615        let client = client_with_user_key();
616        let user_key_id = user_key_id(&client);
617        let system = PinLockSystem::with_client(&client);
618
619        system
620            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
621            .await
622            .expect("set_pin succeeds");
623
624        let bridge = client.km_state_bridge();
625        assert!(bridge.get_persistent_pin_envelope().await.is_none());
626        let ephemeral = bridge
627            .get_ephemeral_pin_envelope()
628            .await
629            .expect("ephemeral envelope present");
630        let encrypted_pin = bridge
631            .get_encrypted_pin()
632            .await
633            .expect("encrypted pin present");
634
635        assert_envelope_wraps_user_key(&client, &ephemeral, "1234", &user_key_id);
636        assert_eq!(decrypt_encrypted_pin(&client, &encrypted_pin), "1234");
637
638        assert_eq!(
639            system.get_pin_lock_type().await,
640            Some(PinLockType::AfterFirstUnlock)
641        );
642        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
643    }
644
645    #[tokio::test]
646    async fn set_pin_overwrites_existing_state() {
647        let client = client_with_user_key();
648        let system = PinLockSystem::with_client(&client);
649
650        system
651            .set_pin("first".into(), PinLockType::BeforeFirstUnlock)
652            .await
653            .expect("first set_pin");
654        system
655            .set_pin("second".into(), PinLockType::AfterFirstUnlock)
656            .await
657            .expect("second set_pin");
658
659        let bridge = client.km_state_bridge();
660        assert!(
661            bridge.get_persistent_pin_envelope().await.is_none(),
662            "switching to AFU must clear the persistent envelope"
663        );
664        assert_eq!(
665            system.get_pin_lock_type().await,
666            Some(PinLockType::AfterFirstUnlock)
667        );
668        assert!(system.validate_pin("second".into()).await);
669        assert!(!system.validate_pin("first".into()).await);
670    }
671
672    #[tokio::test]
673    async fn unset_pin_clears_all_state() {
674        let client = client_with_user_key();
675        let system = PinLockSystem::with_client(&client);
676
677        system
678            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
679            .await
680            .expect("set_pin succeeds");
681        system.unset_pin().await;
682
683        let bridge = client.km_state_bridge();
684        assert!(bridge.get_persistent_pin_envelope().await.is_none());
685        assert!(bridge.get_ephemeral_pin_envelope().await.is_none());
686        assert!(bridge.get_encrypted_pin().await.is_none());
687        assert_eq!(system.get_pin_lock_type().await, None);
688        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NotSet);
689    }
690
691    #[tokio::test]
692    async fn unlock_with_correct_pin_persists_user_key() {
693        let client = client_with_user_key();
694        let system = PinLockSystem::with_client(&client);
695
696        let pre_unlock_user_key_id = user_key_id(&client);
697        // Snapshot ciphertext under the original user key, then drop the key from memory.
698        system
699            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
700            .await
701            .expect("set_pin succeeds");
702        client.internal.get_key_store().clear();
703
704        assert!(system.unlock("1234").await.is_ok());
705        let post_unlock_user_key_id = user_key_id(&client);
706        assert_eq!(post_unlock_user_key_id, pre_unlock_user_key_id);
707    }
708
709    #[tokio::test]
710    async fn unlock_with_wrong_pin_returns_pin_wrong() {
711        let client = client_with_user_key();
712        let system = PinLockSystem::with_client(&client);
713        system
714            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
715            .await
716            .expect("set_pin succeeds");
717
718        assert!(matches!(
719            system.unlock("wrong").await,
720            Err(UnlockError::PinWrong)
721        ));
722    }
723
724    #[tokio::test]
725    async fn unlock_with_no_pin_set_returns_no_pin_set() {
726        let client = client_with_user_key();
727        let system = PinLockSystem::with_client(&client);
728
729        assert!(matches!(
730            system.unlock("anything").await,
731            Err(UnlockError::NoPinSet)
732        ));
733    }
734
735    #[tokio::test]
736    async fn unlock_prefers_ephemeral_envelope_over_persistent() {
737        let client = client_with_user_key();
738        let system = PinLockSystem::with_client(&client);
739        system
740            .set_pin("persistent".into(), PinLockType::BeforeFirstUnlock)
741            .await
742            .expect("set_pin succeeds");
743
744        // Replace the ephemeral envelope with one sealed under a different PIN
745        // (same user key still in the slot).
746        let ephemeral = seal_envelope(&client, "ephemeral");
747        client
748            .km_state_bridge()
749            .set_ephemeral_pin_envelope(&ephemeral)
750            .await;
751
752        assert!(system.unlock("ephemeral").await.is_ok());
753        assert!(matches!(
754            system.unlock("persistent").await,
755            Err(UnlockError::PinWrong)
756        ));
757    }
758
759    #[tokio::test]
760    async fn get_pin_status_available_bfu() {
761        let client = client_with_user_key();
762        let system = PinLockSystem::with_client(&client);
763        system
764            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
765            .await
766            .expect("set_pin succeeds");
767
768        // Simulate app restart: ephemeral memory state is gone, only persisted disk state remains.
769        client
770            .km_state_bridge()
771            .clear_ephemeral_pin_envelope()
772            .await;
773
774        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
775        assert_eq!(
776            system.get_pin_lock_type().await,
777            Some(PinLockType::BeforeFirstUnlock)
778        );
779    }
780
781    #[tokio::test]
782    async fn on_unlock_rebuilds_ephemeral_envelope() {
783        let client = client_with_user_key();
784        let user_key_id = user_key_id(&client);
785        let system = PinLockSystem::with_client(&client);
786        system
787            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
788            .await
789            .expect("set_pin succeeds");
790        client
791            .km_state_bridge()
792            .clear_ephemeral_pin_envelope()
793            .await;
794        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NeedsUnlock);
795
796        system.on_unlock().await;
797
798        let rebuilt = client
799            .km_state_bridge()
800            .get_ephemeral_pin_envelope()
801            .await
802            .expect("on_unlock should restore the ephemeral envelope");
803        assert_envelope_wraps_user_key(&client, &rebuilt, "1234", &user_key_id);
804        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
805        assert!(system.unlock("1234").await.is_ok());
806    }
807
808    #[tokio::test]
809    async fn on_unlock_keeps_ephemeral_envelope_for_current_user_key() {
810        let client = client_with_user_key();
811        let system = PinLockSystem::with_client(&client);
812        system
813            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
814            .await
815            .expect("set_pin succeeds");
816        let before = client
817            .km_state_bridge()
818            .get_ephemeral_pin_envelope()
819            .await
820            .expect("set_pin stores an ephemeral envelope");
821
822        system.on_unlock().await;
823
824        let after = client
825            .km_state_bridge()
826            .get_ephemeral_pin_envelope()
827            .await
828            .expect("the ephemeral envelope is kept");
829        assert_eq!(Vec::<u8>::from(&before), Vec::<u8>::from(&after));
830        assert!(system.unlock("1234").await.is_ok());
831    }
832
833    #[tokio::test]
834    async fn on_unlock_rebuilds_ephemeral_envelope_for_other_user_key() {
835        let client = client_with_user_key();
836        let user_key_id = user_key_id(&client);
837        let system = PinLockSystem::with_client(&client);
838        system
839            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
840            .await
841            .expect("set_pin succeeds");
842
843        let other_client = client_with_user_key();
844        PinLockSystem::with_client(&other_client)
845            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
846            .await
847            .expect("set_pin succeeds");
848        let foreign_envelope = other_client
849            .km_state_bridge()
850            .get_ephemeral_pin_envelope()
851            .await
852            .expect("set_pin stores an ephemeral envelope");
853        client
854            .km_state_bridge()
855            .set_ephemeral_pin_envelope(&foreign_envelope)
856            .await;
857
858        system.on_unlock().await;
859
860        let rebuilt = client
861            .km_state_bridge()
862            .get_ephemeral_pin_envelope()
863            .await
864            .expect("on_unlock should restore the ephemeral envelope");
865        assert_envelope_wraps_user_key(&client, &rebuilt, "1234", &user_key_id);
866        assert!(system.unlock("1234").await.is_ok());
867    }
868
869    #[tokio::test]
870    async fn on_unlock_is_noop_when_no_encrypted_pin() {
871        let client = client_with_user_key();
872        let system = PinLockSystem::with_client(&client);
873
874        system.on_unlock().await;
875
876        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NotSet);
877    }
878
879    #[tokio::test]
880    async fn on_unlock_is_noop_when_bridge_not_registered() {
881        let client = Client::new(None);
882        let system = PinLockSystem::with_client(&client);
883
884        // Must not panic even though no StateBridgeImpl is registered.
885        system.on_unlock().await;
886    }
887
888    #[tokio::test]
889    async fn get_pin_returns_set_pin() {
890        let client = client_with_user_key();
891        let system = PinLockSystem::with_client(&client);
892
893        assert_eq!(system.get_pin().await, None);
894
895        system
896            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
897            .await
898            .expect("set_pin succeeds");
899        assert_eq!(system.get_pin().await, Some("1234".to_owned()));
900
901        system.unset_pin().await;
902        assert_eq!(system.get_pin().await, None);
903    }
904
905    #[tokio::test]
906    async fn validate_pin_matches_only_correct_pin() {
907        let client = client_with_user_key();
908        let system = PinLockSystem::with_client(&client);
909
910        assert!(!system.validate_pin("anything".into()).await);
911
912        system
913            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
914            .await
915            .expect("set_pin succeeds");
916        assert!(system.validate_pin("1234".into()).await);
917        assert!(!system.validate_pin("wrong".into()).await);
918    }
919
920    /// Snapshot of the persisted state a client would have after a V1→V2 user-key upgrade,
921    /// before the PIN envelope has been re-sealed.
922    struct V1State {
923        envelope: PasswordProtectedKeyEnvelope,
924        encrypted_pin: EncString,
925        token: V2UpgradeToken,
926    }
927
928    /// Builds a client with a V2 user key in the `User` slot plus the disk-shaped artifacts
929    /// of a prior V1 PIN enrollment: a V1 key sealed in a PIN envelope, an encrypted PIN under that
930    /// V1 key, and a V2 upgrade token tying the two.
931    fn fresh_v1_state_with_v2_user_key(pin: &str) -> (Client, V1State) {
932        let client = Client::new(None);
933        client
934            .km_state_bridge()
935            .register_bridge(Box::new(InMemoryStateBridge::default()));
936
937        let state = {
938            let key_store = client.internal.get_key_store();
939            let mut ctx = key_store.context_mut();
940
941            let v1_local = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
942            let v2_local = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
943
944            let envelope = PasswordProtectedKeyEnvelope::seal(
945                v1_local,
946                pin,
947                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
948                &ctx,
949            )
950            .expect("v1 envelope seals");
951            let encrypted_pin = pin
952                .encrypt(&mut ctx, v1_local)
953                .expect("pin encrypts under v1 key");
954            let token =
955                V2UpgradeToken::create(v1_local, v2_local, &ctx).expect("upgrade token created");
956
957            ctx.persist_symmetric_key(v2_local, SymmetricKeySlotId::User)
958                .expect("persisting v2 user key succeeds");
959
960            V1State {
961                envelope,
962                encrypted_pin,
963                token,
964            }
965        };
966
967        (client, state)
968    }
969
970    /// Like `client_with_user_key`, but installs a V1 (Aes256CbcHmac) user key, so
971    /// `get_symmetric_key_id(User)` returns `None`.
972    fn client_with_v1_user_key() -> Client {
973        let client = Client::new(None);
974        client
975            .km_state_bridge()
976            .register_bridge(Box::new(InMemoryStateBridge::default()));
977        {
978            let key_store = client.internal.get_key_store();
979            let mut ctx = key_store.context_mut();
980            let user_key = ctx.generate_symmetric_key();
981            ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)
982                .expect("persisting v1 user key should succeed");
983        }
984        client
985    }
986
987    fn assert_pin_envelopes_equal(
988        envelope_1: &PasswordProtectedKeyEnvelope,
989        envelope_2: &PasswordProtectedKeyEnvelope,
990    ) {
991        assert_eq!(
992            serde_json::to_string(envelope_1).expect("envelope serializes"),
993            serde_json::to_string(envelope_2).expect("envelope serializes"),
994            "envelopes should be identical",
995        );
996    }
997
998    async fn assert_pin_fully_unenrolled(client: &Client) {
999        let bridge = client.km_state_bridge();
1000        assert!(bridge.get_persistent_pin_envelope().await.is_none());
1001        assert!(bridge.get_ephemeral_pin_envelope().await.is_none());
1002        assert!(bridge.get_encrypted_pin().await.is_none());
1003        assert_eq!(
1004            PinLockSystem::with_client(client).get_pin_status().await,
1005            PinUnlockStatus::NotSet,
1006        );
1007    }
1008
1009    // ------------------------------------------------------------------------------------
1010    // PIN migration
1011    //
1012    // One scenario per outcome of `migrate_pin_envelope_if_needed`, in the order of
1013    // `classify_encrypted_pin`, then the failures, then `on_unlock` end to end.
1014    // ------------------------------------------------------------------------------------
1015
1016    /// Stores the V1 PIN enrollment from `state` the way `lock_type` persists it.
1017    async fn store_v1_pin(client: &Client, state: &V1State, lock_type: &PinLockType) {
1018        let bridge = client.km_state_bridge();
1019        bridge.set_encrypted_pin(&state.encrypted_pin).await;
1020        if *lock_type == PinLockType::BeforeFirstUnlock {
1021            bridge.set_persistent_pin_envelope(&state.envelope).await;
1022        }
1023    }
1024
1025    #[tokio::test]
1026    async fn migrate_without_pin_is_noop() {
1027        let client = client_with_user_key();
1028        let system = PinLockSystem::with_client(&client);
1029
1030        system
1031            .migrate_pin_envelope_if_needed()
1032            .await
1033            .expect("migration succeeds");
1034
1035        assert_pin_fully_unenrolled(&client).await;
1036    }
1037
1038    /// The encrypted PIN is under the current V2 user key.
1039    #[tokio::test]
1040    async fn migrate_up_to_date_v2_pin_is_noop() {
1041        let client = client_with_user_key();
1042        let system = PinLockSystem::with_client(&client);
1043        system
1044            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
1045            .await
1046            .expect("set_pin succeeds");
1047
1048        let bridge = client.km_state_bridge();
1049        let persistent_before = bridge.get_persistent_pin_envelope().await.expect("present");
1050        let encrypted_pin_before = bridge.get_encrypted_pin().await.expect("present");
1051
1052        system
1053            .migrate_pin_envelope_if_needed()
1054            .await
1055            .expect("migration succeeds");
1056
1057        let persistent_after = bridge.get_persistent_pin_envelope().await.expect("present");
1058        let encrypted_pin_after = bridge.get_encrypted_pin().await.expect("present");
1059        assert_pin_envelopes_equal(&persistent_before, &persistent_after);
1060        assert_eq!(
1061            encrypted_pin_before.to_string(),
1062            encrypted_pin_after.to_string()
1063        );
1064    }
1065
1066    /// The encrypted PIN is under the current V1 user key. The envelope predates derived key ids,
1067    /// which must not matter: every existing V1 PIN user is in this state.
1068    #[tokio::test]
1069    async fn migrate_up_to_date_v1_pin_is_noop() {
1070        let client = client_with_v1_user_key();
1071        let encrypted_pin = TESTVECTOR_LEGACY_ENVELOPE_PIN
1072            .encrypt(
1073                &mut client.internal.get_key_store().context_mut(),
1074                SymmetricKeySlotId::User,
1075            )
1076            .expect("encrypt under v1 user key");
1077
1078        let bridge = client.km_state_bridge();
1079        bridge.set_persistent_pin_envelope(&legacy_envelope()).await;
1080        bridge.set_encrypted_pin(&encrypted_pin).await;
1081
1082        let system = PinLockSystem::with_client(&client);
1083        system
1084            .migrate_pin_envelope_if_needed()
1085            .await
1086            .expect("migration succeeds");
1087
1088        let persistent_after = bridge.get_persistent_pin_envelope().await.expect("present");
1089        let encrypted_pin_after = bridge.get_encrypted_pin().await.expect("present");
1090        assert_pin_envelopes_equal(&legacy_envelope(), &persistent_after);
1091        assert_eq!(encrypted_pin.to_string(), encrypted_pin_after.to_string());
1092    }
1093
1094    /// The encrypted PIN is under the V1 key the upgrade token unwraps. It is re-enrolled under
1095    /// the V2 user key, keeping its lock type.
1096    #[tokio::test]
1097    async fn migrate_v1_pin_with_v2_user_key_reenrolls() {
1098        for lock_type in [
1099            PinLockType::BeforeFirstUnlock,
1100            PinLockType::AfterFirstUnlock,
1101        ] {
1102            let pin = "1234";
1103            let (client, state) = fresh_v1_state_with_v2_user_key(pin);
1104            store_v1_pin(&client, &state, &lock_type).await;
1105            client
1106                .km_state_bridge()
1107                .set_v2_upgrade_token(&state.token)
1108                .await;
1109
1110            let system = PinLockSystem::with_client(&client);
1111            system
1112                .migrate_pin_envelope_if_needed()
1113                .await
1114                .expect("migration succeeds");
1115
1116            let bridge = client.km_state_bridge();
1117            let encrypted_pin = bridge.get_encrypted_pin().await.expect("present");
1118            let ephemeral = bridge.get_ephemeral_pin_envelope().await.expect("present");
1119
1120            assert_eq!(decrypt_encrypted_pin(&client, &encrypted_pin), pin);
1121            assert_envelope_wraps_user_key(&client, &ephemeral, pin, &user_key_id(&client));
1122            assert_eq!(system.get_pin_lock_type().await, Some(lock_type));
1123            assert!(system.unlock(pin).await.is_ok());
1124        }
1125    }
1126
1127    /// Legacy compat encryption gives V1 PINs a key id. It is the algorithm, not a missing key id,
1128    /// that marks a PIN as V1.
1129    #[test]
1130    fn classify_v1_pin_with_key_id() {
1131        let user_key_id = KeyId::from([1; 16]);
1132        let other_key_id = KeyId::from([2; 16]);
1133
1134        assert_eq!(
1135            classify_encrypted_pin(
1136                Some(SymmetricKeyAlgorithm::Aes256CbcHmac),
1137                Some(&other_key_id),
1138                SymmetricKeyAlgorithm::XAes256Gcm,
1139                &user_key_id
1140            ),
1141            PinMigrationAction::MigrateV1ToV2,
1142        );
1143        assert_eq!(
1144            classify_encrypted_pin(
1145                Some(SymmetricKeyAlgorithm::Aes256CbcHmac),
1146                Some(&user_key_id),
1147                SymmetricKeyAlgorithm::Aes256CbcHmac,
1148                &user_key_id
1149            ),
1150            PinMigrationAction::UpToDate,
1151        );
1152        assert_eq!(
1153            classify_encrypted_pin(
1154                Some(SymmetricKeyAlgorithm::Aes256CbcHmac),
1155                Some(&other_key_id),
1156                SymmetricKeyAlgorithm::Aes256CbcHmac,
1157                &user_key_id
1158            ),
1159            PinMigrationAction::Failed(MigrationFailed::UnrecoverablePinKey),
1160        );
1161    }
1162
1163    /// The encrypted PIN is under a previous V2 user key, which nothing can recover.
1164    #[tokio::test]
1165    async fn migrate_v2_pin_with_rotated_user_key_fails() {
1166        let client = client_with_user_key();
1167        let encrypted_pin = {
1168            let mut ctx = client.internal.get_key_store().context_mut();
1169            let other_v2 = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
1170            "1234"
1171                .encrypt(&mut ctx, other_v2)
1172                .expect("encrypt under other v2 key")
1173        };
1174        client
1175            .km_state_bridge()
1176            .set_encrypted_pin(&encrypted_pin)
1177            .await;
1178
1179        let system = PinLockSystem::with_client(&client);
1180        assert_eq!(
1181            system.migrate_pin_envelope_if_needed().await,
1182            Err(MigrationFailed::UnrecoverablePinKey),
1183        );
1184    }
1185
1186    #[tokio::test]
1187    async fn migrate_without_user_key_fails_locked() {
1188        let client = Client::new(None);
1189        client
1190            .km_state_bridge()
1191            .register_bridge(Box::new(InMemoryStateBridge::default()));
1192        let (_, state) = fresh_v1_state_with_v2_user_key("1234");
1193        client
1194            .km_state_bridge()
1195            .set_encrypted_pin(&state.encrypted_pin)
1196            .await;
1197
1198        let system = PinLockSystem::with_client(&client);
1199        assert_eq!(
1200            system.migrate_pin_envelope_if_needed().await,
1201            Err(MigrationFailed::Locked),
1202        );
1203    }
1204
1205    #[tokio::test]
1206    async fn migrate_envelope_without_encrypted_pin_fails() {
1207        let client = client_with_v1_user_key();
1208        client
1209            .km_state_bridge()
1210            .set_persistent_pin_envelope(&legacy_envelope())
1211            .await;
1212
1213        let system = PinLockSystem::with_client(&client);
1214        assert_eq!(
1215            system.migrate_pin_envelope_if_needed().await,
1216            Err(MigrationFailed::MissingEncryptedPin),
1217        );
1218    }
1219
1220    #[tokio::test]
1221    async fn migrate_v1_pin_without_token_fails() {
1222        let (client, state) = fresh_v1_state_with_v2_user_key("1234");
1223        store_v1_pin(&client, &state, &PinLockType::AfterFirstUnlock).await;
1224
1225        let system = PinLockSystem::with_client(&client);
1226        assert_eq!(
1227            system.migrate_pin_envelope_if_needed().await,
1228            Err(MigrationFailed::MissingV2UpgradeToken),
1229        );
1230    }
1231
1232    /// The token unwraps a V1 key other than the one the PIN is encrypted with.
1233    #[tokio::test]
1234    async fn migrate_v1_pin_with_mismatched_token_fails() {
1235        let (client, state) = fresh_v1_state_with_v2_user_key("1234");
1236        store_v1_pin(&client, &state, &PinLockType::AfterFirstUnlock).await;
1237
1238        // Wraps the current V2 user key, but alongside an unrelated V1 key.
1239        let unrelated_token = {
1240            let mut ctx = client.internal.get_key_store().context_mut();
1241            let other_v1 = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
1242            V2UpgradeToken::create(other_v1, SymmetricKeySlotId::User, &ctx)
1243                .expect("unrelated token created")
1244        };
1245        client
1246            .km_state_bridge()
1247            .set_v2_upgrade_token(&unrelated_token)
1248            .await;
1249
1250        let system = PinLockSystem::with_client(&client);
1251        assert_eq!(
1252            system.migrate_pin_envelope_if_needed().await,
1253            Err(MigrationFailed::PinDecryption),
1254        );
1255    }
1256
1257    /// An AfterFirstUnlock PIN is available again after unlocking onto the upgraded key.
1258    #[tokio::test]
1259    async fn on_unlock_restores_afu_pin_after_upgrade() {
1260        let pin = "1234";
1261        let (client, state) = fresh_v1_state_with_v2_user_key(pin);
1262        store_v1_pin(&client, &state, &PinLockType::AfterFirstUnlock).await;
1263        client
1264            .km_state_bridge()
1265            .set_v2_upgrade_token(&state.token)
1266            .await;
1267
1268        let system = PinLockSystem::with_client(&client);
1269        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NeedsUnlock);
1270
1271        system.on_unlock().await;
1272
1273        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
1274        assert!(system.unlock(pin).await.is_ok());
1275    }
1276
1277    #[tokio::test]
1278    async fn on_unlock_unenrolls_when_migration_fails() {
1279        // Reuse the missing-upgrade-token scenario to drive migration failure end-to-end.
1280        let (client, state) = fresh_v1_state_with_v2_user_key("1234");
1281        store_v1_pin(&client, &state, &PinLockType::BeforeFirstUnlock).await;
1282
1283        let system = PinLockSystem::with_client(&client);
1284        system.on_unlock().await;
1285
1286        assert_pin_fully_unenrolled(&client).await;
1287    }
1288}