XOR seeds of differing entropy

This commit is contained in:
kaiwitt 2021-08-24 09:30:59 +02:00
parent f947ffdad9
commit 3fccb84af2
No known key found for this signature in database
GPG Key ID: 2D2F3D411C032247
2 changed files with 74 additions and 105 deletions

View File

@ -1,21 +1,27 @@
# seed-xor # seed-xor
seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/) seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/)
and lets you XOR 24-word mnemonics as defined in [Coldcard docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md). and lets you XOR bip39 mnemonics as defined in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md).
Future versions will also allow you to XOR different seed lengths. 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 ## Example
```rust ```rust
use seed_xor::Mnemonic;
use std::str::FromStr;
// Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md // 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 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 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 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 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 a = Mnemonic::from_str(a_str).unwrap();
let b = Mnemonic::from_str(b_str).unwrap(); let b = Mnemonic::from_str(b_str).unwrap();
let c = Mnemonic::from_str(c_str).unwrap(); let c = Mnemonic::from_str(c_str).unwrap();
@ -26,4 +32,4 @@
## Useful resources ## Useful resources
- Coldcard docs: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md - Coldcard docs: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
- Easy mnemonic seed explanation: https://learnmeabitcoin.com/technical/mnemonic - Easy bip39 mnemonic explanation: https://learnmeabitcoin.com/technical/mnemonic

View File

