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 29, 2023
1 parent 92c02cc commit 5deba12
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 66 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.

52 changes: 22 additions & 30 deletions payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use clap::ArgMatches;
use config::{Config, File, FileFormat};
use payjoin::bitcoin::psbt::Psbt;
use payjoin::bitcoin::{self, base64};
use payjoin::receive::{Error, ProvisionalProposal, UncheckedProposal};
use payjoin::receive::{Error, PayjoinProposal, ProvisionalProposal, UncheckedProposal};
#[cfg(not(feature = "v2"))]
use rouille::{Request, Response};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -218,13 +218,9 @@ impl App {

#[cfg(feature = "v2")]
pub async fn receive_payjoin(self, amount_arg: &str) -> Result<()> {
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))?;
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 @@ -236,20 +232,21 @@ impl App {
.build()
.with_context(|| "Failed to build reqwest http client")?;
log::debug!("Awaiting request");
let receive_endpoint =
format!("{}/{}/{}", self.config.pj_endpoint, pubkey_base64, payjoin::v2::RECEIVE);
let buffer = Self::long_poll_get(&client, &receive_endpoint).await?;
let receive_endpoint = format!("{}/{}", self.config.pj_endpoint, context.receive_subdir());
let mut buffer = Self::long_poll_get(&client, &receive_endpoint).await?;

log::debug!("Received request");
let proposal = UncheckedProposal::from_streamed(&buffer)
let proposal = context
.parse_proposal(&mut buffer)
.map_err(|e| anyhow!("Failed to parse into UncheckedProposal {}", e))?;
let payjoin_psbt = self
let payjoin_proposal = self
.process_proposal(proposal)
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
let payjoin_psbt_ser = payjoin::bitcoin::base64::encode(&payjoin_psbt.serialize());

let body = payjoin_proposal.serialize_body();
let _ = client
.post(receive_endpoint)
.body(payjoin_psbt_ser)
.body(body)
.send()
.await
.with_context(|| "HTTP request failed")?;
Expand Down Expand Up @@ -376,15 +373,15 @@ impl App {
headers,
)?;

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

let body = payjoin_proposal.serialize_body().as_str().to_sring();

let payload = payjoin::bitcoin::base64::encode(&payjoin_proposal_psbt.serialize());
log::info!("successful response");
Ok(Response::text(payload))
Ok(Response::text(body))
}

fn process_proposal(&self, proposal: UncheckedProposal) -> Result<Psbt, Error> {
fn process_proposal(&self, proposal: UncheckedProposal) -> Result<PayjoinProposal, Error> {
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast();

Expand Down Expand Up @@ -456,23 +453,18 @@ impl App {
.assume_checked();
provisional_payjoin.substitute_output_address(receiver_substitute_address);

let payjoi_proposal = provisional_payjoin.finalize_proposal(
let payjoin_proposal = provisional_payjoin.finalize_proposal(
|psbt: &Psbt| {
self.bitcoind
.wallet_process_psbt(
&payjoin::base64::encode(psbt.serialize()),
None,
None,
Some(false),
)
.wallet_process_psbt(&base64::encode(psbt.serialize()), None, None, Some(false))
.map(|res| Psbt::from_str(&res.psbt).map_err(|e| Error::Server(e.into())))
.map_err(|e| Error::Server(e.into()))?
},
Some(bitcoin::FeeRate::MIN),
)?;
let payjoin_proposal_psbt = payjoi_proposal.psbt();
let payjoin_proposal_psbt = payjoin_proposal.psbt();
log::debug!("Receiver's Payjoin proposal PSBT Rsponse: {:#?}", payjoin_proposal_psbt);
Ok(payjoin_proposal_psbt.clone())
Ok(payjoin_proposal)
}

fn insert_input_seen_before(&self, input: bitcoin::OutPoint) -> Result<bool> {
Expand Down Expand Up @@ -621,4 +613,4 @@ impl payjoin::receive::Headers for Headers<'_> {
}
}

fn serialize_psbt(psbt: &Psbt) -> String { payjoin::base64::encode(&psbt.serialize()) }
fn serialize_psbt(psbt: &Psbt) -> String { base64::encode(&psbt.serialize()) }
3 changes: 2 additions & 1 deletion payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ edition = "2018"
send = []
receive = ["rand"]
base64 = ["bitcoin/base64"]
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
Loading

0 comments on commit 5deba12

Please sign in to comment.