From 6ca06b1ec89eb797e5cafd84846a7d7c1e2e37dd Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 19 Oct 2023 10:28:09 +1100 Subject: [PATCH] Enforce maximum string length BIP-173 states that a bech32 string must not exceed 90 characters but this is a bip specification, the bech32/bech32m checksum algos can actually cover more characters than that. To keep the `segwit` stuff bip compliant and also keep the library general add an associated `CODE_LENGTH` const to the `Checksum` trait. Then in the `segwit` module and types enforce the 90 character limit but in the general API and modules enforce the `Ck::CODE_LENGTH` limit. FTR in `bech32 v0.9.0` no lengths were not enforced. --- src/lib.rs | 180 +++++++++++++++++++++++++++++++++---- src/primitives/checksum.rs | 9 +- src/primitives/decode.rs | 96 +++++++++++++++++++- src/primitives/mod.rs | 3 + src/primitives/segwit.rs | 8 ++ src/segwit.rs | 108 ++++++++++++++++++---- 6 files changed, 363 insertions(+), 41 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6c077209b..6c7a13f97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,12 @@ //! The original description in [BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) //! has more details. See also [BIP-0350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki). //! +//! # Deviation from spec +//! +//! We do not enforce the 90 character limit specified by [BIP-173], instead we enforce the code +//! length for the respective checksum algorithm (see [`Checksum::CODE_LENGTH`]). We do however +//! enforce the 90 character limit within the `segwit` modules and types. +//! //! # Examples //! //! ## Encoding @@ -100,6 +106,7 @@ //! impl Checksum for Codex32 { //! type MidstateRepr = u128; //! const CHECKSUM_LENGTH: usize = 13; +//! const CODE_LENGTH: usize = 93; //! // Copied from BIP-93 //! const GENERATOR_SH: [u128; 5] = [ //! 0x19dc500ce73fde210, @@ -113,6 +120,8 @@ //! //! # } //! ``` +//! +//! [`Checksum::CODE_LENGTH`]: crate::primitives::checksum::Checksum::CODE_LENGTH #![cfg_attr(all(not(feature = "std"), not(test)), no_std)] // Experimental features we need. @@ -142,14 +151,12 @@ pub mod segwit; use alloc::{string::String, vec::Vec}; use core::fmt; -#[cfg(feature = "alloc")] use crate::error::write_err; #[cfg(doc)] use crate::primitives::decode::CheckedHrpstring; +use crate::primitives::decode::CodeLengthError; #[cfg(feature = "alloc")] -use crate::primitives::decode::UncheckedHrpstringError; -#[cfg(feature = "alloc")] -use crate::primitives::decode::{ChecksumError, UncheckedHrpstring}; +use crate::primitives::decode::{ChecksumError, UncheckedHrpstring, UncheckedHrpstringError}; #[rustfmt::skip] // Keep public re-exports separate. #[doc(inline)] @@ -216,7 +223,7 @@ pub fn decode(s: &str) -> Result<(Hrp, Vec), DecodeError> { /// `Ck` algorithm (`NoChecksum` to exclude checksum all together). #[cfg(feature = "alloc")] #[inline] -pub fn encode(hrp: Hrp, data: &[u8]) -> Result { +pub fn encode(hrp: Hrp, data: &[u8]) -> Result { encode_lower::(hrp, data) } @@ -226,7 +233,7 @@ pub fn encode(hrp: Hrp, data: &[u8]) -> Result /// `Ck` algorithm (`NoChecksum` to exclude checksum all together). #[cfg(feature = "alloc")] #[inline] -pub fn encode_lower(hrp: Hrp, data: &[u8]) -> Result { +pub fn encode_lower(hrp: Hrp, data: &[u8]) -> Result { let mut buf = String::new(); encode_lower_to_fmt::(&mut buf, hrp, data)?; Ok(buf) @@ -238,7 +245,7 @@ pub fn encode_lower(hrp: Hrp, data: &[u8]) -> Result(hrp: Hrp, data: &[u8]) -> Result { +pub fn encode_upper(hrp: Hrp, data: &[u8]) -> Result { let mut buf = String::new(); encode_upper_to_fmt::(&mut buf, hrp, data)?; Ok(buf) @@ -253,7 +260,7 @@ pub fn encode_to_fmt( fmt: &mut W, hrp: Hrp, data: &[u8], -) -> Result<(), fmt::Error> { +) -> Result<(), EncodeError> { encode_lower_to_fmt::(fmt, hrp, data) } @@ -266,7 +273,9 @@ pub fn encode_lower_to_fmt( fmt: &mut W, hrp: Hrp, data: &[u8], -) -> Result<(), fmt::Error> { +) -> Result<(), EncodeError> { + let _ = encoded_length::(hrp, data)?; + let iter = data.iter().copied().bytes_to_fes(); let chars = iter.with_checksum::(&hrp).chars(); for c in chars { @@ -284,7 +293,9 @@ pub fn encode_upper_to_fmt( fmt: &mut W, hrp: Hrp, data: &[u8], -) -> Result<(), fmt::Error> { +) -> Result<(), EncodeError> { + let _ = encoded_length::(hrp, data)?; + let iter = data.iter().copied().bytes_to_fes(); let chars = iter.with_checksum::(&hrp).chars(); for c in chars { @@ -303,7 +314,7 @@ pub fn encode_to_writer( w: &mut W, hrp: Hrp, data: &[u8], -) -> Result<(), std::io::Error> { +) -> Result<(), EncodeIoError> { encode_lower_to_writer::(w, hrp, data) } @@ -317,7 +328,9 @@ pub fn encode_lower_to_writer( w: &mut W, hrp: Hrp, data: &[u8], -) -> Result<(), std::io::Error> { +) -> Result<(), EncodeIoError> { + let _ = encoded_length::(hrp, data)?; + let iter = data.iter().copied().bytes_to_fes(); let chars = iter.with_checksum::(&hrp).chars(); for c in chars { @@ -336,7 +349,9 @@ pub fn encode_upper_to_writer( w: &mut W, hrp: Hrp, data: &[u8], -) -> Result<(), std::io::Error> { +) -> Result<(), EncodeIoError> { + let _ = encoded_length::(hrp, data)?; + let iter = data.iter().copied().bytes_to_fes(); let chars = iter.with_checksum::(&hrp).chars(); for c in chars { @@ -345,10 +360,23 @@ pub fn encode_upper_to_writer( Ok(()) } -/// Returns the length of the bech32 string after encoding `hrp` and `data` (incl. checksum). -pub fn encoded_length(hrp: Hrp, data: &[u8]) -> usize { +/// Checks that encoding `hrp` and `data` creates a code that is less than the code length for `Ck`. +/// +/// The length of the code is how long a coded message can be (including the checksum!) for the code +/// to retain its error-correcting properties. +/// +/// # Returns +/// +/// `Ok(encoded_string_length)` if the encoded length is less than or equal to `Ck::CODE_LENGTH` +/// otherwise a [`CodeLengthError`] containing the encoded length and the maximum allowed. +pub fn encoded_length(hrp: Hrp, data: &[u8]) -> Result { let iter = data.iter().copied().bytes_to_fes(); - hrp.len() + 1 + iter.len() + Ck::CHECKSUM_LENGTH // +1 for separator + let len = hrp.len() + 1 + iter.len() + Ck::CHECKSUM_LENGTH; // +1 for separator + if len > Ck::CODE_LENGTH { + Err(CodeLengthError { encoded_length: len, code_length: Ck::CODE_LENGTH }) + } else { + Ok(len) + } } /// An error while decoding a bech32 string. @@ -392,6 +420,96 @@ impl From 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(CodeLengthError), + /// 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(ref e) => write_err!(f, "encode error"; e), + 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(ref e) => Some(e), + Fmt(ref e) => Some(e), + } + } +} + +impl From for EncodeError { + #[inline] + fn from(e: CodeLengthError) -> Self { Self::TooLong(e) } +} + +impl From 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(CodeLengthError), + /// 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(ref e) => write_err!(f, "encode error"; e), + 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(ref e) => Some(e), + Write(ref e) => Some(e), + } + } +} + +#[cfg(feature = "std")] +impl From for EncodeIoError { + #[inline] + fn from(e: CodeLengthError) -> Self { Self::TooLong(e) } +} + +#[cfg(feature = "std")] +impl From for EncodeIoError { + #[inline] + fn from(e: std::io::Error) -> Self { Self::Write(e) } +} + #[cfg(test)] #[cfg(feature = "alloc")] mod tests { @@ -489,8 +607,36 @@ mod tests { let encoded = encode::(hrp, &data).expect("valid data"); let want = encoded.len(); - let got = encoded_length::(hrp, &data); + let got = encoded_length::(hrp, &data).expect("encoded length"); 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::(hrp, &data).expect("valid data"); + 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::(hrp, &data) { + Ok(_) => panic!("false positive"), + Err(EncodeError::TooLong(CodeLengthError { encoded_length, code_length: _ })) => + assert_eq!(encoded_length, 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()); + } } diff --git a/src/primitives/checksum.rs b/src/primitives/checksum.rs index 3927ab08e..bcdb8d2b3 100644 --- a/src/primitives/checksum.rs +++ b/src/primitives/checksum.rs @@ -27,11 +27,16 @@ pub trait Checksum { /// be pretty efficient no matter what. type MidstateRepr: PackedFe32; + /// The length of the code. + /// + /// The length of the code is how long a coded message can be (including the + /// checksum!) for the code to retain its error-correcting properties. + const CODE_LENGTH: usize; + /// The number of characters in the checksum. /// /// Alternately, the degree of the generator polynomial. This is **not** the same - /// as the "length of the code", which is the maximum number of characters that - /// the checksum can usefully cover. + /// as `Self::CODE_LENGTH`. const CHECKSUM_LENGTH: usize; /// The coefficients of the generator polynomial, except the leading monic term, diff --git a/src/primitives/decode.rs b/src/primitives/decode.rs index 0870d3fa7..386078685 100644 --- a/src/primitives/decode.rs +++ b/src/primitives/decode.rs @@ -119,6 +119,8 @@ pub struct UncheckedHrpstring<'s> { /// /// Contains the checksum if one was present in the parsed string. data: &'s [u8], + /// The length of the parsed hrpstring. + hrpstring_length: usize, } impl<'s> UncheckedHrpstring<'s> { @@ -133,6 +135,7 @@ impl<'s> UncheckedHrpstring<'s> { let ret = UncheckedHrpstring { hrp: Hrp::parse(hrp)?, data: data[1..].as_bytes(), // Skip the separator. + hrpstring_length: s.len(), }; Ok(ret) @@ -168,6 +171,13 @@ impl<'s> UncheckedHrpstring<'s> { pub fn validate_checksum(&self) -> Result<(), ChecksumError> { use ChecksumError::*; + if self.hrpstring_length > Ck::CODE_LENGTH { + return Err(ChecksumError::CodeLength(CodeLengthError { + encoded_length: self.hrpstring_length, + code_length: Ck::CODE_LENGTH, + })); + } + if Ck::CHECKSUM_LENGTH == 0 { // Called with NoChecksum return Ok(()); @@ -205,7 +215,11 @@ impl<'s> UncheckedHrpstring<'s> { pub fn remove_checksum(self) -> CheckedHrpstring<'s> { let data_len = self.data.len() - Ck::CHECKSUM_LENGTH; - CheckedHrpstring { hrp: self.hrp(), data: &self.data[..data_len] } + CheckedHrpstring { + hrp: self.hrp(), + data: &self.data[..data_len], + hrpstring_length: self.hrpstring_length, + } } } @@ -239,6 +253,8 @@ pub struct CheckedHrpstring<'s> { /// This is ASCII byte values of the parsed string, guaranteed to be valid bech32 characters, /// with the checksum removed. data: &'s [u8], + /// The length of the parsed hrpstring. + hrpstring_length: usize, // Guaranteed to be <= CK::CODE_LENGTH } impl<'s> CheckedHrpstring<'s> { @@ -273,6 +289,11 @@ impl<'s> CheckedHrpstring<'s> { if self.data.is_empty() { return Err(SegwitHrpstringError::NoData); } + + if self.hrpstring_length > segwit::MAX_STRING_LENGTH { + return Err(SegwitHrpstringError::TooLong(self.hrpstring_length)); + } + // Unwrap ok since check_characters checked the bech32-ness of this char. let witness_version = Fe32::from_char(self.data[0].into()).unwrap(); self.data = &self.data[1..]; // Remove the witness version byte from data. @@ -331,8 +352,8 @@ impl<'s> CheckedHrpstring<'s> { } } -/// An HRP string that has been parsed, had the checksum validated, had the witness version -/// validated, had the witness data length checked, and the had witness version and checksum +/// An valid length HRP string that has been parsed, had the checksum validated, had the witness +/// version validated, had the witness data length checked, and the had witness version and checksum /// removed. /// /// # Examples @@ -368,6 +389,11 @@ impl<'s> SegwitHrpstring<'s> { /// to get strict BIP conformance (also [`Hrp::is_valid_on_mainnet`] and friends). #[inline] pub fn new(s: &'s str) -> Result { + let len = s.len(); + if len > segwit::MAX_STRING_LENGTH { + return Err(SegwitHrpstringError::TooLong(len)); + } + let unchecked = UncheckedHrpstring::new(s)?; if unchecked.data.is_empty() { @@ -551,6 +577,8 @@ pub enum SegwitHrpstringError { Unchecked(UncheckedHrpstringError), /// No data found after removing the checksum. NoData, + /// String exceeds maximum allowed length. + TooLong(usize), /// Invalid witness version (must be 0-16 inclusive). InvalidWitnessVersion(Fe32), /// Invalid padding on the witness data. @@ -568,6 +596,12 @@ impl fmt::Display for SegwitHrpstringError { match *self { Unchecked(ref e) => write_err!(f, "parsing unchecked hrpstring failed"; e), NoData => write!(f, "no data found after removing the checksum"), + TooLong(len) => write!( + f, + "encoded length {} exceeds spec limit {} chars", + len, + segwit::MAX_STRING_LENGTH + ), InvalidWitnessVersion(fe) => write!(f, "invalid segwit witness version: {}", fe), Padding(ref e) => write_err!(f, "invalid padding on the witness data"; e), WitnessLength(ref e) => write_err!(f, "invalid witness length"; e), @@ -586,7 +620,7 @@ impl std::error::Error for SegwitHrpstringError { Padding(ref e) => Some(e), WitnessLength(ref e) => Some(e), Checksum(ref e) => Some(e), - NoData | InvalidWitnessVersion(_) => None, + NoData | TooLong(_) | InvalidWitnessVersion(_) => None, } } } @@ -739,6 +773,8 @@ impl std::error::Error for CharError { #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum ChecksumError { + /// String exceeds maximum allowed length. + CodeLength(CodeLengthError), /// The checksum residue is not valid for the data. InvalidResidue, /// The checksummed string is not a valid length. @@ -750,6 +786,7 @@ impl fmt::Display for ChecksumError { use ChecksumError::*; match *self { + CodeLength(ref e) => write_err!(f, "string exceeds maximum allowed length"; e), InvalidResidue => write!(f, "the checksum residue is not valid for the data"), InvalidLength => write!(f, "the checksummed string is not a valid length"), } @@ -762,11 +799,62 @@ impl std::error::Error for ChecksumError { use ChecksumError::*; match *self { + CodeLength(ref e) => Some(e), InvalidResidue | InvalidLength => None, } } } +/// Encoding HRP and data into a bech32 string exceeds the checksum code length. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct CodeLengthError { + /// The length of the string if encoded with checksum. + pub encoded_length: usize, + /// The checksum specific code length. + pub code_length: usize, +} + +impl fmt::Display for CodeLengthError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "encoded length {} exceeds maximum (code length) {}", + self.encoded_length, self.code_length + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CodeLengthError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +/// Encoding HRP, witver, and program into an address exceeds maximum allowed. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct SegwitCodeLengthError(pub usize); + +impl fmt::Display for SegwitCodeLengthError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "encoded length {} exceeds maximum (code length) {}", + self.0, + segwit::MAX_STRING_LENGTH + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SegwitCodeLengthError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +impl From for SegwitCodeLengthError { + fn from(e: CodeLengthError) -> Self { Self(e.encoded_length) } +} + /// Error validating the padding bits on the witness data. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] diff --git a/src/primitives/mod.rs b/src/primitives/mod.rs index 1d496ba2b..26403abc0 100644 --- a/src/primitives/mod.rs +++ b/src/primitives/mod.rs @@ -28,6 +28,7 @@ pub enum Bech32m {} impl Checksum for NoChecksum { type MidstateRepr = PackedNull; + const CODE_LENGTH: usize = usize::MAX; const CHECKSUM_LENGTH: usize = 0; const GENERATOR_SH: [PackedNull; 5] = [PackedNull; 5]; const TARGET_RESIDUE: PackedNull = PackedNull; @@ -38,6 +39,7 @@ const GEN: [u32; 5] = [0x3b6a_57b2, 0x2650_8e6d, 0x1ea1_19fa, 0x3d42_33dd, 0x2a1 impl Checksum for Bech32 { type MidstateRepr = u32; + const CODE_LENGTH: usize = 1023; const CHECKSUM_LENGTH: usize = 6; const GENERATOR_SH: [u32; 5] = GEN; const TARGET_RESIDUE: u32 = 1; @@ -45,6 +47,7 @@ impl Checksum for Bech32 { // Same as Bech32 except TARGET_RESIDUE is different impl Checksum for Bech32m { type MidstateRepr = u32; + const CODE_LENGTH: usize = 1023; const CHECKSUM_LENGTH: usize = 6; const GENERATOR_SH: [u32; 5] = GEN; const TARGET_RESIDUE: u32 = 0x2bc830a3; diff --git a/src/primitives/segwit.rs b/src/primitives/segwit.rs index 52e41f8d9..4413a05bc 100644 --- a/src/primitives/segwit.rs +++ b/src/primitives/segwit.rs @@ -9,6 +9,14 @@ use core::fmt; use crate::primitives::gf32::Fe32; +/// The maximum enforced string length of a segwit address. +/// +/// The maximum length as specified in BIP-173, this is less than the 1023 character code length. +/// This limit is based on empirical error-correcting properties, see ["Checksum design"] section. +/// +/// ["Checksum design"]: +pub const MAX_STRING_LENGTH: usize = 90; + /// The field element representing segwit version 0. pub const VERSION_0: Fe32 = Fe32::Q; /// The field element representing segwit version 1 (taproot). diff --git a/src/segwit.rs b/src/segwit.rs index 7cfbe7cd3..16e6a8558 100644 --- a/src/segwit.rs +++ b/src/segwit.rs @@ -45,6 +45,7 @@ use alloc::{string::String, vec::Vec}; use core::fmt; use crate::error::write_err; +use crate::primitives::decode::SegwitCodeLengthError; #[cfg(feature = "alloc")] use crate::primitives::decode::{SegwitHrpstring, SegwitHrpstringError}; use crate::primitives::gf32::Fe32; @@ -52,7 +53,9 @@ use crate::primitives::hrp::Hrp; use crate::primitives::iter::{ByteIterExt, Fe32IterExt}; #[cfg(feature = "alloc")] use crate::primitives::segwit; -use crate::primitives::segwit::{InvalidWitnessVersionError, WitnessLengthError}; +use crate::primitives::segwit::{ + InvalidWitnessVersionError, WitnessLengthError, MAX_STRING_LENGTH, +}; use crate::primitives::{Bech32, Bech32m}; #[rustfmt::skip] // Keep public re-exports separate. @@ -79,7 +82,8 @@ pub fn decode(s: &str) -> Result<(Hrp, Fe32, Vec), DecodeError> { /// Encodes a segwit address. /// -/// Does validity checks on the `witness_version` and length checks on the `witness_program`. +/// Does validity checks on the `witness_version`, length checks on the `witness_program`, and +/// checks the total encoded string length. /// /// As specified by [`BIP-350`] we use the [`Bech32m`] checksum algorithm for witness versions 1 and /// above, and for witness version 0 we use the original ([`BIP-173`]) [`Bech32`] checksum @@ -101,12 +105,17 @@ pub fn encode( segwit::validate_witness_version(witness_version)?; segwit::validate_witness_program_length(witness_program.len(), witness_version)?; + let _ = encoded_length(hrp, witness_version, witness_program)?; + let mut buf = String::new(); encode_to_fmt_unchecked(&mut buf, hrp, witness_version, witness_program)?; Ok(buf) } /// Encodes a segwit version 0 address. +/// +/// Does validity checks on the `witness_version`, length checks on the `witness_program`, and +/// checks the total encoded string length. #[cfg(feature = "alloc")] #[inline] pub fn encode_v0(hrp: Hrp, witness_program: &[u8]) -> Result { @@ -114,6 +123,9 @@ pub fn encode_v0(hrp: Hrp, witness_program: &[u8]) -> Result Result { @@ -122,8 +134,8 @@ pub fn encode_v1(hrp: Hrp, witness_program: &[u8]) -> Result( fmt: &mut W, @@ -136,8 +148,8 @@ pub fn encode_to_fmt_unchecked( /// Encodes a segwit address to a writer ([`fmt::Write`]) using lowercase characters. /// -/// Does not check the validity of the witness version and witness program lengths (see -/// the [`crate::primitives::segwit`] module for validation functions). +/// There are no guarantees that the written string is a valid segwit address unless all the +/// parameters are valid, see the body of `encode()` to see the validity checks required. pub fn encode_lower_to_fmt_unchecked( fmt: &mut W, hrp: Hrp, @@ -164,8 +176,8 @@ pub fn encode_lower_to_fmt_unchecked( /// /// This is provided for use when creating QR codes. /// -/// Does not check the validity of the witness version and witness program lengths (see -/// the [`crate::primitives::segwit`] module for validation functions). +/// There are no guarantees that the written string is a valid segwit address unless all the +/// parameters are valid, see the body of `encode()` to see the validity checks required. #[inline] pub fn encode_upper_to_fmt_unchecked( fmt: &mut W, @@ -192,8 +204,8 @@ pub fn encode_upper_to_fmt_unchecked( /// Encodes a segwit address to a writer ([`std::io::Write`]) using lowercase characters. /// -/// Does not check the validity of the witness version and witness program lengths (see -/// the [`crate::primitives::segwit`] module for validation functions). +/// There are no guarantees that the written string is a valid segwit address unless all the +/// parameters are valid, see the body of `encode()` to see the validity checks required. #[cfg(feature = "std")] #[inline] pub fn encode_to_writer_unchecked( @@ -207,8 +219,8 @@ pub fn encode_to_writer_unchecked( /// Encodes a segwit address to a writer ([`std::io::Write`]) using lowercase characters. /// -/// Does not check the validity of the witness version and witness program lengths (see -/// the [`crate::primitives::segwit`] module for validation functions). +/// There are no guarantees that the written string is a valid segwit address unless all the +/// parameters are valid, see the body of `encode()` to see the validity checks required. #[cfg(feature = "std")] #[inline] pub fn encode_lower_to_writer_unchecked( @@ -237,8 +249,8 @@ pub fn encode_lower_to_writer_unchecked( /// /// This is provided for use when creating QR codes. /// -/// Does not check the validity of the witness version and witness program lengths (see -/// the [`crate::primitives::segwit`] module for validation functions). +/// There are no guarantees that the written string is a valid segwit address unless all the +/// parameters are valid, see the body of `encode()` to see the validity checks required. #[cfg(feature = "std")] #[inline] pub fn encode_upper_to_writer_unchecked( @@ -264,14 +276,25 @@ pub fn encode_upper_to_writer_unchecked( Ok(()) } -/// Returns the length of the bech32 string after encoding HRP, witness version and program. +/// Returns the length of the address after encoding HRP, witness version and program. +/// +/// # Returns +/// +/// `Ok(address_length)` if the encoded address length is less than or equal to 90. Otherwise +/// returns a [`SegwitCodeLengthError`] containing the encoded address length. pub fn encoded_length( hrp: Hrp, _witness_version: Fe32, // Emphasize that this is only for segwit. witness_program: &[u8], -) -> usize { +) -> Result { // Ck is only for length and since they are both the same we can use either here. - crate::encoded_length::(hrp, witness_program) + 1 // +1 for witness version. + let len = crate::encoded_length::(hrp, witness_program).map(|len| len + 1)?; // +1 for witness version. + + if len > MAX_STRING_LENGTH { + Err(SegwitCodeLengthError(len)) + } else { + Ok(len) + } } /// An error while decoding a segwit address. @@ -306,6 +329,8 @@ pub enum EncodeError { WitnessVersion(InvalidWitnessVersionError), /// Invalid witness length. WitnessLength(WitnessLengthError), + /// Encoding HRP, witver, and program into a bech32 string exceeds maximum allowed. + TooLong(SegwitCodeLengthError), /// Writing to formatter failed. Fmt(fmt::Error), } @@ -317,6 +342,7 @@ impl fmt::Display for EncodeError { match *self { WitnessVersion(ref e) => write_err!(f, "witness version"; e), WitnessLength(ref e) => write_err!(f, "witness length"; e), + TooLong(ref e) => write_err!(f, "encode error"; e), Fmt(ref e) => write_err!(f, "writing to formatter failed"; e), } } @@ -330,6 +356,7 @@ impl std::error::Error for EncodeError { match *self { WitnessVersion(ref e) => Some(e), WitnessLength(ref e) => Some(e), + TooLong(ref e) => Some(e), Fmt(ref e) => Some(e), } } @@ -345,6 +372,11 @@ impl From for EncodeError { fn from(e: WitnessLengthError) -> Self { Self::WitnessLength(e) } } +impl From for EncodeError { + #[inline] + fn from(e: SegwitCodeLengthError) -> Self { Self::TooLong(e) } +} + impl From for EncodeError { #[inline] fn from(e: fmt::Error) -> Self { Self::Fmt(e) } @@ -353,6 +385,7 @@ impl From for EncodeError { #[cfg(all(test, feature = "alloc"))] mod tests { use super::*; + use crate::primitives::decode::{SegwitCodeLengthError, SegwitHrpstringError}; use crate::primitives::hrp; #[test] @@ -453,9 +486,48 @@ mod tests { let encoded = encode(hrp, version, &program).expect("valid data"); let want = encoded.len(); - let got = encoded_length(hrp, version, &program); + let got = encoded_length(hrp, version, &program).expect("encoded length"); assert_eq!(got, want); } } + + #[test] + fn can_encode_maximum_length_address() { + let program = [0_u8; 40]; // Maximum witness program length. + let hrp = Hrp::parse_unchecked("anhrpthatis18chars"); + let addr = encode(hrp, VERSION_1, &program).expect("valid data"); + assert_eq!(addr.len(), MAX_STRING_LENGTH); + } + + #[test] + fn can_not_encode_address_too_long() { + let tcs = vec![ + ("anhrpthatis19charsx", 91), + ("anhrpthatisthemaximumallowedlengthofeightythreebytesxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 155) + ]; + + for (hrp, len) in tcs { + let program = [0_u8; 40]; // Maximum witness program length. + let hrp = Hrp::parse_unchecked(hrp); + let err = encode(hrp, VERSION_1, &program).unwrap_err(); + assert_eq!(err, EncodeError::TooLong(SegwitCodeLengthError(len))); + } + } + + #[test] + fn can_decode_maximum_length_address() { + let address = "anhrpthatisnineteen1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqghfyyfz"; + assert_eq!(address.len(), MAX_STRING_LENGTH); + + assert!(decode(address).is_ok()); + } + + #[test] + fn can_not_decode_address_too_long() { + let address = "anhrpthatistwentycha1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgqfrwjz"; + assert_eq!(address.len(), MAX_STRING_LENGTH + 1); + + assert_eq!(decode(address).unwrap_err(), DecodeError(SegwitHrpstringError::TooLong(91))); + } }