Skip to main content

codec_multibase/
base58btc.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use bs58::decode::Error as Bs58DecodeError;
6use thiserror::Error;
7use zeroize::Zeroizing;
8
9/// Maximum accepted base58btc input length.
10///
11/// The underlying base58 conversion is not linear in input size. This cap keeps
12/// untrusted byte and text inputs bounded while leaving headroom above the
13/// largest currently supported multikey encodings.
14pub const MAX_BASE58BTC_INPUT_LEN: usize = 5_600;
15
16/// Maximum decoded bytes whose worst-case base58btc spelling fits the text cap.
17/// Includes the two-byte multicodec prefix of a 4096-byte RSA public key.
18pub const MAX_BASE58BTC_DECODED_LEN: usize = 4_098;
19
20/// Error returned when base58btc decoding fails.
21#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum Base58Error {
24    /// The output buffer was too small to hold the decoded bytes.
25    #[error("base58btc output buffer too small")]
26    BufferTooSmall,
27    /// Decoding failed for a reason not covered by the other variants.
28    #[error("base58btc decode failed")]
29    DecodeFailed,
30    /// The input contained a character outside the base58btc alphabet.
31    #[error("invalid base58btc character")]
32    InvalidCharacter,
33    /// The input contained a non-ASCII character.
34    #[error("non-ascii base58btc character")]
35    NonAsciiCharacter,
36    /// The input exceeded the accepted base58btc text length.
37    #[error("base58btc input too large")]
38    InputTooLarge,
39}
40
41impl From<Bs58DecodeError> for Base58Error {
42    fn from(value: Bs58DecodeError) -> Self {
43        match value {
44            Bs58DecodeError::BufferTooSmall => Self::BufferTooSmall,
45            Bs58DecodeError::InvalidCharacter { .. } => Self::InvalidCharacter,
46            Bs58DecodeError::NonAsciiCharacter { .. } => Self::NonAsciiCharacter,
47            _ => Self::DecodeFailed,
48        }
49    }
50}
51
52/// Encodes bytes as a base58btc string.
53pub fn base58btc_encode(bytes: &[u8]) -> Result<String, Base58Error> {
54    if bytes.len() > MAX_BASE58BTC_DECODED_LEN {
55        return Err(Base58Error::InputTooLarge);
56    }
57    let mut output = Zeroizing::new(Vec::new());
58    bs58::encode(bytes)
59        .onto(&mut *output)
60        .map_err(|_| Base58Error::BufferTooSmall)?;
61    match String::from_utf8(core::mem::take(&mut *output)) {
62        Ok(encoded) => Ok(encoded),
63        Err(error) => {
64            let _bytes = Zeroizing::new(error.into_bytes());
65            Err(Base58Error::DecodeFailed)
66        }
67    }
68}
69
70/// Decodes a base58btc string into bytes.
71///
72/// Fails closed: returns an error on any invalid or non-ASCII character.
73pub fn base58btc_decode(s: &str) -> Result<Vec<u8>, Base58Error> {
74    if s.len() > MAX_BASE58BTC_INPUT_LEN {
75        return Err(Base58Error::InputTooLarge);
76    }
77    // The decoded byte count cannot exceed the ASCII input length. Supplying
78    // our own wiping buffer also covers a late invalid-character failure.
79    let mut output = Zeroizing::new(vec![0_u8; s.len()]);
80    let length = bs58::decode(s)
81        .onto(output.as_mut_slice())
82        .map_err(Base58Error::from)?;
83    output.truncate(length);
84    if length > MAX_BASE58BTC_DECODED_LEN {
85        return Err(Base58Error::InputTooLarge);
86    }
87    Ok(core::mem::take(&mut *output))
88}