use bip39::{Language, Mnemonic};
use clap::{Args, ValueEnum};
use ms_codec::{Payload, Tag};
use serde_json::to_string;
use zeroize::Zeroizing;
use crate::advisory::{OutputClass, emit_output_class_advisory};
use crate::error::{CliError, Result};
use crate::format::CombineJson;
use crate::language::CliLanguage;
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum CombineTo {
Phrase,
Entropy,
Ms1,
}
#[derive(Args, Debug)]
pub struct CombineArgs {
#[arg(required = true)]
pub shares: Vec<String>,
#[arg(long, value_enum, default_value = "phrase")]
pub to: CombineTo,
#[arg(long)]
pub json: bool,
}
fn read_shares(args: &[String]) -> Result<Zeroizing<Vec<String>>> {
let mut out: Vec<String> = Vec::with_capacity(args.len());
let mut consumed_stdin = false;
for a in args {
if a == "-" && !consumed_stdin {
consumed_stdin = true;
let buf = crate::parse::read_stdin()?;
for line in buf.lines() {
let s = crate::format::strip_display_separators(line);
if !s.is_empty() {
out.push(s);
}
}
} else if a == "-" {
} else {
out.push(crate::format::strip_display_separators(a));
}
}
if out.is_empty() {
return Err(CliError::BadInput(
"expected at least one share (positional or via stdin with '-')".into(),
));
}
Ok(Zeroizing::new(out))
}
pub fn run(mut args: CombineArgs) -> Result<u8> {
let shares: Zeroizing<Vec<String>> = read_shares(&std::mem::take(&mut args.shares))?;
let (_tag, payload): (Tag, Payload) = ms_codec::combine_shares(&shares)?;
let (entropy, language, kind): (Zeroizing<Vec<u8>>, CliLanguage, &'static str) = match &payload
{
Payload::Entr(b) => (Zeroizing::new(b.clone()), CliLanguage::English, "entr"),
Payload::Mnem {
language: wire_code,
entropy,
} => {
let lang = CliLanguage::from_code(*wire_code).unwrap_or(CliLanguage::English);
(Zeroizing::new(entropy.clone()), lang, "mnem")
}
_ => unreachable!("combine_shares returned an unknown Payload variant"),
};
match args.to {
CombineTo::Phrase => emit_phrase(&entropy, language, kind, args.json)?,
CombineTo::Entropy => {
emit_entropy(&entropy, kind, args.json)?;
crate::advisory::non_english_seed_advisory(
&mut std::io::stderr().lock(),
language,
"raw entropy",
);
}
CombineTo::Ms1 => emit_ms1(&payload, &entropy, kind, args.json)?,
}
emit_output_class_advisory(OutputClass::PrivateKeyMaterial, &mut std::io::stderr().lock());
Ok(0)
}
fn emit_phrase(
entropy: &[u8],
language: CliLanguage,
kind: &'static str,
json: bool,
) -> Result<()> {
let lang: Language = language.into();
let mnemonic = Mnemonic::from_entropy_in(lang, entropy)
.expect("combine_shares validates entropy length; from_entropy_in cannot fail");
let phrase: Zeroizing<String> = Zeroizing::new(mnemonic.to_string());
let word_count = phrase.split_whitespace().count();
if json {
let json = CombineJson {
schema_version: "1",
kind: kind.to_string(),
ms1: None,
entropy_hex: hex::encode(entropy),
phrase: Some(phrase.to_string()),
language: Some(language.as_str()),
word_count: Some(word_count),
};
let s =
to_string(&json).map_err(|e| CliError::BadInput(format!("json serialization: {e}")))?;
println!("{s}");
} else {
println!("entropy: {}", hex::encode(entropy));
println!("phrase: {}", *phrase);
println!("language: {} ({} words)", language.as_str(), word_count);
println!("kind: {kind}");
}
Ok(())
}
fn emit_entropy(entropy: &[u8], kind: &'static str, json: bool) -> Result<()> {
if json {
let json = CombineJson {
schema_version: "1",
kind: kind.to_string(),
ms1: None,
entropy_hex: hex::encode(entropy),
phrase: None,
language: None,
word_count: None,
};
let s =
to_string(&json).map_err(|e| CliError::BadInput(format!("json serialization: {e}")))?;
println!("{s}");
} else {
println!("{}", hex::encode(entropy));
}
Ok(())
}
fn emit_ms1(payload: &Payload, entropy: &[u8], kind: &'static str, json: bool) -> Result<()> {
let ms1 = ms_codec::encode(Tag::ENTR, payload)?;
if json {
let json = CombineJson {
schema_version: "1",
kind: kind.to_string(),
ms1: Some(ms1.clone()),
entropy_hex: hex::encode(entropy),
phrase: None,
language: None,
word_count: None,
};
let s =
to_string(&json).map_err(|e| CliError::BadInput(format!("json serialization: {e}")))?;
println!("{s}");
} else {
println!("{ms1}");
}
Ok(())
}