seed_xor/
lib.rs

1//! # seed-xor
2//!
3//! seed-xor builds on top of [rust-bip39](https://github.com/rust-bitcoin/rust-bip39/)
4//! and lets you XOR bip39 mnemonics as described in [Coldcards docs](https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md).
5//!
6//!
7//! It is also possible to XOR mnemonics with differing numbers of words.
8//! For this the xored value takes on the entropy surplus of the longer seed.
9//!
10//!
11//! ## Example
12//!
13//! ```rust
14//! use seed_xor::Mnemonic;
15//! use std::str::FromStr;
16//!
17//! // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
18//! 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";
19//! 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";
20//! 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";
21//! 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";
22//!
23//! // Mnemonic is a wrapper for bip39::Mnemonic which implements the XOR operation `^`.
24//! // Mnemonics can also be created from entropy.
25//! let a = Mnemonic::from_str(a_str).unwrap();
26//! let b = Mnemonic::from_str(b_str).unwrap();
27//! let c = Mnemonic::from_str(c_str).unwrap();
28//! let result = Mnemonic::from_str(result_str).unwrap();
29//!
30//! assert_eq!(result, a ^ b ^ c);
31//! ```
32//!
33use std::{
34    fmt,
35    ops::{BitXor, BitXorAssign},
36    str::FromStr,
37};
38
39/// Trait for a `XOR`.
40pub trait SeedXor {
41    /// XOR two values without consuming them.
42    fn xor(&self, rhs: &Self) -> Self;
43}
44
45impl SeedXor for bip39::Mnemonic {
46    /// XOR self with another [bip39::Mnemonic] without consuming it or itself.
47    fn xor(&self, rhs: &Self) -> Self {
48        let mut entropy = self.to_entropy();
49        let xor_values = rhs.to_entropy();
50
51        // XOR each Byte
52        entropy
53            .iter_mut()
54            .zip(xor_values.iter())
55            .for_each(|(a, b)| *a ^= b);
56
57        // Extend entropy with values of xor_values if it has a shorter entropy length.
58        if entropy.len() < xor_values.len() {
59            entropy.extend(xor_values.iter().skip(entropy.len()))
60        }
61
62        // We unwrap here because entropy has either as many Bytes
63        // as self or rhs and both are valid mnemonics.
64        bip39::Mnemonic::from_entropy(&entropy).unwrap()
65    }
66}
67
68/// Wrapper for a [bip39::Mnemonic] for the implementation of `^` and `^=` operators.
69#[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
70pub struct Mnemonic {
71    /// Actual [bip39::Mnemonic] which is wrapped to be able to implement the XOR operator.
72    pub inner: bip39::Mnemonic,
73}
74
75impl Mnemonic {
76    /// Private constructor.
77    fn new(inner: bip39::Mnemonic) -> Self {
78        Mnemonic { inner }
79    }
80
81    /// Wrapper for the same method as in [bip39::Mnemonic].
82    pub fn from_entropy(entropy: &[u8]) -> Result<Self, bip39::Error> {
83        match bip39::Mnemonic::from_entropy(entropy) {
84            Ok(inner) => Ok(Mnemonic::new(inner)),
85            Err(err) => Err(err),
86        }
87    }
88
89    /// XOR two [Mnemonic]s without consuming them.
90    /// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well.
91    fn xor(&self, rhs: &Self) -> Self {
92        Mnemonic::new(self.inner.xor(&rhs.inner))
93    }
94}
95
96impl FromStr for Mnemonic {
97    type Err = bip39::Error;
98
99    fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
100        match bip39::Mnemonic::from_str(mnemonic) {
101            Ok(inner) => Ok(Mnemonic::new(inner)),
102            Err(err) => Err(err),
103        }
104    }
105}
106
107impl fmt::Display for Mnemonic {
108    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
109        for (i, word) in self.inner.word_iter().enumerate() {
110            if i > 0 {
111                f.write_str(" ")?;
112            }
113            f.write_str(word)?;
114        }
115        Ok(())
116    }
117}
118
119impl BitXor for Mnemonic {
120    type Output = Self;
121
122    fn bitxor(self, rhs: Self) -> Self::Output {
123        self.xor(&rhs)
124    }
125}
126
127impl BitXorAssign for Mnemonic {
128    fn bitxor_assign(&mut self, rhs: Self) {
129        *self = self.xor(&rhs)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::Mnemonic;
136    use std::str::FromStr;
137
138    #[test]
139    fn seed_xor_works() {
140        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
141        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";
142        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";
143        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";
144        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";
145
146        let a = Mnemonic::from_str(a_str).unwrap();
147        let b = Mnemonic::from_str(b_str).unwrap();
148        let c = Mnemonic::from_str(c_str).unwrap();
149        let result = Mnemonic::from_str(result_str).unwrap();
150
151        assert_eq!(result, a.clone() ^ b.clone() ^ c.clone());
152        assert_eq!(result, b ^ c ^ a); // Commutative
153    }
154
155    #[test]
156    fn seed_xor_assignment_works() {
157        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
158        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";
159        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";
160        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";
161        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";
162
163        let a = Mnemonic::from_str(a_str).unwrap();
164        let b = Mnemonic::from_str(b_str).unwrap();
165        let c = Mnemonic::from_str(c_str).unwrap();
166        let result = Mnemonic::from_str(result_str).unwrap();
167
168        let mut assigned = a.xor(&b); // XOR without consuming
169        assigned ^= c;
170
171        assert_eq!(result, assigned);
172    }
173
174    #[test]
175    fn seed_xor_with_different_lengths_works() {
176        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
177        // but truncated mnemonics with correct last word.
178        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";
179        let str_16 = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series number";
180        let str_12 = "vault nominee cradle silk own frown throw leg cactus recall talent wisdom";
181        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";
182
183        let w_24 = Mnemonic::from_str(str_24).unwrap();
184        let w_16 = Mnemonic::from_str(str_16).unwrap();
185        let w_12 = Mnemonic::from_str(str_12).unwrap();
186        let result = Mnemonic::from_str(result_str).unwrap();
187
188        assert_eq!(result, w_24.clone() ^ w_16.clone() ^ w_12.clone());
189        assert_eq!(result, w_12 ^ w_24 ^ w_16); // Commutative
190    }
191}