bitwarden_auth/login/models/
user_decryption_options_response.rs

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/// SDK domain model for user decryption options.
13/// Provides the various methods available to unlock a user's vault.
14#[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    /// Master password unlock option. None if user doesn't have a master password.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub master_password_unlock: Option<MasterPasswordUnlockData>,
26
27    /// Trusted Device decryption option.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub trusted_device_option: Option<TrustedDeviceUserDecryptionOption>,
30
31    /// Key Connector decryption option.
32    /// Mutually exclusive with Trusted Device option.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub key_connector_option: Option<KeyConnectorUserDecryptionOption>,
35
36    /// WebAuthn PRF decryption option.
37    #[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        // Test data constants
109        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            // Note: the trusted device option && the key connector option are mutually exclusive
129            // from the server, but this test is just verifying that the conversion logic works
130            // for all option types.
131            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        // Verify master password unlock
153        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        // Verify trusted device option
164        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        // Verify key connector option
180        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        // Verify webauthn prf option
185        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}