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