Skip to main content

bitwarden_user_crypto_management/key_rotation/
unlock.rs

1//! Functionality for re-encrypting unlock (decryption) methods during user key rotation.
2//! During key-rotation, a new user-key is sampled. The unlock module then creates a set of newly
3//! encrypted copies, one for each decryption/unlock method.
4use 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/// The data necessary to re-share the user-key to a V1 emergency access membership. Note: The
35/// Public-key must be verified/trusted. Further, there is no sender authentication possible here.
36#[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            // The name can be null if a user does not set a name; fall back to the email and
53            // then to "Unknown" so we always have a non-empty display name.
54            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/// The data necessary to re-share the user-key to a V1 organization membership. Note: The
64/// Public-key must be verified/trusted. Further, there is no sender authentication possible here.
65#[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    /// Failed to update the unlock data for the master password
94    MasterPasswordDerivation,
95    /// Failed to update the unlock data for TDE/PRF-Passkey
96    KeysetUnlockDataReencryption,
97    /// Failed to update the unlock data for emergency access or organization membership
98    KeySharingError,
99    /// Failed to wrap the user key with the Key Connector key
100    KeyConnectorWrapping,
101    /// Failed to create v2 upgrade token
102    UpgradeTokenCreation,
103}
104
105pub(super) struct ReencryptMasterPasswordChangeAndUnlockInput {
106    /// Master password change data.
107    pub(super) password: String,
108    pub(super) hint: Option<String>,
109    pub(super) kdf: Kdf,
110    pub(super) salt: String,
111    /// Common unlock data to re-encrypt
112    pub(super) common_unlock_data: ReencryptCommonUnlockDataInput,
113}
114
115pub(super) struct ReencryptCommonUnlockDataInput {
116    /// The trusted device keysets.
117    pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
118    /// The webauthn credential keysets.
119    pub(super) webauthn_credentials: Vec<PartialRotateableKeyset>,
120    /// The V1 organization memberships.
121    pub(super) trusted_organization_keys: Vec<V1OrganizationMembership>,
122    /// The V1 emergency access memberships.
123    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        // Master password change + key rotation always needs to logout other sessions, so no
157        // upgrade token is created.
158        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
202/// Re-encrypt TDE device keys for the new user key.
203fn 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
221/// Re-encrypt passkey (WebAuthn PRF) credentials for the new user key.
222fn 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
240/// Re-encrypt emergency access keys for the new user key.
241fn 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            // Share the key to the organization. Note: No sender authentication
252            // and the passed in public-key must be verified/trusted.
253            match UnsignedSharedKey::encapsulate(new_user_key_id, &ea.public_key, ctx) {
254                Ok(reencrypted_key) => Ok(EmergencyAccessWithIdRequestModel {
255                    // Default value that is ignored on the server
256                    r#type: models::EmergencyAccessType::Takeover,
257                    // Default value that is ignored on the server
258                    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
268/// Re-encrypt organization membership keys for the new user key.
269fn 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            // Share the key to the organization. Note: No sender authentication
281            // and the passed in public-key must be verified/trusted.
282            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        // Verify the unlock data can decrypt the user key
482        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        // Verify the unlock data can decrypt the user key
525        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        // Ensure it decrypts to the correct key after rotation
608        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        // Ensure it decrypts to the correct key after rotation
658        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}