Skip to content

Commit 64310d5

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 us, architecturally speaking 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 not in the functions in `lib.rs`. We document this decision in the crate level docs as well as on the individual functions. FTR in `bech32 v0.9.0` the lengths were not enforced either.
1 parent a0fa43d commit 64310d5

File tree

4 files changed

+199
-25
lines changed

4 files changed

+199
-25
lines changed

src/lib.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
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, so as to support lighting ([BOLT-11]) we explicitly do not do string length checks
13+
//! in the top level API. We do however enforce the 90 character limit within the `segwit` modules.
14+
//!
1215
//! # Usage
1316
//!
1417
//! - If you are doing segwit stuff you likely want to use the [`segwit`] API.
@@ -113,6 +116,8 @@
113116
//!
114117
//! # }
115118
//! ```
119+
//!
120+
//! [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
116121
117122
#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
118123
// Experimental features we need.
@@ -169,6 +174,8 @@ pub use {
169174
/// If your input string has no checksum use the [`CheckedHrpstring`] constructor, which allows
170175
/// selecting the checksum algorithm explicitly.
171176
///
177+
/// Note: this function does not enforce any restrictions on the total length of the input string.
178+
///
172179
/// # Returns
173180
///
174181
/// The human-readable part and the encoded data with the checksum removed.
@@ -214,6 +221,15 @@ pub fn decode(s: &str) -> Result<(Hrp, Vec<u8>), DecodeError> {
214221
///
215222
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
216223
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
224+
///
225+
/// ## Deviation from spec (BIP-173)
226+
///
227+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
228+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
229+
///
230+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
231+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
232+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
217233
#[cfg(feature = "alloc")]
218234
#[inline]
219235
pub fn encode<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
@@ -224,6 +240,15 @@ pub fn encode<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error>
224240
///
225241
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
226242
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
243+
///
244+
/// ## Deviation from spec (BIP-173)
245+
///
246+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
247+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
248+
///
249+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
250+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
251+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
227252
#[cfg(feature = "alloc")]
228253
#[inline]
229254
pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
@@ -236,6 +261,15 @@ pub fn encode_lower<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::
236261
///
237262
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
238263
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
264+
///
265+
/// ## Deviation from spec (BIP-173)
266+
///
267+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
268+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
269+
///
270+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
271+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
272+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
239273
#[cfg(feature = "alloc")]
240274
#[inline]
241275
pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::Error> {
@@ -248,6 +282,15 @@ pub fn encode_upper<Ck: Checksum>(hrp: Hrp, data: &[u8]) -> Result<String, fmt::
248282
///
249283
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
250284
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
285+
///
286+
/// ## Deviation from spec (BIP-173)
287+
///
288+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
289+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
290+
///
291+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
292+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
293+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
251294
#[inline]
252295
pub fn encode_to_fmt<Ck: Checksum, W: fmt::Write>(
253296
fmt: &mut W,
@@ -261,6 +304,15 @@ pub fn encode_to_fmt<Ck: Checksum, W: fmt::Write>(
261304
///
262305
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
263306
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
307+
///
308+
/// ## Deviation from spec (BIP-173)
309+
///
310+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
311+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
312+
///
313+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
314+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
315+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
264316
#[inline]
265317
pub fn encode_lower_to_fmt<Ck: Checksum, W: fmt::Write>(
266318
fmt: &mut W,
@@ -279,6 +331,15 @@ pub fn encode_lower_to_fmt<Ck: Checksum, W: fmt::Write>(
279331
///
280332
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
281333
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
334+
///
335+
/// ## Deviation from spec (BIP-173)
336+
///
337+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
338+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
339+
///
340+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
341+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
342+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
282343
#[inline]
283344
pub fn encode_upper_to_fmt<Ck: Checksum, W: fmt::Write>(
284345
fmt: &mut W,
@@ -297,6 +358,15 @@ pub fn encode_upper_to_fmt<Ck: Checksum, W: fmt::Write>(
297358
///
298359
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
299360
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
361+
///
362+
/// ## Deviation from spec (BIP-173)
363+
///
364+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
365+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
366+
///
367+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
368+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
369+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
300370
#[cfg(feature = "std")]
301371
#[inline]
302372
pub fn encode_to_writer<Ck: Checksum, W: std::io::Write>(
@@ -311,6 +381,15 @@ pub fn encode_to_writer<Ck: Checksum, W: std::io::Write>(
311381
///
312382
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
313383
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
384+
///
385+
/// ## Deviation from spec (BIP-173)
386+
///
387+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
388+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
389+
///
390+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
391+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
392+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
314393
#[cfg(feature = "std")]
315394
#[inline]
316395
pub fn encode_lower_to_writer<Ck: Checksum, W: std::io::Write>(
@@ -330,6 +409,15 @@ pub fn encode_lower_to_writer<Ck: Checksum, W: std::io::Write>(
330409
///
331410
/// Encoded string will be prefixed with the `hrp` and have a checksum appended as specified by the
332411
/// `Ck` algorithm (`NoChecksum` to exclude checksum all together).
412+
///
413+
/// ## Deviation from spec (BIP-173)
414+
///
415+
/// In order to support [BOLT-11] this function does not restrict the total length of the returned
416+
/// string. To encode [BIP-173] / [BIP-350] compliant segwit addresses use [`segwit::encode`].
417+
///
418+
/// [BIP-173]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
419+
/// [BIP-350]: <https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki>
420+
/// [BOLT-11]: <https://github.com/lightning/bolts/blob/master/11-payment-encoding.md>
333421
#[cfg(feature = "std")]
334422
#[inline]
335423
pub fn encode_upper_to_writer<Ck: Checksum, W: std::io::Write>(
@@ -493,4 +581,29 @@ mod tests {
493581

494582
assert_eq!(got, want);
495583
}
584+
585+
#[test]
586+
fn can_encode_really_long_string() {
587+
// Encode around the bech32 limit, mainly here as documentation.
588+
let tcs = vec![
589+
// Also shows there are no limit checks on the data slice (since segwit limits this to 40 bytes).
590+
([0_u8; 50], Hrp::parse_unchecked("abc")), // Encodes to 90 characters.
591+
([0_u8; 50], Hrp::parse_unchecked("abcd")), // Encodes to 91 characters.
592+
];
593+
for (data, hrp) in tcs {
594+
assert!(encode::<Bech32>(&hrp, &data).is_ok());
595+
}
596+
597+
// Encode something arbitrarily long.
598+
let data = [0_u8; 1024];
599+
let hrp = Hrp::parse_unchecked("abc");
600+
assert!(encode::<Bech32m>(&hrp, &data).is_ok());
601+
}
602+
603+
#[test]
604+
fn can_decode_long_string() {
605+
// A 91 character long string, greater than the segwit enforced maximum of 90.
606+
let s = "abcd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrw9z3s";
607+
assert!(decode(s).is_ok());
608+
}
496609
}

src/primitives/decode.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ use crate::primitives::gf32::Fe32;
8181
use crate::primitives::hrp::{self, Hrp};
8282
use crate::primitives::iter::{Fe32IterExt, FesToBytes};
8383
use crate::primitives::segwit::{self, WitnessLengthError, VERSION_0};
84+
use crate::primitives::MAX_STRING_LENGTH;
8485
use crate::{Bech32, Bech32m};
8586

8687
/// Separator between the hrp and payload (as defined by BIP-173).
@@ -89,7 +90,7 @@ const SEP: char = '1';
8990
/// An HRP string that has been parsed but not yet had the checksum checked.
9091
///
9192
/// Parsing an HRP string only checks validity of the characters, it does not validate the
92-
/// checksum in any way.
93+
/// checksum in any way. Nor do we check the length of the parsed string.
9394
///
9495
/// Unless you are attempting to validate a string with multiple checksums then you likely do not
9596
/// want to use this type directly, instead use [`CheckedHrpstring::new(s)`].
@@ -217,7 +218,9 @@ impl<'s> UncheckedHrpstring<'s> {
217218
/// > We first describe the general checksummed base32 format called Bech32 and then
218219
/// > define Segregated Witness addresses using it.
219220
///
220-
/// This type abstracts over the general checksummed base32 format called Bech32.
221+
/// This type abstracts over the general checksummed base32 format called Bech32. In order to
222+
/// support `BOLT-11` we explicitly do not check the length of the parsed string, this is however,
223+
/// done by the [`SegwitHrpstring`] type.
221224
///
222225
/// # Examples
223226
///
@@ -331,8 +334,8 @@ impl<'s> CheckedHrpstring<'s> {
331334
}
332335
}
333336

334-
/// An HRP string that has been parsed, had the checksum validated, had the witness version
335-
/// validated, had the witness data length checked, and the had witness version and checksum
337+
/// An valid length HRP string that has been parsed, had the checksum validated, had the witness
338+
/// version validated, had the witness data length checked, and the had witness version and checksum
336339
/// removed.
337340
///
338341
/// # Examples
@@ -368,6 +371,11 @@ impl<'s> SegwitHrpstring<'s> {
368371
/// to get strict BIP conformance (also [`Hrp::is_valid_on_mainnet`] and friends).
369372
#[inline]
370373
pub fn new(s: &'s str) -> Result<Self, SegwitHrpstringError> {
374+
let len = s.len();
375+
if len > MAX_STRING_LENGTH {
376+
return Err(SegwitHrpstringError::Unchecked(UncheckedHrpstringError::TooLong(len)));
377+
}
378+
371379
let unchecked = UncheckedHrpstring::new(s)?;
372380

373381
if unchecked.data.is_empty() {
@@ -658,6 +666,8 @@ impl From<ChecksumError> for CheckedHrpstringError {
658666
#[derive(Debug, Clone, PartialEq, Eq)]
659667
#[non_exhaustive]
660668
pub enum UncheckedHrpstringError {
669+
/// String exceeds maximum allowed length.
670+
TooLong(usize),
661671
/// An error with the characters of the input string.
662672
Char(CharError),
663673
/// The human-readable part is invalid.
@@ -669,6 +679,8 @@ impl fmt::Display for UncheckedHrpstringError {
669679
use UncheckedHrpstringError::*;
670680

671681
match *self {
682+
TooLong(len) =>
683+
write!(f, "string exceeds spec limit of {} chars: {}", MAX_STRING_LENGTH, len),
672684
Char(ref e) => write_err!(f, "character error"; e),
673685
Hrp(ref e) => write_err!(f, "invalid human-readable part"; e),
674686
}
@@ -681,6 +693,7 @@ impl std::error::Error for UncheckedHrpstringError {
681693
use UncheckedHrpstringError::*;
682694

683695
match *self {
696+
TooLong(_) => None,
684697
Char(ref e) => Some(e),
685698
Hrp(ref e) => Some(e),
686699
}

src/primitives/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ pub mod segwit;
1212

1313
use checksum::{Checksum, PackedNull};
1414

15+
/// The maximum allowed length of a bech32 string (see [`BIP-173`]).
16+
///
17+
/// [`BIP-173`]: <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki>
18+
pub const MAX_STRING_LENGTH: usize = 90;
19+
1520
/// The "null checksum" used on bech32 strings for which we want to do no checksum checking.
1621
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1722
pub enum NoChecksum {}

0 commit comments

Comments
 (0)