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                    r#type: models::EmergencyAccessType::Takeover,
194                    wait_time_days: 0,
195                    id: ea.id,
196                    key_encrypted: reencrypted_key.to_string().into(),
197                }),
198                Err(_) => Err(ReencryptError::KeySharingError),
199            }
200        })
201        .collect()
202}
203
204/// Re-encrypt organization membership keys for the new user key.
205fn reencrypt_organization_memberships(
206    trusted_organization_keys: Vec<V1OrganizationMembership>,
207    new_user_key_id: SymmetricKeyId,
208    ctx: &mut KeyStoreContext<KeyIds>,
209) -> Result<Vec<ResetPasswordWithOrgIdRequestModel>, ReencryptError> {
210    trusted_organization_keys
211        .into_iter()
212        .map(|org_membership| {
213            let _span =
214                debug_span!("reencrypt_organization_key", organization = ?org_membership.organization_id)
215                    .entered();
216            // Share the key to the organization. Note: No sender authentication
217            // and the passed in public-key must be verified/trusted.
218            match UnsignedSharedKey::encapsulate(new_user_key_id, &org_membership.public_key, ctx) {
219                Ok(reencrypted_key) => Ok(ResetPasswordWithOrgIdRequestModel {
220                    reset_password_key: Some(reencrypted_key.to_string()),
221                    master_password_hash: None,
222                    organization_id: org_membership.organization_id,
223                }),
224                Err(_) => Err(ReencryptError::KeySharingError),
225            }
226        })
227        .collect()
228}
229
230fn reencrypt_userkey_for_masterpassword_unlock(
231    password: String,
232    hint: Option<String>,
233    kdf: Kdf,
234    salt: String,
235    new_user_key_id: SymmetricKeyId,
236    ctx: &mut KeyStoreContext<KeyIds>,
237) -> Result<MasterPasswordUnlockAndAuthenticationDataModel, ReencryptError> {
238    let _span = debug_span!("derive_master_password_unlock_data").entered();
239    let unlock_data =
240        MasterPasswordUnlockData::derive(&password, &kdf, &salt, new_user_key_id, ctx)
241            .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
242    let authentication_data = MasterPasswordAuthenticationData::derive(&password, &kdf, &salt)
243        .map_err(|_| ReencryptError::MasterPasswordDerivation)?;
244    to_authentication_and_unlock_data(unlock_data, authentication_data, hint)
245        .map_err(|_| ReencryptError::MasterPasswordDerivation)
246}
247
248#[derive(Debug)]
249struct ParsingError;
250
251fn to_authentication_and_unlock_data(
252    master_password_unlock_data: MasterPasswordUnlockData,
253    master_password_authentication_data: MasterPasswordAuthenticationData,
254    hint: Option<String>,
255) -> Result<MasterPasswordUnlockAndAuthenticationDataModel, ParsingError> {
256    let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) =
257        match master_password_unlock_data.kdf {
258            bitwarden_crypto::Kdf::PBKDF2 { iterations } => {
259                (models::KdfType::PBKDF2_SHA256, iterations, None, None)
260            }
261            bitwarden_crypto::Kdf::Argon2id {
262                iterations,
263                memory,
264                parallelism,
265            } => (
266                models::KdfType::Argon2id,
267                iterations,
268                Some(memory),
269                Some(parallelism),
270            ),
271        };
272    Ok(MasterPasswordUnlockAndAuthenticationDataModel {
273        kdf_type,
274        kdf_iterations: kdf_iterations.get().try_into().map_err(|_| ParsingError)?,
275        kdf_memory: kdf_memory
276            .map(|m| m.get().try_into().map_err(|_| ParsingError))
277            .transpose()?,
278        kdf_parallelism: kdf_parallelism
279            .map(|p| p.get().try_into().map_err(|_| ParsingError))
280            .transpose()?,
281        email: Some(master_password_unlock_data.salt.clone()),
282        master_key_authentication_hash: Some(
283            master_password_authentication_data
284                .master_password_authentication_hash
285                .to_string(),
286        ),
287        master_key_encrypted_user_key: Some(
288            master_password_unlock_data
289                .master_key_wrapped_user_key
290                .to_string(),
291        ),
292        master_password_hint: hint,
293    })
294}
295
296#[cfg(test)]
297mod tests {
298    use std::num::NonZeroU32;
299
300    use bitwarden_api_api::models::KdfType;
301    use bitwarden_core::key_management::KeyIds;
302    use bitwarden_crypto::{Kdf, KeyStore, PublicKeyEncryptionAlgorithm, UnsignedSharedKey};
303    use uuid::Uuid;
304
305    use super::*;
306    use crate::key_rotation::partial_rotateable_keyset::PartialRotateableKeyset;
307
308    fn create_test_kdf_pbkdf2() -> Kdf {
309        Kdf::PBKDF2 {
310            iterations: NonZeroU32::new(600000).expect("valid iterations"),
311        }
312    }
313
314    fn create_test_kdf_argon2id() -> Kdf {
315        Kdf::Argon2id {
316            iterations: NonZeroU32::new(3).expect("valid iterations"),
317            memory: NonZeroU32::new(64).expect("valid memory"),
318            parallelism: NonZeroU32::new(4).expect("valid parallelism"),
319        }
320    }
321
322    fn create_test_unlock_data() -> MasterkeyUnlockMethod {
323        let kdf = create_test_kdf_argon2id();
324        let salt = "[email protected]".to_string();
325        let password = "test_password".to_string();
326        MasterkeyUnlockMethod::Password {
327            password,
328            hint: None,
329            kdf,
330            salt,
331        }
332    }
333
334    fn assert_symmetric_keys_equal(
335        key_id_1: SymmetricKeyId,
336        key_id_2: SymmetricKeyId,
337        ctx: &mut KeyStoreContext<KeyIds>,
338    ) {
339        #[allow(deprecated)]
340        let key_1 = ctx
341            .dangerous_get_symmetric_key(key_id_1)
342            .expect("key 1 should exist");
343        #[allow(deprecated)]
344        let key_2 = ctx
345            .dangerous_get_symmetric_key(key_id_2)
346            .expect("key 2 should exist");
347        assert_eq!(key_1, key_2, "symmetric keys should be equal");
348    }
349
350    #[test]
351    fn test_to_authentication_and_unlock_data_pbkdf2() {
352        let store: KeyStore<KeyIds> = KeyStore::default();
353        let mut ctx = store.context_mut();
354
355        let kdf = create_test_kdf_pbkdf2();
356        let salt = "[email protected]";
357        let password = "test_password";
358
359        let user_key_id = ctx.generate_symmetric_key();
360        let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
361            .expect("derive should succeed");
362        let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
363            .expect("derive should succeed");
364
365        let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
366        assert!(result.is_ok());
367
368        let model = result.expect("should be ok");
369        assert_eq!(model.kdf_type, KdfType::PBKDF2_SHA256);
370        assert_eq!(model.kdf_iterations, 600000);
371        assert!(model.kdf_memory.is_none());
372        assert!(model.kdf_parallelism.is_none());
373        assert_eq!(model.email, Some(salt.to_string()));
374        assert!(model.master_key_authentication_hash.is_some());
375        assert!(model.master_key_encrypted_user_key.is_some());
376        assert!(model.master_password_hint.is_none());
377
378        // Verify the unlock data can decrypt the user key
379        let master_password_unlock_data = MasterPasswordUnlockData {
380            master_key_wrapped_user_key: model
381                .master_key_encrypted_user_key
382                .expect("should be present")
383                .parse()
384                .expect("should parse"),
385            kdf: kdf.clone(),
386            salt: salt.to_string(),
387        };
388        let decrypted_user_key = master_password_unlock_data
389            .unwrap_to_context(password, &mut ctx)
390            .expect("unwrap should succeed");
391        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
392    }
393
394    #[test]
395    fn test_to_authentication_and_unlock_data_argon2id() {
396        let store: KeyStore<KeyIds> = KeyStore::default();
397        let mut ctx = store.context_mut();
398
399        let kdf = create_test_kdf_argon2id();
400        let salt = "[email protected]";
401        let password = "test_password";
402
403        let user_key_id = ctx.generate_symmetric_key();
404        let unlock_data = MasterPasswordUnlockData::derive(password, &kdf, salt, user_key_id, &ctx)
405            .expect("derive should succeed");
406        let auth_data = MasterPasswordAuthenticationData::derive(password, &kdf, salt)
407            .expect("derive should succeed");
408
409        let result = to_authentication_and_unlock_data(unlock_data, auth_data, None);
410        assert!(result.is_ok());
411
412        let model = result.expect("should be ok");
413        assert_eq!(model.kdf_type, KdfType::Argon2id);
414        assert_eq!(model.kdf_iterations, 3);
415        assert_eq!(model.kdf_memory, Some(64));
416        assert_eq!(model.kdf_parallelism, Some(4));
417        assert_eq!(model.email, Some(salt.to_string()));
418        assert!(model.master_key_authentication_hash.is_some());
419        assert!(model.master_key_encrypted_user_key.is_some());
420
421        // Verify the unlock data can decrypt the user key
422        let master_password_unlock_data = MasterPasswordUnlockData {
423            master_key_wrapped_user_key: model
424                .master_key_encrypted_user_key
425                .expect("should be present")
426                .parse()
427                .expect("should parse"),
428            kdf: kdf.clone(),
429            salt: salt.to_string(),
430        };
431        let decrypted_user_key = master_password_unlock_data
432            .unwrap_to_context(password, &mut ctx)
433            .expect("unwrap should succeed");
434        assert_symmetric_keys_equal(user_key_id, decrypted_user_key, &mut ctx);
435    }
436
437    #[test]
438    fn test_reencrypt_unlock_device_key_data() {
439        let store: KeyStore<KeyIds> = KeyStore::default();
440        let mut ctx = store.context_mut();
441
442        let current_user_key_id = ctx.generate_symmetric_key();
443        let new_user_key_id = ctx.generate_symmetric_key();
444        let master_key_unlock_method = create_test_unlock_data();
445
446        let (device_keyset, device_private_key) =
447            PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
448
449        let result = reencrypt_unlock(
450            ReencryptUnlockInput {
451                master_key_unlock_method,
452                trusted_devices: vec![device_keyset],
453                webauthn_credentials: vec![],
454                trusted_organization_keys: vec![],
455                trusted_emergency_access_keys: vec![],
456            },
457            current_user_key_id,
458            new_user_key_id,
459            &mut ctx,
460        );
461
462        let unlock_data = result.expect("should be ok");
463
464        let device_unlock = unlock_data
465            .device_key_unlock_data
466            .as_ref()
467            .expect("should be present")
468            .first()
469            .expect("should have at least one");
470        let decrypted_user_key = device_unlock
471            .encrypted_user_key
472            .parse::<UnsignedSharedKey>()
473            .expect("should parse")
474            .decapsulate(device_private_key, &mut ctx)
475            .expect("unwrap should succeed");
476        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
477    }
478
479    #[test]
480    fn test_reencrypt_unlock_webauthn_prf_credential_data() {
481        let store: KeyStore<KeyIds> = KeyStore::default();
482        let mut ctx = store.context_mut();
483
484        let current_user_key_id = ctx.generate_symmetric_key();
485        let new_user_key_id = ctx.generate_symmetric_key();
486        let master_key_unlock_method = create_test_unlock_data();
487
488        let (credential_keyset, credential_private_key) =
489            PartialRotateableKeyset::make_test_keyset(current_user_key_id, &mut ctx);
490
491        let result = reencrypt_unlock(
492            ReencryptUnlockInput {
493                master_key_unlock_method,
494                trusted_devices: vec![],
495                webauthn_credentials: vec![credential_keyset],
496                trusted_organization_keys: vec![],
497                trusted_emergency_access_keys: vec![],
498            },
499            current_user_key_id,
500            new_user_key_id,
501            &mut ctx,
502        );
503
504        let unlock_data = result.expect("should be ok");
505
506        // Ensure it decrypts to the correct key after rotation
507        let credential_unlock = unlock_data
508            .passkey_unlock_data
509            .as_ref()
510            .expect("should be present")
511            .first()
512            .expect("should have at least one");
513        let decrypted_user_key = credential_unlock
514            .encrypted_user_key
515            .parse::<UnsignedSharedKey>()
516            .expect("should parse")
517            .decapsulate(credential_private_key, &mut ctx)
518            .expect("unwrap should succeed");
519        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
520    }
521
522    #[test]
523    fn test_reencrypt_unlock_emergency_access_data() {
524        let store: KeyStore<KeyIds> = KeyStore::default();
525        let mut ctx = store.context_mut();
526
527        let current_user_key_id = ctx.generate_symmetric_key();
528        let new_user_key_id = ctx.generate_symmetric_key();
529        let master_key_unlock_method = create_test_unlock_data();
530
531        let organization_private_key =
532            ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
533        let emergency_access = V1EmergencyAccessMembership {
534            id: Uuid::new_v4(),
535            name: "Test User".to_string(),
536            public_key: ctx
537                .get_public_key(organization_private_key)
538                .expect("key exists"),
539        };
540
541        let result = reencrypt_unlock(
542            ReencryptUnlockInput {
543                master_key_unlock_method,
544                trusted_devices: vec![],
545                webauthn_credentials: vec![],
546                trusted_organization_keys: vec![],
547                trusted_emergency_access_keys: vec![emergency_access],
548            },
549            current_user_key_id,
550            new_user_key_id,
551            &mut ctx,
552        );
553
554        let unlock_data = result.expect("should be ok");
555
556        // Ensure it decrypts to the correct key after rotation
557        let emergency_access_unlock = unlock_data
558            .emergency_access_unlock_data
559            .as_ref()
560            .expect("should be present")
561            .first()
562            .expect("should have at least one");
563        let decrypted_user_key = emergency_access_unlock
564            .key_encrypted
565            .as_ref()
566            .map(|k| k.parse::<UnsignedSharedKey>())
567            .expect("should be present")
568            .expect("should parse")
569            .decapsulate(organization_private_key, &mut ctx)
570            .expect("unwrap should succeed");
571        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
572    }
573
574    #[test]
575    fn test_reencrypt_unlock_organization_membership_data() {
576        let store: KeyStore<KeyIds> = KeyStore::default();
577        let mut ctx = store.context_mut();
578
579        let kdf = create_test_kdf_argon2id();
580        let salt = "[email protected]".to_string();
581        let password = "test_password".to_string();
582
583        let current_user_key_id = ctx.generate_symmetric_key();
584        let new_user_key_id = ctx.generate_symmetric_key();
585
586        let org_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
587        let org_membership = V1OrganizationMembership {
588            organization_id: Uuid::new_v4(),
589            name: "Test Org".to_string(),
590            public_key: ctx.get_public_key(org_key).expect("key exists"),
591        };
592
593        // Note: Replace this with [`MasterkeyUnlockMethod::None`] when implemented.
594        let master_key_unlock_method = MasterkeyUnlockMethod::Password {
595            password: password.clone(),
596            hint: None,
597            kdf: kdf.clone(),
598            salt: salt.clone(),
599        };
600
601        let result = reencrypt_unlock(
602            ReencryptUnlockInput {
603                master_key_unlock_method,
604                trusted_devices: vec![],
605                webauthn_credentials: vec![],
606                trusted_organization_keys: vec![org_membership],
607                trusted_emergency_access_keys: vec![],
608            },
609            current_user_key_id,
610            new_user_key_id,
611            &mut ctx,
612        );
613
614        let unlock_data = result.expect("should be ok");
615
616        let org_membership_unlock = unlock_data
617            .organization_account_recovery_unlock_data
618            .as_ref()
619            .expect("should be present")
620            .first()
621            .expect("should have at least one");
622        let decrypted_user_key = org_membership_unlock
623            .reset_password_key
624            .as_ref()
625            .map(|k| k.parse::<UnsignedSharedKey>())
626            .expect("should be present")
627            .expect("should parse")
628            .decapsulate(org_key, &mut ctx)
629            .expect("unwrap should succeed");
630        assert_symmetric_keys_equal(new_user_key_id, decrypted_user_key, &mut ctx);
631    }
632}