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, EncryptMode, EncryptionContext, FolderView};
13use tracing::{debug, debug_span};
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#[bitwarden_logging::instrument(name = "reencrypt_data", fields(current_user_key_id = ?current_user_key_id, new_user_key_id = ?new_user_key_id))]
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
71    // Every cipher here was just re-encrypted under the new user key, so that is the key id the
72    // server needs to validate this rotation's cipher writes against. `reencrypt_data` is only
73    // ever passed the user's own ciphers, never organization ciphers.
74    let encrypted_by_key_id = ctx
75        .get_symmetric_key_id(new_user_key_id)
76        .map(|id| id.to_string());
77
78    Ok(AccountDataRequestModel {
79        folders: Some(
80            reencrypted_folders
81                .into_iter()
82                .map(|folder| (&folder).into())
83                .collect(),
84        ),
85        ciphers: Some(
86            reencrypted_ciphers
87                .into_iter()
88                .map(|cipher| {
89                    EncryptionContext {
90                        // Encrypted for is not used in key-rotation, and ciphers are validated to
91                        // be correct server-side
92                        encrypted_for: UserId::new(Uuid::nil()),
93                        encrypted_by_key_id: encrypted_by_key_id.clone(),
94                        cipher,
95                    }
96                    .try_into()
97                    .map_err(|_| DataReencryptionError::DataConversion)
98                })
99                .collect::<Result<Vec<CipherWithIdRequestModel>, DataReencryptionError>>()?,
100        ),
101        sends: Some(
102            reencrypted_sends
103                .into_iter()
104                .map(|send| Ok(send.into()))
105                .collect::<Result<Vec<SendWithIdRequestModel>, DataReencryptionError>>()?,
106        ),
107    })
108}
109
110#[bitwarden_logging::instrument(name = "reencrypt_folders", fields(current_key = ?current_key, new_key = ?new_key))]
111fn reencrypt_folders(
112    folders: &[bitwarden_vault::Folder],
113    current_key: SymmetricKeySlotId,
114    new_key: SymmetricKeySlotId,
115    ctx: &mut KeyStoreContext<KeySlotIds>,
116) -> Result<Vec<bitwarden_vault::Folder>, DataReencryptionError> {
117    folders
118        .iter()
119        .map(|folder| {
120            let _span = debug_span!("reencrypt_folder", folder_id = ?folder.id).entered();
121            let folder_view: FolderView = folder
122                .decrypt(ctx, current_key)
123                .map_err(|_| DataReencryptionError::Decryption)?;
124            folder_view
125                .encrypt_composite(ctx, new_key)
126                .map_err(|_| DataReencryptionError::Encryption)
127        })
128        .collect::<Result<Vec<bitwarden_vault::Folder>, DataReencryptionError>>()
129}
130
131#[bitwarden_logging::instrument(name = "reencrypt_ciphers", fields(current_key = ?current_key, new_key = ?new_key))]
132fn reencrypt_ciphers(
133    ciphers: &[bitwarden_vault::Cipher],
134    current_key: SymmetricKeySlotId,
135    new_key: SymmetricKeySlotId,
136    ctx: &mut KeyStoreContext<KeySlotIds>,
137) -> Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError> {
138    ciphers
139        .iter()
140        .map(|cipher| {
141            let _span = debug_span!("reencrypt_cipher", cipher_id = ?cipher.id).entered();
142
143            // Rotation always lands the account on the V2 security state, so every individual
144            // cipher ends up blob-encrypted. Ciphers that are already sealed blobs only need their
145            // per-item key re-wrapped; the sealed blob is left untouched. Legacy ciphers are
146            // decrypted and re-sealed as blobs.
147            if cipher.is_blob_encrypted() && cipher.key.is_some() {
148                debug!("Cipher already blob-encrypted, re-wrapping cipher key");
149                let mut cipher = cipher.clone();
150                cipher
151                    .rewrap_cipher_key(current_key, new_key, ctx)
152                    .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
153                Ok(cipher)
154            } else {
155                debug!("Upgrading legacy cipher to blob encryption");
156                let cipher_view = decrypt_for_blob_upgrade(cipher, current_key, new_key, ctx)?;
157                EncryptMode::Blob(cipher_view)
158                    .encrypt_composite(ctx, new_key)
159                    .map_err(|_| DataReencryptionError::Encryption)
160            }
161        })
162        .collect::<Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError>>()
163}
164
165/// Decrypts a legacy cipher into a view that can be re-sealed as a blob under `new_key`.
166///
167/// If the cipher has a per-item key, it is re-wrapped under the new user key first so the cipher
168/// key (and the data sealed against it) stay consistent under the new key. Otherwise the cipher is
169/// decrypted under the current key and blob sealing will generate a fresh per-item key.
170fn decrypt_for_blob_upgrade(
171    cipher: &bitwarden_vault::Cipher,
172    current_key: SymmetricKeySlotId,
173    new_key: SymmetricKeySlotId,
174    ctx: &mut KeyStoreContext<KeySlotIds>,
175) -> Result<CipherView, DataReencryptionError> {
176    if cipher.key.is_some() {
177        let mut rewrapped = cipher.clone();
178        rewrapped
179            .rewrap_cipher_key(current_key, new_key, ctx)
180            .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
181        rewrapped
182            .decrypt(ctx, new_key)
183            .map_err(|_| DataReencryptionError::Decryption)
184    } else {
185        cipher
186            .decrypt(ctx, current_key)
187            .map_err(|_| DataReencryptionError::Decryption)
188    }
189}
190
191#[bitwarden_logging::instrument(name = "reencrypt_sends", fields(current_key = ?current_key, new_key = ?new_key))]
192fn reencrypt_sends(
193    sends: &[bitwarden_send::Send],
194    current_key: SymmetricKeySlotId,
195    new_key: SymmetricKeySlotId,
196    ctx: &mut KeyStoreContext<KeySlotIds>,
197) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
198    sends
199        .iter()
200        .map(|send| {
201            let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
202            let send_view: SendView = send
203                .decrypt(ctx, current_key)
204                .map_err(|_| DataReencryptionError::Decryption)?;
205            send_view
206                .encrypt_composite(ctx, new_key)
207                .map_err(|_| DataReencryptionError::Encryption)
208        })
209        .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
210}
211
212#[cfg(test)]
213mod tests {
214    use bitwarden_core::key_management::KeySlotIds;
215    use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore};
216    use bitwarden_send::SendView;
217    use bitwarden_vault::{Attachment, Cipher, CipherRepromptType, CipherType, EncryptMode};
218    use chrono::Utc;
219
220    use super::check_for_old_attachments;
221    use crate::key_rotation::RotateUserKeysError;
222
223    const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
224
225    fn make_test_cipher(attachments: Option<Vec<Attachment>>) -> Cipher {
226        Cipher {
227            id: None,
228            organization_id: None,
229            folder_id: None,
230            collection_ids: vec![],
231            key: None,
232            name: Some(TEST_ENC_STRING.parse().unwrap()),
233            notes: None,
234            r#type: CipherType::Login,
235            login: None,
236            identity: None,
237            card: None,
238            secure_note: None,
239            ssh_key: None,
240            bank_account: None,
241            passport: None,
242            drivers_license: None,
243            favorite: false,
244            reprompt: CipherRepromptType::None,
245            organization_use_totp: false,
246            edit: true,
247            permissions: None,
248            view_password: true,
249            local_data: None,
250            attachments,
251            fields: None,
252            password_history: None,
253            creation_date: "2024-01-01T00:00:00Z".parse().unwrap(),
254            deleted_date: None,
255            revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
256            archived_date: None,
257            data: None,
258        }
259    }
260
261    #[test]
262    fn test_check_for_old_attachments_no_attachments() {
263        let ciphers = vec![make_test_cipher(None)];
264        assert!(check_for_old_attachments(&ciphers).is_ok());
265    }
266
267    #[test]
268    fn test_check_for_old_attachments_empty_ciphers() {
269        assert!(check_for_old_attachments(&[]).is_ok());
270    }
271
272    #[test]
273    fn test_check_for_old_attachments_all_have_keys() {
274        let ciphers = vec![make_test_cipher(Some(vec![Attachment {
275            id: Some("att1".to_string()),
276            url: None,
277            size: None,
278            size_name: None,
279            file_name: Some(TEST_ENC_STRING.parse().unwrap()),
280            key: Some(TEST_ENC_STRING.parse().unwrap()),
281        }]))];
282        assert!(check_for_old_attachments(&ciphers).is_ok());
283    }
284
285    #[test]
286    fn test_check_for_old_attachments_one_missing_key() {
287        let ciphers = vec![make_test_cipher(Some(vec![Attachment {
288            id: Some("att1".to_string()),
289            url: None,
290            size: None,
291            size_name: None,
292            file_name: Some(TEST_ENC_STRING.parse().unwrap()),
293            key: None,
294        }]))];
295        assert!(matches!(
296            check_for_old_attachments(&ciphers),
297            Err(RotateUserKeysError::OldAttachments)
298        ));
299    }
300
301    #[test]
302    fn test_check_for_old_attachments_ignores_organization_ciphers() {
303        let mut cipher = make_test_cipher(Some(vec![Attachment {
304            id: Some("att1".to_string()),
305            url: None,
306            size: None,
307            size_name: None,
308            file_name: Some(TEST_ENC_STRING.parse().unwrap()),
309            key: None,
310        }]));
311        cipher.organization_id = Some(bitwarden_core::OrganizationId::new_v4());
312        let ciphers = vec![cipher];
313        assert!(check_for_old_attachments(&ciphers).is_ok());
314    }
315
316    fn make_cipher_view() -> bitwarden_vault::CipherView {
317        use bitwarden_vault::{CipherView, LoginView};
318        CipherView {
319            id: None,
320            organization_id: None,
321            folder_id: None,
322            r#type: CipherType::Login,
323            name: "Test Cipher".to_string(),
324            notes: Some("Some cipher notes".to_string()),
325            favorite: false,
326            revision_date: Utc::now(),
327            deleted_date: None,
328            fields: None,
329            login: Some(LoginView {
330                username: Some("user".to_string()),
331                password: Some("pass".to_string()),
332                totp: None,
333                uris: None,
334                autofill_on_page_load: None,
335                fido2_credentials: None,
336                password_revision_date: None,
337            }),
338            card: None,
339            identity: None,
340            secure_note: None,
341            attachments: None,
342            attachment_decryption_failures: None,
343            organization_use_totp: false,
344            collection_ids: vec![],
345            reprompt: CipherRepromptType::None,
346            local_data: None,
347            key: None,
348            ssh_key: None,
349            bank_account: None,
350            passport: None,
351            drivers_license: None,
352            permissions: None,
353            view_password: false,
354            creation_date: Utc::now(),
355            archived_date: None,
356            edit: false,
357            password_history: None,
358        }
359    }
360
361    fn assert_decrypts_to(
362        cipher: &Cipher,
363        expected: &bitwarden_vault::CipherView,
364        key: bitwarden_core::key_management::SymmetricKeySlotId,
365        ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
366    ) {
367        use bitwarden_vault::CipherView;
368        let decrypted: CipherView = cipher.decrypt(ctx, key).unwrap();
369        assert_eq!(expected.name, decrypted.name);
370        assert_eq!(expected.notes, decrypted.notes);
371        assert_eq!(expected.r#type, decrypted.r#type);
372        assert_eq!(
373            expected.login.as_ref().unwrap().username,
374            decrypted.login.as_ref().unwrap().username
375        );
376        assert_eq!(
377            expected.login.as_ref().unwrap().password,
378            decrypted.login.as_ref().unwrap().password
379        );
380    }
381
382    #[test]
383    fn test_ciphers() {
384        let store: KeyStore<KeySlotIds> = KeyStore::default();
385        let mut ctx = store.context_mut();
386
387        let user_key_old =
388            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
389        let user_key_new =
390            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
391
392        let cipher = make_cipher_view();
393        let encrypted_cipher = EncryptMode::Legacy(cipher.clone())
394            .encrypt_composite(&mut ctx, user_key_old)
395            .unwrap();
396
397        // Rotate it
398        let ciphers = vec![encrypted_cipher];
399        let reencrypted_ciphers =
400            super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
401                .unwrap();
402
403        // The keyless legacy cipher is upgraded to the blob format and decrypts under the new key
404        assert!(reencrypted_ciphers[0].is_blob_encrypted());
405        assert_decrypts_to(&reencrypted_ciphers[0], &cipher, user_key_new, &mut ctx);
406    }
407
408    #[test]
409    fn test_blob_gate_upgrades_keyed_legacy_cipher() {
410        let store: KeyStore<KeySlotIds> = KeyStore::default();
411        let mut ctx = store.context_mut();
412
413        let user_key_old =
414            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
415        let user_key_new =
416            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XAes256Gcm);
417
418        // A legacy cipher that already carries a per-item cipher key
419        let mut cipher = make_cipher_view();
420        cipher.generate_cipher_key(&mut ctx, user_key_old).unwrap();
421        let encrypted = EncryptMode::Legacy(cipher.clone())
422            .encrypt_composite(&mut ctx, user_key_old)
423            .unwrap();
424        assert!(!encrypted.is_blob_encrypted());
425        assert!(encrypted.key.is_some());
426
427        let reencrypted =
428            super::reencrypt_ciphers(&[encrypted], user_key_old, user_key_new, &mut ctx).unwrap();
429
430        // The keyed legacy cipher is fully upgraded to the blob format
431        assert!(reencrypted[0].is_blob_encrypted());
432        assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
433    }
434
435    #[test]
436    fn test_blob_gate_rewraps_existing_blob_without_re_encrypting() {
437        let store: KeyStore<KeySlotIds> = KeyStore::default();
438        let mut ctx = store.context_mut();
439
440        let user_key_old =
441            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
442        let user_key_new =
443            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XAes256Gcm);
444
445        // An already blob-encrypted cipher
446        let cipher = make_cipher_view();
447        let encrypted = EncryptMode::Blob(cipher.clone())
448            .encrypt_composite(&mut ctx, user_key_old)
449            .unwrap();
450        assert!(encrypted.is_blob_encrypted());
451
452        let reencrypted = super::reencrypt_ciphers(
453            std::slice::from_ref(&encrypted),
454            user_key_old,
455            user_key_new,
456            &mut ctx,
457        )
458        .unwrap();
459
460        // The sealed blob is left intact; only the wrapped cipher key is rewrapped
461        assert!(reencrypted[0].is_blob_encrypted());
462        assert_eq!(encrypted.data, reencrypted[0].data);
463        assert_ne!(encrypted.key, reencrypted[0].key);
464        assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
465    }
466
467    #[test]
468    fn test_folders() {
469        let store: KeyStore<KeySlotIds> = KeyStore::default();
470        let mut ctx = store.context_mut();
471
472        let user_key_old =
473            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
474        let user_key_new =
475            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
476
477        // Create an encrypted folder
478        let folder = bitwarden_vault::FolderView {
479            id: None,
480            name: "Test Folder".to_string(),
481            revision_date: Utc::now(),
482        };
483        let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
484
485        // Rotate it
486        let folders = vec![encrypted_folder];
487        let reencrypted_folders =
488            super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
489                .unwrap();
490
491        // Decrypt and assert
492        let decrypted_folder = reencrypted_folders[0]
493            .decrypt(&mut ctx, user_key_new)
494            .unwrap();
495        assert_eq!(folder, decrypted_folder);
496    }
497
498    #[test]
499    fn test_sends() {
500        let store: KeyStore<KeySlotIds> = KeyStore::default();
501        let mut ctx = store.context_mut();
502
503        let user_key_old =
504            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
505        let user_key_new =
506            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
507
508        // Create an encrypted send
509        let send = bitwarden_send::SendView {
510            id: None,
511            access_id: None,
512            name: "Test Send".to_string(),
513            notes: Some("Some notes".to_string()),
514            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
515            text: Some(bitwarden_send::SendTextView {
516                text: Some("This is a test send".to_string()),
517                hidden: false,
518            }),
519            data: None,
520            r#type: bitwarden_send::SendType::Text,
521            max_access_count: None,
522            access_count: 0,
523            disabled: false,
524            hide_email: false,
525            revision_date: Utc::now(),
526            deletion_date: Utc::now(),
527            expiration_date: None,
528            new_password: None,
529            has_password: false,
530            file: None,
531            emails: vec![],
532            auth_type: bitwarden_send::AuthType::None,
533        };
534        let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
535
536        // Rotate it
537        let sends = vec![encrypted_send];
538        let reencrypted_sends =
539            super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
540
541        // Decrypt and assert
542        let decrypted_send: SendView = reencrypted_sends[0]
543            .decrypt(&mut ctx, user_key_new)
544            .unwrap();
545
546        // The send seed must be the same
547        assert_eq!(send.key, decrypted_send.key);
548    }
549}