use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use serde::{Deserialize, Serialize};
use crate::config::ProtonClientConfiguration;
use crate::crypto;
use crate::error::{ProtonError, Result};
use crate::http::{self, ApiHttpClient, Tokens};
use crate::ids::{SessionId, UserId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PasswordMode {
Single,
Dual,
}
impl PasswordMode {
fn from_wire(value: i32) -> Self {
match value {
1 => PasswordMode::Single,
_ => PasswordMode::Dual,
}
}
}
#[derive(Debug, Clone)]
pub struct ResumeParameters {
pub session_id: SessionId,
pub username: String,
pub user_id: UserId,
pub access_token: String,
pub refresh_token: String,
pub scopes: Vec<String>,
pub is_waiting_for_second_factor_code: bool,
pub password_mode: PasswordMode,
}
#[derive(Clone)]
pub struct ProtonApiSession {
http: ApiHttpClient,
session_id: SessionId,
username: String,
user_id: UserId,
scopes: Vec<String>,
password_mode: PasswordMode,
is_waiting_for_second_factor: bool,
}
impl ProtonApiSession {
pub fn resume(config: ProtonClientConfiguration, params: ResumeParameters) -> Result<Self> {
let tokens = Tokens {
access_token: params.access_token,
refresh_token: params.refresh_token,
};
let http = ApiHttpClient::new(config, params.session_id.clone(), tokens)?;
Ok(Self {
http,
session_id: params.session_id,
username: params.username,
user_id: params.user_id,
scopes: params.scopes,
password_mode: params.password_mode,
is_waiting_for_second_factor: params.is_waiting_for_second_factor_code,
})
}
pub async fn begin(
config: ProtonClientConfiguration,
username: &str,
password: &[u8],
) -> Result<Self> {
let init: SessionInitiationResponse = http::post_unauthenticated(
&config,
"auth/v4/info",
&SessionInitiationRequest { username },
)
.await?;
let salt = BASE64
.decode(init.salt.trim())
.map_err(|e| ProtonError::invalid_operation(format!("decode SRP salt: {e}")))?;
let server_ephemeral = BASE64
.decode(init.server_ephemeral.trim())
.map_err(|e| ProtonError::invalid_operation(format!("decode server ephemeral: {e}")))?;
let proofs = crypto::generate_proofs(
init.version,
password,
&salt,
&init.modulus,
&server_ephemeral,
crypto::DEFAULT_BIT_LENGTH,
)?;
let auth: AuthenticationResponse = http::post_unauthenticated(
&config,
"auth/v4",
&AuthenticationRequest {
username,
client_ephemeral: BASE64.encode(&proofs.client_ephemeral),
client_proof: BASE64.encode(&proofs.client_proof),
srp_session: init.srp_session,
},
)
.await?;
let server_proof = BASE64
.decode(auth.server_proof.trim())
.map_err(|e| ProtonError::invalid_operation(format!("decode server proof: {e}")))?;
if server_proof != proofs.expected_server_proof {
return Err(ProtonError::invalid_operation(
"SRP server proof mismatch — server failed authentication",
));
}
let tokens = Tokens {
access_token: auth.access_token,
refresh_token: auth.refresh_token,
};
let http = ApiHttpClient::new(config, auth.session_id.clone(), tokens)?;
Ok(Self {
http,
session_id: auth.session_id,
username: username.to_owned(),
user_id: auth.user_id,
scopes: auth.scopes,
password_mode: PasswordMode::from_wire(auth.password_mode),
is_waiting_for_second_factor: auth
.second_factor
.map(|f| f.is_enabled())
.unwrap_or(false),
})
}
pub async fn apply_second_factor_code(&mut self, code: &str) -> Result<()> {
let response: ScopesResponse = self
.http
.post("auth/v4/2fa", &SecondFactorValidationRequest { code })
.await?;
self.is_waiting_for_second_factor = false;
self.scopes = response.scopes;
Ok(())
}
pub async fn refresh_scopes(&mut self) -> Result<()> {
let response: ScopesResponse = self.http.get("auth/v4/scopes").await?;
self.scopes = response.scopes;
Ok(())
}
pub async fn end(&self) -> Result<()> {
let _: crate::api::ApiResponse = self.http.delete("auth/v4").await?;
Ok(())
}
pub fn http(&self) -> &ApiHttpClient {
&self.http
}
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
pub fn user_id(&self) -> &UserId {
&self.user_id
}
pub fn username(&self) -> &str {
&self.username
}
pub fn scopes(&self) -> &[String] {
&self.scopes
}
pub fn password_mode(&self) -> PasswordMode {
self.password_mode
}
pub fn is_waiting_for_second_factor(&self) -> bool {
self.is_waiting_for_second_factor
}
pub async fn current_tokens(&self) -> Tokens {
self.http.current_tokens().await
}
}
#[derive(Serialize)]
struct SessionInitiationRequest<'a> {
#[serde(rename = "Username")]
username: &'a str,
}
#[derive(Deserialize)]
struct SessionInitiationResponse {
#[serde(rename = "Version")]
version: i32,
#[serde(rename = "Modulus")]
modulus: String,
#[serde(rename = "ServerEphemeral")]
server_ephemeral: String,
#[serde(rename = "Salt")]
salt: String,
#[serde(rename = "SRPSession")]
srp_session: String,
}
#[derive(Serialize)]
struct AuthenticationRequest<'a> {
#[serde(rename = "Username")]
username: &'a str,
#[serde(rename = "ClientEphemeral")]
client_ephemeral: String,
#[serde(rename = "ClientProof")]
client_proof: String,
#[serde(rename = "SRPSession")]
srp_session: String,
}
#[derive(Deserialize)]
struct AuthenticationResponse {
#[serde(rename = "UID")]
session_id: SessionId,
#[serde(rename = "UserID")]
user_id: UserId,
#[serde(rename = "ServerProof")]
server_proof: String,
#[serde(rename = "AccessToken")]
access_token: String,
#[serde(rename = "RefreshToken")]
refresh_token: String,
#[serde(rename = "Scopes", default)]
scopes: Vec<String>,
#[serde(rename = "PasswordMode")]
password_mode: i32,
#[serde(rename = "2FA")]
second_factor: Option<SecondFactorInfo>,
}
#[derive(Serialize)]
struct SecondFactorValidationRequest<'a> {
#[serde(rename = "TwoFactorCode")]
code: &'a str,
}
#[derive(Deserialize)]
struct ScopesResponse {
#[serde(rename = "Scopes", default)]
scopes: Vec<String>,
}
#[derive(Deserialize)]
struct SecondFactorInfo {
#[serde(rename = "Enabled", default)]
enabled: i32,
}
impl SecondFactorInfo {
fn is_enabled(&self) -> bool {
self.enabled != 0
}
}