Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions payjoin/src/ohttp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,77 @@ pub fn ohttp_encapsulate(
Ok((buffer, ohttp_ctx))
}

#[derive(Debug)]
pub enum DirectoryResponseError {
InvalidSize(usize),
OhttpDecapsulation(OhttpEncapsulationError),
UnexpectedStatusCode(http::StatusCode),
}

impl fmt::Display for DirectoryResponseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use DirectoryResponseError::*;

match self {
OhttpDecapsulation(e) => write!(f, "OHTTP Decapsulation Error: {e}"),
InvalidSize(size) => write!(
f,
"Unexpected response size {}, expected {} bytes",
size,
crate::directory::ENCAPSULATED_MESSAGE_BYTES
),
UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
}
}
}

impl error::Error for DirectoryResponseError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use DirectoryResponseError::*;

match self {
OhttpDecapsulation(e) => Some(e),
InvalidSize(_) => None,
UnexpectedStatusCode(_) => None,
}
}
}

pub fn process_get_res(
res: &[u8],
ohttp_context: ohttp::ClientResponse,
) -> Result<Option<Vec<u8>>, DirectoryResponseError> {
let response = process_ohttp_res(res, ohttp_context)?;
match response.status() {
http::StatusCode::OK => Ok(Some(response.body().to_vec())),
http::StatusCode::ACCEPTED => Ok(None),
status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
}
}

pub fn process_post_res(
res: &[u8],
ohttp_context: ohttp::ClientResponse,
) -> Result<(), DirectoryResponseError> {
let response = process_ohttp_res(res, ohttp_context)?;
match response.status() {
http::StatusCode::OK => Ok(()),
status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
}
}

fn process_ohttp_res(
res: &[u8],
ohttp_context: ohttp::ClientResponse,
) -> Result<http::Response<Vec<u8>>, DirectoryResponseError> {
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
res.try_into().map_err(|_| DirectoryResponseError::InvalidSize(res.len()))?;
log::trace!("decapsulating directory response");
let res = ohttp_decapsulate(ohttp_context, response_array)
.map_err(DirectoryResponseError::OhttpDecapsulation)?;
Ok(res)
}

