Skip to content

Conversation

@benalleng
Copy link
Collaborator

@benalleng benalleng commented Feb 4, 2025

We had a function that checked and then padded the length of the plaintext when running message encryption. Instead we should use a fixed array length and return an error if the message is of the wrong length.

I mentioned this is related to #402 but really that issue only has to do with public api calls

@benalleng benalleng marked this pull request as draft February 4, 2025 19:19
@benalleng benalleng force-pushed the remove-pad-plaintext branch from 717a1a9 to 18e6985 Compare February 5, 2025 14:32
@coveralls
Copy link
Collaborator

coveralls commented Feb 5, 2025

Pull Request Test Coverage Report for Build 17480395424

Details

  • 77 of 91 (84.62%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.007%) to 85.947%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin/src/core/send/v2/mod.rs 60 62 96.77%
payjoin/src/core/receive/error.rs 0 6 0.0%
payjoin/src/core/receive/v2/mod.rs 3 9 33.33%
Totals Coverage Status
Change from base Build 17446213698: 0.007%
Covered Lines: 8238
Relevant Lines: 9585

💛 - Coveralls

@benalleng benalleng force-pushed the remove-pad-plaintext branch 2 times, most recently from 81a3c3c to f00820b Compare February 5, 2025 15:14
@benalleng benalleng force-pushed the remove-pad-plaintext branch from f00820b to f92af6c Compare February 21, 2025 17:20
@arminsabouri arminsabouri added the enhancement New feature or request label Aug 21, 2025
@spacebear21
Copy link
Collaborator

@benalleng was there something blocking this PR?

@benalleng
Copy link
Collaborator Author

benalleng commented Aug 21, 2025

I need to come back to this, I totally forgot it existed. I think the issue I was having had to do with trying to simplify this down to an array with some dynamic length while also trying to stay true to this original comment from #405. https://github.com/DanGould/rust-payjoin/blob/4998108a1ac86bad14cdb9b2332a629f2e23b7e4/payjoin/src/hpke.rs#L247-L255

@benalleng benalleng force-pushed the remove-pad-plaintext branch 7 times, most recently from 7887f4c to b8a9582 Compare August 22, 2025 17:33
@benalleng
Copy link
Collaborator Author

A follow-up question, where is an appropriate place to actually pad this message to equal the array length of the PADDED_PLAINTEXT_A/B_LENGTH

@spacebear21
Copy link
Collaborator

A follow-up question, where is an appropriate place to actually pad this message to equal the array length of the PADDED_PLAINTEXT_A/B_LENGTH

I'm not sure I actually understand the motivation for this PR. To me encrypt_message_a/b does seem like the appropriate place to apply padding to a variable length message. Is there an issue that prompted this and provides more context?

