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 (
377 Ok(SymmetricKeyAlgorithm::Aes256CbcHmac),
378 Ok(SymmetricKeyAlgorithm::XChaCha20Poly1305),
379 ) => {
380 let token =
381 V2UpgradeToken::create(current_user_key_id, new_user_key_id, ctx).map_err(|e| {
382 error!("Failed to create V2 upgrade token: {e}");
383 ReencryptError::UpgradeTokenCreation
384 })?;
385 info!("Upgrade token created for the key rotation");
386 Ok(Some(Box::new(token.into())))
387 }
388 _ => Ok(None),
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use std::num::NonZeroU32;
395
396 use bitwarden_api_api::models::KdfType;
397 use bitwarden_core::key_management::KeySlotIds;
398 use bitwarden_crypto::{Kdf, KeyStore, PublicKeyEncryptionAlgorithm, UnsignedSharedKey};
399 use uuid::Uuid;
400
401 use super::*;
402 use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
403
404 fn create_test_kdf_pbkdf2() -> Kdf {
405 Kdf::PBKDF2 {
406 iterations: NonZeroU32::new(600000).expect("valid iterations"),
407 }
408 }
409
410 fn create_test_kdf_argon2id() -> Kdf {
411 Kdf::Argon2id {
412 iterations: NonZeroU32::new(3).expect("valid iterations"),
413 memory: NonZeroU32::new(64).expect("valid memory"),
414 parallelism: NonZeroU32::new(4).expect("valid parallelism"),
415 }
416 }
417
418 fn assert_symmetric_keys_equal(
419 key_id_1: SymmetricKeySlotId,
420 key_id_2: SymmetricKeySlotId,
421 ctx: &mut KeyStoreContext<KeySlotIds>,
422 ) {
423 #[allow(deprecated)]
424 let key_1 = ctx
425 .dangerous_get_symmetric_key(key_id_1)
426 .expect("key 1 should exist");
427 #[allow(deprecated)]
428 let key_2 = ctx
429 .dangerous_get_symmetric_key(key_id_2)
430 .expect("key 2 should exist");
431 assert_eq!(key_1, key_2, "symmetric keys should be equal");
432 }
433
434 fn empty_common_unlock_input() -> ReencryptCommonUnlockDataInput {
435 ReencryptCommonUnlockDataInput {
436 trusted_devices: vec![],
437 webauthn_credentials: vec![],
438 trusted_organization_keys: vec![],
439 trusted_emergency_access_keys: vec![],
440 }
441 }
442
443 fn request_model_to_token(request: V2UpgradeTokenRequestModel) -> V2UpgradeToken {
444 V2UpgradeToken {
445 wrapped_user_key_1: request
446 .wrapped_user_key1
447 .parse()
448 .expect("wrapped_user_key1 should parse"),
449 wrapped_user_key_2: request
450 .wrapped_user_key2
451 .parse()
452 .expect("wrapped_user_key2 should parse"),
453 }
454 }
455
456 #[test]
457 fn test_to_authentication_and_unlock_data_pbkdf2() {
458 let store: KeyStore<KeySlotIds> = KeyStore::default();
459 let mut ctx = store.context_mut();
460
461 let kdf = create_test_kdf_pbkdf2();
462 let salt = "[email protected]";
463 let password = "test_password";
464
465 let user_key_id = ctx.generate_symmetric_key();
466 let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
467 .expect("derive should succeed");
468 let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
469 .expect("derive should succeed");
470
471 let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
472 assert!(result.is_ok());
473
474 let model = result.expect("should be ok");
475 assert_eq!(model.kdf_type, KdfType::PBKDF2_SHA256);
476 assert_eq!(model.kdf_iterations, 600000);
477 assert!(model.kdf_memory.is_none());
478 assert!(model.kdf_parallelism.is_none());
479 assert_eq!(model.email, Some(salt.to_string()));
480 assert!(model.master_key_authentication_hash.is_some());
481 assert!(model.master_key_encrypted_user_key.is_some());
482 assert!(model.master_password_hint.is_none());
483
484 let master_password_unlock_data = MasterPasswordUnlockData {
486 master_key_wrapped_user_key: model
487 .master_key_encrypted_user_key
488 .expect("should be present")
489 .parse()
490 .expect("should parse"),
491 kdf: kdf.clone(),
492 salt: salt.to_string(),
493 };
494 let decrypted_user_key = master_password_unlock_data
495 .unwrap_to_context(password, &mut ctx)
496 .expect("unwrap should succeed");
497 assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
498 }
499
500 #[test]
501 fn test_to_authentication_and_unlock_data_argon2id() {
502 let store: KeyStore<KeySlotIds> = KeyStore::default();
503 let mut ctx = store.context_mut();
504
505 let kdf = create_test_kdf_argon2id();
506 let salt = "[email protected]";
507 let password = "test_password";
508
509 let user_key_id = ctx.generate_symmetric_key();
510 let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
511 .expect("derive should succeed");
512 let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
513 .expect("derive should succeed");
514
515 let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
516 assert!(result.is_ok());
517
518 let model = result.expect("should be ok");
519 assert_eq!(model.kdf_type, KdfType::Argon2id);
520 assert_eq!(model.kdf_iterations, 3);
521 assert_eq!(model.kdf_memory, Some(64));
522 assert_eq!(model.kdf_parallelism, Some(4));
523 assert_eq!(model.email, Some(salt.to_string()));
524 assert!(model.master_key_authentication_hash.is_some());
525 assert!(model.master_key_encrypted_user_key.is_some());
526
527 let master_password_unlock_data = MasterPasswordUnlockData {
529 master_key_wrapped_user_key: model
530 .master_key_encrypted_user_key
531 .expect("should be present")
532 .parse()
533 .expect("should parse"),
534 kdf: kdf.clone(),
535 salt: salt.to_string(),
536 };
537 let decrypted_user_key = master_password_unlock_data
538 .unwrap_to_context(password, &mut ctx)
539 .expect("unwrap should succeed");
540 assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
541 }
542
543 #[test]
544 fn test_reencrypt_unlock_device_key_data() {
545 let store: KeyStore<KeySlotIds> = KeyStore::default();
546 let mut ctx = store.context_mut();
547
548 let current_user_key_id = ctx.generate_symmetric_key();
549 let new_user_key_id = ctx.generate_symmetric_key();
550
551 let (device_keyset, device_private_key) =
552 PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
553
554 let result = reencrypt_common_unlock_data(
555 ReencryptCommonUnlockDataInput {
556 trusted_devices: vec![device_keyset],
557 webauthn_credentials: vec![],
558 trusted_organization_keys: vec![],
559 trusted_emergency_access_keys: vec![],
560 },
561 current_user_key_id,
562 new_user_key_id,
563 UpgradeTokenAction::CreateIfNeeded,
564 &mut ctx,
565 );
566
567 let unlock_data = result.expect("should be ok");
568
569 let device_unlock = unlock_data
570 .device_key_unlock_data
571 .as_ref()
572 .expect("should be present")
573 .first()
574 .expect("should have at least one");
575 let decrypted_user_key = device_unlock
576 .encrypted_user_key
577 .parse::<UnsignedSharedKey>()
578 .expect("should parse")
579 .decapsulate(device_private_key, &mut ctx)
580 .expect("unwrap should succeed");
581 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
582 }
583
584 #[test]
585 fn test_reencrypt_unlock_webauthn_prf_credential_data() {
586 let store: KeyStore<KeySlotIds> = KeyStore::default();
587 let mut ctx = store.context_mut();
588
589 let current_user_key_id = ctx.generate_symmetric_key();
590 let new_user_key_id = ctx.generate_symmetric_key();
591
592 let (credential_keyset, credential_private_key) =
593 PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
594
595 let result = reencrypt_common_unlock_data(
596 ReencryptCommonUnlockDataInput {
597 trusted_devices: vec![],
598 webauthn_credentials: vec![credential_keyset],
599 trusted_organization_keys: vec![],
600 trusted_emergency_access_keys: vec![],
601 },
602 current_user_key_id,
603 new_user_key_id,
604 UpgradeTokenAction::CreateIfNeeded,
605 &mut ctx,
606 );
607
608 let unlock_data = result.expect("should be ok");
609
610 let credential_unlock = unlock_data
612 .passkey_unlock_data
613 .as_ref()
614 .expect("should be present")
615 .first()
616 .expect("should have at least one");
617 let decrypted_user_key = credential_unlock
618 .encrypted_user_key
619 .parse::<UnsignedSharedKey>()
620 .expect("should parse")
621 .decapsulate(credential_private_key, &mut ctx)
622 .expect("unwrap should succeed");
623 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
624 }
625
626 #[test]
627 fn test_reencrypt_unlock_emergency_access_data() {
628 let store: KeyStore<KeySlotIds> = KeyStore::default();
629 let mut ctx = store.context_mut();
630
631 let current_user_key_id = ctx.generate_symmetric_key();
632 let new_user_key_id = ctx.generate_symmetric_key();
633
634 let organization_private_key =
635 ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
636 let emergency_access = V1EmergencyAccessMembership {
637 id: Uuid::new_v4(),
638 grantee_id: Uuid::new_v4(),
639 name: "Test User".to_string(),
640 public_key: ctx
641 .get_public_key(organization_private_key)
642 .expect("key exists"),
643 };
644
645 let result = reencrypt_common_unlock_data(
646 ReencryptCommonUnlockDataInput {
647 trusted_devices: vec![],
648 webauthn_credentials: vec![],
649 trusted_organization_keys: vec![],
650 trusted_emergency_access_keys: vec![emergency_access],
651 },
652 current_user_key_id,
653 new_user_key_id,
654 UpgradeTokenAction::CreateIfNeeded,
655 &mut ctx,
656 );
657
658 let unlock_data = result.expect("should be ok");
659
660 let emergency_access_unlock = unlock_data
662 .emergency_access_unlock_data
663 .as_ref()
664 .expect("should be present")
665 .first()
666 .expect("should have at least one");
667 let decrypted_user_key = emergency_access_unlock
668 .key_encrypted
669 .as_ref()
670 .map(|k| k.parse::<UnsignedSharedKey>())
671 .expect("should be present")
672 .expect("should parse")
673 .decapsulate(organization_private_key, &mut ctx)
674 .expect("unwrap should succeed");
675 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
676 }
677
678 #[test]
679 fn test_reencrypt_unlock_organization_membership_data() {
680 let store: KeyStore<KeySlotIds> = KeyStore::default();
681 let mut ctx = store.context_mut();
682
683 let current_user_key_id = ctx.generate_symmetric_key();
684 let new_user_key_id = ctx.generate_symmetric_key();
685
686 let org_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
687 let org_membership = V1OrganizationMembership {
688 organization_id: Uuid::new_v4(),
689 name: "Test Org".to_string(),
690 public_key: ctx.get_public_key(org_key).expect("key exists"),
691 };
692
693 let result = reencrypt_common_unlock_data(
694 ReencryptCommonUnlockDataInput {
695 trusted_devices: vec![],
696 webauthn_credentials: vec![],
697 trusted_organization_keys: vec![org_membership],
698 trusted_emergency_access_keys: vec![],
699 },
700 current_user_key_id,
701 new_user_key_id,
702 UpgradeTokenAction::CreateIfNeeded,
703 &mut ctx,
704 );
705
706 let unlock_data = result.expect("should be ok");
707
708 let org_membership_unlock = unlock_data
709 .organization_account_recovery_unlock_data
710 .as_ref()
711 .expect("should be present")
712 .first()
713 .expect("should have at least one");
714 let decrypted_user_key = org_membership_unlock
715 .reset_password_key
716 .as_ref()
717 .map(|k| k.parse::<UnsignedSharedKey>())
718 .expect("should be present")
719 .expect("should parse")
720 .decapsulate(org_key, &mut ctx)
721 .expect("unwrap should succeed");
722 assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
723 }
724
725 #[test]
726 fn test_reencrypt_common_unlock_data_v1_to_v2_creates_upgrade_token() {
727 let store: KeyStore<KeySlotIds> = KeyStore::default();
728 let mut ctx = store.context_mut();
729
730 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
731 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
732
733 let result = reencrypt_common_unlock_data(
734 empty_common_unlock_input(),
735 current_user_key_id,
736 new_user_key_id,
737 UpgradeTokenAction::CreateIfNeeded,
738 &mut ctx,
739 );
740
741 let unlock_data = result.expect("should be ok");
742 let token_request = *unlock_data
743 .v2_upgrade_token
744 .expect("v2_upgrade_token should be populated for V1 -> V2 rotation");
745 let token = request_model_to_token(token_request);
746
747 let unwrapped_v2_id = token
748 .unwrap_v2(current_user_key_id, &mut ctx)
749 .expect("unwrap_v2 should succeed");
750 assert_symmetric_keys_equal(new_user_key_id, unwrapped_v2_id, &mut ctx);
751
752 let unwrapped_v1_id = token
753 .unwrap_v1(new_user_key_id, &mut ctx)
754 .expect("unwrap_v1 should succeed");
755 assert_symmetric_keys_equal(current_user_key_id, unwrapped_v1_id, &mut ctx);
756 }
757
758 #[test]
759 fn test_reencrypt_common_unlock_data_v1_to_v2_upgrade_token_action_skip_returns_none() {
760 let store: KeyStore<KeySlotIds> = KeyStore::default();
761 let mut ctx = store.context_mut();
762
763 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
764 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
765
766 let result = reencrypt_common_unlock_data(
767 empty_common_unlock_input(),
768 current_user_key_id,
769 new_user_key_id,
770 UpgradeTokenAction::Skip,
771 &mut ctx,
772 );
773
774 let unlock_data = result.expect("should be ok");
775 assert!(
776 unlock_data.v2_upgrade_token.is_none(),
777 "UpgradeTokenAction::Skip skips the creation of the upgrade token"
778 );
779 }
780
781 #[test]
782 fn test_reencrypt_common_unlock_data_v2_to_v2_upgrade_token_action_create_if_needed_returns_none()
783 {
784 let store: KeyStore<KeySlotIds> = KeyStore::default();
785 let mut ctx = store.context_mut();
786
787 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
788 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
789
790 let result = reencrypt_common_unlock_data(
791 empty_common_unlock_input(),
792 current_user_key_id,
793 new_user_key_id,
794 UpgradeTokenAction::CreateIfNeeded,
795 &mut ctx,
796 );
797
798 let unlock_data = result.expect("should be ok");
799 assert!(
800 unlock_data.v2_upgrade_token.is_none(),
801 "UpgradeTokenAction::CreateIfNeeded should not create a v2_upgrade_token for V2 -> V2 rotation"
802 );
803 }
804
805 fn make_valid_public_key_b64() -> String {
806 let store: KeyStore<KeySlotIds> = KeyStore::default();
807 let mut ctx = store.context_mut();
808 let key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
809 ctx.get_public_key(key_id)
810 .expect("public key exists")
811 .to_string()
812 }
813
814 #[test]
815 fn test_v1_emergency_access_membership_try_from_uses_name_when_present() {
816 let id = Uuid::new_v4();
817 let grantee_id = Uuid::new_v4();
818 let model = EmergencyAccessKeyDataResponseModel {
819 id: Some(id),
820 grantee_id: Some(grantee_id),
821 grantee_name: Some("Alice".to_string()),
822 grantee_email: Some("[email protected]".to_string()),
823 public_key: Some(make_valid_public_key_b64()),
824 ..Default::default()
825 };
826
827 let membership = V1EmergencyAccessMembership::try_from(model).expect("should be ok");
828 assert_eq!(membership.id, id);
829 assert_eq!(membership.grantee_id, grantee_id);
830 assert_eq!(membership.name, "Alice");
831 }
832
833 #[test]
834 fn test_v1_emergency_access_membership_try_from_falls_back_to_email_when_name_missing() {
835 let model = EmergencyAccessKeyDataResponseModel {
836 id: Some(Uuid::new_v4()),
837 grantee_id: Some(Uuid::new_v4()),
838 grantee_name: None,
839 grantee_email: Some("[email protected]".to_string()),
840 public_key: Some(make_valid_public_key_b64()),
841 ..Default::default()
842 };
843
844 let membership = V1EmergencyAccessMembership::try_from(model).expect("should be ok");
845 assert_eq!(membership.name, "[email protected]");
846 }
847
848 #[test]
849 fn test_v1_emergency_access_membership_try_from_falls_back_to_unknown_when_name_and_email_missing()
850 {
851 let model = EmergencyAccessKeyDataResponseModel {
852 id: Some(Uuid::new_v4()),
853 grantee_id: Some(Uuid::new_v4()),
854 grantee_name: None,
855 grantee_email: None,
856 public_key: Some(make_valid_public_key_b64()),
857 ..Default::default()
858 };
859
860 let membership = V1EmergencyAccessMembership::try_from(model).expect("should be ok");
861 assert_eq!(membership.name, "Unknown");
862 }
863
864 #[test]
865 fn test_v1_emergency_access_membership_try_from_missing_id_returns_error() {
866 let model = EmergencyAccessKeyDataResponseModel {
867 id: None,
868 grantee_id: Some(Uuid::new_v4()),
869 grantee_name: Some("Alice".to_string()),
870 grantee_email: None,
871 public_key: Some(make_valid_public_key_b64()),
872 ..Default::default()
873 };
874
875 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
876 panic!("expected error")
877 };
878 assert!(matches!(err, KeyRotationDataParseError::MissingField(_)));
879 }
880
881 #[test]
882 fn test_v1_emergency_access_membership_try_from_missing_grantee_id_returns_error() {
883 let model = EmergencyAccessKeyDataResponseModel {
884 id: Some(Uuid::new_v4()),
885 grantee_id: None,
886 grantee_name: Some("Alice".to_string()),
887 grantee_email: None,
888 public_key: Some(make_valid_public_key_b64()),
889 ..Default::default()
890 };
891
892 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
893 panic!("expected error")
894 };
895 assert!(matches!(err, KeyRotationDataParseError::MissingField(_)));
896 }
897
898 #[test]
899 fn test_v1_emergency_access_membership_try_from_missing_public_key_returns_error() {
900 let model = EmergencyAccessKeyDataResponseModel {
901 id: Some(Uuid::new_v4()),
902 grantee_id: Some(Uuid::new_v4()),
903 grantee_name: Some("Alice".to_string()),
904 grantee_email: None,
905 public_key: None,
906 ..Default::default()
907 };
908
909 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
910 panic!("expected error")
911 };
912 assert!(matches!(err, KeyRotationDataParseError::MissingField(_)));
913 }
914
915 #[test]
916 fn test_v1_emergency_access_membership_try_from_invalid_b64_public_key_returns_error() {
917 let model = EmergencyAccessKeyDataResponseModel {
918 id: Some(Uuid::new_v4()),
919 grantee_id: Some(Uuid::new_v4()),
920 grantee_name: Some("Alice".to_string()),
921 grantee_email: None,
922 public_key: Some("not valid base64 !!!".to_string()),
923 ..Default::default()
924 };
925
926 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
927 panic!("expected error")
928 };
929 assert!(matches!(err, KeyRotationDataParseError::B64(_)));
930 }
931
932 #[test]
933 fn test_v1_emergency_access_membership_try_from_invalid_spki_public_key_returns_error() {
934 let model = EmergencyAccessKeyDataResponseModel {
935 id: Some(Uuid::new_v4()),
936 grantee_id: Some(Uuid::new_v4()),
937 grantee_name: Some("Alice".to_string()),
938 grantee_email: None,
939 public_key: Some(B64::from(b"not-a-real-public-key".as_slice()).to_string()),
940 ..Default::default()
941 };
942
943 let Err(err) = V1EmergencyAccessMembership::try_from(model) else {
944 panic!("expected error")
945 };
946 assert!(matches!(err, KeyRotationDataParseError::Crypto(_)));
947 }
948
949 #[test]
950 fn test_reencrypt_master_password_change_unlock_data_never_returns_upgrade_token() {
951 let store: KeyStore<KeySlotIds> = KeyStore::default();
952 let mut ctx = store.context_mut();
953
954 let current_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
955 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
956
957 let input = ReencryptMasterPasswordChangeAndUnlockInput {
958 password: "test_password".to_string(),
959 hint: None,
960 kdf: create_test_kdf_pbkdf2(),
961 salt: "[email protected]".to_string(),
962 common_unlock_data: empty_common_unlock_input(),
963 };
964
965 let unlock_data = reencrypt_master_password_change_unlock_data(
966 input,
967 current_user_key_id,
968 new_user_key_id,
969 &mut ctx,
970 )
971 .expect("should be ok");
972
973 assert!(
974 unlock_data.v2_upgrade_token.is_none(),
975 "master password change rotation must never include a v2 upgrade token"
976 );
977 }
978}