bitwarden_auth/login/login_via_password/
password_prelogin_response.rs1use 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#[derive(Serialize, Deserialize, Debug)]
10#[serde(rename_all = "camelCase")]
11#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(
13 feature = "wasm",
14 derive(tsify::Tsify),
15 tsify(into_wasm_abi, from_wasm_abi)
16)] pub struct PasswordPreloginResponse {
18 pub kdf: Kdf,
20
21 pub salt: String,
24}
25
26impl 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, 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, };
162
163 let result = PasswordPreloginResponse::try_from((response, TEST_EMAIL)).unwrap();
164
165 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, };
186
187 let result = PasswordPreloginResponse::try_from((response, " [email protected] ")).unwrap();
189
190 assert_eq!(result.salt, TEST_EMAIL);
191 }
192}