diff --git a/fuzz/src/base32.rs b/fuzz/src/base32.rs new file mode 100644 index 00000000000..8171f19f637 --- /dev/null +++ b/fuzz/src/base32.rs @@ -0,0 +1,52 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use lightning::util::base32; + +use crate::utils::test_logger; + +#[inline] +pub fn do_test(data: &[u8]) { + if let Ok(s) = std::str::from_utf8(data) { + let first_decoding = base32::Alphabet::RFC4648 { padding: true }.decode(s); + if let Ok(first_decoding) = first_decoding { + let encoding_response = base32::Alphabet::RFC4648 { padding: true }.encode(&first_decoding); + assert_eq!(encoding_response, s.to_ascii_uppercase()); + let second_decoding = base32::Alphabet::RFC4648 { padding: true }.decode(&encoding_response).unwrap(); + assert_eq!(first_decoding, second_decoding); + } + } + + if let Ok(s) = std::str::from_utf8(data) { + let first_decoding = base32::Alphabet::RFC4648 { padding: false }.decode(s); + if let Ok(first_decoding) = first_decoding { + let encoding_response = base32::Alphabet::RFC4648 { padding: false }.encode(&first_decoding); + assert_eq!(encoding_response, s.to_ascii_uppercase()); + let second_decoding = base32::Alphabet::RFC4648 { padding: false }.decode(&encoding_response).unwrap(); + assert_eq!(first_decoding, second_decoding); + } + } + + let encode_response = base32::Alphabet::RFC4648 { padding: false }.encode(&data); + let decode_response = base32::Alphabet::RFC4648 { padding: false }.decode(&encode_response).unwrap(); + assert_eq!(data, decode_response); + + let encode_response = base32::Alphabet::RFC4648 { padding: true }.encode(&data); + let decode_response = base32::Alphabet::RFC4648 { padding: true }.decode(&encode_response).unwrap(); + assert_eq!(data, decode_response); +} + +pub fn base32_test(data: &[u8], _out: Out) { + do_test(data); +} + +#[no_mangle] +pub extern "C" fn base32_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }); +} diff --git a/fuzz/src/bin/base32_target.rs b/fuzz/src/bin/base32_target.rs new file mode 100644 index 00000000000..a7951c77004 --- /dev/null +++ b/fuzz/src/bin/base32_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::base32::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + base32_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + base32_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + base32_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + base32_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + base32_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/base32") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + base32_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/bin/fromstr_to_netaddress_target.rs b/fuzz/src/bin/fromstr_to_netaddress_target.rs new file mode 100644 index 00000000000..29c984e6016 --- /dev/null +++ b/fuzz/src/bin/fromstr_to_netaddress_target.rs @@ -0,0 +1,113 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::fromstr_to_netaddress::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + fromstr_to_netaddress_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + fromstr_to_netaddress_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + fromstr_to_netaddress_run(data.as_ptr(), data.len()); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + fromstr_to_netaddress_run(data.as_ptr(), data.len()); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + fromstr_to_netaddress_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/fromstr_to_netaddress") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + fromstr_to_netaddress_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index fe17e4bab8f..2fa7debdf46 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -21,6 +21,8 @@ GEN_TEST router GEN_TEST zbase32 GEN_TEST indexedmap GEN_TEST onion_hop_data +GEN_TEST base32 +GEN_TEST fromstr_to_netaddress GEN_TEST msg_accept_channel msg_targets:: GEN_TEST msg_announcement_signatures msg_targets:: diff --git a/fuzz/src/fromstr_to_netaddress.rs b/fuzz/src/fromstr_to_netaddress.rs new file mode 100644 index 00000000000..19984112369 --- /dev/null +++ b/fuzz/src/fromstr_to_netaddress.rs @@ -0,0 +1,31 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use lightning::ln::msgs::NetAddress; +use core::str::FromStr; + +use crate::utils::test_logger; + +#[inline] +pub fn do_test(data: &[u8]) { + if let Ok(s) = std::str::from_utf8(data) { + let _ = NetAddress::from_str(s); + } + +} + +pub fn fromstr_to_netaddress_test(data: &[u8], _out: Out) { + do_test(data); +} + +#[no_mangle] +pub extern "C" fn fromstr_to_netaddress_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }); +} + diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 6cdeb8ab5d2..5b5cd69cf96 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -29,5 +29,7 @@ pub mod refund_deser; pub mod router; pub mod zbase32; pub mod onion_hop_data; +pub mod base32; +pub mod fromstr_to_netaddress; pub mod msg_targets; diff --git a/fuzz/src/zbase32.rs b/fuzz/src/zbase32.rs index 5ea453bfb86..04979204e98 100644 --- a/fuzz/src/zbase32.rs +++ b/fuzz/src/zbase32.rs @@ -7,18 +7,19 @@ // You may not use this file except in accordance with one or both of these // licenses. -use lightning::util::zbase32; +use lightning::util::base32; use crate::utils::test_logger; #[inline] pub fn do_test(data: &[u8]) { - let res = zbase32::encode(data); - assert_eq!(&zbase32::decode(&res).unwrap()[..], data); + let res = base32::Alphabet::ZBase32.encode(data); + assert_eq!(&base32::Alphabet::ZBase32.decode(&res).unwrap()[..], data); if let Ok(s) = std::str::from_utf8(data) { - if let Ok(decoded) = zbase32::decode(s) { - assert_eq!(&zbase32::encode(&decoded), &s.to_ascii_lowercase()); + let res = base32::Alphabet::ZBase32.decode(s); + if let Ok(decoded) = res { + assert_eq!(&base32::Alphabet::ZBase32.encode(&decoded), &s.to_ascii_lowercase()); } } } diff --git a/fuzz/targets.h b/fuzz/targets.h index 9b5a6d45536..cad0ac4d822 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -14,6 +14,8 @@ void router_run(const unsigned char* data, size_t data_len); void zbase32_run(const unsigned char* data, size_t data_len); void indexedmap_run(const unsigned char* data, size_t data_len); void onion_hop_data_run(const unsigned char* data, size_t data_len); +void base32_run(const unsigned char* data, size_t data_len); +void fromstr_to_netaddress_run(const unsigned char* data, size_t data_len); void msg_accept_channel_run(const unsigned char* data, size_t data_len); void msg_announcement_signatures_run(const unsigned char* data, size_t data_len); void msg_channel_reestablish_run(const unsigned char* data, size_t data_len); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index e8a226932b4..d12dafb65af 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -37,14 +37,17 @@ use crate::ln::onion_utils; use crate::onion_message; use crate::prelude::*; +use core::convert::TryFrom; use core::fmt; use core::fmt::Debug; +use core::str::FromStr; use crate::io::{self, Read}; use crate::io_extras::read_to_end; use crate::events::{MessageSendEventsProvider, OnionMessageProvider}; use crate::util::logger; use crate::util::ser::{LengthReadable, Readable, ReadableArgs, Writeable, Writer, WithoutLength, FixedLengthReader, HighZeroBytesDroppedBigSize, Hostname, TransactionU16LenLimited, BigSize}; +use crate::util::base32; use crate::routing::gossip::{NodeAlias, NodeId}; @@ -899,6 +902,104 @@ impl Readable for NetAddress { } } +/// [`NetAddress`] error variants +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum NetAddressParseError { + /// Socket address (IPv4/IPv6) parsing error + SocketAddrParse, + /// Invalid input format + InvalidInput, + /// Invalid port + InvalidPort, + /// Invalid onion v3 address + InvalidOnionV3, +} + +impl fmt::Display for NetAddressParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NetAddressParseError::SocketAddrParse => write!(f, "Socket address (IPv4/IPv6) parsing error"), + NetAddressParseError::InvalidInput => write!(f, "Invalid input format. \ + Expected: \":\", \"[]:\", \".onion:\" or \":\""), + NetAddressParseError::InvalidPort => write!(f, "Invalid port"), + NetAddressParseError::InvalidOnionV3 => write!(f, "Invalid onion v3 address"), + } + } +} + +#[cfg(feature = "std")] +impl From for NetAddress { + fn from(addr: std::net::SocketAddrV4) -> Self { + NetAddress::IPv4 { addr: addr.ip().octets(), port: addr.port() } + } +} + +#[cfg(feature = "std")] +impl From for NetAddress { + fn from(addr: std::net::SocketAddrV6) -> Self { + NetAddress::IPv6 { addr: addr.ip().octets(), port: addr.port() } + } +} + +#[cfg(feature = "std")] +impl From for NetAddress { + fn from(addr: std::net::SocketAddr) -> Self { + match addr { + std::net::SocketAddr::V4(addr) => addr.into(), + std::net::SocketAddr::V6(addr) => addr.into(), + } + } +} + +fn parse_onion_address(host: &str, port: u16) -> Result { + if host.ends_with(".onion") { + let domain = &host[..host.len() - ".onion".len()]; + if domain.len() != 56 { + return Err(NetAddressParseError::InvalidOnionV3); + } + let onion = base32::Alphabet::RFC4648 { padding: false }.decode(&domain).map_err(|_| NetAddressParseError::InvalidOnionV3)?; + if onion.len() != 35 { + return Err(NetAddressParseError::InvalidOnionV3); + } + let version = onion[0]; + let first_checksum_flag = onion[1]; + let second_checksum_flag = onion[2]; + let mut ed25519_pubkey = [0; 32]; + ed25519_pubkey.copy_from_slice(&onion[3..35]); + let checksum = u16::from_be_bytes([first_checksum_flag, second_checksum_flag]); + return Ok(NetAddress::OnionV3 { ed25519_pubkey, checksum, version, port }); + + } else { + return Err(NetAddressParseError::InvalidInput); + } +} + +#[cfg(feature = "std")] +impl FromStr for NetAddress { + type Err = NetAddressParseError; + + fn from_str(s: &str) -> Result { + match std::net::SocketAddr::from_str(s) { + Ok(addr) => Ok(addr.into()), + Err(_) => { + let trimmed_input = match s.rfind(":") { + Some(pos) => pos, + None => return Err(NetAddressParseError::InvalidInput), + }; + let host = &s[..trimmed_input]; + let port: u16 = s[trimmed_input + 1..].parse().map_err(|_| NetAddressParseError::InvalidPort)?; + if host.ends_with(".onion") { + return parse_onion_address(host, port); + }; + if let Ok(hostname) = Hostname::try_from(s[..trimmed_input].to_string()) { + return Ok(NetAddress::Hostname { hostname, port }); + }; + return Err(NetAddressParseError::SocketAddrParse) + }, + } + } +} + /// Represents the set of gossip messages that require a signature from a node's identity key. pub enum UnsignedGossipMessage<'a> { /// An unsigned channel announcement. @@ -2471,6 +2572,7 @@ impl_writeable_msg!(GossipTimestampFilter, { #[cfg(test)] mod tests { + use std::convert::TryFrom; use bitcoin::blockdata::constants::ChainHash; use bitcoin::{Transaction, PackedLockTime, TxIn, Script, Sequence, Witness, TxOut}; use hex; @@ -2478,6 +2580,7 @@ mod tests { use crate::ln::ChannelId; use crate::ln::features::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; use crate::ln::msgs::{self, FinalOnionHopData, OnionErrorPacket}; + use crate::ln::msgs::NetAddress; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::util::ser::{Writeable, Readable, Hostname, TransactionU16LenLimited}; @@ -2493,11 +2596,13 @@ mod tests { use crate::io::{self, Cursor}; use crate::prelude::*; - use core::convert::TryFrom; use core::str::FromStr; - use crate::chain::transaction::OutPoint; + #[cfg(feature = "std")] + use std::net::{Ipv4Addr, Ipv6Addr}; + use crate::ln::msgs::NetAddressParseError; + #[test] fn encoding_channel_reestablish() { let public_key = { @@ -2663,24 +2768,24 @@ mod tests { }; let mut addresses = Vec::new(); if ipv4 { - addresses.push(msgs::NetAddress::IPv4 { + addresses.push(NetAddress::IPv4 { addr: [255, 254, 253, 252], port: 9735 }); } if ipv6 { - addresses.push(msgs::NetAddress::IPv6 { + addresses.push(NetAddress::IPv6 { addr: [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240], port: 9735 }); } if onionv2 { - addresses.push(msgs::NetAddress::OnionV2( + addresses.push(NetAddress::OnionV2( [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 38, 7] )); } if onionv3 { - addresses.push(msgs::NetAddress::OnionV3 { + addresses.push(NetAddress::OnionV3 { ed25519_pubkey: [255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240, 239, 238, 237, 236, 235, 234, 233, 232, 231, 230, 229, 228, 227, 226, 225, 224], checksum: 32, version: 16, @@ -2688,7 +2793,7 @@ mod tests { }); } if hostname { - addresses.push(msgs::NetAddress::Hostname { + addresses.push(NetAddress::Hostname { hostname: Hostname::try_from(String::from("host")).unwrap(), port: 9735, }); @@ -3296,10 +3401,10 @@ mod tests { let shutdown = msgs::Shutdown { channel_id: ChannelId::from_bytes([2; 32]), scriptpubkey: - if script_type == 1 { Address::p2pkh(&::bitcoin::PublicKey{compressed: true, inner: pubkey_1}, Network::Testnet).script_pubkey() } + if script_type == 1 { Address::p2pkh(&::bitcoin::PublicKey{compressed: true, inner: pubkey_1}, Network::Testnet).script_pubkey() } else if script_type == 2 { Address::p2sh(&script, Network::Testnet).unwrap().script_pubkey() } else if script_type == 3 { Address::p2wpkh(&::bitcoin::PublicKey{compressed: true, inner: pubkey_1}, Network::Testnet).unwrap().script_pubkey() } - else { Address::p2wsh(&script, Network::Testnet).script_pubkey() }, + else { Address::p2wsh(&script, Network::Testnet).script_pubkey() }, }; let encoded_value = shutdown.encode(); let mut target_value = hex::decode("0202020202020202020202020202020202020202020202020202020202020202").unwrap(); @@ -3504,7 +3609,7 @@ mod tests { }.encode(), hex::decode("00000000014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202").unwrap()); let init_msg = msgs::Init { features: InitFeatures::from_le_bytes(vec![]), networks: Some(vec![mainnet_hash]), - remote_network_address: Some(msgs::NetAddress::IPv4 { + remote_network_address: Some(NetAddress::IPv4 { addr: [127, 0, 0, 1], port: 1000, }), @@ -3869,4 +3974,47 @@ mod tests { } Ok(encoded_payload) } + + #[test] + #[cfg(feature = "std")] + fn test_net_address_from_str() { + assert_eq!(NetAddress::IPv4 { + addr: Ipv4Addr::new(127, 0, 0, 1).octets(), + port: 1234, + }, NetAddress::from_str("127.0.0.1:1234").unwrap()); + + assert_eq!(NetAddress::IPv6 { + addr: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).octets(), + port: 1234, + }, NetAddress::from_str("[0:0:0:0:0:0:0:1]:1234").unwrap()); + assert_eq!( + NetAddress::Hostname { + hostname: Hostname::try_from("lightning-node.mydomain.com".to_string()).unwrap(), + port: 1234, + }, NetAddress::from_str("lightning-node.mydomain.com:1234").unwrap()); + assert_eq!( + NetAddress::Hostname { + hostname: Hostname::try_from("example.com".to_string()).unwrap(), + port: 1234, + }, NetAddress::from_str("example.com:1234").unwrap()); + assert_eq!(NetAddress::OnionV3 { + ed25519_pubkey: [37, 24, 75, 5, 25, 73, 117, 194, 139, 102, 182, 107, 4, 105, 247, 246, 85, + 111, 177, 172, 49, 137, 167, 155, 64, 221, 163, 47, 31, 33, 71, 3], + checksum: 48326, + version: 121, + port: 1234 + }, NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:1234").unwrap()); + assert_eq!(Err(NetAddressParseError::InvalidOnionV3), NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6.onion:1234")); + assert_eq!(Err(NetAddressParseError::InvalidInput), NetAddress::from_str("127.0.0.1@1234")); + assert_eq!(Err(NetAddressParseError::InvalidInput), "".parse::()); + assert!(NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion.onion:9735:94").is_err()); + assert!(NetAddress::from_str("wrong$%#.com:1234").is_err()); + assert_eq!(Err(NetAddressParseError::InvalidPort), NetAddress::from_str("example.com:wrong")); + assert!("localhost".parse::().is_err()); + assert!("localhost:invalid-port".parse::().is_err()); + assert!( "invalid-onion-v3-hostname.onion:8080".parse::().is_err()); + assert!("b32.example.onion:invalid-port".parse::().is_err()); + assert!("invalid-address".parse::().is_err()); + assert!(NetAddress::from_str("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion.onion:1234").is_err()); + } } diff --git a/lightning/src/util/base32.rs b/lightning/src/util/base32.rs new file mode 100644 index 00000000000..2e66d59383f --- /dev/null +++ b/lightning/src/util/base32.rs @@ -0,0 +1,273 @@ +// This is a modification of base32 encoding to support the zbase32 alphabet. +// The original piece of software can be found at https://crates.io/crates/base32(v0.4.0) +// The original portions of this software are Copyright (c) 2015 The base32 Developers + +// This file is licensed under either of +// Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or +// MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option. + + +use crate::prelude::*; + +/// RFC4648 encoding table +const RFC4648_ALPHABET: &'static [u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +/// Zbase encoding alphabet +const ZBASE_ALPHABET: &'static [u8] = b"ybndrfg8ejkmcpqxot1uwisza345h769"; + +/// RFC4648 decoding table +const RFC4648_INV_ALPHABET: [i8; 43] = [ + -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, +]; + +/// Zbase decoding table +const ZBASE_INV_ALPHABET: [i8; 43] = [ + -1, 18, -1, 25, 26, 27, 30, 29, 7, 31, -1, -1, -1, -1, -1, -1, -1, 24, 1, 12, 3, 8, 5, 6, 28, + 21, 9, 10, -1, 11, 2, 16, 13, 14, 4, 22, 17, 19, -1, 20, 15, 0, 23, +]; + +/// Alphabet used for encoding and decoding. +#[derive(Copy, Clone)] +pub enum Alphabet { + /// RFC4648 encoding. + RFC4648 { + /// Whether to use padding. + padding: bool + }, + /// Zbase32 encoding. + ZBase32 +} + +impl Alphabet { + /// Encode bytes into a base32 string. + pub fn encode(&self, data: &[u8]) -> String { + // output_length is calculated as follows: + // / 5 divides the data length by the number of bits per chunk (5), + // * 8 multiplies the result by the number of characters per chunk (8). + // + 4 rounds up to the nearest character. + let output_length = (data.len() * 8 + 4) / 5; + let mut ret = match self { + Self::RFC4648 { padding } => { + let mut ret = Self::encode_data(data, RFC4648_ALPHABET); + if *padding { + let len = ret.len(); + for i in output_length..len { + ret[i] = b'='; + } + + return String::from_utf8(ret).expect("Invalid UTF-8"); + } + ret + }, + Self::ZBase32 => { + Self::encode_data(data, ZBASE_ALPHABET) + }, + }; + ret.truncate(output_length); + + #[cfg(fuzzing)] + assert_eq!(ret.capacity(), (data.len() + 4) / 5 * 8); + + String::from_utf8(ret).expect("Invalid UTF-8") + } + + /// Decode a base32 string into a byte vector. + pub fn decode(&self, data: &str) -> Result, ()> { + let data = data.as_bytes(); + let (data, alphabet) = match self { + Self::RFC4648 { padding } => { + let mut unpadded_data_length = data.len(); + if *padding { + if data.len() % 8 != 0 { return Err(()); } + data.iter().rev().take(6).for_each(|&c| { + if c == b'=' { + unpadded_data_length -= 1; + } + }); + } + (&data[..unpadded_data_length], RFC4648_INV_ALPHABET) + }, + Self::ZBase32 => { + (data, ZBASE_INV_ALPHABET) + } + }; + // If the string has more characters than are required to alphabet_encode the number of bytes + // decodable, treat the string as invalid. + match data.len() % 8 { 1|3|6 => return Err(()), _ => {} } + Ok(Self::decode_data(data, alphabet)?) + } + + /// Encode a byte slice into a base32 string. + fn encode_data(data: &[u8], alphabet: &'static [u8]) -> Vec { + // cap is calculated as follows: + // / 5 divides the data length by the number of bits per chunk (5), + // * 8 multiplies the result by the number of characters per chunk (8). + // + 4 rounds up to the nearest character. + let cap = (data.len() + 4) / 5 * 8; + let mut ret = Vec::with_capacity(cap); + for chunk in data.chunks(5) { + let mut buf = [0u8; 5]; + for (i, &b) in chunk.iter().enumerate() { + buf[i] = b; + } + ret.push(alphabet[((buf[0] & 0xF8) >> 3) as usize]); + ret.push(alphabet[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize]); + ret.push(alphabet[((buf[1] & 0x3E) >> 1) as usize]); + ret.push(alphabet[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize]); + ret.push(alphabet[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize]); + ret.push(alphabet[((buf[3] & 0x7C) >> 2) as usize]); + ret.push(alphabet[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize]); + ret.push(alphabet[(buf[4] & 0x1F) as usize]); + } + #[cfg(fuzzing)] + assert_eq!(ret.capacity(), cap); + + ret + } + + fn decode_data(data: &[u8], alphabet: [i8; 43]) -> Result, ()> { + // cap is calculated as follows: + // / 8 divides the data length by the number of characters per chunk (8), + // * 5 multiplies the result by the number of bits per chunk (5), + // + 7 rounds up to the nearest byte. + let cap = (data.len() + 7) / 8 * 5; + let mut ret = Vec::with_capacity(cap); + for chunk in data.chunks(8) { + let mut buf = [0u8; 8]; + for (i, &c) in chunk.iter().enumerate() { + match alphabet.get(c.to_ascii_uppercase().wrapping_sub(b'0') as usize) { + Some(&-1) | None => return Err(()), + Some(&value) => buf[i] = value as u8, + }; + } + ret.push((buf[0] << 3) | (buf[1] >> 2)); + ret.push((buf[1] << 6) | (buf[2] << 1) | (buf[3] >> 4)); + ret.push((buf[3] << 4) | (buf[4] >> 1)); + ret.push((buf[4] << 7) | (buf[5] << 2) | (buf[6] >> 3)); + ret.push((buf[6] << 5) | buf[7]); + } + let output_length = data.len() * 5 / 8; + for c in ret.drain(output_length..) { + if c != 0 { + // If the original string had any bits set at positions outside of the encoded data, + // treat the string as invalid. + return Err(()); + } + } + + // Check that our capacity calculation doesn't under-shoot in fuzzing + #[cfg(fuzzing)] + assert_eq!(ret.capacity(), cap); + Ok(ret) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ZBASE32_TEST_DATA: &[(&str, &[u8])] = &[ + ("", &[]), + ("yy", &[0x00]), + ("oy", &[0x80]), + ("tqrey", &[0x8b, 0x88, 0x80]), + ("6n9hq", &[0xf0, 0xbf, 0xc7]), + ("4t7ye", &[0xd4, 0x7a, 0x04]), + ("6im5sdy", &[0xf5, 0x57, 0xbb, 0x0c]), + ("ybndrfg8ejkmcpqxot1uwisza345h769", &[0x00, 0x44, 0x32, 0x14, 0xc7, 0x42, 0x54, 0xb6, + 0x35, 0xcf, 0x84, 0x65, 0x3a, 0x56, 0xd7, 0xc6, + 0x75, 0xbe, 0x77, 0xdf]) + ]; + + #[test] + fn test_zbase32_encode() { + for &(zbase32, data) in ZBASE32_TEST_DATA { + assert_eq!(Alphabet::ZBase32.encode(data), zbase32); + } + } + + #[test] + fn test_zbase32_decode() { + for &(zbase32, data) in ZBASE32_TEST_DATA { + assert_eq!(Alphabet::ZBase32.decode(zbase32).unwrap(), data); + } + } + + #[test] + fn test_decode_wrong() { + const WRONG_DATA: &[&str] = &["00", "l1", "?", "="]; + for &data in WRONG_DATA { + match Alphabet::ZBase32.decode(data) { + Ok(_) => assert!(false, "Data shouldn't be decodable"), + Err(_) => assert!(true), + } + } + } + + const RFC4648_NON_PADDED_TEST_VECTORS: &[(&[u8], &[u8])] = &[ + (&[0xF8, 0x3E, 0x7F, 0x83, 0xE7], b"7A7H7A7H"), + (&[0x77, 0xC1, 0xF7, 0x7C, 0x1F], b"O7A7O7A7"), + (&[0xF8, 0x3E, 0x7F, 0x83, 0xE7], b"7A7H7A7H"), + (&[0x77, 0xC1, 0xF7, 0x7C, 0x1F], b"O7A7O7A7"), + ]; + + const RFC4648_TEST_VECTORS: &[(&[u8], &str)] = &[ + (b"", ""), + (b"f", "MY======"), + (b"fo", "MZXQ===="), + (b"foo", "MZXW6==="), + (b"foob", "MZXW6YQ="), + (b"fooba", "MZXW6YTB"), + (b"foobar", "MZXW6YTBOI======"), + (&[0xF8, 0x3E, 0x7F, 0x83], "7A7H7AY="), + ]; + + #[test] + fn test_rfc4648_encode() { + for (input, encoded) in RFC4648_TEST_VECTORS { + assert_eq!(&Alphabet::RFC4648 { padding: true }.encode(input), encoded); + } + + for (input, encoded) in RFC4648_NON_PADDED_TEST_VECTORS { + assert_eq!(&Alphabet::RFC4648 { padding: false }.encode(input).as_bytes(), encoded); + } + } + + #[test] + fn test_rfc4648_decode() { + for (input, encoded) in RFC4648_TEST_VECTORS { + let res = &Alphabet::RFC4648 { padding: true }.decode(encoded).unwrap(); + assert_eq!(&res[..], &input[..]); + } + + for (input, encoded) in RFC4648_NON_PADDED_TEST_VECTORS { + let res = &Alphabet::RFC4648 { padding: false }.decode(std::str::from_utf8(encoded).unwrap()).unwrap(); + assert_eq!(&res[..], &input[..]); + } + } + + #[test] + fn padding() { + let num_padding = [0, 6, 4, 3, 1]; + for i in 1..6 { + let encoded = Alphabet::RFC4648 { padding: true }.encode( + (0..(i as u8)).collect::>().as_ref() + ); + assert_eq!(encoded.len(), 8); + for j in 0..(num_padding[i % 5]) { + assert_eq!(encoded.as_bytes()[encoded.len() - j - 1], b'='); + } + for j in 0..(8 - num_padding[i % 5]) { + assert!(encoded.as_bytes()[j] != b'='); + } + } + } + + #[test] + fn test_decode_rfc4648_errors() { + assert!(Alphabet::RFC4648 { padding: false }.decode("abc2def===").is_err()); // Invalid char because padding is disabled + assert!(Alphabet::RFC4648 { padding: true }.decode("abc2def===").is_err()); // Invalid length + assert!(Alphabet::RFC4648 { padding: true }.decode("MZX=6YTB").is_err()); // Invalid char + } +} diff --git a/lightning/src/util/message_signing.rs b/lightning/src/util/message_signing.rs index aa1fdfcee15..477cbcbdd9a 100644 --- a/lightning/src/util/message_signing.rs +++ b/lightning/src/util/message_signing.rs @@ -21,7 +21,7 @@ //! use crate::prelude::*; -use crate::util::zbase32; +use crate::util::base32; use bitcoin::hashes::{sha256d, Hash}; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; use bitcoin::secp256k1::{Error, Message, PublicKey, Secp256k1, SecretKey}; @@ -29,118 +29,119 @@ use bitcoin::secp256k1::{Error, Message, PublicKey, Secp256k1, SecretKey}; static LN_MESSAGE_PREFIX: &[u8] = b"Lightning Signed Message:"; fn sigrec_encode(sig_rec: RecoverableSignature) -> Vec { - let (rid, rsig) = sig_rec.serialize_compact(); - let prefix = rid.to_i32() as u8 + 31; + let (rid, rsig) = sig_rec.serialize_compact(); + let prefix = rid.to_i32() as u8 + 31; - [&[prefix], &rsig[..]].concat() + [&[prefix], &rsig[..]].concat() } fn sigrec_decode(sig_rec: Vec) -> Result { - // Signature must be 64 + 1 bytes long (compact signature + recovery id) - if sig_rec.len() != 65 { - return Err(Error::InvalidSignature); - } - - let rsig = &sig_rec[1..]; - let rid = sig_rec[0] as i32 - 31; - - match RecoveryId::from_i32(rid) { - Ok(x) => RecoverableSignature::from_compact(rsig, x), - Err(e) => Err(e) - } + // Signature must be 64 + 1 bytes long (compact signature + recovery id) + if sig_rec.len() != 65 { + return Err(Error::InvalidSignature); + } + + let rsig = &sig_rec[1..]; + let rid = sig_rec[0] as i32 - 31; + + match RecoveryId::from_i32(rid) { + Ok(x) => RecoverableSignature::from_compact(rsig, x), + Err(e) => Err(e) + } } /// Creates a digital signature of a message given a SecretKey, like the node's secret. /// A receiver knowing the PublicKey (e.g. the node's id) and the message can be sure that the signature was generated by the caller. /// Signatures are EC recoverable, meaning that given the message and the signature the PublicKey of the signer can be extracted. pub fn sign(msg: &[u8], sk: &SecretKey) -> Result { - let secp_ctx = Secp256k1::signing_only(); - let msg_hash = sha256d::Hash::hash(&[LN_MESSAGE_PREFIX, msg].concat()); + let secp_ctx = Secp256k1::signing_only(); + let msg_hash = sha256d::Hash::hash(&[LN_MESSAGE_PREFIX, msg].concat()); - let sig = secp_ctx.sign_ecdsa_recoverable(&Message::from_slice(&msg_hash)?, sk); - Ok(zbase32::encode(&sigrec_encode(sig))) + let sig = secp_ctx.sign_ecdsa_recoverable(&Message::from_slice(&msg_hash)?, sk); + Ok(base32::Alphabet::ZBase32.encode(&sigrec_encode(sig))) } /// Recovers the PublicKey of the signer of the message given the message and the signature. pub fn recover_pk(msg: &[u8], sig: &str) -> Result { - let secp_ctx = Secp256k1::verification_only(); - let msg_hash = sha256d::Hash::hash(&[LN_MESSAGE_PREFIX, msg].concat()); - - match zbase32::decode(&sig) { - Ok(sig_rec) => { - match sigrec_decode(sig_rec) { - Ok(sig) => secp_ctx.recover_ecdsa(&Message::from_slice(&msg_hash)?, &sig), - Err(e) => Err(e) - } - }, - Err(_) => Err(Error::InvalidSignature) - } + let secp_ctx = Secp256k1::verification_only(); + let msg_hash = sha256d::Hash::hash(&[LN_MESSAGE_PREFIX, msg].concat()); + + match base32::Alphabet::ZBase32.decode(&sig) { + Ok(sig_rec) => { + match sigrec_decode(sig_rec) { + Ok(sig) => secp_ctx.recover_ecdsa(&Message::from_slice(&msg_hash)?, &sig), + Err(e) => Err(e) + } + }, + Err(_) => Err(Error::InvalidSignature) + } } /// Verifies a message was signed by a PrivateKey that derives to a given PublicKey, given a message, a signature, /// and the PublicKey. pub fn verify(msg: &[u8], sig: &str, pk: &PublicKey) -> bool { - match recover_pk(msg, sig) { - Ok(x) => x == *pk, - Err(_) => false - } + match recover_pk(msg, sig) { + Ok(x) => x == *pk, + Err(_) => false + } } #[cfg(test)] mod test { - use core::str::FromStr; - use crate::util::message_signing::{sign, recover_pk, verify}; - use bitcoin::secp256k1::ONE_KEY; - use bitcoin::secp256k1::{PublicKey, Secp256k1}; - - #[test] - fn test_sign() { - let message = "test message"; - let zbase32_sig = sign(message.as_bytes(), &ONE_KEY); - - assert_eq!(zbase32_sig.unwrap(), "d9tibmnic9t5y41hg7hkakdcra94akas9ku3rmmj4ag9mritc8ok4p5qzefs78c9pqfhpuftqqzhydbdwfg7u6w6wdxcqpqn4sj4e73e") - } - - #[test] - fn test_recover_pk() { - let message = "test message"; - let sig = "d9tibmnic9t5y41hg7hkakdcra94akas9ku3rmmj4ag9mritc8ok4p5qzefs78c9pqfhpuftqqzhydbdwfg7u6w6wdxcqpqn4sj4e73e"; - let pk = recover_pk(message.as_bytes(), sig); - - assert_eq!(pk.unwrap(), PublicKey::from_secret_key(&Secp256k1::signing_only(), &ONE_KEY)) - } - - #[test] - fn test_verify() { - let message = "another message"; - let sig = sign(message.as_bytes(), &ONE_KEY).unwrap(); - let pk = PublicKey::from_secret_key(&Secp256k1::signing_only(), &ONE_KEY); - - assert!(verify(message.as_bytes(), &sig, &pk)) - } - - #[test] - fn test_verify_ground_truth_ish() { - // There are no standard tests vectors for Sign/Verify, using the same tests vectors as c-lightning to see if they are compatible. - // Taken from https://github.com/ElementsProject/lightning/blob/1275af6fbb02460c8eb2f00990bb0ef9179ce8f3/tests/test_misc.py#L1925-L1938 - - let corpus = [ - ["@bitconner", - "is this compatible?", - "rbgfioj114mh48d8egqx8o9qxqw4fmhe8jbeeabdioxnjk8z3t1ma1hu1fiswpakgucwwzwo6ofycffbsqusqdimugbh41n1g698hr9t", - "02b80cabdf82638aac86948e4c06e82064f547768dcef977677b9ea931ea75bab5"], - ["@duck1123", - "hi", - "rnrphcjswusbacjnmmmrynh9pqip7sy5cx695h6mfu64iac6qmcmsd8xnsyczwmpqp9shqkth3h4jmkgyqu5z47jfn1q7gpxtaqpx4xg", - "02de60d194e1ca5947b59fe8e2efd6aadeabfb67f2e89e13ae1a799c1e08e4a43b"], - ["@jochemin", - "hi", - "ry8bbsopmduhxy3dr5d9ekfeabdpimfx95kagdem7914wtca79jwamtbw4rxh69hg7n6x9ty8cqk33knbxaqftgxsfsaeprxkn1k48p3", - "022b8ece90ee891cbcdac0c1cc6af46b73c47212d8defbce80265ac81a6b794931"], - ]; - - for c in &corpus { - assert!(verify(c[1].as_bytes(), c[2], &PublicKey::from_str(c[3]).unwrap())) - } - } + use core::str::FromStr; + use crate::util::message_signing::{sign, recover_pk, verify}; + use bitcoin::secp256k1::ONE_KEY; + use bitcoin::secp256k1::{PublicKey, Secp256k1}; + + #[test] + fn test_sign() { + let message = "test message"; + let zbase32_sig = sign(message.as_bytes(), &ONE_KEY); + + assert_eq!(zbase32_sig.unwrap(), "d9tibmnic9t5y41hg7hkakdcra94akas9ku3rmmj4ag9mritc8ok4p5qzefs78c9pqfhpuftqqzhydbdwfg7u6w6wdxcqpqn4sj4e73e") + } + + #[test] + fn test_recover_pk() { + let message = "test message"; + let sig = "d9tibmnic9t5y41hg7hkakdcra94akas9ku3rmmj4ag9mritc8ok4p5qzefs78c9pqfhpuftqqzhydbdwfg7u6w6wdxcqpqn4sj4e73e"; + let pk = recover_pk(message.as_bytes(), sig); + + assert_eq!(pk.unwrap(), PublicKey::from_secret_key(&Secp256k1::signing_only(), &ONE_KEY)) + } + + #[test] + fn test_verify() { + let message = "another message"; + let sig = sign(message.as_bytes(), &ONE_KEY).unwrap(); + let pk = PublicKey::from_secret_key(&Secp256k1::signing_only(), &ONE_KEY); + + assert!(verify(message.as_bytes(), &sig, &pk)) + } + + #[test] + fn test_verify_ground_truth_ish() { + // There are no standard tests vectors for Sign/Verify, using the same tests vectors as c-lightning to see if they are compatible. + // Taken from https://github.com/ElementsProject/lightning/blob/1275af6fbb02460c8eb2f00990bb0ef9179ce8f3/tests/test_misc.py#L1925-L1938 + + let corpus = [ + ["@bitconner", + "is this compatible?", + "rbgfioj114mh48d8egqx8o9qxqw4fmhe8jbeeabdioxnjk8z3t1ma1hu1fiswpakgucwwzwo6ofycffbsqusqdimugbh41n1g698hr9t", + "02b80cabdf82638aac86948e4c06e82064f547768dcef977677b9ea931ea75bab5"], + ["@duck1123", + "hi", + "rnrphcjswusbacjnmmmrynh9pqip7sy5cx695h6mfu64iac6qmcmsd8xnsyczwmpqp9shqkth3h4jmkgyqu5z47jfn1q7gpxtaqpx4xg", + "02de60d194e1ca5947b59fe8e2efd6aadeabfb67f2e89e13ae1a799c1e08e4a43b"], + ["@jochemin", + "hi", + "ry8bbsopmduhxy3dr5d9ekfeabdpimfx95kagdem7914wtca79jwamtbw4rxh69hg7n6x9ty8cqk33knbxaqftgxsfsaeprxkn1k48p3", + "022b8ece90ee891cbcdac0c1cc6af46b73c47212d8defbce80265ac81a6b794931"], + ]; + + for c in &corpus { + assert!(verify(c[1].as_bytes(), c[2], &PublicKey::from_str(c[3]).unwrap())) + } + } } + diff --git a/lightning/src/util/mod.rs b/lightning/src/util/mod.rs index cc1b5f581af..e86885a83db 100644 --- a/lightning/src/util/mod.rs +++ b/lightning/src/util/mod.rs @@ -22,14 +22,14 @@ pub mod invoice; pub mod persist; pub mod string; pub mod wakers; +#[cfg(fuzzing)] +pub mod base32; +#[cfg(not(fuzzing))] +pub(crate) mod base32; pub(crate) mod atomic_counter; pub(crate) mod byte_utils; pub(crate) mod chacha20; -#[cfg(fuzzing)] -pub mod zbase32; -#[cfg(not(fuzzing))] -pub(crate) mod zbase32; #[cfg(not(fuzzing))] pub(crate) mod poly1305; pub(crate) mod chacha20poly1305rfc; diff --git a/lightning/src/util/zbase32.rs b/lightning/src/util/zbase32.rs deleted file mode 100644 index 8a7bf3516f1..00000000000 --- a/lightning/src/util/zbase32.rs +++ /dev/null @@ -1,144 +0,0 @@ -// This is a modification of base32 encoding to support the zbase32 alphabet. -// The original piece of software can be found at https://github.com/andreasots/base32 -// The original portions of this software are Copyright (c) 2015 The base32 Developers - -/* This file is licensed under either of - * Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or - * MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) - * at your option. -*/ - -use crate::prelude::*; - -const ALPHABET: &'static [u8] = b"ybndrfg8ejkmcpqxot1uwisza345h769"; - -/// Encodes some bytes as a zbase32 string -pub fn encode(data: &[u8]) -> String { - let mut ret = Vec::with_capacity((data.len() + 4) / 5 * 8); - - for chunk in data.chunks(5) { - let buf = { - let mut buf = [0u8; 5]; - for (i, &b) in chunk.iter().enumerate() { - buf[i] = b; - } - buf - }; - - ret.push(ALPHABET[((buf[0] & 0xF8) >> 3) as usize]); - ret.push(ALPHABET[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize]); - ret.push(ALPHABET[((buf[1] & 0x3E) >> 1) as usize]); - ret.push(ALPHABET[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize]); - ret.push(ALPHABET[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize]); - ret.push(ALPHABET[((buf[3] & 0x7C) >> 2) as usize]); - ret.push(ALPHABET[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize]); - ret.push(ALPHABET[(buf[4] & 0x1F) as usize]); - } - - ret.truncate((data.len() * 8 + 4) / 5); - - // Check that our capacity calculation doesn't under-shoot in fuzzing - #[cfg(fuzzing)] - assert_eq!(ret.capacity(), (data.len() + 4) / 5 * 8); - - String::from_utf8(ret).unwrap() -} - -// ASCII 0-Z -const INV_ALPHABET: [i8; 43] = [ - -1, 18, -1, 25, 26, 27, 30, 29, 7, 31, -1, -1, -1, -1, -1, -1, -1, 24, 1, 12, 3, 8, 5, 6, 28, - 21, 9, 10, -1, 11, 2, 16, 13, 14, 4, 22, 17, 19, -1, 20, 15, 0, 23, -]; - -/// Decodes a zbase32 string to the original bytes, failing if the string was not encoded by a -/// proper zbase32 encoder. -pub fn decode(data: &str) -> Result, ()> { - if !data.is_ascii() { - return Err(()); - } - - let data = data.as_bytes(); - let output_length = data.len() * 5 / 8; - if data.len() > (output_length * 8 + 4) / 5 { - // If the string has more charachters than are required to encode the number of bytes - // decodable, treat the string as invalid. - return Err(()); - } - - let mut ret = Vec::with_capacity((data.len() + 7) / 8 * 5); - - for chunk in data.chunks(8) { - let buf = { - let mut buf = [0u8; 8]; - for (i, &c) in chunk.iter().enumerate() { - match INV_ALPHABET.get(c.to_ascii_uppercase().wrapping_sub(b'0') as usize) { - Some(&-1) | None => return Err(()), - Some(&value) => buf[i] = value as u8, - }; - } - buf - }; - ret.push((buf[0] << 3) | (buf[1] >> 2)); - ret.push((buf[1] << 6) | (buf[2] << 1) | (buf[3] >> 4)); - ret.push((buf[3] << 4) | (buf[4] >> 1)); - ret.push((buf[4] << 7) | (buf[5] << 2) | (buf[6] >> 3)); - ret.push((buf[6] << 5) | buf[7]); - } - for c in ret.drain(output_length..) { - if c != 0 { - // If the original string had any bits set at positions outside of the encoded data, - // treat the string as invalid. - return Err(()); - } - } - - // Check that our capacity calculation doesn't under-shoot in fuzzing - #[cfg(fuzzing)] - assert_eq!(ret.capacity(), (data.len() + 7) / 8 * 5); - - Ok(ret) -} - -#[cfg(test)] -mod tests { - use super::*; - - const TEST_DATA: &[(&str, &[u8])] = &[ - ("", &[]), - ("yy", &[0x00]), - ("oy", &[0x80]), - ("tqrey", &[0x8b, 0x88, 0x80]), - ("6n9hq", &[0xf0, 0xbf, 0xc7]), - ("4t7ye", &[0xd4, 0x7a, 0x04]), - ("6im5sdy", &[0xf5, 0x57, 0xbb, 0x0c]), - ("ybndrfg8ejkmcpqxot1uwisza345h769", &[0x00, 0x44, 0x32, 0x14, 0xc7, 0x42, 0x54, 0xb6, - 0x35, 0xcf, 0x84, 0x65, 0x3a, 0x56, 0xd7, 0xc6, - 0x75, 0xbe, 0x77, 0xdf]) - ]; - - #[test] - fn test_encode() { - for &(zbase32, data) in TEST_DATA { - assert_eq!(encode(data), zbase32); - } - } - - #[test] - fn test_decode() { - for &(zbase32, data) in TEST_DATA { - assert_eq!(decode(zbase32).unwrap(), data); - } - } - - #[test] - fn test_decode_wrong() { - const WRONG_DATA: &[&str] = &["00", "l1", "?", "="]; - - for &data in WRONG_DATA { - match decode(data) { - Ok(_) => assert!(false, "Data shouldn't be decodable"), - Err(_) => assert!(true), - } - } - } -}