use crate::math::FiniteFieldElement;
use crate::secret_sharing::get_modulus_for_words;
use crate::word_list::DEFAULT_WORD_LIST;
use crate::{HarpoError, HarpoResult, SeedPhraseResult};
use sha2::{Digest, Sha256};
use std::cmp;
use std::fmt;
const NUM_BITS_PER_WORD: usize = 11;
pub const NUM_BITS_FOR_INDEX: usize = 4;
const ENTROPY_INCREMENT: usize = 32;
#[derive(Eq, Debug)]
pub struct SeedPhrase {
words: Vec<String>,
index: Option<u32>,
}
impl SeedPhrase {
pub fn new(words: &[String]) -> Self {
let internal_words: Vec<String> = words.to_vec();
SeedPhrase {
words: internal_words,
index: None,
}
}
pub fn new_with_index(words: &[String], index: u32) -> Self {
let internal_words: Vec<String> = words.to_vec();
SeedPhrase {
words: internal_words,
index: Some(index),
}
}
pub fn len(&self) -> usize {
self.words.len()
}
pub fn is_empty(&self) -> bool {
self.words.len() == 0
}
pub fn get_words(&self) -> Vec<&str> {
self.words.iter().map(|s| s.as_str()).collect()
}
pub fn get_index(&self) -> Option<u32> {
self.index
}
pub fn get_num_bits(&self) -> usize {
((self.words.len() * NUM_BITS_PER_WORD) / ENTROPY_INCREMENT) * ENTROPY_INCREMENT
}
}
impl Clone for SeedPhrase {
fn clone(&self) -> SeedPhrase {
SeedPhrase {
words: self.words.clone(),
index: self.index,
}
}
}
impl fmt::Display for SeedPhrase {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut words_with_spaces = String::new();
for index in 0..(self.words.len() - 1) {
words_with_spaces.push_str(&self.words[index]);
words_with_spaces.push(' ');
}
words_with_spaces.push_str(&self.words[self.words.len() - 1]);
match self.index {
Some(index) => write!(formatter, "{}: {}", index, words_with_spaces),
None => write!(formatter, "{}", words_with_spaces),
}
}
}
impl PartialEq for SeedPhrase {
fn eq(&self, other: &Self) -> bool {
self.words == other.words
}
}
pub(crate) fn get_random_seed_phrase(num_words: usize, word_list: &[&str]) -> SeedPhraseResult {
if num_words % 3 != 0 || num_words < 12 || num_words > 24 {
return Err(HarpoError::InvalidParameter(
"The number of words must be 12, 15, 18, 21, or 24.".to_string(),
));
}
let num_bits = ((num_words * NUM_BITS_PER_WORD) / ENTROPY_INCREMENT) * ENTROPY_INCREMENT;
match get_modulus_for_words(num_words) {
Some(modulus) => {
let element = FiniteFieldElement::new_random(num_bits, &modulus);
get_seed_phrase_for_element(&element, word_list)
}
None => Err(HarpoError::InvalidSeedPhrase(
"Could not generate a seed phrase.".to_string(),
)),
}
}
fn get_index(word: &str, word_list: &[&str]) -> Option<usize> {
if word_list[0] == DEFAULT_WORD_LIST[0] {
let mut left = 0;
let mut right = word_list.len() - 1;
while left <= right {
let mid = ((left + right) / 2) as usize;
match word_list[mid] {
w if w == word => return Some(mid),
w if w < word => left = mid + 1,
_ => right = mid - 1,
};
}
} else {
for (index, word_at_index) in word_list.iter().enumerate() {
if word_at_index == &word {
return Some(index);
}
}
}
None
}
pub(crate) fn get_element_for_seed_phrase(
seed_phrase: &SeedPhrase,
word_list: &[&str],
) -> HarpoResult<FiniteFieldElement> {
let (element, _) = get_element_and_index_for_seed_phrase(seed_phrase, word_list)?;
Ok(element)
}
fn get_index_list(seed_phrase: &SeedPhrase, word_list: &[&str]) -> HarpoResult<Vec<usize>> {
let num_words = seed_phrase.len();
if num_words % 3 != 0 || num_words < 12 || num_words > 24 {
return Err(HarpoError::InvalidParameter(
"The number of words must be 12, 15, 18, 21, or 24.".to_string(),
));
}
let mut index_list: Vec<usize> = vec![];
for word in seed_phrase.get_words() {
match get_index(word, word_list) {
Some(index) => index_list.push(index),
None => {
return Err(HarpoError::InvalidSeedPhrase(format!(
"Invalid word in the seed phrase: {}",
word
)))
}
};
}
Ok(index_list)
}
pub(crate) fn is_compliant(seed_phrase: &SeedPhrase, word_list: &[&str]) -> bool {
match get_index_list(seed_phrase, word_list) {
Ok(index_list) => {
let bytes = get_bytes_from_indices(&index_list);
let num_used_bytes = (bytes.len() >> 2) << 2;
let mut used_bytes: Vec<u8> = vec![0; num_used_bytes];
used_bytes.clone_from_slice(&bytes[0..num_used_bytes]);
let mut hasher = Sha256::new();
hasher.update(&used_bytes);
let hash = hasher.finalize();
let num_words = seed_phrase.len();
let num_hash_bits = NUM_BITS_PER_WORD * num_words - (num_used_bytes << 3);
let num_zero_bits = 8 - num_hash_bits;
let hash_byte = (hash[0] >> num_zero_bits) << num_zero_bits;
hash_byte == bytes[num_used_bytes]
}
Err(_) => false,
}
}
pub(crate) fn get_element_and_index_for_seed_phrase(
seed_phrase: &SeedPhrase,
word_list: &[&str],
) -> HarpoResult<(FiniteFieldElement, u32)> {
let index_list = get_index_list(seed_phrase, word_list)?;
let bytes = get_bytes_from_indices(&index_list);
let num_used_bytes = (bytes.len() >> 2) << 2;
let mut used_bytes: Vec<u8> = vec![0; num_used_bytes];
used_bytes.clone_from_slice(&bytes[0..num_used_bytes]);
let num_words = seed_phrase.len();
let modulus = get_modulus_for_words(num_words).unwrap();
let index = if let Some(index) = seed_phrase.get_index() {
index
} else {
((bytes[num_used_bytes] >> (8 - NUM_BITS_FOR_INDEX)) + 1) as u32
};
Ok((FiniteFieldElement::new(&bytes, &modulus), index))
}
fn get_bytes_from_indices(indices: &[usize]) -> Vec<u8> {
let size = (indices.len() * NUM_BITS_PER_WORD + 7) / 8;
let mut bytes: Vec<u8> = vec![0; size];
let mut num_used_bits = 0;
let mut current_index = 0;
for index in indices {
let num_bits_first_byte = 8 - num_used_bits;
let num_bits_second_byte = cmp::min(8, 11 - num_bits_first_byte);
let num_bits_third_byte = cmp::max(0, 11 - num_bits_first_byte - num_bits_second_byte);
let first_byte_part = (index >> (11 - num_bits_first_byte)) as u8;
bytes[current_index] += first_byte_part;
current_index += 1;
let second_byte_part = ((index >> num_bits_third_byte) % (1 << num_bits_second_byte)) as u8;
bytes[current_index] = second_byte_part << (8 - num_bits_second_byte);
if num_bits_third_byte > 0 {
current_index += 1;
let third_byte_part = (index % (1 << num_bits_third_byte)) as u8;
bytes[current_index] = third_byte_part << (8 - num_bits_third_byte);
num_used_bits = num_bits_third_byte;
} else if num_bits_second_byte == 8 {
current_index += 1;
num_used_bits = 0;
} else {
num_used_bits = num_bits_second_byte;
}
}
bytes
}
pub(crate) fn get_seed_phrase_for_element(
element: &FiniteFieldElement,
word_list: &[&str],
) -> SeedPhraseResult {
get_seed_phrase_for_element_with_embedding(element, None, false, word_list)
}
pub(crate) fn get_seed_phrase_for_element_with_embedding(
element: &FiniteFieldElement,
index: Option<u32>,
embed_index: bool,
word_list: &[&str],
) -> SeedPhraseResult {
if embed_index && index.is_none() {
return Err(HarpoError::InvalidParameter(
"No index is provided to embed in the seed phrase.".into(),
));
}
let bytes = element.get_bytes();
let mut hasher = Sha256::new();
hasher.update(&bytes);
let hash = hasher.finalize();
let num_words = ((bytes.len() << 3) + NUM_BITS_PER_WORD - 1) / NUM_BITS_PER_WORD;
let total_num_bits = num_words * NUM_BITS_PER_WORD;
let mut encoded_words = vec![0; (total_num_bits + 7) >> 3];
encoded_words[..bytes.len()].clone_from_slice(&bytes[..]);
encoded_words[bytes.len()] = if embed_index {
match index {
Some(embedded_index) => (((embedded_index - 1) as u8) << 4) + (hash[0] % (1 << 4)),
None => hash[0],
}
} else {
hash[0]
};
let indices = get_indices_from_bytes(&encoded_words, num_words)?;
let words: Vec<String> = indices
.iter()
.map(|index| word_list[*index].to_string())
.collect();
if !embed_index {
match index {
Some(embedded_index) => Ok(SeedPhrase::new_with_index(&words, embedded_index)),
None => Ok(SeedPhrase::new(&words)),
}
} else {
Ok(SeedPhrase::new(&words))
}
}
fn get_indices_from_bytes(bytes: &[u8], num_words: usize) -> HarpoResult<Vec<usize>> {
let mut current_index: usize = 0;
let mut read_bits = 0;
let mut indices = vec![];
for byte in bytes {
if read_bits + 8 >= NUM_BITS_PER_WORD {
let processed_bits = NUM_BITS_PER_WORD - read_bits;
let remaining_bits = 8 - processed_bits;
let processed_part = (*byte as usize) >> remaining_bits;
current_index = (current_index << processed_bits) + processed_part;
indices.push(current_index);
current_index = (*byte as usize) % (1 << remaining_bits);
read_bits = remaining_bits;
} else {
current_index = (current_index << 8) + (*byte as usize);
read_bits += 8;
}
if indices.len() == num_words {
return Ok(indices);
}
}
Err(HarpoError::InvalidSeedPhrase(
"Error parsing indices from byte array.".to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secret_sharing::get_modulus_for_bits;
use rand::{seq::SliceRandom, Rng};
use std::error::Error;
const NUM_VALID_KEY_SIZES: usize = 5;
const NUM_TEST_RUNS: usize = 1000;
fn decode_hex_bytes(input: &str) -> Result<Vec<u8>, Box<dyn Error>> {
if input.len() % 2 != 0 {
Err("Error decoding hex string: The input length is not a multiple of 2.".into())
} else {
(0..input.len())
.step_by(2)
.map(|i| u8::from_str_radix(&input[i..i + 2], 16).map_err(|e| e.into()))
.collect()
}
}
#[test]
fn test_indices_from_bytes() {
let num_words = 4;
let bytes: &[u8] = &[107, 139, 93, 210, 150, 45];
let indices = get_indices_from_bytes(bytes, num_words).unwrap();
let expected_indices: Vec<usize> = vec![860, 727, 933, 354];
assert_eq!(indices, expected_indices);
let num_words = 5;
let bytes: &[u8] = &[229, 26, 179, 110, 211, 38, 214];
let indices = get_indices_from_bytes(bytes, num_words).unwrap();
let expected_indices: Vec<usize> = vec![1832, 1708, 1757, 1330, 875];
assert_eq!(indices, expected_indices);
}
fn test_seed_phrase_conversion_vector(hex_number: &str, phrase: &str) {
let value = decode_hex_bytes(hex_number).unwrap();
let modulus = get_modulus_for_bits(value.len() << 3).unwrap();
let element = FiniteFieldElement::new(&value, &modulus);
let seed_phrase = get_seed_phrase_for_element(&element, DEFAULT_WORD_LIST).unwrap();
let target_list: Vec<&str> = phrase.split(' ').collect();
assert_eq!(seed_phrase.get_words(), target_list);
let target_string_list: Vec<String> =
target_list.iter().map(|slice| slice.to_string()).collect();
let derived_seed_phrase = SeedPhrase::new(&target_string_list);
let derived_element =
get_element_for_seed_phrase(&derived_seed_phrase, DEFAULT_WORD_LIST).unwrap();
assert_eq!(derived_element, element);
}
#[test]
fn test_random_seed_phrase_conversion() {
let key_sizes: [usize; NUM_VALID_KEY_SIZES] = [16, 20, 24, 28, 32];
let mut rng = rand::thread_rng();
for _test in 0..NUM_TEST_RUNS {
let random_bytes = rng.gen::<[u8; 32]>();
let size = key_sizes.choose(&mut rng).unwrap();
let mut random_key: Vec<u8> = vec![0; *size];
random_key.clone_from_slice(&random_bytes[..*size]);
let modulus = get_modulus_for_bits(size << 3).unwrap();
let element = FiniteFieldElement::new(&random_key, &modulus);
let seed_phrase = get_seed_phrase_for_element(&element, DEFAULT_WORD_LIST).unwrap();
let derived_element =
get_element_for_seed_phrase(&seed_phrase, DEFAULT_WORD_LIST).unwrap();
assert_eq!(element, derived_element);
}
}
#[test]
fn test_random_seed_phrase_generation() {
let valid_num_words: [usize; NUM_VALID_KEY_SIZES] = [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 = get_random_seed_phrase(*num_words, DEFAULT_WORD_LIST)
.expect("A valid seed phrase should be generated.");
assert_eq!(seed_phrase.len(), *num_words);
assert!(is_compliant(&seed_phrase, DEFAULT_WORD_LIST));
}
}
macro_rules! tests {
($([$hex_number:expr, $phrase:expr]),*) => {
#[test]
fn test_seed_phrase_conversion() {
$(
test_seed_phrase_conversion_vector($hex_number, $phrase);
)*
}
};
}
tests! {
[
"00000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank yellow"
],
[
"80808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above"
],
[
"ffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
],
[
"000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will"
],
[
"808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when"
],
[
"0000000000000000000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title"
],
[
"8080808080808080808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote"
],
[
"9e885d952ad362caeb4efe34a8e91bd2",
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic"
],
[
"6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog"
],
[
"68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length"
],
[
"c0ba5a8e914111210f2bd131f3d5e08d",
"scheme spot photo card baby mountain device kick cradle pact join borrow"
],
[
"6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave"
],
[
"9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside"
],
[
"23db8160a31d3e0dca3688ed941adbf3",
"cat swing flag economy stadium alone churn speed unique patch report train"
],
[
"8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access"
],
[
"066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform"
],
[
"f30f8c1da665478f49b001d94c5fc452",
"vessel ladder alter error federal sibling chat ability sun glass valve picture"
],
[
"c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump"
],
[
"f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold"
]
}
}