/// decapsulate ohttp, bhttp response and return http response body and status code
pub fn ohttp_decapsulate(
res_ctx: ohttp::ClientResponse,
Expand Down
19 changes: 5 additions & 14 deletions payjoin/src/receive/v2/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::error;

use super::Error::V2;
use crate::hpke::HpkeError;
use crate::ohttp::OhttpEncapsulationError;
use crate::ohttp::{DirectoryResponseError, OhttpEncapsulationError};
use crate::receive::error::Error;

/// Error that may occur during a v2 session typestate change
Expand Down Expand Up @@ -31,10 +31,8 @@ pub(crate) enum InternalSessionError {
OhttpEncapsulation(OhttpEncapsulationError),
/// Hybrid Public Key Encryption failed
Hpke(HpkeError),
/// Unexpected response size
UnexpectedResponseSize(usize),
/// Unexpected status code
UnexpectedStatusCode(http::StatusCode),
/// The directory returned a bad response
DirectoryResponse(DirectoryResponseError),
}

impl From<OhttpEncapsulationError> for Error {
Expand All @@ -56,13 +54,7 @@ impl fmt::Display for SessionError {
Expired(expiry) => write!(f, "Session expired at {expiry:?}"),
OhttpEncapsulation(e) => write!(f, "OHTTP Encapsulation Error: {e}"),
Hpke(e) => write!(f, "Hpke decryption failed: {e}"),
UnexpectedResponseSize(size) => write!(
f,
"Unexpected response size {}, expected {} bytes",
size,
crate::directory::ENCAPSULATED_MESSAGE_BYTES
),
UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
DirectoryResponse(e) => write!(f, "Directory response error: {e}"),
}
}
}
Expand All @@ -76,8 +68,7 @@ impl error::Error for SessionError {
Expired(_) => None,
OhttpEncapsulation(e) => Some(e),
Hpke(e) => Some(e),
UnexpectedResponseSize(_) => None,
UnexpectedStatusCode(_) => None,
DirectoryResponse(e) => Some(e),
}
}
}
49 changes: 15 additions & 34 deletions payjoin/src/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ use super::{
v1, InternalPayloadError, JsonReply, OutputSubstitutionError, ReplyableError, SelectionError,
};
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate, OhttpEncapsulationError, OhttpKeys};
use crate::ohttp::{
ohttp_encapsulate, process_get_res, process_post_res, OhttpEncapsulationError, OhttpKeys,
};
use crate::output_substitution::OutputSubstitution;
use crate::persist::Persister;
use crate::receive::{parse_payload, InputPair};
Expand Down Expand Up @@ -180,23 +182,17 @@ impl Receiver<WithContext> {
body: &[u8],
context: ohttp::ClientResponse,
) -> Result<Option<Receiver<UncheckedProposal>>, Error> {
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
body.try_into()
.map_err(|_| InternalSessionError::UnexpectedResponseSize(body.len()))?;
log::trace!("decapsulating directory response");
let response = ohttp_decapsulate(context, response_array)
.map_err(InternalSessionError::OhttpEncapsulation)?;
if response.body().is_empty() {
log::debug!("response is empty");
return Ok(None);
}
match String::from_utf8(response.body().to_vec()) {
let body = match process_get_res(body, context)
.map_err(InternalSessionError::DirectoryResponse)?
{
Some(body) => body,
None => return Ok(None),
};
match String::from_utf8(body.clone()) {
// V1 response bodies are utf8 plaintext
Ok(response) => Ok(Some(Receiver { state: self.extract_proposal_from_v1(response)? })),
// V2 response bodies are encrypted binary
Err(_) => Ok(Some(Receiver {
state: self.extract_proposal_from_v2(response.body().to_vec())?,
})),
Err(_) => Ok(Some(Receiver { state: self.extract_proposal_from_v2(body)? })),
}
}

Expand Down Expand Up @@ -350,16 +346,8 @@ impl Receiver<UncheckedProposal> {
body: &[u8],
context: ohttp::ClientResponse,
) -> Result<(), SessionError> {
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
body.try_into()
.map_err(|_| InternalSessionError::UnexpectedResponseSize(body.len()))?;
let response = ohttp_decapsulate(context, response_array)
.map_err(InternalSessionError::OhttpEncapsulation)?;

match response.status() {
http::StatusCode::OK => Ok(()),
_ => Err(InternalSessionError::UnexpectedStatusCode(response.status()).into()),
}
process_post_res(body, context)
.map_err(|e| InternalSessionError::DirectoryResponse(e).into())
}
}

Expand Down Expand Up @@ -648,15 +636,8 @@ impl Receiver<PayjoinProposal> {
res: &[u8],
ohttp_context: ohttp::ClientResponse,
) -> Result<(), Error> {
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
res.try_into().map_err(|_| InternalSessionError::UnexpectedResponseSize(res.len()))?;
let res = ohttp_decapsulate(ohttp_context, response_array)
.map_err(InternalSessionError::OhttpEncapsulation)?;
if res.status().is_success() {
Ok(())
} else {
Err(InternalSessionError::UnexpectedStatusCode(res.status()).into())
}
process_post_res(res, ohttp_context)
.map_err(|e| InternalSessionError::DirectoryResponse(e).into())
}
}

Expand Down
21 changes: 6 additions & 15 deletions payjoin/src/send/v2/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use core::fmt;

use crate::ohttp::DirectoryResponseError;
use crate::uri::url_ext::ParseReceiverPubkeyParamError;

/// Error returned when request could not be created.
Expand Down Expand Up @@ -73,25 +74,19 @@ pub struct EncapsulationError(InternalEncapsulationError);

#[derive(Debug)]
pub(crate) enum InternalEncapsulationError {
/// The response size is not the expected size.
InvalidSize(usize),
/// The status code is not the expected status code.
UnexpectedStatusCode(http::StatusCode),
/// The HPKE failed.
Hpke(crate::hpke::HpkeError),
/// The encapsulation failed.
Ohttp(crate::ohttp::OhttpEncapsulationError),
/// The directory returned a bad response
DirectoryResponse(DirectoryResponseError),
}

