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