Add binary, rename to seedxor, version 1.0.0
All checks were successful
moparisthebest/seedxor/pipeline/head This commit looks good

This commit is contained in:
Travis Burtrum 2023-09-25 23:42:04 -04:00
parent ee37db0592
commit 373e12b7ef
11 changed files with 608 additions and 107 deletions

49
.ci/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,49 @@
properties(
[
disableConcurrentBuilds()
]
)
node('linux && docker') {
try {
stage('Checkout') {
//branch name from Jenkins environment variables
echo "My branch is: ${env.BRANCH_NAME}"
// this doesn't grab tags pointing to this branch
//checkout scm
// this hack does... https://issues.jenkins.io/browse/JENKINS-45164
checkout([
$class: 'GitSCM',
branches: [[name: 'refs/heads/'+env.BRANCH_NAME]],
extensions: [[$class: 'CloneOption', noTags: false, shallow: false, depth: 0, reference: '']],
userRemoteConfigs: scm.userRemoteConfigs,
])
sh '''
set -euxo pipefail
git checkout "$BRANCH_NAME" --
git reset --hard "origin/$BRANCH_NAME"
'''
}
stage('Build + Deploy') {
sh '''
cargo clean
mkdir -p release
curl --compressed -sL https://code.moparisthebest.com/moparisthebest/self-ci/raw/branch/master/build-ci.sh | bash
ret=$?
docker system prune -af
exit $ret
'''
}
currentBuild.result = 'SUCCESS'
} catch (Exception err) {
currentBuild.result = 'FAILURE'
} finally {
stage('Email') {
step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'admin.jenkins@moparisthebest.com', sendToIndividuals: true])
}
deleteDir()
}
}

24
.ci/build.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -exo pipefail
echo "starting build for TARGET $TARGET"
export CRATE_NAME=seedxor
SUFFIX=""
echo "$TARGET" | grep -E '^x86_64-pc-windows-gnu$' >/dev/null && SUFFIX=".exe"
# build binary
cross build --target $TARGET --release
# to check how they are built
file "target/$TARGET/release/${CRATE_NAME}$SUFFIX"
# if this commit has a tag, upload artifact to release
strip "target/$TARGET/release/${CRATE_NAME}$SUFFIX" || true # if strip fails, it's fine
mkdir -p release
cp "target/$TARGET/release/${CRATE_NAME}$SUFFIX" "release/${CRATE_NAME}-$TARGET$SUFFIX"
echo 'build success!'
exit 0

View File

@ -1,17 +0,0 @@
on: [push]
name: CI
jobs:
build_and_test:
name: cargo_test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: test
args: --release --all-features

View File

@ -1,12 +0,0 @@
on: push
name: Clippy check
jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: rustup component add clippy
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target /target
/.idea
todos.txt todos.txt

63
Cargo.lock generated
View File

@ -4,61 +4,52 @@ version = 3
[[package]] [[package]]
name = "bip39" name = "bip39"
version = "1.0.1" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f" checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f"
dependencies = [ dependencies = [
"bitcoin_hashes", "bitcoin_hashes",
"rand_core",
"serde",
"unicode-normalization",
] ]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"
version = "0.9.7" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
[[package]] [[package]]
name = "maybe-uninit" name = "cfg-if"
version = "2.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "rand_core" name = "getrandom"
version = "0.4.2" version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "seed-xor" name = "libc"
version = "0.2.0" version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "seedxor"
version = "1.0.0"
dependencies = [ dependencies = [
"bip39", "bip39",
"getrandom",
] ]
[[package]] [[package]]
name = "serde" name = "wasi"
version = "1.0.128" version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "smallvec"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [
"maybe-uninit",
]
[[package]]
name = "unicode-normalization"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf"
dependencies = [
"smallvec",
]

View File

