use clap::{CommandFactory as _, Parser};
use lesspass::*;
use std::io::{IsTerminal, Write};
#[derive(Parser)]
#[command(after_help = r#"EXAMPLES:
Generate a password:
lesspass example.org contact@example.org password
Generate the fingerprint of a master password:
lesspass password -F
Generate a 32-characters password using SHA-512:
echo password | lesspass example.org contact@example.org --sha512 -l 32
Generate the entropy of a password, using 10,000 iterations:
lesspass example.org contact@example.org password -i 10000 -E > entropy.txt
Generate an alphanumeric password using the previously saved entropy:
cat entropy.txt | lesspass -S
The two previous examples are equivalent to:
lesspass example.org contact@example.org password -i 10000 -S
"#)]
pub struct Args {
#[arg(name = "website")]
website: Option<String>,
#[arg(name = "login")]
login: Option<String>,
#[arg(name = "password")]
master_password: Option<String>,
#[arg(short = 'i', long = "iterations", default_value = "100000")]
iterations: u32,
#[arg(short = 'l', long = "length", default_value = "16")]
length: u32,
#[arg(short = 'c', long = "counter", default_value = "1")]
counter: u32,
#[arg(long = "sha256")]
sha256: bool,
#[arg(long = "sha384")]
sha384: bool,
#[arg(long = "sha512")]
sha512: bool,
#[arg(short = 'L', long = "no-lower")]
exclude_lower: bool,
#[arg(short = 'U', long = "no-upper")]
exclude_upper: bool,
#[arg(short = 'D', long = "no-digits")]
exclude_digits: bool,
#[arg(short = 'S', long = "no-symbols")]
exclude_symbols: bool,
#[arg(short = 'E', long = "return-entropy")]
return_entropy: bool,
#[arg(short = 'F', long = "print-fingerprint")]
print_fingerprint: bool,
}
fn main() {
if let Err(err) = run() {
let mut out = std::io::stderr();
let res = if !err.is_empty() {
out.write_all(err.as_bytes()).map_err(|_| ())
} else {
Args::command().write_long_help(&mut out).map_err(|_| ())
};
std::process::exit(if res.is_ok() { 1 } else { 2 })
}
}
fn run() -> Result<(), &'static str> {
let Args {
website,
login,
master_password,
iterations,
length,
counter,
sha256,
sha384,
sha512,
exclude_lower,
exclude_upper,
exclude_digits,
exclude_symbols,
return_entropy,
print_fingerprint,
} = Args::parse();
let mut out = std::io::stdout();
let algorithm = match (sha256, sha384, sha512) {
(false, false, false) | (true, false, false) => Algorithm::SHA256,
(false, true, false) => Algorithm::SHA384,
(false, false, true) => Algorithm::SHA512,
_ => return Err("Only one algorithm must be provided."),
};
let mut charset = CharacterSet::All;
if exclude_lower {
charset.remove(CharacterSet::Lowercase);
}
if exclude_upper {
charset.remove(CharacterSet::Uppercase);
}
if exclude_digits {
charset.remove(CharacterSet::Digits);
}
if exclude_symbols {
charset.remove(CharacterSet::Symbols);
}
if charset.is_empty() {
return Err("Not all characters can be excluded from the generation algorithm.");
}
let length = length as usize;
if !(MIN_PASSWORD_LEN..=MAX_PASSWORD_LEN).contains(&length) {
return Err("The length must be an integer in the [6; 64] range.");
}
if !(1..=100_000_000).contains(&iterations) {
return Err("The iterations must be an integer in the [1; 100,000,000] range.");
}
let entropy = match (website, login, master_password) {
(pass, None, None) => {
if print_fingerprint {
let master_password = match pass {
Some(pass) => pass,
None => read_password()?, };
print_buffer_hex(get_fingerprint(&master_password).as_ref(), &mut out)?;
return Ok(());
}
let entropy = match pass {
Some(pass) => pass,
None => {
if std::io::stdin().is_terminal() {
return Err("");
}
read_password()?
}
};
match parse_entropy(&entropy) {
Some(entropy) => {
entropy
}
None => return Err("Invalid entropy format."),
}
}
(Some(website), Some(login), pass) => {
let master_password = match pass {
Some(pass) => pass,
None => read_password()?, };
let salt = generate_salt(&website, &login, counter);
if print_fingerprint {
print_buffer_hex(get_fingerprint(&master_password).as_ref(), &mut out)?;
}
generate_entropy(&master_password, &salt, algorithm, iterations)
}
_ => {
return Err("");
}
};
if return_entropy {
print_buffer_hex(&entropy, &mut out)?;
} else {
let password = render_password(&entropy, charset, length);
println!("{}", password);
}
Ok(())
}
fn print_buffer_hex(buf: &[u8], out: &mut dyn Write) -> Result<(), &'static str> {
for byte in buf {
write!(out, "{:02x}", byte).map_err(|_| "Unable to write to standard output.")?;
}
out.write(b"\n")
.map_err(|_| "Unable to write to standard output.")?;
Ok(())
}
fn read_password() -> Result<String, &'static str> {
if std::io::stdin().is_terminal() {
rpassword::read_password().map_err(|_| "Unable to read password or entropy.")
} else {
let stdin = std::io::stdin();
let mut input = String::new();
if stdin.read_line(&mut input).is_err() {
return Err("Unable to read password or entropy from standard input.");
}
if input.ends_with('\n') {
let new_len = input.len() - (if input.ends_with("\r\n") { 2 } else { 1 });
input.truncate(new_len);
}
Ok(input)
}
}
fn parse_entropy(entropy: &str) -> Option<Vec<u8>> {
if (entropy.len() & 1) == 1 {
return None;
}
let len = entropy.len() / 2;
let mut result = Vec::with_capacity(len);
for i in 0..len {
result.push(u8::from_str_radix(&entropy[i * 2..i * 2 + 2], 16).ok()?);
}
Some(result)
}