Skip to content

Commit 2af2300

Browse files
committed
Bech32m encode HPKE PublicKey, OhttpKeys
Bech32m has a human-readable prefix, checksum, creates smaller QR codes, and is already available in rust-bitcoin. It's a much better encoding scheme than base64 for bip21 uri params.
1 parent 81d1a1f commit 2af2300

File tree

9 files changed

+164
-75
lines changed

9 files changed

+164
-75
lines changed

Cargo.lock

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

payjoin-directory/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ danger-local-https = ["hyper-rustls", "rustls"]
1818

1919
[dependencies]
2020
anyhow = "1.0.71"
21+
bech32 = "0.11.0"
2122
bitcoin = { version = "0.30.0", features = ["base64"] }
2223
bhttp = { version = "0.4.0", features = ["http"] }
2324
futures = "0.3.17"

payjoin-directory/src/lib.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::sync::Arc;
33
use std::time::Duration;
44

55
use anyhow::Result;
6-
use bitcoin::{self, base64};
76
use hyper::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE};
87
use hyper::server::conn::AddrIncoming;
98
use hyper::server::Builder;
@@ -101,12 +100,11 @@ fn init_ohttp() -> Result<ohttp::Server> {
101100

102101
// create or read from file
103102
let server_config = ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))?;
104-
let encoded_config = server_config.encode()?;
105-
let b64_config = base64::encode_config(
106-
encoded_config,
107-
base64::Config::new(base64::CharacterSet::UrlSafe, false),
108-
);
109-
info!("ohttp-keys server config base64 UrlSafe: {:?}", b64_config);
103+
let encoded_config = bech32::encode::<bech32::Bech32m>(
104+
bech32::Hrp::parse("oh").unwrap(),
105+
&server_config.encode()?,
106+
)?;
107+
info!("ohttp-keys server config encoded: {:?}", encoded_config);
110108
Ok(ohttp::Server::new(server_config)?)
111109
}
112110

@@ -242,13 +240,12 @@ impl From<hyper::http::Error> for HandlerError {
242240
}
243241

244242
async fn post_enroll(body: Body) -> Result<Response<Body>, HandlerError> {
245-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
246243
let bytes =
247244
hyper::body::to_bytes(body).await.map_err(|e| HandlerError::BadRequest(e.into()))?;
248-
let base64_id =
245+
let encoded_pubkey =
249246
String::from_utf8(bytes.to_vec()).map_err(|e| HandlerError::BadRequest(e.into()))?;
250-
let pubkey_bytes: Vec<u8> = base64::decode_config(base64_id, b64_config)
251-
.map_err(|e| HandlerError::BadRequest(e.into()))?;
247+
let (_hrp, pubkey_bytes) =
248+
bech32::decode(&encoded_pubkey).map_err(|e| HandlerError::BadRequest(e.into()))?;
252249
let pubkey = bitcoin::secp256k1::PublicKey::from_slice(&pubkey_bytes)
253250
.map_err(|e| HandlerError::BadRequest(e.into()))?;
254251
tracing::info!("Enrolled valid pubkey: {:?}", pubkey);

payjoin/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ exclude = ["tests"]
1818
send = []
1919
receive = ["rand"]
2020
base64 = ["bitcoin/base64"]
21-
v2 = ["bitcoin/rand-std", "bitcoin/serde", "chacha20poly1305", "ohttp", "bhttp", "serde"]
21+
v2 = ["bech32", "bitcoin/rand-std", "bitcoin/serde", "chacha20poly1305", "ohttp", "bhttp", "serde"]
2222

2323
[dependencies]
24+
bech32 = { version = "0.11.0", optional = true }
2425
bitcoin = { version = "0.30.0", features = ["base64"] }
2526
bip21 = "0.3.1"
2627
chacha20poly1305 = { version = "0.10.1", optional = true }

payjoin/src/receive/v2.rs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use url::Url;
99
use super::{Error, InternalRequestError, RequestError, SelectionError};
1010
use crate::psbt::PsbtExt;
1111
use crate::receive::optional_parameters::Params;
12+
use crate::v2::encode_bech32_pubkey;
1213
use crate::OhttpKeys;
1314

1415
/// Represents data that needs to be transmitted to the payjoin directory.
@@ -56,11 +57,7 @@ impl Enroller {
5657
}
5758
}
5859

59-
pub fn subdirectory(&self) -> String {
60-
let pubkey = &self.s.public_key().serialize();
61-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
62-
base64::encode_config(pubkey, b64_config)
63-
}
60+
pub fn subdirectory(&self) -> String { encode_bech32_pubkey(&self.s.public_key()) }
6461

6562
pub fn payjoin_subdir(&self) -> String { format!("{}/{}", self.subdirectory(), "payjoin") }
6663

@@ -97,11 +94,7 @@ impl Enroller {
9794
}
9895
}
9996

