#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(unused_must_use)]
#![deny(unused_mut)]
use crate::Seed;
use anyhow::{bail, Error, Result};
use dictionary_1024::{index_of_word, word_at_index, words_match};
use sha2::{Digest, Sha256};
pub const SEED_ENTROPY_WORDS: usize = 13;
pub const SEED_CHECKSUM_WORDS: usize = 2;
pub fn seed_to_seed_phrase(seed: Seed) -> String {
let mut phrase: String = "".to_string();
let mut current_byte = 0;
let mut current_bit = 0;
for i in 0..SEED_ENTROPY_WORDS {
let mut bits = 10;
if i == SEED_ENTROPY_WORDS - 1 {
bits = 8;
}
let mut word_index: usize = 0;
for j in 0..bits {
let bit_is_set = (seed[current_byte] & (1 << (8 - current_bit - 1))) > 0;
if bit_is_set {
word_index |= 1 << (bits - j - 1);
}
current_bit += 1;
if current_bit == 8 {
current_bit = 0;
current_byte += 1;
}
}
if i != 0 {
phrase += " ";
}
phrase += &word_at_index(word_index);
}
let checksum_words = seed_to_checksum_words(seed);
phrase += " ";
phrase += &checksum_words[0];
phrase += " ";
phrase += &checksum_words[1];
phrase
}
pub fn seed_phrase_to_seed(phrase: &str) -> Result<Seed, Error> {
let all_words: Vec<&str> = phrase.split(' ').collect();
let expected_words = SEED_ENTROPY_WORDS + SEED_CHECKSUM_WORDS;
if all_words.len() != expected_words {
bail!(
"expecting {} words but got {} words",
expected_words,
all_words.len()
);
}
let mut seed: Seed = [0u8; 16];
let mut current_byte = 0;
let mut current_bit = 0;
for i in 0..SEED_ENTROPY_WORDS {
let word_index = index_of_word(all_words[i])?;
let mut bits = 10;
if i == SEED_ENTROPY_WORDS - 1 {
bits = 8;
if word_index > 255 {
bail!(
"seed phrase is not valid: {} cannot be the 13th word prefix",
&all_words[SEED_ENTROPY_WORDS - 1]
);
}
}
for j in 0..bits {
let bit_is_set = (word_index & (1 << (bits - j - 1))) > 0;
if bit_is_set {
seed[current_byte] |= 1 << (8 - current_bit - 1);
}
current_bit += 1;
if current_bit == 8 {
current_bit = 0;
current_byte += 1;
}
}
}
let checksum_words = seed_to_checksum_words(seed);
if !words_match(&checksum_words[0], all_words[SEED_ENTROPY_WORDS]) {
bail!(
"first checksum word is incorrect, expecting prefix {} but got {}",
checksum_words[0],
all_words[SEED_ENTROPY_WORDS]
);
}
if !words_match(&checksum_words[1], all_words[SEED_ENTROPY_WORDS + 1]) {
bail!(
"second checksum word is incorrect, expecting prefix {} but got {}",
checksum_words[1],
all_words[SEED_ENTROPY_WORDS + 1]
);
}
Ok(seed)
}
fn seed_to_checksum_words(seed: Seed) -> [String; SEED_CHECKSUM_WORDS] {
let mut hasher = Sha256::new();
hasher.update(&seed);
let r = hasher.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(&r);
let mut word1: usize = (result[0] as usize) << 8;
word1 += result[1] as usize;
word1 >>= 6;
let mut word2: usize = (result[1] as usize) << 10;
word2 &= 0xffff;
word2 += (result[2] as usize) << 2;
word2 >>= 6;
[word_at_index(word1), word_at_index(word2)]
}
pub fn valid_seed_phrase(phrase: &str) -> Result<(), Error> {
match seed_phrase_to_seed(phrase) {
Ok(_) => Ok(()),
Err(e) => bail!("seed phrase invalid: {}", e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::random_seed;
fn verify_conversion(seed: Seed) {
let phrase = seed_to_seed_phrase(seed);
println!("{}", phrase);
valid_seed_phrase(&phrase).unwrap();
let seed_conf = match seed_phrase_to_seed(&phrase) {
Ok(s) => s,
Err(e) => panic!("verify_conversion failed: {}\n\t{:?}", e, seed),
};
if seed != seed_conf {
panic!(
"seed conversion failed: \n\t{:?}\n\t{:?}\n\t{}",
seed, seed_conf, phrase
);
}
}
#[test]
fn check_unhappy_seeds() {
let good_seed = random_seed();
let good_phrase = seed_to_seed_phrase(good_seed);
let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
let wai0 = word_at_index(0);
let wai1 = word_at_index(1);
let wai2 = word_at_index(2);
phrase_words[0] = &wai0;
phrase_words[1] = &wai1;
phrase_words[2] = &wai2;
let bad_phrase = phrase_words.join(" ");
valid_seed_phrase(&bad_phrase).unwrap_err();
let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
phrase_words[0] = "ab";
let bad_phrase = phrase_words.join(" ");
valid_seed_phrase(&bad_phrase).unwrap_err();
let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
if phrase_words[14] == word_at_index(0) {
phrase_words[14] = &wai1;
} else {
phrase_words[14] = &wai0;
}
let bad_phrase = phrase_words.join(" ");
valid_seed_phrase(&bad_phrase).unwrap_err();
let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
phrase_words[0] = "abx";
let bad_phrase = phrase_words.join(" ");
valid_seed_phrase(&bad_phrase).unwrap_err();
let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
phrase_words.push(&wai0);
let bad_phrase = phrase_words.join(" ");
valid_seed_phrase(&bad_phrase).unwrap_err();
let phrase_words: Vec<&str> = good_phrase.split(" ").collect();
let bad_phrase = phrase_words[..14].join(" ");
valid_seed_phrase(&bad_phrase).unwrap_err();
}
#[test]
fn check_seed_phrases() {
let mut seed = [0u8; 16];
verify_conversion(seed);
seed[0] = 185;
verify_conversion(seed);
seed[1] = 46;
verify_conversion(seed);
seed[2] = 7;
verify_conversion(seed);
seed[3] = 1;
verify_conversion(seed);
seed[4] = 254;
verify_conversion(seed);
seed[5] = 2;
verify_conversion(seed);
for _ in 0..1000 {
let seed = random_seed();
verify_conversion(seed);
}
for _ in 0..1000 {
let seed = random_seed();
let phrase = seed_to_seed_phrase(seed);
let mut words: Vec<&str> = phrase.split(" ").collect();
let word_index = index_of_word(words[12]).unwrap();
if word_index > 255 {
panic!("seed generated randomly with 13th word out of bounds");
}
let wai = word_at_index(word_index + 256);
words[12] = &wai;
let mut altered_phrase = words[0].to_string();
for i in 1..words.len() {
altered_phrase += " ";
altered_phrase += words[i];
}
match valid_seed_phrase(&altered_phrase) {
Ok(()) => panic!("phrase should not be valid after manipulation"),
Err(_) => {}
};
}
}
}