@ -1,10 +1,11 @@
//! # seed-xor //! # seed-xor
//! //!
//! seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/) //! seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/)
//! and lets you XOR 24-word mnemonics as defined in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md). //! and lets you XOR bip39 mnemonics as defined in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md).
//! //!
//! //!
//! Future versions will also allow you to XOR different seed lengths. //! 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 //! ## Example
@ -19,8 +20,8 @@
//! 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 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 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 ensures a 24 word seed length. //! // Mnemonic is a wrapper for bip39::Mnemonic which implements the XOR operation `^`.
//! // Mnemonics can also be created from 256bit entropy. //! // Mnemonics can also be created from entropy.
//! let a = Mnemonic::from_str(a_str).unwrap(); //! let a = Mnemonic::from_str(a_str).unwrap();
//! let b = Mnemonic::from_str(b_str).unwrap(); //! let b = Mnemonic::from_str(b_str).unwrap();
//! let c = Mnemonic::from_str(c_str).unwrap(); //! let c = Mnemonic::from_str(c_str).unwrap();
@ -37,53 +38,6 @@ use std::{
use bip39::Mnemonic as Inner; use bip39::Mnemonic as Inner;
/// Maximal number of words in a mnemonic.
const MAX_MNEMONIC_LENGTH: usize = 24;
/// Errors same as [bip39::Error] but specifically for 24 word mnemonics.
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum SeedXorError {
/// Mnemonic has a word count that is not 24.
WordCountNot24,
/// Mnemonic contains an unknown word.
/// Error contains the index of the word.
UnknownWord(usize),
/// Entropy was not a 256 bits in length.
EntropyBitsNot256,
/// The mnemonic has an invalid checksum.
InvalidChecksum,
/// The mnemonic can be interpreted as multiple languages.
AmbiguousLanguages,
}
impl From<bip39::Error> for SeedXorError {
fn from(err: bip39::Error) -> Self {
match err {
bip39::Error::BadEntropyBitCount(_) => Self::EntropyBitsNot256,
bip39::Error::BadWordCount(_) => Self::WordCountNot24,
bip39::Error::UnknownWord(i) => Self::UnknownWord(i),
bip39::Error::InvalidChecksum => Self::InvalidChecksum,
bip39::Error::AmbiguousLanguages(_) => Self::AmbiguousLanguages,
}
}
}
impl fmt::Display for SeedXorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
SeedXorError::WordCountNot24 => {
write!(f, "Mnemonic has a word count that is not 24",)
}
SeedXorError::UnknownWord(i) => {
write!(f, "Mnemonic contains an unknown word (word {})", i,)
}
SeedXorError::EntropyBitsNot256 => write!(f, "Entropy was not between 256 bits",),
SeedXorError::InvalidChecksum => write!(f, "Mnemonic has an invalid checksum"),
SeedXorError::AmbiguousLanguages => write!(f, "Ambiguous language"),
}
}
}
/// Wrapper for a [bip39::Mnemonic] which is aliased as `Inner`. /// Wrapper for a [bip39::Mnemonic] which is aliased as `Inner`.
#[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] #[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct Mnemonic { pub struct Mnemonic {
@ -92,11 +46,9 @@ pub struct Mnemonic {
} }
impl Mnemonic { impl Mnemonic {
/// Private constructor which ensures that a new [Mnemonic] instance has 24 words. /// Private constructor.
fn new(inner: Inner) -> Result<Self, SeedXorError> { fn new(inner: Inner) -> Self {
ensure_24_words(&inner)?; Mnemonic { inner }
Ok(Mnemonic { inner })
} }
/// Access the private inner [bip39::Mnemonic] for more functionality. /// Access the private inner [bip39::Mnemonic] for more functionality.
@ -104,40 +56,44 @@ impl Mnemonic {
&self.inner &self.inner
} }
/// Wrapper for the same method as in [bip39::Mnemonic] /// Wrapper for the same method as in [bip39::Mnemonic].
/// but it returns an `Err` if the entropy does not result in a 24 word mnemonic. pub fn from_entropy(entropy: &[u8]) -> Result<Self, bip39::Error> {
pub fn from_entropy(entropy: &[u8]) -> Result<Self, SeedXorError> {
match Inner::from_entropy(entropy) { match Inner::from_entropy(entropy) {
Ok(inner) => Mnemonic::new(inner), Ok(inner) => Ok(Mnemonic::new(inner)),
Err(err) => Err(SeedXorError::from(err)), Err(err) => Err(err),
} }
} }
/// XOR two [Mnemonic]s without consuming them. /// XOR two [Mnemonic]s without consuming them.
/// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well. /// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well.
fn xor(&self, rhs: &Self) -> Self { fn xor(&self, rhs: &Self) -> Self {
// self and rhs have both 256bit entropy let mut entropy = self.inner.to_entropy();
let xor_result = self let xor_values = rhs.inner.to_entropy();
.inner
.to_entropy()
.iter()
.zip(rhs.inner.to_entropy().iter())
.map(|(a, b)| a ^ b)
.collect::<Vec<u8>>();
// We unwrap here because xor_result has as many bytes as self and rhs // XOR each Byte
// which in turn have a valid number of bytes. entropy
Mnemonic::from_entropy(&xor_result).unwrap() .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 { impl FromStr for Mnemonic {
type Err = SeedXorError; type Err = bip39::Error;
fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> { fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
match Inner::from_str(mnemonic) { match Inner::from_str(mnemonic) {
Ok(inner) => Mnemonic::new(inner), Ok(inner) => Ok(Mnemonic::new(inner)),
Err(err) => Err(SeedXorError::from(err)), Err(err) => Err(err),
} }
} }
} }
@ -168,15 +124,6 @@ impl BitXorAssign for Mnemonic {
} }
} }
/// Ensures that an [Inner] is a 24 word mnemonic for wrapping into a [Mnemonic].
fn ensure_24_words(inner: &Inner) -> Result<(), SeedXorError> {
if inner.word_count() != MAX_MNEMONIC_LENGTH {
return Err(SeedXorError::WordCountNot24);
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::Mnemonic; use crate::Mnemonic;
@ -196,9 +143,7 @@ mod tests {
let result = Mnemonic::from_str(result_str).unwrap(); let result = Mnemonic::from_str(result_str).unwrap();
assert_eq!(result, a.clone() ^ b.clone() ^ c.clone()); assert_eq!(result, a.clone() ^ b.clone() ^ c.clone());
assert_eq!(result, b ^ c ^ a); // Commutative
// Different order
assert_eq!(result, b ^ c ^ a);
} }
#[test] #[test]
@ -219,4 +164,22 @@ mod tests {
assert_eq!(result, assigned); 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
}
} }