Skip to main content

bitwarden_user_crypto_management/public_key_encryption_key_pair_regeneration/
should_regenerate.rs

1use std::str::FromStr;
2
3use bitwarden_core::key_management::{KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId};
4use bitwarden_crypto::{EncString, KeyStore};
5use bitwarden_encoding::B64;
6use bitwarden_vault::{Cipher, CipherView};
7use tracing::{error, info, warn};
8
9use super::KeyPairRegenerationError;
10
11/// Checks whether the user's public key encryption key pair needs regeneration.
12///
13/// When the private key cannot be decrypted, validates the user key by attempting to
14/// decrypt a personal cipher fetched from the API.
15#[bitwarden_logging::instrument(name = "should_regenerate_public_key_encryption_key_pair", err)]
16pub(super) async fn internal_should_regenerate_public_key_encryption_key_pair(
17    key_store: &KeyStore<KeySlotIds>,
18    api_client: &bitwarden_api_api::apis::ApiClient,
19) -> Result<bool, KeyPairRegenerationError> {
20    match check_key_pair(key_store, api_client).await? {
21        KeyPairCheckResult::Valid => Ok(false),
22        KeyPairCheckResult::NeedsRegeneration => Ok(true),
23        KeyPairCheckResult::RegenerateIfUserKeyIsValid => {
24            is_user_key_valid_from_api(key_store, api_client).await
25        }
26    }
27}
28
29/// Checks whether the user's public key encryption key pair needs regeneration.
30///
31/// When the private key cannot be decrypted, validates the user key by attempting to
32/// decrypt one of the provided ciphers.
33#[bitwarden_logging::instrument(
34    name = "should_regenerate_public_key_encryption_key_pair_with_ciphers",
35    err
36)]
37pub(super) async fn internal_should_regenerate_public_key_encryption_key_pair_with_ciphers(
38    key_store: &KeyStore<KeySlotIds>,
39    api_client: &bitwarden_api_api::apis::ApiClient,
40    ciphers: &[Cipher],
41) -> Result<bool, KeyPairRegenerationError> {
42    match check_key_pair(key_store, api_client).await? {
43        KeyPairCheckResult::Valid => Ok(false),
44        KeyPairCheckResult::NeedsRegeneration => Ok(true),
45        KeyPairCheckResult::RegenerateIfUserKeyIsValid => is_user_key_valid(key_store, ciphers),
46    }
47}
48
49enum KeyPairCheckResult {
50    Valid,
51    NeedsRegeneration,
52    /// Private key is undecryptable — need to confirm user key is valid (e.g. via cipher
53    /// decryption) before regenerating.
54    RegenerateIfUserKeyIsValid,
55}
56
57/// Returns whether the key pair is valid, needs regeneration, or requires a cipher-based
58/// user key check to decide.
59async fn check_key_pair(
60    key_store: &KeyStore<KeySlotIds>,
61    api_client: &bitwarden_api_api::apis::ApiClient,
62) -> Result<KeyPairCheckResult, KeyPairRegenerationError> {
63    // Step 1-2: Check user key availability and encryption version
64    {
65        let ctx = key_store.context();
66
67        if !ctx.has_symmetric_key(SymmetricKeySlotId::User) {
68            info!("User key not available, skipping key pair regeneration check");
69            return Ok(KeyPairCheckResult::Valid);
70        }
71
72        if !ctx
73            .is_v1_symmetric_key(SymmetricKeySlotId::User)
74            .map_err(|_| KeyPairRegenerationError::Crypto)?
75        {
76            info!("User has non-V1 encryption, key pair regeneration not applicable");
77            return Ok(KeyPairCheckResult::Valid);
78        }
79    }
80
81    // Step 3: Fetch key pair from server. A 404 means the user has no keys at all.
82    let keys_response = match api_client.accounts_api().get_keys().await {
83        Ok(response) => response,
84        Err(bitwarden_api_api::apis::Error::Response(e))
85            if e.status == reqwest::StatusCode::NOT_FOUND =>
86        {
87            info!("User has no public key encryption key pair (404), regeneration needed");
88            return Ok(KeyPairCheckResult::NeedsRegeneration);
89        }
90        Err(e) => {
91            error!("Failed to fetch user keys from server: {e:?}");
92            return Err(KeyPairRegenerationError::Api);
93        }
94    };
95
96    let public_key_str = keys_response.public_key.as_deref();
97    let private_key_str = keys_response.private_key.as_deref();
98
99    // Step 4: Handle missing key pair, or proceed with verification
100    let (public_key_str, private_key_str) = match (public_key_str, private_key_str) {
101        (None, None) => {
102            info!("User has no public key encryption key pair, regeneration needed");
103            return Ok(KeyPairCheckResult::NeedsRegeneration);
104        }
105        (Some(_), None) | (None, Some(_)) => {
106            info!(
107                "User has inconsistent public key encryption key pair (one present, one missing), \
108                 regeneration needed"
109            );
110            return Ok(KeyPairCheckResult::NeedsRegeneration);
111        }
112        (Some(pub_key), Some(priv_key)) => (pub_key, priv_key),
113    };
114
115    let Ok(encrypted_private_key) = private_key_str.parse::<EncString>() else {
116        info!("User's private key is not a valid encrypted string, regeneration needed");
117        return Ok(KeyPairCheckResult::NeedsRegeneration);
118    };
119
120    // Step 5: Verify existing key pair using key store
121    {
122        let mut ctx = key_store.context_mut();
123
124        if let Ok(temp_private_key_id) =
125            ctx.unwrap_private_key(SymmetricKeySlotId::User, &encrypted_private_key)
126        {
127            return match verify_public_key_matches(&ctx, temp_private_key_id, public_key_str) {
128                Ok(true) => {
129                    info!("User's public key encryption key pair is valid, no regeneration needed");
130                    Ok(KeyPairCheckResult::Valid)
131                }
132                Ok(false) => {
133                    info!(
134                        "User's private key is decryptable but does not match public key, \
135                         regeneration needed"
136                    );
137                    Ok(KeyPairCheckResult::NeedsRegeneration)
138                }
139                Err(_) => {
140                    info!(
141                        "User's private key is decryptable but public key derivation failed, \
142                         regeneration needed"
143                    );
144                    Ok(KeyPairCheckResult::NeedsRegeneration)
145                }
146            };
147        }
148    }
149
150    // Step 6: Private key is undecryptable — need to validate user key before regenerating
151    Ok(KeyPairCheckResult::RegenerateIfUserKeyIsValid)
152}
153
154/// Validates the user key by fetching ciphers from the API and attempting to decrypt a personal
155/// one. Returns `Ok(true)` (should regenerate) if the user key is valid, `Ok(false)` otherwise.
156async fn is_user_key_valid_from_api(
157    key_store: &KeyStore<KeySlotIds>,
158    api_client: &bitwarden_api_api::apis::ApiClient,
159) -> Result<bool, KeyPairRegenerationError> {
160    let Ok(ciphers_response) = api_client.ciphers_api().get_all().await else {
161        warn!("Failed to fetch ciphers for user key validation, skipping regeneration");
162        return Ok(false);
163    };
164
165    let personal_cipher = ciphers_response
166        .data
167        .into_iter()
168        .flatten()
169        .find(|c| c.organization_id.is_none());
170
171    let Some(cipher_response) = personal_cipher else {
172        warn!("No personal ciphers available for user key validation, skipping regeneration");
173        return Ok(false);
174    };
175
176    let Ok(cipher) = Cipher::try_from(cipher_response) else {
177        warn!("Failed to parse cipher for user key validation, skipping regeneration");
178        return Ok(false);
179    };
180
181    is_user_key_valid(key_store, std::slice::from_ref(&cipher))
182}
183
184/// Validates the user key by attempting to decrypt a personal cipher. Returns `Ok(true)`
185/// (should regenerate) if the user key is valid, `Ok(false)` otherwise.
186fn is_user_key_valid(
187    key_store: &KeyStore<KeySlotIds>,
188    ciphers: &[Cipher],
189) -> Result<bool, KeyPairRegenerationError> {
190    let Some(cipher) = ciphers
191        .iter()
192        .find(|cipher| cipher.organization_id.is_none())
193    else {
194        warn!("No personal ciphers available for user key validation, skipping regeneration");
195        return Ok(false);
196    };
197
198    if key_store.decrypt::<_, _, CipherView>(cipher).is_ok() {
199        info!(
200            "User's private key cannot be decrypted but user key can decrypt vault data, \
201             regeneration needed"
202        );
203        Ok(true)
204    } else {
205        warn!(
206            "User's private key cannot be decrypted and user key cannot decrypt vault data, \
207             skipping regeneration"
208        );
209        Ok(false)
210    }
211}
212
213/// Verifies that the public key derived from the decrypted private key matches the
214/// public key stored on the server.
215fn verify_public_key_matches(
216    ctx: &bitwarden_crypto::KeyStoreContext<KeySlotIds>,
217    private_key_id: PrivateKeySlotId,
218    server_public_key_b64: &str,
219) -> Result<bool, KeyPairRegenerationError> {
220    let derived_public_key = ctx
221        .get_public_key(private_key_id)
222        .map_err(|_| KeyPairRegenerationError::Crypto)?;
223    let derived_b64 = B64::from(
224        derived_public_key
225            .to_der()
226            .map_err(|_| KeyPairRegenerationError::Crypto)?,
227    );
228    let server_b64 =
229        B64::from_str(server_public_key_b64).map_err(|_| KeyPairRegenerationError::Crypto)?;
230    Ok(derived_b64.to_string() == server_b64.to_string())
231}
232
233#[cfg(test)]
234mod tests {
235    use bitwarden_api_api::{
236        apis::ApiClient,
237        models::{
238            CipherDetailsResponseModel, CipherDetailsResponseModelListResponseModel,
239            KeysResponseModel,
240        },
241    };
242    use bitwarden_core::{
243        Client,
244        key_management::{KeySlotIds, SymmetricKeySlotId},
245    };
246    use bitwarden_crypto::{
247        EncString, KeyStore, PrimitiveEncryptable, PublicKeyEncryptionAlgorithm,
248        SymmetricKeyAlgorithm,
249    };
250    use bitwarden_encoding::B64;
251
252    use super::*;
253    use crate::UserCryptoManagementClient;
254
255    fn unlocked_v1_key_store() -> KeyStore<KeySlotIds> {
256        let store: KeyStore<KeySlotIds> = KeyStore::default();
257        {
258            let mut ctx = store.context_mut();
259            let local_user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
260            let _ = ctx.persist_symmetric_key(local_user_key, SymmetricKeySlotId::User);
261        }
262        store
263    }
264
265    fn unlocked_v1_client() -> (UserCryptoManagementClient, KeyStore<KeySlotIds>) {
266        let client = Client::new(None);
267        {
268            let key_store = client.internal.get_key_store();
269            let mut ctx = key_store.context_mut();
270            let local_user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
271            let _ = ctx.persist_symmetric_key(local_user_key, SymmetricKeySlotId::User);
272        }
273        let key_store = client.internal.get_key_store().clone();
274        (UserCryptoManagementClient::new(client), key_store)
275    }
276
277    fn make_valid_key_pair(key_store: &KeyStore<KeySlotIds>) -> (String, String) {
278        let mut ctx = key_store.context_mut();
279        let private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
280        let wrapped = ctx
281            .wrap_private_key(SymmetricKeySlotId::User, private_key_id)
282            .unwrap();
283        let public_key = ctx.get_public_key(private_key_id).unwrap();
284        let public_key_b64 = B64::from(public_key.to_der().unwrap()).to_string();
285        (wrapped.to_string(), public_key_b64)
286    }
287
288    fn keys_response(public_key: Option<String>, private_key: Option<String>) -> KeysResponseModel {
289        KeysResponseModel {
290            object: None,
291            key: None,
292            public_key,
293            private_key,
294            account_keys: None,
295        }
296    }
297
298    #[tokio::test]
299    async fn test_should_regenerate_no_user_key() {
300        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
301
302        let api_client = ApiClient::new_mocked(|mock| {
303            mock.accounts_api.expect_get_keys().never();
304        });
305
306        let result =
307            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
308                .await;
309        assert!(matches!(result, Ok(false)));
310
311        if let ApiClient::Mock(mut mock) = api_client {
312            mock.accounts_api.checkpoint();
313        }
314    }
315
316    #[tokio::test]
317    async fn test_should_regenerate_v2_encryption() {
318        let key_store: KeyStore<KeySlotIds> = KeyStore::default();
319        {
320            let mut ctx = key_store.context_mut();
321            let key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
322            let _ = ctx.persist_symmetric_key(key, SymmetricKeySlotId::User);
323        }
324
325        let api_client = ApiClient::new_mocked(|mock| {
326            mock.accounts_api.expect_get_keys().never();
327        });
328
329        let result =
330            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
331                .await;
332        // V2 encryption should not regenerate
333        assert!(matches!(result, Ok(false)));
334
335        if let ApiClient::Mock(mut mock) = api_client {
336            mock.accounts_api.checkpoint();
337        }
338    }
339
340    #[tokio::test]
341    async fn test_should_regenerate_get_keys_api_error() {
342        let key_store = unlocked_v1_key_store();
343
344        let api_client = ApiClient::new_mocked(|mock| {
345            mock.accounts_api.expect_get_keys().once().returning(|| {
346                Err(bitwarden_api_api::apis::Error::Response(
347                    bitwarden_api_api::apis::ResponseContent {
348                        status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
349                        message: "Internal Server Error".to_string(),
350                    },
351                ))
352            });
353        });
354
355        let result =
356            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
357                .await;
358        assert!(matches!(result, Err(KeyPairRegenerationError::Api)));
359
360        if let ApiClient::Mock(mut mock) = api_client {
361            mock.accounts_api.checkpoint();
362        }
363    }
364
365    #[tokio::test]
366    async fn test_should_regenerate_get_keys_404() {
367        let key_store = unlocked_v1_key_store();
368
369        let api_client = ApiClient::new_mocked(|mock| {
370            mock.accounts_api.expect_get_keys().once().returning(|| {
371                Err(bitwarden_api_api::apis::Error::Response(
372                    bitwarden_api_api::apis::ResponseContent {
373                        status: reqwest::StatusCode::NOT_FOUND,
374                        message: "Not Found".to_string(),
375                    },
376                ))
377            });
378        });
379
380        let result =
381            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
382                .await;
383        assert!(matches!(result, Ok(true)));
384
385        if let ApiClient::Mock(mut mock) = api_client {
386            mock.accounts_api.checkpoint();
387        }
388    }
389
390    #[tokio::test]
391    async fn test_should_regenerate_no_key_pair_on_server() {
392        let key_store = unlocked_v1_key_store();
393
394        let api_client = ApiClient::new_mocked(|mock| {
395            mock.accounts_api
396                .expect_get_keys()
397                .once()
398                .returning(|| Ok(keys_response(None, None)));
399        });
400
401        let result =
402            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
403                .await;
404        assert!(matches!(result, Ok(true)));
405
406        if let ApiClient::Mock(mut mock) = api_client {
407            mock.accounts_api.checkpoint();
408        }
409    }
410
411    #[tokio::test]
412    async fn test_should_regenerate_inconsistent_key_pair_public_only() {
413        let key_store = unlocked_v1_key_store();
414
415        let api_client = ApiClient::new_mocked(|mock| {
416            mock.accounts_api
417                .expect_get_keys()
418                .once()
419                .returning(|| Ok(keys_response(Some("some-public-key".to_string()), None)));
420        });
421
422        let result =
423            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
424                .await;
425        assert!(matches!(result, Ok(true)));
426
427        if let ApiClient::Mock(mut mock) = api_client {
428            mock.accounts_api.checkpoint();
429        }
430    }
431
432    #[tokio::test]
433    async fn test_should_regenerate_inconsistent_key_pair_private_only() {
434        let key_store = unlocked_v1_key_store();
435
436        let api_client = ApiClient::new_mocked(|mock| {
437            mock.accounts_api
438                .expect_get_keys()
439                .once()
440                .returning(|| Ok(keys_response(None, Some("some-private-key".to_string()))));
441        });
442
443        let result =
444            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
445                .await;
446        assert!(matches!(result, Ok(true)));
447
448        if let ApiClient::Mock(mut mock) = api_client {
449            mock.accounts_api.checkpoint();
450        }
451    }
452
453    #[tokio::test]
454    async fn test_should_regenerate_valid_key_pair() {
455        let key_store = unlocked_v1_key_store();
456        let (wrapped_private_key, public_key_b64) = make_valid_key_pair(&key_store);
457
458        let api_client = ApiClient::new_mocked(|mock| {
459            mock.accounts_api
460                .expect_get_keys()
461                .once()
462                .returning(move || {
463                    Ok(keys_response(
464                        Some(public_key_b64.clone()),
465                        Some(wrapped_private_key.clone()),
466                    ))
467                });
468        });
469
470        let result =
471            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
472                .await;
473        assert!(matches!(result, Ok(false)));
474
475        if let ApiClient::Mock(mut mock) = api_client {
476            mock.accounts_api.checkpoint();
477        }
478    }
479
480    #[tokio::test]
481    async fn test_should_regenerate_undecryptable_private_key_no_ciphers() {
482        let key_store = unlocked_v1_key_store();
483
484        let api_client = ApiClient::new_mocked(|mock| {
485            mock.accounts_api
486                .expect_get_keys()
487                .once()
488                .returning(|| {
489                    Ok(keys_response(
490                        Some("some-public-key".to_string()),
491                        Some("2.AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string()),
492                    ))
493                });
494            mock.ciphers_api.expect_get_all().once().returning(|| {
495                Ok(CipherDetailsResponseModelListResponseModel {
496                    object: None,
497                    data: Some(vec![]),
498                    continuation_token: None,
499                })
500            });
501        });
502
503        let result =
504            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
505                .await;
506        assert!(matches!(result, Ok(false)));
507
508        if let ApiClient::Mock(mut mock) = api_client {
509            mock.accounts_api.checkpoint();
510            mock.ciphers_api.checkpoint();
511        }
512    }
513
514    #[tokio::test]
515    async fn test_should_regenerate_undecryptable_private_key_cipher_fetch_fails() {
516        let key_store = unlocked_v1_key_store();
517
518        let api_client = ApiClient::new_mocked(|mock| {
519            mock.accounts_api
520                .expect_get_keys()
521                .once()
522                .returning(|| {
523                    Ok(keys_response(
524                        Some("some-public-key".to_string()),
525                        Some("2.AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string()),
526                    ))
527                });
528            mock.ciphers_api.expect_get_all().once().returning(|| {
529                Err(bitwarden_api_api::apis::Error::Serde(
530                    serde_json::Error::io(std::io::Error::other("API error")),
531                ))
532            });
533        });
534
535        let result =
536            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
537                .await;
538        assert!(matches!(result, Ok(false)));
539
540        if let ApiClient::Mock(mut mock) = api_client {
541            mock.accounts_api.checkpoint();
542            mock.ciphers_api.checkpoint();
543        }
544    }
545
546    #[tokio::test]
547    async fn test_should_regenerate_undecryptable_private_key_with_valid_user_key() {
548        let (_, key_store) = unlocked_v1_client();
549
550        let encrypted_name = {
551            let mut ctx = key_store.context_mut();
552            let name: EncString = "test cipher"
553                .to_string()
554                .encrypt(&mut ctx, SymmetricKeySlotId::User)
555                .unwrap();
556            name.to_string()
557        };
558
559        let api_client = ApiClient::new_mocked(|mock| {
560            mock.accounts_api
561                .expect_get_keys()
562                .once()
563                .returning(|| {
564                    Ok(keys_response(
565                        Some("some-public-key".to_string()),
566                        Some("2.AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string()),
567                    ))
568                });
569            mock.ciphers_api.expect_get_all().once().returning(move || {
570                Ok(CipherDetailsResponseModelListResponseModel {
571                    object: None,
572                    data: Some(vec![CipherDetailsResponseModel {
573                        id: Some(uuid::Uuid::new_v4()),
574                        name: Some(encrypted_name.clone()),
575                        organization_id: None,
576                        r#type: Some(bitwarden_api_api::models::CipherType::Login),
577                        revision_date: Some("2024-01-01T00:00:00Z".to_string()),
578                        creation_date: Some("2024-01-01T00:00:00Z".to_string()),
579                        ..CipherDetailsResponseModel::default()
580                    }]),
581                    continuation_token: None,
582                })
583            });
584        });
585
586        let result =
587            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
588                .await;
589        assert!(matches!(result, Ok(true)));
590
591        if let ApiClient::Mock(mut mock) = api_client {
592            mock.accounts_api.checkpoint();
593            mock.ciphers_api.checkpoint();
594        }
595    }
596
597    #[tokio::test]
598    async fn test_should_regenerate_invalid_enc_string() {
599        let key_store = unlocked_v1_key_store();
600
601        let api_client = ApiClient::new_mocked(|mock| {
602            mock.accounts_api.expect_get_keys().once().returning(|| {
603                Ok(keys_response(
604                    Some("some-public-key".to_string()),
605                    Some("not-a-valid-enc-string".to_string()),
606                ))
607            });
608        });
609
610        let result =
611            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
612                .await;
613        assert!(matches!(result, Ok(true)));
614
615        if let ApiClient::Mock(mut mock) = api_client {
616            mock.accounts_api.checkpoint();
617        }
618    }
619
620    #[tokio::test]
621    async fn test_should_regenerate_decryptable_but_malformed_private_key() {
622        let (_, key_store) = unlocked_v1_client();
623
624        let (wrapped_malformed_private_key, encrypted_name) = {
625            let mut ctx = key_store.context_mut();
626            let malformed_private_key: EncString = "not a valid RSA key"
627                .to_string()
628                .encrypt(&mut ctx, SymmetricKeySlotId::User)
629                .unwrap();
630            let name: EncString = "test cipher"
631                .to_string()
632                .encrypt(&mut ctx, SymmetricKeySlotId::User)
633                .unwrap();
634            (malformed_private_key.to_string(), name.to_string())
635        };
636
637        let api_client = ApiClient::new_mocked(|mock| {
638            mock.accounts_api
639                .expect_get_keys()
640                .once()
641                .returning(move || {
642                    Ok(keys_response(
643                        Some("some-public-key".to_string()),
644                        Some(wrapped_malformed_private_key.clone()),
645                    ))
646                });
647            mock.ciphers_api.expect_get_all().once().returning(move || {
648                Ok(CipherDetailsResponseModelListResponseModel {
649                    object: None,
650                    data: Some(vec![CipherDetailsResponseModel {
651                        id: Some(uuid::Uuid::new_v4()),
652                        name: Some(encrypted_name.clone()),
653                        organization_id: None,
654                        r#type: Some(bitwarden_api_api::models::CipherType::Login),
655                        revision_date: Some("2024-01-01T00:00:00Z".to_string()),
656                        creation_date: Some("2024-01-01T00:00:00Z".to_string()),
657                        ..CipherDetailsResponseModel::default()
658                    }]),
659                    continuation_token: None,
660                })
661            });
662        });
663
664        let result =
665            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
666                .await;
667        assert!(matches!(result, Ok(true)));
668
669        if let ApiClient::Mock(mut mock) = api_client {
670            mock.accounts_api.checkpoint();
671            mock.ciphers_api.checkpoint();
672        }
673    }
674
675    #[tokio::test]
676    async fn test_should_regenerate_decryptable_but_public_key_mismatched() {
677        let key_store = unlocked_v1_key_store();
678        let (wrapped_private_key, _) = make_valid_key_pair(&key_store);
679        let wrong_public_key = "AAAAAAAAAA==".to_string();
680
681        let api_client = ApiClient::new_mocked(|mock| {
682            mock.accounts_api
683                .expect_get_keys()
684                .once()
685                .returning(move || {
686                    Ok(keys_response(
687                        Some(wrong_public_key.clone()),
688                        Some(wrapped_private_key.clone()),
689                    ))
690                });
691        });
692
693        let result =
694            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
695                .await;
696        assert!(matches!(result, Ok(true)));
697
698        if let ApiClient::Mock(mut mock) = api_client {
699            mock.accounts_api.checkpoint();
700        }
701    }
702
703    #[tokio::test]
704    async fn test_should_regenerate_decryptable_but_server_public_key_invalid_b64() {
705        let key_store = unlocked_v1_key_store();
706        let (wrapped_private_key, _) = make_valid_key_pair(&key_store);
707        let invalid_b64_public_key = "not valid base64!!!".to_string();
708
709        let api_client = ApiClient::new_mocked(|mock| {
710            mock.accounts_api
711                .expect_get_keys()
712                .once()
713                .returning(move || {
714                    Ok(keys_response(
715                        Some(invalid_b64_public_key.clone()),
716                        Some(wrapped_private_key.clone()),
717                    ))
718                });
719        });
720
721        let result =
722            internal_should_regenerate_public_key_encryption_key_pair(&key_store, &api_client)
723                .await;
724        assert!(matches!(result, Ok(true)));
725
726        if let ApiClient::Mock(mut mock) = api_client {
727            mock.accounts_api.checkpoint();
728        }
729    }
730
731    #[tokio::test]
732    async fn test_should_regenerate_with_ciphers_undecryptable_private_key_no_personal_ciphers() {
733        let key_store = unlocked_v1_key_store();
734
735        let api_client = ApiClient::new_mocked(|mock| {
736            mock.accounts_api
737                .expect_get_keys()
738                .once()
739                .returning(|| {
740                    Ok(keys_response(
741                        Some("some-public-key".to_string()),
742                        Some("2.AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string()),
743                    ))
744                });
745        });
746
747        let result = internal_should_regenerate_public_key_encryption_key_pair_with_ciphers(
748            &key_store,
749            &api_client,
750            &[],
751        )
752        .await;
753        assert!(matches!(result, Ok(false)));
754
755        if let ApiClient::Mock(mut mock) = api_client {
756            mock.accounts_api.checkpoint();
757        }
758    }
759
760    #[tokio::test]
761    async fn test_should_regenerate_with_ciphers_undecryptable_private_key_and_undecryptable_cipher()
762     {
763        let key_store = unlocked_v1_key_store();
764
765        // Create a cipher with a cipher key encrypted under a different user key.
766        // This makes Cipher::decrypt fail at decrypt_cipher_key (unwrap_symmetric_key),
767        // unlike ciphers without a key field where field-level errors are swallowed.
768        let cipher_with_wrong_key = {
769            let other_store: KeyStore<KeySlotIds> = KeyStore::default();
770            let mut ctx = other_store.context_mut();
771            let other_user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
772            let _ = ctx.persist_symmetric_key(other_user_key, SymmetricKeySlotId::User);
773            let cipher_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
774            let wrapped_cipher_key: EncString = ctx
775                .wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
776                .unwrap();
777            let name: EncString = "test cipher"
778                .to_string()
779                .encrypt(&mut ctx, cipher_key)
780                .unwrap();
781            Cipher {
782                id: None,
783                organization_id: None,
784                folder_id: None,
785                collection_ids: vec![],
786                key: Some(wrapped_cipher_key),
787                name: Some(name),
788                notes: None,
789                r#type: bitwarden_vault::CipherType::Login,
790                login: None,
791                identity: None,
792                card: None,
793                secure_note: None,
794                ssh_key: None,
795                bank_account: None,
796                favorite: false,
797                reprompt: bitwarden_vault::CipherRepromptType::None,
798                organization_use_totp: false,
799                edit: false,
800                permissions: None,
801                view_password: false,
802                local_data: None,
803                attachments: None,
804                fields: None,
805                password_history: None,
806                creation_date: "2024-01-01T00:00:00Z".parse().unwrap(),
807                deleted_date: None,
808                revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
809                archived_date: None,
810                data: None,
811                drivers_license: None,
812                passport: None,
813            }
814        };
815
816        // Encrypt the private key with a different key so unwrap_private_key fails
817        let undecryptable_private_key = {
818            let other_store: KeyStore<KeySlotIds> = KeyStore::default();
819            let mut ctx = other_store.context_mut();
820            let other_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
821            let _ = ctx.persist_symmetric_key(other_key, SymmetricKeySlotId::User);
822            let enc: EncString = "fake private key"
823                .to_string()
824                .encrypt(&mut ctx, SymmetricKeySlotId::User)
825                .unwrap();
826            enc.to_string()
827        };
828
829        let api_client = ApiClient::new_mocked(|mock| {
830            mock.accounts_api
831                .expect_get_keys()
832                .once()
833                .returning(move || {
834                    Ok(keys_response(
835                        Some("some-public-key".to_string()),
836                        Some(undecryptable_private_key.clone()),
837                    ))
838                });
839        });
840
841        let result = internal_should_regenerate_public_key_encryption_key_pair_with_ciphers(
842            &key_store,
843            &api_client,
844            &[cipher_with_wrong_key],
845        )
846        .await;
847        assert!(matches!(result, Ok(false)));
848
849        if let ApiClient::Mock(mut mock) = api_client {
850            mock.accounts_api.checkpoint();
851        }
852    }
853}