bitwarden_crypto_sync_handler/
crypto_sync_handler.rs1#[cfg(not(target_arch = "wasm32"))]
4use bitwarden_core::key_management::{
5 MasterPasswordError, V2UpgradeTokenError, WebAuthnPrfError,
6 account_cryptographic_state::AccountKeysResponseParseError,
7};
8use bitwarden_core::{
9 Client,
10 key_management::{
11 MasterPasswordUnlockData, V2UpgradeToken, WebAuthnPrfUnlockData, WebAuthnPrfUnlockOption,
12 account_cryptographic_state::WrappedAccountCryptographicState,
13 },
14};
15use bitwarden_crypto::KeyId;
16use serde::{Deserialize, Serialize};
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::*;
19
20#[derive(Serialize, Deserialize, Debug, Clone, Default)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
24#[cfg_attr(
25 feature = "wasm",
26 derive(tsify::Tsify),
27 tsify(into_wasm_abi, from_wasm_abi)
28)]
29pub struct CryptoSyncData {
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 #[cfg_attr(feature = "wasm", tsify(optional))]
33 pub user_decryption: Option<CryptoSyncUserDecryption>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 #[cfg_attr(feature = "wasm", tsify(optional))]
37 pub account_cryptographic_state: Option<WrappedAccountCryptographicState>,
38}
39
40#[derive(Serialize, Deserialize, Debug, Clone, Default)]
42#[serde(rename_all = "camelCase", deny_unknown_fields)]
43#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
44#[cfg_attr(
45 feature = "wasm",
46 derive(tsify::Tsify),
47 tsify(into_wasm_abi, from_wasm_abi)
48)]
49pub struct CryptoSyncUserDecryption {
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 #[cfg_attr(feature = "wasm", tsify(optional))]
53 pub master_password_unlock: Option<MasterPasswordUnlockData>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 #[cfg_attr(feature = "wasm", tsify(optional))]
57 pub v2_upgrade_token: Option<V2UpgradeToken>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 #[cfg_attr(feature = "wasm", tsify(optional))]
61 pub web_authn_prf_options: Option<Vec<WebAuthnPrfUnlockOption>>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 #[cfg_attr(feature = "wasm", tsify(optional))]
65 pub user_key_id: Option<KeyId>,
66}
67
68#[cfg(not(target_arch = "wasm32"))]
73#[derive(Debug, thiserror::Error)]
74pub enum CryptoSyncDataParseError {
75 #[error("Sync response carried unparseable master password unlock data")]
77 MasterPasswordUnlock(#[source] MasterPasswordError),
78 #[error("Sync response carried an unparseable V2 upgrade token")]
80 V2UpgradeToken(#[source] V2UpgradeTokenError),
81 #[error("Sync response carried an unparseable WebAuthn PRF unlock option")]
83 WebAuthnPrfOption(#[source] WebAuthnPrfError),
84 #[error("Sync response carried an unparseable user key id")]
86 UserKeyId(#[source] bitwarden_crypto::CryptoError),
87 #[error("Sync response carried unparseable account cryptographic state")]
89 AccountCryptographicState(#[source] AccountKeysResponseParseError),
90}
91
92#[cfg(not(target_arch = "wasm32"))]
93impl TryFrom<&bitwarden_api_api::models::SyncResponseModel> for CryptoSyncData {
94 type Error = CryptoSyncDataParseError;
95
96 fn try_from(
97 response: &bitwarden_api_api::models::SyncResponseModel,
98 ) -> Result<Self, Self::Error> {
99 Ok(Self {
100 user_decryption: response
101 .user_decryption
102 .as_deref()
103 .map(CryptoSyncUserDecryption::try_from)
104 .transpose()?,
105 account_cryptographic_state: response
106 .profile
107 .as_deref()
108 .and_then(|p| p.account_keys.as_deref())
109 .map(WrappedAccountCryptographicState::try_from)
110 .transpose()
111 .map_err(CryptoSyncDataParseError::AccountCryptographicState)?,
112 })
113 }
114}
115
116#[cfg(not(target_arch = "wasm32"))]
117impl TryFrom<&bitwarden_api_api::models::UserDecryptionResponseModel> for CryptoSyncUserDecryption {
118 type Error = CryptoSyncDataParseError;
119
120 fn try_from(
121 response: &bitwarden_api_api::models::UserDecryptionResponseModel,
122 ) -> Result<Self, Self::Error> {
123 Ok(Self {
124 master_password_unlock: response
125 .master_password_unlock
126 .as_deref()
127 .map(MasterPasswordUnlockData::try_from)
128 .transpose()
129 .map_err(CryptoSyncDataParseError::MasterPasswordUnlock)?,
130 v2_upgrade_token: response
131 .v2_upgrade_token
132 .as_deref()
133 .map(V2UpgradeToken::try_from)
134 .transpose()
135 .map_err(CryptoSyncDataParseError::V2UpgradeToken)?,
136 web_authn_prf_options: response
137 .web_authn_prf_options
138 .as_deref()
139 .map(|options| {
140 options
141 .iter()
142 .map(WebAuthnPrfUnlockOption::try_from)
143 .collect::<Result<Vec<_>, _>>()
144 })
145 .transpose()
146 .map_err(CryptoSyncDataParseError::WebAuthnPrfOption)?,
147 user_key_id: response
148 .user_key_id
149 .as_deref()
150 .map(str::parse)
151 .transpose()
152 .map_err(CryptoSyncDataParseError::UserKeyId)?,
153 })
154 }
155}
156
157async fn handle_crypto_sync(client: &Client, data: &CryptoSyncData) {
159 handle_user_decryption_options(client, data).await;
161 handle_account_cryptographic_state(client, data).await;
162
163 }
165
166async fn handle_user_decryption_options(client: &Client, data: &CryptoSyncData) {
168 let Some(user_decryption) = data.user_decryption.as_ref() else {
169 return;
170 };
171
172 let state_bridge = client.km_state_bridge();
174 if !state_bridge.is_bridge_registered() {
175 return;
176 }
177
178 match user_decryption.master_password_unlock.as_ref() {
179 Some(master_password_unlock) => {
180 state_bridge
181 .set_masterpassword_unlock_data(master_password_unlock)
182 .await;
183 state_bridge
184 .set_kdf_config(&master_password_unlock.kdf)
185 .await;
186 }
187 None => state_bridge.clear_masterpassword_unlock_data().await,
188 }
189
190 match user_decryption.v2_upgrade_token.as_ref() {
191 Some(v2_upgrade_token) => state_bridge.set_v2_upgrade_token(v2_upgrade_token).await,
192 None => state_bridge.clear_v2_upgrade_token().await,
193 }
194
195 match user_decryption.web_authn_prf_options.as_ref() {
197 Some(options) if !options.is_empty() => {
198 state_bridge
199 .set_webauthn_prf_unlock_data(&WebAuthnPrfUnlockData {
200 options: options.clone(),
201 })
202 .await
203 }
204 _ => state_bridge.clear_webauthn_prf_unlock_data().await,
205 }
206
207 match user_decryption.user_key_id.as_ref() {
210 Some(user_key_id) => state_bridge.set_user_key_id(user_key_id).await,
211 None => state_bridge.clear_user_key_id().await,
212 }
213}
214
215async fn handle_account_cryptographic_state(client: &Client, data: &CryptoSyncData) {
217 let Some(account_cryptographic_state) = data.account_cryptographic_state.as_ref() else {
218 return;
219 };
220
221 let state_bridge = client.km_state_bridge();
223 if !state_bridge.is_bridge_registered() {
224 return;
225 }
226
227 state_bridge
228 .set_account_cryptographic_state(account_cryptographic_state)
229 .await;
230}
231
232#[derive(Clone)]
234#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
235#[cfg_attr(feature = "wasm", wasm_bindgen)]
236pub struct CryptoSyncHandlerClient {
237 client: Client,
238}
239
240impl CryptoSyncHandlerClient {
241 fn new(client: Client) -> Self {
242 Self { client }
243 }
244}
245
246#[cfg_attr(feature = "wasm", wasm_bindgen)]
247#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
248impl CryptoSyncHandlerClient {
249 pub async fn on_sync(&self, data: CryptoSyncData) {
252 handle_crypto_sync(&self.client, &data).await
253 }
254}
255
256pub trait CryptoSyncHandlerClientExt {
258 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient;
260}
261
262impl CryptoSyncHandlerClientExt for Client {
263 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient {
264 CryptoSyncHandlerClient::new(self.clone())
265 }
266}
267
268#[cfg(not(target_arch = "wasm32"))]
279pub struct CryptoSyncHandler {
280 client: Client,
281}
282
283#[cfg(not(target_arch = "wasm32"))]
284impl CryptoSyncHandler {
285 pub fn new(client: Client) -> Self {
287 Self { client }
288 }
289}
290
291#[cfg(not(target_arch = "wasm32"))]
292#[async_trait::async_trait]
293impl bitwarden_sync::SyncHandler for CryptoSyncHandler {
294 async fn on_sync(
295 &self,
296 response: &bitwarden_api_api::models::SyncResponseModel,
297 ) -> Result<(), bitwarden_sync::SyncHandlerError> {
298 let data = CryptoSyncData::try_from(response)?;
301
302 handle_crypto_sync(&self.client, &data).await;
303 Ok(())
304 }
305}
306
307#[cfg(all(test, not(target_arch = "wasm32")))]
308mod tests {
309 use bitwarden_api_api::models::{
310 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
311 SyncResponseModel, UserDecryptionResponseModel, WebAuthnPrfDecryptionOption,
312 };
313 use bitwarden_core::key_management::state_bridge::test_support::InMemoryStateBridge;
314
315 use super::*;
316
317 const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
318 const TEST_SALT: &str = "[email protected]";
319 const TEST_USER_KEY_ID: &str = "000102030405060708090a0b0c0d0e0f";
320
321 fn master_password_unlock(
322 master_key_encrypted_user_key: Option<String>,
323 ) -> MasterPasswordUnlockResponseModel {
324 MasterPasswordUnlockResponseModel {
325 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
326 kdf_type: KdfType::PBKDF2_SHA256,
327 iterations: 600_000,
328 memory: None,
329 parallelism: None,
330 }),
331 master_key_encrypted_user_key,
332 salt: Some(TEST_SALT.to_string()),
333 contained_key_id: None,
334 }
335 }
336
337 fn sync_response(user_decryption: UserDecryptionResponseModel) -> SyncResponseModel {
338 SyncResponseModel {
339 user_decryption: Some(Box::new(user_decryption)),
340 ..Default::default()
341 }
342 }
343
344 #[test]
345 fn test_try_from_empty_response_is_empty_data() {
346 let data = CryptoSyncData::try_from(&SyncResponseModel::default()).unwrap();
347
348 assert!(data.user_decryption.is_none());
349 assert!(data.account_cryptographic_state.is_none());
350 }
351
352 #[test]
353 fn test_try_from_valid_master_password_unlock_succeeds() {
354 let response = sync_response(UserDecryptionResponseModel {
355 master_password_unlock: Some(Box::new(master_password_unlock(Some(
356 TEST_USER_KEY.to_string(),
357 )))),
358 ..Default::default()
359 });
360
361 let data = CryptoSyncData::try_from(&response).unwrap();
362
363 let user_decryption = data.user_decryption.unwrap();
364 assert_eq!(
365 user_decryption.master_password_unlock.unwrap().salt,
366 TEST_SALT
367 );
368 }
369
370 #[test]
371 fn test_try_from_malformed_master_password_unlock_errors() {
372 let response = sync_response(UserDecryptionResponseModel {
374 master_password_unlock: Some(Box::new(master_password_unlock(None))),
375 ..Default::default()
376 });
377
378 assert!(matches!(
379 CryptoSyncData::try_from(&response),
380 Err(CryptoSyncDataParseError::MasterPasswordUnlock(_))
381 ));
382 }
383
384 #[test]
385 fn test_try_from_malformed_webauthn_prf_option_errors() {
386 let response = sync_response(UserDecryptionResponseModel {
387 web_authn_prf_options: Some(vec![WebAuthnPrfDecryptionOption::default()]),
388 ..Default::default()
389 });
390
391 assert!(matches!(
392 CryptoSyncData::try_from(&response),
393 Err(CryptoSyncDataParseError::WebAuthnPrfOption(_))
394 ));
395 }
396
397 #[test]
398 fn test_try_from_valid_user_key_id_succeeds() {
399 let response = sync_response(UserDecryptionResponseModel {
400 user_key_id: Some(TEST_USER_KEY_ID.to_string()),
401 ..Default::default()
402 });
403
404 let data = CryptoSyncData::try_from(&response).unwrap();
405
406 let user_key_id = data.user_decryption.unwrap().user_key_id.unwrap();
407 assert_eq!(user_key_id.to_string(), TEST_USER_KEY_ID);
408 }
409
410 #[test]
411 fn test_try_from_absent_user_key_id_is_none() {
412 let response = sync_response(UserDecryptionResponseModel {
414 master_password_unlock: Some(Box::new(master_password_unlock(Some(
415 TEST_USER_KEY.to_string(),
416 )))),
417 ..Default::default()
418 });
419
420 let data = CryptoSyncData::try_from(&response).unwrap();
421
422 assert!(data.user_decryption.unwrap().user_key_id.is_none());
423 }
424
425 #[test]
426 fn test_try_from_malformed_user_key_id_errors() {
427 for malformed in [
428 "not hex at all",
429 "00ff",
431 "000102030405060708090a0b0c0d0e0",
433 ] {
434 let response = sync_response(UserDecryptionResponseModel {
435 user_key_id: Some(malformed.to_string()),
436 ..Default::default()
437 });
438
439 assert!(
440 matches!(
441 CryptoSyncData::try_from(&response),
442 Err(CryptoSyncDataParseError::UserKeyId(_))
443 ),
444 "{malformed:?} should not parse as a key id"
445 );
446 }
447 }
448
449 #[tokio::test]
452 async fn test_handle_crypto_sync_writes_and_clears_user_key_id() {
453 let client = Client::new(None);
454 client
455 .km_state_bridge()
456 .register_bridge(Box::new(InMemoryStateBridge::default()));
457
458 let with_key_id = CryptoSyncData::try_from(&sync_response(UserDecryptionResponseModel {
459 user_key_id: Some(TEST_USER_KEY_ID.to_string()),
460 ..Default::default()
461 }))
462 .unwrap();
463 handle_crypto_sync(&client, &with_key_id).await;
464
465 let stored = client.km_state_bridge().get_user_key_id().await.unwrap();
466 assert_eq!(stored.to_string(), TEST_USER_KEY_ID);
467
468 let without_key_id =
469 CryptoSyncData::try_from(&sync_response(UserDecryptionResponseModel::default()))
470 .unwrap();
471 handle_crypto_sync(&client, &without_key_id).await;
472
473 assert!(client.km_state_bridge().get_user_key_id().await.is_none());
474 }
475}