100-
fn subdirectory(pubkey: &bitcoin::secp256k1::PublicKey) -> String {
101-
let pubkey = pubkey.serialize();
102-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
103-
base64::encode_config(pubkey, b64_config)
104-
}
97+
fn subdirectory(pubkey: &bitcoin::secp256k1::PublicKey) -> String { encode_bech32_pubkey(&pubkey) }
10598

10699
#[derive(Debug, Clone, PartialEq, Eq)]
107100
pub struct Enrolled {
@@ -271,10 +264,8 @@ impl Enrolled {
271264
pub fn pubkey(&self) -> [u8; 33] { self.s.public_key().serialize() }
272265

273266
pub fn fallback_target(&self) -> String {
274-
let pubkey = &self.s.public_key().serialize();
275-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
276-
let pubkey_base64 = base64::encode_config(pubkey, b64_config);
277-
format!("{}{}", &self.directory, pubkey_base64)
267+
let subdirectory = encode_bech32_pubkey(&self.s.public_key());
268+
format!("{}{}", &self.directory, subdirectory)
278269
}
279270
}
280271

payjoin/src/send/error.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ pub(crate) enum InternalCreateRequestError {
187187
#[cfg(feature = "v2")]
188188
OhttpEncapsulation(crate::v2::OhttpEncapsulationError),
189189
#[cfg(feature = "v2")]
190-
SubdirectoryNotBase64(bitcoin::base64::DecodeError),
190+
PubkeyEncoding,
191191
#[cfg(feature = "v2")]
192192
SubdirectoryInvalidPubkey(bitcoin::secp256k1::Error),
193193
#[cfg(feature = "v2")]
@@ -219,7 +219,7 @@ impl fmt::Display for CreateRequestError {
219219
#[cfg(feature = "v2")]
220220
OhttpEncapsulation(e) => write!(f, "v2 error: {}", e),
221221
#[cfg(feature = "v2")]
222-
SubdirectoryNotBase64(e) => write!(f, "subdirectory is not valid base64 error: {}", e),
222+
PubkeyEncoding => write!(f, "Bad public key encoding"),
223223
#[cfg(feature = "v2")]
224224
SubdirectoryInvalidPubkey(e) => write!(f, "subdirectory does not represent a valid pubkey: {}", e),
225225
#[cfg(feature = "v2")]
@@ -253,7 +253,7 @@ impl std::error::Error for CreateRequestError {
253253
#[cfg(feature = "v2")]
254254
OhttpEncapsulation(error) => Some(error),
255255
#[cfg(feature = "v2")]
256-
SubdirectoryNotBase64(error) => Some(error),
256+
PubkeyEncoding => None,
257257
#[cfg(feature = "v2")]
258258
SubdirectoryInvalidPubkey(error) => Some(error),
259259
#[cfg(feature = "v2")]

payjoin/src/send/mod.rs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,8 @@ impl RequestContext {
325325
) -> Result<(Request, ContextV2), CreateRequestError> {
326326
let rs_base64 = crate::v2::subdir(self.endpoint.as_str()).to_string();
327327
log::debug!("rs_base64: {:?}", rs_base64);
328-
let b64_config =
329-
bitcoin::base64::Config::new(bitcoin::base64::CharacterSet::UrlSafe, false);
330-
let rs = bitcoin::base64::decode_config(rs_base64, b64_config)
331-
.map_err(InternalCreateRequestError::SubdirectoryNotBase64)?;
328+
let rs = crate::v2::decode_bech32_pubkey(&rs_base64)
329+
.map_err(|_| InternalCreateRequestError::PubkeyEncoding)?;
332330
log::debug!("rs: {:?}", rs.len());
333331
let rs = bitcoin::secp256k1::PublicKey::from_slice(&rs)
334332
.map_err(InternalCreateRequestError::SubdirectoryInvalidPubkey)?;
@@ -379,12 +377,8 @@ impl Serialize for RequestContext {
379377
let mut state = serializer.serialize_struct("RequestContext", 8)?;
380378
state.serialize_field("psbt", &self.psbt.to_string())?;
381379
state.serialize_field("endpoint", &self.endpoint.as_str())?;
382-
let ohttp_string = self.ohttp_keys.as_ref().map_or(Ok("".to_string()), |config| {
383-
config
384-
.encode()
385-
.map_err(|e| serde::ser::Error::custom(format!("ohttp-keys encoding error: {}", e)))
386-
.map(bitcoin::base64::encode)
387-
})?;
380+
let ohttp_string =
381+
self.ohttp_keys.as_ref().map_or_else(|| "".to_string(), |keys| keys.to_string());
388382
state.serialize_field("ohttp_keys", &ohttp_string)?;
389383
state.serialize_field("disable_output_substitution", &self.disable_output_substitution)?;
390384
state.serialize_field(
@@ -455,17 +449,13 @@ impl<'de> Deserialize<'de> for RequestContext {
455449
.map_err(de::Error::custom)?,
456450
),
457451
"ohttp_keys" => {
458-
let ohttp_base64: String = map.next_value()?;
459-
ohttp_keys = if ohttp_base64.is_empty() {
452+
let ohttp_encoded: String = map.next_value()?;
453+
ohttp_keys = if ohttp_encoded.is_empty() {
460454
None
461455
} else {
462456
Some(
463-
crate::v2::OhttpKeys::decode(
464-
bitcoin::base64::decode(&ohttp_base64)
465-
.map_err(de::Error::custom)?
466-
.as_slice(),
467-
)
468-
.map_err(de::Error::custom)?,
457+
crate::v2::OhttpKeys::from_str(&ohttp_encoded)
458+
.map_err(de::Error::custom)?,
469459
)
470460
};
471461
}

payjoin/src/uri.rs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::borrow::Cow;
22
use std::convert::TryFrom;
3+
use std::str::FromStr;
34

45
use bitcoin::address::{Error, NetworkChecked, NetworkUnchecked};
56
use bitcoin::{Address, Amount, Network};
67
use url::Url;
78

9+
use crate::v2::DecodeOhttpKeysError;
810
#[cfg(feature = "v2")]
911
use crate::OhttpKeys;
1012

@@ -238,13 +240,10 @@ impl<'a> bip21::SerializeParams for &'a PayjoinExtras {
238240
("pjos", if self.disable_output_substitution { "1" } else { "0" }.to_string()),
239241
];
240242
#[cfg(feature = "v2")]
241-
if let Some(ohttp_keys) = self.ohttp_keys.clone().and_then(|c| c.encode().ok()) {
242-
let config =
243-
bitcoin::base64::Config::new(bitcoin::base64::CharacterSet::UrlSafe, false);
244-
let base64_ohttp_keys = bitcoin::base64::encode_config(ohttp_keys, config);
245-
params.push(("ohttp", base64_ohttp_keys));
243+
if let Some(ohttp) = self.ohttp_keys.clone().map(|ohttp_keys| ohttp_keys.to_string()) {
244+
params.push(("ohttp", ohttp));
246245
} else {
247-
log::warn!("Failed to encode ohttp config, ignoring");
246+
log::warn!("No ohttp keys found, ignoring");
248247
}
249248
params.into_iter()
250249
}
@@ -266,13 +265,10 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
266265
match key {
267266
#[cfg(feature = "v2")]
268267
"ohttp" if self.ohttp.is_none() => {
269-
let base64_config = Cow::try_from(value).map_err(InternalPjParseError::NotUtf8)?;
270-
let config_bytes =
271-
bitcoin::base64::decode_config(&*base64_config, bitcoin::base64::URL_SAFE)
272-
.map_err(InternalPjParseError::NotBase64)?;
273-
let config = OhttpKeys::decode(&config_bytes)
268+
let ohttp_encoded = Cow::try_from(value).map_err(InternalPjParseError::NotUtf8)?;
269+
let ohttp_keys = OhttpKeys::from_str(&ohttp_encoded)
274270
.map_err(InternalPjParseError::DecodeOhttpKeys)?;
275-
self.ohttp = Some(config);
271+
self.ohttp = Some(ohttp_keys);
276272
Ok(bip21::de::ParamKind::Known)
277273
}
278274
#[cfg(feature = "v2")]
@@ -332,8 +328,6 @@ impl std::fmt::Display for PjParseError {
332328
}
333329
InternalPjParseError::MissingEndpoint => write!(f, "Missing payjoin endpoint"),
334330
InternalPjParseError::NotUtf8(_) => write!(f, "Endpoint is not valid UTF-8"),
335-
#[cfg(feature = "v2")]
336-
InternalPjParseError::NotBase64(_) => write!(f, "ohttp config is not valid base64"),
337331
InternalPjParseError::BadEndpoint(_) => write!(f, "Endpoint is not valid"),
338332
#[cfg(feature = "v2")]
339333
InternalPjParseError::DecodeOhttpKeys(_) => write!(f, "ohttp config is not valid"),
@@ -350,11 +344,9 @@ enum InternalPjParseError {
350344
MultipleParams(&'static str),
351345
MissingEndpoint,
352346
NotUtf8(core::str::Utf8Error),
353-
#[cfg(feature = "v2")]
354-
NotBase64(bitcoin::base64::DecodeError),
355347
BadEndpoint(url::ParseError),
356348
#[cfg(feature = "v2")]
357-
DecodeOhttpKeys(ohttp::Error),
349+
DecodeOhttpKeys(DecodeOhttpKeysError),
358350
UnsecureEndpoint,
359351
}
360352

0 commit comments

Comments
 (0)