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::{AirError, ApplicationState::ApplicationState, Configuration::ConfigurationManager, Result, Utility};
16
17/// Authentication service implementation
18pub struct AuthenticationService {
19	/// Application state
20	AppState:Arc<ApplicationState>,
21
22	/// Active sessions
23	Sessions:Arc<RwLock<HashMap<String, AuthSession>>>,
24
25	/// Credentials storage
26	Credentials:Arc<Mutex<CredentialsStore>>,
27
28	/// Cryptographic keys
29	CryptoKeys:Arc<Mutex<CryptoKeys>>,
30	/// AEAD algorithm for encryption/decryption
31	AeadAlgo:&'static aead::Algorithm,
32}
33
34/// Authentication session
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AuthSession {
37	pub SessionId:String,
38	pub UserId:String,
39	pub Provider:String,
40	pub Token:String,
41	pub CreatedAt:DateTime<Utc>,
42	pub ExpiresAt:DateTime<Utc>,
43	pub IsValid:bool,
44}
45
46/// Credentials storage
47#[derive(Debug, Serialize, Deserialize)]
48struct CredentialsStore {
49	Credentials:HashMap<String, UserCredentials>,
50	FilePath:String,
51}
52
53/// User credentials
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct UserCredentials {
56	pub UserId:String,
57	pub Provider:String,
58	pub EncryptedPassword:String,
59	pub LastUsed:DateTime<Utc>,
60	pub IsValid:bool,
61}
62
63/// Cryptographic keys
64#[derive(Debug)]
65struct CryptoKeys {
66	SigningKey:ring::signature::Ed25519KeyPair,
67	EncryptionKey:[u8; 32],
68}
69
70impl AuthenticationService {
71	/// Create a new authentication service
72	pub async fn new(AppState:Arc<ApplicationState>) -> Result<Self> {
73		let config = &AppState.Configuration.Authentication;
74
75		// Expand credentials path
76		let CredentialsPath = ConfigurationManager::ExpandPath(&config.CredentialsPath)?;
77
78		// Load or create credentials store
79		let CredentialsStore = Self::LoadCredentialsStore(&CredentialsPath).await?;
80
81		// Generate cryptographic keys
82		let CryptoKeys = Self::GenerateCryptoKeys()?;
83		let AeadAlgo = &aead::AES_256_GCM;
84
85		let Service = Self {
86			AppState,
87			Sessions:Arc::new(RwLock::new(HashMap::new())),
88			Credentials:Arc::new(Mutex::new(CredentialsStore)),
89			CryptoKeys:Arc::new(Mutex::new(CryptoKeys)),
90			AeadAlgo,
91		};
92
93		// Initialize service status
94		Service
95			.AppState
96			.UpdateServiceStatus("authentication", crate::ApplicationState::ServiceStatus::Running)
97			.await
98			.map_err(|e| AirError::Authentication(e.to_string()))?;
99
100		Ok(Service)
101	}
102
103	/// Authenticate a user
104	pub async fn AuthenticateUser(&self, Username:String, Password:String, Provider:String) -> Result<String> {
105		// Validate input
106		if Username.is_empty() || Password.is_empty() || Provider.is_empty() {
107			return Err(AirError::Authentication("Invalid authentication parameters".to_string()));
108		}
109
110		// Check credentials
111		let _UserCredentials = self.ValidateCredentials(&Username, &Password, &Provider).await?;
112
113		// Generate session token
114		let Token = self.GenerateSessionToken(&Username, &Provider).await?;
115
116		// Create session
117		let SessionId = Utility::GenerateRequestId();
118		let Session = AuthSession {
119			SessionId,
120			UserId:Username.clone(),
121			Provider:Provider.clone(),
122			Token:Token.clone(),
123			CreatedAt:chrono::Utc::now(),
124			ExpiresAt:chrono::Utc::now()
125				+ chrono::Duration::hours(self.AppState.Configuration.Authentication.TokenExpirationHours as i64),
126			IsValid:true,
127		};
128
129		// Store session
130		{
131			let mut Sessions = self.Sessions.write().await;
132			Sessions.insert(Session.SessionId.clone(), Session);
133		}
134
135		// Update credentials usage
136		self.UpdateCredentialsUsage(&Username, &Provider).await?;
137
138		Ok(Token)
139	}
140
141	/// Validate user credentials
142	async fn ValidateCredentials(&self, Username:&str, Password:&str, Provider:&str) -> Result<UserCredentials> {
143		let CredentialsStore = self.Credentials.lock().await;
144
145		let Key = format!("{}:{}", Provider, Username);
146
147		if let Some(UserCredentials) = CredentialsStore.Credentials.get(&Key) {
148			if !UserCredentials.IsValid {
149				return Err(AirError::Authentication("Credentials are invalid".to_string()));
150			}
151
152			// Verify password (in a real implementation, this would decrypt and verify)
153			// For now, we'll use a simple approach
154			let DecryptedPassword = self.DecryptPassword(&UserCredentials.EncryptedPassword).await?;
155
156			if DecryptedPassword == Password {
157				Ok(UserCredentials.clone())
158			} else {
159				Err(AirError::Authentication("Invalid password".to_string()))
160			}
161		} else {
162			Err(AirError::Authentication("User not found".to_string()))
163		}
164	}
165
166	/// Generate a session token
167	async fn GenerateSessionToken(&self, Username:&str, Provider:&str) -> Result<String> {
168		let CryptoKeys = self.CryptoKeys.lock().await;
169
170		let Payload = format!("{}:{}:{}", Username, Provider, Utility::CurrentTimestamp());
171
172		// Sign the payload
173		let Signature = CryptoKeys.SigningKey.sign(Payload.as_bytes());
174
175		// Encode token
176		let Token = URL_SAFE.encode(format!("{}:{}", Payload, URL_SAFE.encode(Signature.as_ref())));
177
178		Ok(Token)
179	}
180
181	/// Update credentials usage timestamp
182	async fn UpdateCredentialsUsage(&self, Username:&str, Provider:&str) -> Result<()> {
183		let mut CredentialsStore = self.Credentials.lock().await;
184
185		let Key = format!("{}:{}", Provider, Username);
186
187		if let Some(UserCredentials) = CredentialsStore.Credentials.get_mut(&Key) {
188			UserCredentials.LastUsed = Utc::now();
189		}
190
191		// Save updated credentials
192		self.SaveCredentialsStore(&CredentialsStore).await?;
193
194		Ok(())
195	}
196
197	/// Encrypt password
198	#[allow(dead_code)]
199	async fn EncryptPassword(&self, Password:&str) -> Result<String> {
200		let CryptoKeys = self.CryptoKeys.lock().await;
201
202		// Use AES-256-GCM via ring::aead. Prefix nonce to ciphertext and base64 encode.
203		let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
204			.map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
205
206		let LessSafe = aead::LessSafeKey::new(UnboundKey);
207		let mut NonceBytes = [0u8; 12];
208		ring::rand::SystemRandom::new()
209			.fill(&mut NonceBytes)
210			.map_err(|e| AirError::Authentication(format!("Failed to generate nonce: {:?}", e)))?;
211
212		let Nonce = aead::Nonce::assume_unique_for_key(NonceBytes);
213
214		let mut InOut = Password.as_bytes().to_vec();
215		// Reserve space for tag
216		InOut.extend_from_slice(&[0u8; 16]); // AES_256_GCM tag length is 16 bytes
217
218		LessSafe
219			.seal_in_place_append_tag(Nonce, aead::Aad::empty(), &mut InOut)
220			.map_err(|e| AirError::Authentication(format!("Encryption failed: {:?}", e)))?;
221
222		// Store nonce + ciphertext
223		let mut Out = Vec::with_capacity(NonceBytes.len() + InOut.len());
224		Out.extend_from_slice(&NonceBytes);
225		Out.extend_from_slice(&InOut);
226
227		Ok(URL_SAFE.encode(&Out))
228	}
229
230	/// Decrypt password
231	async fn DecryptPassword(&self, EncryptedPassword:&str) -> Result<String> {
232		let CryptoKeys = self.CryptoKeys.lock().await;
233
234		let Data = URL_SAFE
235			.decode(EncryptedPassword)
236			.map_err(|e| AirError::Authentication(format!("Failed to decode password: {}", e)))?;
237
238		if Data.len() < 12 + aead::AES_256_GCM.tag_len() {
239			return Err(AirError::Authentication("Encrypted data too short".to_string()));
240		}
241
242		let (NonceBytes, CipherBytes) = Data.split_at(12);
243
244		let mut NonceArr = [0u8; 12];
245		NonceArr.copy_from_slice(&NonceBytes[0..12]);
246
247		let UnboundKey = aead::UnboundKey::new(&aead::AES_256_GCM, &CryptoKeys.EncryptionKey)
248			.map_err(|e| AirError::Authentication(format!("Failed to create AEAD key: {:?}", e)))?;
249
250		let LessSafe = aead::LessSafeKey::new(UnboundKey);
251		let Nonce = aead::Nonce::assume_unique_for_key(NonceArr);
252
253		let mut CipherVec = CipherBytes.to_vec();
254		let Plain = LessSafe
255			.open_in_place(Nonce, aead::Aad::empty(), &mut CipherVec)
256			.map_err(|e| AirError::Authentication(format!("Decryption failed: {:?}", e)))?;
257
258		String::from_utf8(Plain.to_vec())
259			.map_err(|e| AirError::Authentication(format!("Failed to decode password string: {}", e)))
260	}
261
262	/// Load credentials store from file
263	async fn LoadCredentialsStore(FilePath:&std::path::Path) -> Result<CredentialsStore> {
264		if FilePath.exists() {
265			let Content = tokio::fs::read_to_string(FilePath)
266				.await
267				.map_err(|e| AirError::Authentication(format!("Failed to read credentials file: {}", e)))?;
268
269			let Credentials:HashMap<String, UserCredentials> = serde_json::from_str(&Content)
270				.map_err(|e| AirError::Authentication(format!("Failed to parse credentials file: {}", e)))?;
271
272			Ok(CredentialsStore { Credentials, FilePath:FilePath.to_string_lossy().to_string() })
273		} else {
274			// Create new credentials store
275			Ok(CredentialsStore { Credentials:HashMap::new(), FilePath:FilePath.to_string_lossy().to_string() })
276		}
277	}
278
279	/// Save credentials store to file
280	async fn SaveCredentialsStore(&self, Store:&CredentialsStore) -> Result<()> {
281		let Content = serde_json::to_string_pretty(&Store.Credentials)
282			.map_err(|e| AirError::Authentication(format!("Failed to serialize credentials: {}", e)))?;
283
284		// Create directory if it doesn't exist
285		if let Some(Parent) = std::path::Path::new(&Store.FilePath).parent() {
286			tokio::fs::create_dir_all(Parent)
287				.await
288				.map_err(|e| AirError::Authentication(format!("Failed to create credentials directory: {}", e)))?;
289
290			tokio::fs::write(&Store.FilePath, Content)
291				.await
292				.map_err(|e| AirError::Authentication(format!("Failed to write credentials file: {}", e)))?;
293
294			Ok(())
295		} else {
296			Err(AirError::Authentication("Invalid file path - no parent directory".to_string()))
297		}
298	}
299
300	/// Generate cryptographic keys
301	fn GenerateCryptoKeys() -> Result<CryptoKeys> {
302		// Generate signing key
303		let Rng = ring::rand::SystemRandom::new();
304		let Pkcs8Bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&Rng)
305			.map_err(|e| AirError::Authentication(format!("Failed to generate signing key: {}", e)))?;
306
307		let SigningKey = ring::signature::Ed25519KeyPair::from_pkcs8(Pkcs8Bytes.as_ref())
308			.map_err(|e| AirError::Authentication(format!("Failed to load signing key: {}", e)))?;
309
310		// Generate encryption key
311		let mut EncryptionKey = [0u8; 32];
312		ring::rand::SystemRandom::new()
313			.fill(&mut EncryptionKey)
314			.map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))
315			.map_err(|e| AirError::Authentication(format!("Failed to generate encryption key: {}", e)))?;
316
317		Ok(CryptoKeys { SigningKey, EncryptionKey })
318	}
319
320	/// Start background tasks
321	pub async fn StartBackgroundTasks(&self) -> Result<tokio::task::JoinHandle<()>> {
322		let Service = self.clone();
323
324		let Handle = tokio::spawn(async move {
325			Service.BackgroundTask().await;
326		});
327
328		Ok(Handle)
329	}
330
331	/// Background task for session cleanup
332	async fn BackgroundTask(&self) {
333		let mut Interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // 5 minutes
334
335		loop {
336			Interval.tick().await;
337
338			// Clean up expired sessions
339			self.CleanupExpiredSessions().await;
340
341			// Save credentials periodically
342			if let Err(E) = self.SaveCredentialsPeriodically().await {
343				log::error!("[Authentication] Failed to save credentials: {}", E);
344			}
345		}
346	}
347
348	/// Clean up expired sessions
349	async fn CleanupExpiredSessions(&self) {
350		let Now = Utc::now();
351		let mut Sessions = self.Sessions.write().await;
352
353		Sessions.retain(|_, Session| Session.ExpiresAt > Now && Session.IsValid);
354
355		log::debug!("[Authentication] Cleaned up expired sessions");
356	}
357
358	/// Save credentials periodically
359	async fn SaveCredentialsPeriodically(&self) -> Result<()> {
360		let CredentialsStore = self.Credentials.lock().await;
361		self.SaveCredentialsStore(&CredentialsStore).await
362	}
363
364	/// Stop background tasks
365	pub async fn StopBackgroundTasks(&self) {
366		// Implementation for graceful shutdown
367		log::info!("[Authentication] Stopping background tasks");
368	}
369}
370
371impl Clone for AuthenticationService {
372	fn clone(&self) -> Self {
373		Self {
374			AppState:self.AppState.clone(),
375			Sessions:self.Sessions.clone(),
376			Credentials:self.Credentials.clone(),
377			CryptoKeys:self.CryptoKeys.clone(),
378			AeadAlgo:self.AeadAlgo,
379		}
380	}
381}