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        // Make the fresh PIN envelope
272        let Ok(pin_envelope) = (|| -> Result<PasswordProtectedKeyEnvelope, ()> {
273            let mut ctx = self.key_store().context_mut();
274            let pin: String = encrypted_pin
275                .decrypt(&mut ctx, SymmetricKeySlotId::User)
276                .map_err(|_| ())?;
277            PasswordProtectedKeyEnvelope::seal(
278                SymmetricKeySlotId::User,
279                pin.as_str(),
280                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
281                &ctx,
282            )
283            .map_err(|_| ())
284        })() else {
285            warn!("Failed to create PIN envelope");
286            return;
287        };
288
289        // Store it to memory
290        self.client
291            .km_state_bridge()
292            .set_ephemeral_pin_envelope(&pin_envelope)
293            .await;
294    }
295
296    /// Sets the PIN and stores the generated envelope according to the lock type.
297    pub async fn set_pin(&self, pin: String, lock_type: PinLockType) -> Result<(), ()> {
298        // Clear the existing configuration
299        self.client
300            .km_state_bridge()
301            .clear_persistent_pin_envelope()
302            .await;
303        self.client
304            .km_state_bridge()
305            .clear_ephemeral_pin_envelope()
306            .await;
307        self.client.km_state_bridge().clear_encrypted_pin().await;
308
309        let pin_envelope: PasswordProtectedKeyEnvelope = PasswordProtectedKeyEnvelope::seal(
310            SymmetricKeySlotId::User,
311            pin.as_str(),
312            PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
313            &self.key_store().context_mut(),
314        )
315        .map_err(|_| ())?;
316        let encrypted_pin = pin
317            .encrypt(
318                &mut self.key_store().context_mut(),
319                SymmetricKeySlotId::User,
320            )
321            .map_err(|_| ())?;
322
323        self.client
324            .km_state_bridge()
325            .set_encrypted_pin(&encrypted_pin)
326            .await;
327        self.client
328            .km_state_bridge()
329            .set_ephemeral_pin_envelope(&pin_envelope)
330            .await;
331
332        if lock_type == PinLockType::BeforeFirstUnlock {
333            self.client
334                .km_state_bridge()
335                .set_persistent_pin_envelope(&pin_envelope)
336                .await;
337        }
338
339        Ok(())
340    }
341
342    /// Clears both persistent and ephemeral PIN envelopes.
343    pub async fn unset_pin(&self) {
344        self.client
345            .km_state_bridge()
346            .clear_persistent_pin_envelope()
347            .await;
348        self.client
349            .km_state_bridge()
350            .clear_ephemeral_pin_envelope()
351            .await;
352        self.client.km_state_bridge().clear_encrypted_pin().await;
353    }
354
355    /// Returns the lock type for the currently configured PIN.
356    pub async fn get_pin_lock_type(&self) -> Option<PinLockType> {
357        if self
358            .client
359            .km_state_bridge()
360            .get_persistent_pin_envelope()
361            .await
362            .is_some()
363        {
364            return Some(PinLockType::BeforeFirstUnlock);
365        }
366
367        // Encrypted pin is set for either lock type, persistent pin only for BFU. The ephemeral
368        // envelope may not be set after restarting a client, until the client enters AFU
369        // mode.
370        if self
371            .client
372            .km_state_bridge()
373            .get_encrypted_pin()
374            .await
375            .is_some()
376        {
377            return Some(PinLockType::AfterFirstUnlock);
378        }
379
380        None
381    }
382
383    /// Returns the current PIN unlock status.
384    ///
385    /// If a lock type is configured but no ephemeral envelope is currently present,
386    /// the status is [`PinUnlockStatus::NeedsUnlock`].
387    pub async fn get_pin_status(&self) -> PinUnlockStatus {
388        match Self::get_pin_lock_type(self).await {
389            Some(PinLockType::BeforeFirstUnlock) => {
390                if self.get_active_pin_envelope().await.is_some() {
391                    PinUnlockStatus::Available
392                } else {
393                    PinUnlockStatus::NeedsUnlock
394                }
395            }
396            Some(PinLockType::AfterFirstUnlock) => {
397                if self
398                    .client
399                    .km_state_bridge()
400                    .get_ephemeral_pin_envelope()
401                    .await
402                    .is_some()
403                {
404                    PinUnlockStatus::Available
405                } else {
406                    // This should not happen as AFU should always have the ephemeral envelope, but
407                    // we handle it just in case.
408                    PinUnlockStatus::NeedsUnlock
409                }
410            }
411            None => PinUnlockStatus::NotSet,
412        }
413    }
414
415    /// Returns the configured PIN, if an encrypted PIN is available and decryptable.
416    pub async fn get_pin(&self) -> Option<String> {
417        let encrypted_pin = self.client.km_state_bridge().get_encrypted_pin().await?;
418        encrypted_pin
419            .decrypt(
420                &mut self.client.internal.get_key_store().context_mut(),
421                SymmetricKeySlotId::User,
422            )
423            .ok()
424    }
425
426    /// Validates that the provided PIN can decrypt the stored PIN envelope.
427    pub async fn validate_pin(&self, pin: String) -> bool {
428        let pin_envelope = self.get_active_pin_envelope().await;
429        let Some(pin_envelope) = pin_envelope else {
430            return false;
431        };
432
433        pin_envelope
434            .unseal(
435                pin.as_str(),
436                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
437                &mut self.key_store().context_mut(),
438            )
439            .is_ok()
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use bitwarden_crypto::{EncString, KeyId, SymmetricKeyAlgorithm};
446
447    use super::*;
448    use crate::key_management::{V2UpgradeToken, state_bridge::test_support::InMemoryStateBridge};
449
450    fn decrypt_encrypted_pin(client: &Client, encrypted_pin: &EncString) -> String {
451        encrypted_pin
452            .decrypt(
453                &mut client.internal.get_key_store().context_mut(),
454                SymmetricKeySlotId::User,
455            )
456            .expect("encrypted pin should decrypt successfully")
457    }
458
459    /// The PIN [`TESTVECTOR_LEGACY_ENVELOPE`] was sealed with.
460    const TESTVECTOR_LEGACY_ENVELOPE_PIN: &str = "1234";
461    /// A `PinUnlock` envelope sealed under an AES-CBC-HMAC (V1) key by a client predating derived
462    /// key ids, so it carries no contained key id.
463    ///
464    /// The migration never unseals this envelope — it reads the contained key id and then
465    /// re-enrolls from the key store — so the key sealed inside is arbitrary and deliberately
466    /// unrelated to the user key the tests install.
467    ///
468    /// The current seal path always writes a contained key id, so re-recording this requires
469    /// temporarily changing `set_contained_key_id(&mut header, key_to_seal.key_id())` in
470    /// `bitwarden-crypto/src/safe/password_protected_key_envelope.rs` to pass `None`, sealing a
471    /// `PinUnlock` envelope for an `Aes256CbcHmac` key with [`TESTVECTOR_LEGACY_ENVELOPE_PIN`],
472    /// printing `Vec::from(&envelope)`, and then reverting that change.
473    const TESTVECTOR_LEGACY_ENVELOPE: &[u8] = &[
474        132, 88, 52, 164, 1, 3, 3, 120, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47,
475        120, 46, 98, 105, 116, 119, 97, 114, 100, 101, 110, 46, 108, 101, 103, 97, 99, 121, 45,
476        107, 101, 121, 58, 0, 1, 56, 129, 1, 58, 0, 1, 56, 128, 1, 161, 5, 76, 148, 49, 223, 205,
477        195, 252, 91, 216, 65, 200, 121, 104, 88, 80, 89, 120, 16, 161, 14, 97, 191, 97, 138, 42,
478        102, 234, 49, 186, 2, 255, 31, 4, 232, 178, 100, 53, 37, 181, 172, 129, 193, 51, 109, 8,
479        160, 29, 254, 181, 242, 102, 73, 229, 89, 150, 227, 252, 120, 156, 71, 202, 200, 241, 74,
480        241, 206, 16, 155, 83, 49, 242, 13, 209, 10, 217, 251, 164, 244, 69, 41, 52, 9, 192, 140,
481        248, 251, 244, 84, 154, 15, 100, 222, 102, 117, 185, 129, 131, 71, 161, 1, 58, 0, 1, 21,
482        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,
483        1, 21, 91, 4, 58, 0, 1, 21, 88, 80, 64, 184, 87, 20, 40, 186, 214, 56, 87, 53, 118, 100, 5,
484        21, 13, 3, 246,
485    ];
486
487    /// Parses [`TESTVECTOR_LEGACY_ENVELOPE`], asserting it really has no contained key id.
488    fn legacy_envelope() -> PasswordProtectedKeyEnvelope {
489        let envelope = PasswordProtectedKeyEnvelope::try_from(&TESTVECTOR_LEGACY_ENVELOPE.to_vec())
490            .expect("legacy envelope test vector parses");
491        assert_eq!(
492            envelope.contained_key_id().expect("readable"),
493            None,
494            "legacy envelope test vector must have no contained key id",
495        );
496        envelope
497    }
498
499    /// Returns the `KeyId` of the symmetric key currently in `SymmetricKeySlotId::User`.
500    fn user_key_id(client: &Client) -> KeyId {
501        client
502            .internal
503            .get_key_store()
504            .context()
505            .get_symmetric_key_id(SymmetricKeySlotId::User)
506            .expect("user key present")
507    }
508
509    /// Asserts the envelope wraps `expected_key_id` and unseals successfully under `pin`.
510    fn assert_envelope_wraps_user_key(
511        client: &Client,
512        envelope: &PasswordProtectedKeyEnvelope,
513        pin: &str,
514        expected_key_id: &KeyId,
515    ) {
516        assert_eq!(
517            envelope
518                .contained_key_id()
519                .expect("contained key id readable"),
520            Some(expected_key_id.clone()),
521            "envelope wraps a key other than the current user key",
522        );
523        let _ = envelope
524            .unseal(
525                pin,
526                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
527                &mut client.internal.get_key_store().context_mut(),
528            )
529            .expect("envelope unseals with the configured pin");
530    }
531
532    fn client_with_user_key() -> Client {
533        let client = Client::new(None);
534        client
535            .km_state_bridge()
536            .register_bridge(Box::new(InMemoryStateBridge::default()));
537        {
538            let key_store = client.internal.get_key_store();
539            let mut ctx = key_store.context_mut();
540            let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
541            ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)
542                .expect("persisting user key should succeed");
543        }
544        client
545    }
546
547    fn seal_envelope(client: &Client, pin: &str) -> PasswordProtectedKeyEnvelope {
548        PasswordProtectedKeyEnvelope::seal(
549            SymmetricKeySlotId::User,
550            pin,
551            PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
552            &client.internal.get_key_store().context_mut(),
553        )
554        .expect("seal succeeds")
555    }
556
557    #[tokio::test]
558    async fn set_pin_bfu_persists_both_envelopes() {
559        let client = client_with_user_key();
560        let user_key_id = user_key_id(&client);
561        let system = PinLockSystem::with_client(&client);
562
563        system
564            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
565            .await
566            .expect("set_pin succeeds");
567
568        let bridge = client.km_state_bridge();
569        let persistent = bridge
570            .get_persistent_pin_envelope()
571            .await
572            .expect("persistent envelope present");
573        let ephemeral = bridge
574            .get_ephemeral_pin_envelope()
575            .await
576            .expect("ephemeral envelope present");
577        let encrypted_pin = bridge
578            .get_encrypted_pin()
579            .await
580            .expect("encrypted pin present");
581
582        assert_envelope_wraps_user_key(&client, &persistent, "1234", &user_key_id);
583        assert_envelope_wraps_user_key(&client, &ephemeral, "1234", &user_key_id);
584        assert_eq!(decrypt_encrypted_pin(&client, &encrypted_pin), "1234");
585
586        assert_eq!(
587            system.get_pin_lock_type().await,
588            Some(PinLockType::BeforeFirstUnlock)
589        );
590        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
591    }
592
593    #[tokio::test]
594    async fn set_pin_afu_persists_only_ephemeral() {
595        let client = client_with_user_key();
596        let user_key_id = user_key_id(&client);
597        let system = PinLockSystem::with_client(&client);
598
599        system
600            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
601            .await
602            .expect("set_pin succeeds");
603
604        let bridge = client.km_state_bridge();
605        assert!(bridge.get_persistent_pin_envelope().await.is_none());
606        let ephemeral = bridge
607            .get_ephemeral_pin_envelope()
608            .await
609            .expect("ephemeral envelope present");
610        let encrypted_pin = bridge
611            .get_encrypted_pin()
612            .await
613            .expect("encrypted pin present");
614
615        assert_envelope_wraps_user_key(&client, &ephemeral, "1234", &user_key_id);
616        assert_eq!(decrypt_encrypted_pin(&client, &encrypted_pin), "1234");
617
618        assert_eq!(
619            system.get_pin_lock_type().await,
620            Some(PinLockType::AfterFirstUnlock)
621        );
622        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
623    }
624
625    #[tokio::test]
626    async fn set_pin_overwrites_existing_state() {
627        let client = client_with_user_key();
628        let system = PinLockSystem::with_client(&client);
629
630        system
631            .set_pin("first".into(), PinLockType::BeforeFirstUnlock)
632            .await
633            .expect("first set_pin");
634        system
635            .set_pin("second".into(), PinLockType::AfterFirstUnlock)
636            .await
637            .expect("second set_pin");
638
639        let bridge = client.km_state_bridge();
640        assert!(
641            bridge.get_persistent_pin_envelope().await.is_none(),
642            "switching to AFU must clear the persistent envelope"
643        );
644        assert_eq!(
645            system.get_pin_lock_type().await,
646            Some(PinLockType::AfterFirstUnlock)
647        );
648        assert!(system.validate_pin("second".into()).await);
649        assert!(!system.validate_pin("first".into()).await);
650    }
651
652    #[tokio::test]
653    async fn unset_pin_clears_all_state() {
654        let client = client_with_user_key();
655        let system = PinLockSystem::with_client(&client);
656
657        system
658            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
659            .await
660            .expect("set_pin succeeds");
661        system.unset_pin().await;
662
663        let bridge = client.km_state_bridge();
664        assert!(bridge.get_persistent_pin_envelope().await.is_none());
665        assert!(bridge.get_ephemeral_pin_envelope().await.is_none());
666        assert!(bridge.get_encrypted_pin().await.is_none());
667        assert_eq!(system.get_pin_lock_type().await, None);
668        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NotSet);
669    }
670
671    #[tokio::test]
672    async fn unlock_with_correct_pin_persists_user_key() {
673        let client = client_with_user_key();
674        let system = PinLockSystem::with_client(&client);
675
676        let pre_unlock_user_key_id = user_key_id(&client);
677        // Snapshot ciphertext under the original user key, then drop the key from memory.
678        system
679            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
680            .await
681            .expect("set_pin succeeds");
682        client.internal.get_key_store().clear();
683
684        assert!(system.unlock("1234").await.is_ok());
685        let post_unlock_user_key_id = user_key_id(&client);
686        assert_eq!(post_unlock_user_key_id, pre_unlock_user_key_id);
687    }
688
689    #[tokio::test]
690    async fn unlock_with_wrong_pin_returns_pin_wrong() {
691        let client = client_with_user_key();
692        let system = PinLockSystem::with_client(&client);
693        system
694            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
695            .await
696            .expect("set_pin succeeds");
697
698        assert!(matches!(
699            system.unlock("wrong").await,
700            Err(UnlockError::PinWrong)
701        ));
702    }
703
704    #[tokio::test]
705    async fn unlock_with_no_pin_set_returns_no_pin_set() {
706        let client = client_with_user_key();
707        let system = PinLockSystem::with_client(&client);
708
709        assert!(matches!(
710            system.unlock("anything").await,
711            Err(UnlockError::NoPinSet)
712        ));
713    }
714
715    #[tokio::test]
716    async fn unlock_prefers_ephemeral_envelope_over_persistent() {
717        let client = client_with_user_key();
718        let system = PinLockSystem::with_client(&client);
719        system
720            .set_pin("persistent".into(), PinLockType::BeforeFirstUnlock)
721            .await
722            .expect("set_pin succeeds");
723
724        // Replace the ephemeral envelope with one sealed under a different PIN
725        // (same user key still in the slot).
726        let ephemeral = seal_envelope(&client, "ephemeral");
727        client
728            .km_state_bridge()
729            .set_ephemeral_pin_envelope(&ephemeral)
730            .await;
731
732        assert!(system.unlock("ephemeral").await.is_ok());
733        assert!(matches!(
734            system.unlock("persistent").await,
735            Err(UnlockError::PinWrong)
736        ));
737    }
738
739    #[tokio::test]
740    async fn get_pin_status_available_bfu() {
741        let client = client_with_user_key();
742        let system = PinLockSystem::with_client(&client);
743        system
744            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
745            .await
746            .expect("set_pin succeeds");
747
748        // Simulate app restart: ephemeral memory state is gone, only persisted disk state remains.
749        client
750            .km_state_bridge()
751            .clear_ephemeral_pin_envelope()
752            .await;
753
754        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
755        assert_eq!(
756            system.get_pin_lock_type().await,
757            Some(PinLockType::BeforeFirstUnlock)
758        );
759    }
760
761    #[tokio::test]
762    async fn on_unlock_rebuilds_ephemeral_envelope() {
763        let client = client_with_user_key();
764        let user_key_id = user_key_id(&client);
765        let system = PinLockSystem::with_client(&client);
766        system
767            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
768            .await
769            .expect("set_pin succeeds");
770        client
771            .km_state_bridge()
772            .clear_ephemeral_pin_envelope()
773            .await;
774        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NeedsUnlock);
775
776        system.on_unlock().await;
777
778        let rebuilt = client
779            .km_state_bridge()
780            .get_ephemeral_pin_envelope()
781            .await
782            .expect("on_unlock should restore the ephemeral envelope");
783        assert_envelope_wraps_user_key(&client, &rebuilt, "1234", &user_key_id);
784        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
785        assert!(system.unlock("1234").await.is_ok());
786    }
787
788    #[tokio::test]
789    async fn on_unlock_is_noop_when_no_encrypted_pin() {
790        let client = client_with_user_key();
791        let system = PinLockSystem::with_client(&client);
792
793        system.on_unlock().await;
794
795        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NotSet);
796    }
797
798    #[tokio::test]
799    async fn on_unlock_is_noop_when_bridge_not_registered() {
800        let client = Client::new(None);
801        let system = PinLockSystem::with_client(&client);
802
803        // Must not panic even though no StateBridgeImpl is registered.
804        system.on_unlock().await;
805    }
806
807    #[tokio::test]
808    async fn get_pin_returns_set_pin() {
809        let client = client_with_user_key();
810        let system = PinLockSystem::with_client(&client);
811
812        assert_eq!(system.get_pin().await, None);
813
814        system
815            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
816            .await
817            .expect("set_pin succeeds");
818        assert_eq!(system.get_pin().await, Some("1234".to_owned()));
819
820        system.unset_pin().await;
821        assert_eq!(system.get_pin().await, None);
822    }
823
824    #[tokio::test]
825    async fn validate_pin_matches_only_correct_pin() {
826        let client = client_with_user_key();
827        let system = PinLockSystem::with_client(&client);
828
829        assert!(!system.validate_pin("anything".into()).await);
830
831        system
832            .set_pin("1234".into(), PinLockType::AfterFirstUnlock)
833            .await
834            .expect("set_pin succeeds");
835        assert!(system.validate_pin("1234".into()).await);
836        assert!(!system.validate_pin("wrong".into()).await);
837    }
838
839    /// Snapshot of the persisted state a client would have after a V1→V2 user-key upgrade,
840    /// before the PIN envelope has been re-sealed.
841    struct V1State {
842        envelope: PasswordProtectedKeyEnvelope,
843        encrypted_pin: EncString,
844        token: V2UpgradeToken,
845    }
846
847    /// Builds a client with a V2 user key in the `User` slot plus the disk-shaped artifacts
848    /// of a prior V1 PIN enrollment: a V1 key sealed in a PIN envelope, an encrypted PIN under that
849    /// V1 key, and a V2 upgrade token tying the two.
850    fn fresh_v1_state_with_v2_user_key(pin: &str) -> (Client, V1State) {
851        let client = Client::new(None);
852        client
853            .km_state_bridge()
854            .register_bridge(Box::new(InMemoryStateBridge::default()));
855
856        let state = {
857            let key_store = client.internal.get_key_store();
858            let mut ctx = key_store.context_mut();
859
860            let v1_local = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
861            let v2_local = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
862
863            let envelope = PasswordProtectedKeyEnvelope::seal(
864                v1_local,
865                pin,
866                PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
867                &ctx,
868            )
869            .expect("v1 envelope seals");
870            let encrypted_pin = pin
871                .encrypt(&mut ctx, v1_local)
872                .expect("pin encrypts under v1 key");
873            let token =
874                V2UpgradeToken::create(v1_local, v2_local, &ctx).expect("upgrade token created");
875
876            ctx.persist_symmetric_key(v2_local, SymmetricKeySlotId::User)
877                .expect("persisting v2 user key succeeds");
878
879            V1State {
880                envelope,
881                encrypted_pin,
882                token,
883            }
884        };
885
886        (client, state)
887    }
888
889    /// Like `client_with_user_key`, but installs a V1 (Aes256CbcHmac) user key, so
890    /// `get_symmetric_key_id(User)` returns `None`.
891    fn client_with_v1_user_key() -> Client {
892        let client = Client::new(None);
893        client
894            .km_state_bridge()
895            .register_bridge(Box::new(InMemoryStateBridge::default()));
896        {
897            let key_store = client.internal.get_key_store();
898            let mut ctx = key_store.context_mut();
899            let user_key = ctx.generate_symmetric_key();
900            ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)
901                .expect("persisting v1 user key should succeed");
902        }
903        client
904    }
905
906    fn assert_pin_envelopes_equal(
907        envelope_1: &PasswordProtectedKeyEnvelope,
908        envelope_2: &PasswordProtectedKeyEnvelope,
909    ) {
910        assert_eq!(
911            serde_json::to_string(envelope_1).expect("envelope serializes"),
912            serde_json::to_string(envelope_2).expect("envelope serializes"),
913            "envelopes should be identical",
914        );
915    }
916
917    async fn assert_pin_fully_unenrolled(client: &Client) {
918        let bridge = client.km_state_bridge();
919        assert!(bridge.get_persistent_pin_envelope().await.is_none());
920        assert!(bridge.get_ephemeral_pin_envelope().await.is_none());
921        assert!(bridge.get_encrypted_pin().await.is_none());
922        assert_eq!(
923            PinLockSystem::with_client(client).get_pin_status().await,
924            PinUnlockStatus::NotSet,
925        );
926    }
927
928    // ------------------------------------------------------------------------------------
929    // PIN migration
930    //
931    // One scenario per outcome of `migrate_pin_envelope_if_needed`, in the order of
932    // `classify_encrypted_pin`, then the failures, then `on_unlock` end to end.
933    // ------------------------------------------------------------------------------------
934
935    /// Stores the V1 PIN enrollment from `state` the way `lock_type` persists it.
936    async fn store_v1_pin(client: &Client, state: &V1State, lock_type: &PinLockType) {
937        let bridge = client.km_state_bridge();
938        bridge.set_encrypted_pin(&state.encrypted_pin).await;
939        if *lock_type == PinLockType::BeforeFirstUnlock {
940            bridge.set_persistent_pin_envelope(&state.envelope).await;
941        }
942    }
943
944    #[tokio::test]
945    async fn migrate_without_pin_is_noop() {
946        let client = client_with_user_key();
947        let system = PinLockSystem::with_client(&client);
948
949        system
950            .migrate_pin_envelope_if_needed()
951            .await
952            .expect("migration succeeds");
953
954        assert_pin_fully_unenrolled(&client).await;
955    }
956
957    /// The encrypted PIN is under the current V2 user key.
958    #[tokio::test]
959    async fn migrate_up_to_date_v2_pin_is_noop() {
960        let client = client_with_user_key();
961        let system = PinLockSystem::with_client(&client);
962        system
963            .set_pin("1234".into(), PinLockType::BeforeFirstUnlock)
964            .await
965            .expect("set_pin succeeds");
966
967        let bridge = client.km_state_bridge();
968        let persistent_before = bridge.get_persistent_pin_envelope().await.expect("present");
969        let encrypted_pin_before = bridge.get_encrypted_pin().await.expect("present");
970
971        system
972            .migrate_pin_envelope_if_needed()
973            .await
974            .expect("migration succeeds");
975
976        let persistent_after = bridge.get_persistent_pin_envelope().await.expect("present");
977        let encrypted_pin_after = bridge.get_encrypted_pin().await.expect("present");
978        assert_pin_envelopes_equal(&persistent_before, &persistent_after);
979        assert_eq!(
980            encrypted_pin_before.to_string(),
981            encrypted_pin_after.to_string()
982        );
983    }
984
985    /// The encrypted PIN is under the current V1 user key. The envelope predates derived key ids,
986    /// which must not matter: every existing V1 PIN user is in this state.
987    #[tokio::test]
988    async fn migrate_up_to_date_v1_pin_is_noop() {
989        let client = client_with_v1_user_key();
990        let encrypted_pin = TESTVECTOR_LEGACY_ENVELOPE_PIN
991            .encrypt(
992                &mut client.internal.get_key_store().context_mut(),
993                SymmetricKeySlotId::User,
994            )
995            .expect("encrypt under v1 user key");
996
997        let bridge = client.km_state_bridge();
998        bridge.set_persistent_pin_envelope(&legacy_envelope()).await;
999        bridge.set_encrypted_pin(&encrypted_pin).await;
1000
1001        let system = PinLockSystem::with_client(&client);
1002        system
1003            .migrate_pin_envelope_if_needed()
1004            .await
1005            .expect("migration succeeds");
1006
1007        let persistent_after = bridge.get_persistent_pin_envelope().await.expect("present");
1008        let encrypted_pin_after = bridge.get_encrypted_pin().await.expect("present");
1009        assert_pin_envelopes_equal(&legacy_envelope(), &persistent_after);
1010        assert_eq!(encrypted_pin.to_string(), encrypted_pin_after.to_string());
1011    }
1012
1013    /// The encrypted PIN is under the V1 key the upgrade token unwraps. It is re-enrolled under
1014    /// the V2 user key, keeping its lock type.
1015    #[tokio::test]
1016    async fn migrate_v1_pin_with_v2_user_key_reenrolls() {
1017        for lock_type in [
1018            PinLockType::BeforeFirstUnlock,
1019            PinLockType::AfterFirstUnlock,
1020        ] {
1021            let pin = "1234";
1022            let (client, state) = fresh_v1_state_with_v2_user_key(pin);
1023            store_v1_pin(&client, &state, &lock_type).await;
1024            client
1025                .km_state_bridge()
1026                .set_v2_upgrade_token(&state.token)
1027                .await;
1028
1029            let system = PinLockSystem::with_client(&client);
1030            system
1031                .migrate_pin_envelope_if_needed()
1032                .await
1033                .expect("migration succeeds");
1034
1035            let bridge = client.km_state_bridge();
1036            let encrypted_pin = bridge.get_encrypted_pin().await.expect("present");
1037            let ephemeral = bridge.get_ephemeral_pin_envelope().await.expect("present");
1038
1039            assert_eq!(decrypt_encrypted_pin(&client, &encrypted_pin), pin);
1040            assert_envelope_wraps_user_key(&client, &ephemeral, pin, &user_key_id(&client));
1041            assert_eq!(system.get_pin_lock_type().await, Some(lock_type));
1042            assert!(system.unlock(pin).await.is_ok());
1043        }
1044    }
1045
1046    /// Legacy compat encryption gives V1 PINs a key id. It is the algorithm, not a missing key id,
1047    /// that marks a PIN as V1.
1048    #[test]
1049    fn classify_v1_pin_with_key_id() {
1050        let user_key_id = KeyId::from([1; 16]);
1051        let other_key_id = KeyId::from([2; 16]);
1052
1053        assert_eq!(
1054            classify_encrypted_pin(
1055                Some(SymmetricKeyAlgorithm::Aes256CbcHmac),
1056                Some(&other_key_id),
1057                SymmetricKeyAlgorithm::XAes256Gcm,
1058                &user_key_id
1059            ),
1060            PinMigrationAction::MigrateV1ToV2,
1061        );
1062        assert_eq!(
1063            classify_encrypted_pin(
1064                Some(SymmetricKeyAlgorithm::Aes256CbcHmac),
1065                Some(&user_key_id),
1066                SymmetricKeyAlgorithm::Aes256CbcHmac,
1067                &user_key_id
1068            ),
1069            PinMigrationAction::UpToDate,
1070        );
1071        assert_eq!(
1072            classify_encrypted_pin(
1073                Some(SymmetricKeyAlgorithm::Aes256CbcHmac),
1074                Some(&other_key_id),
1075                SymmetricKeyAlgorithm::Aes256CbcHmac,
1076                &user_key_id
1077            ),
1078            PinMigrationAction::Failed(MigrationFailed::UnrecoverablePinKey),
1079        );
1080    }
1081
1082    /// The encrypted PIN is under a previous V2 user key, which nothing can recover.
1083    #[tokio::test]
1084    async fn migrate_v2_pin_with_rotated_user_key_fails() {
1085        let client = client_with_user_key();
1086        let encrypted_pin = {
1087            let mut ctx = client.internal.get_key_store().context_mut();
1088            let other_v2 = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
1089            "1234"
1090                .encrypt(&mut ctx, other_v2)
1091                .expect("encrypt under other v2 key")
1092        };
1093        client
1094            .km_state_bridge()
1095            .set_encrypted_pin(&encrypted_pin)
1096            .await;
1097
1098        let system = PinLockSystem::with_client(&client);
1099        assert_eq!(
1100            system.migrate_pin_envelope_if_needed().await,
1101            Err(MigrationFailed::UnrecoverablePinKey),
1102        );
1103    }
1104
1105    #[tokio::test]
1106    async fn migrate_without_user_key_fails_locked() {
1107        let client = Client::new(None);
1108        client
1109            .km_state_bridge()
1110            .register_bridge(Box::new(InMemoryStateBridge::default()));
1111        let (_, state) = fresh_v1_state_with_v2_user_key("1234");
1112        client
1113            .km_state_bridge()
1114            .set_encrypted_pin(&state.encrypted_pin)
1115            .await;
1116
1117        let system = PinLockSystem::with_client(&client);
1118        assert_eq!(
1119            system.migrate_pin_envelope_if_needed().await,
1120            Err(MigrationFailed::Locked),
1121        );
1122    }
1123
1124    #[tokio::test]
1125    async fn migrate_envelope_without_encrypted_pin_fails() {
1126        let client = client_with_v1_user_key();
1127        client
1128            .km_state_bridge()
1129            .set_persistent_pin_envelope(&legacy_envelope())
1130            .await;
1131
1132        let system = PinLockSystem::with_client(&client);
1133        assert_eq!(
1134            system.migrate_pin_envelope_if_needed().await,
1135            Err(MigrationFailed::MissingEncryptedPin),
1136        );
1137    }
1138
1139    #[tokio::test]
1140    async fn migrate_v1_pin_without_token_fails() {
1141        let (client, state) = fresh_v1_state_with_v2_user_key("1234");
1142        store_v1_pin(&client, &state, &PinLockType::AfterFirstUnlock).await;
1143
1144        let system = PinLockSystem::with_client(&client);
1145        assert_eq!(
1146            system.migrate_pin_envelope_if_needed().await,
1147            Err(MigrationFailed::MissingV2UpgradeToken),
1148        );
1149    }
1150
1151    /// The token unwraps a V1 key other than the one the PIN is encrypted with.
1152    #[tokio::test]
1153    async fn migrate_v1_pin_with_mismatched_token_fails() {
1154        let (client, state) = fresh_v1_state_with_v2_user_key("1234");
1155        store_v1_pin(&client, &state, &PinLockType::AfterFirstUnlock).await;
1156
1157        // Wraps the current V2 user key, but alongside an unrelated V1 key.
1158        let unrelated_token = {
1159            let mut ctx = client.internal.get_key_store().context_mut();
1160            let other_v1 = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
1161            V2UpgradeToken::create(other_v1, SymmetricKeySlotId::User, &ctx)
1162                .expect("unrelated token created")
1163        };
1164        client
1165            .km_state_bridge()
1166            .set_v2_upgrade_token(&unrelated_token)
1167            .await;
1168
1169        let system = PinLockSystem::with_client(&client);
1170        assert_eq!(
1171            system.migrate_pin_envelope_if_needed().await,
1172            Err(MigrationFailed::PinDecryption),
1173        );
1174    }
1175
1176    /// An AfterFirstUnlock PIN is available again after unlocking onto the upgraded key.
1177    #[tokio::test]
1178    async fn on_unlock_restores_afu_pin_after_upgrade() {
1179        let pin = "1234";
1180        let (client, state) = fresh_v1_state_with_v2_user_key(pin);
1181        store_v1_pin(&client, &state, &PinLockType::AfterFirstUnlock).await;
1182        client
1183            .km_state_bridge()
1184            .set_v2_upgrade_token(&state.token)
1185            .await;
1186
1187        let system = PinLockSystem::with_client(&client);
1188        assert_eq!(system.get_pin_status().await, PinUnlockStatus::NeedsUnlock);
1189
1190        system.on_unlock().await;
1191
1192        assert_eq!(system.get_pin_status().await, PinUnlockStatus::Available);
1193        assert!(system.unlock(pin).await.is_ok());
1194    }
1195
1196    #[tokio::test]
1197    async fn on_unlock_unenrolls_when_migration_fails() {
1198        // Reuse the missing-upgrade-token scenario to drive migration failure end-to-end.
1199        let (client, state) = fresh_v1_state_with_v2_user_key("1234");
1200        store_v1_pin(&client, &state, &PinLockType::BeforeFirstUnlock).await;
1201
1202        let system = PinLockSystem::with_client(&client);
1203        system.on_unlock().await;
1204
1205        assert_pin_fully_unenrolled(&client).await;
1206    }
1207}