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))] #[bitwarden_ffi::wasm_record] pub struct PasswordPreloginResponse {
14 pub kdf: Kdf,
16
17 pub salt: String,
20}
21
22impl 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, 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, };
158
159 let result = PasswordPreloginResponse::try_from((response, TEST_EMAIL)).unwrap();
160
161 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, };
182
183 let result = PasswordPreloginResponse::try_from((response, " [email protected] ")).unwrap();
185
186 assert_eq!(result.salt, TEST_EMAIL);
187 }
188}