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