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    })
296}
297
298#[cfg(test)]
299mod tests {
300    use std::num::NonZeroU32;
301
302    use bitwarden_api_api::models::KdfType;
303    use bitwarden_core::key_management::KeyIds;
304    use bitwarden_crypto::{Kdf, KeyStore, PublicKeyEncryptionAlgorithm, UnsignedSharedKey};
305    use uuid::Uuid;
306
307    use super::*;
308    use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
309
310    fn create_test_kdf_pbkdf2() -> Kdf {
311        Kdf::PBKDF2 {
312            iterations: NonZeroU32::new(600000).expect("valid iterations"),
313        }
314    }
315
316    fn create_test_kdf_argon2id() -> Kdf {
317        Kdf::Argon2id {
318            iterations: NonZeroU32::new(3).expect("valid iterations"),
319            memory: NonZeroU32::new(64).expect("valid memory"),
320            parallelism: NonZeroU32::new(4).expect("valid parallelism"),
321        }
322    }
323
324    fn create_test_unlock_data() -> MasterkeyUnlockMethod {
325        let kdf = create_test_kdf_argon2id();
326        let salt = "[email protected]".to_string();
327        let password = "test_password".to_string();
328        MasterkeyUnlockMethod::Password {
329            password,
330            hint: None,
331            kdf,
332            salt,
333        }
334    }
335
336    fn assert_symmetric_keys_equal(
337        key_id_1: SymmetricKeyId,
338        key_id_2: SymmetricKeyId,
339        ctx: &mut KeyStoreContext<KeyIds>,
340    ) {
341        #[allow(deprecated)]
342        let key_1 = ctx
343            .dangerous_get_symmetric_key(key_id_1)
344            .expect("key 1 should exist");
345        #[allow(deprecated)]
346        let key_2 = ctx
347            .dangerous_get_symmetric_key(key_id_2)
348            .expect("key 2 should exist");
349        assert_eq!(key_1, key_2, "symmetric keys should be equal");
350    }
351
352    #[test]
353    fn test_to_authentication_and_unlock_data_pbkdf2() {
354        let store: KeyStore<KeyIds> = KeyStore::default();
355        let mut ctx = store.context_mut();
356
357        let kdf = create_test_kdf_pbkdf2();
358        let salt = "[email protected]";
359        let password = "test_password";
360
361        let user_key_id = ctx.generate_symmetric_key();
362        let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
363            .expect("derive should succeed");
364        let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
365            .expect("derive should succeed");
366
367        let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
368        assert!(result.is_ok());
369
370        let model = result.expect("should be ok");
371        assert_eq!(model.kdf_type, KdfType::PBKDF2_SHA256);
372        assert_eq!(model.kdf_iterations, 600000);
373        assert!(model.kdf_memory.is_none());
374        assert!(model.kdf_parallelism.is_none());
375        assert_eq!(model.email, Some(salt.to_string()));
376        assert!(model.master_key_authentication_hash.is_some());
377        assert!(model.master_key_encrypted_user_key.is_some());
378        assert!(model.master_password_hint.is_none());
379
380        // Verify the unlock data can decrypt the user key
381        let master_password_unlock_data = MasterPasswordUnlockData {
382            master_key_wrapped_user_key: model
383                .master_key_encrypted_user_key
384                .expect("should be present")
385                .parse()
386                .expect("should parse"),
387            kdf: kdf.clone(),
388            salt: salt.to_string(),
389        };
390        let decrypted_user_key = master_password_unlock_data
391            .unwrap_to_context(password, &mut ctx)
392            .expect("unwrap should succeed");
393        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
394    }
395
396    #[test]
397    fn test_to_authentication_and_unlock_data_argon2id() {
398        let store: KeyStore<KeyIds> = KeyStore::default();
399        let mut ctx = store.context_mut();
400
401        let kdf = create_test_kdf_argon2id();
402        let salt = "[email protected]";
403        let password = "test_password";
404
405        let user_key_id = ctx.generate_symmetric_key();
406        let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
407            .expect("derive should succeed");
408        let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
409            .expect("derive should succeed");
410
411        let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
412        assert!(result.is_ok());
413
414        let model = result.expect("should be ok");
415        assert_eq!(model.kdf_type, KdfType::Argon2id);
416        assert_eq!(model.kdf_iterations, 3);
417        assert_eq!(model.kdf_memory, Some(64));
418        assert_eq!(model.kdf_parallelism, Some(4));
419        assert_eq!(model.email, Some(salt.to_string()));
420        assert!(model.master_key_authentication_hash.is_some());
421        assert!(model.master_key_encrypted_user_key.is_some());
422
423        // Verify the unlock data can decrypt the user key
424        let master_password_unlock_data = MasterPasswordUnlockData {
425            master_key_wrapped_user_key: model
426                .master_key_encrypted_user_key
427                .expect("should be present")
428                .parse()
429                .expect("should parse"),
430            kdf: kdf.clone(),
431            salt: salt.to_string(),
432        };
433        let decrypted_user_key = master_password_unlock_data
434            .unwrap_to_context(password, &mut ctx)
435            .expect("unwrap should succeed");
436        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
437    }
438
439    #[test]
440    fn test_reencrypt_unlock_device_key_data() {
441        let store: KeyStore<KeyIds> = KeyStore::default();
442        let mut ctx = store.context_mut();
443
444        let current_user_key_id = ctx.generate_symmetric_key();
445        let new_user_key_id = ctx.generate_symmetric_key();
446        let master_key_unlock_method = create_test_unlock_data();
447
448        let (device_keyset, device_private_key) =
449            PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
450
451        let result = reencrypt_unlock(
452            ReencryptUnlockInput {
453                master_key_unlock_method,
454                trusted_devices: vec![device_keyset],
455                webauthn_credentials: vec![],
456                trusted_organization_keys: vec![],
457                trusted_emergency_access_keys: vec![],
458            },
459            current_user_key_id,
460            new_user_key_id,
461            &mut ctx,
462        );
463
464        let unlock_data = result.expect("should be ok");
465
466        let device_unlock = unlock_data
467            .device_key_unlock_data
468            .as_ref()
469            .expect("should be present")
470            .first()
471            .expect("should have at least one");
472        let decrypted_user_key = device_unlock
473            .encrypted_user_key
474            .parse::<UnsignedSharedKey>()
475            .expect("should parse")
476            .decapsulate(device_private_key, &mut ctx)
477            .expect("unwrap should succeed");
478        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
479    }
480
481    #[test]
482    fn test_reencrypt_unlock_webauthn_prf_credential_data() {
483        let store: KeyStore<KeyIds> = KeyStore::default();
484        let mut ctx = store.context_mut();
485
486        let current_user_key_id = ctx.generate_symmetric_key();
487        let new_user_key_id = ctx.generate_symmetric_key();
488        let master_key_unlock_method = create_test_unlock_data();
489
490        let (credential_keyset, credential_private_key) =
491            PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
492
493        let result = reencrypt_unlock(
494            ReencryptUnlockInput {
495                master_key_unlock_method,
496                trusted_devices: vec![],
497                webauthn_credentials: vec![credential_keyset],
498                trusted_organization_keys: vec![],
499                trusted_emergency_access_keys: vec![],
500            },
501            current_user_key_id,
502            new_user_key_id,
503            &mut ctx,
504        );
505
506        let unlock_data = result.expect("should be ok");
507
508        // Ensure it decrypts to the correct key after rotation
509        let credential_unlock = unlock_data
510            .passkey_unlock_data
511            .as_ref()
512            .expect("should be present")
513            .first()
514            .expect("should have at least one");
515        let decrypted_user_key = credential_unlock
516            .encrypted_user_key
517            .parse::<UnsignedSharedKey>()
518            .expect("should parse")
519            .decapsulate(credential_private_key, &mut ctx)
520            .expect("unwrap should succeed");
521        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
522    }
523
524    #[test]
525    fn test_reencrypt_unlock_emergency_access_data() {
526        let store: KeyStore<KeyIds> = KeyStore::default();
527        let mut ctx = store.context_mut();
528
529        let current_user_key_id = ctx.generate_symmetric_key();
530        let new_user_key_id = ctx.generate_symmetric_key();
531        let master_key_unlock_method = create_test_unlock_data();
532
533        let organization_private_key =
534            ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
535        let emergency_access = V1EmergencyAccessMembership {
536            id: Uuid::new_v4(),
537            name: "Test User".to_string(),
538            public_key: ctx
539                .get_public_key(organization_private_key)
540                .expect("key exists"),
541        };
542
543        let result = reencrypt_unlock(
544            ReencryptUnlockInput {
545                master_key_unlock_method,
546                trusted_devices: vec![],
547                webauthn_credentials: vec![],
548                trusted_organization_keys: vec![],
549                trusted_emergency_access_keys: vec![emergency_access],
550            },
551            current_user_key_id,
552            new_user_key_id,
553            &mut ctx,
554        );
555
556        let unlock_data = result.expect("should be ok");
557
558        // Ensure it decrypts to the correct key after rotation
559        let emergency_access_unlock = unlock_data
560            .emergency_access_unlock_data
561            .as_ref()
562            .expect("should be present")
563            .first()
564            .expect("should have at least one");
565        let decrypted_user_key = emergency_access_unlock
566            .key_encrypted
567            .as_ref()
568            .map(|k| k.parse::<UnsignedSharedKey>())
569            .expect("should be present")
570            .expect("should parse")
571            .decapsulate(organization_private_key, &mut ctx)
572            .expect("unwrap should succeed");
573        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
574    }
575
576    #[test]
577    fn test_reencrypt_unlock_organization_membership_data() {
578        let store: KeyStore<KeyIds> = KeyStore::default();
579        let mut ctx = store.context_mut();
580
581        let kdf = create_test_kdf_argon2id();
582        let salt = "[email protected]".to_string();
583        let password = "test_password".to_string();
584
585        let current_user_key_id = ctx.generate_symmetric_key();
586        let new_user_key_id = ctx.generate_symmetric_key();
587
588        let org_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
589        let org_membership = V1OrganizationMembership {
590            organization_id: Uuid::new_v4(),
591            name: "Test Org".to_string(),
592            public_key: ctx.get_public_key(org_key).expect("key exists"),
593        };
594
595        // Note: Replace this with [`MasterkeyUnlockMethod::None`] when implemented.
596        let master_key_unlock_method = MasterkeyUnlockMethod::Password {
597            password: password.clone(),
598            hint: None,
599            kdf: kdf.clone(),
600            salt: salt.clone(),
601        };
602
603        let result = reencrypt_unlock(
604            ReencryptUnlockInput {
605                master_key_unlock_method,
606                trusted_devices: vec![],
607                webauthn_credentials: vec![],
608                trusted_organization_keys: vec![org_membership],
609                trusted_emergency_access_keys: vec![],
610            },
611            current_user_key_id,
612            new_user_key_id,
613            &mut ctx,
614        );
615
616        let unlock_data = result.expect("should be ok");
617
618        let org_membership_unlock = unlock_data
619            .organization_account_recovery_unlock_data
620            .as_ref()
621            .expect("should be present")
622            .first()
623            .expect("should have at least one");
624        let decrypted_user_key = org_membership_unlock
625            .reset_password_key
626            .as_ref()
627            .map(|k| k.parse::<UnsignedSharedKey>())
628            .expect("should be present")
629            .expect("should parse")
630            .decapsulate(org_key, &mut ctx)
631            .expect("unwrap should succeed");
632        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
633    }
634}