commit
723ebe1ad8
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
todos.txt
|
64
Cargo.lock
generated
Normal file
64
Cargo.lock
generated
Normal file
@ -0,0 +1,64 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "seed-xor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.128"
|
||||
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",
|
||||
]
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "seed-xor"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
authors = ["KaiWitt <kaiwitt@protonmail.com>"]
|
||||
description = "XOR bip39 mnemonics."
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/KaiWitt/seed-xor"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
bip39 = "1.0.1"
|
36
README.md
36
README.md
@ -1 +1,35 @@
|
||||
# seed-xor
|
||||
# seed-xor
|
||||
|
||||
seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/)
|
||||
and lets you XOR bip39 mnemonics as defined in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md).
|
||||
|
||||
|
||||
It is also possible to XOR mnemonics with differing numbers of words.
|
||||
For this the shorter one will be extended with 0s during the XOR calculation.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use seed_xor::Mnemonic;
|
||||
use std::str::FromStr;
|
||||
|
||||
// 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 miss spice fitness easy legal release recall obey exchange recycle dragon room";
|
||||
let b_str = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series noodle toy crowd jeans select depth lounge";
|
||||
let c_str = "vault nominee cradle silk own frown throw leg cactus recall talent worry gadget surface shy planet purpose coffee drip few seven term squeeze educate";
|
||||
let result_str = "silent toe meat possible chair blossom wait occur this worth option bag nurse find fish scene bench asthma bike wage world quit primary indoor";
|
||||
|
||||
// Mnemonic is a wrapper for bip39::Mnemonic which implements the XOR operation `^`.
|
||||
// Mnemonics can also be created from entropy.
|
||||
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 ^ 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
|
||||
|
185
src/lib.rs
Normal file
185
src/lib.rs
Normal file
@ -0,0 +1,185 @@
|
||||
//! # seed-xor
|
||||
//!
|
||||
//! seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/)
|
||||
//! and lets you XOR bip39 mnemonics as defined in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md).
|
||||
//!
|
||||
//!
|
||||
//! It is also possible to XOR mnemonics with differing numbers of words.
|
||||
//! For this the shorter one will be extended with 0s during the XOR calculation.
|
||||
//!
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use seed_xor::Mnemonic;
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! // 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 miss spice fitness easy legal release recall obey exchange recycle dragon room";
|
||||
//! let b_str = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series noodle toy crowd jeans select depth lounge";
|
||||
//! let c_str = "vault nominee cradle silk own frown throw leg cactus recall talent worry gadget surface shy planet purpose coffee drip few seven term squeeze educate";
|
||||
//! let result_str = "silent toe meat possible chair blossom wait occur this worth option bag nurse find fish scene bench asthma bike wage world quit primary indoor";
|
||||
//!
|
||||
//! // Mnemonic is a wrapper for bip39::Mnemonic which implements the XOR operation `^`.
|
||||
//! // Mnemonics can also be created from entropy.
|
||||
//! 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 ^ b ^ c);
|
||||
//! ```
|
||||
//!
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{BitXor, BitXorAssign},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bip39::Mnemonic as Inner;
|
||||
|
||||
/// Wrapper for a [bip39::Mnemonic] which is aliased as `Inner`.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct Mnemonic {
|
||||
/// Actual [bip39::Mnemonic] which is wrapped to be able to implement the XOR operator.
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
impl Mnemonic {
|
||||
/// Private constructor.
|
||||
fn new(inner: Inner) -> Self {
|
||||
Mnemonic { inner }
|
||||
}
|
||||
|
||||
/// Access the private inner [bip39::Mnemonic] for more functionality.
|
||||
pub fn inner(&self) -> &Inner {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Wrapper for the same method as in [bip39::Mnemonic].
|
||||
pub fn from_entropy(entropy: &[u8]) -> Result<Self, bip39::Error> {
|
||||
match Inner::from_entropy(entropy) {
|
||||
Ok(inner) => Ok(Mnemonic::new(inner)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let mut entropy = self.inner.to_entropy();
|
||||
let xor_values = rhs.inner.to_entropy();
|
||||
|
||||
// XOR each Byte
|
||||
entropy
|
||||
.iter_mut()
|
||||
.zip(xor_values.iter())
|
||||
.for_each(|(a, b)| *a ^= b);
|
||||
|
||||
// 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()))
|
||||
}
|
||||
|
||||
// We unwrap here because entropy has either as many Bytes
|
||||
// as self or rhs and both are valid mnemonics.
|
||||
Mnemonic::from_entropy(&entropy).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mnemonic {
|
||||
type Err = bip39::Error;
|
||||
|
||||
fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
|
||||
match Inner::from_str(mnemonic) {
|
||||
Ok(inner) => Ok(Mnemonic::new(inner)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mnemonic {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for (i, word) in self.inner.word_iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
f.write_str(word)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BitXor for Mnemonic {
|
||||
type Output = Self;
|
||||
|
||||
fn bitxor(self, rhs: Self) -> Self::Output {
|
||||
self.xor(&rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitXorAssign for Mnemonic {
|
||||
fn bitxor_assign(&mut self, rhs: Self) {
|
||||
*self = self.xor(&rhs)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Mnemonic;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn seed_xor_works() {
|
||||
// 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 miss spice fitness easy legal release recall obey exchange recycle dragon room";
|
||||
let b_str = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series noodle toy crowd jeans select depth lounge";
|
||||
let c_str = "vault nominee cradle silk own frown throw leg cactus recall talent worry gadget surface shy planet purpose coffee drip few seven term squeeze educate";
|
||||
let result_str = "silent toe meat possible chair blossom wait occur this worth option bag nurse find fish scene bench asthma bike wage world quit primary indoor";
|
||||
|
||||
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 seed_xor_assignment_works() {
|
||||
// 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 miss spice fitness easy legal release recall obey exchange recycle dragon room";
|
||||
let b_str = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series noodle toy crowd jeans select depth lounge";
|
||||
let c_str = "vault nominee cradle silk own frown throw leg cactus recall talent worry gadget surface shy planet purpose coffee drip few seven term squeeze educate";
|
||||
let result_str = "silent toe meat possible chair blossom wait occur this worth option bag nurse find fish scene bench asthma bike wage world quit primary indoor";
|
||||
|
||||
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();
|
||||
|
||||
let mut assigned = a.xor(&b); // XOR without consuming
|
||||
assigned ^= c;
|
||||
|
||||
assert_eq!(result, assigned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_xor_with_different_lengths_works() {
|
||||
// Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
|
||||
// but truncated mnemonics with correct last word.
|
||||
let str_24 = "romance wink lottery autumn shop bring dawn tongue range crater truth ability miss spice fitness easy legal release recall obey exchange recycle dragon room";
|
||||
let str_16 = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series number";
|
||||
let str_12 = "vault nominee cradle silk own frown throw leg cactus recall talent wisdom";
|
||||
let result_str = "silent toe meat possible chair blossom wait occur this worth option aware since milk mother grace rocket cement recall obey exchange recycle dragon rocket";
|
||||
|
||||
let w_24 = Mnemonic::from_str(str_24).unwrap();
|
||||
let w_16 = Mnemonic::from_str(str_16).unwrap();
|
||||
let w_12 = Mnemonic::from_str(str_12).unwrap();
|
||||
let result = Mnemonic::from_str(result_str).unwrap();
|
||||
|
||||
assert_eq!(result, w_24.clone() ^ w_16.clone() ^ w_12.clone());
|
||||
assert_eq!(result, w_12 ^ w_24 ^ w_16); // Commutative
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user