449 lines
17 KiB
Rust
449 lines
17 KiB
Rust
//! # seedxor
|
|
//!
|
|
//! 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 seedxor::{Mnemonic, SeedXor};
|
|
//! 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);
|
|
//!
|
|
//! // 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,
|
|
fmt::Display,
|
|
ops::{BitXor, BitXorAssign, Deref, DerefMut},
|
|
str::FromStr,
|
|
};
|
|
|
|
/// Trait for a `XOR`.
|
|
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, 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
|
|
.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() {
|
|
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, 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 {
|
|
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, 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| {
|
|
if w.len() == 3 {
|
|
s.push_str(w);
|
|
s.push_str(" ");
|
|
} else {
|
|
w.chars().take(4).for_each(|c| s.push(c));
|
|
s.push(' ');
|
|
}
|
|
s
|
|
});
|
|
while ret.chars().last() == Some(' ') {
|
|
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 {
|
|
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 }
|
|
}
|
|
}
|
|
|
|
impl FromStr for Mnemonic {
|
|
type Err = bip39::Error;
|
|
|
|
fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
|
|
bip39::Mnemonic::from_str(&expand_words(mnemonic)?).map(|m| m.into())
|
|
}
|
|
}
|
|
|
|
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 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;
|
|
|
|
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::*;
|
|
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
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
}
|