1use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
9use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey};
10use serde::{Deserialize, Serialize};
11
12use crate::error::{Result, UgosError};
13
14#[derive(Clone)]
17pub struct Credentials {
18 pub username: String,
20 pub password: String,
22}
23
24impl std::fmt::Debug for Credentials {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("Credentials")
27 .field("username", &self.username)
28 .field("password", &"[REDACTED]")
29 .finish()
30 }
31}
32
33#[derive(Clone, Serialize, Deserialize)]
35pub struct Session {
36 pub token: String,
38 #[serde(default)]
42 pub public_key: String,
43}
44
45impl std::fmt::Debug for Session {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("Session")
48 .field("token", &"[REDACTED]")
49 .finish()
50 }
51}
52
53#[derive(Debug, Deserialize)]
55struct LoginData {
56 token: String,
57 #[serde(default)]
59 public_key: String,
60}
61
62pub async fn fetch_rsa_key(
69 client: &reqwest::Client,
70 base_url: &str,
71 username: &str,
72) -> Result<RsaPublicKey> {
73 let url = format!("{base_url}/v1/verify/check");
74 let body = serde_json::json!({"username": username});
75
76 let resp = client.post(&url).json(&body).send().await?;
77
78 let header = resp
79 .headers()
80 .get("x-rsa-token")
81 .and_then(|v| v.to_str().ok())
82 .ok_or_else(|| UgosError::Encryption("missing x-rsa-token header".into()))?;
83
84 let pem_bytes = B64
85 .decode(header)
86 .map_err(|e| UgosError::Encryption(format!("base64 decode: {e}")))?;
87 let pem_str = String::from_utf8(pem_bytes)
88 .map_err(|e| UgosError::Encryption(format!("PEM not UTF-8: {e}")))?;
89
90 parse_public_key(&pem_str)
91}
92
93pub fn parse_public_key(pem: &str) -> Result<RsaPublicKey> {
99 RsaPublicKey::from_pkcs1_pem(pem)
100 .or_else(|_| RsaPublicKey::from_public_key_pem(pem))
101 .or_else(|_| {
102 let relabeled = pem
105 .replace("BEGIN RSA PUBLIC KEY", "BEGIN PUBLIC KEY")
106 .replace("END RSA PUBLIC KEY", "END PUBLIC KEY");
107 RsaPublicKey::from_public_key_pem(&relabeled)
108 })
109 .map_err(|e| UgosError::Encryption(format!("RSA key parse: {e}")))
110}
111
112pub fn encrypt_password(key: &RsaPublicKey, password: &str) -> Result<String> {
118 let mut rng = rsa::rand_core::OsRng;
119 let ciphertext = key
120 .encrypt(&mut rng, Pkcs1v15Encrypt, password.as_bytes())
121 .map_err(|e| UgosError::Encryption(format!("RSA encrypt: {e}")))?;
122 Ok(B64.encode(ciphertext))
123}
124
125pub async fn login(
136 client: &reqwest::Client,
137 base_url: &str,
138 creds: &Credentials,
139) -> Result<Session> {
140 let pubkey = fetch_rsa_key(client, base_url, &creds.username).await?;
142
143 let encrypted = encrypt_password(&pubkey, &creds.password)?;
145
146 let url = format!("{base_url}/v1/verify/login");
148 let body = serde_json::json!({
149 "username": creds.username,
150 "password": encrypted,
151 "keepalive": true,
152 "otp": false,
153 });
154
155 let resp = client.post(&url).json(&body).send().await?;
156
157 let api: crate::types::common::ApiResponse<serde_json::Value> = resp.json().await?;
161 let data: LoginData = serde_json::from_value(api.into_result()?)?;
162
163 let public_key = if data.public_key.is_empty() {
165 String::new()
166 } else {
167 B64.decode(&data.public_key)
168 .ok()
169 .and_then(|b| String::from_utf8(b).ok())
170 .unwrap_or_default()
171 };
172
173 Ok(Session {
174 token: data.token,
175 public_key,
176 })
177}