use serde::Deserialize;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process;
#[derive(Debug, Deserialize)]
struct Config {
access_level_type: String,
#[serde(rename = "users")]
users: Vec<UserConfig>,
}
#[derive(Debug, Deserialize)]
struct UserConfig {
username: String,
password: String,
level: String,
}
struct GeneratedUser {
username: String,
password_hash: [u8; 32],
salt: [u8; 16],
level: String,
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <credentials.toml>", args[0]);
eprintln!();
eprintln!("Generates Rust code with pre-hashed credentials from TOML configuration.");
eprintln!();
eprintln!("Example credentials.toml:");
eprintln!(" access_level_type = \"my_crate::AccessLevel\"");
eprintln!();
eprintln!(" [[users]]");
eprintln!(" username = \"admin\"");
eprintln!(" password = \"secret123\"");
eprintln!(" level = \"Admin\"");
process::exit(1);
}
let input_path = PathBuf::from(&args[1]);
let toml_content = fs::read_to_string(&input_path).unwrap_or_else(|e| {
eprintln!("Error reading {}: {}", input_path.display(), e);
process::exit(1);
});
let config: Config = toml::from_str(&toml_content).unwrap_or_else(|e| {
eprintln!("Error parsing TOML: {}", e);
process::exit(1);
});
if let Err(e) = validate_config(&config) {
eprintln!("Configuration error: {}", e);
process::exit(1);
}
let users = generate_users(&config.users).unwrap_or_else(|e| {
eprintln!("Error generating credentials: {}", e);
process::exit(1);
});
let code = generate_code(&config.access_level_type, &users);
println!("{}", code);
}
fn validate_config(config: &Config) -> Result<(), String> {
if config.access_level_type.is_empty() {
return Err("access_level_type cannot be empty".into());
}
if config.users.is_empty() {
return Err("at least one user must be defined".into());
}
for user in &config.users {
if user.username.is_empty() {
return Err("username cannot be empty".into());
}
if user.password.is_empty() {
return Err("password cannot be empty".into());
}
if user.level.is_empty() {
return Err("access level cannot be empty".into());
}
}
let mut usernames = std::collections::HashSet::new();
for user in &config.users {
if !usernames.insert(&user.username) {
return Err(format!("duplicate username: {}", user.username));
}
}
Ok(())
}
fn generate_users(configs: &[UserConfig]) -> Result<Vec<GeneratedUser>, String> {
configs
.iter()
.map(|cfg| {
let salt = generate_salt();
let hash = hash_password(&cfg.password, &salt)?;
Ok(GeneratedUser {
username: cfg.username.clone(),
password_hash: hash,
salt,
level: cfg.level.clone(),
})
})
.collect()
}
fn generate_salt() -> [u8; 16] {
let mut salt = [0u8; 16];
getrandom::fill(&mut salt).expect("Failed to generate random salt");
salt
}
fn hash_password(password: &str, salt: &[u8; 16]) -> Result<[u8; 32], String> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(password.as_bytes());
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
Ok(hash)
}
fn generate_code(access_level_type: &str, users: &[GeneratedUser]) -> String {
let type_name = access_level_type
.split("::")
.last()
.unwrap_or(access_level_type);
let mut output = String::new();
output.push_str("// Generated by nut-shell-credgen - DO NOT EDIT\n");
output.push_str("// This file contains hashed credentials compiled at build time.\n");
output.push_str("//\n");
output.push_str("// WARNING: Credentials are visible in the binary.\n");
output.push_str("// Only use in production if you understand the security implications.\n");
output.push('\n');
output.push_str("use nut_shell::auth::{ConstCredentialProvider, Sha256Hasher, User};\n");
output.push_str(&format!("use {};\n\n", access_level_type));
output.push_str("/// Build-time credential provider with pre-hashed credentials.\n");
output.push_str(&format!(
"pub type BuildTimeProvider = ConstCredentialProvider<{}, Sha256Hasher, {}>;\n\n",
type_name,
users.len()
));
output.push_str("/// Create the build-time credential provider with pre-hashed credentials.\n");
output.push_str("pub fn create_provider() -> BuildTimeProvider {\n");
output.push_str(" // Pre-hashed credentials generated at build time\n");
output.push_str(" let users = [\n");
for user in users {
output.push_str(" User::new(\n");
output.push_str(&format!(" \"{}\",\n", user.username));
output.push_str(&format!(" {}::{},\n", type_name, user.level));
output.push_str(&format!(
" {},\n",
format_byte_array(&user.password_hash)
));
output.push_str(&format!(" {},\n", format_byte_array(&user.salt)));
output.push_str(" ).unwrap(),\n");
}
output.push_str(" ];\n\n");
output.push_str(" ConstCredentialProvider::new(users, Sha256Hasher)\n");
output.push_str("}\n");
output
}
fn format_byte_array(bytes: &[u8]) -> String {
let hex_bytes: Vec<String> = bytes.iter().map(|b| format!("0x{:02x}", b)).collect();
format!("[{}]", hex_bytes.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_config_valid() {
let config = Config {
access_level_type: "my_crate::Level".into(),
users: vec![UserConfig {
username: "admin".into(),
password: "pass".into(),
level: "Admin".into(),
}],
};
assert!(validate_config(&config).is_ok());
}
#[test]
fn test_validate_config_empty_access_level_type() {
let config = Config {
access_level_type: "".into(),
users: vec![UserConfig {
username: "admin".into(),
password: "pass".into(),
level: "Admin".into(),
}],
};
assert!(validate_config(&config).is_err());
}
#[test]
fn test_validate_config_no_users() {
let config = Config {
access_level_type: "Foo::Bar".into(),
users: vec![],
};
assert!(validate_config(&config).is_err());
}
#[test]
fn test_validate_config_empty_username() {
let config = Config {
access_level_type: "Foo::Bar".into(),
users: vec![UserConfig {
username: "".into(),
password: "pass".into(),
level: "Admin".into(),
}],
};
assert!(validate_config(&config).is_err());
}
#[test]
fn test_validate_config_duplicate_username() {
let config = Config {
access_level_type: "Foo::Bar".into(),
users: vec![
UserConfig {
username: "admin".into(),
password: "pass1".into(),
level: "Admin".into(),
},
UserConfig {
username: "admin".into(),
password: "pass2".into(),
level: "User".into(),
},
],
};
assert!(validate_config(&config).is_err());
}
#[test]
fn test_format_byte_array() {
let bytes = [0x00, 0xff, 0x42];
assert_eq!(format_byte_array(&bytes), "[0x00, 0xff, 0x42]");
}
#[test]
fn test_extract_type_name() {
let full_path = "my_crate::auth::Level";
let name = full_path.split("::").last().unwrap();
assert_eq!(name, "Level");
}
#[test]
fn test_hash_password() {
let salt = [1u8; 16];
let hash = hash_password("test123", &salt).unwrap();
assert_eq!(hash.len(), 32);
let hash2 = hash_password("test123", &salt).unwrap();
assert_eq!(hash, hash2);
let hash3 = hash_password("different", &salt).unwrap();
assert_ne!(hash, hash3);
}
}