impl fmt::Display for EncapsulationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use InternalEncapsulationError::*;

match &self.0 {
InvalidSize(size) => write!(f, "invalid size: {size}"),
UnexpectedStatusCode(status) => write!(f, "unexpected status code: {status}"),
Ohttp(error) => write!(f, "OHTTP encapsulation error: {error}"),
Hpke(error) => write!(f, "HPKE error: {error}"),
DirectoryResponse(e) => write!(f, "Directory response error: {e}"),
}
}
}
Expand All @@ -101,10 +96,8 @@ impl std::error::Error for EncapsulationError {
use InternalEncapsulationError::*;

match &self.0 {
InvalidSize(_) => None,
UnexpectedStatusCode(_) => None,
Ohttp(error) => Some(error),
Hpke(error) => Some(error),
DirectoryResponse(e) => Some(e),
}
}
}
Expand All @@ -115,8 +108,6 @@ impl From<InternalEncapsulationError> for EncapsulationError {

impl From<InternalEncapsulationError> for super::ResponseError {
fn from(value: InternalEncapsulationError) -> Self {
super::ResponseError::Validation(
super::InternalValidationError::V2Encapsulation(value.into()).into(),
)
super::InternalValidationError::V2Encapsulation(value.into()).into()
}
}
44 changes: 15 additions & 29 deletions payjoin/src/send/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use url::Url;
use super::error::BuildSenderError;
use super::*;
use crate::hpke::{decrypt_message_b, encrypt_message_a, HpkeSecretKey};
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate};
use crate::ohttp::{ohttp_encapsulate, process_get_res, process_post_res};
use crate::persist::Persister;
use crate::send::v1;
use crate::uri::{ShortId, UrlExt};
Expand Down Expand Up @@ -325,24 +325,15 @@ impl Sender<V2PostContext> {
self,
response: &[u8],
) -> Result<Sender<V2GetContext>, EncapsulationError> {
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] = response
.try_into()
.map_err(|_| InternalEncapsulationError::InvalidSize(response.len()))?;
let response = ohttp_decapsulate(self.state.ohttp_ctx, response_array)
.map_err(InternalEncapsulationError::Ohttp)?;
match response.status() {
http::StatusCode::OK => {
// return OK with new Typestate
Ok(Sender {
state: V2GetContext {
endpoint: self.state.endpoint,
psbt_ctx: self.state.psbt_ctx,
hpke_ctx: self.state.hpke_ctx,
},
})
}
_ => Err(InternalEncapsulationError::UnexpectedStatusCode(response.status()))?,
}
process_post_res(response, self.state.ohttp_ctx)
.map_err(InternalEncapsulationError::DirectoryResponse)?;
Ok(Sender {
state: V2GetContext {
endpoint: self.state.endpoint,
psbt_ctx: self.state.psbt_ctx,
hpke_ctx: self.state.hpke_ctx,
},
})
}
}

Expand Down Expand Up @@ -404,16 +395,11 @@ impl Sender<V2GetContext> {
response: &[u8],
ohttp_ctx: ohttp::ClientResponse,
) -> Result<Option<Psbt>, ResponseError> {
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] = response
.try_into()
.map_err(|_| InternalEncapsulationError::InvalidSize(response.len()))?;

let response = ohttp_decapsulate(ohttp_ctx, response_array)
.map_err(InternalEncapsulationError::Ohttp)?;
let body = match response.status() {
http::StatusCode::OK => response.body().to_vec(),
http::StatusCode::ACCEPTED => return Ok(None),
_ => return Err(InternalEncapsulationError::UnexpectedStatusCode(response.status()))?,
let body = match process_get_res(response, ohttp_ctx)
.map_err(InternalEncapsulationError::DirectoryResponse)?
{
Some(body) => body,
None => return Ok(None),
};
let psbt = decrypt_message_b(
&body,
Expand Down