Add binary, rename to seedxor, version 1.0.0
All checks were successful
moparisthebest/seedxor/pipeline/head This commit looks good
All checks were successful
moparisthebest/seedxor/pipeline/head This commit looks good
This commit is contained in:
parent
ee37db0592
commit
373e12b7ef
49
.ci/Jenkinsfile
vendored
Normal file
49
.ci/Jenkinsfile
vendored
Normal 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
24
.ci/build.sh
Executable 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
|
17
.github/workflows/cargo.yml
vendored
17
.github/workflows/cargo.yml
vendored
@ -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
|
12
.github/workflows/clippy.yml
vendored
12
.github/workflows/clippy.yml
vendored
@ -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
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
/.idea
|
||||
todos.txt
|
||||
|
63
Cargo.lock
generated
63
Cargo.lock
generated
@ -4,61 +4,52 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "1.0.1"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f"
|
||||
checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.9.7"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
||||
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seed-xor"
|
||||
version = "0.2.0"
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
|
||||
[[package]]
|
||||
name = "seedxor"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.128"
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834"
|
||||
|
||||
[[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",
|
||||
]
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
21
Cargo.toml
21
Cargo.toml
@ -1,17 +1,24 @@
|
||||
[package]
|
||||
name = "seed-xor"
|
||||
version = "0.2.0"
|
||||
edition = "2018"
|
||||
authors = ["KaiWitt <kaiwitt@protonmail.com>"]
|
||||
name = "seedxor"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
authors = ["moparisthebest <admin@moparisthebest.com>", "KaiWitt <kaiwitt@protonmail.com>"]
|
||||
description = "XOR bip39 mnemonics."
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/KaiWitt/seed-xor"
|
||||
repository = "https://github.com/moparisthebest/seedxor"
|
||||
license = "MIT"
|
||||
keywords = ["bitcoin", "seed", "mnemonic", "bip39", "xor"]
|
||||
categories = ["cryptography::cryptocurrencies"]
|
||||
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]
|
||||
bip39 = "1.0.*"
|
||||
bip39 = { version = "2.0", default-features = false }
|
||||
getrandom = { version = "0.2", default-features = false }
|
||||
|
1
LICENSE
1
LICENSE
@ -1,6 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Kai
|
||||
Copyright (c) 2023 moparisthebest
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
59
README.md
59
README.md
@ -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).
|
||||
|
||||
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.
|
||||
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
|
||||
use seed_xor::Mnemonic;
|
||||
use seedxor::{Mnemonic, SeedXor};
|
||||
use std::str::FromStr;
|
||||
|
||||
// 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();
|
||||
|
||||
assert_eq!(result, a ^ b ^ c);
|
||||
```
|
||||
|
||||
## Useful resources
|
||||
- Coldcard docs: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
|
||||
- Easy bip39 mnemonic explanation: https://learnmeabitcoin.com/technical/mnemonic
|
||||
// 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());
|
||||
```
|
||||
|
297
src/lib.rs
297
src/lib.rs
@ -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).
|
||||
//!
|
||||
//! 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.
|
||||
//! For this the xored value takes on the entropy surplus of the longer seed.
|
||||
//!
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use seed_xor::Mnemonic;
|
||||
//! use seedxor::{Mnemonic, SeedXor};
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! // 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();
|
||||
//!
|
||||
//! 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::{
|
||||
fmt,
|
||||
ops::{BitXor, BitXorAssign},
|
||||
@ -40,13 +49,26 @@ use std::{
|
||||
pub trait SeedXor {
|
||||
/// XOR two values without consuming them.
|
||||
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 {
|
||||
/// XOR self with another [bip39::Mnemonic] without consuming it or itself.
|
||||
fn xor(&self, rhs: &Self) -> Self {
|
||||
let mut entropy = self.to_entropy();
|
||||
let xor_values = rhs.to_entropy();
|
||||
let (mut entropy, entropy_len) = self.to_entropy_array();
|
||||
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
|
||||
entropy
|
||||
@ -56,40 +78,144 @@ impl SeedXor for bip39::Mnemonic {
|
||||
|
||||
// Extend entropy with values of xor_values if it has a shorter entropy length.
|
||||
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
|
||||
// as self or rhs and both are valid mnemonics.
|
||||
bip39::Mnemonic::from_entropy(&entropy).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Actual [bip39::Mnemonic] which is wrapped to be able to implement the XOR operator.
|
||||
pub inner: bip39::Mnemonic,
|
||||
}
|
||||
|
||||
impl Mnemonic {
|
||||
/// Private constructor.
|
||||
fn new(inner: bip39::Mnemonic) -> Self {
|
||||
Mnemonic { inner }
|
||||
pub fn split(&self) -> Result<[Self; 2], Error> {
|
||||
let random = Self::generate_in(self.language(), self.word_count())?;
|
||||
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].
|
||||
pub fn from_entropy(entropy: &[u8]) -> Result<Self, bip39::Error> {
|
||||
match bip39::Mnemonic::from_entropy(entropy) {
|
||||
Ok(inner) => Ok(Mnemonic::new(inner)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
pub fn from_entropy(entropy: &[u8]) -> Result<Self, Error> {
|
||||
bip39::Mnemonic::from_entropy(entropy).map(|m| m.into())
|
||||
}
|
||||
|
||||
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.
|
||||
/// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well.
|
||||
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;
|
||||
|
||||
fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
|
||||
match bip39::Mnemonic::from_str(mnemonic) {
|
||||
Ok(inner) => Ok(Mnemonic::new(inner)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
bip39::Mnemonic::from_str(&expand_words(mnemonic)?).map(|m| m.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
type Output = Self;
|
||||
|
||||
@ -132,7 +261,7 @@ impl BitXorAssign for Mnemonic {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Mnemonic;
|
||||
use crate::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
@ -188,4 +317,126 @@ mod tests {
|
||||
assert_eq!(result, w_24.clone() ^ w_16.clone() ^ w_12.clone());
|
||||
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
165
src/main.rs
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user