Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ bip39 = { version = "2.0.0", features = ["rand"] }
bip21 = { version = "0.5", features = ["std"], default-features = false }

base64 = { version = "0.22.1", default-features = false, features = ["std"] }
chacha20-poly1305 = { version = "0.1.2", default-features = false, features = ["std"] }
getrandom = { version = "0.3", default-features = false }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros", "net" ] }
Expand Down
4 changes: 2 additions & 2 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::ffi::{maybe_deref, maybe_try_convert_enum, maybe_wrap};
use crate::liquidity::LiquiditySource;
use crate::logger::{log_error, log_info, LdkLogger, Logger};
use crate::payment::store::{
LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind,
LSPS2Parameters, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind,
PaymentStatus,
};
use crate::peer_store::{PeerInfo, PeerStore};
Expand Down Expand Up @@ -201,7 +201,7 @@ impl Bolt11Payment {
// Register payment in payment store.
let payment_hash = invoice.payment_hash();
let payment_secret = invoice.payment_secret();
let lsp_fee_limits = LSPFeeLimits {
let lsp_fee_limits = LSPS2Parameters {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure if this rename is strictly better. Parameters sounds broader than what it is.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that's intentional as we might add more fields in the BOLT12 context that are not 'fee limits'. Sorry, maybe should have given that rationale in the commit description.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Which parameters are that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

max_total_opening_fee_msat: lsp_total_opening_fee,
max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee,
};
Expand Down
204 changes: 204 additions & 0 deletions src/payment/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// This file is Copyright its original authors, visible in version control history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.

use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
use chacha20_poly1305::{ChaCha20Poly1305, Key, Nonce};
use lightning::util::ser::{Readable, Writeable};
use lightning_types::payment::{PaymentHash, PaymentSecret};

use crate::payment::store::LSPS2Parameters;

/// Metadata carried in invoice payment metadata fields.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PaymentMetadata {
pub(crate) lsps2_parameters: Option<LSPS2Parameters>,
}

#[derive(Clone, Copy)]
pub(crate) struct PaymentMetadataKeys {
encryption_key: [u8; 32],
nonce_key: [u8; 32],
}

impl PaymentMetadataKeys {
pub(crate) fn new(base_secret: [u8; 32]) -> Self {
Self {
encryption_key: hmac_sha256(&base_secret, b"ldk_node_payment_metadata_encryption_key"),
Comment thread
joostjager marked this conversation as resolved.
nonce_key: hmac_sha256(&base_secret, b"ldk_node_payment_metadata_nonce_key"),
}
}

fn nonce(&self, payment_hash: &PaymentHash, payment_secret: &PaymentSecret) -> [u8; 12] {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it totally ruled out that payment hash and secret are never reused, also not in some manual flow for example? Perhaps defensively picking a random nonce and storing it in the metadata is safer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, pretty much. If we reuse the payment hash for example we have worse issues than just privacy leakage at our hands. Happy to store the nonce if you insist, but note that we try to keep invoices as small as possible, in particular for QR encoding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Still I think it is better to not rely on that, but might be worth getting a 2nd opinion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Related question: if size is important, is the current scheme minimal? Perhaps the double tlv wrapper and/or u64 can be shaved down too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And with lightningdevkit/rust-lightning#4528, perhaps the tag isn't needed anymore for auth, because auth is already on the rust-lightning level?

let mut engine = HmacEngine::<sha256::Hash>::new(&self.nonce_key);
engine.input(b"ldk_node_payment_metadata_nonce");
engine.input(&payment_hash.0);
engine.input(&payment_secret.0);
let hmac = Hmac::<sha256::Hash>::from_engine(engine).to_byte_array();

let mut nonce = [0u8; 12];
nonce.copy_from_slice(&hmac[..12]);
nonce
}
}

const PAYMENT_METADATA_AAD: &[u8] = b"ldk_node_payment_metadata";
const PAYMENT_METADATA_TAG_LEN: usize = 16;

/// Encrypted invoice payment metadata.
pub(crate) struct EncryptedPaymentMetadata {
pub(crate) raw: Vec<u8>,
}

impl PaymentMetadata {
pub(crate) fn encrypt(
&self, keys: &PaymentMetadataKeys, payment_hash: &PaymentHash,
payment_secret: &PaymentSecret,
) -> EncryptedPaymentMetadata {
let nonce = keys.nonce(payment_hash, payment_secret);
let mut ciphertext = sealed::PaymentMetadataTlv::from(self.clone()).encode();
let cipher = ChaCha20Poly1305::new(Key::new(keys.encryption_key), Nonce::new(nonce));
let tag = cipher.encrypt(&mut ciphertext, Some(PAYMENT_METADATA_AAD));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would really check to see if anything useful can be exposed from lightning/src/crypto rather than doing it again here. And maybe also just drop encryption from this PR until there is an answer to that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Well, upstream we use the same library (chacha20poly1305), which is why adding the direct dependency is also fine, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What I was trying to question is whether RL has a reusable layer above the raw ChaCha call, something that already standardizes the envelope, key separation, nonce handling, etc.


let mut raw = Vec::with_capacity(tag.len() + ciphertext.len());
raw.extend_from_slice(&tag);
raw.extend_from_slice(&ciphertext);

EncryptedPaymentMetadata { raw }
}
}

impl EncryptedPaymentMetadata {
pub(crate) fn from_raw(raw: Vec<u8>) -> Self {
Self { raw }
}

pub(crate) fn decrypt(
&self, keys: &PaymentMetadataKeys, payment_hash: &PaymentHash,
payment_secret: &PaymentSecret,
) -> Option<PaymentMetadata> {
if self.raw.len() < PAYMENT_METADATA_TAG_LEN {
return None;
}

let mut tag = [0u8; PAYMENT_METADATA_TAG_LEN];
tag.copy_from_slice(&self.raw[..PAYMENT_METADATA_TAG_LEN]);

let mut plaintext = self.raw[PAYMENT_METADATA_TAG_LEN..].to_vec();
let nonce = keys.nonce(payment_hash, payment_secret);
let cipher = ChaCha20Poly1305::new(Key::new(keys.encryption_key), Nonce::new(nonce));
cipher.decrypt(&mut plaintext, tag, Some(PAYMENT_METADATA_AAD)).ok()?;

sealed::PaymentMetadataTlv::read(&mut &plaintext[..]).ok().map(Into::into)
}
}

fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
let mut engine = HmacEngine::<sha256::Hash>::new(key);
engine.input(data);
Hmac::<sha256::Hash>::from_engine(engine).to_byte_array()
}

