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