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 handle_user_decryption_options(client, data).await;
162 handle_account_cryptographic_state(client, data).await;
163
164 }
166
167async fn handle_user_decryption_options(client: &Client, data: &CryptoSyncData) {
169 let Some(user_decryption) = data.user_decryption.as_ref() else {
170 return;
171 };
172
173 let state_bridge = client.km_state_bridge();
175 if !state_bridge.is_bridge_registered() {
176 return;
177 }
178
179 match user_decryption.master_password_unlock.as_ref() {
180 Some(master_password_unlock) => {
181 state_bridge
182 .set_masterpassword_unlock_data(master_password_unlock)
183 .await;
184 state_bridge
185 .set_kdf_config(&master_password_unlock.kdf)
186 .await;
187 }
188 None => state_bridge.clear_masterpassword_unlock_data().await,
189 }
190
191 match user_decryption.v2_upgrade_token.as_ref() {
192 Some(v2_upgrade_token) => state_bridge.set_v2_upgrade_token(v2_upgrade_token).await,
193 None => state_bridge.clear_v2_upgrade_token().await,
194 }
195
196 match user_decryption.web_authn_prf_options.as_ref() {
198 Some(options) if !options.is_empty() => {
199 state_bridge
200 .set_webauthn_prf_unlock_data(&WebAuthnPrfUnlockData {
201 options: options.clone(),
202 })
203 .await
204 }
205 _ => state_bridge.clear_webauthn_prf_unlock_data().await,
206 }
207
208 match user_decryption.user_key_id.as_ref() {
211 Some(user_key_id) => state_bridge.set_user_key_id(user_key_id).await,
212 None => state_bridge.clear_user_key_id().await,
213 }
214}
215
216async fn handle_account_cryptographic_state(client: &Client, data: &CryptoSyncData) {
218 let Some(incoming) = data.account_cryptographic_state.as_ref() else {
219 return;
220 };
221
222 let state_bridge = client.km_state_bridge();
224 if !state_bridge.is_bridge_registered() {
225 return;
226 }
227
228 if let Some(local) = state_bridge.get_account_cryptographic_state().await
231 && is_v2_to_v1_downgrade(&local, incoming)
232 {
233 warn!("Refusing a V2 to V1 account cryptographic state downgrade; keeping the local state");
234 return;
235 }
236
237 state_bridge.set_account_cryptographic_state(incoming).await;
238}
239
240fn is_v2_to_v1_downgrade(
244 local: &WrappedAccountCryptographicState,
245 incoming: &WrappedAccountCryptographicState,
246) -> bool {
247 matches!(local, WrappedAccountCryptographicState::V2 { .. })
248 && matches!(incoming, WrappedAccountCryptographicState::V1 { .. })
249}
250
251#[derive(Clone)]
253#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
254#[cfg_attr(feature = "wasm", wasm_bindgen)]
255pub struct CryptoSyncHandlerClient {
256 client: Client,
257}
258
259impl CryptoSyncHandlerClient {
260 fn new(client: Client) -> Self {
261 Self { client }
262 }
263}
264
265#[cfg_attr(feature = "wasm", wasm_bindgen)]
266#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
267impl CryptoSyncHandlerClient {
268 pub async fn on_sync(&self, data: CryptoSyncData) {
271 handle_crypto_sync(&self.client, &data).await
272 }
273}
274
275pub trait CryptoSyncHandlerClientExt {
277 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient;
279}
280
281impl CryptoSyncHandlerClientExt for Client {
282 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient {
283 CryptoSyncHandlerClient::new(self.clone())
284 }
285}
286
287#[cfg(not(target_arch = "wasm32"))]
298pub struct CryptoSyncHandler {
299 client: Client,
300}
301
302#[cfg(not(target_arch = "wasm32"))]
303impl CryptoSyncHandler {
304 pub fn new(client: Client) -> Self {
306 Self { client }
307 }
308}
309
310#[cfg(not(target_arch = "wasm32"))]
311#[async_trait::async_trait]
312impl bitwarden_sync::SyncHandler for CryptoSyncHandler {
313 async fn on_sync(
314 &self,
315 response: &bitwarden_api_api::models::SyncResponseModel,
316 ) -> Result<(), bitwarden_sync::SyncHandlerError> {
317 let data = CryptoSyncData::try_from(response)?;
320
321 handle_crypto_sync(&self.client, &data).await;
322 Ok(())
323 }
324}
325
326#[cfg(all(test, not(target_arch = "wasm32")))]
327mod tests {
328 use bitwarden_api_api::models::{
329 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
330 SyncResponseModel, UserDecryptionResponseModel, WebAuthnPrfDecryptionOption,
331 };
332 use bitwarden_core::key_management::{
333 KeySlotIds, state_bridge::test_support::InMemoryStateBridge,
334 };
335 use bitwarden_crypto::{KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm};
336
337 use super::*;
338
339 const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
340 const TEST_SALT: &str = "[email protected]";
341 const TEST_USER_KEY_ID: &str = "000102030405060708090a0b0c0d0e0f";
342
343 fn master_password_unlock(
344 master_key_encrypted_user_key: Option<String>,
345 ) -> MasterPasswordUnlockResponseModel {
346 MasterPasswordUnlockResponseModel {
347 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
348 kdf_type: KdfType::PBKDF2_SHA256,
349 iterations: 600_000,
350 memory: None,
351 parallelism: None,
352 }),
353 master_key_encrypted_user_key,
354 salt: Some(TEST_SALT.to_string()),
355 contained_key_id: None,
356 }
357 }
358
359 fn sync_response(user_decryption: UserDecryptionResponseModel) -> SyncResponseModel {
360 SyncResponseModel {
361 user_decryption: Some(Box::new(user_decryption)),
362 ..Default::default()
363 }
364 }
365
366 #[test]
367 fn test_try_from_empty_response_is_empty_data() {
368 let data = CryptoSyncData::try_from(&SyncResponseModel::default()).unwrap();
369
370 assert!(data.user_decryption.is_none());
371 assert!(data.account_cryptographic_state.is_none());
372 }
373
374 #[test]
375 fn test_try_from_valid_master_password_unlock_succeeds() {
376 let response = sync_response(UserDecryptionResponseModel {
377 master_password_unlock: Some(Box::new(master_password_unlock(Some(
378 TEST_USER_KEY.to_string(),
379 )))),
380 ..Default::default()
381 });
382
383 let data = CryptoSyncData::try_from(&response).unwrap();
384
385 let user_decryption = data.user_decryption.unwrap();
386 assert_eq!(
387 user_decryption.master_password_unlock.unwrap().salt,
388 TEST_SALT
389 );
390 }
391
392 #[test]
393 fn test_try_from_malformed_master_password_unlock_errors() {
394 let response = sync_response(UserDecryptionResponseModel {
396 master_password_unlock: Some(Box::new(master_password_unlock(None))),
397 ..Default::default()
398 });
399
400 assert!(matches!(
401 CryptoSyncData::try_from(&response),
402 Err(CryptoSyncDataParseError::MasterPasswordUnlock(_))
403 ));
404 }
405
406 #[test]
407 fn test_try_from_malformed_webauthn_prf_option_errors() {
408 let response = sync_response(UserDecryptionResponseModel {
409 web_authn_prf_options: Some(vec![WebAuthnPrfDecryptionOption::default()]),
410 ..Default::default()
411 });
412
413 assert!(matches!(
414 CryptoSyncData::try_from(&response),
415 Err(CryptoSyncDataParseError::WebAuthnPrfOption(_))
416 ));
417 }
418
419 #[test]
420 fn test_try_from_valid_user_key_id_succeeds() {
421 let response = sync_response(UserDecryptionResponseModel {
422 user_key_id: Some(TEST_USER_KEY_ID.to_string()),
423 ..Default::default()
424 });
425
426 let data = CryptoSyncData::try_from(&response).unwrap();
427
428 let user_key_id = data.user_decryption.unwrap().user_key_id.unwrap();
429 assert_eq!(user_key_id.to_string(), TEST_USER_KEY_ID);
430 }
431
432 #[test]
433 fn test_try_from_absent_user_key_id_is_none() {
434 let response = sync_response(UserDecryptionResponseModel {
436 master_password_unlock: Some(Box::new(master_password_unlock(Some(
437 TEST_USER_KEY.to_string(),
438 )))),
439 ..Default::default()
440 });
441
442 let data = CryptoSyncData::try_from(&response).unwrap();
443
444 assert!(data.user_decryption.unwrap().user_key_id.is_none());
445 }
446
447 #[test]
448 fn test_try_from_malformed_user_key_id_errors() {
449 for malformed in [
450 "not hex at all",
451 "00ff",
453 "000102030405060708090a0b0c0d0e0",
455 ] {
456 let response = sync_response(UserDecryptionResponseModel {
457 user_key_id: Some(malformed.to_string()),
458 ..Default::default()
459 });
460
461 assert!(
462 matches!(
463 CryptoSyncData::try_from(&response),
464 Err(CryptoSyncDataParseError::UserKeyId(_))
465 ),
466 "{malformed:?} should not parse as a key id"
467 );
468 }
469 }
470
471 #[tokio::test]
474 async fn test_handle_crypto_sync_writes_and_clears_user_key_id() {
475 let client = Client::new(None);
476 client
477 .km_state_bridge()
478 .register_bridge(Box::new(InMemoryStateBridge::default()));
479
480 let with_key_id = CryptoSyncData::try_from(&sync_response(UserDecryptionResponseModel {
481 user_key_id: Some(TEST_USER_KEY_ID.to_string()),
482 ..Default::default()
483 }))
484 .unwrap();
485 handle_crypto_sync(&client, &with_key_id).await;
486
487 let stored = client.km_state_bridge().get_user_key_id().await.unwrap();
488 assert_eq!(stored.to_string(), TEST_USER_KEY_ID);
489
490 let without_key_id =
491 CryptoSyncData::try_from(&sync_response(UserDecryptionResponseModel::default()))
492 .unwrap();
493 handle_crypto_sync(&client, &without_key_id).await;
494
495 assert!(client.km_state_bridge().get_user_key_id().await.is_none());
496 }
497
498 fn client_with_bridge() -> Client {
500 let client = Client::new(None);
501 client
502 .km_state_bridge()
503 .register_bridge(Box::new(InMemoryStateBridge::default()));
504 client
505 }
506
507 fn make_v2_state() -> WrappedAccountCryptographicState {
508 let store: KeyStore<KeySlotIds> = KeyStore::default();
509 let mut ctx = store.context_mut();
510 let (_, state) =
511 WrappedAccountCryptographicState::make(&mut ctx).expect("making a V2 state succeeds");
512 state
513 }
514
515 fn make_v1_state() -> WrappedAccountCryptographicState {
516 let store: KeyStore<KeySlotIds> = KeyStore::default();
517 let mut ctx = store.context_mut();
518 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
519 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
520
521 WrappedAccountCryptographicState::V1 {
522 private_key: ctx
523 .wrap_private_key(user_key, private_key)
524 .expect("wrapping the private key succeeds"),
525 }
526 }
527
528 async fn sync_account_cryptographic_state(
530 client: &Client,
531 incoming: &WrappedAccountCryptographicState,
532 ) -> Option<WrappedAccountCryptographicState> {
533 let data = CryptoSyncData {
534 account_cryptographic_state: Some(incoming.clone()),
535 ..Default::default()
536 };
537 handle_account_cryptographic_state(client, &data).await;
538 client
539 .km_state_bridge()
540 .get_account_cryptographic_state()
541 .await
542 }
543
544 #[tokio::test]
545 async fn test_account_cryptographic_state_is_persisted_when_there_is_no_local_state() {
546 let client = client_with_bridge();
547 let incoming = make_v2_state();
548
549 let stored = sync_account_cryptographic_state(&client, &incoming).await;
550
551 assert_eq!(stored.as_ref(), Some(&incoming));
552 }
553
554 #[tokio::test]
555 async fn test_account_cryptographic_state_upgrade_from_v1_to_v2_is_persisted() {
556 let client = client_with_bridge();
557 client
558 .km_state_bridge()
559 .set_account_cryptographic_state(&make_v1_state())
560 .await;
561 let incoming = make_v2_state();
562
563 let stored = sync_account_cryptographic_state(&client, &incoming).await;
564
565 assert_eq!(stored.as_ref(), Some(&incoming));
566 }
567
568 #[tokio::test]
569 async fn test_account_cryptographic_state_downgrade_from_v2_to_v1_is_rejected() {
570 let client = client_with_bridge();
571 let local = make_v2_state();
572 client
573 .km_state_bridge()
574 .set_account_cryptographic_state(&local)
575 .await;
576
577 let stored = sync_account_cryptographic_state(&client, &make_v1_state()).await;
578
579 assert_eq!(stored.as_ref(), Some(&local));
580 }
581}