1use std::str::FromStr;
5
6use bitwarden_api_api::models::{
7 self, CommonUnlockDataRequestModel, EmergencyAccessKeyDataResponseModel,
8 EmergencyAccessWithIdRequestModel, MasterPasswordUnlockAndAuthenticationDataModel,
9 OrganizationPasswordResetKeyDataResponseModel, OrganizationUserAccountRecoveryRequestModel,
10 OtherDeviceKeysUpdateRequestModel, UnlockDataRequestModel, V2UpgradeTokenRequestModel,
11 WebAuthnLoginRotateKeyRequestModel,
12};
13use bitwarden_core::{
14 key_management::{
15 KeySlotIds, MasterPasswordAuthenticationData, MasterPasswordUnlockData, SymmetricKeySlotId,
16 V2UpgradeToken,
17 },
18 require,
19};
20use bitwarden_crypto::{Kdf, KeyStoreContext, PublicKey, SpkiPublicKeyBytes, UnsignedSharedKey};
21use bitwarden_encoding::B64;
22use serde::{Deserialize, Serialize};
23use tracing::{debug, debug_span, error, info};
24
25use crate::key_rotation::{
26 KeyRotationDataParseError, partial_rotateable_keyset::PartialRotateableKeyset,
27};
28
29#[derive(Serialize, Deserialize, Clone)]
32#[bitwarden_ffi::wasm_record]
33pub struct V1EmergencyAccessMembership {
34 pub id: uuid::Uuid,
35 pub grantee_id: uuid::Uuid,
36 pub name: String,
37 pub public_key: PublicKey,
38}
39
40impl TryFrom<EmergencyAccessKeyDataResponseModel> for V1EmergencyAccessMembership {
41 type Error = KeyRotationDataParseError;
42
43 fn try_from(ea: EmergencyAccessKeyDataResponseModel) -> Result<Self, Self::Error> {
44 Ok(Self {
45 id: require!(ea.id),
46 grantee_id: require!(ea.grantee_id),
47 name: ea
50 .grantee_name
51 .or(ea.grantee_email)
52 .unwrap_or_else(|| "Unknown".to_string()),
53 public_key: parse_public_key(&require!(ea.public_key))?,
54 })
55 }
56}
57
58#[derive(Serialize, Deserialize, Clone)]
61#[bitwarden_ffi::wasm_record]
62pub struct V1OrganizationMembership {
63 pub organization_id: uuid::Uuid,
64 pub name: String,
65 pub public_key: PublicKey,
66}
67
68impl TryFrom<OrganizationPasswordResetKeyDataResponseModel> for V1OrganizationMembership {
69 type Error = KeyRotationDataParseError;
70
71 fn try_from(o: OrganizationPasswordResetKeyDataResponseModel) -> Result<Self, Self::Error> {
72 Ok(Self {
73 organization_id: require!(o.organization_id),
74 name: require!(o.organization_name),
75 public_key: parse_public_key(&require!(o.organization_public_key))?,
76 })
77 }
78}
79
80fn parse_public_key(public_key_b64: &str) -> Result<PublicKey, KeyRotationDataParseError> {
81 Ok(PublicKey::from_der(&SpkiPublicKeyBytes::from(
82 B64::from_str(public_key_b64)?.into_bytes(),
83 ))?)
84}
85
86#[derive(Debug)]
87pub(super) enum ReencryptError {
88 MasterPasswordDerivation,
90 KeysetUnlockDataReencryption,
92 KeySharingError,
94 KeyConnectorWrapping,
96 UpgradeTokenCreation,
98}
99
100pub(super) struct ReencryptMasterPasswordChangeAndUnlockInput {
101 pub(super) password: String,
103 pub(super) hint: Option<String>,
104 pub(super) kdf: Kdf,
105 pub(super) salt: String,
106 pub(super) common_unlock_data: ReencryptCommonUnlockDataInput,
108}
109
110pub(super) struct ReencryptCommonUnlockDataInput {
111 pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
113 pub(super) webauthn_credentials: Vec<PartialRotateableKeyset>,
115 pub(super) trusted_organization_keys: Vec<V1OrganizationMembership>,
117 pub(super) trusted_emergency_access_keys: Vec<V1EmergencyAccessMembership>,
119}
120
121pub(super) fn reencrypt_master_password_change_unlock_data(
122 input: ReencryptMasterPasswordChangeAndUnlockInput,
123 current_user_key_id: SymmetricKeySlotId,
124 new_user_key_id: SymmetricKeySlotId,
125 ctx: &mut KeyStoreContext<KeySlotIds>,
126) -> Result<UnlockDataRequestModel, ReencryptError> {
127 let master_password_unlock_data = reencrypt_userkey_for_masterpassword_unlock(
128 input.password,
129 input.hint,
130 input.kdf,
131 input.salt,
132 new_user_key_id,
133 ctx,
134 )?;
135
136 let common_unlock_data = reencrypt_common_unlock_data(
137 input.common_unlock_data,
138 current_user_key_id,
139 new_user_key_id,
140 false,
141 ctx,
142 )?;
143
144 Ok(UnlockDataRequestModel {
145 master_password_unlock_data: Box::new(master_password_unlock_data),
146 emergency_access_unlock_data: common_unlock_data.emergency_access_unlock_data,
147 organization_account_recovery_unlock_data: common_unlock_data
148 .organization_account_recovery_unlock_data,
149 passkey_unlock_data: common_unlock_data.passkey_unlock_data,
150 device_key_unlock_data: common_unlock_data.device_key_unlock_data,
151 v2_upgrade_token: None,
154 })
155}
156
157pub(super) fn reencrypt_common_unlock_data(
159 input: ReencryptCommonUnlockDataInput,
160 current_user_key_id: SymmetricKeySlotId,
161 new_user_key_id: SymmetricKeySlotId,
162 creates_v2_upgrade_token: bool,
163 ctx: &mut KeyStoreContext<KeySlotIds>,
164) -> Result<CommonUnlockDataRequestModel, ReencryptError> {
165 let tde_device_unlock_data = reencrypt_tde_devices(
166 &input.trusted_devices,
167 current_user_key_id,
168 new_user_key_id,
169 ctx,
170 )?;
171 let prf_passkey_unlock_data = reencrypt_passkey_credentials(
172 &input.webauthn_credentials,
173 current_user_key_id,
174 new_user_key_id,
175 ctx,
176 )?;
177 let emergency_accesses =
178 reencrypt_emergency_access_keys(input.trusted_emergency_access_keys, new_user_key_id, ctx)?;
179 let organizations_memberships = if creates_v2_upgrade_token {
180 defer_organization_account_recovery_to_admins(input.trusted_organization_keys)
181 } else {
182 reencrypt_organization_memberships(input.trusted_organization_keys, new_user_key_id, ctx)?
183 };
184
185 let upgrade_token = make_upgrade_token_if_needed(
186 current_user_key_id,
187 new_user_key_id,
188 creates_v2_upgrade_token,
189 ctx,
190 )?;
191
192 Ok(CommonUnlockDataRequestModel {
193 emergency_access_unlock_data: Some(emergency_accesses),
194 organization_account_recovery_unlock_data: Some(organizations_memberships),
195 passkey_unlock_data: Some(prf_passkey_unlock_data),
196 device_key_unlock_data: Some(tde_device_unlock_data),
197 v2_upgrade_token: upgrade_token,
198 })
199}
200
201fn reencrypt_tde_devices(
203 trusted_devices: &[PartialRotateableKeyset],
204 current_user_key_id: SymmetricKeySlotId,
205 new_user_key_id: SymmetricKeySlotId,
206 ctx: &mut KeyStoreContext<KeySlotIds>,
207) -> Result<Vec<OtherDeviceKeysUpdateRequestModel>, ReencryptError> {
208 trusted_devices
209 .iter()
210 .map(|device| {
211 let _span = debug_span!("reencrypt_device_key", device_id = ?device.id).entered();
212 device
213 .rotate_userkey(current_user_key_id, new_user_key_id, ctx)
214 .map_err(|_| ReencryptError::KeysetUnlockDataReencryption)
215 .map(Into::into)
216 })
217 .collect()
218}
219
220fn reencrypt_passkey_credentials(
222 webauthn_credentials: &[PartialRotateableKeyset],
223 current_user_key_id: SymmetricKeySlotId,
224 new_user_key_id: SymmetricKeySlotId,
225 ctx: &mut KeyStoreContext<KeySlotIds>,
226) -> Result<Vec<WebAuthnLoginRotateKeyRequestModel>, ReencryptError> {
227 webauthn_credentials
228 .iter()
229 .map(|cred| {
230 let _span =
231 debug_span!("reencrypt_webauthn_credential", credential_id = ?cred.id).entered();
232 cred.rotate_userkey(current_user_key_id, new_user_key_id, ctx)
233 .map_err(|_| ReencryptError::KeysetUnlockDataReencryption)
234 .map(Into::into)
235 })
236 .collect()
237}
238
239fn reencrypt_emergency_access_keys(
241 trusted_emergency_access_keys: Vec<V1EmergencyAccessMembership>,
242 new_user_key_id: SymmetricKeySlotId,
243 ctx: &mut KeyStoreContext<KeySlotIds>,
244) -> Result<Vec<EmergencyAccessWithIdRequestModel>, ReencryptError> {
245 trusted_emergency_access_keys
246 .into_iter()
247 .map(|ea| {
248 let _span =
249 debug_span!("reencrypt_emergency_access_key", grantee_id = ?ea.id).entered();
250 match UnsignedSharedKey::encapsulate(new_user_key_id, &ea.public_key, ctx) {
253 Ok(reencrypted_key) => Ok(EmergencyAccessWithIdRequestModel {
254 r#type: models::EmergencyAccessType::Takeover,
256 wait_time_days: 1,
258 id: ea.id,
259 key_encrypted: reencrypted_key.to_string().into(),
260 }),
261 Err(_) => Err(ReencryptError::KeySharingError),
262 }
263 })
264 .collect()
265}
266
267fn reencrypt_organization_memberships(
269 trusted_organization_keys: Vec<V1OrganizationMembership>,
270 new_user_key_id: SymmetricKeySlotId,
271 ctx: &mut KeyStoreContext<KeySlotIds>,
272) -> Result<Vec<OrganizationUserAccountRecoveryRequestModel>, ReencryptError> {
273 trusted_organization_keys
274 .into_iter()
275 .map(|org_membership| {
276 let _span =
277 debug_span!("reencrypt_organization_key", organization = ?org_membership.organization_id)
278 .entered();
279 match UnsignedSharedKey::encapsulate(new_user_key_id, &org_membership.public_key, ctx) {
282 Ok(reencrypted_key) => Ok(OrganizationUserAccountRecoveryRequestModel {
283 reset_password_key: Some(reencrypted_key.to_string()),
284 organization_id: org_membership.organization_id,
285 }),
286 Err(_) => Err(ReencryptError::KeySharingError),
287 }
288 })
289 .collect()
290}
291
292fn defer_organization_account_recovery_to_admins(
297 organization_memberships: Vec<V1OrganizationMembership>,
298) -> Vec<OrganizationUserAccountRecoveryRequestModel> {
299 organization_memberships
300 .into_iter()
301 .map(|org_membership| {
302 debug!(
303 organization = ?org_membership.organization_id,
304 "Leaving account recovery for organization admins to update",
305 );
306 OrganizationUserAccountRecoveryRequestModel {
307 reset_password_key: None,
308 organization_id: org_membership.organization_id,
309 }
310 })
311 .collect()
312}
313
314fn reencrypt_userkey_for_masterpassword_unlock(
315 password: String,
316 hint: Option<String>,
317 kdf: Kdf,
318 salt: String,
319 new_user_key_id: SymmetricKeySlotId,
320 ctx: &mut KeyStoreContext<KeySlotIds>,
321) -> Result<MasterPasswordUnlockAndAuthenticationDataModel, ReencryptError> {
322 let _span = debug_span!("derive_master_password_unlock_data").entered();
323 let unlock_data =
324 MasterPasswordUnlockData::derive(&password, &kdf, &salt, new_user_key_id, ctx)
325 .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
326 let authentication_data = MasterPasswordAuthenticationData::derive(&password, &kdf, &salt)
327 .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
328 to_authentication_and_unlock_data(unlock_data, authentication_data, hint)
329 .map_err(|_| ReencryptError::MasterPasswordDerivation)
330}
331
332#[derive(Debug)]
333struct ParsingError;
334
335fn to_authentication_and_unlock_data(
336 master_password_unlock_data: MasterPasswordUnlockData,
337 master_password_authentication_data: MasterPasswordAuthenticationData,
338 hint: Option<String>,
339) -> Result<MasterPasswordUnlockAndAuthenticationDataModel, ParsingError> {
340 let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) =
341 match master_password_unlock_data.kdf {
342 bitwarden_crypto::Kdf::PBKDF2 { iterations } => {
343 (models::KdfType::PBKDF2_SHA256, iterations, None, None)
344 }
345 bitwarden_crypto::Kdf::Argon2id {
346 iterations,
347 memory,
348 parallelism,
349 } => (
350 models::KdfType::Argon2id,
351 iterations,
352 Some(memory),
353 Some(parallelism),
354 ),
355 };
356 Ok(MasterPasswordUnlockAndAuthenticationDataModel {
357 kdf_type,
358 kdf_iterations: kdf_iterations.get().try_into().map_err(|_| ParsingError)?,
359 kdf_memory: kdf_memory
360 .map(|m| m.get().try_into().map_err(|_| ParsingError))
361 .transpose()?,
362 kdf_parallelism: kdf_parallelism
363 .map(|p| p.get().try_into().map_err(|_| ParsingError))
364 .transpose()?,
365 email: Some(master_password_unlock_data.salt.clone()),
366 master_key_authentication_hash: Some(
367 master_password_authentication_data
368 .master_password_authentication_hash
369 .to_string(),
370 ),
371 master_key_encrypted_user_key: Some(
372 master_password_unlock_data
373 .master_key_wrapped_user_key
374 .to_string(),
375 ),
376 master_password_hint: hint,
377 master_password_salt: Some(master_password_unlock_data.salt.clone()),
378 contained_key_id: master_password_unlock_data
379 .contained_key_id
380 .as_ref()
381 .map(|id| id.to_string()),
382 })
383}
384
385fn make_upgrade_token_if_needed(
386 current_user_key_id: SymmetricKeySlotId,
387 new_user_key_id: SymmetricKeySlotId,
388 creates_v2_upgrade_token: bool,
389 ctx: &mut KeyStoreContext<KeySlotIds>,
390) -> Result<Option<Box<V2UpgradeTokenRequestModel>>, ReencryptError> {
391 if !creates_v2_upgrade_token {
392 return Ok(None);
393 }
394
395 let token = V2UpgradeToken::create(current_user_key_id, new_user_key_id, ctx).map_err(|e| {
396 error!("Failed to create V2 upgrade token: {e}");
397 ReencryptError::UpgradeTokenCreation
398 })?;
399 info!("Upgrade token created for the key rotation");
400 Ok(Some(Box::new(token.into())))
401}
402
403#[cfg(test)]
404mod tests {
405 use std::num::NonZeroU32;
406
407 use bitwarden_api_api::models::KdfType;
408 use bitwarden_core::key_management::KeySlotIds;
409 use bitwarden_crypto::{
410 Kdf, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm, UnsignedSharedKey,
411 };
412 use uuid::Uuid;
413
414 use super::*;
415 use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
416
417 fn create_test_kdf_pbkdf2() -> Kdf {
418 Kdf::PBKDF2 {
419 iterations: NonZeroU32::new(600000).expect("valid iterations"),
420 }
421 }
422
423 fn create_test_kdf_argon2id() -> Kdf {
424 Kdf::Argon2id {
425 iterations: NonZeroU32::new(3).expect("valid iterations"),
426 memory: NonZeroU32::new(64).expect("valid memory"),
427 parallelism: NonZeroU32::new(4).expect("valid parallelism"),
428 }
429 }
430
431 fn assert_symmetric_keys_equal(
432 key_id_1: SymmetricKeySlotId,
433 key_id_2: SymmetricKeySlotId,
434 ctx: &mut KeyStoreContext<KeySlotIds>,
435 ) {
436 #[allow(deprecated)]
437 let key_1 = ctx
438 .dangerous_get_symmetric_key(key_id_1)
439 .expect("key 1 should exist");
440 #[allow(deprecated)]
441 let key_2 = ctx
442 .dangerous_get_symmetric_key(key_id_2)
443 .expect("key 2 should exist");
444 assert_eq!(key_1, key_2, "symmetric keys should be equal");
445 }
446
447 fn empty_common_unlock_input() -> ReencryptCommonUnlockDataInput {
448 ReencryptCommonUnlockDataInput {
449 trusted_devices: vec![],
450 webauthn_credentials: vec![],
451 trusted_organization_keys: vec![],
452 trusted_emergency_access_keys: vec![],
453 }
454 }
455
456 fn request_model_to_token(request: V2UpgradeTokenRequestModel) -> V2UpgradeToken {
457 V2UpgradeToken {
458 wrapped_user_key_1: request
459 .wrapped_user_key1
460 .parse()
461 .expect("wrapped_user_key1 should parse"),
462 wrapped_user_key_2: request
463 .wrapped_user_key2
464 .parse()
465 .expect("wrapped_user_key2 should parse"),
466 }
467 }
468
469 #[test]
470 fn test_to_authentication_and_unlock_data_pbkdf2() {
471 let store: KeyStore<KeySlotIds> = KeyStore::default();
472 let mut ctx = store.context_mut();
473
474 let kdf = create_test_kdf_pbkdf2();
475 let salt = "[email protected]";
476 let password = "test_password";
477
478 let user_key_id = ctx.generate_symmetric_key();
479 let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
480 .expect("derive should succeed");
481 let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
482 .expect("derive should succeed");
483
484 let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
485 assert!(result.is_ok());
486
487 let model = result.expect("should be ok");
488 assert_eq!(model.kdf_type, KdfType::PBKDF2_SHA256);
489 assert_eq!(model.kdf_iterations, 600000);
490 assert!(model.kdf_memory.is_none());
491 assert!(model.kdf_parallelism.is_none());
492 assert_eq!(model.email, Some(salt.to_string()));
493 assert!(model.master_key_authentication_hash.is_some());
494 assert!(model.master_key_encrypted_user_key.is_some());
495 assert!(model.master_password_hint.is_none());
496
497 let master_password_unlock_data = MasterPasswordUnlockData {
499 master_key_wrapped_user_key: model
500 .master_key_encrypted_user_key
501 .expect("should be present")
502 .parse()
503 .expect("should parse"),
504 kdf: kdf.clone(),
505 salt: salt.to_string(),
506 contained_key_id: None,
507 };
508 let decrypted_user_key = master_password_unlock_data
509 .unwrap_to_context(password, &mut ctx)
510 .expect("unwrap should succeed");
511 assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
512 }
513
514 #[test]
515 fn test_to_authentication_and_unlock_data_argon2id() {
516 let store: KeyStore<KeySlotIds> = KeyStore::default();
517 let mut ctx = store.context_mut();
518
519 let kdf = create_test_kdf_argon2id();
520 let salt = "[email protected]";
521 let password = "test_password";
522
523 let user_key_id = ctx.generate_symmetric_key();
524 let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
525 .expect("derive should succeed");
526 let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
527 .expect("derive should succeed");
528
529 let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
530 assert!(result.is_ok());
531
532 let model = result.expect("should be ok");
533 assert_eq!(model.kdf_type, KdfType::Argon2id);
534 assert_eq!(model.kdf_iterations, 3);
535 assert_eq!(model.kdf_memory, Some(64));
536 assert_eq!(model.kdf_parallelism, Some(4));
537 assert_eq!(model.email, Some(salt.to_string()));
538 assert!(model.master_key_authentication_hash.is_some());
539 assert!(model.master_key_encrypted_user_key.is_some());
540
541 let master_password_unlock_data = MasterPasswordUnlockData {
543 master_key_wrapped_user_key: model
544 .master_key_encrypted_user_key
545 .expect("should be present")
546 .parse()
547 .expect("should parse"),
548 kdf: kdf.clone(),
549 salt: salt.to_string(),
550 contained_key_id: None,
551 };
552 let decrypted_user_key = master_password_unlock_data
553 .unwrap_to_context(password, &mut ctx)
554 .expect("unwrap should succeed");
555 assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
556 }
557
558 #[test]
559 fn test_reencrypt_unlock_device_key_data() {
560 let store: KeyStore<KeySlotIds> = KeyStore::default();
561 let mut ctx = store.context_mut();
562
563 let current_user_key_id = ctx.generate_symmetric_key();
564 let new_user_key_id = ctx.generate_symmetric_key();
565
566 let (device_keyset, device_private_key) =
567 PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
568
569 let result = reencrypt_common_unlock_data(
570 ReencryptCommonUnlockDataInput {
571 trusted_devices: vec![device_keyset],
572 webauthn_credentials: vec![],
573 trusted_organization_keys: vec![],
574 trusted_emergency_access_keys: vec![],
575 },
576 current_user_key_id,
577 new_user_key_id,
578 false,
579 &mut ctx,
580 );
581
582 let unlock_data = result.expect("should be ok");
583
584 let device_unlock = unlock_data
585 .device_key_unlock_data
586 .as_ref()
587 .expect("should be present")
588 .first()
589 .expect("should have at least one");
590 let decrypted_user_key = device_unlock
591 .encrypted_user_key
592 .parse::<UnsignedSharedKey>()
593 .expect("should parse")
594 .decapsulate(device_private_key, &mut ctx)
595 .expect("unwrap should succeed");
596 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
597 }
598
599 #[test]
600 fn test_reencrypt_unlock_webauthn_prf_credential_data() {
601 let store: KeyStore<KeySlotIds> = KeyStore::default();
602 let mut ctx = store.context_mut();
603
604 let current_user_key_id = ctx.generate_symmetric_key();
605 let new_user_key_id = ctx.generate_symmetric_key();
606
607 let (credential_keyset, credential_private_key) =
608 PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
609
610 let result = reencrypt_common_unlock_data(
611 ReencryptCommonUnlockDataInput {
612 trusted_devices: vec![],
613 webauthn_credentials: vec![credential_keyset],
614 trusted_organization_keys: vec![],
615 trusted_emergency_access_keys: vec![],
616 },
617 current_user_key_id,
618 new_user_key_id,
619 false,
620 &mut ctx,
621 );
622
623 let unlock_data = result.expect("should be ok");
624
625 let credential_unlock = unlock_data
627 .passkey_unlock_data
628 .as_ref()
629 .expect("should be present")
630 .first()
631 .expect("should have at least one");
632 let decrypted_user_key = credential_unlock
633 .encrypted_user_key
634 .parse::<UnsignedSharedKey>()
635 .expect("should parse")
636 .decapsulate(credential_private_key, &mut ctx)
637 .expect("unwrap should succeed");
638 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
639 }
640
641 #[test]
642 fn test_reencrypt_unlock_emergency_access_data() {
643 let store: KeyStore<KeySlotIds> = KeyStore::default();
644 let mut ctx = store.context_mut();
645
646 let current_user_key_id = ctx.generate_symmetric_key();
647 let new_user_key_id = ctx.generate_symmetric_key();
648
649 let organization_private_key =
650 ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
651 let emergency_access = V1EmergencyAccessMembership {
652 id: Uuid::new_v4(),
653 grantee_id: Uuid::new_v4(),
654 name: "Test User".to_string(),
655 public_key: ctx
656 .get_public_key(organization_private_key)
657 .expect("key exists"),
658 };
659
660 let result = reencrypt_common_unlock_data(
661 ReencryptCommonUnlockDataInput {
662 trusted_devices: vec![],
663 webauthn_credentials: vec![],
664 trusted_organization_keys: vec![],
665 trusted_emergency_access_keys: vec![emergency_access],
666 },
667 current_user_key_id,
668 new_user_key_id,
669 false,
670 &mut ctx,
671 );
672
673 let unlock_data = result.expect("should be ok");
674
675 let emergency_access_unlock = unlock_data
677 .emergency_access_unlock_data
678 .as_ref()
679 .expect("should be present")
680 .first()
681 .expect("should have at least one");
682 let decrypted_user_key = emergency_access_unlock
683 .key_encrypted
684 .as_ref()
685 .map(|k| k.parse::<UnsignedSharedKey>())
686 .expect("should be present")
687 .expect("should parse")
688 .decapsulate(organization_private_key, &mut ctx)
689 .expect("unwrap should succeed");
690 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
691 }
692
693 #[test]
694 fn test_reencrypt_unlock_organization_membership_data() {
695 let store: KeyStore<KeySlotIds> = KeyStore::default();
696 let mut ctx = store.context_mut();
697
698 let current_user_key_id = ctx.generate_symmetric_key();
699 let new_user_key_id = ctx.generate_symmetric_key();
700
701 let org_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
702 let org_membership = V1OrganizationMembership {
703 organization_id: Uuid::new_v4(),
704 name: "Test Org".to_string(),
705 public_key: ctx.get_public_key(org_key).expect("key exists"),
706 };
707
708 let result = reencrypt_common_unlock_data(
709 ReencryptCommonUnlockDataInput {
710 trusted_devices: vec![],
711 webauthn_credentials: vec![],
712 trusted_organization_keys: vec![org_membership],
713 trusted_emergency_access_keys: vec![],
714 },
715 current_user_key_id,
716 new_user_key_id,
717 false,
718 &mut ctx,
719 );
720
721 let unlock_data = result.expect("should be ok");
722
723 let org_membership_unlock = unlock_data
724 .organization_account_recovery_unlock_data
725 .as_ref()
726 .expect("should be present")
727 .first()
728 .expect("should have at least one");
729 let decrypted_user_key = org_membership_unlock
730 .reset_password_key
731 .as_ref()
732 .map(|k| k.parse::<UnsignedSharedKey>())
733 .expect("should be present")
734 .expect("should parse")
735 .decapsulate(org_key, &mut ctx)
736 .expect("unwrap should succeed");
737 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
738 }
739
740 #[test]
741 fn test_reencrypt_unlock_organization_membership_data_with_upgrade_token_sends_no_key() {
742 let store: KeyStore<KeySlotIds> = KeyStore::default();
743 let mut ctx = store.context_mut();
744
745 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
746 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
747
748 let org_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
749 let organization_id = Uuid::new_v4();
750 let org_membership = V1OrganizationMembership {
751 organization_id,
752 name: "Test Org".to_string(),
753 public_key: ctx.get_public_key(org_key).expect("key exists"),
754 };
755
756 let result = reencrypt_common_unlock_data(
757 ReencryptCommonUnlockDataInput {
758 trusted_devices: vec![],
759 webauthn_credentials: vec![],
760 trusted_organization_keys: vec![org_membership],
761 trusted_emergency_access_keys: vec![],
762 },
763 current_user_key_id,
764 new_user_key_id,
765 true,
766 &mut ctx,
767 );
768
769 let unlock_data = result.expect("should be ok");
770
771 let org_membership_unlock = unlock_data
772 .organization_account_recovery_unlock_data
773 .as_ref()
774 .expect("should be present");
775 assert_eq!(org_membership_unlock.len(), 1);
776 assert_eq!(org_membership_unlock[0].organization_id, organization_id);
777 assert!(
778 org_membership_unlock[0].reset_password_key.is_none(),
779 "account recovery is left for organization admins to update"
780 );
781 }
782
783 #[test]
784 fn test_reencrypt_common_unlock_data_v1_to_v2_creates_upgrade_token() {
785 let store: KeyStore<KeySlotIds> = KeyStore::default();
786 let mut ctx = store.context_mut();
787
788 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
789 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
790
791 let result = reencrypt_common_unlock_data(
792 empty_common_unlock_input(),
793 current_user_key_id,
794 new_user_key_id,
795 true,
796 &mut ctx,
797 );
798
799 let unlock_data = result.expect("should be ok");
800 let token_request = *unlock_data
801 .v2_upgrade_token
802 .expect("v2_upgrade_token should be populated for V1 -> V2 rotation");
803 let token = request_model_to_token(token_request);
804
805 let unwrapped_v2_id = token
806 .unwrap_v2(current_user_key_id, &mut ctx)
807 .expect("unwrap_v2 should succeed");
808 assert_symmetric_keys_equal(new_user_key_id, unwrapped_v2_id, &mut ctx);
809
810 let unwrapped_v1_id = token
811 .unwrap_v1(new_user_key_id, &mut ctx)
812 .expect("unwrap_v1 should succeed");
813 assert_symmetric_keys_equal(current_user_key_id, unwrapped_v1_id, &mut ctx);
814 }
815
816 #[test]
817 fn test_reencrypt_common_unlock_data_without_upgrade_token_returns_none() {
818 let store: KeyStore<KeySlotIds> = KeyStore::default();
819 let mut ctx = store.context_mut();
820
821 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
822 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
823
824 let result = reencrypt_common_unlock_data(
825 empty_common_unlock_input(),
826 current_user_key_id,
827 new_user_key_id,
828 false,
829 &mut ctx,
830 );
831
832 let unlock_data = result.expect("should be ok");
833 assert!(unlock_data.v2_upgrade_token.is_none());
834 }
835
836 fn make_valid_public_key_b64() -> String {
837 let store: KeyStore<KeySlotIds> = KeyStore::default();
838 let mut ctx = store.context_mut();
839 let key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
840 ctx.get_public_key(key_id)
841 .expect("public key exists")
842 .to_string()
843 }
844
845 #[test]
846 fn test_v1_emergency_access_membership_try_from_uses_name_when_present() {
847 let id = Uuid::new_v4();
848 let grantee_id = Uuid::new_v4();
849 let model = EmergencyAccessKeyDataResponseModel {
850 id: Some(id),
851 grantee_id: Some(grantee_id),
852 grantee_name: Some("Alice".to_string()),
853 grantee_email: Some("[email protected]".to_string()),
854 public_key: Some(make_valid_public_key_b64()),
855 ..Default::default()
856 };
857
858 let membership = V1EmergencyAccessMembership::try_from(model).expect("should be ok");
859 assert_eq!(membership.id, id);
860 assert_eq!(membership.grantee_id, grantee_id);
861 assert_eq!(membership.name, "Alice");
862 }
863
864 #[test]
865 fn test_v1_emergency_access_membership_try_from_falls_back_to_email_when_name_missing() {
866 let model = EmergencyAccessKeyDataResponseModel {
867 id: Some(Uuid::new_v4()),
868 grantee_id: Some(Uuid::new_v4()),
869 grantee_name: None,
870 grantee_email: Some("[email protected]".to_string()),
871 public_key: Some(make_valid_public_key_b64()),
872 ..Default::default()
873 };
874
875 let membership = V1EmergencyAccessMembership::try_from(model).expect("should be ok");
876 assert_eq!(membership.name, "[email protected]");
877 }
878
879 #[test]
880 fn test_v1_emergency_access_membership_try_from_falls_back_to_unknown_when_name_and_email_missing()
881 {
882 let model = EmergencyAccessKeyDataResponseModel {
883 id: Some(Uuid::new_v4()),
884 grantee_id: Some(Uuid::new_v4()),
885 grantee_name: None,
886 grantee_email: None,
887 public_key: Some(make_valid_public_key_b64()),
888 ..Default::default()
889 };
890
891 let membership = V1EmergencyAccessMembership::try_from(model).expect("should be ok");
892 assert_eq!(membership.name, "Unknown");
893 }
894
895 #[test]
896 fn test_v1_emergency_access_membership_try_from_missing_id_returns_error() {
897 let model = EmergencyAccessKeyDataResponseModel {
898 id: None,
899 grantee_id: Some(Uuid::new_v4()),
900 grantee_name: Some("Alice".to_string()),
901 grantee_email: None,
902 public_key: Some(make_valid_public_key_b64()),
903 ..Default::default()
904 };
905
906 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
907 panic!("expected error")
908 };
909 assert!(matches!(err, KeyRotationDataParseError::MissingField(_)));
910 }
911
912 #[test]
913 fn test_v1_emergency_access_membership_try_from_missing_grantee_id_returns_error() {
914 let model = EmergencyAccessKeyDataResponseModel {
915 id: Some(Uuid::new_v4()),
916 grantee_id: None,
917 grantee_name: Some("Alice".to_string()),
918 grantee_email: None,
919 public_key: Some(make_valid_public_key_b64()),
920 ..Default::default()
921 };
922
923 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
924 panic!("expected error")
925 };
926 assert!(matches!(err, KeyRotationDataParseError::MissingField(_)));
927 }
928
929 #[test]
930 fn test_v1_emergency_access_membership_try_from_missing_public_key_returns_error() {
931 let model = EmergencyAccessKeyDataResponseModel {
932 id: Some(Uuid::new_v4()),
933 grantee_id: Some(Uuid::new_v4()),
934 grantee_name: Some("Alice".to_string()),
935 grantee_email: None,
936 public_key: None,
937 ..Default::default()
938 };
939
940 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
941 panic!("expected error")
942 };
943 assert!(matches!(err, KeyRotationDataParseError::MissingField(_)));
944 }
945
946 #[test]
947 fn test_v1_emergency_access_membership_try_from_invalid_b64_public_key_returns_error() {
948 let model = EmergencyAccessKeyDataResponseModel {
949 id: Some(Uuid::new_v4()),
950 grantee_id: Some(Uuid::new_v4()),
951 grantee_name: Some("Alice".to_string()),
952 grantee_email: None,
953 public_key: Some("not valid base64 !!!".to_string()),
954 ..Default::default()
955 };
956
957 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
958 panic!("expected error")
959 };
960 assert!(matches!(err, KeyRotationDataParseError::B64(_)));
961 }
962
963 #[test]
964 fn test_v1_emergency_access_membership_try_from_invalid_spki_public_key_returns_error() {
965 let model = EmergencyAccessKeyDataResponseModel {
966 id: Some(Uuid::new_v4()),
967 grantee_id: Some(Uuid::new_v4()),
968 grantee_name: Some("Alice".to_string()),
969 grantee_email: None,
970 public_key: Some(B64::from(b"not-a-real-public-key".as_slice()).to_string()),
971 ..Default::default()
972 };
973
974 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
975 panic!("expected error")
976 };
977 assert!(matches!(err, KeyRotationDataParseError::Crypto(_)));
978 }
979
980 #[test]
981 fn test_reencrypt_master_password_change_unlock_data_never_returns_upgrade_token() {
982 let store: KeyStore<KeySlotIds> = KeyStore::default();
983 let mut ctx = store.context_mut();
984
985 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
986 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
987
988 let input = ReencryptMasterPasswordChangeAndUnlockInput {
989 password: "test_password".to_string(),
990 hint: None,
991 kdf: create_test_kdf_pbkdf2(),
992 salt: "[email protected]".to_string(),
993 common_unlock_data: empty_common_unlock_input(),
994 };
995
996 let unlock_data = reencrypt_master_password_change_unlock_data(
997 input,
998 current_user_key_id,
999 new_user_key_id,
1000 &mut ctx,
1001 )
1002 .expect("should be ok");
1003
1004 assert!(
1005 unlock_data.v2_upgrade_token.is_none(),
1006 "master password change rotation must never include a v2 upgrade token"
1007 );
1008 }
1009}