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
16/// Errors that can occur during data re-encryption
17#[derive(Debug)]
18pub(crate) enum DataReencryptionError {
19    /// Failed to decrypt data with the current user key
20    Decryption,
21    /// Failed to encrypt data with the new user key
22    Encryption,
23    /// Failed to convert data to API model
24    DataConversion,
25    /// CipherKeyRewrap
26    CipherKeyRewrap,
27}
28
29/// Re-encrypts all user data (folders, ciphers, sends) with the new user key for the purpose of
30/// key-rotation. Note: Ciphers must be filtered to just contain the user's ciphers, not
31/// organization ciphers.
32#[instrument(name = "reencrypt_data", skip(folders, ciphers, sends, ctx))]
33pub(super) fn reencrypt_data(
34    folders: &[bitwarden_vault::Folder],
35    ciphers: &[bitwarden_vault::Cipher],
36    sends: &[bitwarden_send::Send],
37    current_user_key_id: SymmetricKeySlotId,
38    new_user_key_id: SymmetricKeySlotId,
39    ctx: &mut KeyStoreContext<KeySlotIds>,
40) -> Result<AccountDataRequestModel, DataReencryptionError> {
41    // Fully re-encrypt all user data with the new user key
42    let reencrypted_folders =
43        reencrypt_folders(folders, current_user_key_id, new_user_key_id, ctx)?;
44    let reencrypted_ciphers =
45        reencrypt_ciphers(ciphers, current_user_key_id, new_user_key_id, ctx)?;
46    let reencrypted_sends = reencrypt_sends(sends, current_user_key_id, new_user_key_id, ctx)?;
47    Ok(AccountDataRequestModel {
48        folders: Some(
49            reencrypted_folders
50                .into_iter()
51                .map(|folder| (&folder).into())
52                .collect(),
53        ),
54        ciphers: Some(
55            reencrypted_ciphers
56                .into_iter()
57                .map(|cipher| {
58                    EncryptionContext {
59                        // Encrypted for is not used in key-rotation, and ciphers are validated to
60                        // be correct server-side
61                        encrypted_for: UserId::new(Uuid::nil()),
62                        cipher,
63                    }
64                    .try_into()
65                    .map_err(|_| DataReencryptionError::DataConversion)
66                })
67                .collect::<Result<Vec<CipherWithIdRequestModel>, DataReencryptionError>>()?,
68        ),
69        sends: Some(
70            reencrypted_sends
71                .into_iter()
72                .map(|send| Ok(send.into()))
73                .collect::<Result<Vec<SendWithIdRequestModel>, DataReencryptionError>>()?,
74        ),
75    })
76}
77
78#[instrument(name = "reencrypt_folders", skip(folders, ctx))]
79fn reencrypt_folders(
80    folders: &[bitwarden_vault::Folder],
81    current_key: SymmetricKeySlotId,
82    new_key: SymmetricKeySlotId,
83    ctx: &mut KeyStoreContext<KeySlotIds>,
84) -> Result<Vec<bitwarden_vault::Folder>, DataReencryptionError> {
85    folders
86        .iter()
87        .map(|folder| {
88            let _span = debug_span!("reencrypt_folder", folder_id = ?folder.id).entered();
89            let folder_view: FolderView = folder
90                .decrypt(ctx, current_key)
91                .map_err(|_| DataReencryptionError::Decryption)?;
92            folder_view
93                .encrypt_composite(ctx, new_key)
94                .map_err(|_| DataReencryptionError::Encryption)
95        })
96        .collect::<Result<Vec<bitwarden_vault::Folder>, DataReencryptionError>>()
97}
98
99#[instrument(name = "reencrypt_ciphers", skip(ciphers, ctx))]
100fn reencrypt_ciphers(
101    ciphers: &[bitwarden_vault::Cipher],
102    current_key: SymmetricKeySlotId,
103    new_key: SymmetricKeySlotId,
104    ctx: &mut KeyStoreContext<KeySlotIds>,
105) -> Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError> {
106    ciphers
107        .iter()
108        .map(|cipher| {
109            let _span = debug_span!("reencrypt_cipher", cipher_id = ?cipher.id).entered();
110
111            // If the cipher has a per-vault-item cipher-key, the cipher-key
112            // is re-wrapped
113            if cipher.key.is_some() {
114                debug!("Re-wrapping cipher key without decrypting cipher");
115                let mut cipher = cipher.clone();
116                cipher
117                    .rewrap_cipher_key(current_key, new_key, ctx)
118                    .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
119                Ok(cipher)
120
121            // If the cipher has no cipher-key, the entire cipher is decrypted and re-encrypted
122            // and has to be re-uploaded.
123            } else {
124                debug!("Cipher has no cipher key, decrypting and re-encrypting entire cipher");
125                let cipher_view: CipherView = cipher
126                    .decrypt(ctx, current_key)
127                    .map_err(|_| DataReencryptionError::Decryption)?;
128                cipher_view
129                    .encrypt_composite(ctx, new_key)
130                    .map_err(|_| DataReencryptionError::Encryption)
131            }
132        })
133        .collect::<Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError>>()
134}
135
136#[instrument(name = "reencrypt_sends", skip(sends, ctx))]
137fn reencrypt_sends(
138    sends: &[bitwarden_send::Send],
139    current_key: SymmetricKeySlotId,
140    new_key: SymmetricKeySlotId,
141    ctx: &mut KeyStoreContext<KeySlotIds>,
142) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
143    sends
144        .iter()
145        .map(|send| {
146            let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
147            let send_view: SendView = send
148                .decrypt(ctx, current_key)
149                .map_err(|_| DataReencryptionError::Decryption)?;
150            send_view
151                .encrypt_composite(ctx, new_key)
152                .map_err(|_| DataReencryptionError::Encryption)
153        })
154        .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
155}
156
157#[cfg(test)]
158mod tests {
159    use bitwarden_core::key_management::KeySlotIds;
160    use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore};
161    use bitwarden_send::SendView;
162    use chrono::Utc;
163
164    #[test]
165    fn test_ciphers() {
166        use bitwarden_vault::{CipherType, CipherView, LoginView};
167        let store: KeyStore<KeySlotIds> = KeyStore::default();
168        let mut ctx = store.context_mut();
169
170        let user_key_old =
171            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
172        let user_key_new =
173            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
174
175        let cipher = CipherView {
176            id: None,
177            organization_id: None,
178            folder_id: None,
179            r#type: CipherType::Login,
180            name: "Test Cipher".to_string(),
181            notes: Some("Some cipher notes".to_string()),
182            favorite: false,
183            revision_date: Utc::now(),
184            deleted_date: None,
185            fields: None,
186            login: Some(LoginView {
187                username: Some("user".to_string()),
188                password: Some("pass".to_string()),
189                totp: None,
190                uris: None,
191                autofill_on_page_load: None,
192                fido2_credentials: None,
193                password_revision_date: None,
194            }),
195            card: None,
196            identity: None,
197            secure_note: None,
198            attachments: None,
199            attachment_decryption_failures: None,
200            organization_use_totp: false,
201            collection_ids: vec![],
202            reprompt: bitwarden_vault::CipherRepromptType::None,
203            local_data: None,
204            key: None,
205            ssh_key: None,
206            bank_account: None,
207            permissions: None,
208            view_password: false,
209            creation_date: Utc::now(),
210            archived_date: None,
211            edit: false,
212            password_history: None,
213        };
214        let encrypted_cipher = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap();
215
216        // Rotate it
217        let ciphers = vec![encrypted_cipher];
218        let reencrypted_ciphers =
219            super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
220                .unwrap();
221
222        // Decrypt and assert
223        let decrypted_cipher: CipherView = reencrypted_ciphers[0]
224            .decrypt(&mut ctx, user_key_new)
225            .unwrap();
226        assert_eq!(cipher.name, decrypted_cipher.name);
227        assert_eq!(cipher.notes, decrypted_cipher.notes);
228        assert_eq!(cipher.r#type, decrypted_cipher.r#type);
229        assert_eq!(
230            cipher.login.as_ref().unwrap().username,
231            decrypted_cipher.login.as_ref().unwrap().username
232        );
233        assert_eq!(
234            cipher.login.as_ref().unwrap().password,
235            decrypted_cipher.login.as_ref().unwrap().password
236        );
237    }
238
239    #[test]
240    fn test_folders() {
241        let store: KeyStore<KeySlotIds> = KeyStore::default();
242        let mut ctx = store.context_mut();
243
244        let user_key_old =
245            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
246        let user_key_new =
247            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
248
249        // Create an encrypted folder
250        let folder = bitwarden_vault::FolderView {
251            id: None,
252            name: "Test Folder".to_string(),
253            revision_date: Utc::now(),
254        };
255        let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
256
257        // Rotate it
258        let folders = vec![encrypted_folder];
259        let reencrypted_folders =
260            super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
261                .unwrap();
262
263        // Decrypt and assert
264        let decrypted_folder = reencrypted_folders[0]
265            .decrypt(&mut ctx, user_key_new)
266            .unwrap();
267        assert_eq!(folder, decrypted_folder);
268    }
269
270    #[test]
271    fn test_sends() {
272        let store: KeyStore<KeySlotIds> = KeyStore::default();
273        let mut ctx = store.context_mut();
274
275        let user_key_old =
276            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
277        let user_key_new =
278            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
279
280        // Create an encrypted send
281        let send = bitwarden_send::SendView {
282            id: None,
283            access_id: None,
284            name: "Test Send".to_string(),
285            notes: Some("Some notes".to_string()),
286            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
287            text: Some(bitwarden_send::SendTextView {
288                text: Some("This is a test send".to_string()),
289                hidden: false,
290            }),
291            r#type: bitwarden_send::SendType::Text,
292            max_access_count: None,
293            access_count: 0,
294            disabled: false,
295            hide_email: false,
296            revision_date: Utc::now(),
297            deletion_date: Utc::now(),
298            expiration_date: None,
299            new_password: None,
300            has_password: false,
301            file: None,
302            emails: vec![],
303            auth_type: bitwarden_send::AuthType::None,
304        };
305        let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
306
307        // Rotate it
308        let sends = vec![encrypted_send];
309        let reencrypted_sends =
310            super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
311
312        // Decrypt and assert
313        let decrypted_send: SendView = reencrypted_sends[0]
314            .decrypt(&mut ctx, user_key_new)
315            .unwrap();
316
317        // The send seed must be the same
318        assert_eq!(send.key, decrypted_send.key);
319    }
320}