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