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