Skip to content

Commit

Permalink
Enforce maximum string length
Browse files Browse the repository at this point in the history
BIP-173 states that a bech32 string must not exceed 90 characters
however BOLT-11 states that the string limit may be exceeded. This puts
in a conundrum - we want to support lightning but this crate pretty
heavily documents itself as an implementation of BIP-173 and BIP-350.

The solution we choose is to enforce the string limit in the segwit
modules and types (`SegwitHrpstring`) and in `lib.rs` and non-segwit
types (eg, `UncheckedHrpstring`) we enforce a limit of 1023.

Enforce string length limits by doing:

- Enforce and document a 1023 character string limit when encoding and
  decoding non-segwit strings.
- Enforce and document a 90 character string limit when encoding and
  decoding segwit strings (addresses).
- Document and make explicit that the 1023 limit is a rust-bech32 thing,
  based on the BCH code design in BIP-173 but is not part of any
  explicit spec.

FTR in `bech32 v0.9.0` no lengths were not enforced.
  • Loading branch information
tcharding committed Oct 24, 2023
1 parent f4a3616 commit 24f1496
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 38 deletions.
182 changes: 172 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
//! a data part. A checksum at the end of the string provides error detection to prevent mistakes
//! when the string is written off or read out loud.
//!
//! Please note, in order to support lighting ([BOLT-11]) we do not enforce the 90 character limit
//! specified by [BIP-173], instead we use 1023 because that is a property of the `Bech32` and
//! `Bech32m` checksum algorithms (specifically error detection, see the [`checksum`] module
//! documentation for more information). We do however enforce the 90 character limit within the
//! `segwit` modules.
//!
//! # Usage
//!
//! - If you are doing segwit stuff you likely want to use the [`segwit`] API.
Expand Down Expand Up @@ -89,6 +95,10 @@
//!
//! ## Custom Checksum
//!
//! Please note, if your checksum algorithm can detect errors in data greater than 1023 characters,
//! and you intend on leveraging this fact, then this crate will not currently serve your needs.
//! Patches welcome.
//!
//! ```
//! # #[cfg(feature = "alloc")] {
//! use bech32::Checksum;
Expand All @@ -113,6 +123,9 @@
//!
//! # }
//! ```
//!
//! [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
//! [`checksum`]: crate::primitives::checksum
#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
// Experimental features we need.
Expand Down Expand Up @@ -142,8 +155,8 @@ pub mod segwit;
use alloc::{string::String, vec::Vec};
use core::fmt;

#[cfg(feature = "alloc")]
use crate::error::write_err;
use crate::primitives::checksum::MAX_STRING_LENGTH;
#[cfg(doc)]
use crate::primitives::decode::CheckedHrpstring;
#[cfg(feature = "alloc")]
Expand Down Expand Up @@ -214,19 +227,32 @@ pub fn decode(s: &str) -> Result<(Hrp, Vec<u8>), DecodeError> {
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[cfg(feature = "alloc")]
#[inline]
pub fn encode<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
pub fn encode<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
let encoded_length = encoded_length::<Ck>(hrp, data);
if encoded_length > MAX_STRING_LENGTH {
return Err(EncodeError::TooLong(encoded_length));
}

encode_lower::<Ck>(hrp, data)
}

/// Encodes `data` as a lowercase bech32 encoded string.
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[cfg(feature = "alloc")]
#[inline]
pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
let mut buf = String::new();
encode_lower_to_fmt::<Ck, String>(&mut buf, hrp, data)?;
Ok(buf)
Expand All @@ -236,9 +262,13 @@ pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[cfg(feature = "alloc")]
#[inline]
pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
let mut buf = String::new();
encode_upper_to_fmt::<Ck, String>(&mut buf, hrp, data)?;
Ok(buf)
Expand All @@ -248,25 +278,33 @@ pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[inline]
pub fn encode_to_fmt<Ck: Checksum, W: fmt::Write>(
fmt: &mut W,
hrp: Hrp,
data: &[u8],
) -> Result<(), fmt::Error> {
) -> Result<(), EncodeError> {
encode_lower_to_fmt::<Ck, W>(fmt, hrp, data)
}

