AirLibrary/Authentication/
mod.rs1use std::{collections::HashMap, sync::Arc};
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use tokio::sync::{Mutex, RwLock};
12use base64::{Engine as _, engine::general_purpose::URL_SAFE};
13use ring::{aead, rand::SecureRandom};
14
15use crate::{
16 AirError,
17 ApplicationState::ApplicationState,
18 Configuration::ConfigurationManager,
19 Result,
20 Utility,
21 dev_log,
22};
23
24pub struct AuthenticationService {
26 AppState:Arc<ApplicationState>,
28
29 Sessions:Arc<RwLock<HashMap<String, AuthSession>>>,
31
32 Credentials:Arc<Mutex<CredentialsStore>>,
34
35 CryptoKeys:Arc<Mutex<CryptoKeys>>,
37 AeadAlgo:&'static aead::Algorithm,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AuthSession {
44 pub SessionId:String,
45 pub UserId:String,
46 pub Provider:String,
47 pub Token:String,
48 pub CreatedAt:DateTime<Utc>,
49 pub ExpiresAt:DateTime<Utc>,
50 pub IsValid:bool,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
55struct CredentialsStore {
56 Credentials:HashMap<String, UserCredentials>,
57 FilePath:String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct UserCredentials {
63 pub UserId:String,
64 pub Provider:String,
65 pub EncryptedPassword:String,
66 pub LastUsed:DateTime<Utc>,
67 pub IsValid:bool,
68}
69
70#[derive(Debug)]
72struct CryptoKeys {
73 SigningKey:ring::signature::Ed25519KeyPair,
74 EncryptionKey:[u8; 32],
75}
76
77impl AuthenticationService {
78 pub async fn new(AppState:Arc<ApplicationState>) -> Result<Self> {
80 let config = &AppState.Configuration.Authentication;
81
82 let CredentialsPath = ConfigurationManager::ExpandPath(&config.CredentialsPath)?;
84
85 let CredentialsStore = Self::LoadCredentialsStore(&CredentialsPath).await?;
87
88 let CryptoKeys = Self::GenerateCryptoKeys()?;
90 let AeadAlgo = &aead::AES_256_GCM;
91
92 let Service = Self {
93 AppState,
94 Sessions:Arc::new(RwLock::new(HashMap::new())),
95 Credentials:Arc::new(Mutex::new(CredentialsStore)),
96 CryptoKeys:Arc::new(Mutex::new(CryptoKeys)),
97 AeadAlgo,
98 };
99
100 Service
102 .AppState
103 .UpdateServiceStatus("authentication", crate::ApplicationState::ServiceStatus::Running)
104 .await
105 .map_err(|e| AirError::Authentication(e.to_string()))?;
106
107 Ok(Service)
108 }
109
110 pub async fn AuthenticateUser(&self, Username:String, Password:String, Provider:String) -> Result<String> {
112 if Username.is_empty() || Password.is_empty() || Provider.is_empty() {
114 return Err(AirError::Authentication("Invalid authentication parameters".to_string()));
115 }
116
117 let _UserCredentials = self.ValidateCredentials(&Username, &Password, &Provider).await?;
119
120 let Token = self.GenerateSessionToken(&Username, &Provider).await?;
122
123 let SessionId = Utility::GenerateRequestId();
125 let Session = AuthSession {
126 SessionId,
127 UserId:Username.clone(),
128 Provider:Provider.clone(),
129 Token:Token.clone(),
130 CreatedAt:chrono::Utc::now(),
131 ExpiresAt:chrono::Utc::now()
132 + chrono::Duration::hours(self.AppState.Configuration.Authentication.TokenExpirationHours as i64),
133 IsValid:true,
134 };
135
136 {
138 let mut Sessions = self.Sessions.write().await;
139 Sessions.insert(Session.SessionId.clone(), Session);
140 }
141
142 self.UpdateCredentialsUsage(&Username, &Provider).await?;
144
145 Ok(Token)
146 }
147
148 async fn ValidateCredentials(&self, Username:&str, Password:&str, Provider:&str) -> Result<UserCredentials> {
150 let CredentialsStore = self.Credentials.lock().await;
151
152 let Key = format!("{}:{}", Provider, Username);
153
154 if let Some(UserCredentials) = CredentialsStore.Credentials.get(&Key) {
155 if !UserCredentials.IsValid {
156 return Err(AirError::Authentication("Credentials are invalid".to_string()));
157 }
158
159 let DecryptedPassword = self.DecryptPassword(&UserCredentials.EncryptedPassword).await?;
162
163 if DecryptedPassword == Password {
164 Ok(UserCredentials.clone())
165 } else {
166 Err(AirError::Authentication("Invalid password".to_string()))
167 }
168 } else {
169 Err(AirError::Authentication("User not found".to_string()))
170 }
171 }
172
173 async fn GenerateSessionToken(&self, Username:&str, Provider:&str) -> Result<String> {
175 let CryptoKeys = self.CryptoKeys.lock().await;
176
177 let Payload = format!("{}:{}:{}", Username, Provider, Utility::CurrentTimestamp());
178
179 let Signature = CryptoKeys.SigningKey.sign(Payload.as_bytes());
181
182 let Token = URL_SAFE.encode(format!("{}:{}", Payload, URL_SAFE.encode(Signature.as_ref())));
184
185 Ok(Token)
186 }
187
188 async fn UpdateCredentialsUsage(&self, Username:&str, Provider:&str) -> Result<()> {
190 let mut CredentialsStore = self.Credentials.lock().await;
191
192 let Key = format!("{}:{}", Provider, Username);
193
194 if let Some(UserCredentials) = CredentialsStore.Credentials.get_mut(&Key) {
195 UserCredentials.LastUsed = Utc::now();
196 }
197
198 self.SaveCredentialsStore(&CredentialsStore).await?;
200
201 Ok(())
202 }
203
204 #[allow(dead_code)]
206 async fn EncryptPassword(&self, Password:&str) -> Result<String> {
207 let CryptoKeys = self.CryptoKeys.lock().await;
208
209 let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
211 .map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
212
213 let LessSafe = aead::LessSafeKey::new(UnboundKey);
214 let mut NonceBytes = [0u8; 12];
215 ring::rand::SystemRandom::new()
216 .fill(&mut NonceBytes)
217 .map_err(|e| AirError::Authentication(format!("Failed to generate nonce: {:?}", e)))?;
218
219 let Nonce = aead::Nonce::assume_unique_for_key(NonceBytes);
220
221 let mut InOut = Password.as_bytes().to_vec();
222 InOut.extend_from_slice(&[0u8; 16]); LessSafe
226 .seal_in_place_append_tag(Nonce, aead::Aad::empty(), &mut InOut)
227 .map_err(|e| AirError::Authentication(format!("Encryption failed: {:?}", e)))?;
228
229 let mut Out = Vec::with_capacity(NonceBytes.len() + InOut.len());
231 Out.extend_from_slice(&NonceBytes);
232 Out.extend_from_slice(&InOut);
233
234 Ok(URL_SAFE.encode(&Out))
235 }
236
237 async fn DecryptPassword(&self, EncryptedPassword:&str) -> Result<String> {
239 let CryptoKeys = self.CryptoKeys.lock().await;
240
241 let Data = URL_SAFE
242 .decode(EncryptedPassword)
243 .map_err(|e| AirError::Authentication(format!("Failed to decode password: {}", e)))?;
244
245 if Data.len() < 12 + aead::AES_256_GCM.tag_len() {
246 return Err(AirError::Authentication("Encrypted data too short".to_string()));
247 }
248
249 let (NonceBytes, CipherBytes) = Data.split_at(12);
250
251 let mut NonceArr = [0u8; 12];
252 NonceArr.copy_from_slice(&NonceBytes[0..12]);
253
254 let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
255 .map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
256
257 let LessSafe = aead::LessSafeKey::new(UnboundKey);
258 let Nonce = aead::Nonce::assume_unique_for_key(NonceArr);
259
260 let mut CipherVec = CipherBytes.to_vec();
261 let Plain = LessSafe
262 .open_in_place(Nonce, aead::Aad::empty(), &mut CipherVec)
263 .map_err(|e| AirError::Authentication(format!("Decryption failed: {:?}", e)))?;
264
265 String::from_utf8(Plain.to_vec())
266 .map_err(|e| AirError::Authentication(format!("Failed to decode password string: {}", e)))
267 }
268
269 async fn LoadCredentialsStore(FilePath:&std::path::Path) -> Result<CredentialsStore> {
271 if FilePath.exists() {
272 let Content = tokio::fs::read_to_string(FilePath)
273 .await
274 .map_err(|e| AirError::Authentication(format!("Failed to read credentials file: {}", e)))?;
275
276 let Credentials:HashMap<String, UserCredentials> = serde_json::from_str(&Content)
277 .map_err(|e| AirError::Authentication(format!("Failed to parse credentials file: {}", e)))?;
278
279 Ok(CredentialsStore { Credentials, FilePath:FilePath.to_string_lossy().to_string() })
280 } else {
281 Ok(CredentialsStore { Credentials:HashMap::new(), FilePath:FilePath.to_string_lossy().to_string() })
283 }
284 }
285
286 async fn SaveCredentialsStore(&self, Store:&CredentialsStore) -> Result<()> {
288 let Content = serde_json::to_string_pretty(&Store.Credentials)
289 .map_err(|e| AirError::Authentication(format!("Failed to serialize credentials: {}", e)))?;
290
291 if let Some(Parent) = std::path::Path::new(&Store.FilePath).parent() {
293 tokio::fs::create_dir_all(Parent)
294 .await
295 .map_err(|e| AirError::Authentication(format!("Failed to create credentials directory: {}", e)))?;
296
297 tokio::fs::write(&Store.FilePath, Content)
298 .await
299 .map_err(|e| AirError::Authentication(format!("Failed to write credentials file: {}", e)))?;
300
301 Ok(())
302 } else {
303 Err(AirError::Authentication("Invalid file path - no parent directory".to_string()))
304 }
305 }
306
307 fn GenerateCryptoKeys() -> Result<CryptoKeys> {
309 let Rng = ring::rand::SystemRandom::new();
311 let Pkcs8Bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&Rng)
312 .map_err(|e| AirError::Authentication(format!("Failed to generate signing key: {}", e)))?;
313
314 let SigningKey = ring::signature::Ed25519KeyPair::from_pkcs8(Pkcs8Bytes.as_ref())
315 .map_err(|e| AirError::Authentication(format!("Failed to load signing key: {}", e)))?;
316
317 let mut EncryptionKey = [0u8; 32];
319 ring::rand::SystemRandom::new()
320 .fill(&mut EncryptionKey)
321 .map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))
322 .map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))?;
323
324 Ok(CryptoKeys { SigningKey, EncryptionKey })
325 }
326
327 pub async fn StartBackgroundTasks(&self) -> Result<tokio::task::JoinHandle<()>> {
329 let Service = self.clone();
330
331 let Handle = tokio::spawn(async move {
332 Service.BackgroundTask().await;
333 });
334
335 Ok(Handle)
336 }
337
338 async fn BackgroundTask(&self) {
340 let mut Interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); loop {
343 Interval.tick().await;
344
345 self.CleanupExpiredSessions().await;
347
348 if let Err(E) = self.SaveCredentialsPeriodically().await {
350 dev_log!("lifecycle", "error: [Authentication] Failed to save credentials: {}", E);
351 }
352 }
353 }
354
355 async fn CleanupExpiredSessions(&self) {
357 let Now = Utc::now();
358 let mut Sessions = self.Sessions.write().await;
359
360 Sessions.retain(|_, Session| Session.ExpiresAt > Now && Session.IsValid);
361
362 dev_log!("lifecycle", "[Authentication] Cleaned up expired sessions");
363 }
364
365 async fn SaveCredentialsPeriodically(&self) -> Result<()> {
367 let CredentialsStore = self.Credentials.lock().await;
368 self.SaveCredentialsStore(&CredentialsStore).await
369 }
370
371 pub async fn StopBackgroundTasks(&self) {
373 dev_log!("lifecycle", "[Authentication] Stopping background tasks");
375 }
376}
377
378impl Clone for AuthenticationService {
379 fn clone(&self) -> Self {
380 Self {
381 AppState:self.AppState.clone(),
382 Sessions:self.Sessions.clone(),
383 Credentials:self.Credentials.clone(),
384 CryptoKeys:self.CryptoKeys.clone(),
385 AeadAlgo:self.AeadAlgo,
386 }
387 }
388}