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