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