bitwarden_crypto_sync_handler/
crypto_sync_handler.rs1#[cfg(any(feature = "uniffi", feature = "wasm"))]
4use bitwarden_core::key_management::crypto::{ReinitUserCryptoError, ReinitUserCryptoRequest};
5#[cfg(not(target_arch = "wasm32"))]
6use bitwarden_core::key_management::{
7 MasterPasswordError, V2UpgradeTokenError, WebAuthnPrfError,
8 account_cryptographic_state::AccountKeysResponseParseError,
9};
10use bitwarden_core::{
11 Client,
12 key_management::{
13 MasterPasswordUnlockData, V2UpgradeToken, WebAuthnPrfUnlockData, WebAuthnPrfUnlockOption,
14 account_cryptographic_state::WrappedAccountCryptographicState,
15 },
16};
17use bitwarden_crypto::KeyId;
18use serde::{Deserialize, Serialize};
19#[cfg(any(feature = "uniffi", feature = "wasm"))]
20use tracing::debug;
21use tracing::warn;
22
23#[derive(Serialize, Deserialize, Debug, Clone, Default)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
27#[bitwarden_ffi::wasm_record]
28pub struct CryptoSyncData {
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 #[cfg_attr(feature = "wasm", tsify(optional))]
32 pub user_decryption: Option<CryptoSyncUserDecryption>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 #[cfg_attr(feature = "wasm", tsify(optional))]
36 pub account_cryptographic_state: Option<WrappedAccountCryptographicState>,
37}
38
39#[derive(Serialize, Deserialize, Debug, Clone, Default)]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
43#[bitwarden_ffi::wasm_record]
44pub struct CryptoSyncUserDecryption {
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 #[cfg_attr(feature = "wasm", tsify(optional))]
48 pub master_password_unlock: Option<MasterPasswordUnlockData>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 #[cfg_attr(feature = "wasm", tsify(optional))]
52 pub v2_upgrade_token: Option<V2UpgradeToken>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 #[cfg_attr(feature = "wasm", tsify(optional))]
56 pub web_authn_prf_options: Option<Vec<WebAuthnPrfUnlockOption>>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 #[cfg_attr(feature = "wasm", tsify(optional))]
60 pub user_key_id: Option<KeyId>,
61}
62
63#[cfg(not(target_arch = "wasm32"))]
68#[derive(Debug, thiserror::Error)]
69pub enum CryptoSyncDataParseError {
70 #[error("Sync response carried unparseable master password unlock data")]
72 MasterPasswordUnlock(#[source] MasterPasswordError),
73 #[error("Sync response carried an unparseable V2 upgrade token")]
75 V2UpgradeToken(#[source] V2UpgradeTokenError),
76 #[error("Sync response carried an unparseable WebAuthn PRF unlock option")]
78 WebAuthnPrfOption(#[source] WebAuthnPrfError),
79 #[error("Sync response carried an unparseable user key id")]
81 UserKeyId(#[source] bitwarden_crypto::CryptoError),
82 #[error("Sync response carried unparseable account cryptographic state")]
84 AccountCryptographicState(#[source] AccountKeysResponseParseError),
85}
86
87#[cfg(not(target_arch = "wasm32"))]
88impl TryFrom<&bitwarden_api_api::models::SyncResponseModel> for CryptoSyncData {
89 type Error = CryptoSyncDataParseError;
90
91 fn try_from(
92 response: &bitwarden_api_api::models::SyncResponseModel,
93 ) -> Result<Self, Self::Error> {
94 Ok(Self {
95 user_decryption: response
96 .user_decryption
97 .as_deref()
98 .map(CryptoSyncUserDecryption::try_from)
99 .transpose()?,
100 account_cryptographic_state: response
101 .profile
102 .as_deref()
103 .and_then(|p| p.account_keys.as_deref())
104 .map(WrappedAccountCryptographicState::try_from)
105 .transpose()
106 .map_err(CryptoSyncDataParseError::AccountCryptographicState)?,
107 })
108 }
109}
110
111#[cfg(not(target_arch = "wasm32"))]
112impl TryFrom<&bitwarden_api_api::models::UserDecryptionResponseModel> for CryptoSyncUserDecryption {
113 type Error = CryptoSyncDataParseError;
114
115 fn try_from(
116 response: &bitwarden_api_api::models::UserDecryptionResponseModel,
117 ) -> Result<Self, Self::Error> {
118 Ok(Self {
119 master_password_unlock: response
120 .master_password_unlock
121 .as_deref()
122 .map(MasterPasswordUnlockData::try_from)
123 .transpose()
124 .map_err(CryptoSyncDataParseError::MasterPasswordUnlock)?,
125 v2_upgrade_token: response
126 .v2_upgrade_token
127 .as_deref()
128 .map(V2UpgradeToken::try_from)
129 .transpose()
130 .map_err(CryptoSyncDataParseError::V2UpgradeToken)?,
131 web_authn_prf_options: response
132 .web_authn_prf_options
133 .as_deref()
134 .map(|options| {
135 options
136 .iter()
137 .map(WebAuthnPrfUnlockOption::try_from)
138 .collect::<Result<Vec<_>, _>>()
139 })
140 .transpose()
141 .map_err(CryptoSyncDataParseError::WebAuthnPrfOption)?,
142 user_key_id: response
143 .user_key_id
144 .as_deref()
145 .map(str::parse)
146 .transpose()
147 .map_err(CryptoSyncDataParseError::UserKeyId)?,
148 })
149 }
150}
151
152async fn handle_crypto_sync(client: &Client, data: &CryptoSyncData) {
154 if is_replayed_state(client, data).await {
157 warn!("WARNING: Refusing a V2 to V1 account cryptographic state downgrade.");
158 return;
159 }
160
161 handle_user_decryption_options(client, data).await;
163 handle_account_cryptographic_state(client, data).await;
164 handle_v2_upgrade(client, data).await;
165
166 }
168
169async fn handle_user_decryption_options(client: &Client, data: &CryptoSyncData) {
171 let Some(user_decryption) = data.user_decryption.as_ref() else {
172 return;
173 };
174
175 let state_bridge = client.km_state_bridge();
177 if !state_bridge.is_bridge_registered() {
178 return;
179 }
180
181 match user_decryption.master_password_unlock.as_ref() {
182 Some(master_password_unlock) => {
183 state_bridge
184 .set_masterpassword_unlock_data(master_password_unlock)
185 .await;
186 state_bridge
187 .set_kdf_config(&master_password_unlock.kdf)
188 .await;
189 }
190 None => state_bridge.clear_masterpassword_unlock_data().await,
191 }
192
193 match user_decryption.v2_upgrade_token.as_ref() {
194 Some(v2_upgrade_token) => state_bridge.set_v2_upgrade_token(v2_upgrade_token).await,
195 None => state_bridge.clear_v2_upgrade_token().await,
196 }
197
198 match user_decryption.web_authn_prf_options.as_ref() {
200 Some(options) if !options.is_empty() => {
201 state_bridge
202 .set_webauthn_prf_unlock_data(&WebAuthnPrfUnlockData {
203 options: options.clone(),
204 })
205 .await
206 }
207 _ => state_bridge.clear_webauthn_prf_unlock_data().await,
208 }
209
210 match user_decryption.user_key_id.as_ref() {
213 Some(user_key_id) => state_bridge.set_user_key_id(user_key_id).await,
214 None => state_bridge.clear_user_key_id().await,
215 }
216}
217
218async fn handle_account_cryptographic_state(client: &Client, data: &CryptoSyncData) {
220 let Some(incoming) = data.account_cryptographic_state.as_ref() else {
221 return;
222 };
223
224 let state_bridge = client.km_state_bridge();
226 if !state_bridge.is_bridge_registered() {
227 return;
228 }
229
230 state_bridge.set_account_cryptographic_state(incoming).await;
231}
232
233#[cfg(any(feature = "uniffi", feature = "wasm"))]
242async fn handle_v2_upgrade(client: &Client, data: &CryptoSyncData) {
243 let Some(account_cryptographic_state) = data.account_cryptographic_state.clone() else {
244 return;
245 };
246 let Some(upgrade_token) = data
247 .user_decryption
248 .as_ref()
249 .and_then(|user_decryption| user_decryption.v2_upgrade_token.clone())
250 else {
251 return;
252 };
253
254 let result = client
255 .crypto()
256 .reinit_user_crypto(ReinitUserCryptoRequest {
257 account_cryptographic_state,
258 upgrade_token,
259 })
260 .await;
261
262 match result {
263 Ok(()) => {}
264 Err(ReinitUserCryptoError::NotUnlocked) => debug!("Locked, skipping V2 upgrade reinit"),
265 Err(e) => warn!(error = ?e, "Failed to apply the V2 upgrade to the unlocked client"),
267 }
268}
269
270#[cfg(not(any(feature = "uniffi", feature = "wasm")))]
271async fn handle_v2_upgrade(_client: &Client, _data: &CryptoSyncData) {}
272
273async fn is_replayed_state(client: &Client, data: &CryptoSyncData) -> bool {
277 let Some(incoming) = data.account_cryptographic_state.as_ref() else {
278 return false;
279 };
280 let Some(local) = client
281 .km_state_bridge()
282 .get_account_cryptographic_state()
283 .await
284 else {
285 return false;
286 };
287
288 if is_v2_to_v1_downgrade(&local, incoming) {
289 return true;
290 }
291
292 false
294}
295
296fn is_v2_to_v1_downgrade(
300 local: &WrappedAccountCryptographicState,
301 incoming: &WrappedAccountCryptographicState,
302) -> bool {
303 matches!(local, WrappedAccountCryptographicState::V2 { .. })
304 && matches!(incoming, WrappedAccountCryptographicState::V1 { .. })
305}
306
307#[derive(Clone)]
309#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
310#[bitwarden_ffi::wasm_object]
311pub struct CryptoSyncHandlerClient {
312 client: Client,
313}
314
315impl CryptoSyncHandlerClient {
316 fn new(client: Client) -> Self {
317 Self { client }
318 }
319}
320
321#[bitwarden_ffi::wasm_export]
322#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
323impl CryptoSyncHandlerClient {
324 pub async fn on_sync(&self, data: CryptoSyncData) {
327 handle_crypto_sync(&self.client, &data).await
328 }
329}
330
331pub trait CryptoSyncHandlerClientExt {
333 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient;
335}
336
337impl CryptoSyncHandlerClientExt for Client {
338 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient {
339 CryptoSyncHandlerClient::new(self.clone())
340 }
341}
342
343#[cfg(not(target_arch = "wasm32"))]
354pub struct CryptoSyncHandler {
355 client: Client,
356}
357
358#[cfg(not(target_arch = "wasm32"))]
359impl CryptoSyncHandler {
360 pub fn new(client: Client) -> Self {
362 Self { client }
363 }
364}
365
366#[cfg(not(target_arch = "wasm32"))]
367#[async_trait::async_trait]
368impl bitwarden_sync::SyncHandler for CryptoSyncHandler {
369 async fn on_sync(
370 &self,
371 response: &bitwarden_api_api::models::SyncResponseModel,
372 ) -> Result<(), bitwarden_sync::SyncHandlerError> {
373 let data = CryptoSyncData::try_from(response)?;
376
377 handle_crypto_sync(&self.client, &data).await;
378 Ok(())
379 }
380}
381
382#[cfg(all(test, not(target_arch = "wasm32")))]
383mod tests {
384 use bitwarden_api_api::models::{
385 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
386 SyncResponseModel, UserDecryptionResponseModel, WebAuthnPrfDecryptionOption,
387 };
388 use bitwarden_core::key_management::{
389 KeySlotIds, state_bridge::test_support::InMemoryStateBridge,
390 };
391 use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
392
393 use super::*;
394
395 const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
396 const TEST_SALT: &str = "[email protected]";
397 const TEST_USER_KEY_ID: &str = "000102030405060708090a0b0c0d0e0f";
398
399 fn master_password_unlock(
400 master_key_encrypted_user_key: Option<String>,
401 ) -> MasterPasswordUnlockResponseModel {
402 MasterPasswordUnlockResponseModel {
403 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
404 kdf_type: KdfType::PBKDF2_SHA256,
405 iterations: 600_000,
406 memory: None,
407 parallelism: None,
408 }),
409 master_key_encrypted_user_key,
410 salt: Some(TEST_SALT.to_string()),
411 contained_key_id: None,
412 }
413 }
414
415 fn sync_response(user_decryption: UserDecryptionResponseModel) -> SyncResponseModel {
416 SyncResponseModel {
417 user_decryption: Some(Box::new(user_decryption)),
418 ..Default::default()
419 }
420 }
421
422 #[test]
423 fn test_try_from_empty_response_is_empty_data() {
424 let data = CryptoSyncData::try_from(&SyncResponseModel::default()).unwrap();
425
426 assert!(data.user_decryption.is_none());
427 assert!(data.account_cryptographic_state.is_none());
428 }
429
430 #[test]
431 fn test_try_from_valid_master_password_unlock_succeeds() {
432 let response = sync_response(UserDecryptionResponseModel {
433 master_password_unlock: Some(Box::new(master_password_unlock(Some(
434 TEST_USER_KEY.to_string(),
435 )))),
436 ..Default::default()
437 });
438
439 let data = CryptoSyncData::try_from(&response).unwrap();
440
441 let user_decryption = data.user_decryption.unwrap();
442 assert_eq!(
443 user_decryption.master_password_unlock.unwrap().salt,
444 TEST_SALT
445 );
446 }
447
448 #[test]
449 fn test_try_from_malformed_master_password_unlock_errors() {
450 let response = sync_response(UserDecryptionResponseModel {
452 master_password_unlock: Some(Box::new(master_password_unlock(None))),
453 ..Default::default()
454 });
455
456 assert!(matches!(
457 CryptoSyncData::try_from(&response),
458 Err(CryptoSyncDataParseError::MasterPasswordUnlock(_))
459 ));
460 }
461
462 #[test]
463 fn test_try_from_malformed_webauthn_prf_option_errors() {
464 let response = sync_response(UserDecryptionResponseModel {
465 web_authn_prf_options: Some(vec![WebAuthnPrfDecryptionOption::default()]),
466 ..Default::default()
467 });
468
469 assert!(matches!(
470 CryptoSyncData::try_from(&response),
471 Err(CryptoSyncDataParseError::WebAuthnPrfOption(_))
472 ));
473 }
474
475 #[test]
476 fn test_try_from_valid_user_key_id_succeeds() {
477 let response = sync_response(UserDecryptionResponseModel {
478 user_key_id: Some(TEST_USER_KEY_ID.to_string()),
479 ..Default::default()
480 });
481
482 let data = CryptoSyncData::try_from(&response).unwrap();
483
484 let user_key_id = data.user_decryption.unwrap().user_key_id.unwrap();
485 assert_eq!(user_key_id.to_string(), TEST_USER_KEY_ID);
486 }
487
488 #[test]
489 fn test_try_from_absent_user_key_id_is_none() {
490 let response = sync_response(UserDecryptionResponseModel {
492 master_password_unlock: Some(Box::new(master_password_unlock(Some(
493 TEST_USER_KEY.to_string(),
494 )))),
495 ..Default::default()
496 });
497
498 let data = CryptoSyncData::try_from(&response).unwrap();
499
500 assert!(data.user_decryption.unwrap().user_key_id.is_none());
501 }
502
503 #[test]
504 fn test_try_from_malformed_user_key_id_errors() {
505 for malformed in [
506 "not hex at all",
507 "00ff",
509 "000102030405060708090a0b0c0d0e0",
511 ] {
512 let response = sync_response(UserDecryptionResponseModel {
513 user_key_id: Some(malformed.to_string()),
514 ..Default::default()
515 });
516
517 assert!(
518 matches!(
519 CryptoSyncData::try_from(&response),
520 Err(CryptoSyncDataParseError::UserKeyId(_))
521 ),
522 "{malformed:?} should not parse as a key id"
523 );
524 }
525 }
526
527 #[tokio::test]
530 async fn test_handle_crypto_sync_writes_and_clears_user_key_id() {
531 let client = Client::new(None);
532 client
533 .km_state_bridge()
534 .register_bridge(Box::new(InMemoryStateBridge::default()));
535
536 let with_key_id = CryptoSyncData::try_from(&sync_response(UserDecryptionResponseModel {
537 user_key_id: Some(TEST_USER_KEY_ID.to_string()),
538 ..Default::default()
539 }))
540 .unwrap();
541 handle_crypto_sync(&client, &with_key_id).await;
542
543 let stored = client.km_state_bridge().get_user_key_id().await.unwrap();
544 assert_eq!(stored.to_string(), TEST_USER_KEY_ID);
545
546 let without_key_id =
547 CryptoSyncData::try_from(&sync_response(UserDecryptionResponseModel::default()))
548 .unwrap();
549 handle_crypto_sync(&client, &without_key_id).await;
550
551 assert!(client.km_state_bridge().get_user_key_id().await.is_none());
552 }
553
554 fn client_with_bridge() -> Client {
556 let client = Client::new(None);
557 client
558 .km_state_bridge()
559 .register_bridge(Box::new(InMemoryStateBridge::default()));
560 client
561 }
562
563 fn make_v2_state() -> WrappedAccountCryptographicState {
564 let store: KeyStore<KeySlotIds> = KeyStore::default();
565 let mut ctx = store.context_mut();
566 let (_, state) =
567 WrappedAccountCryptographicState::make(&mut ctx).expect("making a V2 state succeeds");
568 state
569 }
570
571 fn make_v1_state() -> WrappedAccountCryptographicState {
572 let store: KeyStore<KeySlotIds> = KeyStore::default();
573 let mut ctx = store.context_mut();
574 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
575 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
576
577 WrappedAccountCryptographicState::V1 {
578 private_key: ctx
579 .wrap_private_key(user_key, private_key)
580 .expect("wrapping the private key succeeds"),
581 }
582 }
583
584 async fn sync_account_cryptographic_state(
586 client: &Client,
587 incoming: &WrappedAccountCryptographicState,
588 ) -> Option<WrappedAccountCryptographicState> {
589 let data = CryptoSyncData {
590 account_cryptographic_state: Some(incoming.clone()),
591 ..Default::default()
592 };
593 handle_crypto_sync(client, &data).await;
594 client
595 .km_state_bridge()
596 .get_account_cryptographic_state()
597 .await
598 }
599
600 #[tokio::test]
601 async fn test_account_cryptographic_state_is_persisted_when_there_is_no_local_state() {
602 let client = client_with_bridge();
603 let incoming = make_v2_state();
604
605 let stored = sync_account_cryptographic_state(&client, &incoming).await;
606
607 assert_eq!(stored.as_ref(), Some(&incoming));
608 }
609
610 #[tokio::test]
611 async fn test_account_cryptographic_state_upgrade_from_v1_to_v2_is_persisted() {
612 let client = client_with_bridge();
613 client
614 .km_state_bridge()
615 .set_account_cryptographic_state(&make_v1_state())
616 .await;
617 let incoming = make_v2_state();
618
619 let stored = sync_account_cryptographic_state(&client, &incoming).await;
620
621 assert_eq!(stored.as_ref(), Some(&incoming));
622 }
623
624 #[tokio::test]
625 async fn test_account_cryptographic_state_downgrade_from_v2_to_v1_is_rejected() {
626 let client = client_with_bridge();
627 let local = make_v2_state();
628 client
629 .km_state_bridge()
630 .set_account_cryptographic_state(&local)
631 .await;
632
633 let stored = sync_account_cryptographic_state(&client, &make_v1_state()).await;
634
635 assert_eq!(stored.as_ref(), Some(&local));
636 }
637}