Skip to content

Commit b7f58cd

Browse files
authored
Merge pull request #4628 from TheBlueMatt/2026-05-encrypt-metadata-internally
Encrypt `payment_metadata` when we build the payment secret
2 parents 6967470 + 4fac0fe commit b7f58cd

12 files changed

Lines changed: 395 additions & 152 deletions

fuzz/src/chanmon_consistency.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1529,7 +1529,7 @@ impl PaymentTracker {
15291529
let mut payment_preimage = PaymentPreimage([0; 32]);
15301530
payment_preimage.0[0..8].copy_from_slice(&self.payment_ctr.to_be_bytes());
15311531
let hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array());
1532-
let secret = dest
1532+
let (secret, _no_metadata) = dest
15331533
.create_inbound_payment_for_hash(hash, None, 3600, None, None)
15341534
.expect("create_inbound_payment_for_hash failed");
15351535
assert!(self.payment_preimages.insert(hash, payment_preimage).is_none());

lightning-liquidity/tests/lsps2_integration_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ fn create_jit_invoice(
120120
) -> Result<Bolt11Invoice, ()> {
121121
// LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual.
122122
let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2;
123-
let (payment_hash, payment_secret) = node
123+
let (payment_hash, payment_secret, _) = node
124124
.node
125125
.create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta), None)
126126
.map_err(|e| {

lightning/src/crypto/utils.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use bitcoin::hashes::sha256::Hash as Sha256;
33
use bitcoin::hashes::{Hash, HashEngine};
44
use bitcoin::secp256k1::{ecdsa::Signature, Message, Secp256k1, SecretKey, Signing};
55

6+
use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce};
7+
68
use crate::sign::EntropySource;
79

810
macro_rules! hkdf_extract_expand {
@@ -22,7 +24,7 @@ macro_rules! hkdf_extract_expand {
2224
let (k1, k2, _) = hkdf_extract_expand!($salt, $ikm);
2325
(k1, k2)
2426
}};
25-
($salt: expr, $ikm: expr, 7) => {{
27+
($salt: expr, $ikm: expr, 8) => {{
2628
let (k1, k2, prk) = hkdf_extract_expand!($salt, $ikm);
2729

2830
let mut hmac = HmacEngine::<Sha256>::new(&prk[..]);
@@ -50,18 +52,23 @@ macro_rules! hkdf_extract_expand {
5052
hmac.input(&[7; 1]);
5153
let k7 = Hmac::from_engine(hmac).to_byte_array();
5254

53-
(k1, k2, k3, k4, k5, k6, k7)
55+
let mut hmac = HmacEngine::<Sha256>::new(&prk[..]);
56+
hmac.input(&k7);
57+
hmac.input(&[8; 1]);
58+
let k8 = Hmac::from_engine(hmac).to_byte_array();
59+
60+
(k1, k2, k3, k4, k5, k6, k7, k8)
5461
}};
5562
}
5663

5764
pub fn hkdf_extract_expand_twice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32]) {
5865
hkdf_extract_expand!(salt, ikm, 2)
5966
}
6067

61-
pub fn hkdf_extract_expand_7x(
68+
pub fn hkdf_extract_expand_8x(
6269
salt: &[u8], ikm: &[u8],
63-
) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) {
64-
hkdf_extract_expand!(salt, ikm, 7)
70+
) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) {
71+
hkdf_extract_expand!(salt, ikm, 8)
6572
}
6673

6774
#[inline]
@@ -91,3 +98,12 @@ pub fn sign_with_aux_rand<C: Signing, ES: EntropySource>(
9198
let sig = sign(ctx, msg, sk);
9299
sig
93100
}
101+
102+
pub fn apply_chacha20(key: [u8; 32], nonce: [u8; 16], data: &mut [u8]) {
103+
ChaCha20::new_from_block(
104+
Key::new(key),
105+
Nonce::new(nonce[4..].try_into().unwrap()),
106+
u32::from_le_bytes(nonce[..4].try_into().unwrap()),
107+
)
108+
.apply_keystream(data);
109+
}

lightning/src/ln/bolt11_payment_tests.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() {
3030

3131
let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42];
3232

33-
let (payment_hash, payment_secret) =
34-
nodes[1].node.create_inbound_payment(None, 7200, None, Some(&payment_metadata)).unwrap();
33+
let (payment_hash, payment_secret, encrypted_metadata) = nodes[1]
34+
.node
35+
.create_inbound_payment(None, 7200, None, Some(payment_metadata.clone()))
36+
.unwrap();
3537

