///|
struct CliOptions {
profile : String
length : Int
count : Int
pin_digits : Int
help : Bool
}
///|
fn default_options() -> CliOptions {
{ profile: "standard", length: 0, count: 1, pin_digits: 0, help: false, }
}
///|
fn parse_positive_int(text : String) -> Int? {
if text.length() == 0 {
return None
}
let mut value = 0
let mut index = 0
while index < text.length() {
let code = match text.get_char(index) {
Some(character) => character.to_int()
None => return None
}
if code < 48 || code > 57 || value > 100000 {
return None
}
value = value * 10 + code - 48
index = index + 1
}
if value > 0 {
Some(value)
} else {
None
}
}
///|
fn option_value(
args : Array[String],
index : Int,
name : String,
) -> Result[String, String] {
if index + 1 >= args.length() {
Err("missing value after \{name}")
} else {
Ok(args[index + 1])
}
}
///|
fn parse_args(args : Array[String]) -> Result[CliOptions, String] {
let defaults = default_options()
let mut profile = defaults.profile
let mut length = defaults.length
let mut count = defaults.count
let mut pin_digits = defaults.pin_digits
let mut help = defaults.help
let mut index = 0
while index < args.length() {
let argument = args[index]
if argument == "--help" || argument == "-h" {
help = true
index = index + 1
} else if argument == "--profile" {
let value = match option_value(args, index, argument) {
Ok(value) => value
Err(message) => return Err(message)
}
if value != "compatible" && value != "standard" && value != "strict" {
return Err("profile must be compatible, standard, or strict")
}
profile = value
index = index + 2
} else if argument == "--length" ||
argument == "--count" ||
argument == "--pin" {
let value_text = match option_value(args, index, argument) {
Ok(value) => value
Err(message) => return Err(message)
}
let value = match parse_positive_int(value_text) {
Some(value) => value
None => return Err("\{argument} requires a positive integer")
}
if argument == "--length" {
if value < 4 || value > 128 {
return Err("--length must be between 4 and 128")
}
length = value
} else if argument == "--count" {
if value > 100 {
return Err("--count must be between 1 and 100")
}
count = value
} else {
if value < 4 || value > 32 {
return Err("--pin must be between 4 and 32 digits")
}
pin_digits = value
}
index = index + 2
} else {
return Err("unknown option: \{argument}")
}
}
Ok({ profile, length, count, pin_digits, help, })
}
///|
fn policy_for(options : CliOptions) -> @lib.PasswordPolicy {
let policy = if options.profile == "compatible" {
@lib.PasswordPolicy::compatible()
} else if options.profile == "strict" {
@lib.PasswordPolicy::strict()
} else {
@lib.PasswordPolicy::standard()
}
if options.length > 0 {
policy.with_length(options.length)
} else {
policy
}
}
///|
fn secure_random_u32() -> UInt {
let bytes = match @env.rand(4) {
Some(bytes) => bytes
None =>
abort(
"the platform did not provide a cryptographically secure random source",
)
}
bytes[0].to_uint() |
(bytes[1].to_uint() << 8) |
(bytes[2].to_uint() << 16) |
(bytes[3].to_uint() << 24)
}
///|
fn print_help() -> Unit {
println("SecureGen CLI - generate credentials using operating-system entropy")
println("")
println(
"Usage: moon run src/cmd/securegen --target js --release -- [options]",
)
println("")
println(
" --profile compatible|standard|strict Password policy (default: standard)",
)
println(" --length N Password length, 4..128")
println(
" --pin N Generate an N-digit PIN instead",
)
println(" --count N Generate 1..100 values")
println(" -h, --help Show this help")
println("")
println(
"Secrets are printed to standard output. Avoid shell history and shared logs.",
)
}
///|
fn application_args() -> Array[String] {
let raw = @env.args()
if raw.length() > 2 {
raw[2:].to_owned()
} else {
[]
}
}
///|
fn main {
let options = match parse_args(application_args()) {
Ok(options) => options
Err(message) => {
println("error: \{message}")
println("use --help for usage")
return
}
}
if options.help {
print_help()
return
}
let policy = policy_for(options)
let mut generated = 0
while generated < options.count {
let result = if options.pin_digits > 0 {
@lib.generate_pin(options.pin_digits, secure_random_u32)
} else {
@lib.generate_password(policy, secure_random_u32)
}
match result {
Ok(value) => println(value)
Err(message) => {
println("generation failed: \{message}")
return
}
}
generated = generated + 1
}
}