Skip to content

Commit bb2b531

Browse files
committed
Secure v2 payloads with authenticated encryption
1 parent 6c7b218 commit bb2b531

File tree

7 files changed

+309
-56
lines changed

7 files changed

+309
-56
lines changed

Cargo.lock

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

payjoin-cli/src/app.rs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl App {
5656
log::debug!("Sending request");
5757
write.send(Message::binary(req.body)).await?;
5858
log::debug!("Awaiting response");
59-
let buffer = read.next().await.unwrap()?.into_text()?;
59+
let buffer = read.next().await.unwrap()?.into_data();
6060
let mut response = std::io::Cursor::new(&buffer);
6161
self.process_pj_response(ctx, &mut response)?;
6262
write.close().await?;
@@ -189,14 +189,11 @@ impl App {
189189
use futures_util::{SinkExt, StreamExt};
190190
use tokio_tungstenite::connect_async;
191191
use tokio_tungstenite::tungstenite::Message;
192-
193-
let secp = bitcoin::secp256k1::Secp256k1::new();
194-
let mut rng = bitcoin::secp256k1::rand::thread_rng();
195-
let key = bitcoin::secp256k1::KeyPair::new(&secp, &mut rng);
196-
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
197-
let pubkey_base64 = base64::encode_config(key.public_key().to_string(), b64_config);
198-
199-
let pj_uri_string = self.construct_payjoin_uri(amount_arg, Some(&pubkey_base64))?;
192+
use tokio::io::AsyncReadExt;
193+
194+
let context = payjoin::receive::ProposalContext::new();
195+
let pj_uri_string =
196+
self.construct_payjoin_uri(amount_arg, Some(&context.subdirectory()))?;
200197
println!(
201198
"Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:",
202199
self.config.pj_host
@@ -208,18 +205,20 @@ impl App {
208205
let (mut write, mut read) = stream.split();
209206
// enroll receiver
210207
log::debug!("Generating ephemeral keypair");
211-
let enroll_string = format!("{} {}", payjoin::v2::RECEIVE, pubkey_base64);
212-
write.send(Message::binary(enroll_string.as_bytes())).await?;
208+
write.send(Message::binary(context.enroll_string().as_bytes())).await?;
213209
log::debug!("Enrolled receiver, awaiting request");
214210
let buffer = read.next().await.unwrap()?;
215211
log::debug!("Received request");
216-
let proposal = UncheckedProposal::from_streamed(&buffer.into_data())
212+
let (proposal, e) = context
213+
.parse_proposal(&mut buffer.into_data())
217214
.map_err(|e| anyhow!("Failed to parse into UncheckedProposal {}", e))?;
218215
let payjoin_psbt = self
219216
.process_proposal(proposal)
220217
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
221-
let payjoin_psbt_ser = base64::encode(&payjoin_psbt.serialize());
222-
write.send(Message::binary(payjoin_psbt_ser)).await?;
218+
let mut payjoin_bytes = payjoin_psbt.serialize();
219+
log::debug!("payjoin_bytes: {:?}", payjoin_bytes);
220+
let payload = payjoin::v2::encrypt_message_b(&mut payjoin_bytes, e);
221+
write.send(Message::binary(payload)).await?;
223222
write.close().await?;
224223
Ok(())
225224
}
@@ -348,10 +347,7 @@ impl App {
348347
headers,
349348
)?;
350349

351-
let payjoin_proposal_psbt = self.process_proposal(proposal)?;
352-
log::debug!("Receiver's Payjoin proposal PSBT Rsponse: {:#?}", payjoin_proposal_psbt);
353-
354-
let payload = base64::encode(&payjoin_proposal_psbt.serialize());
350+
let payload = self.process_proposal(proposal)?;
355351
log::info!("successful response");
356352
Ok(Response::text(payload))
357353
}

payjoin-relay/src/main.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,40 @@ async fn handle_connection_impl(connection: TcpStream, pool: DbPool) -> Result<(
6363
.await?;
6464
let (mut write, mut read) = stream.split();
6565
info!("Accepted stream");
66-
match read_stream_to_string(&mut read).await? {
67-
Some(data) => {
68-
let mut parts = data.split_whitespace();
69-
let operation = parts.next().ok_or(anyhow::anyhow!("No operation"))?;
70-
if operation == RECEIVE {
71-
let pubkey_id = parts.next().ok_or(anyhow::anyhow!("No pubkey_id"))?;
72-
let pubkey_id = shorten_string(pubkey_id);
73-
info!("Received receiver enroll request for pubkey_id {}", pubkey_id);
74-
handle_receiver_request(&mut write, &mut read, &pool, &pubkey_id).await?;
75-
} else {
76-
handle_sender_request(&mut write, &data, &pool, &pubkey_id).await?;
66+
match read.next().await {
67+
Some(bytes) => {
68+
let bytes = bytes?.into_data();
69+
match std::str::from_utf8(&bytes) {
70+
Ok(message) => {
71+
let mut parts = message.split_whitespace();
72+
let operation = parts.next().ok_or(anyhow::anyhow!("No operation"))?;
73+
if operation == RECEIVE {
74+
let pubkey_id = parts.next().ok_or(anyhow::anyhow!("No pubkey_id"))?;
75+
let pubkey_id = shorten_string(pubkey_id);
76+
info!("Received receiver enroll request for pubkey_id {}", pubkey_id);
77+
handle_receiver_request(&mut write, &mut read, &pool, &pubkey_id).await?;
78+
}
79+
}
80+
_ => handle_sender_request(&mut write, bytes.to_vec(), &pool, &pubkey_id).await?,
7781
}
7882
}
7983
None => (),
8084
}
85+
// match read_stream_to_string(&mut read).await? {
86+
// Some(data) => {
87+
// let mut parts = data.split_whitespace();
88+
// let operation = parts.next().ok_or(anyhow::anyhow!("No operation"))?;
89+
// if operation == RECEIVE {
90+
// let pubkey_id = parts.next().ok_or(anyhow::anyhow!("No pubkey_id"))?;
91+
// let pubkey_id = shorten_string(pubkey_id);
92+
// info!("Received receiver enroll request for pubkey_id {}", pubkey_id);
93+
// handle_receiver_request(&mut write, &mut read, &pool, &pubkey_id).await?;
94+
// } else {
95+
// handle_sender_request(&mut write, &data, &pool, &pubkey_id).await?;
96+
// }
97+
// }
98+
// None => (),
99+
// }
81100
info!("Closing stream");
82101
write.close().await?;
83102
Ok(())
@@ -92,13 +111,6 @@ fn init_logging() {
92111
println!("Logging initialized");
93112
}
94113

95-
async fn read_stream_to_string(read: &mut Stream) -> Result<Option<String>> {
96-
match read.next().await {
97-
Some(msg) => Ok(Some(msg?.to_string())),
98-
None => Ok(None),
99-
}
100-
}
101-
102114
async fn handle_receiver_request(
103115
write: &mut Sink,
104116
read: &mut Stream,
@@ -107,21 +119,21 @@ async fn handle_receiver_request(
107119
) -> Result<()> {
108120
let buffered_req = pool.peek_req(pubkey_id).await?;
109121
write.send(Message::binary(buffered_req)).await?;
110-
111-
if let Some(response) = read_stream_to_string(read).await? {
112-
pool.push_res(pubkey_id, response.as_bytes().to_vec()).await?;
122+
if let Some(bytes) = read.next().await {
123+
let bytes = bytes?.into_data();
124+
pool.push_res(pubkey_id, bytes).await?;
113125
}
114126

115127
Ok(())
116128
}
117129

118130
async fn handle_sender_request(
119131
write: &mut Sink,
120-
data: &str,
132+
data: Vec<u8>,
121133
pool: &DbPool,
122134
pubkey_id: &str,
123135
) -> Result<()> {
124-
pool.push_req(pubkey_id, data.as_bytes().to_vec()).await?;
136+
pool.push_req(pubkey_id, data).await?;
125137
debug!("pushed req");
126138
let response = pool.peek_res(pubkey_id).await?;
127139
debug!("peek req");

payjoin/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ edition = "2018"
1515
[features]
1616
send = []
1717
receive = ["rand"]
18-
v2 = ["serde", "serde_json"]
18+
v2 = ["bitcoin/rand-std", "chacha20poly1305", "serde", "serde_json"]
1919

2020
[dependencies]
2121
bitcoin = { version = "0.30.0", features = ["base64"] }
2222
bip21 = "0.3.1"
23+
chacha20poly1305 = { version = "0.10.1", optional = true }
2324
log = { version = "0.4.14"}
2425
rand = { version = "0.8.4", optional = true }
2526
serde = { version = "1.0", optional = true }

payjoin/src/receive/mod.rs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ pub use error::{Error, RequestError, SelectionError};
278278
use error::{InternalRequestError, InternalSelectionError};
279279
use rand::seq::SliceRandom;
280280
use rand::Rng;
281+
use serde::Serialize;
281282

282283
use crate::input_type::InputType;
283284
use crate::optional_parameters::Params;
@@ -287,6 +288,45 @@ pub trait Headers {
287288
fn get_header(&self, key: &str) -> Option<&str>;
288289
}
289290

291+
#[cfg(feature = "v2")]
292+
pub struct ProposalContext {
293+
s: bitcoin::secp256k1::KeyPair,
294+
}
295+
296+
impl ProposalContext {
297+
pub fn new() -> Self {
298+
let secp = bitcoin::secp256k1::Secp256k1::new();
299+
let (sk, _) = secp.generate_keypair(&mut rand::rngs::OsRng);
300+
ProposalContext { s: bitcoin::secp256k1::KeyPair::from_secret_key(&secp, &sk) }
301+
}
302+
303+
pub fn subdirectory(&self) -> String {
304+
let pubkey = &self.s.public_key().serialize();
305+
let b64_config =
306+
bitcoin::base64::Config::new(bitcoin::base64::CharacterSet::UrlSafe, false);
307+
let pubkey_base64 = bitcoin::base64::encode_config(pubkey, b64_config);
308+
pubkey_base64
309+
}
310+
311+
pub fn enroll_string(&self) -> String {
312+
format!("{} {}", crate::v2::RECEIVE, self.subdirectory())
313+
}
314+
315+
pub fn parse_proposal(
316+
self,
317+
encrypted_proposal: &mut [u8],
318+
) -> Result<(UncheckedProposal, bitcoin::secp256k1::PublicKey), RequestError> {
319+
let (proposal, e) = crate::v2::decrypt_message_a(encrypted_proposal, self.s.secret_key());
320+
let mut proposal = serde_json::from_slice::<UncheckedProposal>(&proposal)
321+
.map_err(InternalRequestError::Json)?;
322+
proposal.psbt = proposal.psbt.validate().map_err(InternalRequestError::InconsistentPsbt)?;
323+
log::debug!("Received original psbt: {:?}", proposal.psbt);
324+
log::debug!("Received request with params: {:?}", proposal.params);
325+
326+
Ok((proposal, e))
327+
}
328+
}
329+
290330
/// The sender's original PSBT and optional parameters
291331
///
292332
/// This type is used to proces the request. It is returned by
@@ -341,20 +381,8 @@ where
341381
Ok(unchecked_psbt)
342382
}
343383

344-
#[cfg(feature = "v2")]
345-
impl UncheckedProposal {
346-
pub fn from_streamed(streamed: &[u8]) -> Result<Self, RequestError> {
347-
let mut proposal = serde_json::from_slice::<UncheckedProposal>(streamed)
348-
.map_err(InternalRequestError::Json)?;
349-
proposal.psbt = proposal.psbt.validate().map_err(InternalRequestError::InconsistentPsbt)?;
350-
log::debug!("Received original psbt: {:?}", proposal.psbt);
351-
log::debug!("Received request with params: {:?}", proposal.params);
352-
353-
Ok(proposal)
354-
}
355-
}
356-
357384
impl UncheckedProposal {
385+
#[cfg(not(feature = "v2"))]
358386
pub fn from_request(
359387
mut body: impl std::io::Read,
360388
query: &str,

0 commit comments

Comments
 (0)