/// Encodes `data` to a writer ([`fmt::Write`]) as a lowercase bech32 encoded string.
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[inline]
pub fn encode_lower_to_fmt<Ck: Checksum, W: fmt::Write>(
fmt: &mut W,
hrp: Hrp,
data: &[u8],
) -> Result<(), fmt::Error> {
) -> Result<(), EncodeError> {
let iter = data.iter().copied().bytes_to_fes();
let chars = iter.with_checksum::<Ck>(&hrp).chars();
for c in chars {
Expand All @@ -279,12 +317,16 @@ pub fn encode_lower_to_fmt<Ck: Checksum, W: fmt::Write>(
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[inline]
pub fn encode_upper_to_fmt<Ck: Checksum, W: fmt::Write>(
fmt: &mut W,
hrp: Hrp,
data: &[u8],
) -> Result<(), fmt::Error> {
) -> Result<(), EncodeError> {
let iter = data.iter().copied().bytes_to_fes();
let chars = iter.with_checksum::<Ck>(&hrp).chars();
for c in chars {
Expand All @@ -297,27 +339,35 @@ pub fn encode_upper_to_fmt<Ck: Checksum, W: fmt::Write>(
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[cfg(feature = "std")]
#[inline]
pub fn encode_to_writer<Ck: Checksum, W: std::io::Write>(
w: &mut W,
hrp: Hrp,
data: &[u8],
) -> Result<(), std::io::Error> {
) -> Result<(), EncodeIoError> {
encode_lower_to_writer::<Ck, W>(w, hrp, data)
}

/// Encodes `data` to a writer ([`std::io::Write`]) as a lowercase bech32 encoded string.
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[cfg(feature = "std")]
#[inline]
pub fn encode_lower_to_writer<Ck: Checksum, W: std::io::Write>(
w: &mut W,
hrp: Hrp,
data: &[u8],
) -> Result<(), std::io::Error> {
) -> Result<(), EncodeIoError> {
let iter = data.iter().copied().bytes_to_fes();
let chars = iter.with_checksum::<Ck>(&hrp).chars();
for c in chars {
Expand All @@ -330,13 +380,17 @@ pub fn encode_lower_to_writer<Ck: Checksum, W: std::io::Write>(
///
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
///
/// ## Deviation from spec (BIP-173)
///
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
#[cfg(feature = "std")]
#[inline]
pub fn encode_upper_to_writer<Ck: Checksum, W: std::io::Write>(
w: &mut W,
hrp: Hrp,
data: &[u8],
) -> Result<(), std::io::Error> {
) -> Result<(), EncodeIoError> {
let iter = data.iter().copied().bytes_to_fes();
let chars = iter.with_checksum::<Ck>(&hrp).chars();
for c in chars {
Expand Down Expand Up @@ -392,6 +446,87 @@ impl From<UncheckedHrpstringError> for DecodeError {
fn from(e: UncheckedHrpstringError) -> Self { Self::Parse(e) }
}

/// An error while encoding a bech32 string.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncodeError {
/// Encoding HRP and data into a bech32 string exceeds maximum allowed.
TooLong(usize),
/// Encode to formatter failed.
Fmt(fmt::Error),
}

impl fmt::Display for EncodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use EncodeError::*;

match *self {
TooLong(len) =>
write!(f, "encoded length {} exceeds spec limit {} chars", len, MAX_STRING_LENGTH),
Fmt(ref e) => write_err!(f, "encode to formatter failed"; e),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for EncodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use EncodeError::*;

match *self {
TooLong(_) => None,
Fmt(ref e) => Some(e),
}
}
}

impl From<fmt::Error> for EncodeError {
#[inline]
fn from(e: fmt::Error) -> Self { Self::Fmt(e) }
}

/// An error while encoding a bech32 string.
#[cfg(feature = "std")]
#[derive(Debug)]
#[non_exhaustive]
pub enum EncodeIoError {
/// Encoding HRP and data into a bech32 string exceeds maximum allowed.
TooLong(usize),
/// Encode to writer failed.
Write(std::io::Error),
}

#[cfg(feature = "std")]
impl fmt::Display for EncodeIoError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use EncodeIoError::*;

match *self {
TooLong(len) =>
write!(f, "encoded length {} exceeds spec limit {} chars", len, MAX_STRING_LENGTH),
Write(ref e) => write_err!(f, "encode to writer failed"; e),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for EncodeIoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use EncodeIoError::*;

match *self {
TooLong(_) => None,
Write(ref e) => Some(e),
}
}
}

#[cfg(feature = "std")]
impl From<std::io::Error> for EncodeIoError {
#[inline]
fn from(e: std::io::Error) -> Self { Self::Write(e) }
}

#[cfg(test)]
#[cfg(feature = "alloc")]
mod tests {
Expand Down Expand Up @@ -493,4 +628,31 @@ mod tests {

assert_eq!(got, want);
}

#[test]
fn can_encode_maximum_length_string() {
let data = [0_u8; 632];
let hrp = Hrp::parse_unchecked("abcd");
let s = encode::<Bech32m>(hrp, &data).expect("failed to encode string");
assert_eq!(s.len(), 1023);
}

#[test]
fn can_not_encode_string_too_long() {
let data = [0_u8; 632];
let hrp = Hrp::parse_unchecked("abcde");

match encode::<Bech32m>(hrp, &data) {
Ok(_) => panic!("false positive"),
Err(EncodeError::TooLong(len)) => assert_eq!(len, 1024),
_ => panic!("false negative"),
}
}

#[test]
fn can_decode_segwit_too_long_string() {
// A 91 character long string, greater than the segwit enforced maximum of 90.
let s = "abcd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrw9z3s";
assert!(decode(s).is_ok());
}
}
17 changes: 17 additions & 0 deletions src/primitives/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,24 @@

//! Degree-2 [BCH] code checksum.
//!
//! How this BCH code was chosen to be used by the bech32 address format is outlined in BIP-173 in
//! the ["Checksum design"] section, of particular importance is:
//!
//! > Even though the chosen code performs reasonably well up to 1023 characters, other designs are
//! > preferable for lengths above 89 characters (excluding the separator).
//!
//! The segwit address format uses, for this reason, a 90 character limit. Lightning's [BOLT-11]
//! does not use such a limit, we would like to support lightning addresses but we choose to enforce
//! a hard limit of 1023 characters, this is purely a `rust-bech32` decision.
//!
//! [BCH]: <https://en.wikipedia.org/wiki/BCH_code>
//! ["Checksum design"]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#user-content-Checksum_design>
/// The maximum enforced string length of a bech32 string.
pub const MAX_STRING_LENGTH: usize = 1023;

/// The maximum enforced string length of a segwit address.
pub const MAX_SEGWIT_STRING_LENGTH: usize = 90;

use core::{mem, ops};

Expand Down
Loading

0 comments on commit 24f1496

Please sign in to comment.