mod sealed {
use lightning::impl_writeable_tlv_based;

use crate::payment::metadata::PaymentMetadata;
use crate::payment::store::LSPS2Parameters;

pub(super) struct PaymentMetadataTlv {
pub(super) lsps2_parameters: Option<LSPS2Parameters>,
}

impl_writeable_tlv_based!(PaymentMetadataTlv, {
(0, lsps2_parameters, option),
});

impl From<PaymentMetadata> for PaymentMetadataTlv {
fn from(metadata: PaymentMetadata) -> Self {
Self { lsps2_parameters: metadata.lsps2_parameters }
}
}

impl From<PaymentMetadataTlv> for PaymentMetadata {
fn from(metadata: PaymentMetadataTlv) -> Self {
Self { lsps2_parameters: metadata.lsps2_parameters }
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn empty_metadata_encrypts_and_decrypts() {
let metadata = PaymentMetadata { lsps2_parameters: None };
let keys = PaymentMetadataKeys::new([42; 32]);
let payment_hash = PaymentHash([7; 32]);
let payment_secret = PaymentSecret([8; 32]);

let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
let decrypted = encrypted.decrypt(&keys, &payment_hash, &payment_secret).unwrap();

assert_eq!(metadata, decrypted);
}

#[test]
fn lsps2_parameters_encrypt_and_decrypt() {
let lsps2_parameters = LSPS2Parameters {
max_total_opening_fee_msat: Some(42_000),
max_proportional_opening_fee_ppm_msat: Some(17_000),
};
let metadata = PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) };
let keys = PaymentMetadataKeys::new([42; 32]);
let payment_hash = PaymentHash([7; 32]);
let payment_secret = PaymentSecret([8; 32]);

let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
let decrypted = encrypted.decrypt(&keys, &payment_hash, &payment_secret).unwrap();

assert_eq!(metadata, decrypted);
}

#[test]
fn encrypted_metadata_uses_deterministic_context_nonce() {
let metadata = PaymentMetadata { lsps2_parameters: None };
let keys = PaymentMetadataKeys::new([42; 32]);
let payment_hash = PaymentHash([7; 32]);
let payment_secret = PaymentSecret([8; 32]);

let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
let encrypted_again = metadata.encrypt(&keys, &payment_hash, &payment_secret);

assert_eq!(encrypted.raw, encrypted_again.raw);
assert_eq!(encrypted.decrypt(&keys, &payment_hash, &payment_secret), Some(metadata));
}

#[test]
fn encrypted_metadata_requires_matching_key_and_context() {
let metadata = PaymentMetadata { lsps2_parameters: None };
let keys = PaymentMetadataKeys::new([42; 32]);
let wrong_keys = PaymentMetadataKeys::new([43; 32]);
let payment_hash = PaymentHash([7; 32]);
let wrong_payment_hash = PaymentHash([9; 32]);
let payment_secret = PaymentSecret([8; 32]);
let wrong_payment_secret = PaymentSecret([10; 32]);

let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);

assert_eq!(encrypted.decrypt(&wrong_keys, &payment_hash, &payment_secret), None);
assert_eq!(encrypted.decrypt(&keys, &wrong_payment_hash, &payment_secret), None);
assert_eq!(encrypted.decrypt(&keys, &payment_hash, &wrong_payment_secret), None);
assert_eq!(
EncryptedPaymentMetadata::from_raw(vec![0; PAYMENT_METADATA_TAG_LEN + 1]).decrypt(
&keys,
&payment_hash,
&payment_secret
),
None
);
}
}
5 changes: 4 additions & 1 deletion src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
pub(crate) mod asynchronous;
mod bolt11;
mod bolt12;
mod metadata;
mod onchain;
pub(crate) mod pending_payment_store;
mod spontaneous;
Expand All @@ -18,10 +19,12 @@ mod unified;

