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| {
73                    Ok(SendWithIdRequestModel {
74                        id: send.id.ok_or(DataReencryptionError::DataConversion)?,
75                        key: send.key.to_string(),
76                        // During key-rotation only the "key" (encrypted seed) and id are used,
77                        // since we only re-encrypt the "key"
78                        ..Default::default()
79                    })
80                })
81                .collect::<Result<Vec<SendWithIdRequestModel>, DataReencryptionError>>()?,
82        ),
83    })
84}
85
86#[instrument(name = "reencrypt_folders", skip(folders, ctx))]
87fn reencrypt_folders(
88    folders: &[bitwarden_vault::Folder],
89    current_key: SymmetricKeyId,
90    new_key: SymmetricKeyId,
91    ctx: &mut KeyStoreContext<KeyIds>,
92) -> Result<Vec<bitwarden_vault::Folder>, DataReencryptionError> {
93    folders
94        .iter()
95        .map(|folder| {
96            let _span = debug_span!("reencrypt_folder", folder_id = ?folder.id).entered();
97            let folder_view: FolderView = folder
98                .decrypt(ctx, current_key)
99                .map_err(|_| DataReencryptionError::Decryption)?;
100            folder_view
101                .encrypt_composite(ctx, new_key)
102                .map_err(|_| DataReencryptionError::Encryption)
103        })
104        .collect::<Result<Vec<bitwarden_vault::Folder>, DataReencryptionError>>()
105}
106
107#[instrument(name = "reencrypt_ciphers", skip(ciphers, ctx))]
108fn reencrypt_ciphers(
109    ciphers: &[bitwarden_vault::Cipher],
110    current_key: SymmetricKeyId,
111    new_key: SymmetricKeyId,
112    ctx: &mut KeyStoreContext<KeyIds>,
113) -> Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError> {
114    ciphers
115        .iter()
116        .map(|cipher| {
117            let _span = debug_span!("reencrypt_cipher", cipher_id = ?cipher.id).entered();
118
119            // If the cipher has a per-vault-item cipher-key, the cipher-key
120            // is re-wrapped
121            if cipher.key.is_some() {
122                debug!("Re-wrapping cipher key without decrypting cipher");
123                let mut cipher = cipher.clone();
124                cipher
125                    .rewrap_cipher_key(current_key, new_key, ctx)
126                    .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
127                Ok(cipher)
128
129            // If the cipher has no cipher-key, the entire cipher is decrypted and re-encrypted
130            // and has to be re-uploaded.
131            } else {
132                debug!("Cipher has no cipher key, decrypting and re-encrypting entire cipher");
133                let cipher_view: CipherView = cipher
134                    .decrypt(ctx, current_key)
135                    .map_err(|_| DataReencryptionError::Decryption)?;
136                cipher_view
137                    .encrypt_composite(ctx, new_key)
138                    .map_err(|_| DataReencryptionError::Encryption)
139            }
140        })
141        .collect::<Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError>>()
142}
143
144#[instrument(name = "reencrypt_sends", skip(sends, ctx))]
145fn reencrypt_sends(
146    sends: &[bitwarden_send::Send],
147    current_key: SymmetricKeyId,
148    new_key: SymmetricKeyId,
149    ctx: &mut KeyStoreContext<KeyIds>,
150) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
151    sends
152        .iter()
153        .map(|send| {
154            let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
155            let send_view: SendView = send
156                .decrypt(ctx, current_key)
157                .map_err(|_| DataReencryptionError::Decryption)?;
158            send_view
159                .encrypt_composite(ctx, new_key)
160                .map_err(|_| DataReencryptionError::Encryption)
161        })
162        .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
163}
164
165#[cfg(test)]
166mod tests {
167    use bitwarden_core::key_management::KeyIds;
168    use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore};
169    use bitwarden_send::SendView;
170    use chrono::Utc;
171
172    #[test]
173    fn test_ciphers() {
174        use bitwarden_vault::{CipherType, CipherView, LoginView};
175        let store: KeyStore<KeyIds> = KeyStore::default();
176        let mut ctx = store.context_mut();
177
178        let user_key_old =
179            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
180        let user_key_new =
181            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
182
183        let cipher = CipherView {
184            id: None,
185            organization_id: None,
186            folder_id: None,
187            r#type: CipherType::Login,
188            name: "Test Cipher".to_string(),
189            notes: Some("Some cipher notes".to_string()),
190            favorite: false,
191            revision_date: Utc::now(),
192            deleted_date: None,
193            fields: None,
194            login: Some(LoginView {
195                username: Some("user".to_string()),
196                password: Some("pass".to_string()),
197                totp: None,
198                uris: None,
199                autofill_on_page_load: None,
200                fido2_credentials: None,
201                password_revision_date: None,
202            }),
203            card: None,
204            identity: None,
205            secure_note: None,
206            attachments: None,
207            attachment_decryption_failures: None,
208            organization_use_totp: false,
209            collection_ids: vec![],
210            reprompt: bitwarden_vault::CipherRepromptType::None,
211            local_data: None,
212            key: None,
213            ssh_key: None,
214            permissions: None,
215            view_password: false,
216            creation_date: Utc::now(),
217            archived_date: None,
218            edit: false,
219            password_history: None,
220        };
221        let encrypted_cipher = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap();
222
223        // Rotate it
224        let ciphers = vec![encrypted_cipher];
225        let reencrypted_ciphers =
226            super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
227                .unwrap();
228
229        // Decrypt and assert
230        let decrypted_cipher: CipherView = reencrypted_ciphers[0]
231            .decrypt(&mut ctx, user_key_new)
232            .unwrap();
233        assert_eq!(cipher.name, decrypted_cipher.name);
234        assert_eq!(cipher.notes, decrypted_cipher.notes);
235        assert_eq!(cipher.r#type, decrypted_cipher.r#type);
236        assert_eq!(
237            cipher.login.as_ref().unwrap().username,
238            decrypted_cipher.login.as_ref().unwrap().username
239        );
240        assert_eq!(
241            cipher.login.as_ref().unwrap().password,
242            decrypted_cipher.login.as_ref().unwrap().password
243        );
244    }
245
246    #[test]
247    fn test_folders() {
248        let store: KeyStore<KeyIds> = KeyStore::default();
249        let mut ctx = store.context_mut();
250
251        let user_key_old =
252            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
253        let user_key_new =
254            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
255
256        // Create an encrypted folder
257        let folder = bitwarden_vault::FolderView {
258            id: None,
259            name: "Test Folder".to_string(),
260            revision_date: Utc::now(),
261        };
262        let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
263
264        // Rotate it
265        let folders = vec![encrypted_folder];
266        let reencrypted_folders =
267            super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
268                .unwrap();
269
270        // Decrypt and assert
271        let decrypted_folder = reencrypted_folders[0]
272            .decrypt(&mut ctx, user_key_new)
273            .unwrap();
274        assert_eq!(folder, decrypted_folder);
275    }
276
277    #[test]
278    fn test_sends() {
279        let store: KeyStore<KeyIds> = KeyStore::default();
280        let mut ctx = store.context_mut();
281
282        let user_key_old =
283            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
284        let user_key_new =
285            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
286
287        // Create an encrypted send
288        let send = bitwarden_send::SendView {
289            id: None,
290            access_id: None,
291            name: "Test Send".to_string(),
292            notes: Some("Some notes".to_string()),
293            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
294            text: Some(bitwarden_send::SendTextView {
295                text: Some("This is a test send".to_string()),
296                hidden: false,
297            }),
298            r#type: bitwarden_send::SendType::Text,
299            max_access_count: None,
300            access_count: 0,
301            disabled: false,
302            hide_email: false,
303            revision_date: Utc::now(),
304            deletion_date: Utc::now(),
305            expiration_date: None,
306            new_password: None,
307            has_password: false,
308            file: None,
309            emails: vec![],
310            auth_type: bitwarden_send::AuthType::None,
311        };
312        let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
313
314        // Rotate it
315        let sends = vec![encrypted_send];
316        let reencrypted_sends =
317            super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
318
319        // Decrypt and assert
320        let decrypted_send: SendView = reencrypted_sends[0]
321            .decrypt(&mut ctx, user_key_new)
322            .unwrap();
323
324        // The send seed must be the same
325        assert_eq!(send.key, decrypted_send.key);
326    }
327}