use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey};
use serde::{Deserialize, Serialize};
use crate::error::{Result, UgosError};
#[derive(Clone)]
pub struct Credentials {
pub username: String,
pub password: String,
}
impl std::fmt::Debug for Credentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Credentials")
.field("username", &self.username)
.field("password", &"[REDACTED]")
.finish()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Session {
pub token: String,
#[serde(default)]
pub public_key: String,
}
impl std::fmt::Debug for Session {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Session")
.field("token", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Deserialize)]
struct LoginData {
token: String,
#[serde(default)]
public_key: String,
}
pub async fn fetch_rsa_key(
client: &reqwest::Client,
base_url: &str,
username: &str,
) -> Result<RsaPublicKey> {
let url = format!("{base_url}/v1/verify/check");
let body = serde_json::json!({"username": username});
let resp = client.post(&url).json(&body).send().await?;
let header = resp
.headers()
.get("x-rsa-token")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| UgosError::Encryption("missing x-rsa-token header".into()))?;
let pem_bytes = B64
.decode(header)
.map_err(|e| UgosError::Encryption(format!("base64 decode: {e}")))?;
let pem_str = String::from_utf8(pem_bytes)
.map_err(|e| UgosError::Encryption(format!("PEM not UTF-8: {e}")))?;
parse_public_key(&pem_str)
}
pub fn parse_public_key(pem: &str) -> Result<RsaPublicKey> {
RsaPublicKey::from_pkcs1_pem(pem)
.or_else(|_| RsaPublicKey::from_public_key_pem(pem))
.or_else(|_| {
let relabeled = pem
.replace("BEGIN RSA PUBLIC KEY", "BEGIN PUBLIC KEY")
.replace("END RSA PUBLIC KEY", "END PUBLIC KEY");
RsaPublicKey::from_public_key_pem(&relabeled)
})
.map_err(|e| UgosError::Encryption(format!("RSA key parse: {e}")))
}
pub fn encrypt_password(key: &RsaPublicKey, password: &str) -> Result<String> {
let mut rng = rsa::rand_core::OsRng;
let ciphertext = key
.encrypt(&mut rng, Pkcs1v15Encrypt, password.as_bytes())
.map_err(|e| UgosError::Encryption(format!("RSA encrypt: {e}")))?;
Ok(B64.encode(ciphertext))
}
pub async fn login(
client: &reqwest::Client,
base_url: &str,
creds: &Credentials,
) -> Result<Session> {
let pubkey = fetch_rsa_key(client, base_url, &creds.username).await?;
let encrypted = encrypt_password(&pubkey, &creds.password)?;
let url = format!("{base_url}/v1/verify/login");
let body = serde_json::json!({
"username": creds.username,
"password": encrypted,
"keepalive": true,
"otp": false,
});
let resp = client.post(&url).json(&body).send().await?;
let api: crate::types::common::ApiResponse<serde_json::Value> = resp.json().await?;
let data: LoginData = serde_json::from_value(api.into_result()?)?;
let public_key = if data.public_key.is_empty() {
String::new()
} else {
B64.decode(&data.public_key)
.ok()
.and_then(|b| String::from_utf8(b).ok())
.unwrap_or_default()
};
Ok(Session {
token: data.token,
public_key,
})
}