splitmonic 0.1.0

Split your BIP39 mnemonic phrase using shamir secret sharing
Documentation
use thiserror::Error;

use crate::wordlist::{English, Wordlist};

#[derive(Debug, Error, PartialEq, Clone)]
pub enum Error {
    #[error("this mnemonic length is invalid, expected {expected:?}, found: {given:?}\nmnemonic: {mnemonic:?}")]
    MnemonicLength {
        expected: usize,
        given: usize,
        mnemonic: String,
    },

    #[error(
        "invalid words found, invalid word indexes: {indexes:?},\ninvalid words: {invalid_words:?}"
    )]
    Words {
        indexes: Vec<usize>,
        invalid_words: Vec<String>,
        given_phrase: String,
    },

    #[error("invalid number of split phrases, expected: {expected:?}, found: {given:?}")]
    PhrasesLengthThreshold {
        expected: usize,
        given: usize,
        all_phrases: String,
    },

    #[error("found invalid split phrase lengths, the following phrases weren't long enough: {invalid_phrases:?}\n\
    they were expected to all be 28 words long. Instead they were of lengths: {invalid_phrase_lengths:?}")]
    PhraseLength {
        invalid_phrase_lengths: Vec<usize>,
        invalid_phrases: Vec<String>,
        all_phrases: String,
    },

    #[error("invalid words in split phrases: {0:?}")]
    InvalidSplitPhraseWords(Vec<(usize, Error)>),

    #[error("mismatched set(s), expected: {expected:?}, found: {given:?}")]
    MismatchedSet {
        expected: String,
        given: Vec<(usize, String)>,
    },
}

pub fn validate_mnemonic_code(mnemonic: &str) -> Result<(), Error> {
    let mnemonic_vec: Vec<&str> = mnemonic.split(' ').collect();

    if mnemonic_vec.len() != 24 {
        return Err(Error::MnemonicLength {
            expected: 24,
            given: mnemonic_vec.len(),
            mnemonic: mnemonic.to_string(),
        });
    }

    validate_all_correct_words(&mnemonic_vec)?;

    Ok(())
}

pub fn validate_split_phrases(split_phrases: Vec<String>) -> Result<(), Error> {
    if split_phrases.len() != 3 {
        return Err(Error::PhrasesLengthThreshold {
            expected: 3,
            given: split_phrases.len(),
            all_phrases: split_phrases.join("\n"),
        });
    }

    let split_phrases_vec: Vec<Vec<&str>> = split_phrases
        .iter()
        .map(|phrase| phrase.split(' ').collect())
        .collect();

    validate_lengths_of_phrases(&split_phrases_vec)?;
    validate_words_in_phrases(&split_phrases_vec)?;
    validate_part_of_same_set(&split_phrases_vec)?;

    Ok(())
}

fn validate_all_correct_words(mnemonic_vec: &[&str]) -> Result<(), Error> {
    let mut indexes = vec![];
    let mut invalid_words = vec![];

    for (index, word) in mnemonic_vec.iter().enumerate() {
        if English::get_index(word).is_err() {
            indexes.push(index);
            invalid_words.push(word.to_string());
        }
    }

    if !indexes.is_empty() {
        return Err(Error::Words {
            indexes,
            invalid_words,
            given_phrase: mnemonic_vec.join(" "),
        });
    }

    Ok(())
}

fn validate_lengths_of_phrases(split_phrases: &[Vec<&str>]) -> Result<(), Error> {
    let mut invalid_phrase_lengths = vec![];
    let mut invalid_phrases = vec![];

    for phrases in split_phrases {
        if phrases.len() != 28 {
            invalid_phrases.push(phrases.join(" "));
            invalid_phrase_lengths.push(phrases.len());
        }
    }

    if !invalid_phrases.is_empty() {
        return Err(Error::PhraseLength {
            invalid_phrase_lengths,
            invalid_phrases,
            all_phrases: split_phrases
                .iter()
                .cloned()
                .map(|phrases| phrases.join(" "))
                .collect::<Vec<String>>()
                .join("\n"),
        });
    }

    Ok(())
}

