mod math;
pub mod seed_phrase;
mod secret_sharing;
mod word_list;
use secret_sharing::{reconstruct_secret, SecretPolynomial, SecretShare};
use seed_phrase::{
get_element_and_index_for_seed_phrase, get_element_for_seed_phrase, get_random_seed_phrase,
get_seed_phrase_for_element, get_seed_phrase_for_element_with_embedding, is_compliant,
SeedPhrase, NUM_BITS_FOR_INDEX,
};
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use word_list::DEFAULT_WORD_LIST;
pub const MAX_EMBEDDED_SHARES: usize = 1 << NUM_BITS_FOR_INDEX;
const NUM_WORDS_IN_LIST: usize = 2048;
#[derive(Debug)]
pub enum HarpoError {
InvalidSeedPhrase(String),
InvalidParameter(String),
IoError(std::io::Error),
ParseIntError(std::num::ParseIntError),
}
impl Display for HarpoError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HarpoError::InvalidSeedPhrase(message) => {
write!(formatter, "Invalid seed phrase error: {}", message)
}
HarpoError::InvalidParameter(message) => {
write!(formatter, "Invalid parameter error: {}", message)
}
HarpoError::IoError(error) => write!(formatter, "I/O error: {}", error),
HarpoError::ParseIntError(error) => write!(formatter, "Parsing error: {}", error),
}
}
}
impl From<std::io::Error> for HarpoError {
fn from(err: std::io::Error) -> Self {
HarpoError::IoError(err)
}
}
impl From<std::num::ParseIntError> for HarpoError {
fn from(err: std::num::ParseIntError) -> Self {
HarpoError::ParseIntError(err)
}
}
pub type HarpoResult<R> = Result<R, HarpoError>;
pub type SeedPhraseResult = HarpoResult<SeedPhrase>;
fn validate_word_list(word_list: &[&str]) -> HarpoResult<()> {
let mut word_set: HashSet<&str> = HashSet::new();
for word in word_list {
word_set.insert(word);
}
if word_set.len() != NUM_WORDS_IN_LIST {
return Err(HarpoError::InvalidSeedPhrase(format!(
"The word list contains {} distinct words instead of {}.",
word_set.len(),
NUM_WORDS_IN_LIST
)));
}
Ok(())
}
pub fn create_secret_shared_seed_phrases(
seed_phrase: &SeedPhrase,
threshold: usize,
num_seed_phrases: usize,
embed_indices: bool,
) -> HarpoResult<Vec<SeedPhrase>> {
create_secret_shared_seed_phrases_for_word_list(
seed_phrase,
threshold,
num_seed_phrases,
embed_indices,
DEFAULT_WORD_LIST,
)
}
pub fn create_secret_shared_seed_phrases_for_word_list(
seed_phrase: &SeedPhrase,
threshold: usize,
num_shares: usize,
embed_indices: bool,
word_list: &[&str],
) -> HarpoResult<Vec<SeedPhrase>> {
validate_word_list(word_list)?;
if threshold > num_shares {
return Err(HarpoError::InvalidParameter(
"The threshold must not exceed the number of secret-shared seed phrases.".to_string(),
));
}
if threshold < 1 {
return Err(HarpoError::InvalidParameter(
"The threshold must be at least 1.".to_string(),
));
}
if (num_shares > MAX_EMBEDDED_SHARES) && embed_indices {
return Err(HarpoError::InvalidParameter(format!(
"Only {} secret-shared pass phrases can be created with embedded indices.\n\
Use a smaller number of shares or turn off index embedding ('--no-embedding').",
MAX_EMBEDDED_SHARES
)));
}
if !is_compliant(seed_phrase, word_list) {
return Err(HarpoError::InvalidSeedPhrase(
"The seed phrase is not BIP-0039-compliant.".to_string(),
));
}
let secret = get_element_for_seed_phrase(seed_phrase, word_list)?;
let degree = threshold - 1;
let num_bits = seed_phrase.get_num_bits();
match SecretPolynomial::new(&secret, num_bits, degree) {
Some(polynomial) => {
let secret_shares = polynomial.get_secret_shares(num_shares as u32);
let mut seed_phrases = vec![];
for share in secret_shares {
let element = get_seed_phrase_for_element_with_embedding(
&share.element,
Some(share.index),
embed_indices,
word_list,
)?;
seed_phrases.push(element);
}
Ok(seed_phrases)
}
None => Err(HarpoError::InvalidParameter(
"Could not instantiate the required secret polynomial.".to_string(),
)),
}
}
pub fn reconstruct_seed_phrase(seed_phrases: &[SeedPhrase]) -> SeedPhraseResult {
reconstruct_seed_phrase_for_word_list(seed_phrases, DEFAULT_WORD_LIST)
}
pub fn reconstruct_seed_phrase_for_word_list(
seed_phrases: &[SeedPhrase],
word_list: &[&str],
) -> SeedPhraseResult {
validate_word_list(word_list)?;
if seed_phrases.is_empty() {
return Err(HarpoError::InvalidSeedPhrase(
"No seed phrases provided.".to_string(),
));
}
let num_words = seed_phrases[0].len();
if !(12..=24).contains(&num_words) || num_words % 3 != 0 {
return Err(HarpoError::InvalidSeedPhrase(
"Invalid number of words.".to_string(),
));
}
if seed_phrases.iter().any(|code| code.len() != num_words) {
return Err(HarpoError::InvalidSeedPhrase(
"Found seed phrases with different lengths.".to_string(),
));
}
for seed_phrase in seed_phrases {
if seed_phrase.get_index().is_some() && !is_compliant(seed_phrase, word_list) {
return Err(HarpoError::InvalidSeedPhrase(format!(
"Seed phrase is not BIP-0039-compliant: {}",
seed_phrase
)));
}
}
let mut secret_shares_map = HashMap::new();
for seed_phrase in seed_phrases {
let (element, index) = get_element_and_index_for_seed_phrase(seed_phrase, word_list)?;
secret_shares_map.insert(index, SecretShare::new(&element, index));
}
let secret_shares: Vec<SecretShare> = secret_shares_map.into_values().collect();
let secret_element = reconstruct_secret(&secret_shares);
get_seed_phrase_for_element(&secret_element, word_list)
}
pub fn generate_seed_phrase_for_word_list(
num_words: usize,
word_list: &[&str],
) -> SeedPhraseResult {
validate_word_list(word_list)?;
get_random_seed_phrase(num_words, word_list)
}
pub fn generate_seed_phrase(num_words: usize) -> SeedPhraseResult {
generate_seed_phrase_for_word_list(num_words, DEFAULT_WORD_LIST)
}
pub fn validate_seed_phrase(seed_phrase: &SeedPhrase) -> HarpoResult<()> {
validate_seed_phrase_for_word_list(seed_phrase, DEFAULT_WORD_LIST)
}
pub fn validate_seed_phrase_for_word_list(
seed_phrase: &SeedPhrase,
word_list: &[&str],
) -> HarpoResult<()> {
if is_compliant(seed_phrase, word_list) {
Ok(())
} else {
Err(HarpoError::InvalidSeedPhrase(
"The seed phrase is not BIP-0039-compliant.".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{seq::SliceRandom, Rng};
const NUM_SEED_PHRASE_LENGTHS: usize = 5;
const NUM_TEST_RUNS: usize = 10;
const MAX_NUM_SEED_PHRASES: usize = 64;
#[test]
fn test_create_secret_shared_seed_phrases() {
let words = [
"legal", "winner", "thank", "year", "wave", "sausage", "worth", "useful", "legal",
"winner", "thank", "yellow",
];
let seed_phrase =
SeedPhrase::new(&words.iter().map(|s| s.to_string()).collect::<Vec<String>>());
let seed_phrases = create_secret_shared_seed_phrases(&seed_phrase, 2, 3, true);
assert!(seed_phrases.is_ok());
assert_eq!(seed_phrases.unwrap().len(), 3);
let words = [
"legal", "winner", "thank", "year", "wave", "sausage", "worth", "useful", "legal",
"winner", "thank", "above",
];
let seed_phrase =
SeedPhrase::new(&words.iter().map(|s| s.to_string()).collect::<Vec<String>>());
let seed_phrases = create_secret_shared_seed_phrases(&seed_phrase, 2, 3, true);
assert!(seed_phrases.is_err());
}
#[test]
fn test_reconstruct_seed_phrase() {
let first_words = [
"coil", "reunion", "immune", "ignore", "custom", "gallery", "dutch", "trouble",
"snake", "ball", "wrong", "bike",
];
let second_words = [
"stable", "biology", "key", "post", "fiction", "concert", "hill", "step", "vibrant",
"ocean", "punch", "car",
];
let first_seed_phrase = SeedPhrase::new(
&first_words
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
);
let second_seed_phrase = SeedPhrase::new(
&second_words
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
);
let seed_phrases = [first_seed_phrase, second_seed_phrase];
let seed_phrase = reconstruct_seed_phrase(&seed_phrases);
assert!(seed_phrase.is_ok());
let expected_words = [
"letter", "advice", "cage", "absurd", "amount", "doctor", "acoustic", "avoid",
"letter", "advice", "cage", "above",
];
let expected_seed_phrase = SeedPhrase::new(
&expected_words
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
);
assert_eq!(seed_phrase.unwrap(), expected_seed_phrase);
}
#[test]
fn test_random_seed_phrase_reconstruction() {
let valid_num_words: [usize; NUM_SEED_PHRASE_LENGTHS] = [12, 15, 18, 21, 24];
let mut rng = rand::thread_rng();
for _test in 0..NUM_TEST_RUNS {
let num_words = valid_num_words
.choose(&mut rng)
.expect("A valid random number of words should be chosen.");
let seed_phrase = generate_seed_phrase(*num_words)
.expect("The generation of a seed phrase should work.");
let embed_indices = rng.gen::<bool>();
let num_seed_phrases = match embed_indices {
true => rng.gen_range(2..MAX_EMBEDDED_SHARES),
false => rng.gen_range(2..MAX_NUM_SEED_PHRASES),
};
let threshold = rng.gen_range(2..num_seed_phrases + 1);
let seed_phrases = create_secret_shared_seed_phrases(
&seed_phrase,
threshold,
num_seed_phrases,
embed_indices,
)
.expect("The creation of secret-shared seed phrases should work.");
let num_selected = rng.gen_range(threshold..num_seed_phrases + 1);
let selected_seed_phrases: Vec<SeedPhrase> = seed_phrases
.choose_multiple(&mut rng, num_selected)
.cloned()
.collect();
let reconstructed_seed_phrase = reconstruct_seed_phrase(&selected_seed_phrases)
.expect("The reconstruction of a seed-phrase should work.");
assert_eq!(seed_phrase, reconstructed_seed_phrase);
let num_selected = rng.gen_range(1..threshold);
let selected_seed_phrases: Vec<SeedPhrase> = seed_phrases
.choose_multiple(&mut rng, num_selected)
.cloned()
.collect();
let reconstructed_seed_phrase = reconstruct_seed_phrase(&selected_seed_phrases)
.expect("The reconstruction of a seed-phrase should work.");
assert_ne!(seed_phrase, reconstructed_seed_phrase);
}
}
#[test]
fn test_seed_phrase_generation_validation() {
let valid_num_words: [usize; NUM_SEED_PHRASE_LENGTHS] = [12, 15, 18, 21, 24];
let mut rng = rand::thread_rng();
for _test in 0..NUM_TEST_RUNS {
let num_words = valid_num_words
.choose(&mut rng)
.expect("A valid random number of words should be chosen.");
let seed_phrase = generate_seed_phrase(*num_words).expect("The generation should work");
assert!(validate_seed_phrase(&seed_phrase).is_ok());
}
}
#[test]
fn test_seed_phrase_validation_invalid_input() {
let words = [
"spacial", "race", "story", "silver", "cruel", "shield", "oil", "prevent", "gasp",
"airport", "ability", "master",
];
let seed_phrase = SeedPhrase::new(&words.map(String::from));
assert!(validate_seed_phrase(&seed_phrase).is_err());
let words = [
"twist", "deliver", "cycle", "jewel", "gas", "ski", "endless", "submit",
];
let seed_phrase = SeedPhrase::new(&words.map(String::from));
assert!(validate_seed_phrase(&seed_phrase).is_err());
let words = [
"level", "jupiter", "run", "review", "nature", "board", "excite", "gown", "young",
"town", "dish", "lemon",
];
let seed_phrase = SeedPhrase::new(&words.map(String::from));
assert!(validate_seed_phrase(&seed_phrase).is_err());
let words = [
"fit", "author", "country", "slice", "exhibit", "indicate", "piano", "filter", "wave",
"square", "vital", "motor",
];
let seed_phrase = SeedPhrase::new(&words.map(String::from));
assert!(validate_seed_phrase(&seed_phrase).is_err());
}
}