use bip39::{Language, Mnemonic};
use clap::Args;
use ms_codec::Payload;
use serde_json::to_string;
use crate::error::{CliError, Result};
use crate::format::VerifySuccessJson;
use crate::language::CliLanguage;
use crate::parse::{is_stdin_arg, read_input, read_phrase_input};
#[derive(Args, Debug)]
pub struct VerifyArgs {
pub ms1: Option<String>,
#[arg(long)]
pub phrase: Option<String>,
#[arg(long, default_value = "english")]
pub language: CliLanguage,
#[arg(long)]
pub json: bool,
}
pub fn run(mut args: VerifyArgs) -> Result<u8> {
use zeroize::Zeroizing;
let phrase_arg: Option<Zeroizing<String>> =
std::mem::take(&mut args.phrase).map(Zeroizing::new);
if is_stdin_arg(args.ms1.as_deref()) && phrase_arg.as_deref().map(|s| s.as_str()) == Some("-") {
return Err(CliError::BadInput(
"cannot read both ms1 and --phrase from stdin".into(),
));
}
let ms1: Zeroizing<String> = Zeroizing::new(read_input(args.ms1.as_deref())?);
let decoded = ms_codec::decode(&ms1);
let entropy: Zeroizing<Vec<u8>> = match decoded {
Ok((_tag, Payload::Entr(b))) => Zeroizing::new(b),
Ok((_, _)) => unreachable!("ms-codec v0.1 only decodes to Payload::Entr"),
Err(ms_codec::Error::ReservedTagNotEmittedInV01 { got }) => {
emit_future_format(&got, args.json)?;
return Ok(0);
}
Err(e) => return Err(e.into()),
};
let phrase_supplied: Option<Zeroizing<String>> = match phrase_arg.as_ref() {
Some(p) => Some(read_phrase_input(Some(p.as_str()))?),
None => None,
};
if let Some(supplied) = phrase_supplied {
let lang: Language = args.language.into();
let supplied_mnemonic = Mnemonic::parse_in(lang, supplied.as_str())?;
let derived_mnemonic = Mnemonic::from_entropy_in(lang, &entropy[..])
.expect("ms-codec validates entropy length");
let supplied_str: Zeroizing<String> = Zeroizing::new(supplied_mnemonic.to_string());
let derived_str: Zeroizing<String> = Zeroizing::new(derived_mnemonic.to_string());
if *supplied_str == *derived_str {
emit_round_trip_ok(&derived_mnemonic, args.language.as_str(), args.json)?;
return Ok(0);
} else {
return Err(CliError::VerifyPhraseMismatch);
}
}
let word_count = entropy.len() * 3 / 4;
let str_len = ms1.len();
emit_simple_ok(word_count, str_len, args.json)?;
Ok(0)
}
fn emit_simple_ok(word_count: usize, str_len: usize, json: bool) -> Result<()> {
if json {
let j = VerifySuccessJson {
schema_version: "1",
status: "valid",
message: &format!("valid v0.1 entr ({} words, {} chars)", word_count, str_len),
};
println!("{}", to_string(&j).expect("verify json"));
} else {
println!(
"OK: valid v0.1 entr ({} words, {} chars)",
word_count, str_len
);
}
Ok(())
}
fn emit_future_format(tag: &[u8; 4], json: bool) -> Result<()> {
let tag_str = std::str::from_utf8(tag).unwrap_or("<non-utf8>");
if !json {
println!("OK: valid future format (v0.2+, tag {})", tag_str);
}
Err(CliError::FutureFormat { tag: *tag })
}
fn emit_round_trip_ok(_mnemonic: &Mnemonic, language: &str, json: bool) -> Result<()> {
let word_count = _mnemonic.to_string().split_whitespace().count();
if json {
let j = VerifySuccessJson {
schema_version: "1",
status: "round-trip-ok",
message: &format!(
"round-trip valid ({} words, language={})",
word_count, language
),
};
println!("{}", to_string(&j).expect("verify json"));
} else {
println!(
"OK: round-trip valid ({} words, language={})",
word_count, language
);
}
Ok(())
}