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
38 AeadAlgo:&'static aead::Algorithm,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AuthSession {
45 pub SessionId:String,
46
47 pub UserId:String,
48
49 pub Provider:String,
50
51 pub Token:String,
52
53 pub CreatedAt:DateTime<Utc>,
54
55 pub ExpiresAt:DateTime<Utc>,
56
57 pub IsValid:bool,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
62struct CredentialsStore {
63 Credentials:HashMap<String, UserCredentials>,
64
65 FilePath:String,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct UserCredentials {
71 pub UserId:String,
72
73 pub Provider:String,
74
75 pub EncryptedPassword:String,
76
77 pub LastUsed:DateTime<Utc>,
78
79 pub IsValid:bool,
80}
81
82#[derive(Debug)]
84struct CryptoKeys {
85 SigningKey:ring::signature::Ed25519KeyPair,
86
87 EncryptionKey:[u8; 32],
88}
89
90impl AuthenticationService {
91 pub async fn new(AppState:Arc<ApplicationState>) -> Result<Self> {
93 let config = &AppState.Configuration.Authentication;
94
95 let CredentialsPath = ConfigurationManager::ExpandPath(&config.CredentialsPath)?;
97
98 let CredentialsStore = Self::LoadCredentialsStore(&CredentialsPath).await?;
100
101 let CryptoKeys = Self::GenerateCryptoKeys()?;
103
104 let AeadAlgo = &aead::AES_256_GCM;
105
106 let Service = Self {
107 AppState,
108
109 Sessions:Arc::new(RwLock::new(HashMap::new())),
110
111 Credentials:Arc::new(Mutex::new(CredentialsStore)),
112
113 CryptoKeys:Arc::new(Mutex::new(CryptoKeys)),
114
115 AeadAlgo,
116 };
117
118 Service
120 .AppState
121 .UpdateServiceStatus("authentication", crate::ApplicationState::ServiceStatus::Running)
122 .await
123 .map_err(|e| AirError::Authentication(e.to_string()))?;
124
125 Ok(Service)
126 }
127
128 pub async fn AuthenticateUser(&self, Username:String, Password:String, Provider:String) -> Result<String> {
130 if Username.is_empty() || Password.is_empty() || Provider.is_empty() {
132 return Err(AirError::Authentication("Invalid authentication parameters".to_string()));
133 }
134
135 let _UserCredentials = self.ValidateCredentials(&Username, &Password, &Provider).await?;
137
138 let Token = self.GenerateSessionToken(&Username, &Provider).await?;
140
141 let SessionId = Utility::GenerateRequestId();
143
144 let Session = AuthSession {
145 SessionId,
146
147 UserId:Username.clone(),
148
149 Provider:Provider.clone(),
150
151 Token:Token.clone(),
152
153 CreatedAt:chrono::Utc::now(),
154
155 ExpiresAt:chrono::Utc::now()
156 + chrono::Duration::hours(self.AppState.Configuration.Authentication.TokenExpirationHours as i64),
157
158 IsValid:true,
159 };
160
161 {
163 let mut Sessions = self.Sessions.write().await;
164
165 Sessions.insert(Session.SessionId.clone(), Session);
166 }
167
168 self.UpdateCredentialsUsage(&Username, &Provider).await?;
170
171 Ok(Token)
172 }
173
174 async fn ValidateCredentials(&self, Username:&str, Password:&str, Provider:&str) -> Result<UserCredentials> {
176 let CredentialsStore = self.Credentials.lock().await;
177
178 let Key = format!("{}:{}", Provider, Username);
179
180 if let Some(UserCredentials) = CredentialsStore.Credentials.get(&Key) {
181 if !UserCredentials.IsValid {
182 return Err(AirError::Authentication("Credentials are invalid".to_string()));
183 }
184
185 let DecryptedPassword = self.DecryptPassword(&UserCredentials.EncryptedPassword).await?;
188
189 if DecryptedPassword == Password {
190 Ok(UserCredentials.clone())
191 } else {
192 Err(AirError::Authentication("Invalid password".to_string()))
193 }
194 } else {
195 Err(AirError::Authentication("User not found".to_string()))
196 }
197 }
198
199 async fn GenerateSessionToken(&self, Username:&str, Provider:&str) -> Result<String> {
201 let CryptoKeys = self.CryptoKeys.lock().await;
202
203 let Payload = format!("{}:{}:{}", Username, Provider, Utility::CurrentTimestamp());
204
205 let Signature = CryptoKeys.SigningKey.sign(Payload.as_bytes());
207
208 let Token = URL_SAFE.encode(format!("{}:{}", Payload, URL_SAFE.encode(Signature.as_ref())));
210
211 Ok(Token)
212 }
213
214 async fn UpdateCredentialsUsage(&self, Username:&str, Provider:&str) -> Result<()> {
216 let mut CredentialsStore = self.Credentials.lock().await;
217
218 let Key = format!("{}:{}", Provider, Username);
219
220 if let Some(UserCredentials) = CredentialsStore.Credentials.get_mut(&Key) {
221 UserCredentials.LastUsed = Utc::now();
222 }
223
224 self.SaveCredentialsStore(&CredentialsStore).await?;
226
227 Ok(())
228 }
229
230 #[allow(dead_code)]
232 async fn EncryptPassword(&self, Password:&str) -> Result<String> {
233 let CryptoKeys = self.CryptoKeys.lock().await;
234
235 let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
237 .map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
238
239 let LessSafe = aead::LessSafeKey::new(UnboundKey);
240
241 let mut NonceBytes = [0u8; 12];
242
243 ring::rand::SystemRandom::new()
244 .fill(&mut NonceBytes)
245 .map_err(|e| AirError::Authentication(format!("Failed to generate nonce: {:?}", e)))?;
246
247 let Nonce = aead::Nonce::assume_unique_for_key(NonceBytes);
248
249 let mut InOut = Password.as_bytes().to_vec();
250
251 InOut.extend_from_slice(&[0u8; 16]); LessSafe
255 .seal_in_place_append_tag(Nonce, aead::Aad::empty(), &mut InOut)
256 .map_err(|e| AirError::Authentication(format!("Encryption failed: {:?}", e)))?;
257
258 let mut Out = Vec::with_capacity(NonceBytes.len() + InOut.len());
260
261 Out.extend_from_slice(&NonceBytes);
262
263 Out.extend_from_slice(&InOut);
264
265 Ok(URL_SAFE.encode(&Out))
266 }
267
268 async fn DecryptPassword(&self, EncryptedPassword:&str) -> Result<String> {
270 let CryptoKeys = self.CryptoKeys.lock().await;
271
272 let Data = URL_SAFE
273 .decode(EncryptedPassword)
274 .map_err(|e| AirError::Authentication(format!("Failed to decode password: {}", e)))?;
275
276 if Data.len() < 12 + aead::AES_256_GCM.tag_len() {
277 return Err(AirError::Authentication("Encrypted data too short".to_string()));
278 }
279
280 let (NonceBytes, CipherBytes) = Data.split_at(12);
281
282 let mut NonceArr = [0u8; 12];
283
284 NonceArr.copy_from_slice(&NonceBytes[0..12]);
285
286 let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
287 .map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
288
289 let LessSafe = aead::LessSafeKey::new(UnboundKey);
290
291 let Nonce = aead::Nonce::assume_unique_for_key(NonceArr);
292
293 let mut CipherVec = CipherBytes.to_vec();
294
295 let Plain = LessSafe
296 .open_in_place(Nonce, aead::Aad::empty(), &mut CipherVec)
297 .map_err(|e| AirError::Authentication(format!("Decryption failed: {:?}", e)))?;
298
299 String::from_utf8(Plain.to_vec())
300 .map_err(|e| AirError::Authentication(format!("Failed to decode password string: {}", e)))
301 }
302
303 async fn LoadCredentialsStore(FilePath:&std::path::Path) -> Result<CredentialsStore> {
305 if FilePath.exists() {
306 let Content = tokio::fs::read_to_string(FilePath)
307 .await
308 .map_err(|e| AirError::Authentication(format!("Failed to read credentials file: {}", e)))?;
309
310 let Credentials:HashMap<String, UserCredentials> = serde_json::from_str(&Content)
311 .map_err(|e| AirError::Authentication(format!("Failed to parse credentials file: {}", e)))?;
312
313 Ok(CredentialsStore { Credentials, FilePath:FilePath.to_string_lossy().to_string() })
314 } else {
315 Ok(CredentialsStore { Credentials:HashMap::new(), FilePath:FilePath.to_string_lossy().to_string() })
317 }
318 }
319
320 async fn SaveCredentialsStore(&self, Store:&CredentialsStore) -> Result<()> {
322 let Content = serde_json::to_string_pretty(&Store.Credentials)
323 .map_err(|e| AirError::Authentication(format!("Failed to serialize credentials: {}", e)))?;
324
325 if let Some(Parent) = std::path::Path::new(&Store.FilePath).parent() {
327 tokio::fs::create_dir_all(Parent)
328 .await
329 .map_err(|e| AirError::Authentication(format!("Failed to create credentials directory: {}", e)))?;
330
331 tokio::fs::write(&Store.FilePath, Content)
332 .await
333 .map_err(|e| AirError::Authentication(format!("Failed to write credentials file: {}", e)))?;
334
335 Ok(())
336 } else {
337 Err(AirError::Authentication("Invalid file path - no parent directory".to_string()))
338 }
339 }
340
341 fn GenerateCryptoKeys() -> Result<CryptoKeys> {
343 let Rng = ring::rand::SystemRandom::new();
345
346 let Pkcs8Bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&Rng)
347 .map_err(|e| AirError::Authentication(format!("Failed to generate signing key: {}", e)))?;
348
349 let SigningKey = ring::signature::Ed25519KeyPair::from_pkcs8(Pkcs8Bytes.as_ref())
350 .map_err(|e| AirError::Authentication(format!("Failed to load signing key: {}", e)))?;
351
352 let mut EncryptionKey = [0u8; 32];
354
355 ring::rand::SystemRandom::new()
356 .fill(&mut EncryptionKey)
357 .map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))
358 .map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))?;
359
360 Ok(CryptoKeys { SigningKey, EncryptionKey })
361 }
362
363 pub async fn StartBackgroundTasks(&self) -> Result<tokio::task::JoinHandle<()>> {
365 let Service = self.clone();
366
367 let Handle = tokio::spawn(async move {
368 Service.BackgroundTask().await;
369 });
370
371 Ok(Handle)
372 }
373
374 async fn BackgroundTask(&self) {
376 let mut Interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); loop {
379 Interval.tick().await;
380
381 self.CleanupExpiredSessions().await;
383
384 if let Err(E) = self.SaveCredentialsPeriodically().await {
386 dev_log!("lifecycle", "error: [Authentication] Failed to save credentials: {}", E);
387 }
388 }
389 }
390
391 async fn CleanupExpiredSessions(&self) {
393 let Now = Utc::now();
394
395 let mut Sessions = self.Sessions.write().await;
396
397 Sessions.retain(|_, Session| Session.ExpiresAt > Now && Session.IsValid);
398
399 dev_log!("lifecycle", "[Authentication] Cleaned up expired sessions");
400 }
401
402 async fn SaveCredentialsPeriodically(&self) -> Result<()> {
404 let CredentialsStore = self.Credentials.lock().await;
405
406 self.SaveCredentialsStore(&CredentialsStore).await
407 }
408
409 pub async fn StopBackgroundTasks(&self) {
411 dev_log!("lifecycle", "[Authentication] Stopping background tasks");
413 }
414}
415
416impl Clone for AuthenticationService {
417 fn clone(&self) -> Self {
418 Self {
419 AppState:self.AppState.clone(),
420
421 Sessions:self.Sessions.clone(),
422
423 Credentials:self.Credentials.clone(),
424
425 CryptoKeys:self.CryptoKeys.clone(),
426
427 AeadAlgo:self.AeadAlgo,
428 }
429 }
430}