Skip to content

Commit 254202e

Browse files
committed
Enforce maximum string length
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.
1 parent ab0fd68 commit 254202e

File tree

5 files changed

+334
-37
lines changed

5 files changed

+334
-37
lines changed

src/lib.rs

Lines changed: 173 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
//! a data part. A checksum at the end of the string provides error detection to prevent mistakes
1010
//! when the string is written off or read out loud.
1111
//!
12+
//! Please note, in order to support lighting ([BOLT-11]) we do not enforce the 90 character limit
13+
//! specified by [BIP-173], instead we use 1023 because that is a property of the `Bech32` and
14+
//! `Bech32m` checksum algorithms (specifically error detection, see the [`checksum`] module
15+
//! documentation for more information). We do however enforce the 90 character limit within the
16+
//! `segwit` modules.
17+
//!
1218
//! # Usage
1319
//!
1420
//! - If you are doing segwit stuff you likely want to use the [`segwit`] API.
@@ -89,6 +95,10 @@
8995
//!
9096
//! ## Custom Checksum
9197
//!
98+
//! Please note, if your checksum algorithm can detect errors in data greater than 1023 characters,
99+
//! and you intend on leveraging this fact, then this crate will not currently serve your needs.
100+
//! Patches welcome.
101+
//!
92102
//! ```
93103
//! # #[cfg(feature = "alloc")] {
94104
//! use bech32::Checksum;
@@ -113,6 +123,9 @@
113123
//!
114124
//! # }
115125
//! ```
126+
//!
127+
//! [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
128+
//! [`checksum`]: crate::primitives::checksum
116129
117130
#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
118131
// Experimental features we need.
@@ -144,6 +157,7 @@ use core::fmt;
144157

145158
#[cfg(feature = "alloc")]
146159
use crate::error::write_err;
160+
use crate::primitives::checksum::MAX_STRING_LENGTH;
147161
#[cfg(doc)]
148162
use crate::primitives::decode::CheckedHrpstring;
149163
#[cfg(feature = "alloc")]
@@ -214,19 +228,32 @@ pub fn decode(s: &str) -> Result<(Hrp, Vec<u8>), DecodeError> {
214228
///
215229
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
216230
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
231+
///
232+
/// ## Deviation from spec (BIP-173)
233+
///
234+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
217235
#[cfg(feature = "alloc")]
218236
#[inline]
219-
pub fn encode<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
237+
pub fn encode<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
238+
let encoded_length = encoded_length::<Ck>(hrp, data);
239+
if encoded_length > MAX_STRING_LENGTH {
240+
return Err(EncodeError::TooLong(encoded_length));
241+
}
242+
220243
encode_lower::<Ck>(hrp, data)
221244
}
222245

223246
/// Encodes `data` as a lowercase bech32 encoded string.
224247
///
225248
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
226249
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
250+
///
251+
/// ## Deviation from spec (BIP-173)
252+
///
253+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
227254
#[cfg(feature = "alloc")]
228255
#[inline]
229-
pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
256+
pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
230257
let mut buf = String::new();
231258
encode_lower_to_fmt::<Ck, String>(&mut buf, hrp, data)?;
232259
Ok(buf)
@@ -236,9 +263,13 @@ pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::
236263
///
237264
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
238265
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
266+
///
267+
/// ## Deviation from spec (BIP-173)
268+
///
269+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
239270
#[cfg(feature = "alloc")]
240271
#[inline]
241-
pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
272+
pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
242273
let mut buf = String::new();
243274
encode_upper_to_fmt::<Ck, String>(&mut buf, hrp, data)?;
244275
Ok(buf)
@@ -248,25 +279,33 @@ pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::
248279
///
249280
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
250281
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
282+
///
283+
/// ## Deviation from spec (BIP-173)
284+
///
285+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
251286
#[inline]
252287
pub fn encode_to_fmt<Ck: Checksum, W: fmt::Write>(
253288
fmt: &mut W,
254289
hrp: Hrp,
255290
data: &[u8],
256-
) -> Result<(), fmt::Error> {
291+
) -> Result<(), EncodeError> {
257292
encode_lower_to_fmt::<Ck, W>(fmt, hrp, data)
258293
}
259294

