Skip to main content

bitwarden_user_crypto_management/public_key_encryption_key_pair_regeneration/
regenerate.rs

1use bitwarden_api_api::models::KeyRegenerationRequestModel;
2use bitwarden_core::key_management::{KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId};
3use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm};
4use bitwarden_encoding::B64;
5use tracing::{error, info, instrument};
6
7use super::KeyPairRegenerationError;
8
9/// Generates a new public key encryption key pair, submits it to the server, and
10/// persists the new private key in the key store.
11#[instrument(name = "regenerate_public_key_encryption_key_pair", skip_all, err)]
12pub(super) async fn internal_regenerate_public_key_encryption_key_pair(
13    key_store: &KeyStore<KeySlotIds>,
14    api_client: &bitwarden_api_api::apis::ApiClient,
15) -> Result<(), KeyPairRegenerationError> {
16    let (wrapped_private_key, public_key_b64) = {
17        let mut ctx = key_store.context_mut();
18
19        if !ctx
20            .is_v1_symmetric_key(SymmetricKeySlotId::User)
21            .map_err(|_| KeyPairRegenerationError::UserKeyNotAvailable)?
22        {
23            return Err(KeyPairRegenerationError::Crypto);
24        }
25
26        let new_private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
27        let wrapped = ctx
28            .wrap_private_key(SymmetricKeySlotId::User, new_private_key_id)
29            .map_err(|_| KeyPairRegenerationError::Crypto)?;
30        let public_key = ctx
31            .get_public_key(new_private_key_id)
32            .map_err(|_| KeyPairRegenerationError::Crypto)?;
33        let public_key_b64 = B64::from(
34            public_key
35                .to_der()
36                .map_err(|_| KeyPairRegenerationError::Crypto)?,
37        )
38        .to_string();
39
40        (wrapped, public_key_b64)
41    };
42
43    info!("Posting regenerated public key encryption key pair to server");
44    let request = KeyRegenerationRequestModel {
45        user_public_key: Some(public_key_b64),
46        user_key_encrypted_user_private_key: Some(wrapped_private_key.to_string()),
47    };
48
49    api_client
50        .accounts_key_management_api()
51        .regenerate_keys(Some(request))
52        .await
53        .map_err(|e| {
54            error!("Failed to post regenerated keys to server: {e:?}");
55            KeyPairRegenerationError::Api
56        })?;
57
58    {
59        let mut ctx = key_store.context_mut();
60
61        let temp_private_key_id = ctx
62            .unwrap_private_key(SymmetricKeySlotId::User, &wrapped_private_key)
63            .map_err(|_| KeyPairRegenerationError::Crypto)?;
64        ctx.persist_private_key(temp_private_key_id, PrivateKeySlotId::UserPrivateKey)
65            .map_err(|_| KeyPairRegenerationError::Crypto)?;
66    }
67
68    info!("Successfully regenerated user public key encryption key pair");
69    Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74    use bitwarden_api_api::{apis::ApiClient, models::KeyRegenerationRequestModel};
75    use bitwarden_core::{
76        Client,
77        key_management::{KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId},
78    };
79    use bitwarden_crypto::{
80        EncString, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm,
81    };
82
83    use super::*;
84    use crate::UserCryptoManagementClient;
85
86    fn unlocked_v1_client() -> (UserCryptoManagementClient, KeyStore<KeySlotIds>) {
87        let client = Client::new(None);
88        {
89            let key_store = client.internal.get_key_store();
90            let mut ctx = key_store.context_mut();
91            let local_user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
92            let _ = ctx.persist_symmetric_key(local_user_key, SymmetricKeySlotId::User);
93        }
94        let key_store = client.internal.get_key_store().clone();
95        (UserCryptoManagementClient::new(client), key_store)
96    }
97
98    fn unlocked_v1_key_store() -> KeyStore<KeySlotIds> {
99        let store: KeyStore<KeySlotIds> = KeyStore::default();
100        {
101            let mut ctx = store.context_mut();
102            let local_user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
103            let _ = ctx.persist_symmetric_key(local_user_key, SymmetricKeySlotId::User);
104        }
105        store
106    }
107
108    fn make_valid_key_pair(key_store: &KeyStore<KeySlotIds>) -> (String, String) {
109        let mut ctx = key_store.context_mut();
110        let private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
111        let wrapped = ctx
112            .wrap_private_key(SymmetricKeySlotId::User, private_key_id)
113            .unwrap();
114        let public_key = ctx.get_public_key(private_key_id).unwrap();
115        let public_key_b64 = B64::from(public_key.to_der().unwrap()).to_string();
116        (wrapped.to_string(), public_key_b64)
117    }
118
119    fn keys_response(
120        public_key: Option<String>,
121        private_key: Option<String>,
122    ) -> bitwarden_api_api::models::KeysResponseModel {
123        bitwarden_api_api::models::KeysResponseModel {
124            object: None,
125            key: None,
126            public_key,
127            private_key,
128            account_keys: None,
129        }
130    }
131
132    // ── regenerate_key_pair tests ──
133
134    #[tokio::test]
135    async fn test_regenerate_success() {
136        let (_, key_store) = unlocked_v1_client();
137
138        let api_client = ApiClient::new_mocked(|mock| {
139            mock.accounts_key_management_api
140                .expect_regenerate_keys()
141                .once()
142                .returning(|body: Option<KeyRegenerationRequestModel>| {
143                    let body = body.expect("body should be Some");
144                    assert!(
145                        body.user_public_key.is_some(),
146                        "user_public_key should be present"
147                    );
148                    let wrapped_key = body
149                        .user_key_encrypted_user_private_key
150                        .expect("user_key_encrypted_user_private_key should be present");
151                    wrapped_key
152                        .parse::<EncString>()
153                        .expect("should be a valid EncString");
154                    Ok(())
155                });
156        });
157
158        internal_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
159            .await
160            .expect("regeneration should succeed");
161
162        let ctx = key_store.context();
163        assert!(
164            ctx.has_private_key(PrivateKeySlotId::UserPrivateKey),
165            "UserPrivateKey should be set after regeneration"
166        );
167
168        if let ApiClient::Mock(mut mock) = api_client {
169            mock.accounts_key_management_api.checkpoint();
170        }
171    }
172
173    #[tokio::test]
174    async fn test_regenerate_api_failure() {
175        let (_, key_store) = unlocked_v1_client();
176
177        let api_client = ApiClient::new_mocked(|mock| {
178            mock.accounts_key_management_api
179                .expect_regenerate_keys()
180                .once()
181                .returning(|_body| {
182                    Err(bitwarden_api_api::apis::Error::Serde(
183                        serde_json::Error::io(std::io::Error::other("API error")),
184                    ))
185                });
186        });
187
188        let result =
189            internal_regenerate_public_key_encryption_key_pair(&key_store, &api_client).await;
190        assert!(matches!(result, Err(KeyPairRegenerationError::Api)));
191
192        {
193            let ctx = key_store.context();
194            assert!(
195                !ctx.has_private_key(PrivateKeySlotId::UserPrivateKey),
196                "UserPrivateKey should NOT be set after API failure"
197            );
198        }
199
200        if let ApiClient::Mock(mut mock) = api_client {
201            mock.accounts_key_management_api.checkpoint();
202        }
203    }
204
205    #[tokio::test]
206    async fn test_regenerate_no_user_key() {
207        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
208
209        let api_client = ApiClient::new_mocked(|mock| {
210            mock.accounts_key_management_api
211                .expect_regenerate_keys()
212                .never();
213        });
214
215        let result =
216            internal_regenerate_public_key_encryption_key_pair(&key_store, &api_client).await;
217        assert!(matches!(
218            result,
219            Err(KeyPairRegenerationError::UserKeyNotAvailable)
220        ));
221
222        if let ApiClient::Mock(mut mock) = api_client {
223            mock.accounts_key_management_api.checkpoint();
224        }
225    }
226
227    #[tokio::test]
228    async fn test_regenerate_v2_user_key() {
229        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
230        {
231            let mut ctx = key_store.context_mut();
232            let key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
233            let _ = ctx.persist_symmetric_key(key, SymmetricKeySlotId::User);
234        }
235
236        let api_client = ApiClient::new_mocked(|mock| {
237            mock.accounts_key_management_api
238                .expect_regenerate_keys()
239                .never();
240        });
241
242        let result =
243            internal_regenerate_public_key_encryption_key_pair(&key_store, &api_client).await;
244        assert!(matches!(result, Err(KeyPairRegenerationError::Crypto)));
245
246        if let ApiClient::Mock(mut mock) = api_client {
247            mock.accounts_key_management_api.checkpoint();
248        }
249    }
250
251    // ── regenerate_if_needed tests ──
252
253    #[tokio::test]
254    async fn test_regenerate_if_needed_no_regeneration() {
255        let key_store = unlocked_v1_key_store();
256        let (wrapped_private_key, public_key_b64) = make_valid_key_pair(&key_store);
257
258        let api_client = ApiClient::new_mocked(|mock| {
259            mock.accounts_api
260                .expect_get_keys()
261                .once()
262                .returning(move || {
263                    Ok(keys_response(
264                        Some(public_key_b64.clone()),
265                        Some(wrapped_private_key.clone()),
266                    ))
267                });
268            mock.accounts_key_management_api
269                .expect_regenerate_keys()
270                .never();
271        });
272
273        let should = crate::public_key_encryption_key_pair_regeneration::should_regenerate::internal_should_regenerate_public_key_encryption_key_pair_with_ciphers(
274            &key_store, &api_client, &[],
275        )
276        .await
277        .unwrap();
278        assert!(!should);
279
280        if let ApiClient::Mock(mut mock) = api_client {
281            mock.accounts_api.checkpoint();
282            mock.accounts_key_management_api.checkpoint();
283        }
284    }
285
286    #[tokio::test]
287    async fn test_regenerate_if_needed_performs_regeneration() {
288        let key_store = unlocked_v1_key_store();
289
290        let api_client = ApiClient::new_mocked(|mock| {
291            mock.accounts_api
292                .expect_get_keys()
293                .once()
294                .returning(|| Ok(keys_response(None, None)));
295            mock.accounts_key_management_api
296                .expect_regenerate_keys()
297                .once()
298                .returning(|_body| Ok(()));
299        });
300
301        let should = crate::public_key_encryption_key_pair_regeneration::should_regenerate::internal_should_regenerate_public_key_encryption_key_pair_with_ciphers(
302            &key_store, &api_client, &[],
303        )
304        .await
305        .unwrap();
306        assert!(should);
307
308        internal_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
309            .await
310            .unwrap();
311
312        if let ApiClient::Mock(mut mock) = api_client {
313            mock.accounts_api.checkpoint();
314            mock.accounts_key_management_api.checkpoint();
315        }
316    }
317}