#![allow(missing_docs)]
mod advisory;
mod bip39_friendly;
mod cmd;
mod codex32_friendly;
mod error;
mod format;
mod language;
#[allow(dead_code)]
mod mlock;
mod parse;
mod process_hardening;
use std::io::Write;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use error::{CliError, Result};
use format::{ErrorBodyJson, ErrorEnvelopeJson};
const PASSPHRASE_RECOVERY_HELP: &str = "\
RECOVERING A FORGOTTEN BIP-39 PASSPHRASE:
If you have your entropy (your ms1 backup or seed words) but not the
BIP-39 passphrase (the optional \"25th word\"), it cannot be
brute-forced from the entropy alone. An external open-source tool can:
btcrecover searches passphrase candidates and confirms each by deriving
an address / xpub / master-fingerprint at common default paths and
matching a value you already know.
btcrecover (maintained): https://github.com/3rdIteration/btcrecover
original: https://github.com/gurnec/btcrecover
Pointer current as of 2026-05-25. Run untrusted recovery tools
offline, on an air-gapped machine.";
#[derive(Parser, Debug)]
#[command(
name = "ms",
version,
about = "ms — engrave-friendly BIP-39 entropy backups (the ms1 format)",
after_help = PASSPHRASE_RECOVERY_HELP
)]
pub(crate) struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Derive(cmd::derive::DeriveArgs),
#[command(
after_long_help = "EXAMPLES:\n ms encode --phrase \"abandon abandon … about\"\n ms encode --phrase - < phrase.txt\n ms encode --hex 00000000000000000000000000000000 --no-engraving-card\n ms encode --phrase \"...\" --json | jq .ms1"
)]
Encode(cmd::encode::EncodeArgs),
#[command(
after_long_help = "EXAMPLES:\n ms decode ms10entrs…\n ms decode - < engraved.txt\n ms decode <ms1> --language french\n ms decode <ms1> --json | jq .phrase"
)]
Decode(cmd::decode::DecodeArgs),
#[command(
after_long_help = "EXAMPLES:\n ms inspect <ms1> # verdict + fields\n ms inspect <ms1> --json # structured output for tooling\n printf \"ms10e ntrsq…\" | ms inspect - # back-typed chunked form"
)]
Inspect(cmd::inspect::InspectArgs),
#[command(
after_long_help = "EXAMPLES:\n ms verify <ms1> # exit 0 = valid v0.1\n ms verify <ms1> --phrase \"abandon … about\" # round-trip; exit 4 on mismatch\n ms verify <ms1> --phrase \"...\" --json # structured outcome"
)]
Verify(cmd::verify::VerifyArgs),
#[command(
after_long_help = "EXAMPLES:\n ms vectors # compact JSON\n ms vectors --pretty # indented JSON\n ms vectors | jq '.[0]' # filter via jq"
)]
Vectors(cmd::vectors::VectorsArgs),
#[command(
name = "gui-schema",
after_long_help = "EXAMPLES:\n ms gui-schema | jq .version # always 1\n ms gui-schema | jq '.subcommands[].name' # list subcommands\n ms gui-schema | jq '.subcommands[] | select(.name == \"encode\").flags'"
)]
GuiSchema,
#[command(
name = "gen-man",
after_long_help = "EXAMPLES:\n ms gen-man --out ./man # write ms.1, ms-encode.1, … into ./man\n ms gen-man --out \"$XDG_DATA_HOME/man/man1\" # user manpath"
)]
GenMan(cmd::gen_man::GenManArgs),
#[command(
after_long_help = "EXAMPLES:\n ms repair --ms1 ms10entrsqq... # text-form report on stdout\n ms repair --ms1 - < broken.txt # read ms1 from stdin\n ms repair --ms1 ms10entrsqq... --json # JSON envelope on stdout"
)]
Repair(cmd::repair::RepairArgs),
#[command(
after_long_help = "EXAMPLES:\n ms split --phrase \"abandon abandon … about\" -k 2 -n 3\n ms split --hex 00000000000000000000000000000000 -k 3 -n 5\n ms split --language japanese --phrase \"…\" -k 2 -n 3 --json | jq .shares"
)]
Split(cmd::split::SplitArgs),
#[command(
after_long_help = "EXAMPLES:\n ms combine <share1> <share2> # recover the BIP-39 phrase\n ms combine <share1> <share2> --to ms1 # recover a single ms1 string\n ms combine <share1> <share2> --to entropy # recover raw entropy hex\n ms combine <share1> <share2> --json | jq .phrase"
)]
Combine(cmd::combine::CombineArgs),
}
fn main() -> ExitCode {
process_hardening::set_non_dumpable();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
e.print().ok();
return match e.kind() {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
ExitCode::SUCCESS
}
_ => ExitCode::from(64),
};
}
};
let json_mode = is_json_mode(&cli.command);
let result: Result<u8> = match cli.command {
Command::Derive(args) => cmd::derive::run(args),
Command::Encode(args) => cmd::encode::run(args),
Command::Decode(args) => cmd::decode::run(args),
Command::Inspect(args) => cmd::inspect::run(args),
Command::Verify(args) => cmd::verify::run(args),
Command::Vectors(args) => cmd::vectors::run(args),
Command::GuiSchema => cmd::gui_schema::run(),
Command::GenMan(args) => cmd::gen_man::run(args),
Command::Repair(args) => cmd::repair::run(args),
Command::Split(args) => cmd::split::run(args),
Command::Combine(args) => cmd::combine::run(args),
};
let exit = match result {
Ok(code) => ExitCode::from(code),
Err(e) => {
emit_error(&e, json_mode);
ExitCode::from(e.exit_code())
}
};
mlock::report_at_exit();
exit
}
fn is_json_mode(cmd: &Command) -> bool {
match cmd {
Command::Derive(a) => a.json,
Command::Encode(a) => a.json,
Command::Decode(a) => a.json,
Command::Inspect(a) => a.json,
Command::Verify(a) => a.json,
Command::Vectors(_) => false, Command::GuiSchema => false, Command::GenMan(_) => false, Command::Repair(a) => a.json,
Command::Split(a) => a.json,
Command::Combine(a) => a.json,
}
}
fn emit_error(e: &CliError, json_mode: bool) {
if matches!(e, CliError::FutureFormat { .. }) && !json_mode {
return;
}
if json_mode {
let envelope = ErrorEnvelopeJson {
schema_version: "1",
error: ErrorBodyJson {
kind: e.kind(),
message: e.message(),
exit_code: e.exit_code(),
details: e.details(),
},
};
let s = serde_json::to_string(&envelope).expect("error envelope serializes");
println!("{}", s);
} else {
let mut stderr = std::io::stderr().lock();
writeln!(stderr, "{}", e).ok();
}
}