Skip to main content

bitwarden_core/key_management/crypto/
reinit_user_crypto.rs

1//! `reinit_user_crypto`: refresh an unlocked user's cryptographic state
2//! intended to be used for mobile clients.
3
4#![cfg(feature = "uniffi")]
5
6use bitwarden_crypto::SymmetricKeyAlgorithm;
7use bitwarden_error::bitwarden_error;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use tracing::{debug, error, info, warn};
11
12use crate::{
13    Client,
14    key_management::{
15        SymmetricKeySlotId, V2UpgradeToken,
16        account_cryptographic_state::WrappedAccountCryptographicState,
17    },
18};
19
20/// State used to re-initialize an unlocked user's cryptographic state after
21/// `accountCryptographicState` and `V2UpgradeToken` are received in a sync.
22///
23/// This presumes the SDK is already unlocked (has user key in memory).
24#[derive(Serialize, Deserialize, Debug)]
25#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
26pub struct ReinitUserCryptoRequest {
27    /// The user's account cryptographic state, encrypted under the user key
28    pub account_cryptographic_state: WrappedAccountCryptographicState,
29
30    /// The SDK uses the in-store (V1) user key to extract the V2 user key from the token,
31    /// then sets the V2 user key as the active user key before decrypting
32    /// `account_cryptographic_state`.
33    pub upgrade_token: V2UpgradeToken,
34}
35
36/// Errors that can occur when re-initializing user cryptography state.
37#[derive(Debug, Error)]
38#[bitwarden_error(flat)]
39pub enum ReinitUserCryptoError {
40    /// The SDK is not in an unlocked state, so it cannot re-initialize user crypto.
41    #[error("The SDK must be unlocked to re-initialize user crypto")]
42    NotUnlocked,
43    /// The provided account cryptographic state is not V2. Re-initialization is only supported for
44    /// upgrading to V2 encryption.
45    #[error(
46        "The provided account cryptographic state is not V2. Re-initialization is only supported for upgrading to V2 encryption."
47    )]
48    InvalidAccountCryptographicState,
49    /// The local migrations (pin key and local user data key) that runs as part of the V1->V2
50    /// upgrade failed, likely due to missing state or keys that should be present during the
51    /// upgrade process. Clients should deconstruct the SDK and initialize a fresh instance to
52    /// recover.
53    #[error("Unable to run local migrations after user key upgrade")]
54    LocalMigrationFailed,
55    /// The provided upgrade token was invalid, such as not decrypting properly with the active user
56    /// key, or containing unexpected data.
57    #[error("Invalid upgrade token")]
58    InvalidUpgradeToken,
59    /// An error occurred during the cryptographic operations to re-initialize user crypto.
60    #[error("Cryptography Initialization error")]
61    CryptoInitialization,
62    /// The SDK does not have a state bridge registered, which is required to perform V1->V2 local
63    /// data migrations.
64    #[error("No state bridge registered, re-initialization is not supported")]
65    StateBridgeNotRegistered,
66}
67
68/// Re-initialize the user's cryptographic state during an unlock session for a V1 -> V2 upgrade.
69/// If the user is already V2 this function is a no-op.
70///
71/// Requires the SDK to be unlocked and the client to have registered a state bridge. Replaces the
72/// in-memory account cryptographic state with the provided one, and upgrades the active user key
73/// from V1 to V2. Performs local data migrations for the local user data key and pin key.
74///
75/// Intended for mobile clients with `accountCryptographicState` and `V2UpgradeToken` received in
76/// a sync for a V1 -> V2 encryption upgrade. This allows the client to apply the received account
77/// cryptographic state and update to reinitialize the SDK without tearing down and recreating the
78/// client.
79pub(crate) async fn reinit_user_crypto(
80    client: &Client,
81    req: ReinitUserCryptoRequest,
82) -> Result<(), ReinitUserCryptoError> {
83    if !matches!(
84        req.account_cryptographic_state,
85        WrappedAccountCryptographicState::V2 { .. }
86    ) {
87        return Err(ReinitUserCryptoError::InvalidAccountCryptographicState);
88    }
89
90    if !client.internal.state_bridge.is_registered() {
91        warn!("No state bridge registered, re-initialization is not supported.");
92        return Err(ReinitUserCryptoError::StateBridgeNotRegistered);
93    }
94
95    {
96        let mut ctx = client.internal.get_key_store().context_mut();
97
98        if !ctx.has_symmetric_key(SymmetricKeySlotId::User) {
99            return Err(ReinitUserCryptoError::NotUnlocked);
100        }
101
102        let current_algorithm = ctx
103            .get_symmetric_key_algorithm(SymmetricKeySlotId::User)
104            .map_err(|_| ReinitUserCryptoError::CryptoInitialization)?;
105
106        let local_v2_user_key_id = match current_algorithm {
107            SymmetricKeyAlgorithm::Aes256CbcHmac => {
108                info!("V1 user key detected with upgrade token, extracting V2 key");
109                req.upgrade_token
110                    .unwrap_v2(SymmetricKeySlotId::User, &mut ctx)
111                    .map_err(|_| ReinitUserCryptoError::InvalidUpgradeToken)?
112            }
113            SymmetricKeyAlgorithm::XChaCha20Poly1305 => {
114                // If the active user key is already V2, then the upgrade token should not be
115                // applied. We return here so calling reinit_user_crypto with the
116                // same sync payload after a successful V2 upgrade is a no-op.
117                debug!("Active user key is already V2, skipping re-initialization.");
118                return Ok(());
119            }
120        };
121
122        req.account_cryptographic_state
123            .set_to_context(
124                &client.internal.security_state,
125                local_v2_user_key_id,
126                client.internal.get_key_store(),
127                ctx,
128            )
129            .map_err(|e| {
130                error!(error = ?e, "Failed to set account cryptographic state to context during reinit_user_crypto");
131                ReinitUserCryptoError::CryptoInitialization
132            })?;
133    }
134
135    client
136        .internal
137        .state_bridge
138        .set_v2_upgrade_token(&req.upgrade_token)
139        .await;
140
141    super::on_unlock_handler(client).await.map_err(|e| {
142        error!(error = ?e, "Failure in on_unlock_handler during reinit_user_crypto.");
143        ReinitUserCryptoError::LocalMigrationFailed
144    })?;
145
146    info!("User crypto re-initialized successfully");
147    Ok(())
148}
149
150#[cfg(test)]
151mod tests {
152    use bitwarden_crypto::{EncString, KeyStore, SymmetricCryptoKey, SymmetricKeyAlgorithm};
153
154    use super::*;
155    use crate::{
156        Client, UserId,
157        client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
158        key_management::{
159            KeySlotIds, PrivateKeySlotId, SigningKeySlotId, V2UpgradeToken,
160            state_bridge::test_support::InMemoryStateBridge,
161        },
162    };
163
164    // v2
165    const TEST_VECTOR_USER_KEY_V2_B64: &str = "pQEEAlACHUUoybNAuJoZzqNMxz2bAzoAARFvBIQDBAUGIFggAvGl4ifaUAomQdCdUPpXLHtypiQxHjZwRHeI83caZM4B";
166    const TEST_VECTOR_PRIVATE_KEY_V2: &str = "7.g1gdowE6AAERbwMZARwEUAIdRSjJs0C4mhnOo0zHPZuhBVgYthGLGqVLPeidY8mNMxpLJn3fyeSxyaWsWQTR6pxmRV2DyGZXly/0l9KK+Rsfetl9wvYIz0O4/RW3R6wf7eGxo5XmicV3WnFsoAmIQObxkKWShxFyjzg+ocKItQDzG7Gp6+MW4biTrAlfK51ML/ZS+PCjLmgI1QQr4eMHjiwA2TBKtKkxfjoTJkMXECpRVLEXOo8/mbIGYkuabbSA7oU+TJ0yXlfKDtD25gnyO7tjW/0JMFUaoEKRJOuKoXTN4n/ks4Hbxk0X5/DzfG05rxWad2UNBjNg7ehW99WrQ+33ckdQFKMQOri/rt8JzzrF1k11/jMJ+Y2TADKNHr91NalnUX+yqZAAe3sRt5Pv5ZhLIwRMKQi/1NrLcsQPRuUnogVSPOoMnE/eD6F70iU60Z6pvm1iBw2IvELZcrs/oxpO2SeCue08fIZW/jNZokbLnm90tQ7QeZTUpiPALhUgfGOa3J9VOJ7jQGCqDjd9CzV2DCVfhKCapeTbldm+RwEWBz5VvorH5vMx1AzbPRJxdIQuxcg3NqRrXrYC7fyZljWaPB9qP1tztiPtd1PpGEgxLByIfR6fqyZMCvOBsWbd0H6NhF8mNVdDw60+skFRdbRBTSCjCtKZeLVuVFb8ioH45PR5oXjtx4atIDzu6DKm6TTMCbR6DjZuZZ8GbwHxuUD2mDD3pAFhaof9kR3lQdjy7Zb4EzUUYskQxzcLPcqzp9ZgB3Rg91SStBCCMhdQ6AnhTy+VTGt/mY5AbBXNRSL6fI0r+P9K8CcEI4bNZCDkwwQr5v4O4ykSUzIvmVU0zKzDngy9bteIZuhkvGUoZlQ9UATNGPhoLfqq2eSvqEXkCbxTVZ5D+Ww9pHmWeVcvoBhcl5MvicfeQt++dY3tPjIfZq87nlugG4HiNbcv9nbVpgwe3v8cFetWXQgnO4uhx8JHSwGoSuxHFZtl2sdahjTHavRHnYjSABEFrViUKgb12UDD5ow1GAL62wVdSJKRf9HlLbJhN3PBxuh5L/E0wy1wGA9ecXtw/R1ktvXZ7RklGAt1TmNzZv6vI2J/CMXvndOX9rEpjKMbwbIDAjQ9PxiWdcnmc5SowT9f6yfIjbjXnRMWWidPAua7sgrtej4HP4Qjz1fpgLMLCRyF97tbMTmsAI5Cuj98Buh9PwcdyXj5SbVuHdJS1ehv9b5SWPsD4pwOm3+otVNK6FTazhoUl47AZoAoQzXfsXxrzqYzvF0yJkCnk9S1dcij1L569gQ43CJO6o6jIZFJvA4EmZDl95ELu+BC+x37Ip8dq4JLPsANDVSqvXO9tfDUIXEx25AaOYhW2KAUoDve/fbsU8d0UZR1o/w+ZrOQwawCIPeVPtbh7KFRVQi/rPI+Abl6XR6qMJbKPegliYGUuGF2oEMEc6QLTsMRCEPuw0S3kxbNfVPqml8nGhB2r8zUHBY1diJEmipVghnwH74gIKnyJ2C9nKjV8noUfKzqyV8vxUX2G5yXgodx8Jn0cWs3XhWuApFla9z4R28W/4jA1jK2WQMlx+b6xKUWgRk8+fYsc0HSt2fDrQ9pLpnjb8ME59RCxSPV++PThpnR2JtastZBZur2hBIJsGILCAmufUU4VC4gBKPhNfu/OK4Ktgz+uQlUa9fEC/FnkpTRQPxHuQjSQSNrIIyW1bIRBtnwjvvvNoui9FZJ";
167    const TEST_VECTOR_SIGNED_PUBLIC_KEY_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8BoFkBTqNpYWxnb3JpdGhtAG1jb250ZW50Rm9ybWF0AGlwdWJsaWNLZXlZASYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP/7WM8nUepxoJ0qtM+azxcly+eZ31qUjjZTZcX/gYw1MzkoXWAjqyeFH/bdktq1lEUwegrxkIxKkY2SMtp0CvPnaV1x5O8E6FBSJbKWRlDg181rfEhgm5tc6aR4PJ827IvFVm9xk6Sj091P5DHZDEOsWLZc2jYjtpUV3X38I4gSR7HiYnR4DcwcWkoJ3FhtxMCwYgPz6RVH0vzhLUmm1mgbzH6IH8Pf9DjLTZSxBikVO7S9s9jzhiZbTeeAl3FbNLxfj9Qkj+NoSfms7jGVTlBwvSXgjJs/ktGkT1cR5QcBMpU4bt41+l73MN8pXapCih9Awf1W+RY7imxpYOMFJ3AgMBAAFYQMq/hT4wod2w8xyoM7D86ctuLNX4ZRo+jRHf2sZfaO7QsvonG/ZYuNKF5fq8wpxMRjfoMvnY2TTShbgzLrW8BA4=";
168    const TEST_VECTOR_SIGNING_KEY_V2: &str = "7.g1gcowE6AAERbwMYZQRQAh1FKMmzQLiaGc6jTMc9m6EFWBhYePc2qkCruHAPXgbzXsIP1WVk11ArbLNYUBpifToURlwHKs1je2BwZ1C/5thz4nyNbL0wDaYkRWI9ex1wvB7KhdzC7ltStEd5QttboTSCaXQROSZaGBPNO5+Bu3sTY8F5qK1pBUo6AHNN";
169    const TEST_VECTOR_SECURITY_STATE_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8CoFgkomhlbnRpdHlJZFBHOOw2BI9OQoNq+Vl1xZZKZ3ZlcnNpb24CWEAlchbJR0vmRfShG8On7Q2gknjkw4Dd6MYBLiH4u+/CmfQdmjNZdf6kozgW/6NXyKVNu8dAsKsin+xxXkDyVZoG";
170
171    // v1
172    const TEST_VECTOR_PRIVATE_KEY_V1: &str = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=";
173
174    fn make_mock_upgrade_token() -> V2UpgradeToken {
175        let key_store = KeyStore::<KeySlotIds>::default();
176        let mut ctx = key_store.context_mut();
177        let v1_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
178        let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
179        V2UpgradeToken::create(v1_id, v2_id, &ctx).unwrap()
180    }
181
182    fn register_in_memory_bridge(client: &Client) {
183        client
184            .km_state_bridge()
185            .register_bridge(Box::new(InMemoryStateBridge::default()));
186    }
187
188    /// Assert that the client's active user key is V2 and matches `expected_v2_key`.
189    fn assert_active_user_key_is_v2(client: &Client, expected_v2_key: &SymmetricCryptoKey) {
190        let key_store = client.internal.get_key_store();
191        let ctx = key_store.context();
192        let algorithm = ctx
193            .get_symmetric_key_algorithm(SymmetricKeySlotId::User)
194            .unwrap();
195        assert_eq!(
196            algorithm,
197            SymmetricKeyAlgorithm::XChaCha20Poly1305,
198            "user-slot algorithm must be V2 after upgrade"
199        );
200
201        #[allow(deprecated)]
202        let user_key = ctx
203            .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
204            .unwrap();
205        assert_eq!(user_key, expected_v2_key);
206    }
207
208    /// V2 wrapped state from the test vectors. Wrapped under the V2 test user
209    /// key, so it only decrypts cleanly when paired with that key.
210    fn test_vector_v2_account_state() -> WrappedAccountCryptographicState {
211        WrappedAccountCryptographicState::V2 {
212            private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(),
213            signing_key: TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap(),
214            security_state: TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap(),
215            signed_public_key: Some(TEST_VECTOR_SIGNED_PUBLIC_KEY_V2.parse().unwrap()),
216        }
217    }
218
219    fn test_vector_v1_account_state() -> WrappedAccountCryptographicState {
220        WrappedAccountCryptographicState::V1 {
221            private_key: TEST_VECTOR_PRIVATE_KEY_V1.parse().unwrap(),
222        }
223    }
224
225    #[tokio::test]
226    async fn reinit_user_crypto_returns_not_unlocked_when_locked() {
227        let client = Client::new_test(None);
228        register_in_memory_bridge(&client);
229
230        let result = reinit_user_crypto(
231            &client,
232            ReinitUserCryptoRequest {
233                account_cryptographic_state: test_vector_v2_account_state(),
234                upgrade_token: make_mock_upgrade_token(),
235            },
236        )
237        .await;
238
239        assert!(
240            matches!(result, Err(ReinitUserCryptoError::NotUnlocked)),
241            "reinit on a locked SDK must return NotUnlocked, got {result:?}"
242        );
243    }
244
245    #[tokio::test]
246    async fn reinit_user_crypto_is_noop_when_active_user_is_already_v2() {
247        let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
248        register_in_memory_bridge(&client);
249
250        let result = reinit_user_crypto(
251            &client,
252            ReinitUserCryptoRequest {
253                account_cryptographic_state: test_vector_v2_account_state(),
254                upgrade_token: make_mock_upgrade_token(),
255            },
256        )
257        .await;
258
259        assert!(
260            result.is_ok(),
261            "reinit on an already-V2 user must be a no-op and return Ok, got {result:?}"
262        );
263
264        let expected_v2_key =
265            SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
266        assert_active_user_key_is_v2(&client, &expected_v2_key);
267
268        let upgrade_token = client.internal.state_bridge.get_v2_upgrade_token().await;
269        assert!(
270            upgrade_token.is_none(),
271            "reinit on an already-V2 user must not set the upgrade token"
272        );
273    }
274
275    #[tokio::test]
276    async fn reinit_user_crypto_upgrades_v1_to_v2_with_token() {
277        let client = Client::init_test_account(test_bitwarden_com_account()).await;
278        register_in_memory_bridge(&client);
279
280        // Build a V2 user key, install it into a temporary local slot, and
281        // create an upgrade token linking the active V1 user key to it.
282        let expected_v2_key =
283            SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
284        let upgrade_token = {
285            let mut ctx = client.internal.get_key_store().context_mut();
286            let v2_key_id = ctx.add_local_symmetric_key(expected_v2_key.clone());
287            V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
288        };
289
290        reinit_user_crypto(
291            &client,
292            ReinitUserCryptoRequest {
293                account_cryptographic_state: test_vector_v2_account_state(),
294                upgrade_token: upgrade_token.clone(),
295            },
296        )
297        .await
298        .expect("V1→V2 reinit with a valid upgrade token should succeed");
299
300        assert_active_user_key_is_v2(&client, &expected_v2_key);
301
302        assert_eq!(
303            client.internal.get_security_version(),
304            2,
305            "security version must reflect the V2 state"
306        );
307
308        {
309            let key_store = client.internal.get_key_store();
310            let ctx = key_store.context();
311            assert!(
312                ctx.has_signing_key(SigningKeySlotId::UserSigningKey),
313                "user signing key must be set after V1→V2 upgrade"
314            );
315            assert!(
316                ctx.has_private_key(PrivateKeySlotId::UserPrivateKey),
317                "user private key must be set after V1→V2 upgrade"
318            );
319        }
320
321        let stored_token = client
322            .internal
323            .state_bridge
324            .get_v2_upgrade_token()
325            .await
326            .expect("the upgrade token must be set on the state bridge after reinit");
327        assert_eq!(
328            stored_token.wrapped_user_key_1,
329            upgrade_token.wrapped_user_key_1
330        );
331        assert_eq!(
332            stored_token.wrapped_user_key_2,
333            upgrade_token.wrapped_user_key_2
334        );
335    }
336
337    #[tokio::test]
338    async fn reinit_user_crypto_called_twice_with_same_payload_is_noop() {
339        let client = Client::init_test_account(test_bitwarden_com_account()).await;
340        register_in_memory_bridge(&client);
341
342        let expected_v2_key =
343            SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
344        let upgrade_token = {
345            let mut ctx = client.internal.get_key_store().context_mut();
346            let v2_key_id = ctx.add_local_symmetric_key(expected_v2_key.clone());
347            V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
348        };
349
350        let request = || ReinitUserCryptoRequest {
351            account_cryptographic_state: test_vector_v2_account_state(),
352            upgrade_token: upgrade_token.clone(),
353        };
354
355        // First call performs the V1→V2 upgrade.
356        reinit_user_crypto(&client, request())
357            .await
358            .expect("V1→V2 reinit with a valid upgrade token should succeed");
359        assert_active_user_key_is_v2(&client, &expected_v2_key);
360
361        // Second call with the same payload is a no-op: the active user key is
362        // already V2, so the token is never re-applied.
363        reinit_user_crypto(&client, request())
364            .await
365            .expect("re-applying the same upgrade after success should be a no-op");
366        assert_active_user_key_is_v2(&client, &expected_v2_key);
367    }
368
369    #[tokio::test]
370    async fn reinit_user_crypto_invalid_upgrade_token_returns_error() {
371        let client = Client::init_test_account(test_bitwarden_com_account()).await;
372        register_in_memory_bridge(&client);
373
374        // Token built with a different V1 key — unwrapping with the client's
375        // V1 key will fail.
376        let mismatched_token = make_mock_upgrade_token();
377
378        let result = reinit_user_crypto(
379            &client,
380            ReinitUserCryptoRequest {
381                account_cryptographic_state: test_vector_v2_account_state(),
382                upgrade_token: mismatched_token,
383            },
384        )
385        .await;
386
387        assert!(
388            matches!(result, Err(ReinitUserCryptoError::InvalidUpgradeToken)),
389            "mismatched upgrade token must return InvalidUpgradeToken, got {result:?}"
390        );
391    }
392
393    #[tokio::test]
394    async fn reinit_user_crypto_returns_crypto_initialization_on_key_mismatch() {
395        let client = Client::init_test_account(test_bitwarden_com_account()).await;
396        register_in_memory_bridge(&client);
397
398        // Build an upgrade token whose V2 target is a fresh random key (not the
399        // test-vector key). `unwrap_v2` only checks internal token consistency,
400        // so it succeeds, but the resolved V2 key cannot decrypt the
401        // test-vector account state.
402        let upgrade_token = {
403            let mut ctx = client.internal.get_key_store().context_mut();
404            let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
405            V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
406        };
407
408        let result = reinit_user_crypto(
409            &client,
410            ReinitUserCryptoRequest {
411                account_cryptographic_state: test_vector_v2_account_state(),
412                upgrade_token,
413            },
414        )
415        .await;
416
417        assert!(
418            matches!(result, Err(ReinitUserCryptoError::CryptoInitialization)),
419            "a V2 key that cannot decrypt the account state must return CryptoInitialization, got {result:?}"
420        );
421
422        // `set_to_context` resolves the V2 key into a local slot and fails
423        // before it ever rewrites the User slot, so the original V1 user key is
424        // left intact. The active session remains usable on failure.
425        let key_store = client.internal.get_key_store();
426        let ctx = key_store.context();
427        assert!(
428            ctx.has_symmetric_key(SymmetricKeySlotId::User),
429            "the original V1 user key must remain in the User slot on failure"
430        );
431        assert_eq!(
432            ctx.get_symmetric_key_algorithm(SymmetricKeySlotId::User)
433                .unwrap(),
434            SymmetricKeyAlgorithm::Aes256CbcHmac,
435            "the User slot must still hold the original V1 key on failure"
436        );
437    }
438
439    #[tokio::test]
440    async fn reinit_user_crypto_returns_invalid_account_state_on_v1_request() {
441        let client = Client::init_test_account(test_bitwarden_com_account()).await;
442
443        let result = reinit_user_crypto(
444            &client,
445            ReinitUserCryptoRequest {
446                account_cryptographic_state: test_vector_v1_account_state(),
447                upgrade_token: make_mock_upgrade_token(),
448            },
449        )
450        .await;
451
452        assert!(
453            matches!(
454                result,
455                Err(ReinitUserCryptoError::InvalidAccountCryptographicState)
456            ),
457            "a V1 account state must return InvalidAccountState, got {result:?}"
458        );
459    }
460
461    #[tokio::test]
462    async fn reinit_user_crypto_returns_state_bridge_not_registered_when_no_bridge() {
463        let client = Client::init_test_account(test_bitwarden_com_account()).await;
464
465        let result = reinit_user_crypto(
466            &client,
467            ReinitUserCryptoRequest {
468                account_cryptographic_state: test_vector_v2_account_state(),
469                upgrade_token: make_mock_upgrade_token(),
470            },
471        )
472        .await;
473
474        assert!(
475            matches!(result, Err(ReinitUserCryptoError::StateBridgeNotRegistered)),
476            "reinit without a registered state bridge must return StateBridgeNotRegistered, got {result:?}"
477        );
478    }
479
480    #[tokio::test]
481    async fn reinit_user_crypto_v1_v2_upgrade_rewraps_local_user_data_key() {
482        use crate::key_management::LocalUserDataKeyState;
483
484        // Bootstrap a V1 client to materialize a V1-wrapped LocalUserDataKey state.
485        let client = Client::init_test_account(test_bitwarden_com_account()).await;
486        let user_id = UserId::new(uuid::uuid!("060000fb-0922-4dd3-b170-6e15cb5df8c8"));
487        register_in_memory_bridge(&client);
488
489        // The V1 init plants a V1-wrapped local user data key in state.
490        let v1_user_data_key = client
491            .platform()
492            .state()
493            .get::<LocalUserDataKeyState>()
494            .unwrap()
495            .get(user_id)
496            .await
497            .unwrap()
498            .expect("V1 init should plant a LocalUserDataKey state");
499        assert!(
500            matches!(
501                v1_user_data_key.wrapped_key,
502                EncString::Aes256Cbc_HmacSha256_B64 { .. }
503            ),
504            "initial local user data key should be V1-wrapped"
505        );
506
507        let v2_key = SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
508        let upgrade_token = {
509            let mut ctx = client.internal.get_key_store().context_mut();
510            let v2_key_id = ctx.add_local_symmetric_key(v2_key.clone());
511            V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
512        };
513
514        reinit_user_crypto(
515            &client,
516            ReinitUserCryptoRequest {
517                account_cryptographic_state: test_vector_v2_account_state(),
518                upgrade_token,
519            },
520        )
521        .await
522        .expect("V1→V2 reinit with a valid upgrade token should succeed");
523
524        // The persisted wrapped local user data key must now be sealed with the V2 user key.
525        let rewrapped_state = client
526            .platform()
527            .state()
528            .get::<LocalUserDataKeyState>()
529            .unwrap()
530            .get(user_id)
531            .await
532            .unwrap()
533            .expect("LocalUserDataKey state must remain present");
534        assert!(
535            matches!(
536                rewrapped_state.wrapped_key,
537                EncString::Cose_Encrypt0_B64 { .. }
538            ),
539            "rewrapped key should be sealed with the V2 user key"
540        );
541        assert_ne!(rewrapped_state.wrapped_key, v1_user_data_key.wrapped_key);
542    }
543}