cli: add 'rate <passphrase>' subcommand (zxcvbn)
This commit is contained in:
@@ -179,6 +179,16 @@ enum Commands {
|
|||||||
#[arg(value_enum)]
|
#[arg(value_enum)]
|
||||||
shell: Shell,
|
shell: Shell,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Rate a passphrase with zxcvbn — prints score (0-4) and estimated
|
||||||
|
/// guesses. Informational only; does not gate vault operations.
|
||||||
|
///
|
||||||
|
/// Pass `-` as the argument to read one line from stdin instead, which
|
||||||
|
/// keeps the passphrase out of shell history.
|
||||||
|
Rate {
|
||||||
|
/// Passphrase to score, or `-` to read from stdin.
|
||||||
|
passphrase: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -375,6 +385,7 @@ fn main() -> Result<()> {
|
|||||||
generate(shell, &mut cmd, "relicario", &mut std::io::stdout());
|
generate(shell, &mut cmd, "relicario", &mut std::io::stdout());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Commands::Rate { passphrase } => cmd_rate(passphrase),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2180,3 +2191,28 @@ struct ParamsKdf {
|
|||||||
argon2_t: u32,
|
argon2_t: u32,
|
||||||
argon2_p: u32,
|
argon2_p: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_rate(passphrase: String) -> Result<()> {
|
||||||
|
let pw: String = if passphrase == "-" {
|
||||||
|
use std::io::BufRead;
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let mut line = String::new();
|
||||||
|
stdin.lock().read_line(&mut line)?;
|
||||||
|
line.trim_end_matches(&['\r', '\n'][..]).to_string()
|
||||||
|
} else {
|
||||||
|
passphrase
|
||||||
|
};
|
||||||
|
let est = relicario_core::generators::rate_passphrase(&pw);
|
||||||
|
let label = match est.score {
|
||||||
|
0 => "very weak",
|
||||||
|
1 => "weak",
|
||||||
|
2 => "fair",
|
||||||
|
3 => "good",
|
||||||
|
4 => "strong",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
println!("score: {}/4 ({})", est.score, label);
|
||||||
|
println!("guesses: ~10^{:.1}", est.guesses_log10);
|
||||||
|
println!("note: init requires score ≥ 3 (see `relicario init`)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
use assert_cmd::Command;
|
use assert_cmd::Command;
|
||||||
|
use predicates::prelude::PredicateBooleanExt;
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -103,3 +104,35 @@ fn no_groups_cache_env_var_suppresses_write() {
|
|||||||
"groups.cache should not exist when RELICARIO_NO_GROUPS_CACHE=1"
|
"groups.cache should not exist when RELICARIO_NO_GROUPS_CACHE=1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rate_strong_passphrase_prints_score_and_guesses() {
|
||||||
|
Command::cargo_bin("relicario").unwrap()
|
||||||
|
.args(["rate", "correct horse battery staple table cocoa rocket spirit ferment"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("score:"))
|
||||||
|
.stdout(contains("guesses:"))
|
||||||
|
.stdout(contains("strong"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rate_weak_passphrase_exits_zero_with_weak_label() {
|
||||||
|
// `rate` is informational — does NOT exit nonzero on weak input.
|
||||||
|
// The hard gate lives at `init` (Plan 2B Task 10).
|
||||||
|
Command::cargo_bin("relicario").unwrap()
|
||||||
|
.args(["rate", "password"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("very weak").or(contains("weak")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rate_reads_from_stdin_when_arg_is_dash() {
|
||||||
|
Command::cargo_bin("relicario").unwrap()
|
||||||
|
.args(["rate", "-"])
|
||||||
|
.write_stdin("correcthorsebatterystaple\n")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("score:"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user