sskr-tool 0.1.1

A tool for splitting and recovering BIP-39 mnemonics according to the SSKR standard
use crate::bytewords::byteword_string;
use anyhow::{anyhow, bail, Error};
use bip39::{Language, Mnemonic, MnemonicType};
use dcbor::{CBOREncodable, CBOR};
use lazy_static::lazy_static;
use regex::Regex;
use sskr::{sskr_generate, GroupSpec, Secret, Spec};

lazy_static! {
    static ref SPEC_REGEX: Regex = Regex::new(r"^((\d+of\d+),)*\d+of\d+$").unwrap();
    static ref SPEC_GROUP_REGEX: Regex = Regex::new(r"(?<m>\d+)of(?<n>\d+)").unwrap();
}

pub fn split(
    spec: &String,
    group_threshold: usize,
    phrase: &String,
) -> Result<(Mnemonic, Vec<Vec<String>>), Error> {
    let sskr_spec = parse_spec(spec, group_threshold)?;
    let mnemonic = Mnemonic::from_phrase(phrase, Language::English)?;
    let entropy = mnemonic.entropy();
    let secret = Secret::new(entropy)?;
    let groups = sskr_generate(&sskr_spec, &secret)?;
    let byteword_groups = to_bytewords(&groups);
    Ok((mnemonic, byteword_groups))
}

pub fn split_random_phrase(
    spec: &String,
    group_threshold: usize,
) -> Result<(Mnemonic, Vec<Vec<String>>), Error> {
    let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
    split(spec, group_threshold, &mnemonic.phrase().to_string())
}

fn to_bytewords(groups: &Vec<Vec<Vec<u8>>>) -> Vec<Vec<String>> {
    groups
        .iter()
        .map(|shares| {
            shares
                .iter()
                .map(|share| {
                    let cbor = CBOR::tagged_value(309, CBOR::byte_string(share));
                    byteword_string(cbor.cbor_data().as_slice())
                })
                .collect()
        })
        .collect()
}

fn parse_spec(spec: &String, group_threshold: usize) -> Result<Spec, Error> {
    if !SPEC_REGEX.is_match(spec) {
        bail!("Invalid group spec");
    }

    let mut group_specs: Vec<GroupSpec> = vec![];

    for part in spec.split(",") {
        let Some(group_match) = SPEC_GROUP_REGEX.captures(&part) else {
            bail!("Invalid group \"{}\" in spec", &part);
        };

        let m = group_match["m"].parse()?;
        let n = group_match["n"].parse()?;

        if m > n {
            bail!(
                "Invalid group \"{}\" in spec ({} is greater than {})",
                &part,
                m,
                n
            );
        }

        if m == 1 && n > 1 {
            bail!(
                "Invalid group \"{}\" in spec: 1 of N groups (where N > 1) not supported",
                &part
            );
        }

        group_specs.push(
            GroupSpec::new(m, n)
                .map_err(|e| anyhow!("Error making group spec for group \"{}\": {}", &part, e))?,
        );
    }

    Ok(Spec::new(group_threshold, group_specs)?)
}