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	/// AEAD algorithm for encryption/decryption
38	AeadAlgo:&'static aead::Algorithm,
39}
40
41/// Authentication session
42#[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/// Credentials storage
54#[derive(Debug, Serialize, Deserialize)]
55struct CredentialsStore {
56	Credentials:HashMap<String, UserCredentials>,
57	FilePath:String,
58}
59
60/// User credentials
61#[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/// Cryptographic keys
71#[derive(Debug)]
72struct CryptoKeys {
73	SigningKey:ring::signature::Ed25519KeyPair,
74	EncryptionKey:[u8; 32],
75}
76
77impl AuthenticationService {
78	/// Create a new authentication service
79	pub async fn new(AppState:Arc<ApplicationState>) -> Result<Self> {
80		let config = &AppState.Configuration.Authentication;
81
82		// Expand credentials path
83		let CredentialsPath = ConfigurationManager::ExpandPath(&config.CredentialsPath)?;
84
85		// Load or create credentials store
86		let CredentialsStore = Self::LoadCredentialsStore(&CredentialsPath).await?;
87
88		// Generate cryptographic keys
89		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		// Initialize service status
101		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	/// Authenticate a user
111	pub async fn AuthenticateUser(&self, Username:String, Password:String, Provider:String) -> Result<String> {
112		// Validate input
113		if Username.is_empty() || Password.is_empty() || Provider.is_empty() {
114			return Err(AirError::Authentication("Invalid authentication parameters".to_string()));
115		}
116
117		// Check credentials
118		let _UserCredentials = self.ValidateCredentials(&Username, &Password, &Provider).await?;
119
120		// Generate session token
121		let Token = self.GenerateSessionToken(&Username, &Provider).await?;
122
123		// Create session
124		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		// Store session
137		{
138			let mut Sessions = self.Sessions.write().await;
139			Sessions.insert(Session.SessionId.clone(), Session);
140		}
141
142		// Update credentials usage
143		self.UpdateCredentialsUsage(&Username, &Provider).await?;
144
145		Ok(Token)
146	}
147
148	/// Validate user credentials
149	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			// Verify password (in a real implementation, this would decrypt and verify)
160			// For now, we'll use a simple approach
161			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	/// Generate a session token
174	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		// Sign the payload
180		let Signature = CryptoKeys.SigningKey.sign(Payload.as_bytes());
181
182		// Encode token
183		let Token = URL_SAFE.encode(format!("{}:{}", Payload, URL_SAFE.encode(Signature.as_ref())));
184
185		Ok(Token)
186	}
187
188	/// Update credentials usage timestamp
189	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		// Save updated credentials
199		self.SaveCredentialsStore(&CredentialsStore).await?;
200
201		Ok(())
202	}
203
204	/// Encrypt password
205	#[allow(dead_code)]
206	async fn EncryptPassword(&self, Password:&str) -> Result<String> {
207		let CryptoKeys = self.CryptoKeys.lock().await;
208
209		// Use AES-256-GCM via ring::aead. Prefix nonce to ciphertext and base64 encode.
210		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		// Reserve space for tag
223		InOut.extend_from_slice(&[0u8; 16]); // AES_256_GCM tag length is 16 bytes
224
225		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		// Store nonce + ciphertext
230		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	/// Decrypt password
238	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	/// Load credentials store from file
270	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			// Create new credentials store
282			Ok(CredentialsStore { Credentials:HashMap::new(), FilePath:FilePath.to_string_lossy().to_string() })
283		}
284	}
285
286	/// Save credentials store to file
287	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		// Create directory if it doesn't exist
292		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	/// Generate cryptographic keys
308	fn GenerateCryptoKeys() -> Result<CryptoKeys> {
309		// Generate signing key
310		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		// Generate encryption key
318		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	/// Start background tasks
328	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	/// Background task for session cleanup
339	async fn BackgroundTask(&self) {
340		let mut Interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // 5 minutes
341
342		loop {
343			Interval.tick().await;
344
345			// Clean up expired sessions
346			self.CleanupExpiredSessions().await;
347
348			// Save credentials periodically
349			if let Err(E) = self.SaveCredentialsPeriodically().await {
350				dev_log!("lifecycle", "error: [Authentication] Failed to save credentials: {}", E);
351			}
352		}
353	}
354
355	/// Clean up expired sessions
356	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	/// Save credentials periodically
366	async fn SaveCredentialsPeriodically(&self) -> Result<()> {
367		let CredentialsStore = self.Credentials.lock().await;
368		self.SaveCredentialsStore(&CredentialsStore).await
369	}
370
371	/// Stop background tasks
372	pub async fn StopBackgroundTasks(&self) {
373		// Implementation for graceful shutdown
374		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}