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}