Skip to main content

quorum_core/utility/
secrets.rs

1//! This file includes everything for server configuration setup and hashing.
2//!
3//! It includes functions for encrypting and decrypting the server configuration
4//! using AES-256-GCM, as well as functions for saving and loading the encrypted configuration
5//! from a file. It also provides a setup process that prompts the user
6//! for necessary configurations and generates secure defaults.
7
8use 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
38// SerializableConfig is now generated by the config_schema macro
39// Import it from config_schema instead of defining it here
40
41/// Derives a key from the given passphrase and salt using Argon2id.
42///
43/// # Arguments
44/// * `passphrase` - The passphrase to derive the key from.
45/// * `salt` - A 32-byte salt used in the key derivation process.
46///
47/// # Returns
48/// A `Zeroizing<[u8; 32]>` containing the derived key. The
49fn 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
63/// Encrypts the given data using AES-256-GCM with a key derived from the provided passphrase.
64///
65/// # Arguments
66/// * `data` - The plaintext data to encrypt.
67/// * `passphrase` - The passphrase used to derive the encryption key.
68///
69/// # Returns
70/// A `Result` containing the encrypted data as a `Vec<u8>` on success, or an error message as a `String` on failure.
71fn 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
88/// Decrypts the given encrypted data using AES-256-GCM with a key derived from the provided passphrase.
89///
90/// # Arguments
91/// * `encrypted_data` - The encrypted data to decrypt, which should include the salt and nonce.
92/// * `passphrase` - The passphrase used to derive the decryption key.
93///
94/// # Returns
95/// A `Result` containing the decrypted data as a `Vec<u8>` on success, or an error message as a `String` on failure.
96fn 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
111/// Saves the given configuration to an encrypted file using the provided passphrase.
112///
113/// Encrypts the config and writes it to both the primary file (`secrets.enc`) and a backup
114/// (`backups/secrets.enc.backup`). The backup directory is created automatically if it doesn't exist.
115/// The backup is written first to ensure a valid copy exists even if the primary write fails.
116///
117/// # Arguments
118/// * `config` - The configuration to save.
119/// * `passphrase` - The passphrase used to encrypt the configuration.
120///
121/// # Returns
122/// A `Result` indicating success or failure, with an error message as a `String` on failure.
123///
124/// # Errors
125/// Returns an error if:
126/// - Serialization of the config fails
127/// - Encryption fails
128/// - The backups directory cannot be created
129/// - Either the backup or primary file cannot be written
130///
131/// # Example
132/// ```rust
133/// let config = SerializableConfig { ... };
134/// save_encrypted_config(&config, "my_secure_passphrase").expect("Failed to save encrypted config");
135/// ```
136pub 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    // Calculate checksum
141    let mut hasher = sha2::Sha256::new();
142    hasher.update(&encrypted);
143    let checksum = hex::encode(hasher.finalize());
144
145    // Ensure backup directory exists
146    std::fs::create_dir_all("backups")
147        .map_err(|e| format!("Failed to create backups directory: {}", e))?;
148
149    // Write backup secrets file
150    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    // Write checksum
157    fs::write(SECRETS_CHECKSUM_PATH, &checksum)
158        .map_err(|e| format!("Failed to write checksum: {}", e))?;
159
160    // Write primary secrets file
161    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
169/// Loads and decrypts the configuration from the encrypted file using the provided passphrase.
170///
171/// # Arguments
172/// * `passphrase` - The passphrase used to decrypt the configuration.
173///
174/// # Returns
175/// A `Result` containing the decrypted configuration as a `SerializableConfig` on success, or an error message as a `String` on failure.
176///
177/// # Example
178/// ```rust
179/// let config = load_encrypted_config("my_secure_passphrase").expect("Failed to load encrypted config");
180/// ```
181pub 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    // Verify checksum if it exists
187    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            // Primary is corrupted, try backup
194            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
206/// Checks if the encrypted secrets file exists.
207///
208/// # Returns
209/// `true` if the secrets file exists, `false` otherwise.
210///
211/// # Example
212/// ```rust
213/// if secrets_exist() {
214///     println!("Secrets file exists.");
215/// } else {
216///     println!("Secrets file does not exist.");
217/// }
218/// ```
219pub fn secrets_exist() -> bool {
220    Path::new(SECRETS_PATH).exists()
221}
222
223/// Helper function that maps a `dialoguer::Error` to a `String` for easier error handling.
224///
225/// # Arguments
226/// * `e` - The `dialoguer::Error` to map.
227///
228/// # Returns
229/// A `String` containing the error message.
230fn map_dialoguer_error(e: dialoguer::Error) -> String {
231    format!("Error: {:?}", e)
232}
233
234/// Runs setup process
235///
236/// This runs the setup process for the Quorum server,
237/// prompting the user for necessary configurations and returning
238/// a `SerializableConfig` object.
239///
240/// # Returns
241/// A `Result` containing the `SerializableConfig` on success, or an error message as a `String` on failure.
242///
243/// # Example
244/// ```rust
245/// let config = run_setup().expect("Failed to run setup");
246/// ```
247pub 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
291/// Prompts the user for a passphrase to encrypt/decrypt the server configuration.
292///
293/// # Returns
294/// A `Result` containing the entered passphrase as a `String` on success, or an error message as a `String` on failure.
295///
296/// # Example
297/// ```rust
298/// let passphrase = prompt_passphrase().expect("Failed to get passphrase");
299/// ```
300pub 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}