diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..8f584268 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,65 @@ +on: [pull_request] + +name: Continuous Integration + +jobs: + test: + name: Test Suite + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.29.0 + - stable + - nightly + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose --features strict + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 16446d22..00000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: rust -rust: - - stable - - nightly - - 1.22.0 # project wide min version -cache: cargo - -script: - - cargo build --verbose --features strict - - cargo test --verbose --features strict - -jobs: - include: - - stage: best practices - rust: stable - install: - - rustup component add rustfmt - - rustup component add clippy - script: - - rustfmt --check src/lib.rs - - cargo clippy -- -D warnings - - stage: fuzz - before_install: - - sudo apt-get -qq update - - sudo apt-get install -y binutils-dev libunwind8-dev - rust: stable - script: cd fuzz && cargo update && cargo test --verbose && ./travis-fuzz.sh diff --git a/Cargo.toml b/Cargo.toml index 8aaaa644..a8838366 100644 --- a/Cargo.toml +++ b/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" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..58490070 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.29.0" diff --git a/fuzz/fuzz_targets/decode_rnd.rs b/fuzz/fuzz_targets/decode_rnd.rs index cc8eea3b..4119d522 100644 --- a/fuzz/fuzz_targets/decode_rnd.rs +++ b/fuzz/fuzz_targets/decode_rnd.rs @@ -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")] @@ -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 { diff --git a/fuzz/fuzz_targets/encode_decode.rs b/fuzz/fuzz_targets/encode_decode.rs index cce3e7b6..bcc587db 100644 --- a/fuzz/fuzz_targets/encode_decode.rs +++ b/fuzz/fuzz_targets/encode_decode.rs @@ -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::>(); - 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); } } @@ -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 { diff --git a/src/lib.rs b/src/lib.rs index 578ab0aa..715d242e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::::from_base32(&data).unwrap(), vec![0x00, 0x01, 0x02]); +//! assert_eq!(variant, Variant::Bech32); //! ``` //! @@ -118,6 +121,7 @@ pub trait WriteBase32 { pub struct Bech32Writer<'a> { formatter: &'a mut fmt::Write, chk: u32, + variant: Variant, } impl<'a> Bech32Writer<'a> { @@ -125,10 +129,15 @@ impl<'a> Bech32Writer<'a> { /// /// 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, fmt::Error> { + pub fn new( + hrp: &str, + variant: Variant, + fmt: &'a mut fmt::Write, + ) -> Result, fmt::Error> { let mut writer = Bech32Writer { formatter: fmt, chk: 1, + variant, }; writer.formatter.write_str(hrp)?; @@ -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 @@ -383,13 +392,14 @@ pub fn encode_to_fmt>( fmt: &mut fmt::Write, hrp: &str, data: T, + variant: Variant, ) -> Result { 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 @@ -400,22 +410,52 @@ pub fn encode_to_fmt>( } } +/// Used for encode/decode operations for the two variants of Bech32 +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum Variant { + /// The original Bech32 described in [BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) + Bech32, + /// 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 { + 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>(hrp: &str, data: T) -> Result { +pub fn encode>(hrp: &str, data: T, variant: Variant) -> Result { 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), Error> { +pub fn decode(s: &str) -> Result<(String, Vec, Variant), Error> { // Ensure overall length is within bounds if s.len() < 8 { return Err(Error::InvalidLength); @@ -477,21 +517,22 @@ pub fn decode(s: &str) -> Result<(String, Vec), Error> { .collect::, 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 { 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 { @@ -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()); } } @@ -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 - ); } } @@ -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) ); } @@ -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); } @@ -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); } @@ -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"); }