Skip to content

Commit

Permalink
Encrypt Message A with ephemeral key like Noise IK
Browse files Browse the repository at this point in the history
Encrypting Message A with an ephemeral "encapsulation key" allows the sender
"reply key" corresponding to its subdirectory to be hidden from the directory.
  • Loading branch information
DanGould committed Oct 15, 2024
1 parent 0464b78 commit c2eafff
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 30 deletions.
43 changes: 27 additions & 16 deletions payjoin/src/hpke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ pub type SecretKey = <SecpK256HkdfSha256 as hpke::Kem>::PrivateKey;
pub type PublicKey = <SecpK256HkdfSha256 as hpke::Kem>::PublicKey;
pub type EncappedKey = <SecpK256HkdfSha256 as hpke::Kem>::EncappedKey;

fn sk_to_pk(sk: &SecretKey) -> PublicKey { <SecpK256HkdfSha256 as hpke::Kem>::sk_to_pk(sk) }

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HpkeKeyPair(pub HpkeSecretKey, pub HpkePublicKey);

Expand Down Expand Up @@ -130,19 +128,24 @@ impl<'de> serde::Deserialize<'de> for HpkePublicKey {
/// Message A is sent from the sender to the receiver containing an Original PSBT payload
#[cfg(feature = "send")]
pub fn encrypt_message_a(
mut plaintext: Vec<u8>,
sender_sk: &HpkeSecretKey,
body: Vec<u8>,
encapsulation_pair: &HpkeKeyPair,
reply_pk: &HpkePublicKey,
receiver_pk: &HpkePublicKey,
) -> Result<Vec<u8>, HpkeError> {
let pk = sk_to_pk(&sender_sk.0);
let (encapsulated_key, mut encryption_context) =
hpke::setup_sender::<ChaCha20Poly1305, HkdfSha256, SecpK256HkdfSha256, _>(
&OpModeS::Auth((sender_sk.0.clone(), pk.clone())),
&OpModeS::Auth((
encapsulation_pair.secret_key().0.clone(),
encapsulation_pair.public_key().0.clone(),
)),
&receiver_pk.0,
INFO_A,
&mut OsRng,
)?;
let aad = pk.to_bytes().to_vec();
let aad = encapsulation_pair.public_key().to_bytes().to_vec();
let mut plaintext = reply_pk.to_bytes().to_vec();
plaintext.extend(body);
let plaintext = pad_plaintext(&mut plaintext, PADDED_PLAINTEXT_A_LENGTH)?;
let ciphertext = encryption_context.seal(plaintext, &aad)?;
let mut message_a = encapsulated_key.to_bytes().to_vec();
Expand All @@ -156,18 +159,26 @@ pub fn decrypt_message_a(
message_a: &[u8],
receiver_sk: HpkeSecretKey,
) -> Result<(Vec<u8>, HpkePublicKey), HpkeError> {
let enc = message_a.get(..65).ok_or(HpkeError::PayloadTooShort)?;
let enc = message_a.get(..UNCOMPRESSED_PUBLIC_KEY_SIZE).ok_or(HpkeError::PayloadTooShort)?;
let enc = EncappedKey::from_bytes(enc)?;
let aad = message_a.get(65..130).ok_or(HpkeError::PayloadTooShort)?;
let pk_s = PublicKey::from_bytes(aad)?;
let mut decryption_ctx = hpke::setup_receiver::<
ChaCha20Poly1305,
HkdfSha256,
SecpK256HkdfSha256,
>(&OpModeR::Auth(pk_s.clone()), &receiver_sk.0, &enc, INFO_A)?;
let aad = message_a
.get(UNCOMPRESSED_PUBLIC_KEY_SIZE..(UNCOMPRESSED_PUBLIC_KEY_SIZE * 2))
.ok_or(HpkeError::PayloadTooShort)?;
let encapsulation_pk = PublicKey::from_bytes(aad)?;
let mut decryption_ctx =
hpke::setup_receiver::<ChaCha20Poly1305, HkdfSha256, SecpK256HkdfSha256>(
&OpModeR::Auth(encapsulation_pk.clone()),
&receiver_sk.0,
&enc,
INFO_A,
)?;
let ciphertext = message_a.get(130..).ok_or(HpkeError::PayloadTooShort)?;
let plaintext = decryption_ctx.open(ciphertext, aad)?;
Ok((plaintext, HpkePublicKey(pk_s)))
let reply_pk =
plaintext.get(..UNCOMPRESSED_PUBLIC_KEY_SIZE).ok_or(HpkeError::PayloadTooShort)?;
let reply_pk = HpkePublicKey(PublicKey::from_bytes(reply_pk)?);
let body = plaintext.get(UNCOMPRESSED_PUBLIC_KEY_SIZE..).ok_or(HpkeError::PayloadTooShort)?;
Ok((body.to_vec(), reply_pk))
}

/// Message B is sent from the receiver to the sender containing a Payjoin PSBT payload or an error
Expand Down
48 changes: 34 additions & 14 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ impl<'a> SenderBuilder<'a> {
payee,
min_fee_rate: self.min_fee_rate,
#[cfg(feature = "v2")]
e: HpkeKeyPair::gen_keypair(),
reply_pair: HpkeKeyPair::gen_keypair(),
})
}
}
Expand All @@ -245,7 +245,7 @@ pub struct Sender {
min_fee_rate: FeeRate,
payee: ScriptBuf,
#[cfg(feature = "v2")]
e: HpkeKeyPair,
reply_pair: HpkeKeyPair,
}

