1use crate::startup;
9use crate::utility::config_schema::SerializableConfig;
10use crate::utility::std::{press_enter_to_continue, typewriter_println};
11use aes_gcm::{
12 Aes256Gcm, Nonce,
13 aead::{Aead, AeadCore, KeyInit},
14};
15use colored::Colorize;
16use dialoguer::Password;
17use rand_core::OsRng;
18use rand_core::RngCore;
19use sha2::Digest;
20use std::{
21 fs::{self, File},
22 io::Write,
23 path::Path,
24};
25use zeroize::Zeroizing;
26use zxcvbn::{Score, zxcvbn};
27
28fn generate_random_bytes() -> [u8; 32] {
29 let mut bytes = [0u8; 32];
30 OsRng.fill_bytes(&mut bytes);
31 bytes
32}
33
34pub const SECRETS_PATH: &str = "secrets.enc";
35pub const SECRETS_BACKUP_PATH: &str = "backups/secrets.enc.backup";
36pub const SECRETS_CHECKSUM_PATH: &str = "backups/secrets.enc.sha256";
37
38fn derive_key(passphrase: &str, salt: &[u8; 32]) -> Zeroizing<[u8; 32]> {
50 use argon2::{Algorithm, Params, Version};
51 let argon2 = argon2::Argon2::new(
52 Algorithm::Argon2id,
53 Version::V0x13,
54 Params::new(65536, 3, 1, Some(32)).unwrap(),
55 );
56 let mut key = Zeroizing::new([0u8; 32]);
57 argon2
58 .hash_password_into(passphrase.as_bytes(), salt, &mut *key)
59 .expect("Failed to derive key");
60 key
61}
62
63fn encrypt(data: &[u8], passphrase: &str) -> Result<Vec<u8>, String> {
72 let salt = generate_random_bytes();
73 let key = derive_key(passphrase, &salt);
74 let cipher = Aes256Gcm::new_from_slice(&*key).unwrap();
75 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
76 cipher
77 .encrypt(&nonce, data)
78 .map(|mut ciphertext| {
79 let mut result = Vec::with_capacity(32 + 12 + ciphertext.len());
80 result.extend_from_slice(&salt);
81 result.extend_from_slice(nonce.as_slice());
82 result.append(&mut ciphertext);
83 result
84 })
85 .map_err(|e| format!("Encryption failed: {}", e))
86}
87
88fn decrypt(encrypted_data: &[u8], passphrase: &str) -> Result<Vec<u8>, String> {
97 if encrypted_data.len() < 44 {
98 return Err("Encrypted data too short".to_string());
99 }
100 let (salt_bytes, rest) = encrypted_data.split_at(32);
101 let (nonce_bytes, ciphertext) = rest.split_at(12);
102 let salt: [u8; 32] = salt_bytes.try_into().map_err(|_| "Invalid salt length")?;
103 let nonce = Nonce::from_slice(nonce_bytes);
104 let key = derive_key(passphrase, &salt);
105 let cipher = Aes256Gcm::new_from_slice(&*key).unwrap();
106 cipher
107 .decrypt(nonce, ciphertext)
108 .map_err(|e| format!("Decryption failed: {}", e))
109}
110
111pub fn save_encrypted_config(config: &SerializableConfig, passphrase: &str) -> Result<(), String> {
137 let json = serde_json::to_vec(config).map_err(|e| format!("Serialization failed: {}", e))?;
138 let encrypted = encrypt(&json, passphrase)?;
139
140 let mut hasher = sha2::Sha256::new();
142 hasher.update(&encrypted);
143 let checksum = hex::encode(hasher.finalize());
144
145 std::fs::create_dir_all("backups")
147 .map_err(|e| format!("Failed to create backups directory: {}", e))?;
148
149 let mut backup =
151 File::create(SECRETS_BACKUP_PATH).map_err(|e| format!("Failed to create backup: {}", e))?;
152 backup
153 .write_all(&encrypted)
154 .map_err(|e| format!("Failed to write backup: {}", e))?;
155
156 fs::write(SECRETS_CHECKSUM_PATH, &checksum)
158 .map_err(|e| format!("Failed to write checksum: {}", e))?;
159
160 let mut file =
162 File::create(SECRETS_PATH).map_err(|e| format!("Failed to create secrets.enc: {}", e))?;
163 file.write_all(&encrypted)
164 .map_err(|e| format!("Failed to write secrets.enc: {}", e))?;
165
166 Ok(())
167}
168
169pub fn load_encrypted_config(passphrase: &str) -> Result<SerializableConfig, String> {
182 let data = fs::read(SECRETS_PATH)
183 .or_else(|_| fs::read(SECRETS_BACKUP_PATH))
184 .map_err(|_| "secrets.enc not found.".to_string())?;
185
186 if let Ok(stored_checksum) = fs::read_to_string(SECRETS_CHECKSUM_PATH) {
188 let mut hasher = sha2::Sha256::new();
189 hasher.update(&data);
190 let computed = hex::encode(hasher.finalize());
191
192 if computed != stored_checksum.trim() {
193 let backup_data = fs::read(SECRETS_BACKUP_PATH)
195 .map_err(|_| "Primary file corrupted and backup missing.".to_string())?;
196 let decrypted = decrypt(&backup_data, passphrase)?;
197 return serde_json::from_slice(&decrypted)
198 .map_err(|e| format!("Failed to deserialize config: {}", e));
199 }
200 }
201
202 let decrypted = decrypt(&data, passphrase)?;
203 serde_json::from_slice(&decrypted).map_err(|e| format!("Failed to deserialize config: {}", e))
204}
205
206pub fn secrets_exist() -> bool {
220 Path::new(SECRETS_PATH).exists()
221}
222
223fn map_dialoguer_error(e: dialoguer::Error) -> String {
231 format!("Error: {:?}", e)
232}
233
234pub fn run_setup() -> Result<SerializableConfig, String> {
248 if !cfg!(debug_assertions) {
249 typewriter_println(&format!(
250 "{}",
251 "\nWelcome to Quorum Server Setup!".cyan().bold()
252 ))
253 .map_err(|e| e.to_string())?;
254 println!();
255
256 typewriter_println(&format!(
257 "{}",
258 "Your server configuration will be encrypted with a passphrase.".dimmed()
259 ))
260 .map_err(|e| e.to_string())?;
261 typewriter_println(&format!(
262 "{}",
263 "You'll need this passphrase every time you start the server.".dimmed()
264 ))
265 .map_err(|e| e.to_string())?;
266 println!();
267
268 press_enter_to_continue(false, false);
269 print!("\x1B[2J\x1B[1;1H");
270 startup::print_banner();
271 }
272
273 Ok(SerializableConfig {
274 server_port: 3000,
275 server_host: "127.0.0.1".to_string(),
276 server_url: "http://127.0.0.1:3000".to_string(),
277 surreal_data_path: "./data/db".to_string(),
278 surreal_ns: "quorum".to_string(),
279 surreal_db: "quorum".to_string(),
280 jwt_secret: hex::encode(generate_random_bytes()),
281 jwt_access_minutes: 15,
282 jwt_refresh_days: 7,
283 enable_testing: false,
284 default_per_second: 100,
285 default_burst_size: 200,
286 testing_per_second: 1000,
287 testing_burst_size: 2000,
288 })
289}
290
291pub fn prompt_passphrase() -> Result<String, String> {
301 Password::new()
302 .with_prompt("Enter server passphrase")
303 .interact()
304 .map_err(map_dialoguer_error)
305}
306
307pub fn prompt_passphrase_new() -> Result<String, String> {
308 loop {
309 let passphrase = Password::new()
310 .with_prompt("Enter server passphrase")
311 .with_confirmation("Confirm passphrase", "Passphrases do not match")
312 .interact()
313 .map_err(map_dialoguer_error)?;
314
315 print!("\x1B[2J\x1B[1;1H");
316 startup::print_banner();
317
318 match validate_passphrase(&passphrase) {
319 Ok(()) => return Ok(passphrase),
320 Err(err) => eprintln!("{}", err.red()),
321 }
322 }
323}
324
325pub fn verify_admin_credentials(
326 username: &str,
327 password: &str,
328 stored_username: &str,
329 stored_hash: &str,
330) -> Result<(), String> {
331 use argon2::{
332 Algorithm, Argon2, Params, Version,
333 password_hash::{PasswordHash, PasswordVerifier},
334 };
335
336 if username != stored_username {
337 return Err("Invalid credentials".to_string());
338 }
339
340 let params = Params::new(65536, 3, 1, Some(32)).map_err(|e| e.to_string())?;
341 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
342
343 let parsed_hash = PasswordHash::new(stored_hash)
344 .map_err(|e| format!("Failed to parse stored hash: {}", e))?;
345
346 argon2
347 .verify_password(password.as_bytes(), &parsed_hash)
348 .map_err(|_| "Invalid credentials".to_string())
349}
350
351pub fn validate_passphrase(passphrase: &str) -> Result<(), String> {
352 let len = passphrase.chars().count();
353
354 if len < 12 {
355 return Err("Use at least 12 characters.".into());
356 }
357
358 if len > 64 {
359 return Err("Passphrase is too long for this field.".into());
360 }
361
362 let estimate = zxcvbn(passphrase, &[]);
363
364 if estimate.score() < Score::Three {
365 if let Some(feedback) = estimate.feedback() {
366 let warning = feedback
367 .warning()
368 .map(|w| w.to_string())
369 .unwrap_or_default();
370
371 let suggestions = feedback
372 .suggestions()
373 .iter()
374 .map(|s| s.to_string())
375 .collect::<Vec<_>>()
376 .join(" ");
377
378 let message = format!("{} {}", warning, suggestions).trim().to_string();
379
380 if message.is_empty() {
381 return Err("Passphrase is too weak.".into());
382 }
383
384 return Err(format!("Passphrase is too weak. {message}"));
385 }
386
387 return Err("Passphrase is too weak.".into());
388 }
389
390 Ok(())
391}