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