1use bitwarden_core::key_management::{MasterPasswordError, MasterPasswordUnlockData};
2use serde::{Deserialize, Serialize};
3
4use crate::login::{
5 api::response::UserDecryptionOptionsApiResponse,
6 models::{
7 KeyConnectorUserDecryptionOption, TrustedDeviceUserDecryptionOption,
8 WebAuthnPrfUserDecryptionOption,
9 },
10};
11
12#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
15#[serde(rename_all = "camelCase")]
16#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
17#[cfg_attr(
18 feature = "wasm",
19 derive(tsify::Tsify),
20 tsify(into_wasm_abi, from_wasm_abi)
21)]
22pub struct UserDecryptionOptionsResponse {
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub master_password_unlock: Option<MasterPasswordUnlockData>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub trusted_device_option: Option<TrustedDeviceUserDecryptionOption>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
34 pub key_connector_option: Option<KeyConnectorUserDecryptionOption>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub webauthn_prf_option: Option<WebAuthnPrfUserDecryptionOption>,
39}
40
41impl TryFrom<UserDecryptionOptionsApiResponse> for UserDecryptionOptionsResponse {
42 type Error = MasterPasswordError;
43
44 fn try_from(api: UserDecryptionOptionsApiResponse) -> Result<Self, Self::Error> {
45 Ok(Self {
46 master_password_unlock: match api.master_password_unlock {
47 Some(ref mp) => Some(MasterPasswordUnlockData::try_from(mp)?),
48 None => None,
49 },
50 trusted_device_option: api.trusted_device_option.map(|tde| tde.into()),
51 key_connector_option: api.key_connector_option.map(|kc| kc.into()),
52 webauthn_prf_option: api.webauthn_prf_option.map(|wa| wa.into()),
53 })
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use bitwarden_api_api::models::{
60 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
61 };
62 use bitwarden_crypto::Kdf;
63
64 use super::*;
65 use crate::login::api::response::{
66 KeyConnectorUserDecryptionOptionApiResponse, TrustedDeviceUserDecryptionOptionApiResponse,
67 WebAuthnPrfUserDecryptionOptionApiResponse,
68 };
69
70 const MASTER_KEY_ENCRYPTED_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
71
72 #[test]
73 fn test_user_decryption_options_conversion_with_master_password() {
74 let api = UserDecryptionOptionsApiResponse {
75 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
76 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
77 kdf_type: KdfType::PBKDF2_SHA256,
78 iterations: 600000,
79 memory: None,
80 parallelism: None,
81 }),
82 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
83 salt: Some("[email protected]".to_string()),
84 contained_key_id: None,
85 }),
86 trusted_device_option: None,
87 key_connector_option: None,
88 webauthn_prf_option: None,
89 };
90
91 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
92
93 assert!(domain.master_password_unlock.is_some());
94 let mp_unlock = domain.master_password_unlock.unwrap();
95 assert_eq!(mp_unlock.salt, "[email protected]");
96 match mp_unlock.kdf {
97 Kdf::PBKDF2 { iterations } => {
98 assert_eq!(iterations.get(), 600000);
99 }
100 _ => panic!("Expected PBKDF2 KDF"),
101 }
102 assert!(domain.trusted_device_option.is_none());
103 assert!(domain.key_connector_option.is_none());
104 assert!(domain.webauthn_prf_option.is_none());
105 }
106
107 #[test]
108 fn test_user_decryption_options_conversion_with_all_options() {
109 const SALT: &str = "[email protected]";
111 const KDF_ITERATIONS: u32 = 600000;
112 const TDE_ENCRYPTED_PRIVATE_KEY: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
113 const TDE_ENCRYPTED_USER_KEY: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
114 const KEY_CONNECTOR_URL: &str = "https://key-connector.bitwarden.com";
115 const WEBAUTHN_ENCRYPTED_PRIVATE_KEY: &str = "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=";
116 const WEBAUTHN_ENCRYPTED_USER_KEY: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
117
118 let api = UserDecryptionOptionsApiResponse {
119 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
120 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
121 kdf_type: KdfType::PBKDF2_SHA256,
122 iterations: KDF_ITERATIONS as i32,
123 memory: None,
124 parallelism: None,
125 }),
126 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
127 salt: Some(SALT.to_string()),
128 contained_key_id: None,
129 }),
130 trusted_device_option: Some(TrustedDeviceUserDecryptionOptionApiResponse {
134 has_admin_approval: true,
135 has_login_approving_device: false,
136 has_manage_reset_password_permission: false,
137 is_tde_offboarding: false,
138 encrypted_private_key: Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap()),
139 encrypted_user_key: Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap()),
140 }),
141 key_connector_option: Some(KeyConnectorUserDecryptionOptionApiResponse {
142 key_connector_url: KEY_CONNECTOR_URL.to_string(),
143 }),
144 webauthn_prf_option: Some(WebAuthnPrfUserDecryptionOptionApiResponse {
145 encrypted_private_key: WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap(),
146 encrypted_user_key: WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap(),
147 credential_id: None,
148 transports: None,
149 }),
150 };
151
152 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
153
154 assert!(domain.master_password_unlock.is_some());
156 let mp_unlock = domain.master_password_unlock.unwrap();
157 assert_eq!(mp_unlock.salt, SALT);
158 match mp_unlock.kdf {
159 Kdf::PBKDF2 { iterations } => {
160 assert_eq!(iterations.get(), KDF_ITERATIONS);
161 }
162 _ => panic!("Expected PBKDF2 KDF"),
163 }
164
165 assert!(domain.trusted_device_option.is_some());
167 let tde = domain.trusted_device_option.unwrap();
168 assert!(tde.has_admin_approval);
169 assert!(!tde.has_login_approving_device);
170 assert!(!tde.has_manage_reset_password_permission);
171 assert!(!tde.is_tde_offboarding);
172 assert_eq!(
173 tde.encrypted_private_key,
174 Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap())
175 );
176 assert_eq!(
177 tde.encrypted_user_key,
178 Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap())
179 );
180
181 assert!(domain.key_connector_option.is_some());
183 let kc = domain.key_connector_option.unwrap();
184 assert_eq!(kc.key_connector_url, KEY_CONNECTOR_URL);
185
186 assert!(domain.webauthn_prf_option.is_some());
188 let webauthn = domain.webauthn_prf_option.unwrap();
189 assert_eq!(
190 webauthn.encrypted_private_key,
191 WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap()
192 );
193 assert_eq!(
194 webauthn.encrypted_user_key,
195 WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap()
196 );
197 assert_eq!(webauthn.credential_id, None);
198 assert_eq!(webauthn.transports, None);
199 }
200
201 #[test]
202 fn test_user_decryption_options_with_trusted_device_only() {
203 const TDE_ENCRYPTED_PRIVATE_KEY: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
204 const TDE_ENCRYPTED_USER_KEY: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
205
206 let api = UserDecryptionOptionsApiResponse {
207 master_password_unlock: None,
208 trusted_device_option: Some(TrustedDeviceUserDecryptionOptionApiResponse {
209 has_admin_approval: false,
210 has_login_approving_device: true,
211 has_manage_reset_password_permission: false,
212 is_tde_offboarding: false,
213 encrypted_private_key: Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap()),
214 encrypted_user_key: Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap()),
215 }),
216 key_connector_option: None,
217 webauthn_prf_option: None,
218 };
219
220 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
221
222 assert!(domain.master_password_unlock.is_none());
223 assert!(domain.trusted_device_option.is_some());
224 assert!(domain.key_connector_option.is_none());
225 assert!(domain.webauthn_prf_option.is_none());
226
227 let tde = domain.trusted_device_option.unwrap();
228 assert!(!tde.has_admin_approval);
229 assert!(tde.has_login_approving_device);
230 assert_eq!(
231 tde.encrypted_private_key,
232 Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap())
233 );
234 assert_eq!(
235 tde.encrypted_user_key,
236 Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap())
237 );
238 }
239
240 #[test]
241 fn test_user_decryption_options_with_key_connector_only() {
242 const KEY_CONNECTOR_URL: &str = "https://key-connector.example.com";
243
244 let api = UserDecryptionOptionsApiResponse {
245 master_password_unlock: None,
246 trusted_device_option: None,
247 key_connector_option: Some(KeyConnectorUserDecryptionOptionApiResponse {
248 key_connector_url: KEY_CONNECTOR_URL.to_string(),
249 }),
250 webauthn_prf_option: None,
251 };
252
253 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
254
255 assert!(domain.master_password_unlock.is_none());
256 assert!(domain.trusted_device_option.is_none());
257 assert!(domain.key_connector_option.is_some());
258 assert!(domain.webauthn_prf_option.is_none());
259
260 let kc = domain.key_connector_option.unwrap();
261 assert_eq!(kc.key_connector_url, KEY_CONNECTOR_URL);
262 }
263
264 #[test]
265 fn test_user_decryption_options_with_webauthn_prf_only() {
266 const WEBAUTHN_ENCRYPTED_PRIVATE_KEY: &str = "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=";
267 const WEBAUTHN_ENCRYPTED_USER_KEY: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
268
269 let api = UserDecryptionOptionsApiResponse {
270 master_password_unlock: None,
271 trusted_device_option: None,
272 key_connector_option: None,
273 webauthn_prf_option: Some(WebAuthnPrfUserDecryptionOptionApiResponse {
274 encrypted_private_key: WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap(),
275 encrypted_user_key: WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap(),
276 credential_id: None,
277 transports: None,
278 }),
279 };
280
281 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
282
283 assert!(domain.master_password_unlock.is_none());
284 assert!(domain.trusted_device_option.is_none());
285 assert!(domain.key_connector_option.is_none());
286 assert!(domain.webauthn_prf_option.is_some());
287
288 let webauthn = domain.webauthn_prf_option.unwrap();
289 assert_eq!(
290 webauthn.encrypted_private_key,
291 WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap()
292 );
293 assert_eq!(
294 webauthn.encrypted_user_key,
295 WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap()
296 );
297 assert_eq!(webauthn.credential_id, None);
298 assert_eq!(webauthn.transports, None);
299 }
300
301 #[test]
302 fn test_user_decryption_options_with_no_options() {
303 let api = UserDecryptionOptionsApiResponse {
304 master_password_unlock: None,
305 trusted_device_option: None,
306 key_connector_option: None,
307 webauthn_prf_option: None,
308 };
309
310 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
311
312 assert!(domain.master_password_unlock.is_none());
313 assert!(domain.trusted_device_option.is_none());
314 assert!(domain.key_connector_option.is_none());
315 assert!(domain.webauthn_prf_option.is_none());
316 }
317
318 #[test]
319 fn test_user_decryption_options_with_master_password_and_trusted_device() {
320 const TDE_ENCRYPTED_PRIVATE_KEY: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
321 const TDE_ENCRYPTED_USER_KEY: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
322
323 let api = UserDecryptionOptionsApiResponse {
324 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
325 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
326 kdf_type: KdfType::PBKDF2_SHA256,
327 iterations: 600000,
328 memory: None,
329 parallelism: None,
330 }),
331 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
332 salt: Some("[email protected]".to_string()),
333 contained_key_id: None,
334 }),
335 trusted_device_option: Some(TrustedDeviceUserDecryptionOptionApiResponse {
336 has_admin_approval: true,
337 has_login_approving_device: false,
338 has_manage_reset_password_permission: true,
339 is_tde_offboarding: false,
340 encrypted_private_key: Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap()),
341 encrypted_user_key: Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap()),
342 }),
343 key_connector_option: None,
344 webauthn_prf_option: None,
345 };
346
347 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
348
349 assert!(domain.master_password_unlock.is_some());
350 assert!(domain.trusted_device_option.is_some());
351 assert!(domain.key_connector_option.is_none());
352 assert!(domain.webauthn_prf_option.is_none());
353 }
354
355 #[test]
356 fn test_user_decryption_options_with_master_password_and_key_connector() {
357 const KEY_CONNECTOR_URL: &str = "https://key-connector.example.com";
358
359 let api = UserDecryptionOptionsApiResponse {
360 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
361 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
362 kdf_type: KdfType::PBKDF2_SHA256,
363 iterations: 600000,
364 memory: None,
365 parallelism: None,
366 }),
367 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
368 salt: Some("[email protected]".to_string()),
369 contained_key_id: None,
370 }),
371 trusted_device_option: None,
372 key_connector_option: Some(KeyConnectorUserDecryptionOptionApiResponse {
373 key_connector_url: KEY_CONNECTOR_URL.to_string(),
374 }),
375 webauthn_prf_option: None,
376 };
377
378 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
379
380 assert!(domain.master_password_unlock.is_some());
381 assert!(domain.trusted_device_option.is_none());
382 assert!(domain.key_connector_option.is_some());
383 assert!(domain.webauthn_prf_option.is_none());
384 }
385
386 #[test]
387 fn test_user_decryption_options_with_master_password_and_webauthn_prf() {
388 const WEBAUTHN_ENCRYPTED_PRIVATE_KEY: &str = "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=";
389 const WEBAUTHN_ENCRYPTED_USER_KEY: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
390
391 let api = UserDecryptionOptionsApiResponse {
392 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
393 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
394 kdf_type: KdfType::PBKDF2_SHA256,
395 iterations: 600000,
396 memory: None,
397 parallelism: None,
398 }),
399 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
400 salt: Some("[email protected]".to_string()),
401 contained_key_id: None,
402 }),
403 trusted_device_option: None,
404 key_connector_option: None,
405 webauthn_prf_option: Some(WebAuthnPrfUserDecryptionOptionApiResponse {
406 encrypted_private_key: WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap(),
407 encrypted_user_key: WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap(),
408 credential_id: None,
409 transports: None,
410 }),
411 };
412
413 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
414
415 assert!(domain.master_password_unlock.is_some());
416 assert!(domain.trusted_device_option.is_none());
417 assert!(domain.key_connector_option.is_none());
418 assert!(domain.webauthn_prf_option.is_some());
419 }
420}