Skip to content

Commit

Permalink
Secure v2 payloads with authenticated encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Sep 12, 2023
1 parent 6c7b218 commit bb2b531
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 56 deletions.
83 changes: 83 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 14 additions & 18 deletions payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl App {
log::debug!("Sending request");
write.send(Message::binary(req.body)).await?;
log::debug!("Awaiting response");
let buffer = read.next().await.unwrap()?.into_text()?;
let buffer = read.next().await.unwrap()?.into_data();
let mut response = std::io::Cursor::new(&buffer);
self.process_pj_response(ctx, &mut response)?;
write.close().await?;
Expand Down Expand Up @@ -189,14 +189,11 @@ impl App {
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;

let secp = bitcoin::secp256k1::Secp256k1::new();
let mut rng = bitcoin::secp256k1::rand::thread_rng();
let key = bitcoin::secp256k1::KeyPair::new(&secp, &mut rng);
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
let pubkey_base64 = base64::encode_config(key.public_key().to_string(), b64_config);

let pj_uri_string = self.construct_payjoin_uri(amount_arg, Some(&pubkey_base64))?;
use tokio::io::AsyncReadExt;

let context = payjoin::receive::ProposalContext::new();
let pj_uri_string =
self.construct_payjoin_uri(amount_arg, Some(&context.subdirectory()))?;
println!(
"Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:",
self.config.pj_host
Expand All @@ -208,18 +205,20 @@ impl App {
let (mut write, mut read) = stream.split();
// enroll receiver
log::debug!("Generating ephemeral keypair");
let enroll_string = format!("{} {}", payjoin::v2::RECEIVE, pubkey_base64);
write.send(Message::binary(enroll_string.as_bytes())).await?;
write.send(Message::binary(context.enroll_string().as_bytes())).await?;
log::debug!("Enrolled receiver, awaiting request");
let buffer = read.next().await.unwrap()?;
log::debug!("Received request");
let proposal = UncheckedProposal::from_streamed(&buffer.into_data())
let (proposal, e) = context
.parse_proposal(&mut buffer.into_data())
.map_err(|e| anyhow!("Failed to parse into UncheckedProposal {}", e))?;
let payjoin_psbt = self
.process_proposal(proposal)
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
let payjoin_psbt_ser = base64::encode(&payjoin_psbt.serialize());
write.send(Message::binary(payjoin_psbt_ser)).await?;
let mut payjoin_bytes = payjoin_psbt.serialize();
log::debug!("payjoin_bytes: {:?}", payjoin_bytes);
let payload = payjoin::v2::encrypt_message_b(&mut payjoin_bytes, e);
write.send(Message::binary(payload)).await?;
write.close().await?;
Ok(())
}
Expand Down Expand Up @@ -348,10 +347,7 @@ impl App {
headers,
)?;

let payjoin_proposal_psbt = self.process_proposal(proposal)?;
log::debug!("Receiver's Payjoin proposal PSBT Rsponse: {:#?}", payjoin_proposal_psbt);

let payload = base64::encode(&payjoin_proposal_psbt.serialize());
let payload = self.process_proposal(proposal)?;
log::info!("successful response");
Ok(Response::text(payload))
}
Expand Down
58 changes: 35 additions & 23 deletions payjoin-relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,40 @@ async fn handle_connection_impl(connection: TcpStream, pool: DbPool) -> Result<(
.await?;
let (mut write, mut read) = stream.split();
info!("Accepted stream");
match read_stream_to_string(&mut read).await? {
Some(data) => {
let mut parts = data.split_whitespace();
let operation = parts.next().ok_or(anyhow::anyhow!("No operation"))?;
if operation == RECEIVE {
let pubkey_id = parts.next().ok_or(anyhow::anyhow!("No pubkey_id"))?;
let pubkey_id = shorten_string(pubkey_id);
info!("Received receiver enroll request for pubkey_id {}", pubkey_id);
handle_receiver_request(&mut write, &mut read, &pool, &pubkey_id).await?;
} else {
handle_sender_request(&mut write, &data, &pool, &pubkey_id).await?;
match read.next().await {
Some(bytes) => {
let bytes = bytes?.into_data();
match std::str::from_utf8(&bytes) {
Ok(message) => {
let mut parts = message.split_whitespace();
let operation = parts.next().ok_or(anyhow::anyhow!("No operation"))?;
if operation == RECEIVE {
let pubkey_id = parts.next().ok_or(anyhow::anyhow!("No pubkey_id"))?;
let pubkey_id = shorten_string(pubkey_id);
info!("Received receiver enroll request for pubkey_id {}", pubkey_id);
handle_receiver_request(&mut write, &mut read, &pool, &pubkey_id).await?;
}
}
_ => handle_sender_request(&mut write, bytes.to_vec(), &pool, &pubkey_id).await?,
}
}
None => (),
}
// match read_stream_to_string(&mut read).await? {
// Some(data) => {
// let mut parts = data.split_whitespace();
// let operation = parts.next().ok_or(anyhow::anyhow!("No operation"))?;
// if operation == RECEIVE {
// let pubkey_id = parts.next().ok_or(anyhow::anyhow!("No pubkey_id"))?;
// let pubkey_id = shorten_string(pubkey_id);
// info!("Received receiver enroll request for pubkey_id {}", pubkey_id);
// handle_receiver_request(&mut write, &mut read, &pool, &pubkey_id).await?;
// } else {
// handle_sender_request(&mut write, &data, &pool, &pubkey_id).await?;
// }
// }
// None => (),
// }
info!("Closing stream");
write.close().await?;
Ok(())
Expand All @@ -92,13 +111,6 @@ fn init_logging() {
println!("Logging initialized");
}

async fn read_stream_to_string(read: &mut Stream) -> Result<Option<String>> {
match read.next().await {
Some(msg) => Ok(Some(msg?.to_string())),
None => Ok(None),
}
}

async fn handle_receiver_request(
write: &mut Sink,
read: &mut Stream,
Expand All @@ -107,21 +119,21 @@ async fn handle_receiver_request(
) -> Result<()> {
let buffered_req = pool.peek_req(pubkey_id).await?;
write.send(Message::binary(buffered_req)).await?;

if let Some(response) = read_stream_to_string(read).await? {
pool.push_res(pubkey_id, response.as_bytes().to_vec()).await?;
if let Some(bytes) = read.next().await {
let bytes = bytes?.into_data();
pool.push_res(pubkey_id, bytes).await?;
}

Ok(())
}

async fn handle_sender_request(
write: &mut Sink,
data: &str,
data: Vec<u8>,
pool: &DbPool,
pubkey_id: &str,
) -> Result<()> {
pool.push_req(pubkey_id, data.as_bytes().to_vec()).await?;
pool.push_req(pubkey_id, data).await?;
debug!("pushed req");
let response = pool.peek_res(pubkey_id).await?;
debug!("peek req");
Expand Down
3 changes: 2 additions & 1 deletion payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ edition = "2018"
[features]
send = []
receive = ["rand"]
v2 = ["serde", "serde_json"]
v2 = ["bitcoin/rand-std", "chacha20poly1305", "serde", "serde_json"]

[dependencies]
bitcoin = { version = "0.30.0", features = ["base64"] }
bip21 = "0.3.1"
chacha20poly1305 = { version = "0.10.1", optional = true }
log = { version = "0.4.14"}
rand = { version = "0.8.4", optional = true }
serde = { version = "1.0", optional = true }
Expand Down
54 changes: 41 additions & 13 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ pub use error::{Error, RequestError, SelectionError};
use error::{InternalRequestError, InternalSelectionError};
use rand::seq::SliceRandom;
use rand::Rng;
use serde::Serialize;

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

#[cfg(feature = "v2")]
pub struct ProposalContext {
s: bitcoin::secp256k1::KeyPair,
}

impl ProposalContext {
pub fn new() -> Self {
let secp = bitcoin::secp256k1::Secp256k1::new();
let (sk, _) = secp.generate_keypair(&mut rand::rngs::OsRng);
ProposalContext { s: bitcoin::secp256k1::KeyPair::from_secret_key(&secp, &sk) }
}

pub fn subdirectory(&self) -> String {
let pubkey = &self.s.public_key().serialize();
let b64_config =
bitcoin::base64::Config::new(bitcoin::base64::CharacterSet::UrlSafe, false);
let pubkey_base64 = bitcoin::base64::encode_config(pubkey, b64_config);
pubkey_base64
}

pub fn enroll_string(&self) -> String {
format!("{} {}", crate::v2::RECEIVE, self.subdirectory())
}

pub fn parse_proposal(
self,
encrypted_proposal: &mut [u8],
) -> Result<(UncheckedProposal, bitcoin::secp256k1::PublicKey), RequestError> {
let (proposal, e) = crate::v2::decrypt_message_a(encrypted_proposal, self.s.secret_key());
let mut proposal = serde_json::from_slice::<UncheckedProposal>(&proposal)
.map_err(InternalRequestError::Json)?;
proposal.psbt = proposal.psbt.validate().map_err(InternalRequestError::InconsistentPsbt)?;
log::debug!("Received original psbt: {:?}", proposal.psbt);
log::debug!("Received request with params: {:?}", proposal.params);

Ok((proposal, e))
}
}

/// The sender's original PSBT and optional parameters
///
/// This type is used to proces the request. It is returned by
Expand Down Expand Up @@ -341,20 +381,8 @@ where
Ok(unchecked_psbt)
}

#[cfg(feature = "v2")]
impl UncheckedProposal {
pub fn from_streamed(streamed: &[u8]) -> Result<Self, RequestError> {
let mut proposal = serde_json::from_slice::<UncheckedProposal>(streamed)
.map_err(InternalRequestError::Json)?;
proposal.psbt = proposal.psbt.validate().map_err(InternalRequestError::InconsistentPsbt)?;
log::debug!("Received original psbt: {:?}", proposal.psbt);
log::debug!("Received request with params: {:?}", proposal.params);

Ok(proposal)
}
}

impl UncheckedProposal {
#[cfg(not(feature = "v2"))]
pub fn from_request(
mut body: impl std::io::Read,
query: &str,
Expand Down
Loading

0 comments on commit bb2b531

Please sign in to comment.