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#[allow(dead_code)]
22#[bitwarden_error(flat)]
23#[derive(Debug, thiserror::Error)]
24pub enum MasterPasswordError {
25 #[error("Wrapped encryption key is malformed")]
27 EncryptionKeyMalformed,
28 #[error("KDF is malformed")]
30 KdfMalformed,
31 #[error("Invalid KDF configuration")]
33 InvalidKdfConfiguration,
34 #[error("Key id is malformed")]
36 KeyIdMalformed,
37 #[error(transparent)]
39 MissingField(#[from] MissingFieldError),
40 #[error(transparent)]
42 Crypto(#[from] bitwarden_crypto::CryptoError),
43 #[error("Wrong password")]
45 WrongPassword,
46}
47
48#[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 pub kdf: Kdf,
60 pub master_key_wrapped_user_key: EncString,
62 pub salt: String,
64 #[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 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 #[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 #[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#[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 #[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 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 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 assert!(ctx.has_symmetric_key(key_id));
607
608 #[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 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 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 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 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 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
654 .expect("Failed to persist symmetric key");
655 }
656
657 let ctx = store.context();
659 assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
660 }
661}