Skip to content

Commit

Permalink
Support bech32 encoding without a checksum
Browse files Browse the repository at this point in the history
BOLT 12 Offers uses bech32 encoding without a checksum since QR codes
already have a checksum. Add functions encode_without_checksum and
decode_without_checksum to support this use case.

Also, remove overall length check in decode since it is unnecessary.
  • Loading branch information
jkczyz committed Aug 6, 2022
1 parent bebd458 commit 9dc60b4
Showing 1 changed file with 84 additions and 23 deletions.
107 changes: 84 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,12 @@ impl<'a> Bech32Writer<'a> {

/// Write out the checksum at the end. If this method isn't called this will happen on drop.
pub fn finalize(mut self) -> fmt::Result {
self.inner_finalize()?;
self.write_checksum()?;
mem::forget(self);
Ok(())
}

fn inner_finalize(&mut self) -> fmt::Result {
fn write_checksum(&mut self) -> fmt::Result {
// Pad with 6 zeros
for _ in 0..CHECKSUM_LENGTH {
self.polymod_step(u5(0))
Expand All @@ -203,6 +203,7 @@ impl<'a> Bech32Writer<'a> {
Ok(())
}
}

impl<'a> WriteBase32 for Bech32Writer<'a> {
type Err = fmt::Error;

Expand All @@ -215,7 +216,7 @@ impl<'a> WriteBase32 for Bech32Writer<'a> {

impl<'a> Drop for Bech32Writer<'a> {
fn drop(&mut self) {
self.inner_finalize()
self.write_checksum()
.expect("Unhandled error writing the checksum on drop.")
}
}
Expand Down Expand Up @@ -421,6 +422,32 @@ pub fn encode_to_fmt<T: AsRef<[u5]>>(
})?)
}

/// Encode a bech32 payload without a checksum to an [fmt::Write].
/// This method is intended for implementing traits from [std::fmt].
///
/// # Errors
/// * If [check_hrp] returns an error for the given HRP.
/// * If `fmt` fails on write
/// # Deviations from standard
/// * No length limits are enforced for the data part
pub fn encode_without_checksum_to_fmt<T: AsRef<[u5]>>(
fmt: &mut fmt::Write,
hrp: &str,
data: T,
) -> Result<(), Error> {
let hrp = match check_hrp(hrp)? {
Case::Upper => Cow::Owned(hrp.to_lowercase()),
Case::Lower | Case::None => Cow::Borrowed(hrp),
};

fmt.write_str(&hrp)?;
fmt.write_char(SEP)?;
for b in data.as_ref() {
fmt.write_char(b.to_char())?;
}
Ok(())
}

/// Used for encode/decode operations for the two variants of Bech32
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Variant {
Expand Down Expand Up @@ -463,15 +490,44 @@ pub fn encode<T: AsRef<[u5]>>(hrp: &str, data: T, variant: Variant) -> Result<St
Ok(buf)
}

/// Encode a bech32 payload to string without the checksum.
///
/// # Errors
/// * If [check_hrp] returns an error for the given HRP.
/// # Deviations from standard
/// * No length limits are enforced for the data part
pub fn encode_without_checksum<T: AsRef<[u5]>>(hrp: &str, data: T) -> Result<String, Error> {
let mut buf = String::new();
encode_without_checksum_to_fmt(&mut buf, hrp, data)?;
Ok(buf)
}

/// Decode a bech32 string into the raw HRP and the data bytes.
///
/// Returns the HRP in lowercase..
/// Returns the HRP in lowercase, the data with the checksum removed, and the encoding.
pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
// Ensure overall length is within bounds
if s.len() < CHECKSUM_LENGTH + 2 {
let (hrp_lower, mut data) = decode_without_checksum(s)?;
if data.len() < CHECKSUM_LENGTH {
return Err(Error::InvalidLength);
}

// Ensure checksum
match verify_checksum(hrp_lower.as_bytes(), &data) {
Some(variant) => {
// Remove checksum from data payload
let dbl: usize = data.len();
data.truncate(dbl - CHECKSUM_LENGTH);

Ok((hrp_lower, data, variant))
}
None => Err(Error::InvalidChecksum),
}
}

/// Decode a bech32 string into the raw HRP and the data bytes, assuming no checksum.
///
/// Returns the HRP in lowercase and the data with the checksum removed.
pub fn decode_without_checksum(s: &str) -> Result<(String, Vec<u5>), Error> {
// Split at separator and check for two pieces
let (raw_hrp, raw_data) = match s.rfind(SEP) {
None => return Err(Error::MissingSeparator),
Expand All @@ -480,9 +536,6 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
(hrp, &data[1..])
}
};
if raw_data.len() < CHECKSUM_LENGTH {
return Err(Error::InvalidLength);
}

let mut case = check_hrp(raw_hrp)?;
let hrp_lower = match case {
Expand All @@ -492,7 +545,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
};

// Check data payload
let mut data = raw_data
let data = raw_data
.chars()
.map(|c| {
// Only check if c is in the ASCII range, all invalid ASCII
Expand Down Expand Up @@ -527,17 +580,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
})
.collect::<Result<Vec<u5>, Error>>()?;

// Ensure checksum
match verify_checksum(hrp_lower.as_bytes(), &data) {
Some(variant) => {
// Remove checksum from data payload
let dbl: usize = data.len();
data.truncate(dbl - CHECKSUM_LENGTH);

Ok((hrp_lower, data, variant))
}
None => Err(Error::InvalidChecksum),
}
Ok((hrp_lower, data))
}

fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option<Variant> {
Expand Down Expand Up @@ -801,6 +844,8 @@ mod tests {
Error::InvalidLength),
("1p2gdwpf",
Error::InvalidLength),
("bc1p2",
Error::InvalidLength),
);
for p in pairs {
let (s, expected_error) = p;
Expand Down Expand Up @@ -930,7 +975,7 @@ mod tests {
}

#[test]
fn writer() {
fn write_with_checksum() {
let hrp = "lnbc";
let data = "Hello World!".as_bytes().to_base32();

Expand All @@ -947,7 +992,23 @@ mod tests {
}

#[test]
fn write_on_drop() {
fn write_without_checksum() {
let hrp = "lnbc";
let data = "Hello World!".as_bytes().to_base32();

let mut written_str = String::new();
{
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, &mut written_str).unwrap();
writer.write(&data).unwrap();
}

let encoded_str = encode_without_checksum(hrp, data).unwrap();

assert_eq!(encoded_str, written_str[..written_str.len() - CHECKSUM_LENGTH]);
}

#[test]
fn write_with_checksum_on_drop() {
let hrp = "lntb";
let data = "Hello World!".as_bytes().to_base32();

Expand Down

0 comments on commit 9dc60b4

Please sign in to comment.