Skip to main content

bitwarden_user_crypto_management/key_rotation/
rotation_context.rs

1use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
2use bitwarden_crypto::{KeyStoreContext, PublicKey, SymmetricKeyAlgorithm};
3use tracing::{debug, info, warn};
4
5use super::{
6    RotateUserKeysError,
7    sync::SyncedAccountData,
8    unlock::{V1EmergencyAccessMembership, V1OrganizationMembership},
9};
10
11#[derive(Debug)]
12struct UntrustedKeyError;
13
14fn filter_trusted_organization(
15    org: &[V1OrganizationMembership],
16    trusted_orgs: &[PublicKey],
17) -> Result<Vec<V1OrganizationMembership>, UntrustedKeyError> {
18    org.iter()
19        .map(|o| {
20            let is_trusted = trusted_orgs.iter().any(|tk| tk == &o.public_key);
21            if !is_trusted {
22                warn!(
23                    "Aborting because untrusted organization detected with id={}",
24                    o.organization_id
25                );
26                Err(UntrustedKeyError)
27            } else {
28                Ok(o.clone())
29            }
30        })
31        .collect::<Result<Vec<V1OrganizationMembership>, UntrustedKeyError>>()
32}
33
34fn filter_trusted_emergency_access(
35    ea: &[V1EmergencyAccessMembership],
36    trusted_emergency_access_user_public_keys: &[PublicKey],
37) -> Result<Vec<V1EmergencyAccessMembership>, UntrustedKeyError> {
38    ea.iter()
39        .map(|e| {
40            let is_trusted = trusted_emergency_access_user_public_keys
41                .iter()
42                .any(|tk| tk == &e.public_key);
43            if !is_trusted {
44                warn!(
45                    "Aborting because untrusted emergency access membership detected with id={}",
46                    e.id
47                );
48                Err(UntrustedKeyError)
49            } else {
50                Ok(e.to_owned())
51            }
52        })
53        .collect::<Result<Vec<V1EmergencyAccessMembership>, UntrustedKeyError>>()
54}
55
56pub(super) struct RotationContext {
57    pub(super) v1_organization_memberships: Vec<V1OrganizationMembership>,
58    pub(super) v1_emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
59    pub(super) current_user_key_id: SymmetricKeySlotId,
60    pub(super) new_user_key_id: SymmetricKeySlotId,
61}
62
63pub(super) fn make_rotation_context(
64    sync: &SyncedAccountData,
65    trusted_organization_public_keys: &[PublicKey],
66    trusted_emergency_access_public_keys: &[PublicKey],
67    ctx: &mut KeyStoreContext<KeySlotIds>,
68) -> Result<RotationContext, RotateUserKeysError> {
69    let v1_organization_memberships = filter_trusted_organization(
70        sync.organization_memberships.as_slice(),
71        trusted_organization_public_keys,
72    )
73    .map_err(|_| RotateUserKeysError::UntrustedKey)?;
74
75    let v1_emergency_access_memberships = filter_trusted_emergency_access(
76        sync.emergency_access_memberships.as_slice(),
77        trusted_emergency_access_public_keys,
78    )
79    .map_err(|_| RotateUserKeysError::UntrustedKey)?;
80
81    info!(
82        "Existing user cryptographic version {:?}",
83        sync.wrapped_account_cryptographic_state
84    );
85    let current_user_key_id = SymmetricKeySlotId::User;
86
87    debug!("Generating new xchacha20-poly1305 user key for key rotation");
88    let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
89
90    Ok(RotationContext {
91        v1_organization_memberships,
92        v1_emergency_access_memberships,
93        current_user_key_id,
94        new_user_key_id,
95    })
96}
97
98#[cfg(test)]
99mod tests {
100    use bitwarden_core::key_management::{
101        KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId,
102        account_cryptographic_state::WrappedAccountCryptographicState,
103    };
104    use bitwarden_crypto::{
105        KeyStore, KeyStoreContext, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm,
106    };
107    use uuid::Uuid;
108
109    use super::{
110        super::{
111            sync::SyncedAccountData,
112            unlock::{V1EmergencyAccessMembership, V1OrganizationMembership},
113        },
114        RotateUserKeysError, filter_trusted_emergency_access, filter_trusted_organization,
115        make_rotation_context,
116    };
117
118    fn make_org_membership(
119        ctx: &mut KeyStoreContext<KeySlotIds>,
120    ) -> (V1OrganizationMembership, PrivateKeySlotId) {
121        let org_private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
122        (
123            V1OrganizationMembership {
124                organization_id: Uuid::new_v4(),
125                name: "Test Org".to_string(),
126                public_key: ctx.get_public_key(org_private_key).expect("key exists"),
127            },
128            org_private_key,
129        )
130    }
131
132    fn make_ea_membership(
133        ctx: &mut KeyStoreContext<KeySlotIds>,
134    ) -> (V1EmergencyAccessMembership, PrivateKeySlotId) {
135        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
136        (
137            V1EmergencyAccessMembership {
138                id: Uuid::new_v4(),
139                name: "Test User".to_string(),
140                public_key: ctx.get_public_key(private_key).expect("key exists"),
141                grantee_id: Uuid::new_v4(),
142            },
143            private_key,
144        )
145    }
146
147    fn assert_org_membership_eq(
148        actual: &V1OrganizationMembership,
149        expected: &V1OrganizationMembership,
150    ) {
151        assert_eq!(actual.organization_id, expected.organization_id);
152        assert_eq!(actual.name, expected.name);
153        assert_eq!(actual.public_key, expected.public_key);
154    }
155
156    fn assert_ea_membership_eq(
157        actual: &V1EmergencyAccessMembership,
158        expected: &V1EmergencyAccessMembership,
159    ) {
160        assert_eq!(actual.id, expected.id);
161        assert_eq!(actual.name, expected.name);
162        assert_eq!(actual.grantee_id, expected.grantee_id);
163        assert_eq!(actual.public_key, expected.public_key);
164    }
165
166    fn make_test_sync(
167        org_memberships: Vec<V1OrganizationMembership>,
168        ea_memberships: Vec<V1EmergencyAccessMembership>,
169        ctx: &mut KeyStoreContext<KeySlotIds>,
170    ) -> SyncedAccountData {
171        let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
172        let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
173        let wrapped_private_key = ctx.wrap_private_key(user_key, private_key).unwrap();
174        SyncedAccountData {
175            wrapped_account_cryptographic_state: WrappedAccountCryptographicState::V1 {
176                private_key: wrapped_private_key,
177            },
178            folders: vec![],
179            ciphers: vec![],
180            sends: vec![],
181            emergency_access_memberships: ea_memberships,
182            organization_memberships: org_memberships,
183            trusted_devices: vec![],
184            passkeys: vec![],
185            kdf_and_salt: None,
186        }
187    }
188
189    #[test]
190    fn test_filter_trusted_org_empty_list() {
191        let store: KeyStore<KeySlotIds> = KeyStore::default();
192        let mut ctx = store.context_mut();
193        let (org, _) = make_org_membership(&mut ctx);
194        let trusted = [org.public_key.clone()];
195
196        // Note this is important to allow for the case where a user has no org memberships, but has
197        // provided a non-empty list of trusted org public keys. For example their
198        // organization membership was removed in the middle of the key rotation process.
199        let result = filter_trusted_organization(&[], &trusted);
200
201        assert!(matches!(result, Ok(ref v) if v.is_empty()));
202    }
203
204    #[test]
205    fn test_filter_trusted_org_all_trusted() {
206        let store: KeyStore<KeySlotIds> = KeyStore::default();
207        let mut ctx = store.context_mut();
208        let (org1, _) = make_org_membership(&mut ctx);
209        let (org2, _) = make_org_membership(&mut ctx);
210        let trusted = [org1.public_key.clone(), org2.public_key.clone()];
211        let expected_org1 = org1.clone();
212        let expected_org2 = org2.clone();
213
214        let result = filter_trusted_organization(&[org1, org2], &trusted);
215
216        let memberships = result.unwrap();
217        assert_eq!(memberships.len(), 2);
218        assert_org_membership_eq(&memberships[0], &expected_org1);
219        assert_org_membership_eq(&memberships[1], &expected_org2);
220    }
221
222    #[test]
223    fn test_filter_trusted_org_one_untrusted() {
224        let store: KeyStore<KeySlotIds> = KeyStore::default();
225        let mut ctx = store.context_mut();
226        let (org1, _) = make_org_membership(&mut ctx);
227        let (org2, _) = make_org_membership(&mut ctx);
228        let trusted = [org1.public_key.clone()];
229
230        let result = filter_trusted_organization(&[org1, org2], &trusted);
231
232        assert!(result.is_err());
233    }
234
235    #[test]
236    fn test_filter_trusted_org_empty_trusted_with_orgs() {
237        let store: KeyStore<KeySlotIds> = KeyStore::default();
238        let mut ctx = store.context_mut();
239        let (org, _) = make_org_membership(&mut ctx);
240
241        let result = filter_trusted_organization(&[org], &[]);
242
243        assert!(result.is_err());
244    }
245
246    #[test]
247    fn test_filter_trusted_ea_empty_list() {
248        let store: KeyStore<KeySlotIds> = KeyStore::default();
249        let mut ctx = store.context_mut();
250        let (ea, _) = make_ea_membership(&mut ctx);
251        let trusted = [ea.public_key.clone()];
252
253        let result = filter_trusted_emergency_access(&[], &trusted);
254
255        assert!(matches!(result, Ok(ref v) if v.is_empty()));
256    }
257
258    #[test]
259    fn test_filter_trusted_ea_all_trusted() {
260        let store: KeyStore<KeySlotIds> = KeyStore::default();
261        let mut ctx = store.context_mut();
262        let (ea1, _) = make_ea_membership(&mut ctx);
263        let (ea2, _) = make_ea_membership(&mut ctx);
264        let trusted = [ea1.public_key.clone(), ea2.public_key.clone()];
265        let expected_ea1 = ea1.clone();
266        let expected_ea2 = ea2.clone();
267
268        let result = filter_trusted_emergency_access(&[ea1, ea2], &trusted);
269
270        let memberships = result.expect("should succeed");
271        assert_eq!(memberships.len(), 2);
272        assert_ea_membership_eq(&memberships[0], &expected_ea1);
273        assert_ea_membership_eq(&memberships[1], &expected_ea2);
274    }
275
276    #[test]
277    fn test_filter_trusted_ea_one_untrusted() {
278        let store: KeyStore<KeySlotIds> = KeyStore::default();
279        let mut ctx = store.context_mut();
280        let (ea1, _) = make_ea_membership(&mut ctx);
281        let (ea2, _) = make_ea_membership(&mut ctx);
282        // only ea1 is trusted
283        let trusted = [ea1.public_key.clone()];
284
285        let result = filter_trusted_emergency_access(&[ea1, ea2], &trusted);
286
287        assert!(result.is_err());
288    }
289
290    #[test]
291    fn test_filter_trusted_ea_empty_trusted_with_memberships() {
292        let store: KeyStore<KeySlotIds> = KeyStore::default();
293        let mut ctx = store.context_mut();
294        let (ea, _) = make_ea_membership(&mut ctx);
295
296        let result = filter_trusted_emergency_access(&[ea], &[]);
297
298        assert!(result.is_err());
299    }
300
301    #[test]
302    fn test_make_rotation_context_empty_data() {
303        let store: KeyStore<KeySlotIds> = KeyStore::default();
304        let mut ctx = store.context_mut();
305        let sync = make_test_sync(vec![], vec![], &mut ctx);
306
307        let result = make_rotation_context(&sync, &[], &[], &mut ctx);
308
309        let rotation_ctx = result.expect("should succeed");
310        assert!(rotation_ctx.v1_organization_memberships.is_empty());
311        assert!(rotation_ctx.v1_emergency_access_memberships.is_empty());
312        assert_eq!(rotation_ctx.current_user_key_id, SymmetricKeySlotId::User);
313        assert_ne!(
314            rotation_ctx.new_user_key_id,
315            rotation_ctx.current_user_key_id
316        );
317    }
318
319    #[test]
320    fn test_make_rotation_context_trusted_org_and_ea() {
321        let store: KeyStore<KeySlotIds> = KeyStore::default();
322        let mut ctx = store.context_mut();
323        let (org, _) = make_org_membership(&mut ctx);
324        let (ea, _) = make_ea_membership(&mut ctx);
325        let trusted_orgs = [org.public_key.clone()];
326        let trusted_eas = [ea.public_key.clone()];
327        let expected_org = org.clone();
328        let expected_ea = ea.clone();
329        let sync = make_test_sync(vec![org], vec![ea], &mut ctx);
330
331        let result = make_rotation_context(&sync, &trusted_orgs, &trusted_eas, &mut ctx);
332
333        let rotation_ctx = result.expect("should succeed");
334        assert_eq!(rotation_ctx.v1_organization_memberships.len(), 1);
335        assert_org_membership_eq(&rotation_ctx.v1_organization_memberships[0], &expected_org);
336        assert_eq!(rotation_ctx.v1_emergency_access_memberships.len(), 1);
337        assert_ea_membership_eq(
338            &rotation_ctx.v1_emergency_access_memberships[0],
339            &expected_ea,
340        );
341        assert_eq!(rotation_ctx.current_user_key_id, SymmetricKeySlotId::User);
342        assert_ne!(
343            rotation_ctx.new_user_key_id,
344            rotation_ctx.current_user_key_id
345        );
346    }
347
348    #[test]
349    fn test_make_rotation_context_untrusted_org_returns_error() {
350        let store: KeyStore<KeySlotIds> = KeyStore::default();
351        let mut ctx = store.context_mut();
352        let (org, _) = make_org_membership(&mut ctx);
353        let sync = make_test_sync(vec![org], vec![], &mut ctx);
354
355        let result = make_rotation_context(&sync, &[], &[], &mut ctx);
356
357        assert!(matches!(result, Err(RotateUserKeysError::UntrustedKey)));
358    }
359
360    #[test]
361    fn test_make_rotation_context_untrusted_ea_returns_error() {
362        let store: KeyStore<KeySlotIds> = KeyStore::default();
363        let mut ctx = store.context_mut();
364        let (ea, _) = make_ea_membership(&mut ctx);
365        let sync = make_test_sync(vec![], vec![ea], &mut ctx);
366
367        let result = make_rotation_context(&sync, &[], &[], &mut ctx);
368
369        assert!(matches!(result, Err(RotateUserKeysError::UntrustedKey)));
370    }
371}