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