bitwarden_core/key_management/crypto/
reinit_user_crypto.rs1#![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
12use crate::{
13 Client,
14 key_management::{
15 SymmetricKeySlotId, V2UpgradeToken,
16 account_cryptographic_state::WrappedAccountCryptographicState,
17 },
18};
19
20#[derive(Serialize, Deserialize, Debug)]
25#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
26#[bitwarden_ffi::wasm_record]
27#[serde(rename_all = "camelCase", deny_unknown_fields)]
28pub struct ReinitUserCryptoRequest {
29 pub account_cryptographic_state: WrappedAccountCryptographicState,
31
32 pub upgrade_token: V2UpgradeToken,
36}
37
38#[derive(Debug, Error)]
40#[bitwarden_error(flat)]
41pub enum ReinitUserCryptoError {
42 #[error("The SDK must be unlocked to re-initialize user crypto")]
44 NotUnlocked,
45 #[error(
48 "The provided account cryptographic state is not V2. Re-initialization is only supported for upgrading to V2 encryption."
49 )]
50 InvalidAccountCryptographicState,
51 #[error("Unable to run local migrations after user key upgrade")]
56 LocalMigrationFailed,
57 #[error("Invalid upgrade token")]
60 InvalidUpgradeToken,
61 #[error("Cryptography Initialization error")]
63 CryptoInitialization,
64 #[error("No state bridge registered, re-initialization is not supported")]
67 StateBridgeNotRegistered,
68}
69
70pub(crate) async fn reinit_user_crypto(
82 client: &Client,
83 req: ReinitUserCryptoRequest,
84) -> Result<(), ReinitUserCryptoError> {
85 if !matches!(
86 req.account_cryptographic_state,
87 WrappedAccountCryptographicState::V2 { .. }
88 ) {
89 return Err(ReinitUserCryptoError::InvalidAccountCryptographicState);
90 }
91
92 if !client.internal.state_bridge.is_registered() {
93 warn!("No state bridge registered, re-initialization is not supported.");
94 return Err(ReinitUserCryptoError::StateBridgeNotRegistered);
95 }
96
97 {
98 let mut ctx = client.internal.get_key_store().context_mut();
99
100 if !ctx.has_symmetric_key(SymmetricKeySlotId::User) {
101 return Err(ReinitUserCryptoError::NotUnlocked);
102 }
103
104 let current_algorithm = ctx
105 .get_symmetric_key_algorithm(SymmetricKeySlotId::User)
106 .map_err(|_| ReinitUserCryptoError::CryptoInitialization)?;
107
108 let local_v2_user_key_id = match current_algorithm {
109 SymmetricKeyAlgorithm::Aes256CbcHmac => {
110 info!("V1 user key detected with upgrade token, extracting V2 key");
111 req.upgrade_token
112 .unwrap_v2(SymmetricKeySlotId::User, &mut ctx)
113 .map_err(|_| ReinitUserCryptoError::InvalidUpgradeToken)?
114 }
115 SymmetricKeyAlgorithm::XAes256Gcm => {
116 debug!("Active user key is already V2, skipping re-initialization.");
120 return Ok(());
121 }
122 SymmetricKeyAlgorithm::XChaCha20Poly1305 | SymmetricKeyAlgorithm::Aes256Gcm => {
123 error!(
124 ?current_algorithm,
125 "Unexpected user key algorithm during reinit_user_crypto"
126 );
127 return Err(ReinitUserCryptoError::CryptoInitialization);
128 }
129 };
130
131 req.account_cryptographic_state
132 .set_to_context(
133 &client.internal.security_state,
134 local_v2_user_key_id,
135 client.internal.get_key_store(),
136 ctx,
137 )
138 .map_err(|e| {
139 error!(error = ?e, "Failed to set account cryptographic state to context during reinit_user_crypto");
140 ReinitUserCryptoError::CryptoInitialization
141 })?;
142 }
143
144 client
145 .internal
146 .state_bridge
147 .set_v2_upgrade_token(&req.upgrade_token)
148 .await;
149
150 super::on_unlock_handler(client).await.map_err(|e| {
151 error!(error = ?e, "Failure in on_unlock_handler during reinit_user_crypto.");
152 ReinitUserCryptoError::LocalMigrationFailed
153 })?;
154
155 info!("User crypto re-initialized successfully");
156 Ok(())
157}
158
159#[cfg(test)]
160mod tests {
161 use bitwarden_crypto::{EncString, KeyStore, SymmetricCryptoKey, SymmetricKeyAlgorithm};
162
163 use super::*;
164 use crate::{
165 Client, UserId,
166 client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
167 key_management::{
168 KeySlotIds, PrivateKeySlotId, SigningKeySlotId, V2UpgradeToken,
169 state_bridge::test_support::InMemoryStateBridge,
170 },
171 };
172
173 const TEST_VECTOR_USER_KEY_V2_B64: &str = "pQEEAlCxZkKFDpp70P5mWPmOjf3xAzoAARF5BIQDBAUGIFggCFcd6XLISUfLaITyU9yimrYHacdS5XhBayO2663jdSUB";
175 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";
176 const TEST_VECTOR_SIGNED_PUBLIC_KEY_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8BoFkBTqNpYWxnb3JpdGhtAG1jb250ZW50Rm9ybWF0AGlwdWJsaWNLZXlZASYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP/7WM8nUepxoJ0qtM+azxcly+eZ31qUjjZTZcX/gYw1MzkoXWAjqyeFH/bdktq1lEUwegrxkIxKkY2SMtp0CvPnaV1x5O8E6FBSJbKWRlDg181rfEhgm5tc6aR4PJ827IvFVm9xk6Sj091P5DHZDEOsWLZc2jYjtpUV3X38I4gSR7HiYnR4DcwcWkoJ3FhtxMCwYgPz6RVH0vzhLUmm1mgbzH6IH8Pf9DjLTZSxBikVO7S9s9jzhiZbTeeAl3FbNLxfj9Qkj+NoSfms7jGVTlBwvSXgjJs/ktGkT1cR5QcBMpU4bt41+l73MN8pXapCih9Awf1W+RY7imxpYOMFJ3AgMBAAFYQMq/hT4wod2w8xyoM7D86ctuLNX4ZRo+jRHf2sZfaO7QsvonG/ZYuNKF5fq8wpxMRjfoMvnY2TTShbgzLrW8BA4=";
177 const TEST_VECTOR_SIGNING_KEY_V2: &str = "7.g1gcowE6AAEReQMYZQRQsWZChQ6ae9D+Zlj5jo398aEFWBj8Gg/gn4tQKWO3nq5e/2p9gkzIrKD829RYT3aEUIDOetEtnFqRuQ3Cz13693WqDnKHM5Buzi6LcTsxo1jphYR7vlE5nYLjCpOCAftPN1oLfs5SCNkwwMENhujpVftfDzciE99aLEJDS9A=";
178 const TEST_VECTOR_SECURITY_STATE_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8CoFgkomhlbnRpdHlJZFBHOOw2BI9OQoNq+Vl1xZZKZ3ZlcnNpb24CWEAlchbJR0vmRfShG8On7Q2gknjkw4Dd6MYBLiH4u+/CmfQdmjNZdf6kozgW/6NXyKVNu8dAsKsin+xxXkDyVZoG";
179
180 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=";
182
183 fn make_mock_upgrade_token() -> V2UpgradeToken {
184 let key_store = KeyStore::<KeySlotIds>::default();
185 let mut ctx = key_store.context_mut();
186 let v1_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
187 let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
188 V2UpgradeToken::create(v1_id, v2_id, &ctx).unwrap()
189 }
190
191 fn register_in_memory_bridge(client: &Client) {
192 client
193 .km_state_bridge()
194 .register_bridge(Box::new(InMemoryStateBridge::default()));
195 }
196
197 fn assert_active_user_key_is_v2(client: &Client, expected_v2_key: &SymmetricCryptoKey) {
199 let key_store = client.internal.get_key_store();
200 let ctx = key_store.context();
201 let algorithm = ctx
202 .get_symmetric_key_algorithm(SymmetricKeySlotId::User)
203 .unwrap();
204 assert_eq!(
205 algorithm,
206 SymmetricKeyAlgorithm::XAes256Gcm,
207 "user-slot algorithm must be XAES-256-GCM after upgrade"
208 );
209
210 #[allow(deprecated)]
211 let user_key = ctx
212 .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
213 .unwrap();
214 assert_eq!(user_key, expected_v2_key);
215 }
216
217 fn test_vector_v2_account_state() -> WrappedAccountCryptographicState {
220 WrappedAccountCryptographicState::V2 {
221 private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(),
222 signing_key: TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap(),
223 security_state: TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap(),
224 signed_public_key: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2.parse().unwrap(),
225 }
226 }
227
228 fn test_vector_v1_account_state() -> WrappedAccountCryptographicState {
229 WrappedAccountCryptographicState::V1 {
230 private_key: TEST_VECTOR_PRIVATE_KEY_V1.parse().unwrap(),
231 }
232 }
233
234 #[tokio::test]
235 async fn reinit_user_crypto_returns_not_unlocked_when_locked() {
236 let client = Client::new_test(None);
237 register_in_memory_bridge(&client);
238
239 let result = reinit_user_crypto(
240 &client,
241 ReinitUserCryptoRequest {
242 account_cryptographic_state: test_vector_v2_account_state(),
243 upgrade_token: make_mock_upgrade_token(),
244 },
245 )
246 .await;
247
248 assert!(
249 matches!(result, Err(ReinitUserCryptoError::NotUnlocked)),
250 "reinit on a locked SDK must return NotUnlocked, got {result:?}"
251 );
252 }
253
254 #[tokio::test]
255 async fn reinit_user_crypto_is_noop_when_active_user_is_already_v2() {
256 let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
257 register_in_memory_bridge(&client);
258
259 let result = reinit_user_crypto(
260 &client,
261 ReinitUserCryptoRequest {
262 account_cryptographic_state: test_vector_v2_account_state(),
263 upgrade_token: make_mock_upgrade_token(),
264 },
265 )
266 .await;
267
268 assert!(
269 result.is_ok(),
270 "reinit on an already-V2 user must be a no-op and return Ok, got {result:?}"
271 );
272
273 let expected_v2_key =
274 SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
275 assert_active_user_key_is_v2(&client, &expected_v2_key);
276
277 let upgrade_token = client.internal.state_bridge.get_v2_upgrade_token().await;
278 assert!(
279 upgrade_token.is_none(),
280 "reinit on an already-V2 user must not set the upgrade token"
281 );
282 }
283
284 #[tokio::test]
285 async fn reinit_user_crypto_upgrades_v1_to_v2_with_token() {
286 let client = Client::init_test_account(test_bitwarden_com_account()).await;
287 register_in_memory_bridge(&client);
288
289 let expected_v2_key =
292 SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
293 let upgrade_token = {
294 let mut ctx = client.internal.get_key_store().context_mut();
295 let v2_key_id = ctx.add_local_symmetric_key(expected_v2_key.clone());
296 V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
297 };
298
299 reinit_user_crypto(
300 &client,
301 ReinitUserCryptoRequest {
302 account_cryptographic_state: test_vector_v2_account_state(),
303 upgrade_token: upgrade_token.clone(),
304 },
305 )
306 .await
307 .expect("V1→V2 reinit with a valid upgrade token should succeed");
308
309 assert_active_user_key_is_v2(&client, &expected_v2_key);
310
311 assert_eq!(
312 client.internal.get_security_version(),
313 2,
314 "security version must reflect the V2 state"
315 );
316
317 {
318 let key_store = client.internal.get_key_store();
319 let ctx = key_store.context();
320 assert!(
321 ctx.has_signing_key(SigningKeySlotId::UserSigningKey),
322 "user signing key must be set after V1→V2 upgrade"
323 );
324 assert!(
325 ctx.has_private_key(PrivateKeySlotId::UserPrivateKey),
326 "user private key must be set after V1→V2 upgrade"
327 );
328 }
329
330 let stored_token = client
331 .internal
332 .state_bridge
333 .get_v2_upgrade_token()
334 .await
335 .expect("the upgrade token must be set on the state bridge after reinit");
336 assert_eq!(
337 stored_token.wrapped_user_key_1,
338 upgrade_token.wrapped_user_key_1
339 );
340 assert_eq!(
341 stored_token.wrapped_user_key_2,
342 upgrade_token.wrapped_user_key_2
343 );
344 }
345
346 #[tokio::test]
347 async fn reinit_user_crypto_called_twice_with_same_payload_is_noop() {
348 let client = Client::init_test_account(test_bitwarden_com_account()).await;
349 register_in_memory_bridge(&client);
350
351 let expected_v2_key =
352 SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
353 let upgrade_token = {
354 let mut ctx = client.internal.get_key_store().context_mut();
355 let v2_key_id = ctx.add_local_symmetric_key(expected_v2_key.clone());
356 V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
357 };
358
359 let request = || ReinitUserCryptoRequest {
360 account_cryptographic_state: test_vector_v2_account_state(),
361 upgrade_token: upgrade_token.clone(),
362 };
363
364 reinit_user_crypto(&client, request())
366 .await
367 .expect("V1→V2 reinit with a valid upgrade token should succeed");
368 assert_active_user_key_is_v2(&client, &expected_v2_key);
369
370 reinit_user_crypto(&client, request())
373 .await
374 .expect("re-applying the same upgrade after success should be a no-op");
375 assert_active_user_key_is_v2(&client, &expected_v2_key);
376 }
377
378 #[tokio::test]
379 async fn reinit_user_crypto_invalid_upgrade_token_returns_error() {
380 let client = Client::init_test_account(test_bitwarden_com_account()).await;
381 register_in_memory_bridge(&client);
382
383 let mismatched_token = make_mock_upgrade_token();
386
387 let result = reinit_user_crypto(
388 &client,
389 ReinitUserCryptoRequest {
390 account_cryptographic_state: test_vector_v2_account_state(),
391 upgrade_token: mismatched_token,
392 },
393 )
394 .await;
395
396 assert!(
397 matches!(result, Err(ReinitUserCryptoError::InvalidUpgradeToken)),
398 "mismatched upgrade token must return InvalidUpgradeToken, got {result:?}"
399 );
400 }
401
402 #[tokio::test]
403 async fn reinit_user_crypto_returns_crypto_initialization_on_key_mismatch() {
404 let client = Client::init_test_account(test_bitwarden_com_account()).await;
405 register_in_memory_bridge(&client);
406
407 let upgrade_token = {
412 let mut ctx = client.internal.get_key_store().context_mut();
413 let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
414 V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
415 };
416
417 let result = reinit_user_crypto(
418 &client,
419 ReinitUserCryptoRequest {
420 account_cryptographic_state: test_vector_v2_account_state(),
421 upgrade_token,
422 },
423 )
424 .await;
425
426 assert!(
427 matches!(result, Err(ReinitUserCryptoError::CryptoInitialization)),
428 "a V2 key that cannot decrypt the account state must return CryptoInitialization, got {result:?}"
429 );
430
431 let key_store = client.internal.get_key_store();
435 let ctx = key_store.context();
436 assert!(
437 ctx.has_symmetric_key(SymmetricKeySlotId::User),
438 "the original V1 user key must remain in the User slot on failure"
439 );
440 assert_eq!(
441 ctx.get_symmetric_key_algorithm(SymmetricKeySlotId::User)
442 .unwrap(),
443 SymmetricKeyAlgorithm::Aes256CbcHmac,
444 "the User slot must still hold the original V1 key on failure"
445 );
446 }
447
448 #[tokio::test]
449 async fn reinit_user_crypto_returns_invalid_account_state_on_v1_request() {
450 let client = Client::init_test_account(test_bitwarden_com_account()).await;
451
452 let result = reinit_user_crypto(
453 &client,
454 ReinitUserCryptoRequest {
455 account_cryptographic_state: test_vector_v1_account_state(),
456 upgrade_token: make_mock_upgrade_token(),
457 },
458 )
459 .await;
460
461 assert!(
462 matches!(
463 result,
464 Err(ReinitUserCryptoError::InvalidAccountCryptographicState)
465 ),
466 "a V1 account state must return InvalidAccountState, got {result:?}"
467 );
468 }
469
470 #[tokio::test]
471 async fn reinit_user_crypto_returns_state_bridge_not_registered_when_no_bridge() {
472 let client = Client::init_test_account(test_bitwarden_com_account()).await;
473
474 let result = reinit_user_crypto(
475 &client,
476 ReinitUserCryptoRequest {
477 account_cryptographic_state: test_vector_v2_account_state(),
478 upgrade_token: make_mock_upgrade_token(),
479 },
480 )
481 .await;
482
483 assert!(
484 matches!(result, Err(ReinitUserCryptoError::StateBridgeNotRegistered)),
485 "reinit without a registered state bridge must return StateBridgeNotRegistered, got {result:?}"
486 );
487 }
488
489 #[tokio::test]
490 async fn reinit_user_crypto_v1_v2_upgrade_rewraps_local_user_data_key() {
491 use crate::key_management::LocalUserDataKeyState;
492
493 let client = Client::init_test_account(test_bitwarden_com_account()).await;
495 let user_id = UserId::new(uuid::uuid!("060000fb-0922-4dd3-b170-6e15cb5df8c8"));
496 register_in_memory_bridge(&client);
497
498 let v1_user_data_key = client
500 .platform()
501 .state()
502 .get::<LocalUserDataKeyState>()
503 .unwrap()
504 .get(user_id)
505 .await
506 .unwrap()
507 .expect("V1 init should plant a LocalUserDataKey state");
508 assert!(
509 matches!(
510 v1_user_data_key.wrapped_key,
511 EncString::Aes256Cbc_HmacSha256_B64 { .. }
512 ),
513 "initial local user data key should be V1-wrapped"
514 );
515
516 let v2_key = SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
517 let upgrade_token = {
518 let mut ctx = client.internal.get_key_store().context_mut();
519 let v2_key_id = ctx.add_local_symmetric_key(v2_key.clone());
520 V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
521 };
522
523 reinit_user_crypto(
524 &client,
525 ReinitUserCryptoRequest {
526 account_cryptographic_state: test_vector_v2_account_state(),
527 upgrade_token,
528 },
529 )
530 .await
531 .expect("V1→V2 reinit with a valid upgrade token should succeed");
532
533 let rewrapped_state = client
535 .platform()
536 .state()
537 .get::<LocalUserDataKeyState>()
538 .unwrap()
539 .get(user_id)
540 .await
541 .unwrap()
542 .expect("LocalUserDataKey state must remain present");
543 assert!(
544 matches!(
545 rewrapped_state.wrapped_key,
546 EncString::Cose_Encrypt0_B64 { .. }
547 ),
548 "rewrapped key should be sealed with the V2 user key"
549 );
550 assert_ne!(rewrapped_state.wrapped_key, v1_user_data_key.wrapped_key);
551 }
552}