impl Sender {
Expand Down Expand Up @@ -321,8 +321,14 @@ impl Sender {
self.fee_contribution,
self.min_fee_rate,
)?;
let body = encrypt_message_a(body, &self.e.secret_key().clone(), &rs)
.map_err(InternalCreateRequestError::Hpke)?;
let hpke_ctx = HpkeContext::new(rs);
let body = encrypt_message_a(
body,
&hpke_ctx.encapsulation_pair.clone(),
&hpke_ctx.reply_pair.public_key().clone(),
&hpke_ctx.receiver.clone(),
)
.map_err(InternalCreateRequestError::Hpke)?;
let mut ohttp =
self.endpoint.ohttp().ok_or(InternalCreateRequestError::MissingOhttpConfig)?;
let (body, ohttp_ctx) = ohttp_encapsulate(&mut ohttp, "POST", url.as_str(), Some(&body))
Expand All @@ -339,7 +345,7 @@ impl Sender {
payee: self.payee.clone(),
min_fee_rate: self.min_fee_rate,
},
hpke_ctx: HpkeContext { rs, e: self.e.clone() },
hpke_ctx,
ohttp_ctx,
}),
))
Expand Down Expand Up @@ -432,13 +438,14 @@ impl V2GetContext {
) -> Result<(Request, ohttp::ClientResponse), CreateRequestError> {
use crate::uri::UrlExt;
let mut url = self.endpoint.clone();
let subdir =
BASE64_URL_SAFE_NO_PAD.encode(self.hpke_ctx.e.public_key().to_compressed_bytes());
let subdir = BASE64_URL_SAFE_NO_PAD
.encode(self.hpke_ctx.reply_pair.public_key().to_compressed_bytes());
url.set_path(&subdir);
let body = encrypt_message_a(
Vec::new(),
&self.hpke_ctx.e.secret_key().clone(),
&self.hpke_ctx.rs.clone(),
&self.hpke_ctx.encapsulation_pair.clone(),
&self.hpke_ctx.reply_pair.public_key().clone(),
&self.hpke_ctx.receiver.clone(),
)
.map_err(InternalCreateRequestError::Hpke)?;
let mut ohttp =
Expand All @@ -465,8 +472,8 @@ impl V2GetContext {
};
let psbt = decrypt_message_b(
&body,
self.hpke_ctx.rs.clone(),
self.hpke_ctx.e.secret_key().clone(),
self.hpke_ctx.receiver.clone(),
self.hpke_ctx.reply_pair.secret_key().clone(),
)
.map_err(InternalValidationError::Hpke)?;

Expand All @@ -488,10 +495,23 @@ pub struct PsbtContext {
min_fee_rate: FeeRate,
payee: ScriptBuf,
}

#[cfg(feature = "v2")]
struct HpkeContext {
rs: HpkePublicKey,
e: HpkeKeyPair,
receiver: HpkePublicKey,
encapsulation_pair: HpkeKeyPair,
reply_pair: HpkeKeyPair,
}

#[cfg(feature = "v2")]
impl HpkeContext {
pub fn new(receiver: HpkePublicKey) -> Self {
Self {
receiver,
encapsulation_pair: HpkeKeyPair::gen_keypair(),
reply_pair: HpkeKeyPair::gen_keypair(),
}
}
}

macro_rules! check_eq {
Expand Down Expand Up @@ -960,7 +980,7 @@ mod test {
fee_contribution: None,
min_fee_rate: FeeRate::ZERO,
payee: ScriptBuf::from(vec![0x00]),
e: HpkeKeyPair::gen_keypair(),
reply_pair: HpkeKeyPair::gen_keypair(),
};
let serialized = serde_json::to_string(&req_ctx).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
Expand Down

0 comments on commit c2eafff

Please sign in to comment.