Skip to content

Commit 1fd2921

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 5a9d327 commit 1fd2921

File tree

9 files changed

+186
-69
lines changed

9 files changed

+186
-69
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: 4 additions & 7 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.
@@ -90,9 +91,7 @@ impl Enroller {
9091
}
9192

9293
fn subdir_path_from_pubkey(pubkey: &bitcoin::secp256k1::PublicKey) -> String {
93-
let pubkey = pubkey.serialize();
94-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
95-
base64::encode_config(pubkey, b64_config)
94+
encode_bech32_pubkey(&pubkey)
9695
}
9796

9897
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -261,10 +260,8 @@ impl Enrolled {
261260
}
262261

263262
pub fn fallback_target(&self) -> String {
264-
let pubkey = &self.s.public_key().serialize();
265-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
266-
let pubkey_base64 = base64::encode_config(pubkey, b64_config);
267-
format!("{}{}", &self.directory, pubkey_base64)
263+
let subdirectory = encode_bech32_pubkey(&self.s.public_key());
264+
format!("{}{}", &self.directory, subdirectory)
268265
}
269266
}
270267

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: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,10 @@ impl RequestContext {
380380
subdirectory = path_and_query;
381381
}
382382

383-
let b64_config =
384-
bitcoin::base64::Config::new(bitcoin::base64::CharacterSet::UrlSafe, false);
385-
let pubkey_bytes = bitcoin::base64::decode_config(subdirectory, b64_config)
386-
.map_err(InternalCreateRequestError::SubdirectoryNotBase64)?;
387-
Ok(bitcoin::secp256k1::PublicKey::from_slice(&pubkey_bytes)
383+
let rs = crate::v2::decode_bech32_pubkey(&subdirectory)
384+
.map_err(|_| InternalCreateRequestError::PubkeyEncoding)?;
385+
log::debug!("rs: {:?}", rs.len());
386+
Ok(bitcoin::secp256k1::PublicKey::from_slice(&rs)
388387
.map_err(InternalCreateRequestError::SubdirectoryInvalidPubkey)?)
389388
}
390389
}
@@ -398,12 +397,8 @@ impl Serialize for RequestContext {
398397
let mut state = serializer.serialize_struct("RequestContext", 8)?;
399398
state.serialize_field("psbt", &self.psbt.to_string())?;
400399
state.serialize_field("endpoint", &self.endpoint.as_str())?;
401-
let ohttp_string = self.ohttp_keys.as_ref().map_or(Ok("".to_string()), |config| {
402-
config
403-
.encode()
404-
.map_err(|e| serde::ser::Error::custom(format!("ohttp-keys encoding error: {}", e)))
405-
.map(bitcoin::base64::encode)
406-
})?;
400+
let ohttp_string =
401+
self.ohttp_keys.as_ref().map_or_else(|| "".to_string(), |keys| keys.to_string());
407402
state.serialize_field("ohttp_keys", &ohttp_string)?;
408403
state.serialize_field("disable_output_substitution", &self.disable_output_substitution)?;
409404
state.serialize_field(
@@ -474,17 +469,13 @@ impl<'de> Deserialize<'de> for RequestContext {
474469
.map_err(de::Error::custom)?,
475470
),
476471
"ohttp_keys" => {
477-
let ohttp_base64: String = map.next_value()?;
478-
ohttp_keys = if ohttp_base64.is_empty() {
472+
let ohttp_encoded: String = map.next_value()?;
473+
ohttp_keys = if ohttp_encoded.is_empty() {
479474
None
480475
} else {
481476
Some(
482-
crate::v2::OhttpKeys::decode(
483-
bitcoin::base64::decode(&ohttp_base64)
484-
.map_err(de::Error::custom)?
485-
.as_slice(),
486-
)
487-
.map_err(de::Error::custom)?,
477+
crate::v2::OhttpKeys::from_str(&ohttp_encoded)
478+
.map_err(de::Error::custom)?,
488479
)
489480
};
490481
}

payjoin/src/uri.rs

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use std::borrow::Cow;
22
use std::convert::TryFrom;
3+
#[cfg(feature = "v2")]
4+
use std::str::FromStr;
35

46
use bitcoin::address::{Error, NetworkChecked, NetworkUnchecked};
57
use bitcoin::{Address, Amount, Network};
68
use url::Url;
79

10+
#[cfg(feature = "v2")]
11+
use crate::v2::DecodeOhttpKeysError;
812
#[cfg(feature = "v2")]
913
use crate::OhttpKeys;
1014

@@ -238,13 +242,10 @@ impl<'a> bip21::SerializeParams for &'a PayjoinExtras {
238242
("pjos", if self.disable_output_substitution { "1" } else { "0" }.to_string()),
239243
];
240244
#[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));
245+
if let Some(ohttp) = self.ohttp_keys.clone().map(|ohttp_keys| ohttp_keys.to_string()) {
246+
params.push(("ohttp", ohttp));
246247
} else {
247-
log::warn!("Failed to encode ohttp config, ignoring");
248+
log::warn!("No ohttp keys found, ignoring");
248249
}
249250
params.into_iter()
250251
}
@@ -266,13 +267,10 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
266267
match key {
267268
#[cfg(feature = "v2")]
268269
"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)
270+
let ohttp_encoded = Cow::try_from(value).map_err(InternalPjParseError::NotUtf8)?;
271+
let ohttp_keys = OhttpKeys::from_str(&ohttp_encoded)
274272
.map_err(InternalPjParseError::DecodeOhttpKeys)?;
275-
self.ohttp = Some(config);
273+
self.ohttp = Some(ohttp_keys);
276274
Ok(bip21::de::ParamKind::Known)
277275
}
278276
#[cfg(feature = "v2")]
@@ -332,8 +330,6 @@ impl std::fmt::Display for PjParseError {
332330
}
333331
InternalPjParseError::MissingEndpoint => write!(f, "Missing payjoin endpoint"),
334332
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"),
337333
InternalPjParseError::BadEndpoint(_) => write!(f, "Endpoint is not valid"),
338334
#[cfg(feature = "v2")]
339335
InternalPjParseError::DecodeOhttpKeys(_) => write!(f, "ohttp config is not valid"),
@@ -350,11 +346,9 @@ enum InternalPjParseError {
350346
MultipleParams(&'static str),
351347
MissingEndpoint,
352348
NotUtf8(core::str::Utf8Error),
353-
#[cfg(feature = "v2")]
354-
NotBase64(bitcoin::base64::DecodeError),
355349
BadEndpoint(url::ParseError),
356350
#[cfg(feature = "v2")]
357-
DecodeOhttpKeys(ohttp::Error),
351+
DecodeOhttpKeys(DecodeOhttpKeysError),
358352
UnsecureEndpoint,
359353
}
360354

0 commit comments

Comments
 (0)