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::{KeyIds, SymmetricKeyId},
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: SymmetricKeyId,
38    new_user_key_id: SymmetricKeyId,
39    ctx: &mut KeyStoreContext<KeyIds>,
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: SymmetricKeyId,
82    new_key: SymmetricKeyId,
83    ctx: &mut KeyStoreContext<KeyIds>,
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: SymmetricKeyId,
103    new_key: SymmetricKeyId,
104    ctx: &mut KeyStoreContext<KeyIds>,
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: SymmetricKeyId,
140    new_key: SymmetricKeyId,
141    ctx: &mut KeyStoreContext<KeyIds>,
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::KeyIds;
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<KeyIds> = 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            permissions: None,
207            view_password: false,
208            creation_date: Utc::now(),
209            archived_date: None,
210            edit: false,
211            password_history: None,
212        };
213        let encrypted_cipher = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap();
214
215        // Rotate it
216        let ciphers = vec![encrypted_cipher];
217        let reencrypted_ciphers =
218            super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
219                .unwrap();
220
221        // Decrypt and assert
222        let decrypted_cipher: CipherView = reencrypted_ciphers[0]
223            .decrypt(&mut ctx, user_key_new)
224            .unwrap();
225        assert_eq!(cipher.name, decrypted_cipher.name);
226        assert_eq!(cipher.notes, decrypted_cipher.notes);
227        assert_eq!(cipher.r#type, decrypted_cipher.r#type);
228        assert_eq!(
229            cipher.login.as_ref().unwrap().username,
230            decrypted_cipher.login.as_ref().unwrap().username
231        );
232        assert_eq!(
233            cipher.login.as_ref().unwrap().password,
234            decrypted_cipher.login.as_ref().unwrap().password
235        );
236    }
237
238    #[test]
239    fn test_folders() {
240        let store: KeyStore<KeyIds> = KeyStore::default();
241        let mut ctx = store.context_mut();
242
243        let user_key_old =
244            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
245        let user_key_new =
246            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
247
248        // Create an encrypted folder
249        let folder = bitwarden_vault::FolderView {
250            id: None,
251            name: "Test Folder".to_string(),
252            revision_date: Utc::now(),
253        };
254        let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
255
256        // Rotate it
257        let folders = vec![encrypted_folder];
258        let reencrypted_folders =
259            super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
260                .unwrap();
261
262        // Decrypt and assert
263        let decrypted_folder = reencrypted_folders[0]
264            .decrypt(&mut ctx, user_key_new)
265            .unwrap();
266        assert_eq!(folder, decrypted_folder);
267    }
268
269    #[test]
270    fn test_sends() {
271        let store: KeyStore<KeyIds> = KeyStore::default();
272        let mut ctx = store.context_mut();
273
274        let user_key_old =
275            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
276        let user_key_new =
277            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
278
279        // Create an encrypted send
280        let send = bitwarden_send::SendView {
281            id: None,
282            access_id: None,
283            name: "Test Send".to_string(),
284            notes: Some("Some notes".to_string()),
285            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
286            text: Some(bitwarden_send::SendTextView {
287                text: Some("This is a test send".to_string()),
288                hidden: false,
289            }),
290            r#type: bitwarden_send::SendType::Text,
291            max_access_count: None,
292            access_count: 0,
293            disabled: false,
294            hide_email: false,
295            revision_date: Utc::now(),
296            deletion_date: Utc::now(),
297            expiration_date: None,
298            new_password: None,
299            has_password: false,
300            file: None,
301            emails: vec![],
302            auth_type: bitwarden_send::AuthType::None,
303        };
304        let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
305
306        // Rotate it
307        let sends = vec![encrypted_send];
308        let reencrypted_sends =
309            super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
310
311        // Decrypt and assert
312        let decrypted_send: SendView = reencrypted_sends[0]
313            .decrypt(&mut ctx, user_key_new)
314            .unwrap();
315
316        // The send seed must be the same
317        assert_eq!(send.key, decrypted_send.key);
318    }
319}