1use crate::AuthBackend;
9use anyhow::{anyhow, Context, Result};
10use async_trait::async_trait;
11use rusmes_proto::Username;
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::fs;
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17use tokio::sync::RwLock;
18
19pub struct FileAuthBackend {
21 file_path: PathBuf,
22 users: Arc<RwLock<HashMap<String, String>>>,
23}
24
25impl FileAuthBackend {
26 pub async fn new(file_path: impl AsRef<Path>) -> Result<Self> {
31 let file_path = file_path.as_ref().to_path_buf();
32 let users = Self::load_users(&file_path).await?;
33
34 Ok(Self {
35 file_path,
36 users: Arc::new(RwLock::new(users)),
37 })
38 }
39
40 async fn load_users(file_path: &Path) -> Result<HashMap<String, String>> {
42 if !file_path.exists() {
44 if let Some(parent) = file_path.parent() {
45 fs::create_dir_all(parent)
46 .await
47 .context("Failed to create parent directory")?;
48 }
49 fs::File::create(file_path)
50 .await
51 .context("Failed to create password file")?;
52 return Ok(HashMap::new());
53 }
54
55 let mut file = fs::File::open(file_path)
56 .await
57 .context("Failed to open password file")?;
58 let mut contents = String::new();
59 file.read_to_string(&mut contents)
60 .await
61 .context("Failed to read password file")?;
62
63 let mut users = HashMap::new();
64 for (line_num, line) in contents.lines().enumerate() {
65 let line = line.trim();
66 if line.is_empty() || line.starts_with('#') {
67 continue;
68 }
69
70 let parts: Vec<&str> = line.splitn(2, ':').collect();
71 if parts.len() != 2 {
72 return Err(anyhow!(
73 "Invalid format on line {}: expected 'username:hash'",
74 line_num + 1
75 ));
76 }
77
78 let username = parts[0].to_string();
79 let hash = parts[1].to_string();
80
81 if username.is_empty() {
82 return Err(anyhow!("Empty username on line {}", line_num + 1));
83 }
84
85 if !hash.starts_with("$2b$") && !hash.starts_with("$2a$") && !hash.starts_with("$2y$") {
86 return Err(anyhow!(
87 "Invalid bcrypt hash on line {}: hash must start with $2a$, $2b$, or $2y$",
88 line_num + 1
89 ));
90 }
91
92 users.insert(username, hash);
93 }
94
95 Ok(users)
96 }
97
98 async fn save_users(&self, users: &HashMap<String, String>) -> Result<()> {
100 let mut contents = String::new();
101 let mut usernames: Vec<&String> = users.keys().collect();
102 usernames.sort();
103
104 for username in usernames {
105 let hash = &users[username];
106 contents.push_str(&format!("{}:{}\n", username, hash));
107 }
108
109 let temp_path = self.file_path.with_extension("tmp");
111 let mut file = fs::File::create(&temp_path)
112 .await
113 .context("Failed to create temporary file")?;
114 file.write_all(contents.as_bytes())
115 .await
116 .context("Failed to write to temporary file")?;
117 file.sync_all()
118 .await
119 .context("Failed to sync temporary file")?;
120 drop(file);
121
122 fs::rename(&temp_path, &self.file_path)
123 .await
124 .context("Failed to rename temporary file")?;
125
126 Ok(())
127 }
128
129 fn hash_password(password: &str) -> Result<String> {
131 bcrypt::hash(password, bcrypt::DEFAULT_COST).context("Failed to hash password")
132 }
133
134 fn verify_password(password: &str, hash: &str) -> Result<bool> {
136 bcrypt::verify(password, hash).context("Failed to verify password")
137 }
138}
139
140#[async_trait]
141impl AuthBackend for FileAuthBackend {
142 async fn authenticate(&self, username: &Username, password: &str) -> Result<bool> {
143 let users = self.users.read().await;
144
145 if let Some(hash) = users.get(username.as_str()) {
146 Self::verify_password(password, hash)
147 } else {
148 let _ = bcrypt::verify(
150 password,
151 "$2b$12$dummy_hash_to_prevent_timing_attack_00000000000000000000000000000",
152 );
153 Ok(false)
154 }
155 }
156
157 async fn verify_identity(&self, username: &Username) -> Result<bool> {
158 let users = self.users.read().await;
159 Ok(users.contains_key(username.as_str()))
160 }
161
162 async fn list_users(&self) -> Result<Vec<Username>> {
163 let users = self.users.read().await;
164 let mut usernames = Vec::new();
165
166 for username_str in users.keys() {
167 let username = Username::new(username_str.clone()).context(format!(
168 "Invalid username in password file: {}",
169 username_str
170 ))?;
171 usernames.push(username);
172 }
173
174 usernames.sort_by(|a, b| a.as_str().cmp(b.as_str()));
175 Ok(usernames)
176 }
177
178 async fn create_user(&self, username: &Username, password: &str) -> Result<()> {
179 let mut users = self.users.write().await;
180
181 if users.contains_key(username.as_str()) {
182 return Err(anyhow!("User '{}' already exists", username.as_str()));
183 }
184
185 let hash = Self::hash_password(password)?;
186 users.insert(username.as_str().to_string(), hash);
187
188 self.save_users(&users).await?;
189
190 Ok(())
191 }
192
193 async fn delete_user(&self, username: &Username) -> Result<()> {
194 let mut users = self.users.write().await;
195
196 if !users.contains_key(username.as_str()) {
197 return Err(anyhow!("User '{}' does not exist", username.as_str()));
198 }
199
200 users.remove(username.as_str());
201 self.save_users(&users).await?;
202
203 Ok(())
204 }
205
206 async fn change_password(&self, username: &Username, new_password: &str) -> Result<()> {
207 let mut users = self.users.write().await;
208
209 if !users.contains_key(username.as_str()) {
210 return Err(anyhow!("User '{}' does not exist", username.as_str()));
211 }
212
213 let hash = Self::hash_password(new_password)?;
214 users.insert(username.as_str().to_string(), hash);
215
216 self.save_users(&users).await?;
217
218 Ok(())
219 }
220}