Skip to main content

AirLibrary/Authentication/
mod.rs

1//! # Authentication Service
2//!
3//! Handles user authentication, token management, and cryptographic operations
4//! for the Air daemon. This service manages secure storage of credentials
5//! and provides authentication services to Mountain with resilient patterns.
6
7use 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
24/// Authentication service implementation
25pub struct AuthenticationService {
26	/// Application state
27	AppState:Arc<ApplicationState>,
28
29	/// Active sessions
30	Sessions:Arc<RwLock<HashMap<String, AuthSession>>>,
31
32	/// Credentials storage
33	Credentials:Arc<Mutex<CredentialsStore>>,
34
35	/// Cryptographic keys
36	CryptoKeys:Arc<Mutex<CryptoKeys>>,
37
38	/// AEAD algorithm for encryption/decryption
39	AeadAlgo:&'static aead::Algorithm,
40}
41
42/// Authentication session
43#[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/// Credentials storage
61#[derive(Debug, Serialize, Deserialize)]
62struct CredentialsStore {
63	Credentials:HashMap<String, UserCredentials>,
64
65	FilePath:String,
66}
67
68/// User credentials
69#[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/// Cryptographic keys
83#[derive(Debug)]
84struct CryptoKeys {
85	SigningKey:ring::signature::Ed25519KeyPair,
86
87	EncryptionKey:[u8; 32],
88}
89
90impl AuthenticationService {
91	/// Create a new authentication service
92	pub async fn new(AppState:Arc<ApplicationState>) -> Result<Self> {
93		let config = &AppState.Configuration.Authentication;
94
95		// Expand credentials path
96		let CredentialsPath = ConfigurationManager::ExpandPath(&config.CredentialsPath)?;
97
98		// Load or create credentials store
99		let CredentialsStore = Self::LoadCredentialsStore(&CredentialsPath).await?;
100
101		// Generate cryptographic keys
102		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		// Initialize service status
119		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	/// Authenticate a user
129	pub async fn AuthenticateUser(&self, Username:String, Password:String, Provider:String) -> Result<String> {
130		// Validate input
131		if Username.is_empty() || Password.is_empty() || Provider.is_empty() {
132			return Err(AirError::Authentication("Invalid authentication parameters".to_string()));
133		}
134
135		// Check credentials
136		let _UserCredentials = self.ValidateCredentials(&Username, &Password, &Provider).await?;
137
138		// Generate session token
139		let Token = self.GenerateSessionToken(&Username, &Provider).await?;
140
141		// Create session
142		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		// Store session
162		{
163			let mut Sessions = self.Sessions.write().await;
164
165			Sessions.insert(Session.SessionId.clone(), Session);
166		}
167
168		// Update credentials usage
169		self.UpdateCredentialsUsage(&Username, &Provider).await?;
170
171		Ok(Token)
172	}
173
174	/// Validate user credentials
175	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			// Verify password (in a real implementation, this would decrypt and verify)
186			// For now, we'll use a simple approach
187			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	/// Generate a session token
200	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		// Sign the payload
206		let Signature = CryptoKeys.SigningKey.sign(Payload.as_bytes());
207
208		// Encode token
209		let Token = URL_SAFE.encode(format!("{}:{}", Payload, URL_SAFE.encode(Signature.as_ref())));
210
211		Ok(Token)
212	}
213
214	/// Update credentials usage timestamp
215	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		// Save updated credentials
225		self.SaveCredentialsStore(&CredentialsStore).await?;
226
227		Ok(())
228	}
229
230	/// Encrypt password
231	#[allow(dead_code)]
232	async fn EncryptPassword(&self, Password:&str) -> Result<String> {
233		let CryptoKeys = self.CryptoKeys.lock().await;
234
235		// Use AES-256-GCM via ring::aead. Prefix nonce to ciphertext and base64 encode.
236		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		// Reserve space for tag
252		InOut.extend_from_slice(&[0u8; 16]); // AES_256_GCM tag length is 16 bytes
253
254		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		// Store nonce + ciphertext
259		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	/// Decrypt password
269	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	/// Load credentials store from file
304	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			// Create new credentials store
316			Ok(CredentialsStore { Credentials:HashMap::new(), FilePath:FilePath.to_string_lossy().to_string() })
317		}
318	}
319
320	/// Save credentials store to file
321	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		// Create directory if it doesn't exist
326		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	/// Generate cryptographic keys
342	fn GenerateCryptoKeys() -> Result<CryptoKeys> {
343		// Generate signing key
344		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		// Generate encryption key
353		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	/// Start background tasks
364	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	/// Background task for session cleanup
375	async fn BackgroundTask(&self) {
376		let mut Interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // 5 minutes
377
378		loop {
379			Interval.tick().await;
380
381			// Clean up expired sessions
382			self.CleanupExpiredSessions().await;
383
384			// Save credentials periodically
385			if let Err(E) = self.SaveCredentialsPeriodically().await {
386				dev_log!("lifecycle", "error: [Authentication] Failed to save credentials: {}", E);
387			}
388		}
389	}
390
391	/// Clean up expired sessions
392	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	/// Save credentials periodically
403	async fn SaveCredentialsPeriodically(&self) -> Result<()> {
404		let CredentialsStore = self.Credentials.lock().await;
405
406		self.SaveCredentialsStore(&CredentialsStore).await
407	}
408
409	/// Stop background tasks
410	pub async fn StopBackgroundTasks(&self) {
411		// Implementation for graceful shutdown
412		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}