sskr-tool 0.1.0

A tool for splitting and recovering BIP-39 mnemonics according to the SSKR standard
use anyhow::{anyhow, Error};
use crc::{Crc, CRC_32_ISO_HDLC};
use lazy_static::lazy_static;
use std::collections::HashMap;

#[rustfmt::skip]
static WORDS: &'static str =
    "ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabias\
     bluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcost\
     cruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdull\
     dutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfish\
     fizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglow\
     goodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhope\
     hornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowl\
     judojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamb\
     lavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmany\
     mathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnote\
     numbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolpose\
     puffpumapurrquadquizraceramprealredorichroadrockroofrubyruinruns\
     rustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotask\
     taxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuser\
     vastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebs\
     whatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom";

lazy_static! {
    static ref WORD_LOOKUP: HashMap<&'static str, u8> = {
        let mut lookup = HashMap::new();
        for i in 0..=255 {
            lookup.insert(index_to_byteword(i), i);
        }
        lookup
    };
}

fn index_to_byteword(i: u8) -> &'static str {
    let begin: usize = (i as u16 * 4) as usize;
    let end: usize = (begin + 4) as usize;
    &WORDS[begin..end]
}

fn byteword_to_index(word: &str) -> u8 {
    WORD_LOOKUP[word]
}

fn byteword_checksum(bytes: &[u8]) -> [u8; 4] {
    Crc::<u32>::new(&CRC_32_ISO_HDLC)
        .checksum(bytes)
        .to_be_bytes()
}

pub fn byteword_string(bytes: &[u8]) -> String {
    let checksum = byteword_checksum(bytes);
    let data_with_checksum = [bytes, &checksum].concat();
    byteword_string_no_checksum(&data_with_checksum)
}

pub fn byteword_string_no_checksum(bytes: &[u8]) -> String {
    bytes
        .iter()
        .map(|i| index_to_byteword(*i))
        .collect::<Vec<&str>>()
        .join(" ")
}

pub fn byteword_string_to_bytes(input: String) -> Result<Vec<u8>, Error> {
    let words = input.split(" ");
    for word in words.clone() {
        if !WORD_LOOKUP.contains_key(word) {
            return Err(anyhow!("Not a valid byteword: \"{}\"", word));
        }
    }
    let all_bytes = words.map(byteword_to_index).collect::<Vec<u8>>();
    if all_bytes.len() < 5 {
        return Err(anyhow!(
            "Byteword string too short (must include checksum): \"{}\"",
            input
        ));
    }
    let (bytes, checksum) = all_bytes.split_at(all_bytes.len() - 4);
    if checksum != byteword_checksum(bytes) {
        return Err(anyhow!(
            "Invalid checksum (last 4 words) for byteword string \"{}\"",
            input
        ));
    }
    Ok(bytes.to_vec())
}