Skip to main content

fedimint_core/
base32.rs

1use std::collections::BTreeMap;
2
3use thiserror::Error;
4
5use crate::encoding::{Decodable, DecodeError, Encodable};
6use crate::module::registry::ModuleDecoderRegistry;
7use crate::util::FmtCompact as _;
8
9/// Lowercase RFC 4648 Base32hex alphabet (32 characters).
10const RFC4648: [u8; 32] = *b"0123456789abcdefghijklmnopqrstuv";
11
12/// Prefix used for some of the user-facing Base32 encodings in Fedimint to
13/// allow easy identification
14pub const FEDIMINT_PREFIX: &str = "fedimint";
15
16/// Encodes the input bytes as Base32 (hex variant) using lowercase characters
17pub fn encode(input: &[u8]) -> String {
18    let mut output = Vec::with_capacity(((8 * input.len()) / 5) + 1);
19
20    let mut buffer = 0;
21    let mut bits = 0;
22
23    for byte in input {
24        buffer |= (*byte as usize) << bits;
25        bits += 8;
26
27        while bits >= 5 {
28            output.push(RFC4648[buffer & 0b11111]);
29
30            buffer >>= 5;
31            bits -= 5;
32        }
33    }
34
35    if bits > 0 {
36        output.push(RFC4648[buffer & 0b11111]);
37    }
38
39    String::from_utf8(output).unwrap()
40}
41
42/// Decodes a base 32 string back to raw bytes. Returns an error
43/// if any invalid character is encountered.
44pub fn decode(input: &str) -> Result<Vec<u8>, Base32DecodeError> {
45    let decode_table = RFC4648
46        .iter()
47        .enumerate()
48        .map(|(i, c)| (*c, i))
49        .collect::<BTreeMap<u8, usize>>();
50
51    let mut output = Vec::with_capacity(((5 * input.len()) / 8) + 1);
52
53    let mut buffer = 0;
54    let mut bits = 0;
55
56    for (index, ch) in input.char_indices() {
57        let value = ch
58            .is_ascii()
59            .then(|| decode_table.get(&(ch as u8)).copied())
60            .flatten()
61            .ok_or(Base32DecodeError::InvalidCharacter { ch, index })?;
62
63        buffer |= value << bits;
64        bits += 5;
65
66        while bits >= 8 {
67            output.push((buffer & 0xFF) as u8);
68
69            buffer >>= 8;
70            bits -= 8;
71        }
72    }
73
74    Ok(output)
75}
76
77pub fn encode_prefixed<T: Encodable>(prefix: &str, encodable: &T) -> String {
78    encode_prefixed_bytes(prefix, &encodable.consensus_encode_to_vec())
79}
80
81pub fn encode_prefixed_bytes(prefix: &str, bytes: &[u8]) -> String {
82    format!("{prefix}{}", encode(bytes))
83}
84
85pub fn decode_prefixed<T: Decodable>(prefix: &str, s: &str) -> Result<T, PrefixedDecodeError> {
86    Ok(T::consensus_decode_whole(
87        &decode_prefixed_bytes(prefix, s)?,
88        &ModuleDecoderRegistry::default(),
89    )?)
90}
91
92pub fn decode_prefixed_bytes(prefix: &str, s: &str) -> Result<Vec<u8>, PrefixedDecodeError> {
93    let s = s.to_lowercase();
94    if !s.starts_with(prefix) {
95        return Err(PrefixedDecodeError::InvalidPrefix {
96            expected: prefix.to_owned(),
97        });
98    }
99    Ok(decode(&s[prefix.len()..])?)
100}
101
102/// Failure to decode a raw base 32 string.
103#[derive(Debug, Error, Clone, Eq, PartialEq)]
104#[non_exhaustive]
105pub enum Base32DecodeError {
106    /// A character outside the RFC 4648 base32hex alphabet was encountered.
107    ///
108    /// `index` is a byte offset into the string passed to [`decode`]. When the
109    /// error originates in [`decode_prefixed`] or [`decode_prefixed_bytes`],
110    /// that string is the payload after the prefix was stripped and the input
111    /// was lowercased, not the string the caller supplied.
112    #[error("Invalid base32 character {ch:?} at byte index {index}")]
113    InvalidCharacter { ch: char, index: usize },
114}
115
116/// Failure to decode a prefixed base 32 string into a value.
117///
118/// The byte offset in a [`Base32DecodeError::InvalidCharacter`] source refers
119/// to the payload after the prefix was stripped and the input was lowercased,
120/// not to the string the caller supplied.
121#[derive(Debug, Error)]
122#[non_exhaustive]
123pub enum PrefixedDecodeError {
124    /// The string does not start with the expected prefix.
125    #[error("Invalid prefix, expected '{expected}'")]
126    InvalidPrefix { expected: String },
127    /// The payload is not valid base 32.
128    #[error("Invalid base32 payload: {0}")]
129    Base32Decode(Base32DecodeError),
130    /// The decoded bytes are not a valid consensus encoding of the target type.
131    #[error("Invalid consensus encoding in base32 payload: {}", .0.fmt_compact())]
132    ConsensusDecode(DecodeError),
133}
134
135impl From<DecodeError> for PrefixedDecodeError {
136    fn from(source: DecodeError) -> Self {
137        Self::ConsensusDecode(source)
138    }
139}
140
141impl From<Base32DecodeError> for PrefixedDecodeError {
142    fn from(source: Base32DecodeError) -> Self {
143        Self::Base32Decode(source)
144    }
145}
146
147#[test]
148fn test_base_32_roundtrip() {
149    const TEST_PREFIX: &str = "test";
150    let data: [u8; 10] = [0x50, 0xAB, 0x3F, 0x77, 0x01, 0xCD, 0x55, 0xFE, 0x10, 0x99];
151
152    for n in 1..10 {
153        let bytes = data[0..n].to_vec();
154
155        assert_eq!(decode(&encode(&bytes)).unwrap(), bytes);
156
157        assert_eq!(
158            decode_prefixed::<Vec<u8>>(TEST_PREFIX, &encode_prefixed(TEST_PREFIX, &bytes)).unwrap(),
159            bytes
160        );
161
162        assert_eq!(
163            decode_prefixed::<Vec<u8>>(
164                TEST_PREFIX,
165                &encode_prefixed(TEST_PREFIX, &bytes).to_ascii_uppercase()
166            )
167            .unwrap(),
168            bytes
169        );
170    }
171}
172
173#[test]
174fn decode_reports_invalid_character_position() {
175    assert_eq!(
176        decode("ab!c"),
177        Err(Base32DecodeError::InvalidCharacter { ch: '!', index: 2 })
178    );
179}
180
181#[test]
182fn decode_escapes_invalid_character_in_message() {
183    let err = decode("a\nb").expect_err("a newline is not in the base32 alphabet");
184
185    assert_eq!(
186        err,
187        Base32DecodeError::InvalidCharacter { ch: '\n', index: 1 }
188    );
189    assert_eq!(
190        err.to_string(),
191        "Invalid base32 character '\\n' at byte index 1"
192    );
193}
194
195#[test]
196fn decode_prefixed_bytes_rejects_wrong_prefix() {
197    assert!(matches!(
198        decode_prefixed_bytes("fed", "xyz00"),
199        Err(PrefixedDecodeError::InvalidPrefix { expected }) if expected == "fed"
200    ));
201}
202
203#[test]
204fn decode_prefixed_reports_the_whole_decode_chain() {
205    use std::str::FromStr;
206
207    use crate::encoding::Encodable;
208    use crate::invite_code::InviteCode;
209
210    // Dropping the last byte of a valid invite code cuts off mid-federation-id, so
211    // the reader runs out of input partway through the consensus decode instead of
212    // failing on the very first byte.
213    let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
214    let invite = InviteCode::from_str(invite_code_str).expect("valid invite code");
215    let bytes = invite.consensus_encode_to_vec();
216    let encoded = encode_prefixed_bytes(FEDIMINT_PREFIX, &bytes[..bytes.len() - 1]);
217
218    let err =
219        decode_prefixed::<InviteCode>(FEDIMINT_PREFIX, &encoded).expect_err("payload is truncated");
220    let text = err.to_string();
221    let err_fmt_compact = err.fmt_compact().to_string();
222    let PrefixedDecodeError::ConsensusDecode(inner) = err else {
223        panic!("a truncated payload is a decode error: {err:?}");
224    };
225
226    assert_eq!(
227        text,
228        format!(
229            "Invalid consensus encoding in base32 payload: {}",
230            inner.fmt_compact()
231        )
232    );
233    assert_ne!(inner.fmt_compact().to_string(), inner.to_string());
234    assert_eq!(
235        err_fmt_compact, text,
236        "no source, so nothing is printed twice"
237    );
238}