Skip to main content

ms_codec/codex32/
mod.rs

1// Vendored from `codex32` v0.1.0 (crates.io checksum
2// d230935faa4d0521349d228f39aba4ff489cf2a8bcab4d84e31f4cbd6fe918e9), CC0-1.0,
3// by Andrew Poelstra. Inlined into ms-codec (Cycle-B, shape A) to own the
4// Zeroize/Drop/redacting-Debug secret-hygiene fixes (FOLLOWUP
5// codex32-upstream-dormant-vendor-vs-accept-decision).
6//
7// Copied from the upstream runtime modules (lib.rs / field.rs / checksum.rs).
8// The ONLY substantive edits are: (1) Zeroize/ZeroizeOnDrop + a redacting Debug
9// on `Codex32String` (Phase 2); (2) module-routing of `use` paths to fit the
10// inlined submodule (`crate::field` -> `super::field` in checksum.rs, since the
11// codex32 crate root is now the `codex32` submodule); (3) crate-local lint
12// `#![allow(..)]`s (the crate denies missing_docs / runs -D-warnings clippy; the
13// upstream copy predates both). rustfmt also normalized a few cosmetic spots
14// (import order, one array literal, one fn-signature wrap) — NONE touch encoding
15// logic. The ENCODING (from_seed/from_string/interpolate_at/checksum/field) is
16// behaviorally UNCHANGED; the wire-byte-identity invariant is proven by
17// tests/codex32_vendor_parity.rs. The upstream CC0 LICENSE is retained verbatim
18// alongside as src/codex32/LICENSE.
19//
20// Rust Codex32 Library and Reference Implementation
21// Written in 2023 by
22//   Andrew Poelstra <apoelstra@wpsoftware.net>
23//
24// To the extent possible under law, the author(s) have dedicated all
25// copyright and related and neighboring rights to this software to
26// the public domain worldwide. This software is distributed without
27// any warranty.
28//
29// You should have received a copy of the CC0 Public Domain Dedication
30// along with this software.
31// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
32//
33
34//! codex32 Reference Implementation
35//!
36//! This project is a reference implementation of BIP-XXX "codex32", a project
37//! by Leon Olson Curr and Pearlwort Snead to produce checksummed and secret-shared
38//! BIP32 master seeds.
39//!
40//! References:
41//!   * BIP-XXX <https://github.com/apoelstra/bips/blob/2023-02--volvelles/bip-0000.mediawiki>
42//!   * The codex32 website <https://www.secretcodex32.com>
43//!   * BIP-0173 "bech32" <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
44//!   * BIP-0032 "BIP 32" <https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki>
45//!
46
47// This is the shittiest lint ever and has literally never been correct when
48// it has fired, and somehow in rust-bitcoin managed NOT to fire in the one
49// case where it might've been useful.
50// https://github.com/rust-bitcoin/rust-bitcoin/pull/1701
51#![allow(clippy::suspicious_arithmetic_impl)]
52// Vendored-module lint allowances (see the header note, edit class 3): the
53// crate denies missing_docs (the upstream `Fe` constants + a few helpers are
54// undocumented) and runs clippy under -D warnings (upstream predates two style
55// lints firing on the byte-identical body — `precedence` in `from_seed`'s base32
56// packing, `needless_lifetimes` on `Parts`). Scope the allows to this module so
57// the runtime body stays verbatim and the crate-wide gates are unaffected.
58#![allow(missing_docs)]
59#![allow(clippy::precedence)]
60#![allow(clippy::needless_lifetimes)]
61
62mod checksum;
63mod field;
64
65pub use checksum::Engine as ChecksumEngine;
66pub use field::Fe;
67use std::{cmp, fmt};
68
69#[derive(Debug)]
70pub enum Error {
71    /// Error related to a single bech32 character
72    Field(field::Error),
73    /// Identifier had wrong length when creating a share
74    IdNotLength4(usize),
75    /// When translating from u5 to u8, there was an incomplete group of
76    /// size greater than 4 bits, meaning an entirely extraneous character.
77    IncompleteGroup(usize),
78    /// Tried a codex32 string of an illegal length
79    InvalidLength(usize),
80    /// Tried to decode a character which was not part of the bech32 alphabet,
81    /// or, if in the HRP, was not ASCII.
82    InvalidChar(char),
83    /// Tried to decode a character but its case did not match the expected case
84    InvalidCase(Case, char),
85    /// String had an invalid checksum
86    InvalidChecksum {
87        /// Checksum we used, "long" or "short"
88        checksum: &'static str,
89        /// The string with the bad checksum
90        string: String,
91    },
92    /// Threshold was not an allowed value (2 through 9, or 0)
93    InvalidThreshold(char),
94    /// Threshold was not an allowed value (2 through 9, or 0)
95    InvalidThresholdN(usize),
96    /// Share index was not an allowed value (only S if the threshold is 0,
97    /// otherwise anything goes)
98    InvalidShareIndex(Fe),
99    /// A set of shares to be interpolated did not all have the same length
100    MismatchedLength(usize, usize),
101    /// A set of shares to be interpolated did not all have the same HRP
102    MismatchedHrp(String, String),
103    /// A set of shares to be interpolated did not all have the same threshold
104    MismatchedThreshold(usize, usize),
105    /// A set of shares to be interpolated did not all have the same ID
106    MismatchedId(String, String),
107    /// A share index was repeated in the set of shares to interpolate.
108    RepeatedIndex(Fe),
109    /// A set of shares to be interpolated did not have enough shares
110    ThresholdNotPassed { threshold: usize, n_shares: usize },
111}
112
113impl From<field::Error> for Error {
114    fn from(e: field::Error) -> Error {
115        Error::Field(e)
116    }
117}
118
119/// Lowercase or uppercase (as applied to the bech32 alphabet)
120#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)]
121pub enum Case {
122    /// qpzr...
123    Lower,
124    /// QPZR...
125    Upper,
126}
127
128/// A codex32 string, containing a valid checksum
129///
130/// Cycle-B P2 (the ONLY behavioral change vs upstream): the inner secret
131/// `String` is scrubbed on drop via `zeroize::ZeroizeOnDrop`, and `Debug` is
132/// hand-rolled length-only (the upstream derived `Debug` echoed the full secret
133/// — the L22-class footgun). `Clone`/`PartialEq`/`Eq`/`Hash` are RETAINED
134/// (load-bearing: `interpolate_at`'s self-return clone, `combine_shares`'s
135/// `derived != parsed[j]` compare, source-compat). The encoding bodies are
136/// UNTOUCHED.
137#[derive(Clone, PartialEq, Eq, Hash, zeroize::ZeroizeOnDrop)]
138pub struct Codex32String(String);
139
140impl fmt::Display for Codex32String {
141    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
142        fmt::Display::fmt(&self.0, f)
143    }
144}
145
146impl fmt::Debug for Codex32String {
147    /// Redacting: NEVER echoes the secret string (the upstream derived `Debug`
148    /// leaked it). Length-only — enough to debug a length/shape bug, nothing of
149    /// the payload. The char-count is non-sensitive (ms1 lengths are a small
150    /// public set).
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(
153            f,
154            "Codex32String([REDACTED; {} chars])",
155            self.0.chars().count()
156        )
157    }
158}
159
160impl Codex32String {
161    fn sanity_check(&self) -> Result<(), Error> {
162        let parts = self.parts_inner()?;
163        let incomplete_group = (parts.payload.len() * 5) % 8;
164        if incomplete_group > 4 {
165            return Err(Error::IncompleteGroup(incomplete_group));
166        }
167        Ok(())
168    }
169
170    /// Construct a codex32 string from a not-yet-checksummed string
171    pub fn from_unchecksummed_string(mut s: String) -> Result<Self, Error> {
172        // Determine what checksum to use and extend the string
173        let (len, mut checksum) = if s.len() < 81 {
174            (13, checksum::Engine::new_codex32_short())
175        } else {
176            (15, checksum::Engine::new_codex32_long())
177        };
178        s.reserve_exact(len);
179
180        // Split out the HRP
181        let (hrp, real_string) = match s.rsplit_once('1') {
182            Some((s1, s2)) => (s1, s2),
183            None => ("", &s[..]),
184        };
185        // Compute the checksum
186        checksum.input_hrp(hrp)?;
187        checksum.input_data_str(real_string)?;
188        for ch in checksum.into_residue() {
189            s.push(ch.to_char());
190        }
191
192        let ret = Codex32String(s);
193        ret.sanity_check()?;
194        Ok(ret)
195    }
196
197    /// Construct a codex32 string from an already-checksummed string
198    pub fn from_string(s: String) -> Result<Self, Error> {
199        let (name, mut checksum) = if s.len() >= 48 && s.len() < 94 {
200            ("short", checksum::Engine::new_codex32_short())
201        } else if s.len() >= 125 && s.len() < 128 {
202            ("long", checksum::Engine::new_codex32_long())
203        } else {
204            return Err(Error::InvalidLength(s.len()));
205        };
206
207        // Split out the HRP
208        let (hrp, real_string) = match s.rsplit_once('1') {
209            Some((s1, s2)) => (s1, s2),
210            None => ("", &s[..]),
211        };
212        checksum.input_hrp(hrp)?;
213        checksum.input_data_str(real_string)?;
214        if !checksum.is_valid() {
215            return Err(Error::InvalidChecksum {
216                checksum: name,
217                string: s,
218            });
219        }
220        // Looks good, return
221        let ret = Codex32String(s);
222        ret.sanity_check()?;
223        Ok(ret)
224    }
225
226    /// Break the string up into its constituent parts
227    fn parts_inner(&self) -> Result<Parts, Error> {
228        let (hrp, s) = match self.0.rsplit_once('1') {
229            Some((s1, s2)) => (s1, s2),
230            None => ("", &self.0[..]),
231        };
232        let checksum_len = if self.0.len() > 93 { 15 } else { 13 };
233        let ret = Parts {
234            hrp,
235            threshold: match s.as_bytes()[0] {
236                b'0' => 0,
237                b'2' => 2,
238                b'3' => 3,
239                b'4' => 4,
240                b'5' => 5,
241                b'6' => 6,
242                b'7' => 7,
243                b'8' => 8,
244                b'9' => 9,
245                _ => return Err(Error::InvalidThreshold(s.as_bytes()[0].into())),
246            },
247            id: &s[1..5],
248            share_index: Fe::from_char(s.as_bytes()[5].into()).unwrap(),
249            payload: &s[6..s.len() - checksum_len],
250            checksum: &s[s.len() - checksum_len..],
251        };
252        if ret.threshold == 0 && ret.share_index != Fe::S {
253            return Err(Error::InvalidShareIndex(ret.share_index));
254        }
255        Ok(ret)
256    }
257
258    /// Break the string up into its constituent parts
259    pub fn parts(&self) -> Parts {
260        // unwrap OK since we validated the input on parse
261        self.parts_inner().unwrap()
262    }
263
264    /// Interpolate a set of shares to derive a share at a specific index.
265    ///
266    /// Using the index `Fe::S` will recover the master seed.
267    pub fn interpolate_at(shares: &[Codex32String], target: Fe) -> Result<Codex32String, Error> {
268        // Collect indices and sanity check
269        if shares.is_empty() {
270            return Err(Error::ThresholdNotPassed {
271                threshold: 1,
272                n_shares: 0,
273            });
274        }
275        let mut indices = Vec::with_capacity(shares.len());
276        let s0_parts = shares[0].parts();
277        if s0_parts.threshold > shares.len() {
278            return Err(Error::ThresholdNotPassed {
279                threshold: s0_parts.threshold,
280                n_shares: shares.len(),
281            });
282        }
283        for share in shares {
284            let parts = share.parts();
285            if shares[0].0.len() != share.0.len() {
286                return Err(Error::MismatchedLength(shares[0].0.len(), share.0.len()));
287            }
288            if s0_parts.hrp != parts.hrp {
289                return Err(Error::MismatchedHrp(s0_parts.hrp.into(), parts.hrp.into()));
290            }
291            if s0_parts.threshold != parts.threshold {
292                return Err(Error::MismatchedThreshold(
293                    s0_parts.threshold,
294                    parts.threshold,
295                ));
296            }
297            if s0_parts.id != parts.id {
298                return Err(Error::MismatchedId(s0_parts.id.into(), parts.id.into()));
299            }
300            indices.push(parts.share_index);
301        }
302
303        // Do lagrange interpolation
304        let mut mult = Fe::P;
305        for i in 0..shares.len() {
306            if indices[i] == target {
307                // If we're trying to output an input share, just output it directly.
308                // Naive Lagrange multiplication would otherwise multiply by 0.
309                return Ok(shares[i].clone());
310            }
311
312            mult *= indices[i] + target;
313        }
314
315        let payload_len = 6 + s0_parts.payload.len() + s0_parts.checksum.len();
316        let hrp_len = shares[0].0.len() - payload_len;
317        let mut result = vec![Fe::Q; payload_len];
318
319        for i in 0..shares.len() {
320            let mut inv = Fe::P;
321            for j in 0..shares.len() {
322                inv *= indices[j]
323                    + if i == j {
324                        target
325                    } else {
326                        // If there is a repeated index, just call this an error. Technically
327                        // speaking, we could reject the other one and re-do the threshold
328                        // check in case we had enough unique ones .. but easier to just make
329                        // it the user's responsibility to provide unique indices to begin with.
330                        if indices[i] == indices[j] {
331                            return Err(Error::RepeatedIndex(indices[i]));
332                        }
333                        indices[i]
334                    }
335            }
336
337            for (j, res_j) in result.iter_mut().enumerate() {
338                let ch_at_i = char::from(shares[i].0.as_bytes()[hrp_len + j]);
339                *res_j += mult / inv * Fe::from_char(ch_at_i).unwrap();
340            }
341        }
342
343        let mut s = s0_parts.hrp.to_owned();
344        s.push('1');
345        if s0_parts.hrp.chars().all(char::is_uppercase) {
346            s.extend(
347                result
348                    .into_iter()
349                    .map(Fe::to_char)
350                    .map(|c| c.to_ascii_uppercase()),
351            );
352        } else {
353            s.extend(result.into_iter().map(Fe::to_char));
354        }
355        Ok(Codex32String(s))
356    }
357
358    /// Creates a S share from bare seed data
359    pub fn from_seed(
360        hrp: &str,
361        threshold: usize,
362        id: &str,
363        share_idx: Fe,
364        data: &[u8],
365    ) -> Result<Codex32String, Error> {
366        if id.len() != 4 {
367            return Err(Error::IdNotLength4(id.len()));
368        }
369
370        let mut ret = String::with_capacity(hrp.len() + 6 + (data.len() * 8 + 4) / 5);
371        ret.push_str(hrp);
372        ret.push('1');
373        let k = match threshold {
374            0 => Fe::_0,
375            2 => Fe::_2,
376            3 => Fe::_3,
377            4 => Fe::_4,
378            5 => Fe::_5,
379            6 => Fe::_6,
380            7 => Fe::_7,
381            8 => Fe::_8,
382            9 => Fe::_9,
383            x => return Err(Error::InvalidThresholdN(x)),
384        };
385        // FIXME correct case to match HRP
386        ret.push(k.to_char());
387        ret.push_str(id);
388        ret.push(share_idx.to_char());
389
390        // Convert byte data to base 32
391        let mut next_u5 = 0;
392        let mut rem = 0;
393        for byte in data {
394            // Each byte provides at least one u5. Push that.
395            let u5 = (next_u5 << (5 - rem)) | byte >> (3 + rem);
396            ret.push(Fe::from_u8(u5).unwrap().to_char());
397            next_u5 = byte & ((1 << (3 + rem)) - 1);
398            // If there were 2 or more bits from the last iteration, then
399            // this iteration will push *two* u5s.
400            if rem >= 2 {
401                ret.push(Fe::from_u8(next_u5 >> (rem - 2)).unwrap().to_char());
402                next_u5 &= (1 << (rem - 2)) - 1;
403            }
404            rem = (rem + 8) % 5;
405        }
406        if rem > 0 {
407            ret.push(Fe::from_u8(next_u5 << (5 - rem)).unwrap().to_char());
408        }
409
410        // Initialize checksum engine with HRP and header
411        let mut checksum = if data.len() < 51 {
412            checksum::Engine::new_codex32_short()
413        } else {
414            checksum::Engine::new_codex32_long()
415        };
416        checksum.input_hrp(hrp)?;
417        checksum.input_data_str(&ret[hrp.len() + 1..])?;
418        // Now, to compute the checksum, we stick the target residue onto the end
419        // of the input string, the take the resulting residue as the checksum
420        checksum.input_own_target();
421        ret.extend(checksum.into_residue().into_iter().map(Fe::to_char));
422
423        let mut checksum = checksum::Engine::new_codex32_short();
424        checksum.input_hrp(hrp)?;
425        checksum.input_data_str(&ret[hrp.len() + 1..])?;
426        Ok(Codex32String(ret))
427    }
428}
429
430/// A codex32 string, split into its constituent partrs
431#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
432pub struct Parts<'s> {
433    hrp: &'s str,
434    threshold: usize,
435    id: &'s str,
436    share_index: Fe,
437    payload: &'s str,
438    checksum: &'s str,
439}
440
441impl<'s> Parts<'s> {
442    /// Extract the binary data from a checksummed string
443    ///
444    /// If the string does not have a multiple-of-8 number of bits, right-pad the
445    /// final byte with 0s.
446    pub fn data(&self) -> Vec<u8> {
447        let mut ret = Vec::with_capacity((self.payload.len() * 5 + 7) / 8);
448
449        let mut next_byte = 0;
450        let mut rem = 0;
451        for ch in self.payload.chars() {
452            let fe = Fe::from_char(ch).unwrap(); // unwrap ok since string is valid bech32
453            match rem.cmp(&3) {
454                cmp::Ordering::Less => {
455                    // If we are within 3 bits of the start we can fit the whole next char in
456                    next_byte |= fe.to_u8() << (3 - rem);
457                }
458                cmp::Ordering::Equal => {
459                    // If we are exactly 3 bits from the start then this char fills in the byte
460                    ret.push(next_byte | fe.to_u8());
461                    next_byte = 0;
462                }
463                cmp::Ordering::Greater => {
464                    // Otherwise we have to break it in two
465                    let overshoot = rem - 3;
466                    assert!(overshoot > 0);
467                    ret.push(next_byte | (fe.to_u8() >> overshoot));
468                    next_byte = fe.to_u8() << (8 - overshoot);
469                }
470            }
471            rem = (rem + 5) % 8;
472        }
473        debug_assert!(rem <= 4); // checked when parsing the string
474        ret
475    }
476}