diff --git a/Cargo.toml b/Cargo.toml index 0c903a3..f952950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,6 @@ structopt = "0.3.2" tiny-bip39 = "0.6.2" [dev-dependencies] + +[patch.crates-io] +sssmc39 = { git = "https://github.com/wigy-opensource-developer/rust-sssmc39", branch = "fix/group_threshold_one" } \ No newline at end of file diff --git a/README.md b/README.md index d314e66..88a5cad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # slip39-rust +![Rust compilation results](https://github.com/Internet-of-People/slip39-rust/workflows/Rust/badge.svg) + [SLIP-0039](https://github.com/satoshilabs/slips/blob/master/slip-0039.md) compatible secret sharing tool ## Table of Contents diff --git a/src/main.rs b/src/main.rs index 7d6a4f3..c892363 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,157 +1,178 @@ -use bip39::{Mnemonic as Bip39Mnemonic, Language}; +use bip39::{Language, Mnemonic as Bip39Mnemonic}; use failure::{format_err, Fallible}; -use structopt::StructOpt; use regex::Regex; +use structopt::StructOpt; mod master_secret; mod slip39; pub use master_secret::MasterSecret; -pub use slip39::{Slip39, ShareInspector}; -use sssmc39::{Share, combine_mnemonics}; +pub use slip39::{ShareInspector, Slip39}; +use sssmc39::{combine_mnemonics, Share}; #[derive(Debug, StructOpt)] -#[structopt(rename_all="kebab")] +#[structopt(rename_all = "kebab")] enum Options { - /// Generate master secret and split it to parts - /// - /// SLIP-0039 defines a 2-level split: The master secret is split into group secrets and then - /// those are split further into member secrets. You can define the required and total number of - /// members in each group, and also define how many groups are required to restore the master secret. - Generate { - #[structopt(short, long, default_value = "256")] - /// Length of the master secret in bits - bits: u16, - #[structopt(flatten)] - split_options: SplitOptions, - }, - /// Split a master secret encoded as a BIP-0039 mnemonic into parts - /// - /// SLIP-0039 defines a 2-level split: The master secret is split into group secrets and then - /// those are split further into member secrets. You can define the required and total number of - /// members in each group, and also define how many groups are required to restore the master secret. - Split { - #[structopt(short, long, env = "SLIP39_ENTROPY", hide_env_values = true)] - /// BIP-0039 mnemonic to split. Use double quotes around it, but preferably provide it - /// through an environment variable to avoid leaking it to other processes on this machine - entropy: String, - #[structopt(long)] - /// If provided, mnemonic needs to be the master secret is decoded as hexadecimal, not as a - /// BIP39 mnemonic - hex: bool, - #[structopt(flatten)] - split_options: SplitOptions, - }, - Combine { - #[structopt(flatten)] - password: Password, - #[structopt(long)] - /// If provided, mnemonic needs to be the master secret is decoded as hexadecimal, not as a - /// BIP39 mnemonic - hex: bool, - #[structopt(short, long("mnemonic"), required=true, number_of_values=1)] - mnemonics: Vec, - }, - Inspect { - #[structopt(short, long)] - /// SLIP-0039 mnemonic to inspect. Use double quotes around it, but preferably provide it - /// through an environment variable to avoid leaking it to other processes on this machine - mnemonic: String, - } + /// Generate master secret and split it to parts + /// + /// SLIP-0039 defines a 2-level split: The master secret is split into group secrets and then + /// those are split further into member secrets. You can define the required and total number of + /// members in each group, and also define how many groups are required to restore the master secret. + Generate { + #[structopt(short, long, default_value = "256")] + /// Length of the master secret in bits + bits: u16, + #[structopt(flatten)] + split_options: SplitOptions, + }, + /// Split a master secret encoded as a BIP-0039 mnemonic into parts + /// + /// SLIP-0039 defines a 2-level split: The master secret is split into group secrets and then + /// those are split further into member secrets. You can define the required and total number of + /// members in each group, and also define how many groups are required to restore the master secret. + Split { + #[structopt(short, long, env = "SLIP39_ENTROPY", hide_env_values = true)] + /// BIP-0039 mnemonic to split. Use double quotes around it, but preferably provide it + /// through an environment variable to avoid leaking it to other processes on this machine + entropy: String, + #[structopt(long)] + /// If provided, mnemonic needs to be the master secret is decoded as hexadecimal, not as a + /// BIP39 mnemonic + hex: bool, + #[structopt(flatten)] + split_options: SplitOptions, + }, + Combine { + #[structopt(flatten)] + password: Password, + #[structopt(long)] + /// If provided, mnemonic needs to be the master secret is decoded as hexadecimal, not as a + /// BIP39 mnemonic + hex: bool, + #[structopt(short, long("mnemonic"), required = true, number_of_values = 1)] + mnemonics: Vec, + }, + Inspect { + #[structopt(short, long)] + /// SLIP-0039 mnemonic to inspect. Use double quotes around it, but preferably provide it + /// through an environment variable to avoid leaking it to other processes on this machine + mnemonic: String, + }, } #[derive(Debug, StructOpt)] struct Password { - #[structopt(short, long, env = "SLIP39_PASSWORD", hide_env_values = true)] - /// Password that is required in addition to the mnemonics to restore the master secret. Preferably - /// provide it through an environment variable to avoid leaking it to other processes. - password: String, + #[structopt(short, long, env = "SLIP39_PASSWORD", hide_env_values = true)] + /// Password that is required in addition to the mnemonics to restore the master secret. Preferably + /// provide it through an environment variable to avoid leaking it to other processes. + password: String, } -fn parse_group_spec(src: &str) -> Fallible<(u8,u8)> { - let pattern = Regex::new(r"^(?P\d+)(-?of-?|:|/)(?P\d+)$")?; - let captures = pattern.captures(src).ok_or_else(|| format_err!("Group specification '{}' is invalid. Write something like '5of8'", src))?; - let group_threshold: u8 = captures["group_threshold"].parse()?; - let group_members: u8 = captures["group_members"].parse()?; - Ok((group_threshold, group_members)) +fn parse_group_spec(src: &str) -> Fallible<(u8, u8)> { + let pattern = Regex::new(r"^(?P\d+)(-?of-?|:|/)(?P\d+)$")?; + let captures = pattern.captures(src).ok_or_else(|| { + format_err!( + "Group specification '{}' is invalid. Write something like '5of8'", + src + ) + })?; + let group_threshold: u8 = captures["group_threshold"].parse()?; + let group_members: u8 = captures["group_members"].parse()?; + Ok((group_threshold, group_members)) } #[derive(Debug, StructOpt)] struct SplitOptions { - #[structopt(flatten)] - password: Password, - #[structopt(short, long)] - /// Number of groups required for restoring the master secret (by default all groups are required) - required_groups: Option, -// TODO The sssmc39 crate does not handle well iteration_exponent values other than 0, so we disabled this parameter for now -// #[structopt(short, long, default_value = "0")] -// /// The higher this number, the safer and slower the splitting and combining is -// iterations: u8, - #[structopt(short, long("group"), parse(try_from_str = parse_group_spec), required=true, number_of_values=1)] - /// Specify required and total number of members for each group (e.g. 8-of-15). - /// Multiple groups need multiple occurences of this option. - groups: Vec<(u8,u8)> + #[structopt(flatten)] + password: Password, + #[structopt(short, long)] + /// Number of groups required for restoring the master secret (by default all groups are required) + required_groups: Option, + #[structopt(short, long, default_value = "0")] + /// The higher this number, the safer and slower the splitting and combining is + iterations: u8, + #[structopt(short, long("group"), parse(try_from_str = parse_group_spec), required=true, number_of_values=1)] + /// Specify required and total number of members for each group (e.g. 8-of-15). + /// Multiple groups need multiple occurences of this option. + groups: Vec<(u8, u8)>, } impl SplitOptions { - fn split(&self, master_secret: &MasterSecret) -> Fallible { - let required_groups = self.required_groups - .unwrap_or_else(|| self.groups.len() as u8); - let slip39 = Slip39::new( - required_groups, - &self.groups, - &master_secret, - &self.password.password, - 0 // self.iterations - )?; - Ok(slip39) - } + fn split(&self, master_secret: &MasterSecret) -> Fallible { + let required_groups = self + .required_groups + .unwrap_or_else(|| self.groups.len() as u8); + let slip39 = Slip39::new( + required_groups, + &self.groups, + &master_secret, + &self.password.password, + self.iterations, + )?; + Ok(slip39) + } } fn print_split(options: &SplitOptions, master_secret: &MasterSecret) -> Fallible<()> { - let slip39 = options.split(&master_secret)?; - println!("{}", serde_json::to_string_pretty(&slip39)?); - Ok(()) + let slip39 = options.split(&master_secret)?; + println!("{}", serde_json::to_string_pretty(&slip39)?); + Ok(()) } fn main() -> Fallible<()> { - use Options::*; - let options = Options::from_args(); - match options { - Generate { bits, split_options } => { - let master_secret = MasterSecret::new(bits)?; - print_split(&split_options, &master_secret)?; - } - Split { entropy, hex, split_options } => { - let master_secret = if hex { - let bytes = hex::decode(entropy)?; - MasterSecret::from(&bytes) - } else { - let bip39 = Bip39Mnemonic::from_phrase(entropy, Language::English)?; - MasterSecret::from(bip39.entropy()) - }; - print_split(&split_options, &master_secret)?; - } - Combine { password, hex, mnemonics} => { - let mnemonics = mnemonics.iter() - .map(|m| { - m.split_ascii_whitespace().map(str::to_owned).collect() - }) - .collect(); - let master_secret = combine_mnemonics(&mnemonics, &password.password)?; - let output = if hex { - hex::encode(master_secret) - } else { - let bip39 = bip39::Mnemonic::from_entropy(&master_secret, Language::English)?; - bip39.into_phrase() - }; - println!("{}", output); - } - Inspect { mnemonic } => { - let words = mnemonic.split_ascii_whitespace().map(str::to_owned).collect(); - let share = Share::from_mnemonic(&words)?; - println!("{}", serde_json::to_string_pretty(&ShareInspector::from(&share))?); - } - } - Ok(()) -} \ No newline at end of file + use Options::*; + let options = Options::from_args(); + match options { + Generate { + bits, + split_options, + } => { + let master_secret = MasterSecret::new(bits)?; + print_split(&split_options, &master_secret)?; + } + Split { + entropy, + hex, + split_options, + } => { + let master_secret = if hex { + let bytes = hex::decode(entropy)?; + MasterSecret::from(&bytes) + } else { + let bip39 = Bip39Mnemonic::from_phrase(entropy, Language::English)?; + MasterSecret::from(bip39.entropy()) + }; + print_split(&split_options, &master_secret)?; + } + Combine { + password, + hex, + mnemonics, + } => { + let mnemonics = mnemonics + .iter() + .map(|m| m.split_ascii_whitespace().map(str::to_owned).collect()) + .collect::>(); + let master_secret = combine_mnemonics(&mnemonics, &password.password)?; + let output = if hex { + hex::encode(master_secret) + } else { + let bip39 = bip39::Mnemonic::from_entropy(&master_secret, Language::English)?; + bip39.into_phrase() + }; + println!("{}", output); + } + Inspect { mnemonic } => { + let words = mnemonic + .split_ascii_whitespace() + .map(str::to_owned) + .collect::>(); + let share = Share::from_mnemonic(&words)?; + println!( + "{}", + serde_json::to_string_pretty(&ShareInspector::from(&share))? + ); + } + } + Ok(()) +} diff --git a/src/master_secret.rs b/src/master_secret.rs index 6621319..a20f4b7 100644 --- a/src/master_secret.rs +++ b/src/master_secret.rs @@ -4,38 +4,37 @@ use sssmc39::*; pub struct MasterSecret(Vec); impl MasterSecret { - pub fn new(strength_bits: u16) -> Result { - use rand::{thread_rng, Rng}; - let proto_share = Share::new()?; // shamir::share::ShareConfig is not exported - if strength_bits < proto_share.config.min_strength_bits { - return Err(ErrorKind::Value(format!( - "The requested strength of the master secret({} bits) must be at least {} bits.", - strength_bits, proto_share.config.min_strength_bits, - )))?; - } - if strength_bits % 16 != 0 { - return Err(ErrorKind::Value(format!( + pub fn new(strength_bits: u16) -> Result { + use rand::{thread_rng, Rng}; + let proto_share = Share::new()?; // shamir::share::ShareConfig is not exported + if strength_bits < proto_share.config.min_strength_bits { + return Err(ErrorKind::Value(format!( + "The requested strength of the master secret({} bits) must be at least {} bits.", + strength_bits, proto_share.config.min_strength_bits, + )))?; + } + if strength_bits % 16 != 0 { + return Err(ErrorKind::Value(format!( "The requested strength of the master secret({} bits) must be a multiple of 16 bits.", strength_bits, )))?; - } - let mut v = vec![]; - for _ in 0..strength_bits as usize / 8 { - v.push(thread_rng().gen()); - } - Ok(Self(v)) - } + } + let mut v = vec![]; + for _ in 0..strength_bits as usize / 8 { + v.push(thread_rng().gen()); + } + Ok(Self(v)) + } } impl> From for MasterSecret { - fn from(value: T) -> Self { - Self( value.as_ref().to_owned() ) - } + fn from(value: T) -> Self { + Self(value.as_ref().to_owned()) + } } impl AsRef> for MasterSecret { - fn as_ref(&self) -> &Vec { - &self.0 - } + fn as_ref(&self) -> &Vec { + &self.0 + } } - diff --git a/src/slip39.rs b/src/slip39.rs index e1a882d..d98337b 100644 --- a/src/slip39.rs +++ b/src/slip39.rs @@ -1,74 +1,79 @@ -use sssmc39::*; use serde::{Serialize, Serializer}; +use sssmc39::*; use crate::MasterSecret; pub struct Slip39(Vec); impl Slip39 { - #[allow(clippy::ptr_arg)] // until PR is merged - pub fn new( - group_threshold: u8, - groups: &Vec<(u8, u8)>, - master_secret: &MasterSecret, - passphrase: &str, - iteration_exponent: u8, - ) -> Result { - let group_shares = generate_mnemonics( - group_threshold, - groups, - master_secret.as_ref(), - passphrase, - iteration_exponent - )?; - Ok(Self(group_shares)) - } - - pub fn iter(&self) -> std::slice::Iter<'_, GroupShare> { - self.0.iter() - } + pub fn new( + group_threshold: u8, + groups: &[(u8, u8)], + master_secret: &MasterSecret, + passphrase: &str, + iteration_exponent: u8, + ) -> Result { + let group_shares = generate_mnemonics( + group_threshold, + groups, + master_secret.as_ref(), + passphrase, + iteration_exponent, + )?; + Ok(Self(group_shares)) + } + + pub fn iter(&self) -> std::slice::Iter<'_, GroupShare> { + self.0.iter() + } } #[derive(Serialize)] struct ShareFormatter { - group_index: u8, - member_index: u8, - mnemonic: String, + group_index: u8, + member_index: u8, + mnemonic: String, } impl From<&Share> for ShareFormatter { - fn from(share: &Share) -> Self { - let mnemonic = share.to_mnemonic().expect("formatting a valid mnemonic should not get an error"); - Self { - member_index: share.member_index + 1, - group_index: share.group_index + 1, - mnemonic: mnemonic.join(" "), - } - } + fn from(share: &Share) -> Self { + let mnemonic = share + .to_mnemonic() + .expect("formatting a valid mnemonic should not get an error"); + Self { + member_index: share.member_index + 1, + group_index: share.group_index + 1, + mnemonic: mnemonic.join(" "), + } + } } #[derive(Serialize)] struct GroupShareFormatter { - member_threshold: u8, - member_count: u8, - shares: Vec, + member_threshold: u8, + member_count: u8, + shares: Vec, } impl From<&GroupShare> for GroupShareFormatter { - fn from(group: &GroupShare) -> Self { - Self { - member_threshold: group.member_threshold, - member_count: group.member_shares.len() as u8, - shares: group.member_shares.iter().map(ShareFormatter::from).collect(), - } - } + fn from(group: &GroupShare) -> Self { + Self { + member_threshold: group.member_threshold, + member_count: group.member_shares.len() as u8, + shares: group + .member_shares + .iter() + .map(ShareFormatter::from) + .collect(), + } + } } #[derive(Serialize)] struct Slip39Formatter { - group_count: u8, - group_threshold: u8, - groups: Vec, + group_count: u8, + group_threshold: u8, + groups: Vec, } impl> From for Slip39Formatter { @@ -84,11 +89,12 @@ impl> From for Slip39Formatter { } impl Serialize for Slip39 { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where S: Serializer - { - Slip39Formatter::from(&self.0).serialize(serializer) - } + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + Slip39Formatter::from(&self.0).serialize(serializer) + } } #[derive(Serialize)] @@ -113,3 +119,31 @@ impl From<&Share> for ShareInspector { } } } + +#[cfg(test)] +mod test { + + use super::*; + use bip39::{Language, Mnemonic}; + use failure::Error; + + /// https://github.com/Internet-of-People/slip39-rust/issues/3 + #[test] + fn group_threshold() -> Result<(), Error> { + let bip39 = Mnemonic::from_phrase( + "shell view flock obvious believe final afraid caught page second arrow predict", + Language::English, + )?; + let passphrase = "morpheus"; + let master_secret = MasterSecret::from(bip39.entropy()); + let groups = vec![(3, 5), (3, 6)]; + let slip39 = Slip39::new(1, &groups, &master_secret, &passphrase, 0)?; + + let group_shares = slip39.iter().collect::>(); + + println!("{}", serde_json::to_string_pretty(&slip39)?); + assert_eq!(group_shares.len(), 2); + + Ok(()) + } +}