@ -1,17 +1,24 @@
[package] [package]
name = "seed-xor" name = "seedxor"
version = "0.2.0" version = "1.0.0"
edition = "2018" edition = "2021"
authors = ["KaiWitt <kaiwitt@protonmail.com>"] authors = ["moparisthebest <admin@moparisthebest.com>", "KaiWitt <kaiwitt@protonmail.com>"]
description = "XOR bip39 mnemonics." description = "XOR bip39 mnemonics."
readme = "README.md" readme = "README.md"
repository = "https://github.com/KaiWitt/seed-xor" repository = "https://github.com/moparisthebest/seedxor"
license = "MIT" license = "MIT"
keywords = ["bitcoin", "seed", "mnemonic", "bip39", "xor"] keywords = ["bitcoin", "seed", "mnemonic", "bip39", "xor"]
categories = ["cryptography::cryptocurrencies"] categories = ["cryptography::cryptocurrencies"]
publish = true publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html include = [
"**/*.rs",
"Cargo.toml",
"Cargo.lock",
"README.md",
"LICENSE",
]
[dependencies] [dependencies]
bip39 = "1.0.*" bip39 = { version = "2.0", default-features = false }
getrandom = { version = "0.2", default-features = false }

View File

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2021 Kai Copyright (c) 2021 Kai
Copyright (c) 2023 moparisthebest
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,17 +1,56 @@
# seed-xor # seedxor
seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/) 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). 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 is also possible to XOR mnemonics with differing numbers of words. It is also possible to XOR mnemonics with differing numbers of words.
For this the shorter one will be extended with the longer one's surplus entropy. For this the xored value takes on the entropy surplus of the longer seed.
## Command line example
## Example ```
usage: seedxor [options...]
-h, --help Display this help
-s, --split <seed> Split seed into num-seeds
-n, --num-seeds <num> 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 <num> Number of words to generate in the seed
default 24
-c, --combine <seeds...> Combine seeds into one seed
-r, --short Display only first 4 letters of seed words
```
```
$ seedxor -s 'silent toe meat possible chair blossom wait occur this worth option boy'
vault junior rather gentle fresh measure waste powder resemble ocean until body
defy one debate situate jungle music achieve cradle fiscal govern intact acquire
$ seedxor -c 'vault junior rather gentle fresh measure waste powder resemble ocean until body' 'defy one debate situate jungle music achieve cradle fiscal govern intact acquire'
silent toe meat possible chair blossom wait occur this worth option boy
$ seedxor -r -s 'silent toe meat possible chair blossom wait occur this worth option boy'
sieg facu mult uniq agai spar diag widt foll worl rela twis
abou rate brea eagl cann sile skin ging risk able conc util
$ seedxor -c 'sieg facu mult uniq agai spar diag widt foll worl rela twis' 'abou rate brea eagl cann sile skin ging risk able conc util'
silent toe meat possible chair blossom wait occur this worth option boy
$ seedxor -r -c 'sieg facu mult uniq agai spar diag widt foll worl rela twis' 'abou rate brea eagl cann sile skin ging risk able conc util'
sile toe meat poss chai blos wait occu this wort opti boy
$ seedxor -g
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
$ 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
```
## Library Example
```rust ```rust
use seed_xor::Mnemonic; use seedxor::{Mnemonic, SeedXor};
use std::str::FromStr; use std::str::FromStr;
// Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
@ -28,8 +67,10 @@ let c = Mnemonic::from_str(c_str).unwrap();
let result = Mnemonic::from_str(result_str).unwrap(); let result = Mnemonic::from_str(result_str).unwrap();
assert_eq!(result, a ^ b ^ c); assert_eq!(result, a ^ b ^ c);
```
## Useful resources // split a into 3 mnemonics
- Coldcard docs: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md let a = Mnemonic::from_str(a_str).unwrap();
- Easy bip39 mnemonic explanation: https://learnmeabitcoin.com/technical/mnemonic let split = a.splitn(3).unwrap();
let recombined_a = Mnemonic::xor_all(&split).unwrap();
assert_eq!(a_str, recombined_a.to_string());
```

View File

@ -1,17 +1,17 @@
//! # seed-xor //! # seedxor
//! //!
//! seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/) //! 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). //! 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 is also possible to XOR mnemonics with differing numbers of words. //! 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. //! For this the xored value takes on the entropy surplus of the longer seed.
//! //!
//!
//! ## Example //! ## Example
//! //!
//! ```rust //! ```rust
//! use seed_xor::Mnemonic; //! use seedxor::{Mnemonic, SeedXor};
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
//! // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md //! // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
@ -28,8 +28,17 @@
//! let result = Mnemonic::from_str(result_str).unwrap(); //! let result = Mnemonic::from_str(result_str).unwrap();
//! //!
//! assert_eq!(result, a ^ b ^ c); //! assert_eq!(result, a ^ b ^ c);
//!
//! // split a into 3 mnemonics
//! let a = Mnemonic::from_str(a_str).unwrap();
//! let split = a.splitn(3).unwrap();
//! let recombined_a = Mnemonic::xor_all(&split).unwrap();
//! assert_eq!(a_str, recombined_a.to_string());
//! ``` //! ```
//! //!
pub use bip39::{Error, Language};
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
use std::{ use std::{
fmt, fmt,
ops::{BitXor, BitXorAssign}, ops::{BitXor, BitXorAssign},
@ -40,13 +49,26 @@ use std::{
pub trait SeedXor { pub trait SeedXor {
/// XOR two values without consuming them. /// XOR two values without consuming them.
fn xor(&self, rhs: &Self) -> Self; fn xor(&self, rhs: &Self) -> Self;
fn xor_all(slice: &[Self]) -> Option<Self>
where
Self: Sized + Clone,
{
let first = slice.get(0)?;
// expensive clone :)
//let first = first.xor(first).xor(first);
let first = first.clone();
Some(slice.iter().skip(1).fold(first, |x, y| x.xor(y)))
}
} }
impl SeedXor for bip39::Mnemonic { impl SeedXor for bip39::Mnemonic {
/// XOR self with another [bip39::Mnemonic] without consuming it or itself. /// XOR self with another [bip39::Mnemonic] without consuming it or itself.
fn xor(&self, rhs: &Self) -> Self { fn xor(&self, rhs: &Self) -> Self {
let mut entropy = self.to_entropy(); let (mut entropy, entropy_len) = self.to_entropy_array();
let xor_values = rhs.to_entropy(); let (xor_values, xor_values_len) = rhs.to_entropy_array();
let entropy = &mut entropy[0..entropy_len];
let xor_values = &xor_values[0..xor_values_len];
// XOR each Byte // XOR each Byte
entropy entropy
@ -56,40 +78,144 @@ impl SeedXor for bip39::Mnemonic {
// Extend entropy with values of xor_values if it has a shorter entropy length. // Extend entropy with values of xor_values if it has a shorter entropy length.
if entropy.len() < xor_values.len() { if entropy.len() < xor_values.len() {
entropy.extend(xor_values.iter().skip(entropy.len())) let mut entropy = entropy.to_vec();
} entropy.extend(xor_values.iter().skip(entropy.len()));
bip39::Mnemonic::from_entropy(&entropy).unwrap()
} else {
// We unwrap here because entropy has either as many Bytes // We unwrap here because entropy has either as many Bytes
// as self or rhs and both are valid mnemonics. // as self or rhs and both are valid mnemonics.
bip39::Mnemonic::from_entropy(&entropy).unwrap() bip39::Mnemonic::from_entropy(&entropy).unwrap()
} }
} }
}
/// Wrapper for a [bip39::Mnemonic] for the implementation of `^` and `^=` operators. /// Wrapper for a [bip39::Mnemonic] for the implementation of `^` and `^=` operators.
#[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Mnemonic { pub struct Mnemonic {
/// Actual [bip39::Mnemonic] which is wrapped to be able to implement the XOR operator. /// Actual [bip39::Mnemonic] which is wrapped to be able to implement the XOR operator.
pub inner: bip39::Mnemonic, pub inner: bip39::Mnemonic,
} }
impl Mnemonic { impl Mnemonic {
/// Private constructor. pub fn split(&self) -> Result<[Self; 2], Error> {
fn new(inner: bip39::Mnemonic) -> Self { let random = Self::generate_in(self.language(), self.word_count())?;
Mnemonic { inner } let calc = self.xor(&random);
Ok([calc, random])
}
pub fn splitn(self, n: usize) -> Result<Vec<Self>, Error> {
let mut ret: Vec<Self> = Vec::with_capacity(n);
if n == 1 {
ret.push(self);
} else {
ret.extend_from_slice(&self.split()?);
for _ in 0..n - 2 {
let split = ret.pop().expect("cannot be empty").split()?;
ret.extend_from_slice(&split);
}
}
Ok(ret)
}
pub fn generate_in(language: Language, word_count: usize) -> Result<Self, Error> {
//let inner = bip39::Mnemonic::generate_in(language, word_count)?;
let mut inner = vec![0u8; (word_count / 3) * 4];
getrandom::getrandom(&mut inner)
.map_err(|e| Error::BadEntropyBitCount(e.code().get() as usize))?;
bip39::Mnemonic::from_entropy_in(language, &inner).map(|m| m.into())
} }
/// Wrapper for the same method as in [bip39::Mnemonic]. /// Wrapper for the same method as in [bip39::Mnemonic].
pub fn from_entropy(entropy: &[u8]) -> Result<Self, bip39::Error> { pub fn from_entropy(entropy: &[u8]) -> Result<Self, Error> {
match bip39::Mnemonic::from_entropy(entropy) { bip39::Mnemonic::from_entropy(entropy).map(|m| m.into())
Ok(inner) => Ok(Mnemonic::new(inner)), }
Err(err) => Err(err),
pub fn parse_normalized_without_checksum_check(s: &str) -> Result<Mnemonic, Error> {
let lang = bip39::Mnemonic::language_of(s).unwrap_or(Language::English);
Self::parse_in_normalized_without_checksum_check(lang, s)
}
pub fn parse_in_normalized_without_checksum_check(
language: Language,
s: &str,
) -> Result<Mnemonic, Error> {
bip39::Mnemonic::parse_in_normalized_without_checksum_check(
language,
&expand_words_in(language, s)?,
)
.map(|m| m.into())
}
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(' ');
s
});
ret.pop();
ret
}
pub fn to_display_string(&self, short: bool) -> String {
if short {
self.to_short_string()
} else {
self.to_string()
}
} }
} }
pub fn expand_words(seed: &str) -> Result<String, Error> {
let lang = bip39::Mnemonic::language_of(seed).unwrap_or(Language::English);
expand_words_in(lang, seed)
}
pub fn expand_words_in(language: Language, seed: &str) -> Result<String, Error> {
let mut ret = String::new();
for (i, prefix) in seed.to_lowercase().split_whitespace().enumerate() {
let words = language.words_by_prefix(prefix);
let word = if words.len() == 1 {
words[0]
} else if words.contains(&prefix) {
prefix
} else {
// println!("prefix: '{prefix}', words: {words:?}");
// not unique or correct prefix
return Err(Error::UnknownWord(i));
};
ret.push_str(word);
ret.push(' ');
}
ret.pop();
Ok(ret)
}
impl SeedXor for Mnemonic {
/// XOR two [Mnemonic]s without consuming them. /// XOR two [Mnemonic]s without consuming them.
/// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well. /// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well.
fn xor(&self, rhs: &Self) -> Self { fn xor(&self, rhs: &Self) -> Self {
Mnemonic::new(self.inner.xor(&rhs.inner)) self.inner.xor(&rhs.inner).into()
}
}
impl Deref for Mnemonic {
type Target = bip39::Mnemonic;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Mnemonic {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl From<bip39::Mnemonic> for Mnemonic {
fn from(inner: bip39::Mnemonic) -> Self {
Self { inner }
} }
} }
@ -97,10 +223,7 @@ impl FromStr for Mnemonic {
type Err = bip39::Error; type Err = bip39::Error;
fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> { fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
match bip39::Mnemonic::from_str(mnemonic) { bip39::Mnemonic::from_str(&expand_words(mnemonic)?).map(|m| m.into())
Ok(inner) => Ok(Mnemonic::new(inner)),
Err(err) => Err(err),
}
} }
} }
@ -116,6 +239,12 @@ impl fmt::Display for Mnemonic {
} }
} }
impl fmt::Debug for Mnemonic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
<Mnemonic as Display>::fmt(self, f)
}
}
impl BitXor for Mnemonic { impl BitXor for Mnemonic {
type Output = Self; type Output = Self;
@ -132,7 +261,7 @@ impl BitXorAssign for Mnemonic {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::Mnemonic; use crate::*;
use std::str::FromStr; use std::str::FromStr;
#[test] #[test]
@ -188,4 +317,126 @@ mod tests {
assert_eq!(result, w_24.clone() ^ w_16.clone() ^ w_12.clone()); assert_eq!(result, w_24.clone() ^ w_16.clone() ^ w_12.clone());
assert_eq!(result, w_12 ^ w_24 ^ w_16); // Commutative assert_eq!(result, w_12 ^ w_24 ^ w_16); // Commutative
} }
#[test]
fn seed_xor_works_12() {
// Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
let a_str = "romance wink lottery autumn shop bring dawn tongue range crater truth ability";
let b_str = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unhappy";
let c_str = "vault nominee cradle silk own frown throw leg cactus recall talent wait";
let result_str = "silent toe meat possible chair blossom wait occur this worth option boy";
let a = Mnemonic::from_str(a_str).unwrap();
let b = Mnemonic::from_str(b_str).unwrap();
let c = Mnemonic::from_str(c_str).unwrap();
let result = Mnemonic::from_str(result_str).unwrap();
assert_eq!(result, a.clone() ^ b.clone() ^ c.clone());
assert_eq!(result, b ^ c ^ a); // Commutative
}
#[test]
fn test_electrum_seed() {
let electrum_seed =
"ramp exotic resource icon sun addict equip sand leisure spare swing tobacco";
// ends up with the incorrect checksum
let expected = "ramp exotic resource icon sun addict equip sand leisure spare swing toast";
//let electrum_seed = Mnemonic::from_str(electrum_seed).unwrap();
let seed = Mnemonic::from(
bip39::Mnemonic::parse_in_normalized_without_checksum_check(
Language::English,
electrum_seed,
)
.unwrap(),
);
assert_eq!(electrum_seed, seed.to_string());
let expected = Mnemonic::from_str(expected).unwrap();
let split = seed.clone().split().unwrap();
println!("1split: '{split:?}'");
let result = Mnemonic::xor_all(&split).unwrap();
println!("result: '{}'", result);
if seed != result {
assert_eq!(expected, result);
}
for x in 1..=5 {
let split = seed.clone().splitn(x).unwrap();
assert_eq!(x, split.len());
println!("split: '{split:?}'");
let result = Mnemonic::xor_all(&split).unwrap();
println!("result: '{}'", result);
if seed != result {
assert_eq!(expected, result);
}
}
}
#[test]
fn derive_from_seed() {
// tl;dr for any seed you can generate a random seed and xor it to "split" it into 2 seeds
// you can then do that any number of times for the sub-seeds
let seed = "silent toe meat possible chair blossom wait occur this worth option boy";
let seed = Mnemonic::from_str(seed).unwrap();
let split = seed.clone().split().unwrap();
println!("split: '{split:?}'");
assert_eq!(seed.clone(), Mnemonic::xor_all(&split).unwrap());
for x in 1..=5 {
let split = seed.clone().splitn(x).unwrap();
assert_eq!(x, split.len());
println!("split: '{split:?}'");
assert_eq!(seed.clone(), Mnemonic::xor_all(&split).unwrap());
}
}
#[test]
fn expand_seed() {
let orig_seed = "silent toe meat possible chair blossom wait occur this worth option boy";
let seed = Mnemonic::from_str(orig_seed).unwrap();
let short_string = seed.to_short_string();
assert_eq!(
"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());
assert_eq!(orig_seed, expand_words(&short_string).unwrap());
// add and addict (addi) are both bip39 words, make sure those work
let orig_seed = "song vanish mistake night drink add modify lens average cool evil chest";
let seed = Mnemonic::from_str(orig_seed).unwrap();
let short_string = seed.to_short_string();
assert_eq!(
"song vani mist nigh drin add modi lens aver cool evil ches",
short_string
);
assert_eq!(
Language::English,
bip39::Mnemonic::language_of(&short_string).unwrap()
);
assert_eq!(orig_seed, expand_words(&short_string).unwrap());
let orig_seed = "ramp exotic resource icon sun addict equip sand leisure spare swing toast";
let seed = Mnemonic::from_str(orig_seed).unwrap();
let short_string = seed.to_short_string();
assert_eq!(
"ramp exot reso icon sun addi equi sand leis spar swin toas",
short_string
);
assert_eq!(
Language::English,
bip39::Mnemonic::language_of(&short_string).unwrap()
);
assert_eq!(orig_seed, expand_words(&short_string).unwrap());
}
} }

165
src/main.rs Normal file
View File

@ -0,0 +1,165 @@
use seedxor::{Language, Mnemonic, SeedXor};
use std::process::ExitCode;
use std::str::FromStr;
pub struct Args {
args: Vec<String>,
}
impl Args {
pub fn new(args: Vec<String>) -> Args {
Args { args }
}
pub fn flags(&mut self, flags: &[&str]) -> bool {
let mut i = 0;
while i < self.args.len() {
if flags.contains(&self.args[i].as_str()) {
self.args.remove(i);
return true;
} else {
i += 1;
}
}
false
}
pub fn flag(&mut self, flag: &str) -> bool {
self.flags(&[flag])
}
pub fn get_option(&mut self, flags: &[&str]) -> Option<String> {
let mut i = 0;
while i < self.args.len() {
if flags.contains(&self.args[i].as_str()) {
// remove the flag
self.args.remove(i);
return if i < self.args.len() {
Some(self.args.remove(i))
} else {
None
};
} else {
i += 1;
}
}
return None;
}
pub fn get_str(&mut self, flags: &[&str], def: &str) -> String {
match self.get_option(flags) {
Some(ret) => ret,
None => def.to_owned(),
}
}
pub fn get<T: FromStr>(&mut self, flags: &[&str], def: T) -> T {
match self.get_option(flags) {
Some(ret) => match ret.parse::<T>() {
Ok(ret) => ret,
Err(_) => def, // or panic
},
None => def,
}
}
pub fn remaining(self) -> Vec<String> {
self.args
}
}
impl Default for Args {
fn default() -> Self {
Self::new(std::env::args().skip(1).collect())
}
}
const NUM_SEEDS: usize = 2;
const WORD_COUNT: usize = 24;
fn help(success: bool) -> ExitCode {
println!(
r###"usage: seedxor [options...]
-h, --help Display this help
-s, --split <seed> Split seed into num-seeds
-n, --num-seeds <num> 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 <num> Number of words to generate in the seed
default {WORD_COUNT}
-c, --combine <seeds...> Combine seeds into one seed
-r, --short Display only first 4 letters of seed words
"###
);
if success {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
fn main() -> ExitCode {
let mut args = Args::default();
let short = args.flags(&["-r", "--short"]);
let num_seeds = args.get(&["-n", "--num-seeds"], NUM_SEEDS);
if num_seeds < 1 {
println!("error: num-seeds must be > 1");
return help(false);
} else if args.flags(&["-h", "--help"]) {
return help(true);
} else if args.flags(&["-s", "--split"]) {
let no_validate = args.flags(&["-y", "--no-validate"]);
let remaining = args.remaining();
if remaining.len() != 1 {
println!("remaining: {remaining:?}");
println!("error: --split needs exactly 1 seed argument");
return help(false);
}
let seed = &remaining[0];
let seed: Mnemonic = if no_validate {
Mnemonic::parse_normalized_without_checksum_check(seed).expect("invalid mnemonic")
} else {
Mnemonic::from_str(seed).expect("invalid bip39 mnemonic")
};
let parts = seed
.clone()
.splitn(num_seeds)
.expect("could not split mnemonic");
if !no_validate {
let result = Mnemonic::xor_all(&parts).unwrap();
if result != seed {
panic!("error: result != seed, '{result}' != '{seed}'");
}
}
for part in parts {
println!("{}", part.to_display_string(short));
}
} else if args.flags(&["-g", "--generate"]) {
let word_count = args.get(&["-w", "--word-count"], WORD_COUNT);
if !args.remaining().is_empty() {
println!("error: --generate needs 0 arguments");
return help(false);
}
for _ in 0..num_seeds {
println!(
"{}",
Mnemonic::generate_in(Language::English, word_count)
.expect("cannot generate seed")
.to_display_string(short)
);
}
} else if args.flags(&["-c", "--combine"]) {
let remaining = args.remaining();
if remaining.is_empty() {
println!("error: --combine needs > 0 arguments");
return help(false);
}
let parts: Vec<Mnemonic> = remaining
.into_iter()
.map(|s| Mnemonic::from_str(&s).expect("invalid bip39 mnemonic"))
.collect();
let seed = Mnemonic::xor_all(&parts).unwrap();
println!("{}", seed.to_display_string(short));
} else {
println!("error: need one of -s/-g/-c");
return help(false);
}
ExitCode::SUCCESS
}