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