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.
4
5use core::panic;
6
7use bitwarden_api_api::models::{
8    self, EmergencyAccessWithIdRequestModel, MasterPasswordUnlockAndAuthenticationDataModel,
9    OtherDeviceKeysUpdateRequestModel, ResetPasswordWithOrgIdRequestModel, UnlockDataRequestModel,
10    WebAuthnLoginRotateKeyRequestModel,
11};
12use bitwarden_core::key_management::{
13    KeyIds, MasterPasswordAuthenticationData, MasterPasswordUnlockData, SymmetricKeyId,
14};
15use bitwarden_crypto::{Kdf, KeyStoreContext, PublicKey, UnsignedSharedKey};
16use serde::{Deserialize, Serialize};
17use tracing::debug_span;
18#[cfg(feature = "wasm")]
19use tsify::Tsify;
20
21use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
22
23/// The unlock method that uses the master-key field on the user's account. This can be either
24/// the master password, or the key-connector. For TDE users without a master password, this field
25/// is empty.
26pub(super) enum MasterkeyUnlockMethod {
27    /// The master password based unlock method.
28    Password {
29        password: String,
30        hint: Option<String>,
31        kdf: Kdf,
32        salt: String,
33    },
34    /// The key-connector based unlock method.
35    /// NOTE: THIS IS NOT SUPPORTED YET AND WILL PANIC IF USED
36    KeyConnector,
37    /// No master-key based unlock method. This is TDE users without a master password.
38    /// NOTE: THIS IS NOT SUPPORTED YET AND WILL PANIC IF USED
39    None,
40}
41
42/// The data necessary to re-share the user-key to a V1 emergency access membership. Note: The
43/// Public-key must be verified/trusted. Further, there is no sender authentication possible here.
44#[derive(Serialize, Deserialize, Clone)]
45#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
46pub struct V1EmergencyAccessMembership {
47    pub id: uuid::Uuid,
48    pub name: String,
49    pub public_key: PublicKey,
50}
51
52/// The data necessary to re-share the user-key to a V1 organization membership. Note: The
53/// Public-key must be verified/trusted. Further, there is no sender authentication possible here.
54#[derive(Serialize, Deserialize, Clone)]
55#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
56pub struct V1OrganizationMembership {
57    pub organization_id: uuid::Uuid,
58    pub name: String,
59    pub public_key: PublicKey,
60}
61
62#[derive(Debug)]
63pub(super) enum ReencryptError {
64    /// Failed to update the unlock data for the master password
65    MasterPasswordDerivation,
66    /// Failed to update the unlock data for TDE/PRF-Passkey
67    KeysetUnlockDataReencryption,
68    /// Failed to update the unlock data for emergency access or organization membership
69    KeySharingError,
70}
71
72/// Input data for re-encrypting unlock methods during user key rotation.
73pub(super) struct ReencryptUnlockInput {
74    /// The master-key based unlock method.
75    pub(super) master_key_unlock_method: MasterkeyUnlockMethod,
76    /// The trusted device keysets.
77    pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
78    /// The webauthn credential keysets.
79    pub(super) webauthn_credentials: Vec<PartialRotateableKeyset>,
80    /// The V1 organization memberships.
81    pub(super) trusted_organization_keys: Vec<V1OrganizationMembership>,
82    /// The V1 emergency access memberships.
83    pub(super) trusted_emergency_access_keys: Vec<V1EmergencyAccessMembership>,
84}
85
86/// Update the unlock methods for the updated user-key.
87pub(super) fn reencrypt_unlock(
88    input: ReencryptUnlockInput,
89    current_user_key_id: SymmetricKeyId,
90    new_user_key_id: SymmetricKeyId,
91    ctx: &mut KeyStoreContext<KeyIds>,
92) -> Result<UnlockDataRequestModel, ReencryptError> {
93    let master_password_unlock_data = match input.master_key_unlock_method {
94        MasterkeyUnlockMethod::Password {
95            password,
96            hint,
97            kdf,
98            salt,
99        } => reencrypt_userkey_for_masterpassword_unlock(
100            password,
101            hint,
102            kdf,
103            salt,
104            new_user_key_id,
105            ctx,
106        )?,
107        MasterkeyUnlockMethod::KeyConnector => {
108            panic!("KeyConnector based masterkey unlock method is not supported yet")
109        }
110        MasterkeyUnlockMethod::None => panic!("None masterkey unlock method is not supported yet"),
111    };
112
113    let tde_device_unlock_data = reencrypt_tde_devices(
114        &input.trusted_devices,
115        current_user_key_id,
116        new_user_key_id,
117        ctx,
118    )?;
119    let prf_passkey_unlock_data = reencrypt_passkey_credentials(
120        &input.webauthn_credentials,
121        current_user_key_id,
122        new_user_key_id,
123        ctx,
124    )?;
125    let emergency_accesses =
126        reencrypt_emergency_access_keys(input.trusted_emergency_access_keys, new_user_key_id, ctx)?;
127    let organizations_memberships =
128        reencrypt_organization_memberships(input.trusted_organization_keys, new_user_key_id, ctx)?;
129
130    Ok(UnlockDataRequestModel {
131        master_password_unlock_data: Box::new(master_password_unlock_data),
132        emergency_access_unlock_data: Some(emergency_accesses),
133        organization_account_recovery_unlock_data: Some(organizations_memberships),
134        passkey_unlock_data: Some(prf_passkey_unlock_data),
135        device_key_unlock_data: Some(tde_device_unlock_data),
136        v2_upgrade_token: None,
137    })
138}
139
140/// Re-encrypt TDE device keys for the new user key.
141fn reencrypt_tde_devices(
142    trusted_devices: &[PartialRotateableKeyset],
143    current_user_key_id: SymmetricKeyId,
144    new_user_key_id: SymmetricKeyId,
145    ctx: &mut KeyStoreContext<KeyIds>,
146) -> Result<Vec<OtherDeviceKeysUpdateRequestModel>, ReencryptError> {
147    trusted_devices
148        .iter()
149        .map(|device| {
150            let _span = debug_span!("reencrypt_device_key", device_id = ?device.id).entered();
151            device
152                .rotate_userkey(current_user_key_id, new_user_key_id, ctx)
153                .map_err(|_| ReencryptError::KeysetUnlockDataReencryption)
154                .map(Into::into)
155        })
156        .collect()
157}
158
159/// Re-encrypt passkey (WebAuthn PRF) credentials for the new user key.
160fn reencrypt_passkey_credentials(
161    webauthn_credentials: &[PartialRotateableKeyset],
162    current_user_key_id: SymmetricKeyId,
163    new_user_key_id: SymmetricKeyId,
164    ctx: &mut KeyStoreContext<KeyIds>,
165) -> Result<Vec<WebAuthnLoginRotateKeyRequestModel>, ReencryptError> {
166    webauthn_credentials
167        .iter()
168        .map(|cred| {
169            let _span =
170                debug_span!("reencrypt_webauthn_credential", credential_id = ?cred.id).entered();
171            cred.rotate_userkey(current_user_key_id, new_user_key_id, ctx)
172                .map_err(|_| ReencryptError::KeysetUnlockDataReencryption)
173                .map(Into::into)
174        })
175        .collect()
176}
177
178/// Re-encrypt emergency access keys for the new user key.
179fn reencrypt_emergency_access_keys(
180    trusted_emergency_access_keys: Vec<V1EmergencyAccessMembership>,
181    new_user_key_id: SymmetricKeyId,
182    ctx: &mut KeyStoreContext<KeyIds>,
183) -> Result<Vec<EmergencyAccessWithIdRequestModel>, ReencryptError> {
184    trusted_emergency_access_keys
185        .into_iter()
186        .map(|ea| {
187            let _span =
188                debug_span!("reencrypt_emergency_access_key", grantee_id = ?ea.id).entered();
189            // Share the key to the organization. Note: No sender authentication
190            // and the passed in public-key must be verified/trusted.
191            match UnsignedSharedKey::encapsulate(new_user_key_id, &ea.public_key, ctx) {
192                Ok(reencrypted_key) => Ok(EmergencyAccessWithIdRequestModel {
193                    // Default value that is ignored on the server
194                    r#type: models::EmergencyAccessType::Takeover,
195                    // Default value that is ignored on the server
196                    wait_time_days: 1,
197                    id: ea.id,
198                    key_encrypted: reencrypted_key.to_string().into(),
199                }),
200                Err(_) => Err(ReencryptError::KeySharingError),
201            }
202        })
203        .collect()
204}
205
206/// Re-encrypt organization membership keys for the new user key.
207fn reencrypt_organization_memberships(
208    trusted_organization_keys: Vec<V1OrganizationMembership>,
209    new_user_key_id: SymmetricKeyId,
210    ctx: &mut KeyStoreContext<KeyIds>,
211) -> Result<Vec<ResetPasswordWithOrgIdRequestModel>, ReencryptError> {
212    trusted_organization_keys
213        .into_iter()
214        .map(|org_membership| {
215            let _span =
216                debug_span!("reencrypt_organization_key", organization = ?org_membership.organization_id)
217                    .entered();
218            // Share the key to the organization. Note: No sender authentication
219            // and the passed in public-key must be verified/trusted.
220            match UnsignedSharedKey::encapsulate(new_user_key_id, &org_membership.public_key, ctx) {
221                Ok(reencrypted_key) => Ok(ResetPasswordWithOrgIdRequestModel {
222                    reset_password_key: Some(reencrypted_key.to_string()),
223                    master_password_hash: None,
224                    organization_id: org_membership.organization_id,
225                }),
226                Err(_) => Err(ReencryptError::KeySharingError),
227            }
228        })
229        .collect()
230}
231
232fn reencrypt_userkey_for_masterpassword_unlock(
233    password: String,
234    hint: Option<String>,
235    kdf: Kdf,
236    salt: String,
237    new_user_key_id: SymmetricKeyId,
238    ctx: &mut KeyStoreContext<KeyIds>,
239) -> Result<MasterPasswordUnlockAndAuthenticationDataModel, ReencryptError> {
240    let _span = debug_span!("derive_master_password_unlock_data").entered();
241    let unlock_data =
242        MasterPasswordUnlockData::derive(&password, &kdf, &salt, new_user_key_id, ctx)
243            .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
244    let authentication_data = MasterPasswordAuthenticationData::derive(&password, &kdf, &salt)
245        .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
246    to_authentication_and_unlock_data(unlock_data, authentication_data, hint)
247        .map_err(|_| ReencryptError::MasterPasswordDerivation)
248}
249
250#[derive(Debug)]
251struct ParsingError;
252
253fn to_authentication_and_unlock_data(
254    master_password_unlock_data: MasterPasswordUnlockData,
255    master_password_authentication_data: MasterPasswordAuthenticationData,
256    hint: Option<String>,
257) -> Result<MasterPasswordUnlockAndAuthenticationDataModel, ParsingError> {
258    let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) =
259        match master_password_unlock_data.kdf {
260            bitwarden_crypto::Kdf::PBKDF2 { iterations } => {
261                (models::KdfType::PBKDF2_SHA256, iterations, None, None)
262            }
263            bitwarden_crypto::Kdf::Argon2id {
264                iterations,
265                memory,
266                parallelism,
267            } => (
268                models::KdfType::Argon2id,
269                iterations,
270                Some(memory),
271                Some(parallelism),
272            ),
273        };
274    Ok(MasterPasswordUnlockAndAuthenticationDataModel {
275        kdf_type,
276        kdf_iterations: kdf_iterations.get().try_into().map_err(|_| ParsingError)?,
277        kdf_memory: kdf_memory
278            .map(|m| m.get().try_into().map_err(|_| ParsingError))
279            .transpose()?,
280        kdf_parallelism: kdf_parallelism
281            .map(|p| p.get().try_into().map_err(|_| ParsingError))
282            .transpose()?,
283        email: Some(master_password_unlock_data.salt.clone()),
284        master_key_authentication_hash: Some(
285            master_password_authentication_data
286                .master_password_authentication_hash
287                .to_string(),
288        ),
289        master_key_encrypted_user_key: Some(
290            master_password_unlock_data
291                .master_key_wrapped_user_key
292                .to_string(),
293        ),
294        master_password_hint: hint,
295        master_password_salt: Some(master_password_unlock_data.salt.clone()),
296    })
297}
298
299#[cfg(test)]
300mod tests {
301    use std::num::NonZeroU32;
302
303    use bitwarden_api_api::models::KdfType;
304    use bitwarden_core::key_management::KeyIds;
305    use bitwarden_crypto::{Kdf, KeyStore, PublicKeyEncryptionAlgorithm, UnsignedSharedKey};
306    use uuid::Uuid;
307
308    use super::*;
309    use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
310
311    fn create_test_kdf_pbkdf2() -> Kdf {
312        Kdf::PBKDF2 {
313            iterations: NonZeroU32::new(600000).expect("valid iterations"),
314        }
315    }
316
317    fn create_test_kdf_argon2id() -> Kdf {
318        Kdf::Argon2id {
319            iterations: NonZeroU32::new(3).expect("valid iterations"),
320            memory: NonZeroU32::new(64).expect("valid memory"),
321            parallelism: NonZeroU32::new(4).expect("valid parallelism"),
322        }
323    }
324
325    fn create_test_unlock_data() -> MasterkeyUnlockMethod {
326        let kdf = create_test_kdf_argon2id();
327        let salt = "[email protected]".to_string();
328        let password = "test_password".to_string();
329        MasterkeyUnlockMethod::Password {
330            password,
331            hint: None,
332            kdf,
333            salt,
334        }
335    }
336
337    fn assert_symmetric_keys_equal(
338        key_id_1: SymmetricKeyId,
339        key_id_2: SymmetricKeyId,
340        ctx: &mut KeyStoreContext<KeyIds>,
341    ) {
342        #[allow(deprecated)]
343        let key_1 = ctx
344            .dangerous_get_symmetric_key(key_id_1)
345            .expect("key 1 should exist");
346        #[allow(deprecated)]
347        let key_2 = ctx
348            .dangerous_get_symmetric_key(key_id_2)
349            .expect("key 2 should exist");
350        assert_eq!(key_1, key_2, "symmetric keys should be equal");
351    }
352
353    #[test]
354    fn test_to_authentication_and_unlock_data_pbkdf2() {
355        let store: KeyStore<KeyIds> = KeyStore::default();
356        let mut ctx = store.context_mut();
357
358        let kdf = create_test_kdf_pbkdf2();
359        let salt = "[email protected]";
360        let password = "test_password";
361
362        let user_key_id = ctx.generate_symmetric_key();
363        let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
364            .expect("derive should succeed");
365        let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
366            .expect("derive should succeed");
367
368        let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
369        assert!(result.is_ok());
370
371        let model = result.expect("should be ok");
372        assert_eq!(model.kdf_type, KdfType::PBKDF2_SHA256);
373        assert_eq!(model.kdf_iterations, 600000);
374        assert!(model.kdf_memory.is_none());
375        assert!(model.kdf_parallelism.is_none());
376        assert_eq!(model.email, Some(salt.to_string()));
377        assert!(model.master_key_authentication_hash.is_some());
378        assert!(model.master_key_encrypted_user_key.is_some());
379        assert!(model.master_password_hint.is_none());
380
381        // Verify the unlock data can decrypt the user key
382        let master_password_unlock_data = MasterPasswordUnlockData {
383            master_key_wrapped_user_key: model
384                .master_key_encrypted_user_key
385                .expect("should be present")
386                .parse()
387                .expect("should parse"),
388            kdf: kdf.clone(),
389            salt: salt.to_string(),
390        };
391        let decrypted_user_key = master_password_unlock_data
392            .unwrap_to_context(password, &mut ctx)
393            .expect("unwrap should succeed");
394        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
395    }
396
397    #[test]
398    fn test_to_authentication_and_unlock_data_argon2id() {
399        let store: KeyStore<KeyIds> = KeyStore::default();
400        let mut ctx = store.context_mut();
401
402        let kdf = create_test_kdf_argon2id();
403        let salt = "[email protected]";
404        let password = "test_password";
405
406        let user_key_id = ctx.generate_symmetric_key();
407        let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
408            .expect("derive should succeed");
409        let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
410            .expect("derive should succeed");
411
412        let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
413        assert!(result.is_ok());
414
415        let model = result.expect("should be ok");
416        assert_eq!(model.kdf_type, KdfType::Argon2id);
417        assert_eq!(model.kdf_iterations, 3);
418        assert_eq!(model.kdf_memory, Some(64));
419        assert_eq!(model.kdf_parallelism, Some(4));
420        assert_eq!(model.email, Some(salt.to_string()));
421        assert!(model.master_key_authentication_hash.is_some());
422        assert!(model.master_key_encrypted_user_key.is_some());
423
424        // Verify the unlock data can decrypt the user key
425        let master_password_unlock_data = MasterPasswordUnlockData {
426            master_key_wrapped_user_key: model
427                .master_key_encrypted_user_key
428                .expect("should be present")
429                .parse()
430                .expect("should parse"),
431            kdf: kdf.clone(),
432            salt: salt.to_string(),
433        };
434        let decrypted_user_key = master_password_unlock_data
435            .unwrap_to_context(password, &mut ctx)
436            .expect("unwrap should succeed");
437        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
438    }
439
440    #[test]
441    fn test_reencrypt_unlock_device_key_data() {
442        let store: KeyStore<KeyIds> = KeyStore::default();
443        let mut ctx = store.context_mut();
444
445        let current_user_key_id = ctx.generate_symmetric_key();
446        let new_user_key_id = ctx.generate_symmetric_key();
447        let master_key_unlock_method = create_test_unlock_data();
448
449        let (device_keyset, device_private_key) =
450            PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
451
452        let result = reencrypt_unlock(
453            ReencryptUnlockInput {
454                master_key_unlock_method,
455                trusted_devices: vec![device_keyset],
456                webauthn_credentials: vec![],
457                trusted_organization_keys: vec![],
458                trusted_emergency_access_keys: vec![],
459            },
460            current_user_key_id,
461            new_user_key_id,
462            &mut ctx,
463        );
464
465        let unlock_data = result.expect("should be ok");
466
467        let device_unlock = unlock_data
468            .device_key_unlock_data
469            .as_ref()
470            .expect("should be present")
471            .first()
472            .expect("should have at least one");
473        let decrypted_user_key = device_unlock
474            .encrypted_user_key
475            .parse::<UnsignedSharedKey>()
476            .expect("should parse")
477            .decapsulate(device_private_key, &mut ctx)
478            .expect("unwrap should succeed");
479        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
480    }
481
482    #[test]
483    fn test_reencrypt_unlock_webauthn_prf_credential_data() {
484        let store: KeyStore<KeyIds> = KeyStore::default();
485        let mut ctx = store.context_mut();
486
487        let current_user_key_id = ctx.generate_symmetric_key();
488        let new_user_key_id = ctx.generate_symmetric_key();
489        let master_key_unlock_method = create_test_unlock_data();
490
491        let (credential_keyset, credential_private_key) =
492            PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
493
494        let result = reencrypt_unlock(
495            ReencryptUnlockInput {
496                master_key_unlock_method,
497                trusted_devices: vec![],
498                webauthn_credentials: vec![credential_keyset],
499                trusted_organization_keys: vec![],
500                trusted_emergency_access_keys: vec![],
501            },
502            current_user_key_id,
503            new_user_key_id,
504            &mut ctx,
505        );
506
507        let unlock_data = result.expect("should be ok");
508
509        // Ensure it decrypts to the correct key after rotation
510        let credential_unlock = unlock_data
511            .passkey_unlock_data
512            .as_ref()
513            .expect("should be present")
514            .first()
515            .expect("should have at least one");
516        let decrypted_user_key = credential_unlock
517            .encrypted_user_key
518            .parse::<UnsignedSharedKey>()
519            .expect("should parse")
520            .decapsulate(credential_private_key, &mut ctx)
521            .expect("unwrap should succeed");
522        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
523    }
524
525    #[test]
526    fn test_reencrypt_unlock_emergency_access_data() {
527        let store: KeyStore<KeyIds> = KeyStore::default();
528        let mut ctx = store.context_mut();
529
530        let current_user_key_id = ctx.generate_symmetric_key();
531        let new_user_key_id = ctx.generate_symmetric_key();
532        let master_key_unlock_method = create_test_unlock_data();
533
534        let organization_private_key =
535            ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
536        let emergency_access = V1EmergencyAccessMembership {
537            id: Uuid::new_v4(),
538            name: "Test User".to_string(),
539            public_key: ctx
540                .get_public_key(organization_private_key)
541                .expect("key exists"),
542        };
543
544        let result = reencrypt_unlock(
545            ReencryptUnlockInput {
546                master_key_unlock_method,
547                trusted_devices: vec![],
548                webauthn_credentials: vec![],
549                trusted_organization_keys: vec![],
550                trusted_emergency_access_keys: vec![emergency_access],
551            },
552            current_user_key_id,
553            new_user_key_id,
554            &mut ctx,
555        );
556
557        let unlock_data = result.expect("should be ok");
558
559        // Ensure it decrypts to the correct key after rotation
560        let emergency_access_unlock = unlock_data
561            .emergency_access_unlock_data
562            .as_ref()
563            .expect("should be present")
564            .first()
565            .expect("should have at least one");
566        let decrypted_user_key = emergency_access_unlock
567            .key_encrypted
568            .as_ref()
569            .map(|k| k.parse::<UnsignedSharedKey>())
570            .expect("should be present")
571            .expect("should parse")
572            .decapsulate(organization_private_key, &mut ctx)
573            .expect("unwrap should succeed");
574        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
575    }
576
577    #[test]
578    fn test_reencrypt_unlock_organization_membership_data() {
579        let store: KeyStore<KeyIds> = KeyStore::default();
580        let mut ctx = store.context_mut();
581
582        let kdf = create_test_kdf_argon2id();
583        let salt = "[email protected]".to_string();
584        let password = "test_password".to_string();
585
586        let current_user_key_id = ctx.generate_symmetric_key();
587        let new_user_key_id = ctx.generate_symmetric_key();
588
589        let org_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
590        let org_membership = V1OrganizationMembership {
591            organization_id: Uuid::new_v4(),
592            name: "Test Org".to_string(),
593            public_key: ctx.get_public_key(org_key).expect("key exists"),
594        };
595
596        // Note: Replace this with [`MasterkeyUnlockMethod::None`] when implemented.
597        let master_key_unlock_method = MasterkeyUnlockMethod::Password {
598            password: password.clone(),
599            hint: None,
600            kdf: kdf.clone(),
601            salt: salt.clone(),
602        };
603
604        let result = reencrypt_unlock(
605            ReencryptUnlockInput {
606                master_key_unlock_method,
607                trusted_devices: vec![],
608                webauthn_credentials: vec![],
609                trusted_organization_keys: vec![org_membership],
610                trusted_emergency_access_keys: vec![],
611            },
612            current_user_key_id,
613            new_user_key_id,
614            &mut ctx,
615        );
616
617        let unlock_data = result.expect("should be ok");
618
619        let org_membership_unlock = unlock_data
620            .organization_account_recovery_unlock_data
621            .as_ref()
622            .expect("should be present")
623            .first()
624            .expect("should have at least one");
625        let decrypted_user_key = org_membership_unlock
626            .reset_password_key
627            .as_ref()
628            .map(|k| k.parse::<UnsignedSharedKey>())
629            .expect("should be present")
630            .expect("should parse")
631            .decapsulate(org_key, &mut ctx)
632            .expect("unwrap should succeed");
633        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
634    }
635}