bitwarden_auth/login/models/
user_decryption_options_response.rs1use 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 }),
85 trusted_device_option: None,
86 key_connector_option: None,
87 webauthn_prf_option: None,
88 };
89
90 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
91
92 assert!(domain.master_password_unlock.is_some());
93 let mp_unlock = domain.master_password_unlock.unwrap();
94 assert_eq!(mp_unlock.salt, "[email protected]");
95 match mp_unlock.kdf {
96 Kdf::PBKDF2 { iterations } => {
97 assert_eq!(iterations.get(), 600000);
98 }
99 _ => panic!("Expected PBKDF2 KDF"),
100 }
101 assert!(domain.trusted_device_option.is_none());
102 assert!(domain.key_connector_option.is_none());
103 assert!(domain.webauthn_prf_option.is_none());
104 }
105
106 #[test]
107 fn test_user_decryption_options_conversion_with_all_options() {
108 const SALT: &str = "[email protected]";
110 const KDF_ITERATIONS: u32 = 600000;
111 const TDE_ENCRYPTED_PRIVATE_KEY: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
112 const TDE_ENCRYPTED_USER_KEY: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
113 const KEY_CONNECTOR_URL: &str = "https://key-connector.bitwarden.com";
114 const WEBAUTHN_ENCRYPTED_PRIVATE_KEY: &str = "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=";
115 const WEBAUTHN_ENCRYPTED_USER_KEY: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
116
117 let api = UserDecryptionOptionsApiResponse {
118 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
119 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
120 kdf_type: KdfType::PBKDF2_SHA256,
121 iterations: KDF_ITERATIONS as i32,
122 memory: None,
123 parallelism: None,
124 }),
125 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
126 salt: Some(SALT.to_string()),
127 }),
128 trusted_device_option: Some(TrustedDeviceUserDecryptionOptionApiResponse {
132 has_admin_approval: true,
133 has_login_approving_device: false,
134 has_manage_reset_password_permission: false,
135 is_tde_offboarding: false,
136 encrypted_private_key: Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap()),
137 encrypted_user_key: Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap()),
138 }),
139 key_connector_option: Some(KeyConnectorUserDecryptionOptionApiResponse {
140 key_connector_url: KEY_CONNECTOR_URL.to_string(),
141 }),
142 webauthn_prf_option: Some(WebAuthnPrfUserDecryptionOptionApiResponse {
143 encrypted_private_key: WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap(),
144 encrypted_user_key: WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap(),
145 credential_id: None,
146 transports: None,
147 }),
148 };
149
150 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
151
152 assert!(domain.master_password_unlock.is_some());
154 let mp_unlock = domain.master_password_unlock.unwrap();
155 assert_eq!(mp_unlock.salt, SALT);
156 match mp_unlock.kdf {
157 Kdf::PBKDF2 { iterations } => {
158 assert_eq!(iterations.get(), KDF_ITERATIONS);
159 }
160 _ => panic!("Expected PBKDF2 KDF"),
161 }
162
163 assert!(domain.trusted_device_option.is_some());
165 let tde = domain.trusted_device_option.unwrap();
166 assert!(tde.has_admin_approval);
167 assert!(!tde.has_login_approving_device);
168 assert!(!tde.has_manage_reset_password_permission);
169 assert!(!tde.is_tde_offboarding);
170 assert_eq!(
171 tde.encrypted_private_key,
172 Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap())
173 );
174 assert_eq!(
175 tde.encrypted_user_key,
176 Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap())
177 );
178
179 assert!(domain.key_connector_option.is_some());
181 let kc = domain.key_connector_option.unwrap();
182 assert_eq!(kc.key_connector_url, KEY_CONNECTOR_URL);
183
184 assert!(domain.webauthn_prf_option.is_some());
186 let webauthn = domain.webauthn_prf_option.unwrap();
187 assert_eq!(
188 webauthn.encrypted_private_key,
189 WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap()
190 );
191 assert_eq!(
192 webauthn.encrypted_user_key,
193 WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap()
194 );
195 assert_eq!(webauthn.credential_id, None);
196 assert_eq!(webauthn.transports, None);
197 }
198
199 #[test]
200 fn test_user_decryption_options_with_trusted_device_only() {
201 const TDE_ENCRYPTED_PRIVATE_KEY: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
202 const TDE_ENCRYPTED_USER_KEY: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
203
204 let api = UserDecryptionOptionsApiResponse {
205 master_password_unlock: None,
206 trusted_device_option: Some(TrustedDeviceUserDecryptionOptionApiResponse {
207 has_admin_approval: false,
208 has_login_approving_device: true,
209 has_manage_reset_password_permission: false,
210 is_tde_offboarding: false,
211 encrypted_private_key: Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap()),
212 encrypted_user_key: Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap()),
213 }),
214 key_connector_option: None,
215 webauthn_prf_option: None,
216 };
217
218 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
219
220 assert!(domain.master_password_unlock.is_none());
221 assert!(domain.trusted_device_option.is_some());
222 assert!(domain.key_connector_option.is_none());
223 assert!(domain.webauthn_prf_option.is_none());
224
225 let tde = domain.trusted_device_option.unwrap();
226 assert!(!tde.has_admin_approval);
227 assert!(tde.has_login_approving_device);
228 assert_eq!(
229 tde.encrypted_private_key,
230 Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap())
231 );
232 assert_eq!(
233 tde.encrypted_user_key,
234 Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap())
235 );
236 }
237
238 #[test]
239 fn test_user_decryption_options_with_key_connector_only() {
240 const KEY_CONNECTOR_URL: &str = "https://key-connector.example.com";
241
242 let api = UserDecryptionOptionsApiResponse {
243 master_password_unlock: None,
244 trusted_device_option: None,
245 key_connector_option: Some(KeyConnectorUserDecryptionOptionApiResponse {
246 key_connector_url: KEY_CONNECTOR_URL.to_string(),
247 }),
248 webauthn_prf_option: None,
249 };
250
251 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
252
253 assert!(domain.master_password_unlock.is_none());
254 assert!(domain.trusted_device_option.is_none());
255 assert!(domain.key_connector_option.is_some());
256 assert!(domain.webauthn_prf_option.is_none());
257
258 let kc = domain.key_connector_option.unwrap();
259 assert_eq!(kc.key_connector_url, KEY_CONNECTOR_URL);
260 }
261
262 #[test]
263 fn test_user_decryption_options_with_webauthn_prf_only() {
264 const WEBAUTHN_ENCRYPTED_PRIVATE_KEY: &str = "2.fkvl0+sL1lwtiOn1eewsvQ==|dT0TynLl8YERZ8x7dxC+DQ==|cWhiRSYHOi/AA2LiV/JBJWbO9C7pbUpOM6TMAcV47hE=";
265 const WEBAUTHN_ENCRYPTED_USER_KEY: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
266
267 let api = UserDecryptionOptionsApiResponse {
268 master_password_unlock: None,
269 trusted_device_option: None,
270 key_connector_option: None,
271 webauthn_prf_option: Some(WebAuthnPrfUserDecryptionOptionApiResponse {
272 encrypted_private_key: WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap(),
273 encrypted_user_key: WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap(),
274 credential_id: None,
275 transports: None,
276 }),
277 };
278
279 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
280
281 assert!(domain.master_password_unlock.is_none());
282 assert!(domain.trusted_device_option.is_none());
283 assert!(domain.key_connector_option.is_none());
284 assert!(domain.webauthn_prf_option.is_some());
285
286 let webauthn = domain.webauthn_prf_option.unwrap();
287 assert_eq!(
288 webauthn.encrypted_private_key,
289 WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap()
290 );
291 assert_eq!(
292 webauthn.encrypted_user_key,
293 WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap()
294 );
295 assert_eq!(webauthn.credential_id, None);
296 assert_eq!(webauthn.transports, None);
297 }
298
299 #[test]
300 fn test_user_decryption_options_with_no_options() {
301 let api = UserDecryptionOptionsApiResponse {
302 master_password_unlock: None,
303 trusted_device_option: None,
304 key_connector_option: None,
305 webauthn_prf_option: None,
306 };
307
308 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
309
310 assert!(domain.master_password_unlock.is_none());
311 assert!(domain.trusted_device_option.is_none());
312 assert!(domain.key_connector_option.is_none());
313 assert!(domain.webauthn_prf_option.is_none());
314 }
315
316 #[test]
317 fn test_user_decryption_options_with_master_password_and_trusted_device() {
318 const TDE_ENCRYPTED_PRIVATE_KEY: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
319 const TDE_ENCRYPTED_USER_KEY: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
320
321 let api = UserDecryptionOptionsApiResponse {
322 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
323 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
324 kdf_type: KdfType::PBKDF2_SHA256,
325 iterations: 600000,
326 memory: None,
327 parallelism: None,
328 }),
329 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
330 salt: Some("[email protected]".to_string()),
331 }),
332 trusted_device_option: Some(TrustedDeviceUserDecryptionOptionApiResponse {
333 has_admin_approval: true,
334 has_login_approving_device: false,
335 has_manage_reset_password_permission: true,
336 is_tde_offboarding: false,
337 encrypted_private_key: Some(TDE_ENCRYPTED_PRIVATE_KEY.parse().unwrap()),
338 encrypted_user_key: Some(TDE_ENCRYPTED_USER_KEY.parse().unwrap()),
339 }),
340 key_connector_option: None,
341 webauthn_prf_option: None,
342 };
343
344 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
345
346 assert!(domain.master_password_unlock.is_some());
347 assert!(domain.trusted_device_option.is_some());
348 assert!(domain.key_connector_option.is_none());
349 assert!(domain.webauthn_prf_option.is_none());
350 }
351
352 #[test]
353 fn test_user_decryption_options_with_master_password_and_key_connector() {
354 const KEY_CONNECTOR_URL: &str = "https://key-connector.example.com";
355
356 let api = UserDecryptionOptionsApiResponse {
357 master_password_unlock: Some(MasterPasswordUnlockResponseModel {
358 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
359 kdf_type: KdfType::PBKDF2_SHA256,
360 iterations: 600000,
361 memory: None,
362 parallelism: None,
363 }),
364 master_key_encrypted_user_key: Some(MASTER_KEY_ENCRYPTED_USER_KEY.to_string()),
365 salt: Some("[email protected]".to_string()),
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 }),
398 trusted_device_option: None,
399 key_connector_option: None,
400 webauthn_prf_option: Some(WebAuthnPrfUserDecryptionOptionApiResponse {
401 encrypted_private_key: WEBAUTHN_ENCRYPTED_PRIVATE_KEY.parse().unwrap(),
402 encrypted_user_key: WEBAUTHN_ENCRYPTED_USER_KEY.parse().unwrap(),
403 credential_id: None,
404 transports: None,
405 }),
406 };
407
408 let domain: UserDecryptionOptionsResponse = api.try_into().unwrap();
409
410 assert!(domain.master_password_unlock.is_some());
411 assert!(domain.trusted_device_option.is_none());
412 assert!(domain.key_connector_option.is_none());
413 assert!(domain.webauthn_prf_option.is_some());
414 }
415}