260295
/// Encodes `data` to a writer ([`fmt::Write`]) as a lowercase bech32 encoded string.
261296
///
262297
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
263298
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
299+
///
300+
/// ## Deviation from spec (BIP-173)
301+
///
302+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
264303
#[inline]
265304
pub fn encode_lower_to_fmt<Ck: Checksum, W: fmt::Write>(
266305
fmt: &mut W,
267306
hrp: Hrp,
268307
data: &[u8],
269-
) -> Result<(), fmt::Error> {
308+
) -> Result<(), EncodeError> {
270309
let iter = data.iter().copied().bytes_to_fes();
271310
let chars = iter.with_checksum::<Ck>(&hrp).chars();
272311
for c in chars {
@@ -279,12 +318,16 @@ pub fn encode_lower_to_fmt<Ck: Checksum, W: fmt::Write>(
279318
///
280319
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
281320
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
321+
///
322+
/// ## Deviation from spec (BIP-173)
323+
///
324+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
282325
#[inline]
283326
pub fn encode_upper_to_fmt<Ck: Checksum, W: fmt::Write>(
284327
fmt: &mut W,
285328
hrp: Hrp,
286329
data: &[u8],
287-
) -> Result<(), fmt::Error> {
330+
) -> Result<(), EncodeError> {
288331
let iter = data.iter().copied().bytes_to_fes();
289332
let chars = iter.with_checksum::<Ck>(&hrp).chars();
290333
for c in chars {
@@ -297,27 +340,35 @@ pub fn encode_upper_to_fmt<Ck: Checksum, W: fmt::Write>(
297340
///
298341
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
299342
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
343+
///
344+
/// ## Deviation from spec (BIP-173)
345+
///
346+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
300347
#[cfg(feature = "std")]
301348
#[inline]
302349
pub fn encode_to_writer<Ck: Checksum, W: std::io::Write>(
303350
w: &mut W,
304351
hrp: Hrp,
305352
data: &[u8],
306-
) -> Result<(), std::io::Error> {
353+
) -> Result<(), EncodeIoError> {
307354
encode_lower_to_writer::<Ck, W>(w, hrp, data)
308355
}
309356

310357
/// Encodes `data` to a writer ([`std::io::Write`]) as a lowercase bech32 encoded string.
311358
///
312359
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
313360
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
361+
///
362+
/// ## Deviation from spec (BIP-173)
363+
///
364+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
314365
#[cfg(feature = "std")]
315366
#[inline]
316367
pub fn encode_lower_to_writer<Ck: Checksum, W: std::io::Write>(
317368
w: &mut W,
318369
hrp: Hrp,
319370
data: &[u8],
320-
) -> Result<(), std::io::Error> {
371+
) -> Result<(), EncodeIoError> {
321372
let iter = data.iter().copied().bytes_to_fes();
322373
let chars = iter.with_checksum::<Ck>(&hrp).chars();
323374
for c in chars {
@@ -330,13 +381,17 @@ pub fn encode_lower_to_writer<Ck: Checksum, W: std::io::Write>(
330381
///
331382
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
332383
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
384+
///
385+
/// ## Deviation from spec (BIP-173)
386+
///
387+
/// We only restrict the total length of the encoded string to 1023 characters (not 90).
333388
#[cfg(feature = "std")]
334389
#[inline]
335390
pub fn encode_upper_to_writer<Ck: Checksum, W: std::io::Write>(
336391
w: &mut W,
337392
hrp: Hrp,
338393
data: &[u8],
339-
) -> Result<(), std::io::Error> {
394+
) -> Result<(), EncodeIoError> {
340395
let iter = data.iter().copied().bytes_to_fes();
341396
let chars = iter.with_checksum::<Ck>(&hrp).chars();
342397
for c in chars {
@@ -392,6 +447,88 @@ impl From<UncheckedHrpstringError> for DecodeError {
392447
fn from(e: UncheckedHrpstringError) -> Self { Self::Parse(e) }
393448
}
394449

450+
/// An error while encoding a bech32 string.
451+
#[cfg(feature = "alloc")]
452+
#[derive(Debug, Clone, PartialEq, Eq)]
453+
#[non_exhaustive]
454+
pub enum EncodeError {
455+
/// Encoding HRP and data into a bech32 string exceeds maximum allowed.
456+
TooLong(usize),
457+
/// Encode to formatter failed.
458+
Fmt(fmt::Error),
459+
}
460+
461+
impl fmt::Display for EncodeError {
462+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
463+
use EncodeError::*;
464+
465+
match *self {
466+
TooLong(len) =>
467+
write!(f, "encoded length {} exceeds spec limit {} chars", len, MAX_STRING_LENGTH),
468+
Fmt(ref e) => write_err!(f, "encode to formatter failed"; e),
469+
}
470+
}
471+
}
472+
473+
#[cfg(feature = "std")]
474+
impl std::error::Error for EncodeError {
475+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
476+
use EncodeError::*;
477+
478+
match *self {
479+
TooLong(_) => None,
480+
Fmt(ref e) => Some(e),
481+
}
482+
}
483+
}
484+
485+
impl From<fmt::Error> for EncodeError {
486+
#[inline]
487+
fn from(e: fmt::Error) -> Self { Self::Fmt(e) }
488+
}
489+
490+
/// An error while encoding a bech32 string.
491+
#[cfg(feature = "std")]
492+
#[derive(Debug)]
493+
#[non_exhaustive]
494+
pub enum EncodeIoError {
495+
/// Encoding HRP and data into a bech32 string exceeds maximum allowed.
496+
TooLong(usize),
497+
/// Encode to writer failed.
498+
Write(std::io::Error),
499+
}
500+
501+
#[cfg(feature = "std")]
502+
impl fmt::Display for EncodeIoError {
503+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
504+
use EncodeIoError::*;
505+
506+
match *self {
507+
TooLong(len) =>
508+
write!(f, "encoded length {} exceeds spec limit {} chars", len, MAX_STRING_LENGTH),
509+
Write(ref e) => write_err!(f, "encode to writer failed"; e),
510+
}
511+
}
512+
}
513+
514+
#[cfg(feature = "std")]
515+
impl std::error::Error for EncodeIoError {
516+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
517+
use EncodeIoError::*;
518+
519+
match *self {
520+
TooLong(_) => None,
521+
Write(ref e) => Some(e),
522+
}
523+
}
524+
}
525+
526+
#[cfg(feature = "std")]
527+
impl From<std::io::Error> for EncodeIoError {
528+
#[inline]
529+
fn from(e: std::io::Error) -> Self { Self::Write(e) }
530+
}
531+
395532
#[cfg(test)]
396533
#[cfg(feature = "alloc")]
397534
mod tests {
@@ -493,4 +630,31 @@ mod tests {
493630

494631
assert_eq!(got, want);
495632
}
633+
634+
#[test]
635+
fn can_encode_maximum_length_string() {
636+
let data = [0_u8; 632];
637+
let hrp = Hrp::parse_unchecked("abcd");
638+
let s = encode::<Bech32m>(hrp, &data).expect("failed to encode string");
639+
assert_eq!(s.len(), 1023);
640+
}
641+
642+
#[test]
643+
fn can_not_encode_string_too_long() {
644+
let data = [0_u8; 632];
645+
let hrp = Hrp::parse_unchecked("abcde");
646+
647+
match encode::<Bech32m>(hrp, &data) {
648+
Ok(_) => panic!("false positive"),
649+
Err(EncodeError::TooLong(len)) => assert_eq!(len, 1024),
650+
_ => panic!("false negative"),
651+
}
652+
}
653+
654+
#[test]
655+
fn can_decode_segwit_too_long_string() {
656+
// A 91 character long string, greater than the segwit enforced maximum of 90.
657+
let s = "abcd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrw9z3s";
658+
assert!(decode(s).is_ok());
659+
}
496660
}

src/primitives/checksum.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,24 @@
22

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

724
use core::{mem, ops};
825

0 commit comments

Comments
 (0)