seedxor/
lib.rs

1//! # seedxor
2//!
3//! 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)
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//! It also lets you split existing mnemonics into as many seeds as you wish
7//!
8//! It is also possible to XOR mnemonics with differing numbers of words.
9//! For this the xored value takes on the entropy surplus of the longer seed.
10//!
11//! ## Example
12//!
13//! ```rust
14//! use seedxor::{Mnemonic, SeedXor};
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//! // split a into 3 mnemonics
33//! let a = Mnemonic::from_str(a_str).unwrap();
34//! let split = a.splitn(3).unwrap();
35//! let recombined_a = Mnemonic::xor_all(&split).unwrap();
36//! assert_eq!(a_str, recombined_a.to_string());
37//! ```
38//!
39pub use bip39::{Error, Language};
40use std::{
41    fmt,
42    fmt::Display,
43    ops::{BitXor, BitXorAssign, Deref, DerefMut},
44    str::FromStr,
45};
46
47/// Trait for a `XOR`.
48pub trait SeedXor {
49    /// XOR two values without consuming them.
50    fn xor(&self, rhs: &Self) -> Self;
51
52    fn xor_all(slice: &[Self]) -> Option<Self>
53    where
54        Self: Sized + Clone,
55    {
56        let first = slice.get(0)?;
57        // expensive clone :)
58        //let first = first.xor(first).xor(first);
59        let first = first.clone();
60        Some(slice.iter().skip(1).fold(first, |x, y| x.xor(y)))
61    }
62}
63
64impl SeedXor for bip39::Mnemonic {
65    /// XOR self with another [bip39::Mnemonic] without consuming it or itself.
66    fn xor(&self, rhs: &Self) -> Self {
67        let (mut entropy, entropy_len) = self.to_entropy_array();
68        let (xor_values, xor_values_len) = rhs.to_entropy_array();
69        let entropy = &mut entropy[0..entropy_len];
70        let xor_values = &xor_values[0..xor_values_len];
71
72        // XOR each Byte
73        entropy
74            .iter_mut()
75            .zip(xor_values.iter())
76            .for_each(|(a, b)| *a ^= b);
77
78        // Extend entropy with values of xor_values if it has a shorter entropy length.
79        if entropy.len() < xor_values.len() {
80            let mut entropy = entropy.to_vec();
81            entropy.extend(xor_values.iter().skip(entropy.len()));
82
83            bip39::Mnemonic::from_entropy(&entropy).unwrap()
84        } else {
85            // We unwrap here because entropy has either as many Bytes
86            // as self or rhs and both are valid mnemonics.
87            bip39::Mnemonic::from_entropy(&entropy).unwrap()
88        }
89    }
90}
91
92/// Wrapper for a [bip39::Mnemonic] for the implementation of `^` and `^=` operators.
93#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
94pub struct Mnemonic {
95    /// Actual [bip39::Mnemonic] which is wrapped to be able to implement the XOR operator.
96    pub inner: bip39::Mnemonic,
97}
98
99impl Mnemonic {
100    pub fn split(&self) -> Result<[Self; 2], Error> {
101        let random = Self::generate_in(self.language(), self.word_count())?;
102        let calc = self.xor(&random);
103        Ok([calc, random])
104    }
105
106    pub fn splitn(self, n: usize) -> Result<Vec<Self>, Error> {
107        let mut ret: Vec<Self> = Vec::with_capacity(n);
108        if n == 1 {
109            ret.push(self);
110        } else {
111            ret.extend_from_slice(&self.split()?);
112            for _ in 0..n - 2 {
113                let split = ret.pop().expect("cannot be empty").split()?;
114                ret.extend_from_slice(&split);
115            }
116        }
117        Ok(ret)
118    }
119
120    pub fn generate_in(language: Language, word_count: usize) -> Result<Self, Error> {
121        //let inner = bip39::Mnemonic::generate_in(language, word_count)?;
122        let mut inner = vec![0u8; (word_count / 3) * 4];
123        getrandom::getrandom(&mut inner)
124            .map_err(|e| Error::BadEntropyBitCount(e.code().get() as usize))?;
125        bip39::Mnemonic::from_entropy_in(language, &inner).map(|m| m.into())
126    }
127
128    /// Wrapper for the same method as in [bip39::Mnemonic].
129    pub fn from_entropy(entropy: &[u8]) -> Result<Self, Error> {
130        bip39::Mnemonic::from_entropy(entropy).map(|m| m.into())
131    }
132
133    pub fn parse_normalized_without_checksum_check(s: &str) -> Result<Mnemonic, Error> {
134        let lang = bip39::Mnemonic::language_of(s).unwrap_or(Language::English);
135        Self::parse_in_normalized_without_checksum_check(lang, s)
136    }
137
138    pub fn parse_in_normalized_without_checksum_check(
139        language: Language,
140        s: &str,
141    ) -> Result<Mnemonic, Error> {
142        bip39::Mnemonic::parse_in_normalized_without_checksum_check(
143            language,
144            &expand_words_in(language, s)?,
145        )
146        .map(|m| m.into())
147    }
148
149    pub fn to_short_string(&self) -> String {
150        let mut ret = self.word_iter().fold(String::new(), |mut s, w| {
151            if w.len() == 3 {
152                s.push_str(w);
153                s.push_str("  ");
154            } else {
155                w.chars().take(4).for_each(|c| s.push(c));
156                s.push(' ');
157            }
158            s
159        });
160        while ret.chars().last() == Some(' ') {
161            ret.pop();
162        }
163        ret
164    }
165
166    pub fn to_display_string(&self, short: bool) -> String {
167        if short {
168            self.to_short_string()
169        } else {
170            self.to_string()
171        }
172    }
173}
174
175pub fn expand_words(seed: &str) -> Result<String, Error> {
176    let lang = bip39::Mnemonic::language_of(seed).unwrap_or(Language::English);
177    expand_words_in(lang, seed)
178}
179
180pub fn expand_words_in(language: Language, seed: &str) -> Result<String, Error> {
181    let mut ret = String::new();
182    for (i, prefix) in seed.to_lowercase().split_whitespace().enumerate() {
183        let words = language.words_by_prefix(prefix);
184        let word = if words.len() == 1 {
185            words[0]
186        } else if words.contains(&prefix) {
187            prefix
188        } else {
189            // println!("prefix: '{prefix}', words: {words:?}");
190            // not unique or correct prefix
191            return Err(Error::UnknownWord(i));
192        };
193        ret.push_str(word);
194        ret.push(' ');
195    }
196    ret.pop();
197    Ok(ret)
198}
199
200impl SeedXor for Mnemonic {
201    /// XOR two [Mnemonic]s without consuming them.
202    /// If consumption is not of relevance the XOR operator `^` and XOR assigner `^=` can be used as well.
203    fn xor(&self, rhs: &Self) -> Self {
204        self.inner.xor(&rhs.inner).into()
205    }
206}
207
208impl Deref for Mnemonic {
209    type Target = bip39::Mnemonic;
210
211    fn deref(&self) -> &Self::Target {
212        &self.inner
213    }
214}
215
216impl DerefMut for Mnemonic {
217    fn deref_mut(&mut self) -> &mut Self::Target {
218        &mut self.inner
219    }
220}
221
222impl From<bip39::Mnemonic> for Mnemonic {
223    fn from(inner: bip39::Mnemonic) -> Self {
224        Self { inner }
225    }
226}
227
228impl FromStr for Mnemonic {
229    type Err = bip39::Error;
230
231    fn from_str(mnemonic: &str) -> Result<Self, <Self as FromStr>::Err> {
232        bip39::Mnemonic::from_str(&expand_words(mnemonic)?).map(|m| m.into())
233    }
234}
235
236impl fmt::Display for Mnemonic {
237    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
238        for (i, word) in self.inner.word_iter().enumerate() {
239            if i > 0 {
240                f.write_str(" ")?;
241            }
242            f.write_str(word)?;
243        }
244        Ok(())
245    }
246}
247
248impl fmt::Debug for Mnemonic {
249    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
250        <Mnemonic as Display>::fmt(self, f)
251    }
252}
253
254impl BitXor for Mnemonic {
255    type Output = Self;
256
257    fn bitxor(self, rhs: Self) -> Self::Output {
258        self.xor(&rhs)
259    }
260}
261
262impl BitXorAssign for Mnemonic {
263    fn bitxor_assign(&mut self, rhs: Self) {
264        *self = self.xor(&rhs)
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use crate::*;
271    use std::str::FromStr;
272
273    #[test]
274    fn seed_xor_works() {
275        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
276        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";
277        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";
278        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";
279        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";
280
281        let a = Mnemonic::from_str(a_str).unwrap();
282        let b = Mnemonic::from_str(b_str).unwrap();
283        let c = Mnemonic::from_str(c_str).unwrap();
284        let result = Mnemonic::from_str(result_str).unwrap();
285
286        assert_eq!(result, a.clone() ^ b.clone() ^ c.clone());
287        assert_eq!(result, b ^ c ^ a); // Commutative
288    }
289
290    #[test]
291    fn seed_xor_assignment_works() {
292        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
293        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";
294        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";
295        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";
296        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";
297
298        let a = Mnemonic::from_str(a_str).unwrap();
299        let b = Mnemonic::from_str(b_str).unwrap();
300        let c = Mnemonic::from_str(c_str).unwrap();
301        let result = Mnemonic::from_str(result_str).unwrap();
302
303        let mut assigned = a.xor(&b); // XOR without consuming
304        assigned ^= c;
305
306        assert_eq!(result, assigned);
307    }
308
309    #[test]
310    fn seed_xor_with_different_lengths_works() {
311        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
312        // but truncated mnemonics with correct last word.
313        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";
314        let str_16 = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series number";
315        let str_12 = "vault nominee cradle silk own frown throw leg cactus recall talent wisdom";
316        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";
317
318        let w_24 = Mnemonic::from_str(str_24).unwrap();
319        let w_16 = Mnemonic::from_str(str_16).unwrap();
320        let w_12 = Mnemonic::from_str(str_12).unwrap();
321        let result = Mnemonic::from_str(result_str).unwrap();
322
323        assert_eq!(result, w_24.clone() ^ w_16.clone() ^ w_12.clone());
324        assert_eq!(result, w_12 ^ w_24 ^ w_16); // Commutative
325    }
326
327    #[test]
328    fn seed_xor_works_12() {
329        // Coldcard example: https://github.com/Coldcard/firmware/blob/master/docs/seed-xor.md
330        let a_str = "romance wink lottery autumn shop bring dawn tongue range crater truth ability";
331        let b_str = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unhappy";
332        let c_str = "vault nominee cradle silk own frown throw leg cactus recall talent wait";
333        let result_str = "silent toe meat possible chair blossom wait occur this worth option boy";
334
335        let a = Mnemonic::from_str(a_str).unwrap();
336        let b = Mnemonic::from_str(b_str).unwrap();
337        let c = Mnemonic::from_str(c_str).unwrap();
338        let result = Mnemonic::from_str(result_str).unwrap();
339
340        assert_eq!(result, a.clone() ^ b.clone() ^ c.clone());
341        assert_eq!(result, b ^ c ^ a); // Commutative
342    }
343
344    #[test]
345    fn test_electrum_seed() {
346        let electrum_seed =
347            "ramp exotic resource icon sun addict equip sand leisure spare swing tobacco";
348        // ends up with the incorrect checksum
349        let expected = "ramp exotic resource icon sun addict equip sand leisure spare swing toast";
350
351        //let electrum_seed = Mnemonic::from_str(electrum_seed).unwrap();
352        let seed = Mnemonic::from(
353            bip39::Mnemonic::parse_in_normalized_without_checksum_check(
354                Language::English,
355                electrum_seed,
356            )
357            .unwrap(),
358        );
359
360        assert_eq!(electrum_seed, seed.to_string());
361
362        let expected = Mnemonic::from_str(expected).unwrap();
363
364        let split = seed.clone().split().unwrap();
365        println!("1split: '{split:?}'");
366        let result = Mnemonic::xor_all(&split).unwrap();
367        println!("result: '{}'", result);
368        if seed != result {
369            assert_eq!(expected, result);
370        }
371
372        for x in 1..=5 {
373            let split = seed.clone().splitn(x).unwrap();
374            assert_eq!(x, split.len());
375            println!("split: '{split:?}'");
376            let result = Mnemonic::xor_all(&split).unwrap();
377            println!("result: '{}'", result);
378            if seed != result {
379                assert_eq!(expected, result);
380            }
381        }
382    }
383
384    #[test]
385    fn derive_from_seed() {
386        // tl;dr for any seed you can generate a random seed and xor it to "split" it into 2 seeds
387        // you can then do that any number of times for the sub-seeds
388        let seed = "silent toe meat possible chair blossom wait occur this worth option boy";
389        let seed = Mnemonic::from_str(seed).unwrap();
390
391        let split = seed.clone().split().unwrap();
392        println!("split: '{split:?}'");
393        assert_eq!(seed.clone(), Mnemonic::xor_all(&split).unwrap());
394
395        for x in 1..=5 {
396            let split = seed.clone().splitn(x).unwrap();
397            assert_eq!(x, split.len());
398            println!("split: '{split:?}'");
399            assert_eq!(seed.clone(), Mnemonic::xor_all(&split).unwrap());
400        }
401    }
402
403    #[test]
404    fn expand_seed() {
405        let orig_seed = "silent toe meat possible chair blossom wait occur this worth option boy";
406        let seed = Mnemonic::from_str(orig_seed).unwrap();
407
408        let short_string = seed.to_short_string();
409        assert_eq!(
410            "sile toe  meat poss chai blos wait occu this wort opti boy",
411            short_string
412        );
413        //assert_eq!(Language::English, bip39::Mnemonic::language_of(&short_string).unwrap());
414
415        assert_eq!(orig_seed, expand_words(&short_string).unwrap());
416
417        // add and addict (addi) are both bip39 words, make sure those work
418        let orig_seed = "song vanish mistake night drink add modify lens average cool evil chest";
419        let seed = Mnemonic::from_str(orig_seed).unwrap();
420
421        let short_string = seed.to_short_string();
422        assert_eq!(
423            "song vani mist nigh drin add  modi lens aver cool evil ches",
424            short_string
425        );
426        assert_eq!(
427            Language::English,
428            bip39::Mnemonic::language_of(&short_string).unwrap()
429        );
430
431        assert_eq!(orig_seed, expand_words(&short_string).unwrap());
432
433        let orig_seed = "ramp exotic resource icon sun addict equip sand leisure spare swing toast";
434        let seed = Mnemonic::from_str(orig_seed).unwrap();
435
436        let short_string = seed.to_short_string();
437        assert_eq!(
438            "ramp exot reso icon sun  addi equi sand leis spar swin toas",
439            short_string
440        );
441        assert_eq!(
442            Language::English,
443            bip39::Mnemonic::language_of(&short_string).unwrap()
444        );
445
446        assert_eq!(orig_seed, expand_words(&short_string).unwrap());
447    }
448}