diff --git a/crates/relicario-cli/src/commands/generate.rs b/crates/relicario-cli/src/commands/generate.rs new file mode 100644 index 0000000..e334338 --- /dev/null +++ b/crates/relicario-cli/src/commands/generate.rs @@ -0,0 +1,68 @@ +//! `relicario generate` — emit a fresh password or BIP39 passphrase. + +use anyhow::Result; + +pub fn cmd_generate( + length: Option, + bip39: bool, + words: Option, + symbols: Option, + separator: Option, +) -> Result<()> { + use relicario_core::{ + generate_passphrase, generate_password, Capitalization, CharClasses, + GeneratorRequest, SymbolCharset, + }; + + // If we're inside a vault, unlock and pull `generator_defaults`. Outside + // a vault, this stays a fast standalone CSPRNG tool (no unlock prompt). + let vault_defaults: Option = if crate::helpers::vault_dir().is_ok() { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + Some(vault.load_settings()?.generator_defaults) + } else { + None + }; + + // `--bip39` flag forces Bip39 mode; otherwise use whatever mode the + // vault default is in (Random when no vault). + let use_bip39 = bip39 || matches!(vault_defaults, Some(GeneratorRequest::Bip39 { .. })); + + let output = if use_bip39 { + let (def_words, def_sep, def_cap) = match &vault_defaults { + Some(GeneratorRequest::Bip39 { word_count, separator, capitalization }) => { + (*word_count, separator.clone(), *capitalization) + } + _ => (5, " ".to_string(), Capitalization::Lower), + }; + generate_passphrase(&GeneratorRequest::Bip39 { + word_count: words.unwrap_or(def_words), + separator: separator.unwrap_or(def_sep), + capitalization: def_cap, + })? + } else { + let (def_length, def_classes, def_charset) = match &vault_defaults { + Some(GeneratorRequest::Random { length, classes, symbol_charset }) => { + (*length, *classes, symbol_charset.clone()) + } + _ => ( + 20, + CharClasses { lower: true, upper: true, digits: true, symbols: true }, + SymbolCharset::SafeOnly, + ), + }; + let symbol_charset = match symbols.as_deref() { + None => def_charset, + Some("safe") => SymbolCharset::SafeOnly, + Some("extended") => SymbolCharset::Extended, + Some(other) => SymbolCharset::Custom(other.to_string()), + }; + generate_password(&GeneratorRequest::Random { + length: length.unwrap_or(def_length), + classes: def_classes, + symbol_charset, + })? + }; + + println!("{}", output.as_str()); + Ok(()) +} diff --git a/crates/relicario-cli/src/commands/mod.rs b/crates/relicario-cli/src/commands/mod.rs index 589e520..38c1cdc 100644 --- a/crates/relicario-cli/src/commands/mod.rs +++ b/crates/relicario-cli/src/commands/mod.rs @@ -5,3 +5,6 @@ //! command modules (e.g. `commit_paths`, `resolve_query`) are defined in //! this file as `pub(crate)` so siblings can pull them in via //! `use crate::commands::*`. + +pub mod generate; +pub mod rate; diff --git a/crates/relicario-cli/src/commands/rate.rs b/crates/relicario-cli/src/commands/rate.rs new file mode 100644 index 0000000..035f556 --- /dev/null +++ b/crates/relicario-cli/src/commands/rate.rs @@ -0,0 +1,28 @@ +//! `relicario rate` — score a passphrase via zxcvbn. + +use anyhow::Result; + +pub 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(()) +} diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 2bfca67..e67872b 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -443,7 +443,7 @@ fn main() -> Result<()> { Commands::Extract { query, aid, out } => cmd_extract(query, aid, out), Commands::Detach { query, aid } => cmd_detach(query, aid), Commands::Generate { length, bip39, words, symbols, separator } => { - cmd_generate(length, bip39, words, symbols, separator) + commands::generate::cmd_generate(length, bip39, words, symbols, separator) } Commands::Settings { action } => cmd_settings(action), Commands::Sync => cmd_sync(), @@ -454,7 +454,7 @@ fn main() -> Result<()> { generate(shell, &mut cmd, "relicario", &mut std::io::stdout()); Ok(()) } - Commands::Rate { passphrase } => cmd_rate(passphrase), + Commands::Rate { passphrase } => commands::rate::cmd_rate(passphrase), Commands::Device { action } => cmd_device(action), Commands::RecoveryQr { cmd } => cmd_recovery_qr(cmd), } @@ -1979,70 +1979,6 @@ fn cmd_detach(query: String, aid: String) -> Result<()> { Ok(()) } -fn cmd_generate( - length: Option, - bip39: bool, - words: Option, - symbols: Option, - separator: Option, -) -> Result<()> { - use relicario_core::{ - generate_passphrase, generate_password, Capitalization, CharClasses, - GeneratorRequest, SymbolCharset, - }; - - // If we're inside a vault, unlock and pull `generator_defaults`. Outside - // a vault, this stays a fast standalone CSPRNG tool (no unlock prompt). - let vault_defaults: Option = if crate::helpers::vault_dir().is_ok() { - let vault = crate::session::UnlockedVault::unlock_interactive()?; - Some(vault.load_settings()?.generator_defaults) - } else { - None - }; - - // `--bip39` flag forces Bip39 mode; otherwise use whatever mode the - // vault default is in (Random when no vault). - let use_bip39 = bip39 || matches!(vault_defaults, Some(GeneratorRequest::Bip39 { .. })); - - let output = if use_bip39 { - let (def_words, def_sep, def_cap) = match &vault_defaults { - Some(GeneratorRequest::Bip39 { word_count, separator, capitalization }) => { - (*word_count, separator.clone(), *capitalization) - } - _ => (5, " ".to_string(), Capitalization::Lower), - }; - generate_passphrase(&GeneratorRequest::Bip39 { - word_count: words.unwrap_or(def_words), - separator: separator.unwrap_or(def_sep), - capitalization: def_cap, - })? - } else { - let (def_length, def_classes, def_charset) = match &vault_defaults { - Some(GeneratorRequest::Random { length, classes, symbol_charset }) => { - (*length, *classes, symbol_charset.clone()) - } - _ => ( - 20, - CharClasses { lower: true, upper: true, digits: true, symbols: true }, - SymbolCharset::SafeOnly, - ), - }; - let symbol_charset = match symbols.as_deref() { - None => def_charset, - Some("safe") => SymbolCharset::SafeOnly, - Some("extended") => SymbolCharset::Extended, - Some(other) => SymbolCharset::Custom(other.to_string()), - }; - generate_password(&GeneratorRequest::Random { - length: length.unwrap_or(def_length), - classes: def_classes, - symbol_charset, - })? - }; - - println!("{}", output.as_str()); - Ok(()) -} fn cmd_settings(action: SettingsAction) -> Result<()> { use relicario_core::{ Capitalization, CharClasses, GeneratorRequest, HistoryRetention, @@ -2210,31 +2146,6 @@ struct ParamsKdf { 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(()) -} - // ── Device management ───────────────────────────────────────────────────────── /// Build a `GiteaClient` from flags or environment variables.