fn validate_words_in_phrases(split_phrases: &[Vec<&str>]) -> Result<(), Error> {
    let mut invalid_words: Vec<(usize, Error)> = vec![];

    for (index, phrases) in split_phrases.iter().enumerate() {
        if let Err(error) = validate_all_correct_words(phrases) {
            invalid_words.push((index, error))
        }
    }

    if !invalid_words.is_empty() {
        return Err(Error::InvalidSplitPhraseWords(invalid_words));
    }

    Ok(())
}

fn validate_part_of_same_set(split_phrases: &[Vec<&str>]) -> Result<(), Error> {
    let mut set_id = Vec::with_capacity(3);
    let mut mismatched_sets = vec![];

    for (index, split_phrase) in split_phrases.iter().enumerate() {
        if set_id.is_empty() {
            set_id = split_phrase[0..3].to_vec()
        }

        if set_id[0..3] != split_phrase[0..3] {
            mismatched_sets.push((index, split_phrase[0..3].join(" ")))
        }
    }

    if !mismatched_sets.is_empty() {
        return Err(Error::MismatchedSet {
            given: mismatched_sets,
            expected: set_id.join(" "),
        });
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn produces_error_on_wrong_length() {
        let error = validate_mnemonic_code("this is a fail").unwrap_err();

        assert_eq!(
            error,
            Error::MnemonicLength {
                expected: 24,
                given: 4,
                mnemonic: "this is a fail".to_string()
            }
        );

        assert_eq!(
            error.to_string(),
            "this mnemonic length is invalid, expected 24, found: 4\nmnemonic: \"this is a fail\""
        )
    }

    #[test]
    fn produces_error_on_wrong_words() {
        let mnemonic = "abandon abandon abandon abandon ford abandon abandon abandon abandon abandan abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon f150 art".to_string();
        let error = validate_mnemonic_code(&mnemonic).unwrap_err();

        assert_eq!(
            error,
            Error::Words {
                indexes: vec![4, 9, 22],
                invalid_words: vec![
                    "ford".to_string(),
                    "abandan".to_string(),
                    "f150".to_string()
                ],
                given_phrase: mnemonic
            }
        );
    }

    #[test]
    fn produces_error_when_not_enough_phrases() {
        let phrases = vec![
            "hello this is my first phrase".to_string(),
            "this is my second phrase".to_string(),
        ];
        let error = validate_split_phrases(phrases).unwrap_err();

        assert_eq!(
            error,
            Error::PhrasesLengthThreshold {
                expected: 3,
                given: 2,
                all_phrases: "hello this is my first phrase\nthis is my second phrase".to_string(),
            }
        )
    }

    #[test]
    fn produces_error_when_phrases_are_not_long_enough() {
        let phrases = vec![
            "hello this is my first phrase".to_string(),
            "this is my second phrase".to_string(),
            "third phrase".to_string(),
        ];

        let error = validate_split_phrases(phrases.clone()).unwrap_err();

        assert_eq!(
            error,
            Error::PhraseLength {
                invalid_phrase_lengths: vec![6, 5, 2],
                invalid_phrases: phrases,
                all_phrases:
                    "hello this is my first phrase\nthis is my second phrase\nthird phrase"
                        .to_string(),
            },
        )
    }

    #[test]
    fn test_validate_part_of_same_set() {
        let phrases: Vec<Vec<&str>> = vec![
            "hello hello hello some other random stuff",
            "hello hello hello more random stuff",
            "hello bad hello even more random stuff",
        ]
        .iter()
        .map(|phrase| phrase.split(' ').collect())
        .collect();

        let error = validate_part_of_same_set(&phrases).unwrap_err();

        assert_eq!(
            error,
            Error::MismatchedSet {
                given: vec![(2, "hello bad hello".to_string())],
                expected: "hello hello hello".to_string(),
            },
        )
    }
}