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 async fn EncryptPassword(&self, Password:&str) -> Result<String> {
232 let CryptoKeys = self.CryptoKeys.lock().await;
233
234 let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
236 .map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
237
238 let LessSafe = aead::LessSafeKey::new(UnboundKey);
239
240 let mut NonceBytes = [0u8; 12];
241
242 ring::rand::SystemRandom::new()
243 .fill(&mut NonceBytes)
244 .map_err(|e| AirError::Authentication(format!("Failed to generate nonce: {:?}", e)))?;
245
246 let Nonce = aead::Nonce::assume_unique_for_key(NonceBytes);
247
248 let mut InOut = Password.as_bytes().to_vec();
249
250 InOut.extend_from_slice(&[0u8; 16]); LessSafe
254 .seal_in_place_append_tag(Nonce, aead::Aad::empty(), &mut InOut)
255 .map_err(|e| AirError::Authentication(format!("Encryption failed: {:?}", e)))?;
256
257 let mut Out = Vec::with_capacity(NonceBytes.len() + InOut.len());
259
260 Out.extend_from_slice(&NonceBytes);
261
262 Out.extend_from_slice(&InOut);
263
264 Ok(URL_SAFE.encode(&Out))
265 }
266
267 async fn DecryptPassword(&self, EncryptedPassword:&str) -> Result<String> {
269 let CryptoKeys = self.CryptoKeys.lock().await;
270
271 let Data = URL_SAFE
272 .decode(EncryptedPassword)
273 .map_err(|e| AirError::Authentication(format!("Failed to decode password: {}", e)))?;
274
275 if Data.len() < 12 + aead::AES_256_GCM.tag_len() {
276 return Err(AirError::Authentication("Encrypted data too short".to_string()));
277 }
278
279 let (NonceBytes, CipherBytes) = Data.split_at(12);
280
281 let mut NonceArr = [0u8; 12];
282
283 NonceArr.copy_from_slice(&NonceBytes[0..12]);
284
285 let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
286 .map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
287
288 let LessSafe = aead::LessSafeKey::new(UnboundKey);
289
290 let Nonce = aead::Nonce::assume_unique_for_key(NonceArr);
291
292 let mut CipherVec = CipherBytes.to_vec();
293
294 let Plain = LessSafe
295 .open_in_place(Nonce, aead::Aad::empty(), &mut CipherVec)
296 .map_err(|e| AirError::Authentication(format!("Decryption failed: {:?}", e)))?;
297
298 String::from_utf8(Plain.to_vec())
299 .map_err(|e| AirError::Authentication(format!("Failed to decode password string: {}", e)))
300 }
301
302 async fn LoadCredentialsStore(FilePath:&std::path::Path) -> Result<CredentialsStore> {
304 if FilePath.exists() {
305 let Content = tokio::fs::read_to_string(FilePath)
306 .await
307 .map_err(|e| AirError::Authentication(format!("Failed to read credentials file: {}", e)))?;
308
309 let Credentials:HashMap<String, UserCredentials> = serde_json::from_str(&Content)
310 .map_err(|e| AirError::Authentication(format!("Failed to parse credentials file: {}", e)))?;
311
312 Ok(CredentialsStore { Credentials, FilePath:FilePath.to_string_lossy().to_string() })
313 } else {
314 Ok(CredentialsStore { Credentials:HashMap::new(), FilePath:FilePath.to_string_lossy().to_string() })
316 }
317 }
318
319 async fn SaveCredentialsStore(&self, Store:&CredentialsStore) -> Result<()> {
321 let Content = serde_json::to_string_pretty(&Store.Credentials)
322 .map_err(|e| AirError::Authentication(format!("Failed to serialize credentials: {}", e)))?;
323
324 if let Some(Parent) = std::path::Path::new(&Store.FilePath).parent() {
326 tokio::fs::create_dir_all(Parent)
327 .await
328 .map_err(|e| AirError::Authentication(format!("Failed to create credentials directory: {}", e)))?;
329
330 tokio::fs::write(&Store.FilePath, Content)
331 .await
332 .map_err(|e| AirError::Authentication(format!("Failed to write credentials file: {}", e)))?;
333
334 Ok(())
335 } else {
336 Err(AirError::Authentication("Invalid file path - no parent directory".to_string()))
337 }
338 }
339
340 fn GenerateCryptoKeys() -> Result<CryptoKeys> {
342 let Rng = ring::rand::SystemRandom::new();
344
345 let Pkcs8Bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&Rng)
346 .map_err(|e| AirError::Authentication(format!("Failed to generate signing key: {}", e)))?;
347
348 let SigningKey = ring::signature::Ed25519KeyPair::from_pkcs8(Pkcs8Bytes.as_ref())
349 .map_err(|e| AirError::Authentication(format!("Failed to load signing key: {}", e)))?;
350
351 let mut EncryptionKey = [0u8; 32];
353
354 ring::rand::SystemRandom::new()
355 .fill(&mut EncryptionKey)
356 .map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))
357 .map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))?;
358
359 Ok(CryptoKeys { SigningKey, EncryptionKey })
360 }
361
362 pub async fn StartBackgroundTasks(&self) -> Result<tokio::task::JoinHandle<()>> {
364 let Service = self.clone();
365
366 let Handle = tokio::spawn(async move {
367 Service.BackgroundTask().await;
368 });
369
370 Ok(Handle)
371 }
372
373 async fn BackgroundTask(&self) {
375 let mut Interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); loop {
378 Interval.tick().await;
379
380 self.CleanupExpiredSessions().await;
382
383 if let Err(E) = self.SaveCredentialsPeriodically().await {
385 dev_log!("lifecycle", "error: [Authentication] Failed to save credentials: {}", E);
386 }
387 }
388 }
389
390 async fn CleanupExpiredSessions(&self) {
392 let Now = Utc::now();
393
394 let mut Sessions = self.Sessions.write().await;
395
396 Sessions.retain(|_, Session| Session.ExpiresAt > Now && Session.IsValid);
397
398 dev_log!("lifecycle", "[Authentication] Cleaned up expired sessions");
399 }
400
401 async fn SaveCredentialsPeriodically(&self) -> Result<()> {
403 let CredentialsStore = self.Credentials.lock().await;
404
405 self.SaveCredentialsStore(&CredentialsStore).await
406 }
407
408 pub async fn StopBackgroundTasks(&self) {
410 dev_log!("lifecycle", "[Authentication] Stopping background tasks");
412 }
413}
414
415impl Clone for AuthenticationService {
416 fn clone(&self) -> Self {
417 Self {
418 AppState:self.AppState.clone(),
419
420 Sessions:self.Sessions.clone(),
421
422 Credentials:self.Credentials.clone(),
423
424 CryptoKeys:self.CryptoKeys.clone(),
425
426 AeadAlgo:self.AeadAlgo,
427 }
428 }
429}