pub use bolt11::Bolt11Payment;
pub use bolt12::Bolt12Payment;
pub(crate) use metadata::{EncryptedPaymentMetadata, PaymentMetadata, PaymentMetadataKeys};
pub use onchain::OnchainPayment;
pub use pending_payment_store::PendingPaymentDetails;
pub use spontaneous::SpontaneousPayment;
pub use store::{
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
ConfirmationStatus, LSPS2Parameters, PaymentDetails, PaymentDirection, PaymentKind,
PaymentStatus,
};
pub use unified::{UnifiedPayment, UnifiedPaymentResult};
10 changes: 5 additions & 5 deletions src/payment/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ pub enum PaymentKind {
/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information.
///
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
lsp_fee_limits: LSPFeeLimits,
lsp_fee_limits: LSPS2Parameters,
},
/// A [BOLT 12] 'offer' payment, i.e., a payment for an [`Offer`].
///
Expand Down Expand Up @@ -529,7 +529,7 @@ impl_writeable_tlv_based_enum!(ConfirmationStatus,
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct LSPFeeLimits {
pub struct LSPS2Parameters {
/// The maximal total amount we allow any configured LSP withhold from us when forwarding the
/// payment.
pub max_total_opening_fee_msat: Option<u64>,
Expand All @@ -538,7 +538,7 @@ pub struct LSPFeeLimits {
pub max_proportional_opening_fee_ppm_msat: Option<u64>,
}

impl_writeable_tlv_based!(LSPFeeLimits, {
impl_writeable_tlv_based!(LSPS2Parameters, {
(0, max_total_opening_fee_msat, option),
(2, max_proportional_opening_fee_ppm_msat, option),
});
Expand Down Expand Up @@ -637,7 +637,7 @@ mod tests {
pub amount_msat: Option<u64>,
pub direction: PaymentDirection,
pub status: PaymentStatus,
pub lsp_fee_limits: Option<LSPFeeLimits>,
pub lsp_fee_limits: Option<LSPS2Parameters>,
}

impl_writeable_tlv_based!(OldPaymentDetails, {
Expand Down Expand Up @@ -695,7 +695,7 @@ mod tests {

// Test `Bolt11Jit` de/ser
{
let lsp_fee_limits = Some(LSPFeeLimits {
let lsp_fee_limits = Some(LSPS2Parameters {
max_total_opening_fee_msat: Some(46_000),
max_proportional_opening_fee_ppm_msat: Some(47_000),
});
Expand Down