///|
/// Command line interface for moon_otp.
fn usage() -> Unit {
println("moon_otp - HOTP/TOTP one-time passwords in MoonBit")
println("")
println("Usage:")
println(
" moon_otp gen [--issuer ] [--account ] [--length ] [--algorithm SHA1|SHA256|SHA512] [--digits ] [--period ]",
)
println(
" moon_otp now --secret [--algorithm ..] [--digits ] [--period ]",
)
println(
" moon_otp hotp --secret --counter [--algorithm ..] [--digits ]",
)
println(
" moon_otp uri --secret --issuer --account [--algorithm ..] [--digits ] [--period ]",
)
println(" moon_otp steam --secret ")
println(" moon_otp recovery [--count ]")
println(
" moon_otp verify --secret --code [--window ] [--algorithm ..]",
)
}
///|
fn parse_flags(argv : Array[String]) -> Map[String, String] {
let flags = Map([], capacity=0)
let mut i = 2
while i < argv.length() {
let token = argv[i]
if token.has_prefix("--") {
if i + 1 >= argv.length() {
eprintln("missing value for flag: " + token)
abort("bad arguments")
}
flags.set(token[2:].to_owned(), argv[i + 1])
i += 2
} else {
eprintln("unexpected argument: " + token)
abort("bad arguments")
}
}
flags
}
///|
fn string_flag(
flags : Map[String, String],
name : String,
default : String,
) -> String {
match flags.get(name) {
Some(v) => v
None => default
}
}
///|
fn int_flag(
flags : Map[String, String],
name : String,
default : Int,
) -> Int raise @lib.OtpError {
match flags.get(name) {
Some(v) =>
@string.parse_int(v) catch {
_ => raise @lib.InvalidUri("invalid integer for --" + name + ": " + v)
}
None => default
}
}
///|
fn algorithm_flag(
flags : Map[String, String],
) -> @lib.HashAlgorithm raise @lib.OtpError {
match flags.get("algorithm") {
None => @lib.Sha1
Some(v) if v == "SHA1" => @lib.Sha1
Some(v) if v == "SHA256" => @lib.Sha256
Some(v) if v == "SHA512" => @lib.Sha512
Some(v) => raise @lib.InvalidUri("unknown algorithm: " + v)
}
}
///|
fn decode_secret(flags : Map[String, String]) -> Bytes raise @lib.OtpError {
match flags.get("secret") {
Some(s) =>
@lib.base32_decode(s) catch {
_ => raise @lib.InvalidUri("--secret is not valid Base32")
}
None => raise @lib.InvalidUri("missing required --secret ")
}
}
///|
fn cmd_gen(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let length = int_flag(flags, "length", 20)
let secret = @lib.generate_secret(length~)
let issuer = string_flag(flags, "issuer", "Example")
let account = string_flag(flags, "account", "user@example.com")
let algorithm = algorithm_flag(flags)
let digits = int_flag(flags, "digits", 6)
let period = int_flag(flags, "period", 30)
let otp : @lib.OtpAuth = {
issuer,
account,
secret,
algorithm,
digits,
period,
otp_type: @lib.Totp,
counter: 0UL,
}
println("Secret (Base32): " + @lib.base32_encode_unpadded(secret))
println("otpauth URI: " + @lib.otpauth_uri(otp))
println(
"Current code: " + @lib.totp_now(algorithm, secret, digits~, period~),
)
}
///|
fn cmd_now(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let secret = decode_secret(flags)
let algorithm = algorithm_flag(flags)
let digits = int_flag(flags, "digits", 6)
let period = int_flag(flags, "period", 30)
let code = @lib.totp_now(algorithm, secret, digits~, period~)
// @env.now() returns milliseconds.
let now_seconds = @env.now() / 1000UL
let remaining = period.to_uint64() - now_seconds % period.to_uint64()
println(code + " (" + remaining.to_string() + "s remaining)")
}
///|
fn cmd_hotp(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let secret = decode_secret(flags)
let algorithm = algorithm_flag(flags)
let digits = int_flag(flags, "digits", 6)
let counter = match flags.get("counter") {
Some(v) =>
@string.parse_uint64(v) catch {
_ => raise @lib.InvalidUri("invalid --counter: " + v)
}
None => raise @lib.InvalidUri("missing required --counter ")
}
println(@lib.hotp(algorithm, secret, counter, digits~))
}
///|
fn cmd_uri(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let secret = decode_secret(flags)
let issuer = match flags.get("issuer") {
Some(v) => v
None => raise @lib.InvalidUri("missing required --issuer ")
}
let account = match flags.get("account") {
Some(v) => v
None => raise @lib.InvalidUri("missing required --account ")
}
let algorithm = algorithm_flag(flags)
let digits = int_flag(flags, "digits", 6)
let period = int_flag(flags, "period", 30)
let otp : @lib.OtpAuth = {
issuer,
account,
secret,
algorithm,
digits,
period,
otp_type: @lib.Totp,
counter: 0UL,
}
println(@lib.otpauth_uri(otp))
}
///|
fn cmd_steam(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let secret = decode_secret(flags)
println(@lib.steam_guard_now(secret))
}
///|
fn cmd_recovery(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let count = int_flag(flags, "count", 10)
println("Recovery codes (each may be used once):")
for code in @lib.generate_recovery_codes(count~) {
println(" " + code)
}
}
///|
fn cmd_verify(flags : Map[String, String]) -> Unit raise @lib.OtpError {
let secret = decode_secret(flags)
let algorithm = algorithm_flag(flags)
let window = int_flag(flags, "window", 1)
let code = match flags.get("code") {
Some(v) => v
None => raise @lib.InvalidUri("missing required --code ")
}
if @lib.totp_verify(algorithm, secret, code, window~) {
println("OK")
} else {
println("FAIL")
abort("verification failed")
}
}
///|
fn main {
let argv = @env.args()
if argv.length() < 2 {
usage()
abort("")
}
let command = argv[1]
let flags = parse_flags(argv)
try {
match command {
"gen" => cmd_gen(flags)
"now" => cmd_now(flags)
"hotp" => cmd_hotp(flags)
"uri" => cmd_uri(flags)
"steam" => cmd_steam(flags)
"recovery" => cmd_recovery(flags)
"verify" => cmd_verify(flags)
_ => {
usage()
abort("")
}
}
} catch {
e => {
eprintln("error:")
eprintln(e.to_repr())
abort("")
}
}
}