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 serde::{Deserialize, Serialize};
16#[cfg(feature = "wasm")]
17use wasm_bindgen::prelude::*;
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#[cfg_attr(
24 feature = "wasm",
25 derive(tsify::Tsify),
26 tsify(into_wasm_abi, from_wasm_abi)
27)]
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#[cfg_attr(
44 feature = "wasm",
45 derive(tsify::Tsify),
46 tsify(into_wasm_abi, from_wasm_abi)
47)]
48pub struct CryptoSyncUserDecryption {
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 #[cfg_attr(feature = "wasm", tsify(optional))]
52 pub master_password_unlock: Option<MasterPasswordUnlockData>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 #[cfg_attr(feature = "wasm", tsify(optional))]
56 pub v2_upgrade_token: Option<V2UpgradeToken>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 #[cfg_attr(feature = "wasm", tsify(optional))]
60 pub web_authn_prf_options: Option<Vec<WebAuthnPrfUnlockOption>>,
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 unparseable account cryptographic state")]
81 AccountCryptographicState(#[source] AccountKeysResponseParseError),
82}
83
84#[cfg(not(target_arch = "wasm32"))]
85impl TryFrom<&bitwarden_api_api::models::SyncResponseModel> for CryptoSyncData {
86 type Error = CryptoSyncDataParseError;
87
88 fn try_from(
89 response: &bitwarden_api_api::models::SyncResponseModel,
90 ) -> Result<Self, Self::Error> {
91 Ok(Self {
92 user_decryption: response
93 .user_decryption
94 .as_deref()
95 .map(CryptoSyncUserDecryption::try_from)
96 .transpose()?,
97 account_cryptographic_state: response
98 .profile
99 .as_deref()
100 .and_then(|p| p.account_keys.as_deref())
101 .map(WrappedAccountCryptographicState::try_from)
102 .transpose()
103 .map_err(CryptoSyncDataParseError::AccountCryptographicState)?,
104 })
105 }
106}
107
108#[cfg(not(target_arch = "wasm32"))]
109impl TryFrom<&bitwarden_api_api::models::UserDecryptionResponseModel> for CryptoSyncUserDecryption {
110 type Error = CryptoSyncDataParseError;
111
112 fn try_from(
113 response: &bitwarden_api_api::models::UserDecryptionResponseModel,
114 ) -> Result<Self, Self::Error> {
115 Ok(Self {
116 master_password_unlock: response
117 .master_password_unlock
118 .as_deref()
119 .map(MasterPasswordUnlockData::try_from)
120 .transpose()
121 .map_err(CryptoSyncDataParseError::MasterPasswordUnlock)?,
122 v2_upgrade_token: response
123 .v2_upgrade_token
124 .as_deref()
125 .map(V2UpgradeToken::try_from)
126 .transpose()
127 .map_err(CryptoSyncDataParseError::V2UpgradeToken)?,
128 web_authn_prf_options: response
129 .web_authn_prf_options
130 .as_deref()
131 .map(|options| {
132 options
133 .iter()
134 .map(WebAuthnPrfUnlockOption::try_from)
135 .collect::<Result<Vec<_>, _>>()
136 })
137 .transpose()
138 .map_err(CryptoSyncDataParseError::WebAuthnPrfOption)?,
139 })
140 }
141}
142
143async fn handle_crypto_sync(client: &Client, data: &CryptoSyncData) {
145 handle_user_decryption_options(client, data).await;
147 handle_account_cryptographic_state(client, data).await;
148
149 }
151
152async fn handle_user_decryption_options(client: &Client, data: &CryptoSyncData) {
154 let Some(user_decryption) = data.user_decryption.as_ref() else {
155 return;
156 };
157
158 let state_bridge = client.km_state_bridge();
160 if !state_bridge.is_bridge_registered() {
161 return;
162 }
163
164 match user_decryption.master_password_unlock.as_ref() {
165 Some(master_password_unlock) => {
166 state_bridge
167 .set_masterpassword_unlock_data(master_password_unlock)
168 .await;
169 state_bridge
170 .set_kdf_config(&master_password_unlock.kdf)
171 .await;
172 }
173 None => state_bridge.clear_masterpassword_unlock_data().await,
174 }
175
176 match user_decryption.v2_upgrade_token.as_ref() {
177 Some(v2_upgrade_token) => state_bridge.set_v2_upgrade_token(v2_upgrade_token).await,
178 None => state_bridge.clear_v2_upgrade_token().await,
179 }
180
181 match user_decryption.web_authn_prf_options.as_ref() {
183 Some(options) if !options.is_empty() => {
184 state_bridge
185 .set_webauthn_prf_unlock_data(&WebAuthnPrfUnlockData {
186 options: options.clone(),
187 })
188 .await
189 }
190 _ => state_bridge.clear_webauthn_prf_unlock_data().await,
191 }
192}
193
194async fn handle_account_cryptographic_state(client: &Client, data: &CryptoSyncData) {
196 let Some(account_cryptographic_state) = data.account_cryptographic_state.as_ref() else {
197 return;
198 };
199
200 let state_bridge = client.km_state_bridge();
202 if !state_bridge.is_bridge_registered() {
203 return;
204 }
205
206 state_bridge
207 .set_account_cryptographic_state(account_cryptographic_state)
208 .await;
209}
210
211#[derive(Clone)]
213#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
214#[cfg_attr(feature = "wasm", wasm_bindgen)]
215pub struct CryptoSyncHandlerClient {
216 client: Client,
217}
218
219impl CryptoSyncHandlerClient {
220 fn new(client: Client) -> Self {
221 Self { client }
222 }
223}
224
225#[cfg_attr(feature = "wasm", wasm_bindgen)]
226#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
227impl CryptoSyncHandlerClient {
228 pub async fn on_sync(&self, data: CryptoSyncData) {
231 handle_crypto_sync(&self.client, &data).await
232 }
233}
234
235pub trait CryptoSyncHandlerClientExt {
237 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient;
239}
240
241impl CryptoSyncHandlerClientExt for Client {
242 fn crypto_sync_handler(&self) -> CryptoSyncHandlerClient {
243 CryptoSyncHandlerClient::new(self.clone())
244 }
245}
246
247#[cfg(not(target_arch = "wasm32"))]
258pub struct CryptoSyncHandler {
259 client: Client,
260}
261
262#[cfg(not(target_arch = "wasm32"))]
263impl CryptoSyncHandler {
264 pub fn new(client: Client) -> Self {
266 Self { client }
267 }
268}
269
270#[cfg(not(target_arch = "wasm32"))]
271#[async_trait::async_trait]
272impl bitwarden_sync::SyncHandler for CryptoSyncHandler {
273 async fn on_sync(
274 &self,
275 response: &bitwarden_api_api::models::SyncResponseModel,
276 ) -> Result<(), bitwarden_sync::SyncHandlerError> {
277 let data = CryptoSyncData::try_from(response)?;
280
281 handle_crypto_sync(&self.client, &data).await;
282 Ok(())
283 }
284}
285
286#[cfg(all(test, not(target_arch = "wasm32")))]
287mod tests {
288 use bitwarden_api_api::models::{
289 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
290 SyncResponseModel, UserDecryptionResponseModel, WebAuthnPrfDecryptionOption,
291 };
292
293 use super::*;
294
295 const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
296 const TEST_SALT: &str = "[email protected]";
297
298 fn master_password_unlock(
299 master_key_encrypted_user_key: Option<String>,
300 ) -> MasterPasswordUnlockResponseModel {
301 MasterPasswordUnlockResponseModel {
302 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
303 kdf_type: KdfType::PBKDF2_SHA256,
304 iterations: 600_000,
305 memory: None,
306 parallelism: None,
307 }),
308 master_key_encrypted_user_key,
309 salt: Some(TEST_SALT.to_string()),
310 }
311 }
312
313 fn sync_response(user_decryption: UserDecryptionResponseModel) -> SyncResponseModel {
314 SyncResponseModel {
315 user_decryption: Some(Box::new(user_decryption)),
316 ..Default::default()
317 }
318 }
319
320 #[test]
321 fn test_try_from_empty_response_is_empty_data() {
322 let data = CryptoSyncData::try_from(&SyncResponseModel::default()).unwrap();
323
324 assert!(data.user_decryption.is_none());
325 assert!(data.account_cryptographic_state.is_none());
326 }
327
328 #[test]
329 fn test_try_from_valid_master_password_unlock_succeeds() {
330 let response = sync_response(UserDecryptionResponseModel {
331 master_password_unlock: Some(Box::new(master_password_unlock(Some(
332 TEST_USER_KEY.to_string(),
333 )))),
334 ..Default::default()
335 });
336
337 let data = CryptoSyncData::try_from(&response).unwrap();
338
339 let user_decryption = data.user_decryption.unwrap();
340 assert_eq!(
341 user_decryption.master_password_unlock.unwrap().salt,
342 TEST_SALT
343 );
344 }
345
346 #[test]
347 fn test_try_from_malformed_master_password_unlock_errors() {
348 let response = sync_response(UserDecryptionResponseModel {
350 master_password_unlock: Some(Box::new(master_password_unlock(None))),
351 ..Default::default()
352 });
353
354 assert!(matches!(
355 CryptoSyncData::try_from(&response),
356 Err(CryptoSyncDataParseError::MasterPasswordUnlock(_))
357 ));
358 }
359
360 #[test]
361 fn test_try_from_malformed_webauthn_prf_option_errors() {
362 let response = sync_response(UserDecryptionResponseModel {
363 web_authn_prf_options: Some(vec![WebAuthnPrfDecryptionOption::default()]),
364 ..Default::default()
365 });
366
367 assert!(matches!(
368 CryptoSyncData::try_from(&response),
369 Err(CryptoSyncDataParseError::WebAuthnPrfOption(_))
370 ));
371 }
372}