use crate::AuthAction;
use anyhow::{Context, Result};
use krab_core::credentials::hash_password;
use std::io::{IsTerminal as _, Read as _};
pub(super) fn dispatch_auth_action(action: &AuthAction) -> Result<()> {
match action {
AuthAction::HashPassword { password, username } => {
run_hash_password(password.as_deref(), username.as_deref())
}
}
}
fn run_hash_password(password: Option<&str>, username: Option<&str>) -> Result<()> {
let password = match password {
Some(value) => value.to_string(),
None => read_password_from_stdin()?,
};
anyhow::ensure!(
!password.trim().is_empty(),
"refusing to hash an empty password"
);
let hash = hash_password(&password)?;
match username {
Some(username) => println!("{}", serde_json::json!({ username: hash })),
None => println!("{hash}"),
}
Ok(())
}
fn read_password_from_stdin() -> Result<String> {
let mut stdin = std::io::stdin();
if let Some(prompt) = stdin_prompt(stdin.is_terminal()) {
eprintln!("{prompt}");
}
let mut buffer = String::new();
stdin
.read_to_string(&mut buffer)
.context("failed to read password from stdin")?;
Ok(trim_one_line_ending(&buffer).to_string())
}
fn stdin_prompt(is_terminal: bool) -> Option<String> {
if !is_terminal {
return None;
}
let eof_keys = if cfg!(windows) {
"Ctrl-Z then Enter"
} else {
"Ctrl-D"
};
Some(format!(
"Enter password (input is not hidden), then press {eof_keys}:"
))
}
fn trim_one_line_ending(value: &str) -> &str {
match value.strip_suffix('\n') {
Some(rest) => rest.strip_suffix('\r').unwrap_or(rest),
None => value,
}
}
#[cfg(test)]
mod tests {
use super::{stdin_prompt, trim_one_line_ending};
use krab_core::credentials::{hash_password, is_valid_password_hash, verify_password};
#[test]
fn only_one_line_ending_is_trimmed_and_spaces_survive() {
assert_eq!(trim_one_line_ending("pw\n"), "pw");
assert_eq!(trim_one_line_ending("pw\r\n"), "pw");
assert_eq!(trim_one_line_ending("pw"), "pw");
assert_eq!(trim_one_line_ending("pw \n"), "pw ");
assert_eq!(trim_one_line_ending("pw\n\n"), "pw\n");
}
#[test]
fn prompt_appears_only_for_interactive_stdin() {
assert_eq!(stdin_prompt(false), None);
let prompt = stdin_prompt(true).expect("a terminal operator gets a prompt");
assert!(prompt.contains("password"));
let eof_keys = if cfg!(windows) { "Ctrl-Z" } else { "Ctrl-D" };
assert!(prompt.contains(eof_keys), "prompt was: {prompt}");
}
#[test]
fn hashed_output_is_a_verifiable_phc_string() {
let hash = hash_password("operator-supplied").unwrap();
assert!(is_valid_password_hash(&hash));
assert!(verify_password("operator-supplied", &hash).unwrap());
assert!(!verify_password("something-else", &hash).unwrap());
}
#[test]
fn username_form_emits_a_parseable_single_entry_map() {
let hash = hash_password("pw").unwrap();
let rendered = serde_json::json!({ "admin": hash }).to_string();
let parsed: std::collections::BTreeMap<String, String> =
serde_json::from_str(&rendered).unwrap();
assert_eq!(parsed.len(), 1);
assert!(is_valid_password_hash(&parsed["admin"]));
}
}