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 = 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#[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 #[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 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 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 assert!(ctx.has_symmetric_key(key_id));
606
607 #[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 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 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 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 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 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
653 .expect("Failed to persist symmetric key");
654 }
655
656 let ctx = store.context();
658 assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
659 }
660}