extern crate clap;
use clap::{App, Arg, ArgMatches, SubCommand};
use harpo::seed_phrase::SeedPhrase;
use harpo::{
create_secret_shared_seed_phrases, create_secret_shared_seed_phrases_for_word_list,
generate_seed_phrase, generate_seed_phrase_for_word_list, reconstruct_seed_phrase,
reconstruct_seed_phrase_for_word_list, validate_seed_phrase,
validate_seed_phrase_for_word_list, HarpoError, HarpoResult, SeedPhraseResult,
MAX_EMBEDDED_SHARES,
};
use std::fs::read_to_string;
const CREATE_SUBCOMMAND: &str = "create";
const RECONSTRUCT_SUBCOMMAND: &str = "reconstruct";
const GENERATE_SUBCOMMAND: &str = "generate";
const VALIDATE_SUBCOMMAND: &str = "validate";
fn parse_command_line<'a>() -> ArgMatches<'a> {
const VERSION: &str = env!("CARGO_PKG_VERSION");
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
let file_argument = Arg::with_name("file")
.takes_value(true)
.short("f")
.long("file")
.help("Uses the data in the provided file as input");
let create_subcommand = SubCommand::with_name(CREATE_SUBCOMMAND)
.about("Creates secret-shared seed phrases")
.arg(file_argument.clone())
.arg(
Arg::with_name("no-embedding") .short("N")
.long("no-embedding")
.help("Stores share identifiers separately")
.takes_value(false),
)
.arg(
Arg::with_name("num-shares") .required(true)
.takes_value(true)
.short("n")
.long("num-shares")
.help("Sets the total number of shares to the given value"),
)
.arg(
Arg::with_name("threshold") .required(true)
.takes_value(true)
.short("t")
.long("threshold")
.help("Sets the threshold to the given value"),
);
let reconstruct_subcommand = SubCommand::with_name(RECONSTRUCT_SUBCOMMAND)
.about("Reconstructs a seed phrase")
.arg(file_argument.clone());
let generate_subcommand = SubCommand::with_name(GENERATE_SUBCOMMAND)
.about("Generates a seed phrase")
.arg(
Arg::with_name("length") .required(true)
.takes_value(true)
.short("l")
.long("length")
.help("Sets the number of words to the given value"),
);
let validate_subcommand = SubCommand::with_name(VALIDATE_SUBCOMMAND)
.about("Validates a seed phrase")
.arg(file_argument);
App::new("harpo")
.version(VERSION)
.author(AUTHORS)
.about("A tool to create secret-shared seed phrases and reconstruct seed phrases.")
.arg(
Arg::with_name("verbose") .short("v")
.long("verbose")
.help("Prints verbose output")
.takes_value(false),
)
.arg(
Arg::with_name("word-list") .short("w")
.long("word-list")
.help("Reads the word list from the provided file")
.takes_value(true),
)
.subcommand(create_subcommand) .subcommand(reconstruct_subcommand) .subcommand(generate_subcommand) .subcommand(validate_subcommand) .get_matches()
}
fn convert_string_to_seed_phrase(input: &str) -> SeedPhraseResult {
let mut words: Vec<String> = input
.replace(':', ": ") .to_lowercase() .trim() .split(' ') .filter(|word| !word.is_empty()) .map(str::to_string) .collect(); if words.is_empty() {
return Err(HarpoError::InvalidSeedPhrase(
"No seed phrase provided.".to_string(),
));
}
if words[0].contains(':') {
let index_string = words.remove(0);
match index_string.replace(":", "").parse::<u32>() {
Ok(index) => Ok(SeedPhrase::new_with_index(&words, index)),
Err(_) => Err(HarpoError::InvalidSeedPhrase(
"Could not parse index of seed phrase.".to_string(),
)),
}
} else {
Ok(SeedPhrase::new(&words))
}
}
fn read_seed_phrase_from_file(file_path: &str) -> SeedPhraseResult {
let file_content = read_to_string(file_path)?;
let seed_phrase_string = file_content
.lines()
.find(|line| !line.starts_with('#') && !line.is_empty());
match seed_phrase_string {
Some(seed_phrase_string) => convert_string_to_seed_phrase(seed_phrase_string),
None => Err(HarpoError::InvalidSeedPhrase(format!(
"Could not read the seed phrase from the file {}.",
file_path
))),
}
}
fn read_seed_phrase_interactively() -> SeedPhraseResult {
let mut seed_phrase_string = String::new();
println!("Please enter your seed phrase (12, 15, 18, 21, or 24 space-delimited words):");
let _ = std::io::stdin().read_line(&mut seed_phrase_string)?;
convert_string_to_seed_phrase(&seed_phrase_string)
}
fn handle_create(
command_line: &clap::ArgMatches,
verbose: bool,
word_list: Option<Vec<String>>,
) -> HarpoResult<Vec<SeedPhrase>> {
let num_shares = command_line
.value_of("num-shares")
.unwrap()
.parse::<usize>()?;
let threshold = command_line
.value_of("threshold")
.unwrap()
.parse::<usize>()?;
let embed_indices = !command_line.is_present("no-embedding");
if threshold < 1 {
return Err(HarpoError::InvalidParameter(
"The threshold must be at least 1.".to_string(),
));
}
if threshold > num_shares {
return Err(HarpoError::InvalidParameter(
"The threshold cannot be larger than the number of shares.".to_string(),
));
}
if num_shares > MAX_EMBEDDED_SHARES && embed_indices {
return Err(HarpoError::InvalidParameter(format!(
"Index embedding must be disabled (--no-embedding) when creating more than {} shares.",
MAX_EMBEDDED_SHARES
)));
}
if threshold > num_shares
|| threshold < 1
|| (num_shares > MAX_EMBEDDED_SHARES && embed_indices)
{
return Err(HarpoError::InvalidParameter(
"The provided parameters are invalid.".to_string(),
));
}
if verbose {
println!(
"Requested number of secret-shared seed phrases: {}",
num_shares
);
println!("Requested threshold for reconstruction: {}", threshold);
println!();
}
let seed_phrase = if let Some(file_path) = command_line.value_of("file") {
if verbose {
println!("Reading the seed phrase from {}...", file_path);
}
read_seed_phrase_from_file(file_path)?
} else {
read_seed_phrase_interactively()?
};
if verbose {
println!();
println!(
"Creating secret-shared seed phrases for seed phrase '{}'...",
seed_phrase
);
}
match word_list {
Some(list) => {
let slice_list: Vec<&str> = list.iter().map(|s| s.as_str()).collect();
create_secret_shared_seed_phrases_for_word_list(
&seed_phrase,
threshold,
num_shares,
embed_indices,
&slice_list,
)
}
None => {
create_secret_shared_seed_phrases(&seed_phrase, threshold, num_shares, embed_indices)
}
}
}
fn read_seed_phrases_from_file(file_path: &str) -> HarpoResult<Vec<SeedPhrase>> {
let file_content = read_to_string(file_path)?;
let seed_phrase_options: Vec<SeedPhraseResult> = file_content
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.map(convert_string_to_seed_phrase)
.collect();
if seed_phrase_options.iter().any(|option| option.is_err()) {
Err(HarpoError::InvalidSeedPhrase(
"Encountered an invalid seed phrase in the file.".to_string(),
))
} else {
Ok(seed_phrase_options
.into_iter()
.flatten()
.collect::<Vec<SeedPhrase>>())
}
}
fn read_seed_phrases_interactively() -> HarpoResult<Vec<SeedPhrase>> {
let mut seed_phrases = vec![];
let mut seed_phrase_string = String::new();
println!("Please enter the first secret-shared seed phrase (12, 15, 18, 21, or 24 space-delimited words):");
let _ = std::io::stdin().read_line(&mut seed_phrase_string)?;
match convert_string_to_seed_phrase(&seed_phrase_string) {
Ok(seed_phrase) => seed_phrases.push(seed_phrase),
Err(e) => return Err(e),
}
seed_phrase_string.clear();
println!();
println!("Please enter the next secret-shared seed phrase (press enter when done):");
let _ = std::io::stdin().read_line(&mut seed_phrase_string)?;
while let Ok(seed_phrase) = convert_string_to_seed_phrase(&seed_phrase_string) {
seed_phrases.push(seed_phrase);
seed_phrase_string.clear();
println!();
println!("Please enter the next secret-shared seed phrase (press enter when done):");
let _ = std::io::stdin().read_line(&mut seed_phrase_string)?;
}
Ok(seed_phrases)
}
fn handle_reconstruct(
command_line: &clap::ArgMatches,
verbose: bool,
word_list: Option<Vec<String>>,
) -> SeedPhraseResult {
let seed_phrases = if let Some(file_path) = command_line.value_of("file") {
if verbose {
println!("Reading seed phrases from {}...", file_path);
println!();
}
read_seed_phrases_from_file(file_path)?
} else {
read_seed_phrases_interactively()?
};
if verbose {
let length = seed_phrases.len();
if length > 1 {
println!(
"Reconstructing the seed phrase using these {} seed phrases:",
seed_phrases.len()
);
} else {
println!("Reconstructing the seed phrase using this seed phrase:")
}
println!();
for seed_phrase in &seed_phrases {
println!("{}", seed_phrase);
}
}
match word_list {
Some(list) => {
let slice_list: Vec<&str> = list.iter().map(|s| s.as_str()).collect();
reconstruct_seed_phrase_for_word_list(&seed_phrases, &slice_list)
}
None => reconstruct_seed_phrase(&seed_phrases),
}
}
fn read_word_list_from_file(file_path: &str) -> HarpoResult<Vec<String>> {
let file_content = read_to_string(file_path)?;
let word_list: Vec<String> = file_content.lines().map(str::to_string).collect();
Ok(word_list)
}
fn handle_generate(
command_line: &clap::ArgMatches,
verbose: bool,
word_list: Option<Vec<String>>,
) -> SeedPhraseResult {
let length = command_line.value_of("length").unwrap().parse::<usize>()?;
if verbose {
println!("Length of seed phrase: {}", length);
}
match word_list {
Some(list) => {
let slice_list: Vec<&str> = list.iter().map(|s| s.as_str()).collect();
generate_seed_phrase_for_word_list(length, &slice_list)
}
None => generate_seed_phrase(length),
}
}
fn handle_validate(
command_line: &clap::ArgMatches,
verbose: bool,
word_list: Option<Vec<String>>,
) -> HarpoResult<()> {
let seed_phrase = if let Some(file_path) = command_line.value_of("file") {
if verbose {
println!("Reading the seed phrase from {}...", file_path);
}
read_seed_phrase_from_file(file_path)?
} else {
read_seed_phrase_interactively()?
};
if verbose {
println!();
println!("Validating the seed phrase '{}'...", seed_phrase);
}
match word_list {
Some(list) => {
let slice_list: Vec<&str> = list.iter().map(|s| s.as_str()).collect();
validate_seed_phrase_for_word_list(&seed_phrase, &slice_list)
}
None => validate_seed_phrase(&seed_phrase),
}
}
fn main() {
let command_line = parse_command_line();
let verbose = command_line.is_present("verbose");
let word_list = match command_line.value_of("word-list") {
Some(file_path) => {
if verbose {
println!("Word list file: {}", file_path);
}
match read_word_list_from_file(file_path) {
Ok(list) => Some(list),
Err(error) => {
eprintln!("{}", error);
return;
}
}
}
None => None,
};
match command_line.subcommand_name() {
Some(CREATE_SUBCOMMAND) => {
match handle_create(
command_line
.subcommand_matches(CREATE_SUBCOMMAND)
.expect("The 'create' command must be specified."),
verbose,
word_list,
) {
Ok(seed_phrases) => {
println!();
println!("Created secret-shared seed phrases:");
println!("-----------------------------------");
for seed_phrase in seed_phrases {
println!("{}", seed_phrase);
}
}
Err(err) => {
println!();
eprintln!("{}", err);
}
};
}
Some(RECONSTRUCT_SUBCOMMAND) => {
match handle_reconstruct(
command_line
.subcommand_matches(RECONSTRUCT_SUBCOMMAND)
.expect("Error: The 'reconstruct' command must be specified."),
verbose,
word_list,
) {
Ok(seed_phrase) => {
println!();
println!("Reconstructed seed phrase:");
println!("--------------------------");
println!("{}", seed_phrase)
}
Err(err) => {
println!();
eprintln!("{}", err);
}
};
}
Some(GENERATE_SUBCOMMAND) => {
match handle_generate(
command_line
.subcommand_matches(GENERATE_SUBCOMMAND)
.expect("Error: The 'generate' command must be specified."),
verbose,
word_list,
) {
Ok(seed_phrase) => {
println!();
println!("Generated seed phrase:");
println!("----------------------");
println!("{}", seed_phrase)
}
Err(err) => {
println!();
eprintln!("{}", err);
}
};
}
Some(VALIDATE_SUBCOMMAND) => {
match handle_validate(
command_line
.subcommand_matches(VALIDATE_SUBCOMMAND)
.expect("Error: The 'validate' command must be specified."),
verbose,
word_list,
) {
Ok(()) => {
println!();
println!("The seed phrase is valid.");
}
Err(_) => {
println!();
println!("The seed phrase is NOT valid!");
}
}
}
_ => eprintln!("Error: A subcommand must be provided. Use --help to view options."),
};
}