3638
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
3739
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
@@ -41,7 +43,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() {
4143
.duration_since_epoch(timestamp)
4244
.min_final_cltv_expiry_delta(144)
4345
.amount_milli_satoshis(50_000)
44-
.payment_metadata(payment_metadata.clone())
46+
.payment_metadata(encrypted_metadata.unwrap())
4547
.build_raw()
4648
.unwrap();
4749
let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap();
@@ -97,8 +99,10 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() {
9799

98100
let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42];
99101

100-
let (payment_hash, payment_secret) =
101-
nodes[1].node.create_inbound_payment(None, 7200, None, Some(&payment_metadata)).unwrap();
102+
let (payment_hash, payment_secret, encrypted_metadata) = nodes[1]
103+
.node
104+
.create_inbound_payment(None, 7200, None, Some(payment_metadata.clone()))
105+
.unwrap();
102106

103107
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
104108
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
@@ -107,7 +111,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() {
107111
.payment_secret(payment_secret)
108112
.duration_since_epoch(timestamp)
109113
.min_final_cltv_expiry_delta(144)
110-
.payment_metadata(payment_metadata.clone())
114+
.payment_metadata(encrypted_metadata.unwrap())
111115
.build_raw()
112116
.unwrap();
113117
let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap();

lightning/src/ln/channelmanager.rs

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8467,7 +8467,7 @@ impl<
84678467
payment_data,
84688468
payment_context,
84698469
phantom_shared_secret,
8470-
onion_fields,
8470+
mut onion_fields,
84718471
has_recipient_created_payment_secret,
84728472
invoice_request_opt,
84738473
trampoline_shared_secret,
@@ -8608,7 +8608,7 @@ impl<
86088608
let verify_res = inbound_payment::verify(
86098609
payment_hash,
86108610
&payment_data,
8611-
onion_fields.payment_metadata.as_deref(),
8611+
onion_fields.payment_metadata.as_mut(),
86128612
self.highest_seen_timestamp.load(Ordering::Acquire) as u64,
86138613
&self.inbound_payment_key,
86148614
&self.logger,
@@ -14477,24 +14477,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1447714477
}
1447814478
}
1447914479

14480-
let (payment_hash, payment_secret) = match payment_hash {
14480+
let (payment_hash, payment_secret, payment_metadata) = match payment_hash {
1448114481
Some(payment_hash) => {
14482-
let payment_secret = self
14482+
let (payment_secret, payment_metadata) = self
1448314483
.create_inbound_payment_for_hash(
1448414484
payment_hash, amount_msats,
1448514485
invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32),
1448614486
min_final_cltv_expiry_delta,
14487-
payment_metadata.as_deref(),
14487+
payment_metadata,
1448814488
)
1448914489
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
14490-
(payment_hash, payment_secret)
14490+
(payment_hash, payment_secret, payment_metadata)
1449114491
},
1449214492
None => {
1449314493
self
1449414494
.create_inbound_payment(
1449514495
amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32),
1449614496
min_final_cltv_expiry_delta,
14497-
payment_metadata.as_deref(),
14497+
payment_metadata,
1449814498
)
1449914499
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?
1450014500
},
@@ -14621,8 +14621,7 @@ pub struct Bolt11InvoiceParameters {
1462114621
/// onion by the sender, available as [`RecipientOnionFields::payment_metadata`] via
1462214622
/// [`Event::PaymentClaimable::onion_fields`].
1462314623
///
14624-
/// Note that because it is exposed to the sender in the invoice you should consider encrypting
14625-
/// it. It is committed to, however, so cannot be modified by the sender.
14624+
/// The metadata itself is encrypted and HMAC'd before being stored in the BOLT 11 invoice.
1462614625
pub payment_metadata: Option<Vec<u8>>,
1462714626
}
1462814627

@@ -15128,6 +15127,7 @@ impl<
1512815127
|amount_msats, relative_expiry| {
1512915128
self.create_inbound_payment(Some(amount_msats), relative_expiry, None, None)
1513015129
.map_err(|()| Bolt12SemanticError::InvalidAmount)
15130+
.map(|(preimage, secret, _no_metadata)| (preimage, secret))
1513115131
},
1513215132
None,
1513315133
)?;
@@ -15138,8 +15138,8 @@ impl<
1513815138
Ok(invoice)
1513915139
}
1514015140

15141-
/// Gets a payment secret and payment hash for use in an invoice given to a third party wishing
15142-
/// to pay us.
15141+
/// Gets a payment secret, payment hash, and encrypts the `payment_metadata` for use in an
15142+
/// invoice given to a third party wishing to pay us.
1514315143
///
1514415144
/// This differs from [`create_inbound_payment_for_hash`] only in that it generates the
1514515145
/// [`PaymentHash`] and [`PaymentPreimage`] for you.
@@ -15170,8 +15170,8 @@ impl<
1517015170
/// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash
1517115171
pub fn create_inbound_payment(
1517215172
&self, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
15173-
min_final_cltv_expiry_delta: Option<u16>, payment_metadata: Option<&[u8]>,
15174-
) -> Result<(PaymentHash, PaymentSecret), ()> {
15173+
min_final_cltv_expiry_delta: Option<u16>, payment_metadata: Option<Vec<u8>>,
15174+
) -> Result<(PaymentHash, PaymentSecret, Option<Vec<u8>>), ()> {
1517515175
inbound_payment::create(
1517615176
&self.inbound_payment_key,
1517715177
min_value_msat,
@@ -15183,8 +15183,8 @@ impl<
1518315183
)
1518415184
}
1518515185

