Skip to main content

bitwarden_user_crypto_management/key_rotation/
data.rs

1//! Functionality for re-encrypting user data during key rotation.
2
3use bitwarden_api_api::models::{
4    AccountDataRequestModel, CipherWithIdRequestModel, SendWithIdRequestModel,
5};
6use bitwarden_core::{
7    UserId,
8    key_management::{KeySlotIds, SymmetricKeySlotId},
9};
10use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStoreContext};
11use bitwarden_send::SendView;
12use bitwarden_vault::{CipherView, EncryptionContext, FolderView};
13use tracing::{debug, debug_span, instrument};
14use uuid::Uuid;
15
16use super::RotateUserKeysError;
17
18/// Errors that can occur during data re-encryption
19#[derive(Debug)]
20pub(crate) enum DataReencryptionError {
21    /// Failed to decrypt data with the current user key
22    Decryption,
23    /// Failed to encrypt data with the new user key
24    Encryption,
25    /// Failed to convert data to API model
26    DataConversion,
27    /// CipherKeyRewrap
28    CipherKeyRewrap,
29}
30
31/// Checks that no cipher contains legacy attachments (attachments where `key` is `None`).
32/// Ciphers with old attachments cannot be safely re-encrypted during key rotation because
33/// the attachment file contents are encrypted directly with the user key and would become
34/// irrecoverable after the user key change.
35pub(super) fn check_for_old_attachments(
36    ciphers: &[bitwarden_vault::Cipher],
37) -> Result<(), RotateUserKeysError> {
38    let has_old = ciphers
39        .iter()
40        .filter(|c| c.organization_id.is_none())
41        .any(|c| {
42            c.attachments
43                .as_ref()
44                .is_some_and(|atts| atts.iter().any(|a| a.key.is_none()))
45        });
46    if has_old {
47        return Err(RotateUserKeysError::OldAttachments);
48    }
49    Ok(())
50}
51
52/// Re-encrypts all user data (folders, ciphers, sends) with the new user key for the purpose of
53/// key-rotation. Note: Ciphers must be filtered to just contain the user's ciphers, not
54/// organization ciphers.
55#[instrument(name = "reencrypt_data", skip(folders, ciphers, sends, ctx))]
56pub(super) fn reencrypt_data(
57    folders: &[bitwarden_vault::Folder],
58    ciphers: &[bitwarden_vault::Cipher],
59    sends: &[bitwarden_send::Send],
60    current_user_key_id: SymmetricKeySlotId,
61    new_user_key_id: SymmetricKeySlotId,
62    ctx: &mut KeyStoreContext<KeySlotIds>,
63) -> Result<AccountDataRequestModel, DataReencryptionError> {
64    // Fully re-encrypt all user data with the new user key
65    let reencrypted_folders =
66        reencrypt_folders(folders, current_user_key_id, new_user_key_id, ctx)?;
67    let reencrypted_ciphers =
68        reencrypt_ciphers(ciphers, current_user_key_id, new_user_key_id, ctx)?;
69    let reencrypted_sends = reencrypt_sends(sends, current_user_key_id, new_user_key_id, ctx)?;
70    Ok(AccountDataRequestModel {
71        folders: Some(
72            reencrypted_folders
73                .into_iter()
74                .map(|folder| (&folder).into())
75                .collect(),
76        ),
77        ciphers: Some(
78            reencrypted_ciphers
79                .into_iter()
80                .map(|cipher| {
81                    EncryptionContext {
82                        // Encrypted for is not used in key-rotation, and ciphers are validated to
83                        // be correct server-side
84                        encrypted_for: UserId::new(Uuid::nil()),
85                        cipher,
86                    }
87                    .try_into()
88                    .map_err(|_| DataReencryptionError::DataConversion)
89                })
90                .collect::<Result<Vec<CipherWithIdRequestModel>, DataReencryptionError>>()?,
91        ),
92        sends: Some(
93            reencrypted_sends
94                .into_iter()
95                .map(|send| Ok(send.into()))
96                .collect::<Result<Vec<SendWithIdRequestModel>, DataReencryptionError>>()?,
97        ),
98    })
99}
100
101#[instrument(name = "reencrypt_folders", skip(folders, ctx))]
102fn reencrypt_folders(
103    folders: &[bitwarden_vault::Folder],
104    current_key: SymmetricKeySlotId,
105    new_key: SymmetricKeySlotId,
106    ctx: &mut KeyStoreContext<KeySlotIds>,
107) -> Result<Vec<bitwarden_vault::Folder>, DataReencryptionError> {
108    folders
109        .iter()
110        .map(|folder| {
111            let _span = debug_span!("reencrypt_folder", folder_id = ?folder.id).entered();
112            let folder_view: FolderView = folder
113                .decrypt(ctx, current_key)
114                .map_err(|_| DataReencryptionError::Decryption)?;
115            folder_view
116                .encrypt_composite(ctx, new_key)
117                .map_err(|_| DataReencryptionError::Encryption)
118        })
119        .collect::<Result<Vec<bitwarden_vault::Folder>, DataReencryptionError>>()
120}
121
122#[instrument(name = "reencrypt_ciphers", skip(ciphers, ctx))]
123fn reencrypt_ciphers(
124    ciphers: &[bitwarden_vault::Cipher],
125    current_key: SymmetricKeySlotId,
126    new_key: SymmetricKeySlotId,
127    ctx: &mut KeyStoreContext<KeySlotIds>,
128) -> Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError> {
129    ciphers
130        .iter()
131        .map(|cipher| {
132            let _span = debug_span!("reencrypt_cipher", cipher_id = ?cipher.id).entered();
133
134            // If the cipher has a per-vault-item cipher-key, the cipher-key
135            // is re-wrapped
136            if cipher.key.is_some() {
137                debug!("Re-wrapping cipher key without decrypting cipher");
138                let mut cipher = cipher.clone();
139                cipher
140                    .rewrap_cipher_key(current_key, new_key, ctx)
141                    .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
142                Ok(cipher)
143
144            // If the cipher has no cipher-key, the entire cipher is decrypted and re-encrypted
145            // and has to be re-uploaded.
146            } else {
147                debug!("Cipher has no cipher key, decrypting and re-encrypting entire cipher");
148                let cipher_view: CipherView = cipher
149                    .decrypt(ctx, current_key)
150                    .map_err(|_| DataReencryptionError::Decryption)?;
151                cipher_view
152                    .encrypt_composite(ctx, new_key)
153                    .map_err(|_| DataReencryptionError::Encryption)
154            }
155        })
156        .collect::<Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError>>()
157}
158
159#[instrument(name = "reencrypt_sends", skip(sends, ctx))]
160fn reencrypt_sends(
161    sends: &[bitwarden_send::Send],
162    current_key: SymmetricKeySlotId,
163    new_key: SymmetricKeySlotId,
164    ctx: &mut KeyStoreContext<KeySlotIds>,
165) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
166    sends
167        .iter()
168        .map(|send| {
169            let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
170            let send_view: SendView = send
171                .decrypt(ctx, current_key)
172                .map_err(|_| DataReencryptionError::Decryption)?;
173            send_view
174                .encrypt_composite(ctx, new_key)
175                .map_err(|_| DataReencryptionError::Encryption)
176        })
177        .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
178}
179
180#[cfg(test)]
181mod tests {
182    use bitwarden_core::key_management::KeySlotIds;
183    use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore};
184    use bitwarden_send::SendView;
185    use bitwarden_vault::{Attachment, Cipher, CipherRepromptType, CipherType};
186    use chrono::Utc;
187
188    use super::check_for_old_attachments;
189    use crate::key_rotation::RotateUserKeysError;
190
191    const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
192
193    fn make_test_cipher(attachments: Option<Vec<Attachment>>) -> Cipher {
194        Cipher {
195            id: None,
196            organization_id: None,
197            folder_id: None,
198            collection_ids: vec![],
199            key: None,
200            name: TEST_ENC_STRING.parse().unwrap(),
201            notes: None,
202            r#type: CipherType::Login,
203            login: None,
204            identity: None,
205            card: None,
206            secure_note: None,
207            ssh_key: None,
208            bank_account: None,
209            passport: None,
210            drivers_license: None,
211            favorite: false,
212            reprompt: CipherRepromptType::None,
213            organization_use_totp: false,
214            edit: true,
215            permissions: None,
216            view_password: true,
217            local_data: None,
218            attachments,
219            fields: None,
220            password_history: None,
221            creation_date: "2024-01-01T00:00:00Z".parse().unwrap(),
222            deleted_date: None,
223            revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
224            archived_date: None,
225            data: None,
226        }
227    }
228
229    #[test]
230    fn test_check_for_old_attachments_no_attachments() {
231        let ciphers = vec![make_test_cipher(None)];
232        assert!(check_for_old_attachments(&ciphers).is_ok());
233    }
234
235    #[test]
236    fn test_check_for_old_attachments_empty_ciphers() {
237        assert!(check_for_old_attachments(&[]).is_ok());
238    }
239
240    #[test]
241    fn test_check_for_old_attachments_all_have_keys() {
242        let ciphers = vec![make_test_cipher(Some(vec![Attachment {
243            id: Some("att1".to_string()),
244            url: None,
245            size: None,
246            size_name: None,
247            file_name: Some(TEST_ENC_STRING.parse().unwrap()),
248            key: Some(TEST_ENC_STRING.parse().unwrap()),
249        }]))];
250        assert!(check_for_old_attachments(&ciphers).is_ok());
251    }
252
253    #[test]
254    fn test_check_for_old_attachments_one_missing_key() {
255        let ciphers = vec![make_test_cipher(Some(vec![Attachment {
256            id: Some("att1".to_string()),
257            url: None,
258            size: None,
259            size_name: None,
260            file_name: Some(TEST_ENC_STRING.parse().unwrap()),
261            key: None,
262        }]))];
263        assert!(matches!(
264            check_for_old_attachments(&ciphers),
265            Err(RotateUserKeysError::OldAttachments)
266        ));
267    }
268
269    #[test]
270    fn test_check_for_old_attachments_ignores_organization_ciphers() {
271        let mut cipher = make_test_cipher(Some(vec![Attachment {
272            id: Some("att1".to_string()),
273            url: None,
274            size: None,
275            size_name: None,
276            file_name: Some(TEST_ENC_STRING.parse().unwrap()),
277            key: None,
278        }]));
279        cipher.organization_id = Some(bitwarden_core::OrganizationId::new_v4());
280        let ciphers = vec![cipher];
281        assert!(check_for_old_attachments(&ciphers).is_ok());
282    }
283
284    #[test]
285    fn test_ciphers() {
286        use bitwarden_vault::{CipherType, CipherView, LoginView};
287        let store: KeyStore<KeySlotIds> = KeyStore::default();
288        let mut ctx = store.context_mut();
289
290        let user_key_old =
291            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
292        let user_key_new =
293            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
294
295        let cipher = CipherView {
296            id: None,
297            organization_id: None,
298            folder_id: None,
299            r#type: CipherType::Login,
300            name: "Test Cipher".to_string(),
301            notes: Some("Some cipher notes".to_string()),
302            favorite: false,
303            revision_date: Utc::now(),
304            deleted_date: None,
305            fields: None,
306            login: Some(LoginView {
307                username: Some("user".to_string()),
308                password: Some("pass".to_string()),
309                totp: None,
310                uris: None,
311                autofill_on_page_load: None,
312                fido2_credentials: None,
313                password_revision_date: None,
314            }),
315            card: None,
316            identity: None,
317            secure_note: None,
318            attachments: None,
319            attachment_decryption_failures: None,
320            organization_use_totp: false,
321            collection_ids: vec![],
322            reprompt: CipherRepromptType::None,
323            local_data: None,
324            key: None,
325            ssh_key: None,
326            bank_account: None,
327            passport: None,
328            drivers_license: None,
329            permissions: None,
330            view_password: false,
331            creation_date: Utc::now(),
332            archived_date: None,
333            edit: false,
334            password_history: None,
335        };
336        let encrypted_cipher = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap();
337
338        // Rotate it
339        let ciphers = vec![encrypted_cipher];
340        let reencrypted_ciphers =
341            super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
342                .unwrap();
343
344        // Decrypt and assert
345        let decrypted_cipher: CipherView = reencrypted_ciphers[0]
346            .decrypt(&mut ctx, user_key_new)
347            .unwrap();
348        assert_eq!(cipher.name, decrypted_cipher.name);
349        assert_eq!(cipher.notes, decrypted_cipher.notes);
350        assert_eq!(cipher.r#type, decrypted_cipher.r#type);
351        assert_eq!(
352            cipher.login.as_ref().unwrap().username,
353            decrypted_cipher.login.as_ref().unwrap().username
354        );
355        assert_eq!(
356            cipher.login.as_ref().unwrap().password,
357            decrypted_cipher.login.as_ref().unwrap().password
358        );
359    }
360
361    #[test]
362    fn test_folders() {
363        let store: KeyStore<KeySlotIds> = KeyStore::default();
364        let mut ctx = store.context_mut();
365
366        let user_key_old =
367            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
368        let user_key_new =
369            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
370
371        // Create an encrypted folder
372        let folder = bitwarden_vault::FolderView {
373            id: None,
374            name: "Test Folder".to_string(),
375            revision_date: Utc::now(),
376        };
377        let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
378
379        // Rotate it
380        let folders = vec![encrypted_folder];
381        let reencrypted_folders =
382            super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
383                .unwrap();
384
385        // Decrypt and assert
386        let decrypted_folder = reencrypted_folders[0]
387            .decrypt(&mut ctx, user_key_new)
388            .unwrap();
389        assert_eq!(folder, decrypted_folder);
390    }
391
392    #[test]
393    fn test_sends() {
394        let store: KeyStore<KeySlotIds> = KeyStore::default();
395        let mut ctx = store.context_mut();
396
397        let user_key_old =
398            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
399        let user_key_new =
400            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
401
402        // Create an encrypted send
403        let send = bitwarden_send::SendView {
404            id: None,
405            access_id: None,
406            name: "Test Send".to_string(),
407            notes: Some("Some notes".to_string()),
408            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
409            text: Some(bitwarden_send::SendTextView {
410                text: Some("This is a test send".to_string()),
411                hidden: false,
412            }),
413            r#type: bitwarden_send::SendType::Text,
414            max_access_count: None,
415            access_count: 0,
416            disabled: false,
417            hide_email: false,
418            revision_date: Utc::now(),
419            deletion_date: Utc::now(),
420            expiration_date: None,
421            new_password: None,
422            has_password: false,
423            file: None,
424            emails: vec![],
425            auth_type: bitwarden_send::AuthType::None,
426        };
427        let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
428
429        // Rotate it
430        let sends = vec![encrypted_send];
431        let reencrypted_sends =
432            super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
433
434        // Decrypt and assert
435        let decrypted_send: SendView = reencrypted_sends[0]
436            .decrypt(&mut ctx, user_key_new)
437            .unwrap();
438
439        // The send seed must be the same
440        assert_eq!(send.key, decrypted_send.key);
441    }
442}