use crate::{error::Result, log_print, log_verbose, wallet::WalletManager};
use colored::Colorize;
#[cfg(unix)]
fn validate_password_file_permissions(file_path: &str) -> Result<()> {
use std::os::unix::fs::MetadataExt;
unsafe extern "C" {
fn geteuid() -> u32;
}
let metadata = std::fs::metadata(file_path).map_err(|e| {
crate::error::QuantusError::Generic(format!(
"Failed to inspect password file '{file_path}': {e}"
))
})?;
if !metadata.is_file() {
return Err(crate::error::QuantusError::Generic(format!(
"Password file '{file_path}' is not a regular file"
)));
}
let effective_uid = unsafe { geteuid() };
if metadata.uid() != effective_uid {
return Err(crate::error::QuantusError::Generic(format!(
"Password file '{file_path}' must be owned by the current user"
)));
}
let mode = metadata.mode() & 0o777;
if mode & 0o077 != 0 {
return Err(crate::error::QuantusError::Generic(format!(
"Password file '{file_path}' must not be accessible by group or other users (mode {mode:o})"
)));
}
Ok(())
}
#[cfg(not(unix))]
fn validate_password_file_permissions(_file_path: &str) -> Result<()> {
Ok(())
}
fn reject_raw_cli_password(password: &Option<String>) -> Result<()> {
if password.is_some() {
return Err(crate::error::QuantusError::Generic(
"Passing wallet passwords with --password/-p is not supported; use --password-file, QUANTUS_WALLET_PASSWORD, or the interactive prompt".to_string(),
));
}
Ok(())
}
fn read_password_file(file_path: &str) -> Result<String> {
log_verbose!("🔑 Reading password from file: {}", file_path);
validate_password_file_permissions(file_path)?;
let mut raw = std::fs::read_to_string(file_path).map_err(|e| {
crate::error::QuantusError::Generic(format!(
"Failed to read password file '{file_path}': {e}"
))
})?;
let pwd = raw.trim().to_string();
crate::wallet::keystore::zeroize_string(&mut raw);
Ok(pwd)
}
pub fn env_wallet_password(wallet_name: &str) -> Option<String> {
password_from_env(wallet_name)
}
fn password_from_env(wallet_name: &str) -> Option<String> {
if let Ok(env_password) = std::env::var("QUANTUS_WALLET_PASSWORD") {
log_verbose!("🔑 Using password from QUANTUS_WALLET_PASSWORD environment variable");
return Some(env_password);
}
let wallet_env_var = format!("QUANTUS_WALLET_PASSWORD_{}", wallet_name.to_uppercase());
if let Ok(env_password) = std::env::var(&wallet_env_var) {
log_verbose!("🔑 Using password from {} environment variable", wallet_env_var);
return Some(env_password);
}
None
}
pub fn ensure_password_allowed(password: String, allow_empty: bool) -> Result<String> {
if password.is_empty() && !allow_empty {
return Err(crate::error::QuantusError::Generic(
"Empty wallet passwords are not allowed; provide a password via --password-file, QUANTUS_WALLET_PASSWORD, or the interactive prompt (use --allow-empty-password only for development wallets)".to_string(),
));
}
Ok(password)
}
pub fn confirm_new_password(first: &str, second: &str) -> Result<String> {
if first != second {
return Err(crate::error::QuantusError::Generic("Passwords do not match".to_string()));
}
Ok(first.to_string())
}
pub fn get_wallet_password(
wallet_name: &str,
password: Option<String>,
password_file: Option<String>,
) -> Result<String> {
reject_raw_cli_password(&password)?;
if let Some(file_path) = password_file {
return read_password_file(&file_path);
}
if let Some(env_password) = password_from_env(wallet_name) {
return Ok(env_password);
}
log_verbose!("🔑 Trying empty password first...");
let wallet_manager = WalletManager::new()?;
if wallet_manager.load_wallet(wallet_name, "").is_ok() {
log_verbose!("✅ Empty password works for wallet '{}'", wallet_name);
return Ok("".to_string());
}
get_password_from_user(&format!("Enter password for wallet '{wallet_name}'"))
}
pub fn get_new_wallet_password(
wallet_name: &str,
password: Option<String>,
password_file: Option<String>,
allow_empty: bool,
) -> Result<String> {
reject_raw_cli_password(&password)?;
if let Some(file_path) = password_file {
return ensure_password_allowed(read_password_file(&file_path)?, allow_empty);
}
if let Some(env_password) = password_from_env(wallet_name) {
return ensure_password_allowed(env_password, allow_empty);
}
if allow_empty {
log_verbose!("🔑 Creating wallet with explicitly allowed empty password");
return Ok(String::new());
}
let first =
get_password_from_user(&format!("Enter a password for new wallet '{wallet_name}'"))?;
let second = get_password_from_user("Confirm password")?;
let confirmed = confirm_new_password(&first, &second)?;
ensure_password_allowed(confirmed, allow_empty)
}
pub fn get_mnemonic_from_user() -> Result<String> {
log_print!("{}", "Please enter or paste your secret phrase:".bright_yellow());
let mut mnemonic = rpassword::read_password().map_err(|e| {
crate::error::QuantusError::Generic(format!("Failed to read secret phrase: {e}"))
})?;
let trimmed = mnemonic.trim().to_string();
crate::wallet::keystore::zeroize_string(&mut mnemonic);
Ok(trimmed)
}
pub fn get_password_from_user(prompt: &str) -> Result<String> {
log_print!("{}", prompt.bright_yellow());
let password = rpassword::read_password().map_err(|e| {
crate::error::QuantusError::Generic(format!("Failed to read password: {e}"))
})?;
Ok(password)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn get_wallet_password_rejects_cli_password_flag() {
let err = get_wallet_password("w", Some("secret".into()), None).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("--password"), "expected unsupported --password message, got: {msg}");
}
#[test]
fn get_new_wallet_password_rejects_cli_password_flag() {
let err = get_new_wallet_password("w", Some("secret".into()), None, false).unwrap_err();
assert!(err.to_string().contains("--password"));
}
#[test]
fn ensure_password_allowed_rejects_empty_without_opt_in() {
let err = ensure_password_allowed(String::new(), false).unwrap_err();
assert!(err.to_string().contains("--allow-empty-password"));
}
#[test]
fn ensure_password_allowed_accepts_empty_with_opt_in() {
assert_eq!(ensure_password_allowed(String::new(), true).unwrap(), "");
}
#[test]
fn confirm_new_password_requires_match() {
assert!(confirm_new_password("a", "b").is_err());
assert_eq!(confirm_new_password("same", "same").unwrap(), "same");
}
#[test]
fn get_new_wallet_password_allow_empty_without_other_sources() {
let pwd = get_new_wallet_password("brand-new-wallet", None, None, true).unwrap();
assert_eq!(pwd, "");
}
#[test]
#[serial]
fn get_new_wallet_password_uses_env_and_rejects_empty_env_without_opt_in() {
unsafe {
std::env::remove_var("QUANTUS_WALLET_PASSWORD");
std::env::remove_var("QUANTUS_WALLET_PASSWORD_ENVWALLET");
std::env::set_var("QUANTUS_WALLET_PASSWORD", "env-secret");
}
let pwd = get_new_wallet_password("envwallet", None, None, false).unwrap();
assert_eq!(pwd, "env-secret");
unsafe {
std::env::set_var("QUANTUS_WALLET_PASSWORD", "");
}
let err = get_new_wallet_password("envwallet", None, None, false).unwrap_err();
assert!(err.to_string().contains("--allow-empty-password"));
unsafe {
std::env::remove_var("QUANTUS_WALLET_PASSWORD");
}
}
#[cfg(unix)]
mod password_file_permissions {
use super::*;
use std::{fs, os::unix::fs::PermissionsExt};
fn write_password_file(mode: u32) -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("wallet-password.txt");
fs::write(&path, "correct horse battery staple\n").expect("write password file");
fs::set_permissions(&path, fs::Permissions::from_mode(mode))
.expect("set password file mode");
let path_str = path.to_string_lossy().into_owned();
(dir, path_str)
}
#[test]
fn rejects_group_or_world_readable_password_file() {
let (_dir, path) = write_password_file(0o644);
let err = validate_password_file_permissions(&path).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("must not be accessible by group or other"),
"expected restrictive-mode rejection, got: {msg}"
);
}
#[test]
fn accepts_owner_only_password_file() {
let (_dir, path) = write_password_file(0o600);
validate_password_file_permissions(&path)
.expect("owner-only password file owned by self should be accepted");
}
}
}