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"
|
34
README.md
34
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