bitwarden_auth/login/login_via_password/
password_prelogin_response.rs

1use std::num::NonZeroU32;
2
3use bitwarden_api_identity::models::{KdfType, PasswordPreloginResponseModel};
4use bitwarden_core::{MissingFieldError, require};
5use bitwarden_crypto::Kdf;
6use serde::{Deserialize, Serialize};
7
8/// Response containing the data required before password-based authentication
9#[derive(Serialize, Deserialize, Debug)]
10#[serde(rename_all = "camelCase")]
11#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] // add mobile support
12#[cfg_attr(
13    feature = "wasm",
14    derive(tsify::Tsify),
15    tsify(into_wasm_abi, from_wasm_abi)
16)] // add wasm support
17pub struct PasswordPreloginResponse {
18    /// The Key Derivation Function (KDF) configuration for the user
19    pub kdf: Kdf,
20
21    /// The salt used in the KDF process
22    // TODO: PM-30183 - make this a type for safety
23    pub salt: String,
24}
25
26impl TryFrom<PasswordPreloginResponseModel> for PasswordPreloginResponse {
27    type Error = MissingFieldError;
28
29    fn try_from(response: PasswordPreloginResponseModel) -> Result<Self, Self::Error> {
30        let kdf_settings = require!(response.kdf_settings);
31
32        let kdf = match kdf_settings.kdf_type {
33            KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
34                iterations: NonZeroU32::new(kdf_settings.iterations as u32)
35                    .expect("Non-zero number"),
36            },
37            KdfType::Argon2id => Kdf::Argon2id {
38                iterations: NonZeroU32::new(kdf_settings.iterations as u32)
39                    .expect("Non-zero number"),
40                memory: NonZeroU32::new(require!(kdf_settings.memory) as u32)
41                    .expect("Non-zero number"),
42                parallelism: NonZeroU32::new(require!(kdf_settings.parallelism) as u32)
43                    .expect("Non-zero number"),
44            },
45            KdfType::__Unknown(_) => {
46                return Err(MissingFieldError("response.kdf_settings.kdf_type"));
47            }
48        };
49
50        Ok(PasswordPreloginResponse {
51            kdf,
52            salt: require!(response.salt),
53        })
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use bitwarden_api_identity::models::KdfSettings;
60
61    use super::*;
62
63    const TEST_SALT: &str = "test-salt";
64
65    #[test]
66    fn test_try_from_pbkdf2_with_iterations() {
67        let kdf_settings = KdfSettings {
68            kdf_type: KdfType::PBKDF2_SHA256,
69            iterations: 100000,
70            memory: None,
71            parallelism: None,
72        };
73
74        let response = PasswordPreloginResponseModel {
75            kdf: None,
76            kdf_iterations: None,
77            kdf_memory: None,
78            kdf_parallelism: None,
79            kdf_settings: Some(Box::new(kdf_settings)),
80            salt: Some(TEST_SALT.to_string()),
81        };
82
83        let result = PasswordPreloginResponse::try_from(response).unwrap();
84
85        assert_eq!(
86            result.kdf,
87            Kdf::PBKDF2 {
88                iterations: NonZeroU32::new(100000).unwrap()
89            }
90        );
91        assert_eq!(result.salt, TEST_SALT);
92    }
93
94    #[test]
95    fn test_try_from_argon2id_with_all_params() {
96        let kdf_settings = KdfSettings {
97            kdf_type: KdfType::Argon2id,
98            iterations: 4,
99            memory: Some(64),
100            parallelism: Some(4),
101        };
102
103        let response = PasswordPreloginResponseModel {
104            kdf: None,
105            kdf_iterations: None,
106            kdf_memory: None,
107            kdf_parallelism: None,
108            kdf_settings: Some(Box::new(kdf_settings)),
109            salt: Some(TEST_SALT.to_string()),
110        };
111
112        let result = PasswordPreloginResponse::try_from(response).unwrap();
113
114        assert_eq!(
115            result.kdf,
116            Kdf::Argon2id {
117                iterations: NonZeroU32::new(4).unwrap(),
118                memory: NonZeroU32::new(64).unwrap(),
119                parallelism: NonZeroU32::new(4).unwrap(),
120            }
121        );
122        assert_eq!(result.salt, TEST_SALT);
123    }
124
125    #[test]
126    fn test_try_from_missing_kdf_settings() {
127        let response = PasswordPreloginResponseModel {
128            kdf: None,
129            kdf_iterations: None,
130            kdf_memory: None,
131            kdf_parallelism: None,
132            kdf_settings: None, // Missing kdf_settings
133            salt: Some(TEST_SALT.to_string()),
134        };
135
136        let result = PasswordPreloginResponse::try_from(response);
137
138        assert!(result.is_err());
139        assert!(matches!(result.unwrap_err(), MissingFieldError { .. }));
140    }
141
142    #[test]
143    fn test_try_from_missing_salt() {
144        let kdf_settings = KdfSettings {
145            kdf_type: KdfType::PBKDF2_SHA256,
146            iterations: 100000,
147            memory: None,
148            parallelism: None,
149        };
150
151        let response = PasswordPreloginResponseModel {
152            kdf: None,
153            kdf_iterations: None,
154            kdf_memory: None,
155            kdf_parallelism: None,
156            kdf_settings: Some(Box::new(kdf_settings)),
157            salt: None, // Missing salt
158        };
159
160        let result = PasswordPreloginResponse::try_from(response);
161
162        assert!(result.is_err());
163        assert!(matches!(result.unwrap_err(), MissingFieldError { .. }));
164    }
165}