Skip to main content

bitwarden_core/key_management/
master_password.rs

1use std::num::NonZeroU32;
2
3use bitwarden_api_api::models::{
4    KdfRequestModel, KdfType, MasterPasswordAuthenticationDataRequestModel,
5    MasterPasswordUnlockDataRequestModel,
6    master_password_unlock_response_model::MasterPasswordUnlockResponseModel,
7};
8use bitwarden_crypto::{
9    EncString, Kdf, KeySlotIds, KeyStoreContext, MasterKey, SymmetricCryptoKey,
10};
11use bitwarden_encoding::B64;
12use bitwarden_error::bitwarden_error;
13use serde::{Deserialize, Serialize};
14use tracing::Level;
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17
18use crate::{MissingFieldError, require};
19
20/// Error for master password related operations.
21#[allow(dead_code)]
22#[bitwarden_error(flat)]
23#[derive(Debug, thiserror::Error)]
24pub enum MasterPasswordError {
25    /// The wrapped encryption key could not be parsed because the encstring is malformed
26    #[error("Wrapped encryption key is malformed")]
27    EncryptionKeyMalformed,
28    /// The KDF data could not be parsed, because it has an invalid value
29    #[error("KDF is malformed")]
30    KdfMalformed,
31    /// The KDF had an invalid configuration
32    #[error("Invalid KDF configuration")]
33    InvalidKdfConfiguration,
34    /// The wrapped encryption key or salt fields are missing or KDF data is incomplete
35    #[error(transparent)]
36    MissingField(#[from] MissingFieldError),
37    /// Generic crypto error
38    #[error(transparent)]
39    Crypto(#[from] bitwarden_crypto::CryptoError),
40    /// The provided password is incorrect
41    #[error("Wrong password")]
42    WrongPassword,
43}
44
45/// Represents the data required to unlock with the master password.
46#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
49#[cfg_attr(
50    feature = "wasm",
51    derive(tsify::Tsify),
52    tsify(into_wasm_abi, from_wasm_abi)
53)]
54pub struct MasterPasswordUnlockData {
55    /// The key derivation function used to derive the master key
56    pub kdf: Kdf,
57    /// The master key wrapped user key
58    pub master_key_wrapped_user_key: EncString,
59    /// The salt used in the KDF, typically the user's email
60    pub salt: String,
61}
62
63impl MasterPasswordUnlockData {
64    /// Unwrap the user key into the key store context using the provided password.
65    pub fn unwrap_to_context<Ids: KeySlotIds>(
66        &self,
67        password: &str,
68        ctx: &mut KeyStoreContext<Ids>,
69    ) -> Result<Ids::Symmetric, MasterPasswordError> {
70        let master_key = MasterKey::derive(password, &self.salt, &self.kdf)
71            .map_err(|_| MasterPasswordError::InvalidKdfConfiguration)?;
72        let user_key = master_key
73            .decrypt_user_key(self.master_key_wrapped_user_key.clone())
74            .map_err(|_| MasterPasswordError::WrongPassword)?;
75        Ok(ctx.add_local_symmetric_key(user_key))
76    }
77
78    pub(crate) fn derive_ref(
79        password: &str,
80        kdf: &Kdf,
81        salt: &str,
82        user_key: &SymmetricCryptoKey,
83    ) -> Result<Self, MasterPasswordError> {
84        let master_key = MasterKey::derive(password, salt, kdf)
85            .map_err(|_| MasterPasswordError::InvalidKdfConfiguration)?;
86        let master_key_wrapped_user_key = master_key
87            .encrypt_user_key(user_key)
88            .map_err(MasterPasswordError::Crypto)?;
89
90        Ok(Self {
91            kdf: kdf.to_owned(),
92            salt: salt.to_owned(),
93            master_key_wrapped_user_key,
94        })
95    }
96
97    /// Derive master password unlock data from a password and user key in the key store.
98    #[tracing::instrument(skip(password, salt, ctx))]
99    pub fn derive<Ids: KeySlotIds>(
100        password: &str,
101        kdf: &Kdf,
102        salt: &str,
103        user_key_id: Ids::Symmetric,
104        ctx: &KeyStoreContext<Ids>,
105    ) -> Result<Self, MasterPasswordError> {
106        tracing::event!(Level::INFO, "deriving master password unlock data");
107        // Temporary workaround until lower level functions also work on the key context
108        #[expect(deprecated)]
109        let key = ctx.dangerous_get_symmetric_key(user_key_id)?;
110        Self::derive_ref(password, kdf, salt, key)
111    }
112}
113
114impl TryFrom<&MasterPasswordUnlockResponseModel> for MasterPasswordUnlockData {
115    type Error = MasterPasswordError;
116
117    fn try_from(response: &MasterPasswordUnlockResponseModel) -> Result<Self, Self::Error> {
118        let kdf = match response.kdf.kdf_type {
119            KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
120                iterations: kdf_parse_nonzero_u32(response.kdf.iterations)?,
121            },
122            KdfType::Argon2id => Kdf::Argon2id {
123                iterations: kdf_parse_nonzero_u32(response.kdf.iterations)?,
124                memory: kdf_parse_nonzero_u32(require!(response.kdf.memory))?,
125                parallelism: kdf_parse_nonzero_u32(require!(response.kdf.parallelism))?,
126            },
127            KdfType::__Unknown(_) => return Err(MasterPasswordError::KdfMalformed),
128        };
129
130        let master_key_wrapped_user_key = require!(&response.master_key_encrypted_user_key)
131            .parse()
132            .map_err(|_| MasterPasswordError::EncryptionKeyMalformed)?;
133        let salt = require!(&response.salt).clone();
134
135        Ok(MasterPasswordUnlockData {
136            kdf,
137            master_key_wrapped_user_key,
138            salt,
139        })
140    }
141}
142
143impl From<&MasterPasswordUnlockData> for MasterPasswordUnlockDataRequestModel {
144    fn from(data: &MasterPasswordUnlockData) -> Self {
145        Self {
146            kdf: Box::new(kdf_to_api_kdf_request_model(&data.kdf)),
147            master_key_wrapped_user_key: data.master_key_wrapped_user_key.to_string(),
148            salt: data.salt.to_owned(),
149        }
150    }
151}
152
153impl From<&MasterPasswordUnlockData>
154    for bitwarden_api_identity::models::MasterPasswordUnlockDataRequestModel
155{
156    fn from(data: &MasterPasswordUnlockData) -> Self {
157        Self {
158            kdf: Box::new(kdf_to_identity_kdf_request_model(&data.kdf)),
159            master_key_wrapped_user_key: data.master_key_wrapped_user_key.to_string(),
160            salt: data.salt.to_owned(),
161        }
162    }
163}
164
165fn kdf_parse_nonzero_u32(value: impl TryInto<u32>) -> Result<NonZeroU32, MasterPasswordError> {
166    value
167        .try_into()
168        .ok()
169        .and_then(NonZeroU32::new)
170        .ok_or(MasterPasswordError::KdfMalformed)
171}
172
173/// Represents the data required to authenticate with the master password.
174#[allow(missing_docs)]
175#[derive(Serialize, Deserialize, Clone, Debug)]
176#[serde(rename_all = "camelCase", deny_unknown_fields)]
177#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
178#[cfg_attr(
179    feature = "wasm",
180    derive(tsify::Tsify),
181    tsify(into_wasm_abi, from_wasm_abi)
182)]
183pub struct MasterPasswordAuthenticationData {
184    pub kdf: Kdf,
185    pub salt: String,
186    pub master_password_authentication_hash: B64,
187}
188
189impl MasterPasswordAuthenticationData {
190    /// Derive master password authentication data from a password, KDF, and salt.
191    #[tracing::instrument(skip(password, kdf, salt))]
192    pub fn derive(password: &str, kdf: &Kdf, salt: &str) -> Result<Self, MasterPasswordError> {
193        tracing::event!(Level::INFO, "deriving master password authentication data");
194        let master_key = MasterKey::derive(password, salt, kdf)
195            .map_err(|_| MasterPasswordError::InvalidKdfConfiguration)?;
196        let hash = master_key.derive_master_key_hash(
197            password.as_bytes(),
198            bitwarden_crypto::HashPurpose::ServerAuthorization,
199        );
200
201        Ok(Self {
202            kdf: kdf.to_owned(),
203            salt: salt.to_owned(),
204            master_password_authentication_hash: hash,
205        })
206    }
207}
208
209impl From<&MasterPasswordAuthenticationData> for MasterPasswordAuthenticationDataRequestModel {
210    fn from(data: &MasterPasswordAuthenticationData) -> Self {
211        Self {
212            kdf: Box::new(kdf_to_api_kdf_request_model(&data.kdf)),
213            master_password_authentication_hash: data
214                .master_password_authentication_hash
215                .to_string(),
216            salt: data.salt.to_owned(),
217        }
218    }
219}
220
221impl From<&MasterPasswordAuthenticationData>
222    for bitwarden_api_identity::models::MasterPasswordAuthenticationDataRequestModel
223{
224    fn from(data: &MasterPasswordAuthenticationData) -> Self {
225        Self {
226            kdf: Box::new(kdf_to_identity_kdf_request_model(&data.kdf)),
227            master_password_authentication_hash: data
228                .master_password_authentication_hash
229                .to_string(),
230            salt: data.salt.to_owned(),
231        }
232    }
233}
234
235fn kdf_to_api_kdf_request_model(kdf: &Kdf) -> KdfRequestModel {
236    match kdf {
237        Kdf::PBKDF2 { iterations } => KdfRequestModel {
238            kdf_type: KdfType::PBKDF2_SHA256,
239            iterations: iterations.get() as i32,
240            memory: None,
241            parallelism: None,
242        },
243        Kdf::Argon2id {
244            iterations,
245            memory,
246            parallelism,
247        } => KdfRequestModel {
248            kdf_type: KdfType::Argon2id,
249            iterations: iterations.get() as i32,
250            memory: Some(memory.get() as i32),
251            parallelism: Some(parallelism.get() as i32),
252        },
253    }
254}
255
256fn kdf_to_identity_kdf_request_model(kdf: &Kdf) -> bitwarden_api_identity::models::KdfRequestModel {
257    match kdf {
258        Kdf::PBKDF2 { iterations } => bitwarden_api_identity::models::KdfRequestModel {
259            kdf_type: bitwarden_api_identity::models::KdfType::PBKDF2_SHA256,
260            iterations: iterations.get() as i32,
261            memory: None,
262            parallelism: None,
263        },
264        Kdf::Argon2id {
265            iterations,
266            memory,
267            parallelism,
268        } => bitwarden_api_identity::models::KdfRequestModel {
269            kdf_type: bitwarden_api_identity::models::KdfType::Argon2id,
270            iterations: iterations.get() as i32,
271            memory: Some(memory.get() as i32),
272            parallelism: Some(parallelism.get() as i32),
273        },
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockKdfResponseModel};
280    use bitwarden_crypto::KeyStore;
281
282    use super::*;
283    use crate::key_management::{KeySlotIds, SymmetricKeySlotId};
284
285    const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
286    const TEST_INVALID_USER_KEY: &str = "-1.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=";
287    const TEST_SALT: &str = "[email protected]";
288    const TEST_PASSWORD: &str = "test_password";
289    const TEST_MASTER_PASSWORD_AUTHENTICATION_HASH: &str =
290        "Lyry95vlXEJ5FE0EXjeR9zgcsFSU0qGhP9l5X2jwE38=";
291
292    #[test]
293    fn test_master_password_unlock_data_derive() {
294        let kdf = Kdf::PBKDF2 {
295            iterations: NonZeroU32::new(600_000).unwrap(),
296        };
297        let salt = TEST_SALT.to_string();
298        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
299        let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, &salt, &user_key)
300            .expect("Failed to derive master password unlock data");
301        assert_eq!(data.salt, salt);
302        assert!(matches!(data.kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600_000));
303
304        let master_key = MasterKey::derive(TEST_PASSWORD, &salt, &data.kdf)
305            .expect("Failed to derive master key");
306        let decrypted_user_key = master_key
307            .decrypt_user_key(data.master_key_wrapped_user_key)
308            .expect("Failed to decrypt user key");
309        assert_eq!(decrypted_user_key, user_key);
310    }
311
312    #[test]
313    fn test_master_password_authentication_data_derive() {
314        let kdf = Kdf::PBKDF2 {
315            iterations: NonZeroU32::new(600_000).unwrap(),
316        };
317        let salt = TEST_SALT.to_string();
318        let data = MasterPasswordAuthenticationData::derive(TEST_PASSWORD, &kdf, &salt)
319            .expect("Failed to derive master password authentication data");
320        assert_eq!(data.salt, salt);
321        assert!(matches!(data.kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600_000));
322        assert_eq!(
323            data.master_password_authentication_hash.to_string(),
324            TEST_MASTER_PASSWORD_AUTHENTICATION_HASH
325        );
326    }
327
328    fn create_pbkdf2_response(
329        master_key_encrypted_user_key: Option<String>,
330        salt: Option<String>,
331        iterations: i32,
332    ) -> MasterPasswordUnlockResponseModel {
333        MasterPasswordUnlockResponseModel {
334            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
335                kdf_type: KdfType::PBKDF2_SHA256,
336                iterations,
337                memory: None,
338                parallelism: None,
339            }),
340            master_key_encrypted_user_key,
341            salt,
342        }
343    }
344
345    #[test]
346    fn test_try_from_master_password_unlock_response_model_pbkdf2_success() {
347        let response = create_pbkdf2_response(
348            Some(TEST_USER_KEY.to_string()),
349            Some(TEST_SALT.to_string()),
350            600_000,
351        );
352
353        let data = MasterPasswordUnlockData::try_from(&response).unwrap();
354
355        if let Kdf::PBKDF2 { iterations } = data.kdf {
356            assert_eq!(iterations.get(), 600_000);
357        } else {
358            panic!("Expected PBKDF2 KDF")
359        }
360
361        assert_eq!(data.salt, TEST_SALT);
362        assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY);
363    }
364
365    #[test]
366    fn test_try_from_master_password_unlock_response_model_argon2id_success() {
367        let response = MasterPasswordUnlockResponseModel {
368            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
369                kdf_type: KdfType::Argon2id,
370                iterations: 3,
371                memory: Some(64),
372                parallelism: Some(4),
373            }),
374            master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()),
375            salt: Some(TEST_SALT.to_string()),
376        };
377
378        let data = MasterPasswordUnlockData::try_from(&response).unwrap();
379
380        if let Kdf::Argon2id {
381            iterations,
382            memory,
383            parallelism,
384        } = data.kdf
385        {
386            assert_eq!(iterations.get(), 3);
387            assert_eq!(memory.get(), 64);
388            assert_eq!(parallelism.get(), 4);
389        } else {
390            panic!("Expected Argon2id KDF")
391        }
392
393        assert_eq!(data.salt, TEST_SALT);
394        assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY);
395    }
396
397    #[test]
398    fn test_try_from_master_password_unlock_response_model_invalid_user_key_encryption_kdf_malformed_error()
399     {
400        let response = create_pbkdf2_response(
401            Some(TEST_INVALID_USER_KEY.to_string()),
402            Some(TEST_SALT.to_string()),
403            600_000,
404        );
405
406        let result = MasterPasswordUnlockData::try_from(&response);
407        assert!(matches!(
408            result,
409            Err(MasterPasswordError::EncryptionKeyMalformed)
410        ));
411    }
412
413    #[test]
414    fn test_try_from_master_password_unlock_response_model_user_key_none_missing_field_error() {
415        let response = create_pbkdf2_response(None, Some(TEST_SALT.to_string()), 600_000);
416
417        let result = MasterPasswordUnlockData::try_from(&response);
418        assert!(matches!(
419            result,
420            Err(MasterPasswordError::MissingField(MissingFieldError(
421                "&response.master_key_encrypted_user_key"
422            )))
423        ));
424    }
425
426    #[test]
427    fn test_try_from_master_password_unlock_response_model_salt_none_missing_field_error() {
428        let response = create_pbkdf2_response(Some(TEST_USER_KEY.to_string()), None, 600_000);
429
430        let result = MasterPasswordUnlockData::try_from(&response);
431        assert!(matches!(
432            result,
433            Err(MasterPasswordError::MissingField(MissingFieldError(
434                "&response.salt"
435            )))
436        ));
437    }
438
439    #[test]
440    fn test_try_from_master_password_unlock_response_model_argon2id_kdf_memory_none_missing_field_error()
441     {
442        let response = MasterPasswordUnlockResponseModel {
443            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
444                kdf_type: KdfType::Argon2id,
445                iterations: 3,
446                memory: None,
447                parallelism: Some(4),
448            }),
449            master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()),
450            salt: Some(TEST_SALT.to_string()),
451        };
452
453        let result = MasterPasswordUnlockData::try_from(&response);
454        assert!(matches!(
455            result,
456            Err(MasterPasswordError::MissingField(MissingFieldError(
457                "response.kdf.memory"
458            )))
459        ));
460    }
461
462    #[test]
463    fn test_try_from_master_password_unlock_response_model_argon2id_kdf_memory_zero_kdf_malformed_error()
464     {
465        let response = MasterPasswordUnlockResponseModel {
466            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
467                kdf_type: KdfType::Argon2id,
468                iterations: 3,
469                memory: Some(0),
470                parallelism: Some(4),
471            }),
472            master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()),
473            salt: Some(TEST_SALT.to_string()),
474        };
475
476        let result = MasterPasswordUnlockData::try_from(&response);
477        assert!(matches!(result, Err(MasterPasswordError::KdfMalformed)));
478    }
479
480    #[test]
481    fn test_try_from_master_password_unlock_response_model_argon2id_kdf_parallelism_none_missing_field_error()
482     {
483        let response = MasterPasswordUnlockResponseModel {
484            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
485                kdf_type: KdfType::Argon2id,
486                iterations: 3,
487                memory: Some(64),
488                parallelism: None,
489            }),
490            master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()),
491            salt: Some(TEST_SALT.to_string()),
492        };
493
494        let result = MasterPasswordUnlockData::try_from(&response);
495        assert!(matches!(
496            result,
497            Err(MasterPasswordError::MissingField(MissingFieldError(
498                "response.kdf.parallelism"
499            )))
500        ));
501    }
502
503    #[test]
504    fn test_try_from_master_password_unlock_response_model_argon2id_kdf_parallelism_zero_kdf_malformed_error()
505     {
506        let response = MasterPasswordUnlockResponseModel {
507            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
508                kdf_type: KdfType::Argon2id,
509                iterations: 3,
510                memory: Some(64),
511                parallelism: Some(0),
512            }),
513            master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()),
514            salt: Some(TEST_SALT.to_string()),
515        };
516
517        let result = MasterPasswordUnlockData::try_from(&response);
518        assert!(matches!(result, Err(MasterPasswordError::KdfMalformed)));
519    }
520
521    #[test]
522    fn test_try_from_master_password_unlock_response_model_pbkdf2_kdf_iterations_zero_kdf_malformed_error()
523     {
524        let response = create_pbkdf2_response(
525            Some(TEST_USER_KEY.to_string()),
526            Some(TEST_SALT.to_string()),
527            0,
528        );
529
530        let result = MasterPasswordUnlockData::try_from(&response);
531        assert!(matches!(result, Err(MasterPasswordError::KdfMalformed)));
532    }
533
534    #[test]
535    fn test_try_from_master_password_unlock_response_model_argon2id_kdf_iterations_zero_kdf_malformed_error()
536     {
537        let response = MasterPasswordUnlockResponseModel {
538            kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
539                kdf_type: KdfType::Argon2id,
540                iterations: 0,
541                memory: Some(64),
542                parallelism: Some(4),
543            }),
544            master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()),
545            salt: Some(TEST_SALT.to_string()),
546        };
547
548        let result = MasterPasswordUnlockData::try_from(&response);
549        assert!(matches!(result, Err(MasterPasswordError::KdfMalformed)));
550    }
551
552    #[test]
553    fn test_unwrap_to_context_success() {
554        // Derive master password unlock data from a known password and user key
555        let kdf = Kdf::PBKDF2 {
556            iterations: NonZeroU32::new(600_000).expect("non-zero"),
557        };
558        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
559        let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, TEST_SALT, &user_key)
560            .expect("Failed to derive master password unlock data");
561
562        // Create a key store and unwrap the user key into the context
563        let store: KeyStore<KeySlotIds> = KeyStore::default();
564        let mut ctx = store.context_mut();
565        let key_id = data
566            .unwrap_to_context::<KeySlotIds>(TEST_PASSWORD, &mut ctx)
567            .expect("Failed to unwrap to context");
568
569        // Verify that the key was added to the context
570        assert!(ctx.has_symmetric_key(key_id));
571
572        // Verify the unwrapped key matches the original
573        #[expect(deprecated)]
574        let unwrapped_key = ctx
575            .dangerous_get_symmetric_key(key_id)
576            .expect("Failed to get symmetric key");
577        assert_eq!(*unwrapped_key, user_key);
578    }
579
580    #[test]
581    fn test_unwrap_to_context_wrong_password() {
582        // Derive master password unlock data from a known password and user key
583        let kdf = Kdf::PBKDF2 {
584            iterations: NonZeroU32::new(600_000).expect("non-zero"),
585        };
586        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
587        let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, TEST_SALT, &user_key)
588            .expect("Failed to derive master password unlock data");
589
590        // Attempt to unwrap with wrong password
591        let store: KeyStore<KeySlotIds> = KeyStore::default();
592        let mut ctx = store.context_mut();
593        let result = data.unwrap_to_context::<KeySlotIds>("wrong_password", &mut ctx);
594
595        assert!(matches!(result, Err(MasterPasswordError::WrongPassword)));
596    }
597
598    #[test]
599    fn test_unwrap_to_context_persists_key() {
600        // Derive master password unlock data from a known password and user key
601        let kdf = Kdf::PBKDF2 {
602            iterations: NonZeroU32::new(600_000).expect("non-zero"),
603        };
604        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
605        let data = MasterPasswordUnlockData::derive_ref(TEST_PASSWORD, &kdf, TEST_SALT, &user_key)
606            .expect("Failed to derive master password unlock data");
607
608        // Create a key store and unwrap the user key into the context
609        let store: KeyStore<KeySlotIds> = KeyStore::default();
610        {
611            let mut ctx = store.context_mut();
612            let local_key_id = data
613                .unwrap_to_context::<KeySlotIds>(TEST_PASSWORD, &mut ctx)
614                .expect("Failed to unwrap to context");
615
616            // Persist the local key to the User key slot
617            ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
618                .expect("Failed to persist symmetric key");
619        }
620
621        // Verify the key is accessible with the User key id in a new context
622        let ctx = store.context();
623        assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
624    }
625}