From b46dac1489f15a6cf858638411e2fc32790098c2 Mon Sep 17 00:00:00 2001 From: DanGould Date: Sat, 30 Sep 2023 13:17:13 -0400 Subject: [PATCH] Handle v2 errors --- payjoin-cli/src/app.rs | 15 ++-- payjoin-relay/src/main.rs | 52 +++++++------ payjoin/src/receive/error.rs | 16 ++++ payjoin/src/receive/mod.rs | 35 +++++---- payjoin/src/send/error.rs | 44 +++++++++-- payjoin/src/send/mod.rs | 16 ++-- payjoin/src/v2.rs | 138 +++++++++++++++++++++++++++-------- 7 files changed, 232 insertions(+), 84 deletions(-) diff --git a/payjoin-cli/src/app.rs b/payjoin-cli/src/app.rs index 062406e9..f984d4ce 100644 --- a/payjoin-cli/src/app.rs +++ b/payjoin-cli/src/app.rs @@ -92,13 +92,15 @@ impl App { &self, client: &reqwest::Client, enroll_context: &mut EnrollContext, - ) -> Result { + ) -> Result { loop { - let (enroll_body, context) = enroll_context.enroll_body(); + let (enroll_body, context) = enroll_context.enroll_body()?; let ohttp_response = client.post(&self.config.ohttp_proxy).body(enroll_body).send().await?; let ohttp_response = ohttp_response.bytes().await?; - let proposal = enroll_context.parse_proposal(ohttp_response.as_ref(), context).unwrap(); + let proposal = enroll_context + .parse_proposal(ohttp_response.as_ref(), context) + .map_err(|e| anyhow!("parse error {}", e))?; match proposal { Some(proposal) => return Ok(proposal), None => tokio::time::sleep(std::time::Duration::from_secs(5)).await, @@ -238,8 +240,11 @@ impl App { .map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?; let receive_endpoint = format!("{}/{}", self.config.pj_endpoint, context.receive_subdir()); - let (body, ohttp_ctx) = - payjoin_proposal.extract_v2_req(&self.config.ohttp_config, &receive_endpoint); + let ohttp_config = + bitcoin::base64::decode_config(&self.config.ohttp_config, base64::URL_SAFE)?; + let (body, ohttp_ctx) = payjoin_proposal + .extract_v2_req(&ohttp_config, &receive_endpoint) + .map_err(|e| anyhow!("v2 req extraction failed {}", e))?; let res = client .post(&self.config.ohttp_proxy) .body(body) diff --git a/payjoin-relay/src/main.rs b/payjoin-relay/src/main.rs index 3c47059c..7af24bf8 100644 --- a/payjoin-relay/src/main.rs +++ b/payjoin-relay/src/main.rs @@ -68,8 +68,8 @@ fn init_ohttp() -> Result { &[SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)]; // create or read from file - let server_config = ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); - let encoded_config = server_config.encode().unwrap(); + let server_config = ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))?; + let encoded_config = server_config.encode()?; let b64_config = payjoin::bitcoin::base64::encode_config( &encoded_config, payjoin::bitcoin::base64::Config::new( @@ -83,43 +83,53 @@ fn init_ohttp() -> Result { async fn handle_ohttp( enc_request: Bytes, - mut target: Router, + target: Router, ohttp: Arc, ) -> (StatusCode, Vec) { + match handle_ohttp_inner(enc_request, target, ohttp).await { + Ok(res) => res, + Err(e) => { + tracing::error!("ohttp error: {:?}", e); + (StatusCode::INTERNAL_SERVER_ERROR, vec![]) + } + } +} + +async fn handle_ohttp_inner( + enc_request: Bytes, + mut target: Router, + ohttp: Arc, +) -> Result<(StatusCode, Vec)> { use axum::body::Body; use http::Uri; use tower_service::Service; - // decapsulate - let (bhttp_req, res_ctx) = ohttp.decapsulate(&enc_request).unwrap(); + let (bhttp_req, res_ctx) = ohttp.decapsulate(&enc_request)?; let mut cursor = std::io::Cursor::new(bhttp_req); - let req = bhttp::Message::read_bhttp(&mut cursor).unwrap(); - // let parsed_request: httparse::Request = httparse::Request::new(&mut vec![]).parse(cursor).unwrap(); - // // handle request - // Request::new + let req = bhttp::Message::read_bhttp(&mut cursor)?; let uri = Uri::builder() - .scheme(req.control().scheme().unwrap()) - .authority(req.control().authority().unwrap()) - .path_and_query(req.control().path().unwrap()) - .build() - .unwrap(); + .scheme(req.control().scheme().unwrap_or_default()) + .authority(req.control().authority().unwrap_or_default()) + .path_and_query(req.control().path().unwrap_or_default()) + .build()?; let body = req.content().to_vec(); - let mut request = Request::builder().uri(uri).method(req.control().method().unwrap()); + let mut request = + Request::builder().uri(uri).method(req.control().method().unwrap_or_default()); for header in req.header().fields() { request = request.header(header.name(), header.value()) } - let request = request.body(Body::from(body)).unwrap(); + let request = request.body(Body::from(body))?; - let response = target.call(request).await.unwrap(); + let response = target.call(request).await?; let (parts, body) = response.into_parts(); let mut bhttp_res = bhttp::Message::response(parts.status.as_u16()); - let full_body = hyper::body::to_bytes(body).await.unwrap(); + let full_body = hyper::body::to_bytes(body).await?; bhttp_res.write_content(&full_body); let mut bhttp_bytes = Vec::new(); - bhttp_res.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_bytes).unwrap(); - let ohttp_res = res_ctx.encapsulate(&bhttp_bytes).unwrap(); - (StatusCode::OK, ohttp_res) + bhttp_res.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_bytes)?; + let ohttp_res = res_ctx.encapsulate(&bhttp_bytes)?; + Ok((StatusCode::OK, ohttp_res)) } fn ohttp_config(server: &ohttp::Server) -> Result { diff --git a/payjoin/src/receive/error.rs b/payjoin/src/receive/error.rs index d9dbc9e3..dcbe6ec8 100644 --- a/payjoin/src/receive/error.rs +++ b/payjoin/src/receive/error.rs @@ -7,6 +7,9 @@ pub enum Error { BadRequest(RequestError), // To be returned as HTTP 500 Server(Box), + // V2 d/encapsulation failed + #[cfg(feature = "v2")] + V2(crate::v2::Error), } impl fmt::Display for Error { @@ -14,6 +17,8 @@ impl fmt::Display for Error { match &self { Self::BadRequest(e) => e.fmt(f), Self::Server(e) => write!(f, "Internal Server Error: {}", e), + #[cfg(feature = "v2")] + Self::V2(e) => e.fmt(f), } } } @@ -23,6 +28,8 @@ impl error::Error for Error { match &self { Self::BadRequest(_) => None, Self::Server(e) => Some(e.as_ref()), + #[cfg(feature = "v2")] + Self::V2(e) => Some(e), } } } @@ -31,6 +38,15 @@ impl From for Error { fn from(e: RequestError) -> Self { Error::BadRequest(e) } } +impl From for Error { + fn from(e: InternalRequestError) -> Self { Error::BadRequest(e.into()) } +} + +impl From for Error { + #[cfg(feature = "v2")] + fn from(e: crate::v2::Error) -> Self { Error::V2(e) } +} + /// Error that may occur when the request from sender is malformed. /// /// This is currently opaque type because we aren't sure which variants will stay. diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index e2c26f04..e76b558b 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -285,6 +285,7 @@ use url::Url; use crate::input_type::InputType; use crate::optional_parameters::Params; use crate::psbt::PsbtExt; +use crate::v2; pub trait Headers { fn get_header(&self, key: &str) -> Option<&str>; @@ -329,30 +330,28 @@ impl EnrollContext { format!("{}/{}", self.subdirectory(), crate::v2::RECEIVE) } - pub fn enroll_body(&mut self) -> (Vec, ohttp::ClientResponse) { + pub fn enroll_body(&mut self) -> Result<(Vec, ohttp::ClientResponse), crate::v2::Error> { let receive_endpoint = self.receive_subdir(); log::debug!("{}{}", self.relay_url.as_str(), receive_endpoint); - let (ohttp_req, ctx) = crate::v2::ohttp_encapsulate( + crate::v2::ohttp_encapsulate( &self.ohttp_config, "GET", format!("{}{}", self.relay_url.as_str(), receive_endpoint).as_str(), None, - ); - - (ohttp_req, ctx) + ) } pub fn parse_proposal( &self, encrypted_proposal: &[u8], context: ohttp::ClientResponse, - ) -> Result, RequestError> { - let response = crate::v2::ohttp_decapsulate(context, &encrypted_proposal); + ) -> Result, Error> { + let response = crate::v2::ohttp_decapsulate(context, &encrypted_proposal)?; if response.is_empty() { log::debug!("response is empty"); return Ok(None); } - let (proposal, e) = crate::v2::decrypt_message_a(&response, self.s.secret_key()); + let (proposal, e) = crate::v2::decrypt_message_a(&response, self.s.secret_key())?; let mut proposal = serde_json::from_slice::(&proposal) .map_err(InternalRequestError::Json)?; proposal.psbt = proposal.psbt.validate().map_err(InternalRequestError::InconsistentPsbt)?; @@ -684,20 +683,26 @@ impl PayjoinProposal { #[cfg(feature = "v2")] pub fn extract_v2_req( &self, - ohttp_config: &str, + ohttp_config: &Vec, receive_endpoint: &str, - ) -> (Vec, ohttp::ClientResponse) { + ) -> Result<(Vec, ohttp::ClientResponse), Error> { let e = self.v2_context.unwrap(); // TODO make v2 only let mut payjoin_bytes = self.payjoin_psbt.serialize(); - let body = crate::v2::encrypt_message_b(&mut payjoin_bytes, e); - let ohttp_config = bitcoin::base64::decode_config(ohttp_config, base64::URL_SAFE).unwrap(); + let body = crate::v2::encrypt_message_b(&mut payjoin_bytes, e)?; dbg!(receive_endpoint); - crate::v2::ohttp_encapsulate(&ohttp_config, "POST", receive_endpoint, Some(&body)) + let (req, ctx) = + crate::v2::ohttp_encapsulate(&ohttp_config, "POST", receive_endpoint, Some(&body))?; + Ok((req, ctx)) } - pub fn deserialize_res(&self, res: Vec, ohttp_context: ohttp::ClientResponse) -> Vec { + pub fn deserialize_res( + &self, + res: Vec, + ohttp_context: ohttp::ClientResponse, + ) -> Result, Error> { // display success or failure - crate::v2::ohttp_decapsulate(ohttp_context, &res) + let res = crate::v2::ohttp_decapsulate(ohttp_context, &res)?; + Ok(res) } } diff --git a/payjoin/src/send/error.rs b/payjoin/src/send/error.rs index 33e7ea50..6e70a822 100644 --- a/payjoin/src/send/error.rs +++ b/payjoin/src/send/error.rs @@ -16,13 +16,22 @@ pub struct ValidationError { #[derive(Debug)] pub(crate) enum InternalValidationError { - Psbt(bitcoin::psbt::PsbtParseError), + PsbtParse(bitcoin::psbt::PsbtParseError), Io(std::io::Error), InvalidInputType(InputTypeError), InvalidProposedInput(crate::psbt::PrevTxOutError), - VersionsDontMatch { proposed: i32, original: i32 }, - LockTimesDontMatch { proposed: LockTime, original: LockTime }, - SenderTxinSequenceChanged { proposed: Sequence, original: Sequence }, + VersionsDontMatch { + proposed: i32, + original: i32, + }, + LockTimesDontMatch { + proposed: LockTime, + original: LockTime, + }, + SenderTxinSequenceChanged { + proposed: Sequence, + original: Sequence, + }, SenderTxinContainsNonWitnessUtxo, SenderTxinContainsWitnessUtxo, SenderTxinContainsFinalScriptSig, @@ -32,7 +41,10 @@ pub(crate) enum InternalValidationError { ReceiverTxinNotFinalized, ReceiverTxinMissingUtxoInfo, MixedSequence, - MixedInputTypes { proposed: InputType, original: InputType }, + MixedInputTypes { + proposed: InputType, + original: InputType, + }, MissingOrShuffledInputs, TxOutContainsKeyPaths, FeeContributionExceedsMaximum, @@ -44,6 +56,10 @@ pub(crate) enum InternalValidationError { PayeeTookContributedFee, FeeContributionPaysOutputSizeIncrease, FeeRateBelowMinimum, + #[cfg(feature = "v2")] + V2(crate::v2::Error), + #[cfg(feature = "v2")] + Psbt(bitcoin::psbt::Error), } impl From for ValidationError { @@ -58,7 +74,7 @@ impl fmt::Display for ValidationError { use InternalValidationError::*; match &self.internal { - Psbt(e) => write!(f, "couldn't decode PSBT: {}", e), + PsbtParse(e) => write!(f, "couldn't decode PSBT: {}", e), Io(e) => write!(f, "couldn't read PSBT: {}", e), InvalidInputType(e) => write!(f, "invalid transaction input type: {}", e), InvalidProposedInput(e) => write!(f, "invalid proposed transaction input: {}", e), @@ -86,6 +102,10 @@ impl fmt::Display for ValidationError { PayeeTookContributedFee => write!(f, "payee tried to take fee contribution for himself"), FeeContributionPaysOutputSizeIncrease => write!(f, "fee contribution pays for additional outputs"), FeeRateBelowMinimum => write!(f, "the fee rate of proposed transaction is below minimum"), + #[cfg(feature = "v2")] + V2(e) => write!(f, "v2 error: {}", e), + #[cfg(feature = "v2")] + Psbt(e) => write!(f, "psbt error: {}", e), } } } @@ -95,7 +115,7 @@ impl std::error::Error for ValidationError { use InternalValidationError::*; match &self.internal { - Psbt(error) => Some(error), + PsbtParse(error) => Some(error), Io(error) => Some(error), InvalidInputType(error) => Some(error), InvalidProposedInput(error) => Some(error), @@ -123,6 +143,10 @@ impl std::error::Error for ValidationError { PayeeTookContributedFee => None, FeeContributionPaysOutputSizeIncrease => None, FeeRateBelowMinimum => None, + #[cfg(feature = "v2")] + V2(error) => Some(error), + #[cfg(feature = "v2")] + Psbt(error) => Some(error), } } } @@ -152,6 +176,8 @@ pub(crate) enum InternalCreateRequestError { UriDoesNotSupportPayjoin, PrevTxOut(crate::psbt::PrevTxOutError), InputType(crate::input_type::InputTypeError), + #[cfg(feature = "v2")] + V2(crate::v2::Error), } impl fmt::Display for CreateRequestError { @@ -174,6 +200,8 @@ impl fmt::Display for CreateRequestError { UriDoesNotSupportPayjoin => write!(f, "the URI does not support payjoin"), PrevTxOut(e) => write!(f, "invalid previous transaction output: {}", e), InputType(e) => write!(f, "invalid input type: {}", e), + #[cfg(feature = "v2")] + V2(e) => write!(f, "v2 error: {}", e), } } } @@ -198,6 +226,8 @@ impl std::error::Error for CreateRequestError { UriDoesNotSupportPayjoin => None, PrevTxOut(error) => Some(error), InputType(error) => Some(error), + #[cfg(feature = "v2")] + V2(error) => Some(error), } } } diff --git a/payjoin/src/send/mod.rs b/payjoin/src/send/mod.rs index edd1aa66..6386430b 100644 --- a/payjoin/src/send/mod.rs +++ b/payjoin/src/send/mod.rs @@ -402,13 +402,15 @@ impl<'a> RequestContext<'a> { self.fee_contribution, self.min_fee_rate, ); - let (body, e) = crate::v2::encrypt_message_a(&body, rs); + let (body, e) = + crate::v2::encrypt_message_a(&body, rs).map_err(InternalCreateRequestError::V2)?; let (body, ohttp_res) = crate::v2::ohttp_encapsulate( &self.uri.extras.ohttp_config.clone().unwrap().encode().unwrap(), "POST", &url.as_str(), Some(&body), - ); + ) + .map_err(InternalCreateRequestError::V2)?; log::debug!("ohttp_proxy_url: {:?}", ohttp_proxy_url); let url = Url::parse(ohttp_proxy_url).map_err(InternalCreateRequestError::Url)?; Ok(( @@ -503,12 +505,14 @@ impl ContextV2 { ) -> Result, ValidationError> { let mut res_buf = Vec::new(); response.read_to_end(&mut res_buf).map_err(InternalValidationError::Io)?; - let mut res_buf = crate::v2::ohttp_decapsulate(self.ohttp_res, &mut res_buf); - let psbt = crate::v2::decrypt_message_b(&mut res_buf, self.e); + let mut res_buf = crate::v2::ohttp_decapsulate(self.ohttp_res, &mut res_buf) + .map_err(InternalValidationError::V2)?; + let psbt = crate::v2::decrypt_message_b(&mut res_buf, self.e) + .map_err(InternalValidationError::V2)?; if psbt.is_empty() { return Ok(None); } - let proposal = Psbt::deserialize(&psbt).expect("PSBT deserialization failed"); + let proposal = Psbt::deserialize(&psbt).map_err(InternalValidationError::Psbt)?; let processed_proposal = self.context_v1.process_proposal(proposal)?; Ok(Some(processed_proposal)) } @@ -526,7 +530,7 @@ impl ContextV1 { ) -> Result { let mut res_str = String::new(); response.read_to_string(&mut res_str).map_err(InternalValidationError::Io)?; - let proposal = Psbt::from_str(&res_str).map_err(InternalValidationError::Psbt)?; + let proposal = Psbt::from_str(&res_str).map_err(InternalValidationError::PsbtParse)?; // process in non-generic function self.process_proposal(proposal).map(Into::into).map_err(Into::into) diff --git a/payjoin/src/v2.rs b/payjoin/src/v2.rs index 0d807e39..46ddce29 100644 --- a/payjoin/src/v2.rs +++ b/payjoin/src/v2.rs @@ -20,6 +20,8 @@ pub fn subdir(path: &str) -> String { pubkey_id } +use std::{error, fmt}; + use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use chacha20poly1305::aead::{Aead, KeyInit, OsRng, Payload}; @@ -30,18 +32,18 @@ use chacha20poly1305::{AeadCore, ChaCha20Poly1305, Nonce}; /// <- Receiver S /// -> Sender E, ES(payload), payload protected by knowledge of receiver key /// <- Receiver E, EE(payload), payload protected by knowledge of sender & receiver key -pub fn encrypt_message_a(msg: &[u8], s: PublicKey) -> (Vec, SecretKey) { +pub fn encrypt_message_a(msg: &[u8], s: PublicKey) -> Result<(Vec, SecretKey), Error> { let secp = Secp256k1::new(); let (e_sec, e_pub) = secp.generate_keypair(&mut OsRng); let es = SharedSecret::new(&s, &e_sec); - let cipher = - ChaCha20Poly1305::new_from_slice(&es.secret_bytes()).expect("cipher creation failed"); + let cipher = ChaCha20Poly1305::new_from_slice(&es.secret_bytes()) + .map_err(|_| InternalError::InvalidKeyLength)?; let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); // key es encrypts only 1 message so 0 is unique let aad = &e_pub.serialize(); let payload = Payload { msg, aad }; log::debug!("payload.msg: {:?}", payload.msg); log::debug!("payload.aad: {:?}", payload.aad); - let c_t: Vec = cipher.encrypt(&nonce, payload).expect("encryption failed"); + let c_t: Vec = cipher.encrypt(&nonce, payload)?; log::debug!("c_t: {:?}", c_t); // let ct_payload = Payload { // msg: &c_t[..], @@ -55,19 +57,19 @@ pub fn encrypt_message_a(msg: &[u8], s: PublicKey) -> (Vec, SecretKey) { message_a.extend(&nonce[..]); log::debug!("nonce: {:?}", nonce); message_a.extend(&c_t[..]); - (message_a, e_sec) + Ok((message_a, e_sec)) } -pub fn decrypt_message_a(message_a: &[u8], s: SecretKey) -> (Vec, PublicKey) { +pub fn decrypt_message_a(message_a: &[u8], s: SecretKey) -> Result<(Vec, PublicKey), Error> { // let message a = [pubkey/AD][nonce][authentication tag][ciphertext] - let e = PublicKey::from_slice(&message_a[..33]).expect("invalid public key"); + let e = PublicKey::from_slice(&message_a[..33])?; log::debug!("e: {:?}", e); let nonce = Nonce::from_slice(&message_a[33..45]); log::debug!("nonce: {:?}", nonce); let es = SharedSecret::new(&e, &s); log::debug!("es: {:?}", es); - let cipher = - ChaCha20Poly1305::new_from_slice(&es.secret_bytes()).expect("cipher creation failed"); + let cipher = ChaCha20Poly1305::new_from_slice(&es.secret_bytes()) + .map_err(|_| InternalError::InvalidKeyLength)?; let c_t = &message_a[45..]; let aad = &e.serialize(); log::debug!("c_t: {:?}", c_t); @@ -75,37 +77,37 @@ pub fn decrypt_message_a(message_a: &[u8], s: SecretKey) -> (Vec, PublicKey) let payload = Payload { msg: &c_t, aad }; log::debug!("payload.msg: {:?}", payload.msg); log::debug!("payload.aad: {:?}", payload.aad); - let buffer = cipher.decrypt(&nonce, payload).expect("decryption failed"); - (buffer, e) + let buffer = cipher.decrypt(&nonce, payload)?; + Ok((buffer, e)) } -pub fn encrypt_message_b(msg: &mut Vec, re_pub: PublicKey) -> Vec { +pub fn encrypt_message_b(msg: &mut Vec, re_pub: PublicKey) -> Result, Error> { // let message b = [pubkey/AD][nonce][authentication tag][ciphertext] let secp = Secp256k1::new(); let (e_sec, e_pub) = secp.generate_keypair(&mut OsRng); let ee = SharedSecret::new(&re_pub, &e_sec); - let cipher = - ChaCha20Poly1305::new_from_slice(&ee.secret_bytes()).expect("cipher creation failed"); + let cipher = ChaCha20Poly1305::new_from_slice(&ee.secret_bytes()) + .map_err(|_| InternalError::InvalidKeyLength)?; let nonce = Nonce::from_slice(&[0u8; 12]); // key es encrypts only 1 message so 0 is unique let aad = &e_pub.serialize(); let payload = Payload { msg, aad }; - let c_t = cipher.encrypt(nonce, payload).expect("encryption failed"); + let c_t = cipher.encrypt(nonce, payload)?; let mut message_b = e_pub.serialize().to_vec(); message_b.extend(&nonce[..]); message_b.extend(&c_t[..]); - message_b + Ok(message_b) } -pub fn decrypt_message_b(message_b: &mut Vec, e: SecretKey) -> Vec { +pub fn decrypt_message_b(message_b: &mut Vec, e: SecretKey) -> Result, Error> { // let message b = [pubkey/AD][nonce][authentication tag][ciphertext] - let re = PublicKey::from_slice(&message_b[..33]).expect("invalid public key"); + let re = PublicKey::from_slice(&message_b[..33])?; let nonce = Nonce::from_slice(&message_b[33..45]); let ee = SharedSecret::new(&re, &e); - let cipher = - ChaCha20Poly1305::new_from_slice(&ee.secret_bytes()).expect("cipher creation failed"); + let cipher = ChaCha20Poly1305::new_from_slice(&ee.secret_bytes()) + .map_err(|_| InternalError::InvalidKeyLength)?; let payload = Payload { msg: &message_b[45..], aad: &re.serialize() }; - let buffer = cipher.decrypt(&nonce, payload).expect("decryption failed"); - buffer + let buffer = cipher.decrypt(&nonce, payload)?; + Ok(buffer) } pub fn ohttp_encapsulate( @@ -113,9 +115,9 @@ pub fn ohttp_encapsulate( method: &str, url: &str, body: Option<&[u8]>, -) -> (Vec, ohttp::ClientResponse) { - let ctx = ohttp::ClientRequest::from_encoded_config(ohttp_config).unwrap(); - let url = url::Url::parse(url).expect("invalid url"); +) -> Result<(Vec, ohttp::ClientResponse), Error> { + let ctx = ohttp::ClientRequest::from_encoded_config(ohttp_config)?; + let url = url::Url::parse(url)?; let mut bhttp_message = bhttp::Message::request( method.as_bytes().to_vec(), url.scheme().as_bytes().to_vec(), @@ -127,13 +129,89 @@ pub fn ohttp_encapsulate( } let mut bhttp_req = Vec::new(); let _ = bhttp_message.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_req); - ctx.encapsulate(&bhttp_req).expect("encapsulation failed") + let encapsulated = ctx.encapsulate(&bhttp_req)?; + Ok(encapsulated) } /// decapsulate ohttp, bhttp response and return http response body and status code -pub fn ohttp_decapsulate(res_ctx: ohttp::ClientResponse, ohttp_body: &[u8]) -> Vec { - let bhttp_body = res_ctx.decapsulate(ohttp_body).expect("decapsulation failed"); +pub fn ohttp_decapsulate( + res_ctx: ohttp::ClientResponse, + ohttp_body: &[u8], +) -> Result, Error> { + let mut bhttp_body = res_ctx.decapsulate(ohttp_body)?; let mut r = std::io::Cursor::new(bhttp_body); - let response = bhttp::Message::read_bhttp(&mut r).expect("read bhttp failed"); - response.content().to_vec() + let response = bhttp::Message::read_bhttp(&mut r)?; + Ok(response.content().to_vec()) +} + +/// Error that may occur when de/encrypting or de/capsulating a v2 message. +/// +/// This is currently opaque type because we aren't sure which variants will stay. +/// You can only display it. +#[derive(Debug)] +pub struct Error(InternalError); + +#[derive(Debug)] +pub(crate) enum InternalError { + Ohttp(ohttp::Error), + Bhttp(bhttp::Error), + ParseUrl(url::ParseError), + Secp256k1(bitcoin::secp256k1::Error), + ChaCha20Poly1305(chacha20poly1305::aead::Error), + InvalidKeyLength, +} + +impl From for Error { + fn from(value: ohttp::Error) -> Self { Self(InternalError::Ohttp(value)) } +} + +impl From for Error { + fn from(value: bhttp::Error) -> Self { Self(InternalError::Bhttp(value)) } +} + +impl From for Error { + fn from(value: url::ParseError) -> Self { Self(InternalError::ParseUrl(value)) } +} + +impl From for Error { + fn from(value: bitcoin::secp256k1::Error) -> Self { Self(InternalError::Secp256k1(value)) } +} + +impl From for Error { + fn from(value: chacha20poly1305::aead::Error) -> Self { + Self(InternalError::ChaCha20Poly1305(value)) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use InternalError::*; + + match &self.0 { + Ohttp(e) => e.fmt(f), + Bhttp(e) => e.fmt(f), + ParseUrl(e) => e.fmt(f), + Secp256k1(e) => e.fmt(f), + ChaCha20Poly1305(e) => e.fmt(f), + InvalidKeyLength => write!(f, "Invalid Length"), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + use InternalError::*; + + match &self.0 { + Ohttp(e) => Some(e), + Bhttp(e) => Some(e), + ParseUrl(e) => Some(e), + Secp256k1(e) => Some(e), + ChaCha20Poly1305(_) | InvalidKeyLength => None, + } + } +} + +impl From for Error { + fn from(value: InternalError) -> Self { Self(value) } }