1use std::fmt;
15use std::str::FromStr;
16
17use serde::{Deserialize, Serialize};
18use zeroize::Zeroizing;
19
20use crate::types::Vocabulary;
21use crate::{bip39, paper32};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
25#[serde(rename_all = "snake_case")]
26#[schemars(description = "The encoding a value is written on paper in.")]
27#[non_exhaustive]
28pub enum RevealFormat {
29 #[schemars(
34 description = "Rows of 32 base-32 characters, 28 of data and 4 of parity; a wrong \
35 character in a row is corrected."
36 )]
37 Paper32,
38 #[schemars(description = "Two hexadecimal digits per byte, in rows of 32.")]
40 Hex,
41 #[schemars(
45 description = "Words from the BIP-39 English list, one per row, for a wallet's \
46 recovery phrase."
47 )]
48 Bip39,
49}
50
51crate::types::vocabulary!(RevealFormat {
52 Paper32 => "paper32",
53 Hex => "hex",
54 Bip39 => "bip39",
55});
56
57impl RevealFormat {
58 pub fn is_words(self) -> bool {
60 match self {
61 RevealFormat::Paper32 | RevealFormat::Hex => false,
62 RevealFormat::Bip39 => true,
63 }
64 }
65
66 pub fn unit(self) -> &'static str {
68 match self {
69 RevealFormat::Paper32 | RevealFormat::Hex => "bytes",
70 RevealFormat::Bip39 => "words",
71 }
72 }
73
74 pub fn length_of(self, bytes: usize) -> usize {
77 match self {
78 RevealFormat::Paper32 | RevealFormat::Hex => bytes,
79 RevealFormat::Bip39 => bip39::words_for(bytes),
80 }
81 }
82
83 pub fn bytes_for(self, length: usize) -> usize {
85 match self {
86 RevealFormat::Paper32 | RevealFormat::Hex => length,
87 RevealFormat::Bip39 => bip39::bytes_for(length),
88 }
89 }
90
91 pub fn layout(self) -> Layout {
93 match self {
94 RevealFormat::Paper32 => Layout {
95 format: self,
96 group: 4,
97 row: paper32::ROW_DATA,
98 parity: paper32::ROW_PARITY,
99 alphabet: "Digits and letters, never I, L, O or U; upper or lower case".to_string(),
100 algorithm: "Paper32: rows of 28 characters and 4 of parity, Reed-Solomon over \
101 GF(32)"
102 .to_string(),
103 },
104 RevealFormat::Hex => Layout {
105 format: self,
106 group: 4,
107 row: 32,
108 parity: 0,
109 alphabet: "Digits 0 to 9 and letters A to F".to_string(),
110 algorithm: "Hexadecimal, two characters per byte".to_string(),
111 },
112 RevealFormat::Bip39 => Layout {
113 format: self,
114 group: bip39::WORD_MAX,
115 row: bip39::WORD_MAX,
116 parity: 0,
117 alphabet: "Words from the BIP-39 English list, one per row; the first four \
118 letters are enough"
119 .to_string(),
120 algorithm: "BIP-39, English: 11 bits per word, the last word carrying a \
121 SHA-256 checksum"
122 .to_string(),
123 },
124 }
125 }
126
127 pub fn layout_for(format: Option<Self>) -> Layout {
130 format.unwrap_or(RevealFormat::Paper32).layout()
131 }
132
133 pub fn rows(self, bytes: usize) -> usize {
135 let layout = self.layout();
136 self.characters(bytes)
137 .div_ceil(layout.row.saturating_add(layout.parity).max(1))
138 }
139
140 pub fn read_row(self, text: &str, row: usize, more: bool) -> Result<TypedRow, String> {
162 let read = self.read_one_row(text, row)?;
163 if self.is_words() {
165 return Ok(read);
166 }
167 let full = self.layout().row.saturating_add(self.layout().parity);
168 let characters = read.text.chars().count();
169 if more && characters < full {
170 return Err(format!(
171 "{characters} characters; every row but the last has {full}"
172 ));
173 }
174 Ok(TypedRow {
175 full: characters == full,
176 ..read
177 })
178 }
179
180 fn read_one_row(self, text: &str, row: usize) -> Result<TypedRow, String> {
181 match self {
182 RevealFormat::Paper32 => paper32::read_row(text, row)
183 .map(|read| TypedRow {
184 text: read.text,
185 repair: read.repair.map(|r| r.to_string()),
186 full: false,
187 })
188 .map_err(|e| e.to_string()),
189 RevealFormat::Hex => {
190 let digits = hex_digits(text)?;
191 let row_len = self.layout().row;
192 if digits.is_empty() {
193 return Err("nothing to read".to_string());
194 }
195 if digits.len() > row_len {
196 return Err(format!(
197 "{} characters; a row has at most {row_len}",
198 digits.len()
199 ));
200 }
201 Ok(TypedRow {
202 text: digits,
203 repair: None,
204 full: false,
205 })
206 }
207 RevealFormat::Bip39 => bip39::read_word(text, row)
208 .map(|text| TypedRow {
209 text,
210 repair: None,
211 full: true,
212 })
213 .map_err(|e| e.to_string()),
214 }
215 }
216
217 pub fn decode(self, text: &str) -> Result<TypedValue, String> {
224 match self {
225 RevealFormat::Paper32 => paper32::decode(text)
226 .map(|decoded| TypedValue {
227 bytes: decoded.bytes,
228 repaired: decoded.repaired.iter().map(ToString::to_string).collect(),
229 })
230 .map_err(|e| e.to_string()),
231 RevealFormat::Hex => {
232 let digits = hex_digits(text)?;
233 if digits.len() % 2 != 0 {
234 return Err(format!(
235 "{} hexadecimal digits; a byte is two",
236 digits.len()
237 ));
238 }
239 let bytes = digits
240 .as_bytes()
241 .chunks(2)
242 .map(|pair| {
243 std::str::from_utf8(pair)
244 .ok()
245 .and_then(|p| u8::from_str_radix(p, 16).ok())
246 .unwrap_or(0)
247 })
248 .collect();
249 Ok(TypedValue {
250 bytes: Zeroizing::new(bytes),
251 repaired: Vec::new(),
252 })
253 }
254 RevealFormat::Bip39 => bip39::decode(text)
255 .map(|decoded| TypedValue {
256 bytes: decoded.bytes,
257 repaired: Vec::new(),
258 })
259 .map_err(|e| e.to_string()),
260 }
261 }
262
263 pub fn characters(self, bytes: usize) -> usize {
265 match self {
266 RevealFormat::Paper32 => {
267 let symbols = bytes.saturating_mul(8).div_ceil(5);
268 let rows = symbols.div_ceil(paper32::ROW_DATA);
269 symbols.saturating_add(rows.saturating_mul(paper32::ROW_PARITY))
270 }
271 RevealFormat::Hex => bytes.saturating_mul(2),
272 RevealFormat::Bip39 => bip39::words_for(bytes).saturating_mul(bip39::WORD_MAX),
274 }
275 }
276}
277
278impl fmt::Display for RevealFormat {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 f.write_str(self.as_str())
281 }
282}
283
284#[derive(Debug)]
286pub struct TypedRow {
287 pub text: Zeroizing<String>,
289 pub repair: Option<String>,
291 pub full: bool,
294}
295
296#[derive(Debug)]
298pub struct TypedValue {
299 pub bytes: Zeroizing<Vec<u8>>,
301 pub repaired: Vec<String>,
303}
304
305fn hex_digits(text: &str) -> Result<Zeroizing<String>, String> {
307 let mut digits = Zeroizing::new(String::with_capacity(text.len()));
308 for (position, c) in text.chars().enumerate() {
309 match c {
310 ' ' | '-' | '|' | '\t' | '\n' | '\r' | '.' => {}
311 c if c.is_ascii_hexdigit() => digits.push(c.to_ascii_uppercase()),
312 _ => {
313 return Err(format!(
314 "character {} is not a hexadecimal digit (0 to 9, A to F)",
315 position.saturating_add(1)
316 ));
317 }
318 }
319 }
320 Ok(digits)
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct UnknownRevealFormat(pub String);
326
327impl fmt::Display for UnknownRevealFormat {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 write!(
330 f,
331 "'{}' is not a format; use {}",
332 self.0,
333 crate::types::either(RevealFormat::ALL.iter().map(|format| format.as_str()))
334 )
335 }
336}
337
338impl std::error::Error for UnknownRevealFormat {}
339
340impl FromStr for RevealFormat {
341 type Err = UnknownRevealFormat;
342
343 fn from_str(s: &str) -> Result<Self, Self::Err> {
344 Self::from_name(s).ok_or_else(|| UnknownRevealFormat(s.to_string()))
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
351pub struct Layout {
352 pub format: RevealFormat,
354 pub group: usize,
356 pub row: usize,
358 pub parity: usize,
360 pub alphabet: String,
362 pub algorithm: String,
364}
365
366impl Default for Layout {
367 fn default() -> Self {
368 RevealFormat::Paper32.layout()
369 }
370}
371
372impl Layout {
373 pub fn rows<'a>(&self, text: &'a str) -> Vec<Row<'a>> {
376 if self.format.is_words() {
377 return text
378 .split_whitespace()
379 .map(|word| Row {
380 groups: vec![word],
381 parity: "",
382 })
383 .collect();
384 }
385 let row_len = self.row.saturating_add(self.parity).max(1);
386 text.as_bytes()
387 .chunks(row_len)
388 .map(|chunk| {
389 let line = std::str::from_utf8(chunk).unwrap_or_default();
390 let data_len = line.len().saturating_sub(self.parity);
391 let (data, parity) = line.split_at(data_len.min(line.len()));
392 let groups = data
393 .as_bytes()
394 .chunks(self.group.max(1))
395 .map(|g| std::str::from_utf8(g).unwrap_or_default())
396 .collect();
397 Row { groups, parity }
398 })
399 .collect()
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct Row<'a> {
406 pub groups: Vec<&'a str>,
408 pub parity: &'a str,
410}
411
412pub fn hex_upper(bytes: &[u8]) -> String {
414 use std::fmt::Write as _;
415 bytes.iter().fold(
416 String::with_capacity(bytes.len().saturating_mul(2)),
417 |mut out, b| {
418 let _ = write!(out, "{b:02X}");
419 out
420 },
421 )
422}
423
424#[cfg(test)]
425#[allow(clippy::indexing_slicing)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn a_row_is_checked_on_its_own_and_a_value_read_whole() {
431 let bytes = [0x5A; 30];
433 let text = paper32::encode(&bytes);
434 let (first, second) = text.split_at(32);
435 let row = RevealFormat::Paper32.read_row(first, 1, true).unwrap();
436 assert_eq!(row.text.as_str(), first);
437 assert!(row.repair.is_none());
438 assert!(row.full);
439
440 let mut slipped: Vec<char> = second.to_ascii_lowercase().chars().collect();
441 slipped[3] = if slipped[3] == 'a' { 'b' } else { 'a' };
442 let slipped: String = slipped.iter().collect();
443 let row = RevealFormat::Paper32.read_row(&slipped, 2, false).unwrap();
444 assert_eq!(row.text.as_str(), second);
445 assert!(!row.full);
446 assert!(row.repair.unwrap().starts_with("row 2: character 4"));
447 assert!(RevealFormat::Paper32.read_row("ABCD", 1, false).is_err());
448 let refused = RevealFormat::Paper32.read_row(second, 1, true).unwrap_err();
450 assert!(
451 refused.contains("every row but the last has 32"),
452 "{refused}"
453 );
454
455 let value = RevealFormat::Paper32
456 .decode(&format!("{first}\n{slipped}"))
457 .unwrap();
458 assert_eq!(*value.bytes, bytes);
459 assert_eq!(value.repaired.len(), 1);
460
461 let hex = RevealFormat::Hex.read_row("de ad-be ef", 1, false).unwrap();
462 assert_eq!(hex.text.as_str(), "DEADBEEF");
463 assert!(!hex.full);
464 assert!(RevealFormat::Hex.read_row("DEAG", 1, false).is_err());
465 assert!(RevealFormat::Hex.read_row("DEADBEEF", 1, true).is_err());
466 assert_eq!(
467 *RevealFormat::Hex.decode("DEAD\nbeef").unwrap().bytes,
468 [0xDE, 0xAD, 0xBE, 0xEF]
469 );
470 assert!(RevealFormat::Hex.decode("DEA").is_err());
471 assert_eq!(RevealFormat::Paper32.rows(35), 2);
472 assert_eq!(RevealFormat::Hex.rows(35), 3);
473 }
474
475 #[test]
476 fn a_paper32_string_splits_into_rows_of_groups_and_parity() {
477 let layout = RevealFormat::Paper32.layout();
478 let text = paper32::encode(&[0x5A; 35]);
479 let rows = layout.rows(&text);
480 assert_eq!(rows.len(), 2);
481 assert_eq!(rows[0].groups.len(), 7);
482 assert!(rows[0].groups.iter().all(|g| g.len() == 4));
483 assert_eq!(rows[0].parity.len(), 4);
484 assert_eq!(rows[1].parity.len(), 4);
485 assert_eq!(RevealFormat::Paper32.characters(35), 64);
486 assert_eq!(RevealFormat::Paper32.characters(32), 60);
487 assert_eq!(RevealFormat::Paper32.characters(1), 6);
488 }
489
490 #[test]
491 fn hex_has_no_parity() {
492 let layout = RevealFormat::Hex.layout();
493 let rows = layout.rows("DEADBEEF01");
494 assert_eq!(rows.len(), 1);
495 assert_eq!(rows[0].groups, ["DEAD", "BEEF", "01"]);
496 assert_eq!(rows[0].parity, "");
497 assert_eq!(hex_upper(&[0xde, 0xad]), "DEAD");
498 assert_eq!(RevealFormat::Hex.characters(5), 10);
499 }
500
501 #[test]
502 fn a_format_is_named_and_parsed() {
503 assert_eq!("paper32".parse(), Ok(RevealFormat::Paper32));
504 assert_eq!(RevealFormat::Hex.to_string(), "hex");
505 assert_eq!(
506 "words".parse::<RevealFormat>().unwrap_err().to_string(),
507 "'words' is not a format; use paper32, hex or bip39"
508 );
509 assert_eq!("bip39".parse(), Ok(RevealFormat::Bip39));
510 }
511
512 #[test]
513 fn a_phrase_is_a_word_per_row() {
514 let phrase = bip39::encode(&[0u8; 32]).unwrap();
515 let rows = RevealFormat::Bip39.layout().rows(&phrase);
516 assert_eq!(rows.len(), 24);
517 assert_eq!(rows[23].groups, ["art"]);
518 assert_eq!(rows[0].parity, "");
519 assert_eq!(RevealFormat::Bip39.rows(32), 24);
520 assert_eq!(RevealFormat::Bip39.rows(16), 12);
521 assert_eq!(RevealFormat::Bip39.length_of(32), 24);
522 assert_eq!(RevealFormat::Bip39.bytes_for(24), 32);
523
524 let row = RevealFormat::Bip39.read_row("ART", 24, false).unwrap();
526 assert_eq!(row.text.as_str(), "art");
527 assert!(row.full);
528 assert!(RevealFormat::Bip39.read_row("zoo", 1, true).unwrap().full);
529 assert!(RevealFormat::Bip39.read_row("abandonx", 1, true).is_err());
530 let typed: Vec<&str> = phrase.split(' ').collect();
531 assert_eq!(
532 *RevealFormat::Bip39.decode(&typed.join("\n")).unwrap().bytes,
533 [0u8; 32]
534 );
535 }
536}