Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bech32m Support #50

Merged
merged 5 commits into from Feb 14, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "bech32"
version = "0.7.3"
version = "0.8.0"
authors = ["Clark Moody"]
repository = "https://github.com/rust-bitcoin/rust-bech32"
description = "Encodes and decodes the Bech32 format"
Expand Down
5 changes: 3 additions & 2 deletions fuzz/fuzz_targets/decode_rnd.rs
Expand Up @@ -10,7 +10,7 @@ fn do_test(data: &[u8]) {
Err(_) => return,
};

assert_eq!(bech32::encode(&b32.0, b32.1).unwrap(), data_str);
assert_eq!(bech32::encode(&b32.0, b32.1, b32.2).unwrap(), data_str);
}

#[cfg(feature = "afl")]
Expand All @@ -23,7 +23,8 @@ fn main() {
}

#[cfg(feature = "honggfuzz")]
#[macro_use] extern crate honggfuzz;
#[macro_use]
extern crate honggfuzz;
#[cfg(feature = "honggfuzz")]
fn main() {
loop {
Expand Down
17 changes: 13 additions & 4 deletions fuzz/fuzz_targets/encode_decode.rs
Expand Up @@ -13,18 +13,26 @@ fn do_test(data: &[u8]) {
return;
}

let hrp = String::from_utf8_lossy(&data[1..hrp_end]).to_lowercase().to_string();
let hrp = String::from_utf8_lossy(&data[1..hrp_end])
.to_lowercase()
.to_string();

let dp = data[hrp_end..]
.iter()
.map(|b| bech32::u5::try_from_u8(b % 32).unwrap())
.collect::<Vec<_>>();

if let Ok(data_str) = bech32::encode(&hrp, &dp).map(|b32| b32.to_string()) {
let variant = if data[0] > 0x0f {
bech32::Variant::Bech32m
} else {
bech32::Variant::Bech32
};

if let Ok(data_str) = bech32::encode(&hrp, &dp, variant).map(|b32| b32.to_string()) {
let decoded = bech32::decode(&data_str);
let b32 = decoded.expect("should be able to decode own encoding");

assert_eq!(bech32::encode(&b32.0, &b32.1).unwrap(), data_str);
assert_eq!(bech32::encode(&b32.0, &b32.1, b32.2).unwrap(), data_str);
}
}

Expand All @@ -38,7 +46,8 @@ fn main() {
}

#[cfg(feature = "honggfuzz")]
#[macro_use] extern crate honggfuzz;
#[macro_use]
extern crate honggfuzz;
#[cfg(feature = "honggfuzz")]
fn main() {
loop {
Expand Down
166 changes: 117 additions & 49 deletions src/lib.rs
Expand Up @@ -22,22 +22,25 @@
//!
//! Bech32 is an encoding scheme that is easy to use for humans and efficient to encode in QR codes.
//!
//! A Bech32 string consists of a human-readable part (HRP), a separator (the character `'1'`), and 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.
//! A Bech32 string consists of a human-readable part (HRP), a separator (the character `'1'`), and
//! 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.
//!
//! The original description in [BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) has more details.
//! The original description in [BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)
//! has more details.
//!
//! # Examples
//!
//! ```
//! use bech32::{self, FromBase32, ToBase32};
//! use bech32::{self, FromBase32, ToBase32, Variant};
//!
//! let encoded = bech32::encode("bech32", vec![0x00, 0x01, 0x02].to_base32()).unwrap();
//! let encoded = bech32::encode("bech32", vec![0x00, 0x01, 0x02].to_base32(), Variant::Bech32).unwrap();
//! assert_eq!(encoded, "bech321qqqsyrhqy2a".to_string());
//!
//! let (hrp, data) = bech32::decode(&encoded).unwrap();
//! let (hrp, data, variant) = bech32::decode(&encoded).unwrap();
//! assert_eq!(hrp, "bech32");
//! assert_eq!(Vec::<u8>::from_base32(&data).unwrap(), vec![0x00, 0x01, 0x02]);
//! assert_eq!(variant, Variant::Bech32);
//! ```
//!

Expand Down Expand Up @@ -118,17 +121,23 @@ pub trait WriteBase32 {
pub struct Bech32Writer<'a> {
formatter: &'a mut fmt::Write,
chk: u32,
variant: Variant,
}

impl<'a> Bech32Writer<'a> {
/// Creates a new writer that can write a bech32 string without allocating itself.
///
/// This is a rather low-level API and doesn't check the HRP or data length for standard
/// compliance.
pub fn new(hrp: &str, fmt: &'a mut fmt::Write) -> Result<Bech32Writer<'a>, fmt::Error> {
pub fn new(
hrp: &str,
variant: Variant,
fmt: &'a mut fmt::Write,
) -> Result<Bech32Writer<'a>, fmt::Error> {
let mut writer = Bech32Writer {
formatter: fmt,
chk: 1,
variant,
};

writer.formatter.write_str(hrp)?;
Expand Down Expand Up @@ -170,7 +179,7 @@ impl<'a> Bech32Writer<'a> {
self.polymod_step(u5(0))
}

let plm: u32 = self.chk ^ 1;
let plm: u32 = self.chk ^ self.variant.constant();

for p in 0..6 {
self.formatter
Expand Down Expand Up @@ -383,13 +392,14 @@ pub fn encode_to_fmt<T: AsRef<[u5]>>(
fmt: &mut fmt::Write,
hrp: &str,
data: T,
variant: Variant,
) -> Result<fmt::Result, Error> {
let hrp_lower = match check_hrp(&hrp)? {
Case::Upper => Cow::Owned(hrp.to_lowercase()),
Case::Lower | Case::None => Cow::Borrowed(hrp),
};

match Bech32Writer::new(&hrp_lower, fmt) {
match Bech32Writer::new(&hrp_lower, variant, fmt) {
Ok(mut writer) => {
Ok(writer.write(data.as_ref()).and_then(|_| {
// Finalize manually to avoid panic on drop if write fails
Expand All @@ -400,22 +410,52 @@ pub fn encode_to_fmt<T: AsRef<[u5]>>(
}
}

/// Used for encode/decode operations for the two variants of Bech32
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Variant {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest also to add repr(u32) here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do for us?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's necessary to implement the Variant::Bech32 as u32 API afaik (making the enum castable to its internal representation). I don't really like it because it isn't too well-known this is possible and also seems a bit like abusing this feature here for probably non-existent performance gains.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I'll leave this out for now, and we can circle back later if the need arises.

/// The original Bech32 described in [BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)
Bech32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pls have the actual constants assigned as the values here? When we will not need BECH32_CONST, BECH32M_CONST, and can simply use Variant::Bech32 as u32 instead of constant() function call (and avoid one call)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with @clarkmoody's version, it's more "standard" imo, do you feel strongly about this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not exactly sure what you mean @dr-orlovsky. Could you post a code example?

Performance-wise, the compiler will inline these simple methods for sure.

/// The improved Bech32m variant described in [BIP-0350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)
Bech32m,
}

const BECH32_CONST: u32 = 1;
const BECH32M_CONST: u32 = 0x2bc830a3;

impl Variant {
// Produce the variant based on the remainder of the polymod operation
fn from_remainder(c: u32) -> Option<Self> {
match c {
BECH32_CONST => Some(Variant::Bech32),
BECH32M_CONST => Some(Variant::Bech32m),
_ => None,
}
}

fn constant(self) -> u32 {
match self {
Variant::Bech32 => BECH32_CONST,
Variant::Bech32m => BECH32M_CONST,
}
}
}

/// Encode a bech32 payload to string.
///
/// # 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<T: AsRef<[u5]>>(hrp: &str, data: T) -> Result<String, Error> {
pub fn encode<T: AsRef<[u5]>>(hrp: &str, data: T, variant: Variant) -> Result<String, Error> {
let mut buf = String::new();
encode_to_fmt(&mut buf, hrp, data)?.unwrap();
encode_to_fmt(&mut buf, hrp, data, variant)?.unwrap();
Ok(buf)
}

/// Decode a bech32 string into the raw HRP and the data bytes.
///
/// Returns the HRP in lowercase..
pub fn decode(s: &str) -> Result<(String, Vec<u5>), Error> {
pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
// Ensure overall length is within bounds
if s.len() < 8 {
return Err(Error::InvalidLength);
Expand Down Expand Up @@ -477,21 +517,22 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>), Error> {
.collect::<Result<Vec<u5>, Error>>()?;

// Ensure checksum
if !verify_checksum(&hrp_lower.as_bytes(), &data) {
return Err(Error::InvalidChecksum);
}
match verify_checksum(&hrp_lower.as_bytes(), &data) {
Some(variant) => {
// Remove checksum from data payload
let dbl: usize = data.len();
data.truncate(dbl - 6);

// Remove checksum from data payload
let dbl: usize = data.len();
data.truncate(dbl - 6);

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

fn verify_checksum(hrp: &[u8], data: &[u5]) -> bool {
fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option<Variant> {
let mut exp = hrp_expand(hrp);
exp.extend_from_slice(data);
polymod(&exp) == 1u32
Variant::from_remainder(polymod(&exp))
}

fn hrp_expand(hrp: &[u8]) -> Vec<u5> {
Expand Down Expand Up @@ -665,25 +706,29 @@ mod tests {
#[test]
fn valid_checksum() {
let strings: Vec<&str> = vec!(
// Bech32
"A12UEL5L",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j",
"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
// Bech32m
"A1LQFN3A",
"a1lqfn3a",
"an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
"abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
"11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8",
"split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
"?1v759aa",
);
for s in strings {
let decode_result = decode(s);
if !decode_result.is_ok() {
panic!(
"Did not decode: {:?} Reason: {:?}",
s,
decode_result.unwrap_err()
);
match decode(s) {
Ok((hrp, payload, variant)) => {
let encoded = encode(&hrp, payload, variant).unwrap();
assert_eq!(s.to_lowercase(), encoded.to_lowercase());
}
Err(e) => panic!("Did not decode: {:?} Reason: {:?}", s, e),
}
assert!(decode_result.is_ok());
let decoded = decode_result.unwrap();
let encode_result = encode(&decoded.0, decoded.1).unwrap();
assert_eq!(s.to_lowercase(), encode_result.to_lowercase());
}
}

Expand All @@ -708,20 +753,39 @@ mod tests {
Error::InvalidLength),
("de1lg7wt\u{ff}",
Error::InvalidChar('\u{ff}')),
("\u{20}1xj0phk",
Error::InvalidChar('\u{20}')),
("\u{7F}1g6xzxy",
Error::InvalidChar('\u{7F}')),
("an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4",
Error::InvalidLength),
("qyrz8wqd2c9m",
Error::MissingSeparator),
("1qyrz8wqd2c9m",
Error::InvalidLength),
("y1b0jsk6g",
Error::InvalidChar('b')),
("lt1igcx5c0",
Error::InvalidChar('i')),
("in1muywd",
Error::InvalidLength),
("mm1crxm3i",
Error::InvalidChar('i')),
("au1s5cgom",
Error::InvalidChar('o')),
("M1VUXWEZ",
Error::InvalidChecksum),
("16plkw9",
Error::InvalidLength),
("1p2gdwpf",
Error::InvalidLength),
);
for p in pairs {
let (s, expected_error) = p;
let dec_result = decode(s);
if dec_result.is_ok() {
println!("{:?}", dec_result.unwrap());
panic!("Should be invalid: {:?}", s);
match decode(s) {
Ok(_) => panic!("Should be invalid: {:?}", s),
Err(e) => assert_eq!(e, expected_error, "testing input '{}'", s),
}
assert_eq!(
dec_result.unwrap_err(),
expected_error,
"testing input '{}'",
s
);
}
}

Expand Down Expand Up @@ -799,7 +863,11 @@ mod tests {
#[test]
fn test_encode() {
assert_eq!(
encode("", vec![1u8, 2, 3, 4].check_base32().unwrap()),
encode(
"",
vec![1u8, 2, 3, 4].check_base32().unwrap(),
Variant::Bech32
),
Err(Error::InvalidLength)
);
}
Expand Down Expand Up @@ -849,12 +917,12 @@ mod tests {

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

let encoded_str = encode(hrp, data).unwrap();
let encoded_str = encode(hrp, data, Variant::Bech32).unwrap();

assert_eq!(encoded_str, written_str);
}
Expand All @@ -866,11 +934,11 @@ mod tests {

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

let encoded_str = encode(hrp, data).unwrap();
let encoded_str = encode(hrp, data, Variant::Bech32).unwrap();

assert_eq!(encoded_str, written_str);
}
Expand All @@ -879,7 +947,7 @@ mod tests {
fn test_hrp_case() {
// Tests for issue with HRP case checking being ignored for encoding
use ToBase32;
let encoded_str = encode("HRP", [0x00, 0x00].to_base32()).unwrap();
let encoded_str = encode("HRP", [0x00, 0x00].to_base32(), Variant::Bech32).unwrap();

assert_eq!(encoded_str, "hrp1qqqq40atq3");
}
Expand Down