Skip to main content

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
26// TODO: PM-28143: tuple is a work around for the time being until PM-28143 removes the fallback
27impl TryFrom<(PasswordPreloginResponseModel, &str)> for PasswordPreloginResponse {
28    type Error = MissingFieldError;
29
30    fn try_from(
31        (response, email): (PasswordPreloginResponseModel, &str),
32    ) -> Result<Self, Self::Error> {
33        let kdf_settings = require!(response.kdf_settings);
34
35        let kdf = match kdf_settings.kdf_type {
36            KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
37                iterations: NonZeroU32::new(kdf_settings.iterations as u32)
38                    .expect("Non-zero number"),
39            },
40            KdfType::Argon2id => Kdf::Argon2id {
41                iterations: NonZeroU32::new(kdf_settings.iterations as u32)
42                    .expect("Non-zero number"),
43                memory: NonZeroU32::new(require!(kdf_settings.memory) as u32)
44                    .expect("Non-zero number"),
45                parallelism: NonZeroU32::new(require!(kdf_settings.parallelism) as u32)
46                    .expect("Non-zero number"),
47            },
48            KdfType::__Unknown(_) => {
49                return Err(MissingFieldError("response.kdf_settings.kdf_type"));
50            }
51        };
52
53        let salt: String = response.salt.unwrap_or_else(|| email.trim().to_lowercase());
54
55        Ok(PasswordPreloginResponse { kdf, salt })
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use bitwarden_api_identity::models::KdfSettings;
62
63    use super::*;
64
65    const TEST_SALT: &str = "test-salt";
66    const TEST_EMAIL: &str = "[email protected]";
67
68    #[test]
69    fn test_try_from_pbkdf2_with_iterations() {
70        let kdf_settings = KdfSettings {
71            kdf_type: KdfType::PBKDF2_SHA256,
72            iterations: 100000,
73            memory: None,
74            parallelism: None,
75        };
76
77        let response = PasswordPreloginResponseModel {
78            kdf: None,
79            kdf_iterations: None,
80            kdf_memory: None,
81            kdf_parallelism: None,
82            kdf_settings: Some(Box::new(kdf_settings)),
83            salt: Some(TEST_SALT.to_string()),
84        };
85
86        let result = PasswordPreloginResponse::try_from((response, TEST_EMAIL)).unwrap();
87
88        assert_eq!(
89            result.kdf,
90            Kdf::PBKDF2 {
91                iterations: NonZeroU32::new(100000).unwrap()
92            }
93        );
94        assert_eq!(result.salt, TEST_SALT);
95    }
96
97    #[test]
98    fn test_try_from_argon2id_with_all_params() {
99        let kdf_settings = KdfSettings {
100            kdf_type: KdfType::Argon2id,
101            iterations: 4,
102            memory: Some(64),
103            parallelism: Some(4),
104        };
105
106        let response = PasswordPreloginResponseModel {
107            kdf: None,
108            kdf_iterations: None,
109            kdf_memory: None,
110            kdf_parallelism: None,
111            kdf_settings: Some(Box::new(kdf_settings)),
112            salt: Some(TEST_SALT.to_string()),
113        };
114
115        let result = PasswordPreloginResponse::try_from((response, TEST_EMAIL)).unwrap();
116
117        assert_eq!(
118            result.kdf,
119            Kdf::Argon2id {
120                iterations: NonZeroU32::new(4).unwrap(),
121                memory: NonZeroU32::new(64).unwrap(),
122                parallelism: NonZeroU32::new(4).unwrap(),
123            }
124        );
125        assert_eq!(result.salt, TEST_SALT);
126    }
127
128    #[test]
129    fn test_try_from_missing_kdf_settings() {
130        let response = PasswordPreloginResponseModel {
131            kdf: None,
132            kdf_iterations: None,
133            kdf_memory: None,
134            kdf_parallelism: None,
135            kdf_settings: None, // Missing kdf_settings
136            salt: Some(TEST_SALT.to_string()),
137        };
138
139        let result = PasswordPreloginResponse::try_from((response, TEST_EMAIL));
140
141        assert!(result.is_err());
142        assert!(matches!(result.unwrap_err(), MissingFieldError { .. }));
143    }
144
145    #[test]
146    fn test_try_from_missing_salt_falls_back_to_email() {
147        let kdf_settings = KdfSettings {
148            kdf_type: KdfType::PBKDF2_SHA256,
149            iterations: 100000,
150            memory: None,
151            parallelism: None,
152        };
153
154        let response = PasswordPreloginResponseModel {
155            kdf: None,
156            kdf_iterations: None,
157            kdf_memory: None,
158            kdf_parallelism: None,
159            kdf_settings: Some(Box::new(kdf_settings)),
160            salt: None, // Missing salt
161        };
162
163        let result = PasswordPreloginResponse::try_from((response, TEST_EMAIL)).unwrap();
164
165        // When the salt is null, we fall back to the normalized email.
166        assert_eq!(result.salt, TEST_EMAIL);
167    }
168
169    #[test]
170    fn test_try_from_missing_salt_normalizes_email() {
171        let kdf_settings = KdfSettings {
172            kdf_type: KdfType::PBKDF2_SHA256,
173            iterations: 100000,
174            memory: None,
175            parallelism: None,
176        };
177
178        let response = PasswordPreloginResponseModel {
179            kdf: None,
180            kdf_iterations: None,
181            kdf_memory: None,
182            kdf_parallelism: None,
183            kdf_settings: Some(Box::new(kdf_settings)),
184            salt: None, // Missing salt
185        };
186
187        // The email fallback is trimmed and lowercased before being used as the salt.
188        let result = PasswordPreloginResponse::try_from((response, "  [email protected]  ")).unwrap();
189
190        assert_eq!(result.salt, TEST_EMAIL);
191    }
192}