From fa6843762672f2d31421899018d7c70b0fa58cc3 Mon Sep 17 00:00:00 2001 From: oyindamola oladapo Date: Thu, 11 Dec 2025 13:09:41 +0100 Subject: [PATCH 1/2] Audit serialization trait implementations Remove unintended serde derives from types that shouldn't be serializable by default. Conditionally enable serialization behind feature flag to prevent accidental API exposure through serialization. --- payjoin-ffi/src/receive/mod.rs | 4 ++-- payjoin-ffi/src/send/mod.rs | 2 +- payjoin/src/core/receive/error.rs | 3 ++- payjoin/src/core/receive/v2/mod.rs | 20 +++++++++++--------- payjoin/src/core/receive/v2/session.rs | 8 ++++---- payjoin/src/core/send/v2/mod.rs | 10 +++++----- payjoin/src/core/send/v2/session.rs | 6 ++++-- 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/payjoin-ffi/src/receive/mod.rs b/payjoin-ffi/src/receive/mod.rs index 963001463..c3c091a9b 100644 --- a/payjoin-ffi/src/receive/mod.rs +++ b/payjoin-ffi/src/receive/mod.rs @@ -43,7 +43,7 @@ macro_rules! impl_save_for_transition { }; } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Object)] +#[derive(Debug, Clone, uniffi::Object)] pub struct ReceiverSessionEvent(payjoin::receive::v2::SessionEvent); impl From for ReceiverSessionEvent { @@ -385,7 +385,7 @@ impl From for payjoin::receive::v2::ReceiverBuilder { fn from(value: ReceiverBuilder) -> Self { value.0 } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, uniffi::Object)] +#[derive(Clone, Debug, uniffi::Object)] pub struct Initialized(payjoin::receive::v2::Receiver); impl From for payjoin::receive::v2::Receiver { diff --git a/payjoin-ffi/src/send/mod.rs b/payjoin-ffi/src/send/mod.rs index fd6f7983c..612c916a8 100644 --- a/payjoin-ffi/src/send/mod.rs +++ b/payjoin-ffi/src/send/mod.rs @@ -32,7 +32,7 @@ macro_rules! impl_save_for_transition { }; } -#[derive(uniffi::Object, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(uniffi::Object, Debug, Clone)] pub struct SenderSessionEvent(payjoin::send::v2::SessionEvent); impl From for payjoin::send::v2::SessionEvent { diff --git a/payjoin/src/core/receive/error.rs b/payjoin/src/core/receive/error.rs index 637d9638c..00a246f5c 100644 --- a/payjoin/src/core/receive/error.rs +++ b/payjoin/src/core/receive/error.rs @@ -77,7 +77,8 @@ pub enum ProtocolError { /// "message": "Human readable error message" /// } /// ``` -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JsonReply { /// The error code error_code: ErrorCode, diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index 06ea44d44..c09df3116 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -24,7 +24,6 @@ //! Note: Even fresh requests may be linkable via metadata (e.g. client IP, request timing), //! but request reuse makes correlation trivial for the relay. -use std::str::FromStr; #[cfg(not(target_arch = "wasm32"))] use std::time::Duration; @@ -33,8 +32,6 @@ use bitcoin::psbt::Psbt; use bitcoin::{Address, Amount, FeeRate, OutPoint, Script, TxOut, Txid}; pub(crate) use error::InternalSessionError; pub use error::SessionError; -use serde::de::Deserializer; -use serde::{Deserialize, Serialize}; pub use session::{replay_event_log, SessionEvent, SessionHistory, SessionOutcome, SessionStatus}; use url::Url; #[cfg(target_arch = "wasm32")] @@ -66,9 +63,10 @@ const SUPPORTED_VERSIONS: &[Version] = &[Version::One, Version::Two]; static TWENTY_FOUR_HOURS_DEFAULT_EXPIRATION: Duration = Duration::from_secs(60 * 60 * 24); -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SessionContext { - #[serde(deserialize_with = "deserialize_address_assume_checked")] + #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_address_assume_checked"))] address: Address, directory: url::Url, ohttp_keys: OhttpKeys, @@ -110,10 +108,14 @@ impl SessionContext { } } +#[cfg(feature = "serde")] fn deserialize_address_assume_checked<'de, D>(deserializer: D) -> Result where - D: Deserializer<'de>, + D: serde::de::Deserializer<'de>, { + use std::str::FromStr; + + use serde::Deserialize; let s = String::deserialize(deserializer)?; let address = Address::from_str(&s).map_err(serde::de::Error::custom)?; Ok(address.assume_checked()) @@ -265,7 +267,7 @@ pub trait State: sealed::State {} /// various functions to accomplish the goals of the typestate, and one or more functions which /// will commit the changes/checks in the current typestate and move to the next one. For more /// information on the typestate pattern, see [The Typestate Pattern in Rust](https://cliffle.com/blog/rust-typestate/). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Receiver { /// Data associated with the current state of the receiver. pub(crate) state: State, @@ -342,7 +344,7 @@ impl ReceiverBuilder { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Initialized {} impl Receiver { @@ -1214,7 +1216,7 @@ impl Receiver { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Monitor { psbt_context: PsbtContext, } diff --git a/payjoin/src/core/receive/v2/session.rs b/payjoin/src/core/receive/v2/session.rs index c9703233f..b47ffb4f8 100644 --- a/payjoin/src/core/receive/v2/session.rs +++ b/payjoin/src/core/receive/v2/session.rs @@ -1,5 +1,3 @@ -use serde::{Deserialize, Serialize}; - use super::{ReceiveSession, SessionContext}; use crate::error::{InternalReplayError, ReplayError}; use crate::output_substitution::OutputSubstitution; @@ -142,7 +140,8 @@ pub enum SessionStatus { /// Represents a piece of information that the receiver has obtained from the session /// Each event can be used to transition the receiver state machine to a new state -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SessionEvent { Created(SessionContext), RetrievedOriginalPayload { original: OriginalPayload, reply_key: Option }, @@ -160,7 +159,8 @@ pub enum SessionEvent { } /// Represents all possible outcomes for a closed Payjoin session -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SessionOutcome { /// Payjoin completed successfully Success(Vec<(bitcoin::ScriptBuf, bitcoin::Witness)>), diff --git a/payjoin/src/core/send/v2/mod.rs b/payjoin/src/core/send/v2/mod.rs index c8fd1240c..d76e3a3e6 100644 --- a/payjoin/src/core/send/v2/mod.rs +++ b/payjoin/src/core/send/v2/mod.rs @@ -33,7 +33,6 @@ use bitcoin::Address; pub use error::{CreateRequestError, EncapsulationError}; use error::{InternalCreateRequestError, InternalEncapsulationError}; use ohttp::ClientResponse; -use serde::{Deserialize, Serialize}; pub use session::{replay_event_log, SessionEvent, SessionHistory, SessionOutcome, SessionStatus}; use url::Url; @@ -190,13 +189,14 @@ mod sealed { /// can implement this trait, ensuring type safety and protocol integrity. pub trait State: sealed::State {} -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Sender { pub(crate) state: State, pub(crate) session_context: SessionContext, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SessionContext { /// The endpoint in the Payjoin URI pub(crate) pj_param: PjParam, @@ -278,7 +278,7 @@ impl SendSession { /// A payjoin V2 sender, allowing the construction of a payjoin V2 request /// and the resulting [`ClientResponse`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct WithReplyKey; impl Sender { @@ -412,7 +412,7 @@ pub(crate) fn serialize_v2_body( /// /// This type is used to make a BIP77 GET request and process the response. /// Call [`Sender::process_response`] on it to continue the BIP77 flow. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PollingForProposal; impl ResponseError { diff --git a/payjoin/src/core/send/v2/session.rs b/payjoin/src/core/send/v2/session.rs index dd881baaa..5162d0b4e 100644 --- a/payjoin/src/core/send/v2/session.rs +++ b/payjoin/src/core/send/v2/session.rs @@ -103,7 +103,8 @@ pub enum SessionStatus { Completed, } -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SessionEvent { /// Sender was created with session data Created(Box), @@ -114,7 +115,8 @@ pub enum SessionEvent { } /// Represents all possible outcomes for a closed Payjoin session -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SessionOutcome { /// Successful payjoin Success(bitcoin::Psbt), From 9c33ef17364940d75399097a6615cd43c80cfc7d Mon Sep 17 00:00:00 2001 From: oyindamola oladapo Date: Tue, 30 Dec 2025 17:50:16 +0100 Subject: [PATCH 2/2] restore previous serde usage in payjoin --- payjoin/src/core/receive/error.rs | 3 +-- payjoin/src/core/receive/v2/mod.rs | 20 +++++++++----------- payjoin/src/core/receive/v2/session.rs | 8 ++++---- payjoin/src/core/send/v2/mod.rs | 10 +++++----- payjoin/src/core/send/v2/session.rs | 6 ++---- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/payjoin/src/core/receive/error.rs b/payjoin/src/core/receive/error.rs index 00a246f5c..637d9638c 100644 --- a/payjoin/src/core/receive/error.rs +++ b/payjoin/src/core/receive/error.rs @@ -77,8 +77,7 @@ pub enum ProtocolError { /// "message": "Human readable error message" /// } /// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct JsonReply { /// The error code error_code: ErrorCode, diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index c09df3116..06ea44d44 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -24,6 +24,7 @@ //! Note: Even fresh requests may be linkable via metadata (e.g. client IP, request timing), //! but request reuse makes correlation trivial for the relay. +use std::str::FromStr; #[cfg(not(target_arch = "wasm32"))] use std::time::Duration; @@ -32,6 +33,8 @@ use bitcoin::psbt::Psbt; use bitcoin::{Address, Amount, FeeRate, OutPoint, Script, TxOut, Txid}; pub(crate) use error::InternalSessionError; pub use error::SessionError; +use serde::de::Deserializer; +use serde::{Deserialize, Serialize}; pub use session::{replay_event_log, SessionEvent, SessionHistory, SessionOutcome, SessionStatus}; use url::Url; #[cfg(target_arch = "wasm32")] @@ -63,10 +66,9 @@ const SUPPORTED_VERSIONS: &[Version] = &[Version::One, Version::Two]; static TWENTY_FOUR_HOURS_DEFAULT_EXPIRATION: Duration = Duration::from_secs(60 * 60 * 24); -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SessionContext { - #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_address_assume_checked"))] + #[serde(deserialize_with = "deserialize_address_assume_checked")] address: Address, directory: url::Url, ohttp_keys: OhttpKeys, @@ -108,14 +110,10 @@ impl SessionContext { } } -#[cfg(feature = "serde")] fn deserialize_address_assume_checked<'de, D>(deserializer: D) -> Result where - D: serde::de::Deserializer<'de>, + D: Deserializer<'de>, { - use std::str::FromStr; - - use serde::Deserialize; let s = String::deserialize(deserializer)?; let address = Address::from_str(&s).map_err(serde::de::Error::custom)?; Ok(address.assume_checked()) @@ -267,7 +265,7 @@ pub trait State: sealed::State {} /// various functions to accomplish the goals of the typestate, and one or more functions which /// will commit the changes/checks in the current typestate and move to the next one. For more /// information on the typestate pattern, see [The Typestate Pattern in Rust](https://cliffle.com/blog/rust-typestate/). -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Receiver { /// Data associated with the current state of the receiver. pub(crate) state: State, @@ -344,7 +342,7 @@ impl ReceiverBuilder { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Initialized {} impl Receiver { @@ -1216,7 +1214,7 @@ impl Receiver { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Monitor { psbt_context: PsbtContext, } diff --git a/payjoin/src/core/receive/v2/session.rs b/payjoin/src/core/receive/v2/session.rs index b47ffb4f8..c9703233f 100644 --- a/payjoin/src/core/receive/v2/session.rs +++ b/payjoin/src/core/receive/v2/session.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + use super::{ReceiveSession, SessionContext}; use crate::error::{InternalReplayError, ReplayError}; use crate::output_substitution::OutputSubstitution; @@ -140,8 +142,7 @@ pub enum SessionStatus { /// Represents a piece of information that the receiver has obtained from the session /// Each event can be used to transition the receiver state machine to a new state -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum SessionEvent { Created(SessionContext), RetrievedOriginalPayload { original: OriginalPayload, reply_key: Option }, @@ -159,8 +160,7 @@ pub enum SessionEvent { } /// Represents all possible outcomes for a closed Payjoin session -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum SessionOutcome { /// Payjoin completed successfully Success(Vec<(bitcoin::ScriptBuf, bitcoin::Witness)>), diff --git a/payjoin/src/core/send/v2/mod.rs b/payjoin/src/core/send/v2/mod.rs index d76e3a3e6..c8fd1240c 100644 --- a/payjoin/src/core/send/v2/mod.rs +++ b/payjoin/src/core/send/v2/mod.rs @@ -33,6 +33,7 @@ use bitcoin::Address; pub use error::{CreateRequestError, EncapsulationError}; use error::{InternalCreateRequestError, InternalEncapsulationError}; use ohttp::ClientResponse; +use serde::{Deserialize, Serialize}; pub use session::{replay_event_log, SessionEvent, SessionHistory, SessionOutcome, SessionStatus}; use url::Url; @@ -189,14 +190,13 @@ mod sealed { /// can implement this trait, ensuring type safety and protocol integrity. pub trait State: sealed::State {} -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Sender { pub(crate) state: State, pub(crate) session_context: SessionContext, } -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SessionContext { /// The endpoint in the Payjoin URI pub(crate) pj_param: PjParam, @@ -278,7 +278,7 @@ impl SendSession { /// A payjoin V2 sender, allowing the construction of a payjoin V2 request /// and the resulting [`ClientResponse`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WithReplyKey; impl Sender { @@ -412,7 +412,7 @@ pub(crate) fn serialize_v2_body( /// /// This type is used to make a BIP77 GET request and process the response. /// Call [`Sender::process_response`] on it to continue the BIP77 flow. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PollingForProposal; impl ResponseError { diff --git a/payjoin/src/core/send/v2/session.rs b/payjoin/src/core/send/v2/session.rs index 5162d0b4e..dd881baaa 100644 --- a/payjoin/src/core/send/v2/session.rs +++ b/payjoin/src/core/send/v2/session.rs @@ -103,8 +103,7 @@ pub enum SessionStatus { Completed, } -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SessionEvent { /// Sender was created with session data Created(Box), @@ -115,8 +114,7 @@ pub enum SessionEvent { } /// Represents all possible outcomes for a closed Payjoin session -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub enum SessionOutcome { /// Successful payjoin Success(bitcoin::Psbt),