15186-
/// Gets a [`PaymentSecret`] for a given [`PaymentHash`], for which the payment preimage is
15187-
/// stored external to LDK.
15186+
/// Gets a [`PaymentSecret`] for a given [`PaymentHash`] (for which the payment preimage is
15187+
/// stored external to LDK) and encrypts the `payment_metadata`.
1518815188
///
1518915189
/// A [`PaymentClaimable`] event will only be generated if the [`PaymentSecret`] matches a
1519015190
/// payment secret fetched via this method or [`create_inbound_payment`], and which is at least
@@ -15220,41 +15220,34 @@ impl<
1522015220
/// Note that a malicious eavesdropper can intuit whether an inbound payment was created by
1522115221
/// `create_inbound_payment` or `create_inbound_payment_for_hash` based on runtime.
1522215222
///
15223-
/// # Note
15224-
///
15225-
/// If you register an inbound payment with this method, then serialize the `ChannelManager`, then
15226-
/// deserialize it with a node running 0.0.103 and earlier, the payment will fail to be received.
15227-
///
1522815223
/// Errors if `min_value_msat` is greater than total bitcoin supply.
1522915224
///
15230-
/// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
15231-
/// on versions of LDK prior to 0.0.114.
15232-
///
1523315225
/// [`create_inbound_payment`]: Self::create_inbound_payment
1523415226
/// [`PaymentClaimable`]: events::Event::PaymentClaimable
1523515227
pub fn create_inbound_payment_for_hash(
1523615228
&self, payment_hash: PaymentHash, min_value_msat: Option<u64>,
1523715229
invoice_expiry_delta_secs: u32, min_final_cltv_expiry: Option<u16>,
15238-
payment_metadata: Option<&[u8]>,
15239-
) -> Result<PaymentSecret, ()> {
15230+
payment_metadata: Option<Vec<u8>>,
15231+
) -> Result<(PaymentSecret, Option<Vec<u8>>), ()> {
1524015232
inbound_payment::create_from_hash(
1524115233
&self.inbound_payment_key,
1524215234
min_value_msat,
1524315235
payment_hash,
1524415236
invoice_expiry_delta_secs,
15237+
&self.entropy_source,
1524515238
self.highest_seen_timestamp.load(Ordering::Acquire) as u64,
1524615239
min_final_cltv_expiry,
1524715240
payment_metadata,
1524815241
)
1524915242
}
1525015243

15251-
/// Gets an LDK-generated payment preimage from a payment hash, metadata and secret that were
15252-
/// previously returned from [`create_inbound_payment`].
15244+
/// Gets an LDK-generated payment preimage from a payment hash and secret and decrypts the
15245+
/// metadata (if any) that were previously returned from [`create_inbound_payment`].
1525315246
///
1525415247
/// [`create_inbound_payment`]: Self::create_inbound_payment
15255-
pub fn get_payment_preimage(
15248+
pub fn get_payment_preimage_decrypt_metadata(
1525615249
&self, payment_hash: PaymentHash, payment_secret: PaymentSecret,
15257-
payment_metadata: Option<&[u8]>,
15250+
payment_metadata: Option<&mut [u8]>,
1525815251
) -> Result<PaymentPreimage, APIError> {
1525915252
let expanded_key = &self.inbound_payment_key;
1526015253
inbound_payment::get_payment_preimage(
@@ -17336,7 +17329,9 @@ impl<
1733617329
relative_expiry,
1733717330
None,
1733817331
None,
17339-
).map_err(|_| Bolt12SemanticError::InvalidAmount)
17332+
)
17333+
.map_err(|_| Bolt12SemanticError::InvalidAmount)
17334+
.map(|(preimage, secret, _no_metadata)| (preimage, secret))
1734017335
};
1734117336

1734217337
let (result, context) = match invoice_request {
@@ -22238,7 +22233,8 @@ pub mod bench {
2223822233
payment_preimage.0[0..8].copy_from_slice(&payment_count.to_le_bytes());
2223922234
payment_count += 1;
2224022235
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array());
22241-
let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap();
22236+
let (payment_secret, _no_payment_metadata) =
22237+
$node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap();
2224222238

2224322239
$node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret, 10_000),
2224422240
PaymentId(payment_hash.0),

lightning/src/ln/functional_test_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2800,7 +2800,7 @@ pub fn get_payment_preimage_hash(
28002800
let payment_preimage = PaymentPreimage([*payment_count; 32]);
28012801
*payment_count += 1;
28022802
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array());
2803-
let payment_secret = recipient
2803+
let (payment_secret, _) = recipient
28042804
.node
28052805
.create_inbound_payment_for_hash(
28062806
payment_hash,

0 commit comments

Comments
 (0)