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