/// Message A is sent from the sender to the receiver containing an Original PSBT payload
pub fn encrypt_message_a(
body: Vec<u8>,
body: &[u8; PADDED_PLAINTEXT_A_LENGTH],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spacebear21 I think the main motivation was just to get these function types to be strongly typed with this function signature. Other than that, I think it was a style thing to get rid of the function. How exactly it's implemented is not of the absolute greatest importance.

@spacebear21
Copy link
Collaborator

I guess the logical place to pad the message is right before encrypt_message_a/b are called in the respective create_x_request functions.

@benalleng benalleng force-pushed the remove-pad-plaintext branch from b8a9582 to 3d89517 Compare September 2, 2025 16:11
Copy link
Collaborator

@nothingmuch nothingmuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops i had a pending review comment from last week i never posted

@benalleng benalleng force-pushed the remove-pad-plaintext branch 2 times, most recently from a8e4477 to afe3076 Compare September 2, 2025 18:32
@benalleng benalleng marked this pull request as ready for review September 2, 2025 18:37
Comment on lines 395 to 404
if body_len > PADDED_PLAINTEXT_A_LENGTH {
return Err(CreateRequestError::from(InternalCreateRequestError::Hpke(
crate::hpke::HpkeError::PayloadSize {
actual: body_len,
expected: PADDED_PLAINTEXT_A_LENGTH,
},
)));
}

let _ = write!(&mut &mut buf[..], "{base64}\n{query_params}");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the main intention behind my suggestion was that length checking can be simplified, but even if not then at least the result should be .expect()ed instead of assigning to _

Suggested change
if body_len > PADDED_PLAINTEXT_A_LENGTH {
return Err(CreateRequestError::from(InternalCreateRequestError::Hpke(
crate::hpke::HpkeError::PayloadSize {
actual: body_len,
expected: PADDED_PLAINTEXT_A_LENGTH,
},
)));
}
let _ = write!(&mut &mut buf[..], "{base64}\n{query_params}");
write!(&mut &mut buf[..], "{base64}\n{query_params}").map_err(|e| match e.kind() {
std::io::ErrorKind::WriteZero => Err(CreateRequestError::from(InternalCreateRequestError::Hpke(
crate::hpke::HpkeError::PayloadSize {
actual: body_len,
expected: PADDED_PLAINTEXT_A_LENGTH,
},
_ => panic!("Uknown error: {e}"), // std::io::Write for &mut [u8] never fails, and write_all may fail if it didn't write all bytes
}))))?;

and if we eliminate actual from this errore, and FWIW i don't think PayloadSize is the correct representation of this failure. it makes sense for incoming data but for outgoing it's that the payload is too large, it's not that we expect it to be a certain size just under a certain size, then psbt's to_string can be used indirectly through fmt, eliminating more temp vars and length checks (since we won't need to calculate body_len at all)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe more readable: map_err(|e| { assert!(e.kind() == WriteZero; Err(...) })

@benalleng benalleng force-pushed the remove-pad-plaintext branch from afe3076 to 833a789 Compare September 3, 2025 18:35
@benalleng benalleng force-pushed the remove-pad-plaintext branch 2 times, most recently from a851044 to b841c19 Compare September 3, 2025 18:44
Comment on lines 1042 to 1044
Error::Implementation(ImplementationError::new(std::io::Error::other(
"failed to pad PSBT to PADDED_B_LENGTH",
)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem to be an error with the downstream Implementation. Do we need another variant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps, not really sure how to categorize this error as I'm not really sure what a failed to convert to array error would be in this case

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This try_into() is a Result since there needs to be a check to ensure that the incoming Vec<u8> from serialize() is the same length as PADDED_PLAINTEXT_B_LENGTH. Since we are resizing just before this do you think it is ok to panic if this fails?

let mut payjoin_bytes = self.psbt.serialize();
payjoin_bytes.resize(PADDED_PLAINTEXT_B_LENGTH, 0);
let plaintext_array: [u8; PADDED_PLAINTEXT_B_LENGTH] =
    payjoin_bytes.try_into().expect("Resized vec to array conversion should not fail");

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is some code an llm cooked up that skips the intermediate resizing entirely.

let plaintext_array = self
  .psbt
  .serialize()
  .into_iter()
  .chain(std::iter::repeat(0))
  .take(PADDED_PLAINTEXT_B_LENGTH)
  .collect::<Vec<_>>()
  .try_into()
  .expect("Resized vec to array conversion should not fail");

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am leaning more towards the first option for readability alone if we're ok with an expect here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not this suggestion? #525 (comment)

seems like if chaining "\n" and serialization of the params to that iter expression would get a bit much, if it's allocated as an array to the right size then just writing the PSBT followed by the params seems simplest to me, requires no expect, and clarifies the error condition to just the one relevant case (the serialized size of the psbt and the params was too large)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ok, i am not sure i fully understand where we get the params from in this stage in the receiver flow to properly build this same syntax here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, my bad, i mixed up the threads and thought this was about sender code. no \n or params in this one... nonetheless, i still think the simplest approach for both is write!(&mut &mut buf[..], "{psbt}") or write!(&mut &mut buf[..], "{psbt}\n{params}") and checking the Result for the length check

Copy link
Collaborator Author

@benalleng benalleng Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I have stumped myself on what seems to be something downstream from this but the sender accepts the buffer written as the raw psbt plaintext + padding, i.e.

cHNidP8BAFICAAAAAYZkepjEDPihy6ASG32F1d55chWrcPpNhkwPM+34gHr8AAAAAAD+////AZLxBSoBAAAAFgAU0ZXZmIg5DH3lYReEqMWoHb/s94sAAAAAAAEAmgIAAAACpKpk+qaExNhC1gUfjFf+/4HpwRDuK/Gjev+lQazVoeAAAAAAAP7////DmKKqh+wCyRLwpzhKrGvyCG7gskUIAhC10mUZLltf7gAAAAAA/v///wIA8gUqAQAAABYAFEIePkhXOqDf2b0+1WE7895FxfP2wOEFKgEAAAAWABSdggRMgtGkP+HEF95JoclVr/3wNmcAAAABAR8A8gUqAQAAABYAFEIePkhXOqDf2b0+1WE7895FxfP2AQhrAkcwRAIgaPMzXFsGOe2o6rsbwQ9MK/XiJ32r9B9KfCzVjLBaY1gCIHtibgdA+Ay3UerUkIlyDCgqbP5h1gnlBinv2tUa3sqVASEDjn1Mt9G9MsYDrbsKWnO6lYknOJjCEWbzi+lIumjVZcIAAA==...000000000000000000000...

However the reciever is only expecting the psbt serialized as it was before. Do we want to modify this to be able to accept psbt.to_string downstream like the sender?

I just went with write_all() for now but curious if you think it matters for this to try and get the sender and receiver format the same?

@benalleng benalleng force-pushed the remove-pad-plaintext branch 2 times, most recently from f97cbc1 to 6992469 Compare September 4, 2025 21:04
This commit forces the message encryption to only accept a message of a
length equal to the padded_plaintext length to just keep the function
signatures strongly typed to accept only a specific value.
@benalleng benalleng force-pushed the remove-pad-plaintext branch from 6992469 to d0aa49f Compare September 5, 2025 00:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants