1use base64::Engine;
10use base64::engine::general_purpose::STANDARD as BASE64;
11use serde::{Deserialize, Serialize};
12
13use crate::api::HumanVerificationCredential;
14use crate::config::ProtonClientConfiguration;
15use crate::crypto;
16use crate::error::{ProtonError, Result};
17use crate::http::{self, ApiHttpClient, Tokens};
18use crate::ids::{SessionId, UserId};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum PasswordMode {
23 Single,
24 Dual,
25}
26
27impl PasswordMode {
28 fn from_wire(value: i32) -> Self {
30 match value {
31 1 => PasswordMode::Single,
32 _ => PasswordMode::Dual,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct ResumeParameters {
40 pub session_id: SessionId,
41 pub username: String,
42 pub user_id: UserId,
43 pub access_token: String,
44 pub refresh_token: String,
45 pub scopes: Vec<String>,
46 pub is_waiting_for_second_factor_code: bool,
47 pub password_mode: PasswordMode,
48}
49
50#[derive(Clone)]
52pub struct ProtonApiSession {
53 http: ApiHttpClient,
54 session_id: SessionId,
55 username: String,
56 user_id: UserId,
57 scopes: Vec<String>,
58 password_mode: PasswordMode,
59 is_waiting_for_second_factor: bool,
60}
61
62impl ProtonApiSession {
63 pub fn resume(config: ProtonClientConfiguration, params: ResumeParameters) -> Result<Self> {
65 let tokens = Tokens {
66 access_token: params.access_token,
67 refresh_token: params.refresh_token,
68 };
69 let http = ApiHttpClient::new(config, params.session_id.clone(), tokens)?;
70
71 Ok(Self {
72 http,
73 session_id: params.session_id,
74 username: params.username,
75 user_id: params.user_id,
76 scopes: params.scopes,
77 password_mode: params.password_mode,
78 is_waiting_for_second_factor: params.is_waiting_for_second_factor_code,
79 })
80 }
81
82 pub async fn begin(
94 config: ProtonClientConfiguration,
95 username: &str,
96 password: &[u8],
97 ) -> Result<Self> {
98 Self::begin_verified(config, username, password, None).await
99 }
100
101 pub async fn begin_verified(
112 config: ProtonClientConfiguration,
113 username: &str,
114 password: &[u8],
115 verification: Option<&HumanVerificationCredential>,
116 ) -> Result<Self> {
117 let init: SessionInitiationResponse = http::post_unauthenticated_verified(
118 &config,
119 "auth/v4/info",
120 &SessionInitiationRequest { username },
121 verification,
122 )
123 .await?;
124
125 let salt = BASE64
126 .decode(init.salt.trim())
127 .map_err(|e| ProtonError::invalid_operation(format!("decode SRP salt: {e}")))?;
128 let server_ephemeral = BASE64
129 .decode(init.server_ephemeral.trim())
130 .map_err(|e| ProtonError::invalid_operation(format!("decode server ephemeral: {e}")))?;
131
132 let proofs = crypto::generate_proofs(
133 init.version,
134 password,
135 &salt,
136 &init.modulus,
137 &server_ephemeral,
138 crypto::DEFAULT_BIT_LENGTH,
139 )?;
140
141 let auth: AuthenticationResponse = http::post_unauthenticated_verified(
142 &config,
143 "auth/v4",
144 &AuthenticationRequest {
145 username,
146 client_ephemeral: BASE64.encode(&proofs.client_ephemeral),
147 client_proof: BASE64.encode(&proofs.client_proof),
148 srp_session: init.srp_session,
149 },
150 verification,
151 )
152 .await?;
153
154 let server_proof = BASE64
156 .decode(auth.server_proof.trim())
157 .map_err(|e| ProtonError::invalid_operation(format!("decode server proof: {e}")))?;
158 if server_proof != proofs.expected_server_proof {
159 return Err(ProtonError::invalid_operation(
160 "SRP server proof mismatch — server failed authentication",
161 ));
162 }
163
164 let tokens = Tokens {
165 access_token: auth.access_token,
166 refresh_token: auth.refresh_token,
167 };
168 let http = ApiHttpClient::new(config, auth.session_id.clone(), tokens)?;
169
170 Ok(Self {
171 http,
172 session_id: auth.session_id,
173 username: username.to_owned(),
174 user_id: auth.user_id,
175 scopes: auth.scopes,
176 password_mode: PasswordMode::from_wire(auth.password_mode),
177 is_waiting_for_second_factor: auth
178 .second_factor
179 .map(|f| f.is_enabled())
180 .unwrap_or(false),
181 })
182 }
183
184 pub async fn apply_second_factor_code(&mut self, code: &str) -> Result<()> {
192 let response: ScopesResponse = self
193 .http
194 .post("auth/v4/2fa", &SecondFactorValidationRequest { code })
195 .await?;
196 self.is_waiting_for_second_factor = false;
197 self.scopes = response.scopes;
198 Ok(())
199 }
200
201 pub async fn refresh_scopes(&mut self) -> Result<()> {
205 let response: ScopesResponse = self.http.get("auth/v4/scopes").await?;
206 self.scopes = response.scopes;
207 Ok(())
208 }
209
210 pub async fn end(&self) -> Result<()> {
212 let _: crate::api::ApiResponse = self.http.delete("auth/v4").await?;
213 Ok(())
214 }
215
216 pub fn http(&self) -> &ApiHttpClient {
217 &self.http
218 }
219
220 pub fn session_id(&self) -> &SessionId {
221 &self.session_id
222 }
223
224 pub fn user_id(&self) -> &UserId {
225 &self.user_id
226 }
227
228 pub fn username(&self) -> &str {
229 &self.username
230 }
231
232 pub fn scopes(&self) -> &[String] {
233 &self.scopes
234 }
235
236 pub fn password_mode(&self) -> PasswordMode {
237 self.password_mode
238 }
239
240 pub fn is_waiting_for_second_factor(&self) -> bool {
243 self.is_waiting_for_second_factor
244 }
245
246 pub async fn current_tokens(&self) -> Tokens {
248 self.http.current_tokens().await
249 }
250}
251
252#[derive(Serialize)]
253struct SessionInitiationRequest<'a> {
254 #[serde(rename = "Username")]
255 username: &'a str,
256}
257
258#[derive(Deserialize)]
259struct SessionInitiationResponse {
260 #[serde(rename = "Version")]
261 version: i32,
262 #[serde(rename = "Modulus")]
264 modulus: String,
265 #[serde(rename = "ServerEphemeral")]
267 server_ephemeral: String,
268 #[serde(rename = "Salt")]
270 salt: String,
271 #[serde(rename = "SRPSession")]
272 srp_session: String,
273}
274
275#[derive(Serialize)]
276struct AuthenticationRequest<'a> {
277 #[serde(rename = "Username")]
278 username: &'a str,
279 #[serde(rename = "ClientEphemeral")]
281 client_ephemeral: String,
282 #[serde(rename = "ClientProof")]
284 client_proof: String,
285 #[serde(rename = "SRPSession")]
286 srp_session: String,
287}
288
289#[derive(Deserialize)]
290struct AuthenticationResponse {
291 #[serde(rename = "UID")]
292 session_id: SessionId,
293 #[serde(rename = "UserID")]
294 user_id: UserId,
295 #[serde(rename = "ServerProof")]
297 server_proof: String,
298 #[serde(rename = "AccessToken")]
299 access_token: String,
300 #[serde(rename = "RefreshToken")]
301 refresh_token: String,
302 #[serde(rename = "Scopes", default)]
303 scopes: Vec<String>,
304 #[serde(rename = "PasswordMode")]
305 password_mode: i32,
306 #[serde(rename = "2FA")]
307 second_factor: Option<SecondFactorInfo>,
308}
309
310#[derive(Serialize)]
311struct SecondFactorValidationRequest<'a> {
312 #[serde(rename = "TwoFactorCode")]
313 code: &'a str,
314}
315
316#[derive(Deserialize)]
317struct ScopesResponse {
318 #[serde(rename = "Scopes", default)]
319 scopes: Vec<String>,
320}
321
322#[derive(Deserialize)]
323struct SecondFactorInfo {
324 #[serde(rename = "Enabled", default)]
325 enabled: i32,
326}
327
328impl SecondFactorInfo {
329 fn is_enabled(&self) -> bool {
330 self.enabled != 0
331 }
332}