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