diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdc789e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +todos.txt diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7448778 --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dbaf3f4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "seed-xor" +version = "0.1.0" +edition = "2018" +authors = ["KaiWitt "] +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" \ No newline at end of file diff --git a/README.md b/README.md index e2c00b0..690e701 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# seed-xor \ No newline at end of file +# 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 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7f9deba --- /dev/null +++ b/src/lib.rs @@ -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 { + 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::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 + } +}