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