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