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, 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/// The data necessary to re-share the user-key to a V1 emergency access membership. Note: The
32/// Public-key must be verified/trusted. Further, there is no sender authentication possible here.
33#[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            // The name can be null if a user does not set a name; fall back to the email and
50            // then to "Unknown" so we always have a non-empty display name.
51            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/// The data necessary to re-share the user-key to a V1 organization membership. Note: The
61/// Public-key must be verified/trusted. Further, there is no sender authentication possible here.
62#[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    /// Failed to update the unlock data for the master password
91    MasterPasswordDerivation,
92    /// Failed to update the unlock data for TDE/PRF-Passkey
93    KeysetUnlockDataReencryption,
94    /// Failed to update the unlock data for emergency access or organization membership
95    KeySharingError,
96    /// Failed to wrap the user key with the Key Connector key
97    KeyConnectorWrapping,
98    /// Failed to create v2 upgrade token
99    UpgradeTokenCreation,
100}
101
102pub(super) struct ReencryptMasterPasswordChangeAndUnlockInput {
103    /// Master password change data.
104    pub(super) password: String,
105    pub(super) hint: Option<String>,
106    pub(super) kdf: Kdf,
107    pub(super) salt: String,
108    /// Common unlock data to re-encrypt
109    pub(super) common_unlock_data: ReencryptCommonUnlockDataInput,
110}
111
112pub(super) struct ReencryptCommonUnlockDataInput {
113    /// The trusted device keysets.
114    pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
115    /// The webauthn credential keysets.
116    pub(super) webauthn_credentials: Vec<PartialRotateableKeyset>,
117    /// The V1 organization memberships.
118    pub(super) trusted_organization_keys: Vec<V1OrganizationMembership>,
119    /// The V1 emergency access memberships.
120    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        // Master password change + key rotation always needs to logout other sessions, so no
154        // upgrade token is created.
155        v2_upgrade_token: None,
156    })
157}
158
159/// Re-encrypts the unlock methods that every key rotation flow shares.
160pub(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
203/// Re-encrypt TDE device keys for the new user key.
204fn 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
222/// Re-encrypt passkey (WebAuthn PRF) credentials for the new user key.
223fn 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
241/// Re-encrypt emergency access keys for the new user key.
242fn 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            // Share the key to the organization. Note: No sender authentication
253            // and the passed in public-key must be verified/trusted.
254            match UnsignedSharedKey::encapsulate(new_user_key_id, &ea.public_key, ctx) {
255                Ok(reencrypted_key) => Ok(EmergencyAccessWithIdRequestModel {
256                    // Default value that is ignored on the server
257                    r#type: models::EmergencyAccessType::Takeover,
258                    // Default value that is ignored on the server
259                    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
269/// Re-encrypt organization membership keys for the new user key.
270fn 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            // Share the key to the organization. Note: No sender authentication
282            // and the passed in public-key must be verified/trusted.
283            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
294/// Leaves account recovery for organization admins to update, so the user is not involved.
295///
296/// Sending no key keeps the one the organization already holds. The organizations are still listed.
297/// Leaving them out would look like the user is no longer enrolled.
298fn 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        // Verify the unlock data can decrypt the user key
500        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        // Verify the unlock data can decrypt the user key
544        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        // Ensure it decrypts to the correct key after rotation
628        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        // Ensure it decrypts to the correct key after rotation
678        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}