From 15885cc00bc82c8a6816f7307f4370b2f66b3c47 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Thu, 7 Dec 2023 02:04:21 -0500 Subject: [PATCH] Add --unscramble and pad short words --- Cargo.lock | 9 +++++++- Cargo.toml | 7 +++--- README.md | 36 ++++++++++++++++++++----------- src/lib.rs | 24 +++++++++++++-------- src/main.rs | 61 ++++++++++++++++++++++++++++++++++++++++------------- 5 files changed, 97 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e9c5c3..b4635cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,12 +40,19 @@ version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +[[package]] +name = "permutohedron" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b687ff7b5da449d39e418ad391e5e08da53ec334903ddbb921db208908fc372c" + [[package]] name = "seedxor" -version = "1.0.0" +version = "1.1.0" dependencies = [ "bip39", "getrandom", + "permutohedron", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 79f7f9d..728a66b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seedxor" -version = "1.0.0" +version = "1.1.0" edition = "2021" authors = ["moparisthebest ", "KaiWitt "] description = "XOR bip39 mnemonics." @@ -20,5 +20,6 @@ include = [ ] [dependencies] -bip39 = { version = "2.0", default-features = false } -getrandom = { version = "0.2", default-features = false } +bip39 = { version = "2.0", default-features = false } +getrandom = { version = "0.2", default-features = false } +permutohedron = { version = "0.2.4", default-features = false } diff --git a/README.md b/README.md index 4b42372..fd0d34e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ seedxor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/) and is a fork of [seed-xor](https://github.com/kaiwolfram/seed-xor) and lets you XOR bip39 mnemonics as described in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md). -It also lets you split existing mnemonics into as many seeds as you wish +It also lets you split existing mnemonics into as many seeds as you wish, and unscramble parts of a seed in random order into valid seeds. It is also possible to XOR mnemonics with differing numbers of words. For this the xored value takes on the entropy surplus of the longer seed. @@ -12,17 +12,18 @@ For this the xored value takes on the entropy surplus of the longer seed. ``` usage: seedxor [options...] - -h, --help Display this help - -s, --split Split seed into num-seeds - -n, --num-seeds Number of seeds to split into or generate - default 2 - -y, --no-validate Do not validate a split can be successfully recombined, useful for - non-bip39 seeds, like ethereum - -g, --generate Generate num-seeds - -w, --word-count Number of words to generate in the seed - default 24 - -c, --combine Combine seeds into one seed - -r, --short Display only first 4 letters of seed words + -h, --help Display this help + -s, --split Split seed into num-seeds + -n, --num-seeds Number of seeds to split into or generate + default 2 + -y, --no-validate Do not validate a split can be successfully recombined, useful for + non-bip39 seeds, like ethereum + -g, --generate Generate num-seeds + -w, --word-count Number of words to generate in the seed + default 24 + -c, --combine Combine seeds into one seed + -r, --short Display only first 4 letters of seed words + -u, --unscramble Unscramble seed words in random order to valid seeds ``` ``` @@ -45,6 +46,17 @@ spell system smoke army frame vacant trick jacket anchor gasp acoustic supply de solar lab option erosion unit example convince viable soft smart smile spoon range card gentle miracle latin they verify want reject side cheese panther $ seedxor -c 'spell system smoke army frame vacant trick jacket anchor gasp acoustic supply deputy portion butter similar trend scorpion cause fish outer armor process faint' 'solar lab option erosion unit example convince viable soft smart smile spoon range card gentle miracle latin they verify want reject side cheese panther' butter patch first doll raise safe side lounge shiver protect solid area melody member lazy easily nice canvas stomach pattern claim slot million stomach + +$ seedxor -u 'affair mutual spare' 'smooth mushroom scale' 'include neck grab' 'fly maze obtain' +# total permutations: 24 +include neck grab smooth mushroom scale fly maze obtain affair mutual spare +# good: 1 bad: 23 total: 24 + +$ seedxor -u 'squirrel tray cheese' 'seek enhance oval' 'expect sense fish' 'total salad page' +# total permutations: 24 +squirrel tray cheese seek enhance oval expect sense fish total salad page +expect sense fish squirrel tray cheese seek enhance oval total salad page +# good: 2 bad: 22 total: 24 ``` ## Library Example diff --git a/src/lib.rs b/src/lib.rs index 6e55b29..e1ae2c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,11 +37,10 @@ //! ``` //! pub use bip39::{Error, Language}; -use std::fmt::Display; -use std::ops::{Deref, DerefMut}; use std::{ fmt, - ops::{BitXor, BitXorAssign}, + fmt::Display, + ops::{BitXor, BitXorAssign, Deref, DerefMut}, str::FromStr, }; @@ -149,11 +148,18 @@ impl Mnemonic { pub fn to_short_string(&self) -> String { let mut ret = self.word_iter().fold(String::new(), |mut s, w| { - w.chars().take(4).for_each(|c| s.push(c)); - s.push(' '); + if w.len() == 3 { + s.push_str(w); + s.push_str(" "); + } else { + w.chars().take(4).for_each(|c| s.push(c)); + s.push(' '); + } s }); - ret.pop(); + while ret.chars().last() == Some(' ') { + ret.pop(); + } ret } @@ -401,7 +407,7 @@ mod tests { let short_string = seed.to_short_string(); assert_eq!( - "sile toe meat poss chai blos wait occu this wort opti boy", + "sile toe meat poss chai blos wait occu this wort opti boy", short_string ); //assert_eq!(Language::English, bip39::Mnemonic::language_of(&short_string).unwrap()); @@ -414,7 +420,7 @@ mod tests { let short_string = seed.to_short_string(); assert_eq!( - "song vani mist nigh drin add modi lens aver cool evil ches", + "song vani mist nigh drin add modi lens aver cool evil ches", short_string ); assert_eq!( @@ -429,7 +435,7 @@ mod tests { let short_string = seed.to_short_string(); assert_eq!( - "ramp exot reso icon sun addi equi sand leis spar swin toas", + "ramp exot reso icon sun addi equi sand leis spar swin toas", short_string ); assert_eq!( diff --git a/src/main.rs b/src/main.rs index 5d8df9e..b21d578 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ -use seedxor::{Language, Mnemonic, SeedXor}; -use std::process::ExitCode; -use std::str::FromStr; +use seedxor::{expand_words, Language, Mnemonic, SeedXor}; +use std::{process::ExitCode, str::FromStr}; pub struct Args { args: Vec, @@ -74,17 +73,18 @@ const WORD_COUNT: usize = 24; fn help(success: bool) -> ExitCode { println!( r###"usage: seedxor [options...] - -h, --help Display this help - -s, --split Split seed into num-seeds - -n, --num-seeds Number of seeds to split into or generate - default {NUM_SEEDS} - -y, --no-validate Do not validate a split can be successfully recombined, useful for - non-bip39 seeds, like ethereum - -g, --generate Generate num-seeds - -w, --word-count Number of words to generate in the seed - default {WORD_COUNT} - -c, --combine Combine seeds into one seed - -r, --short Display only first 4 letters of seed words + -h, --help Display this help + -s, --split Split seed into num-seeds + -n, --num-seeds Number of seeds to split into or generate + default {NUM_SEEDS} + -y, --no-validate Do not validate a split can be successfully recombined, useful for + non-bip39 seeds, like ethereum + -g, --generate Generate num-seeds + -w, --word-count Number of words to generate in the seed + default {WORD_COUNT} + -c, --combine Combine seeds into one seed + -r, --short Display only first 4 letters of seed words + -u, --unscramble Unscramble seed words in random order to valid seeds "### ); if success { @@ -157,8 +157,39 @@ fn main() -> ExitCode { .collect(); let seed = Mnemonic::xor_all(&parts).unwrap(); println!("{}", seed.to_display_string(short)); + } else if args.flags(&["-u", "--unscramble"]) { + let remaining = args.remaining(); + if remaining.is_empty() { + println!("error: --unscramble needs > 0 arguments"); + return help(false); + } + let mut parts: Vec = remaining + .into_iter() + .map(|s| expand_words(&s).expect("invalid bip39 seed words")) + .collect(); + let total: u128 = (1..=parts.len() as u128).product(); + eprintln!("# total permutations: {total}"); + if total > u64::MAX as u128 { + println!("total too large, will never finish, aborting"); + return ExitCode::FAILURE; + } + let mut heap = permutohedron::Heap::new(&mut parts); + let mut good = 0u64; + while let Some(words) = heap.next_permutation() { + let words = words.join(" "); + if let Ok(mnemonic) = Mnemonic::from_str(&words) { + if short { + println!("{}", mnemonic.to_short_string()); + } else { + println!("{words}"); + } + good += 1; + } + } + let bad = total - good as u128; + eprintln!("# good: {good} bad: {bad} total: {total}"); } else { - println!("error: need one of -s/-g/-c"); + println!("error: need one of -s/-g/-c/-u"); return help(false); } ExitCode::SUCCESS