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