Skip to content

Commit ec1b601

Browse files
committed
Add encrypted payment metadata helpers
1 parent 77172d2 commit ec1b601

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ bip39 = { version = "2.0.0", features = ["rand"] }
6666
bip21 = { version = "0.5", features = ["std"], default-features = false }
6767

6868
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
69+
chacha20-poly1305 = { version = "0.1.2", default-features = false, features = ["std"] }
6970
getrandom = { version = "0.3", default-features = false }
7071
chrono = { version = "0.4", default-features = false, features = ["clock"] }
7172
tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros", "net" ] }

src/payment/metadata.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
9+
use chacha20_poly1305::{ChaCha20Poly1305, Key, Nonce};
10+
use lightning::util::ser::{Readable, Writeable};
11+
use lightning_types::payment::{PaymentHash, PaymentSecret};
12+
13+
use crate::payment::store::LSPS2Parameters;
14+
15+
/// Metadata carried in invoice payment metadata fields.
16+
#[derive(Clone, Debug, PartialEq, Eq)]
17+
pub(crate) struct PaymentMetadata {
18+
pub(crate) lsps2_parameters: Option<LSPS2Parameters>,
19+
}
20+
21+
#[derive(Clone, Copy)]
22+
pub(crate) struct PaymentMetadataKeys {
23+
encryption_key: [u8; 32],
24+
nonce_key: [u8; 32],
25+
}
26+
27+
impl PaymentMetadataKeys {
28+
pub(crate) fn new(base_secret: [u8; 32]) -> Self {
29+
Self {
30+
encryption_key: hmac_sha256(&base_secret, b"ldk_node_payment_metadata_encryption_key"),
31+
nonce_key: hmac_sha256(&base_secret, b"ldk_node_payment_metadata_nonce_key"),
32+
}
33+
}
34+
35+
fn nonce(&self, payment_hash: &PaymentHash, payment_secret: &PaymentSecret) -> [u8; 12] {
36+
let mut engine = HmacEngine::<sha256::Hash>::new(&self.nonce_key);
37+
engine.input(b"ldk_node_payment_metadata_nonce");
38+
engine.input(&payment_hash.0);
39+
engine.input(&payment_secret.0);
40+
let hmac = Hmac::<sha256::Hash>::from_engine(engine).to_byte_array();
41+
42+
let mut nonce = [0u8; 12];
43+
nonce.copy_from_slice(&hmac[..12]);
44+
nonce
45+
}
46+
}
47+
48+
const PAYMENT_METADATA_AAD: &[u8] = b"ldk_node_payment_metadata";
49+
const PAYMENT_METADATA_TAG_LEN: usize = 16;
50+
51+
/// Encrypted invoice payment metadata.
52+
pub(crate) struct EncryptedPaymentMetadata {
53+
pub(crate) raw: Vec<u8>,
54+
}
55+
56+
impl PaymentMetadata {
57+
pub(crate) fn encrypt(
58+
&self, keys: &PaymentMetadataKeys, payment_hash: &PaymentHash,
59+
payment_secret: &PaymentSecret,
60+
) -> EncryptedPaymentMetadata {
61+
let nonce = keys.nonce(payment_hash, payment_secret);
62+
let mut ciphertext = sealed::PaymentMetadataTlv::from(self.clone()).encode();
63+
let cipher = ChaCha20Poly1305::new(Key::new(keys.encryption_key), Nonce::new(nonce));
64+
let tag = cipher.encrypt(&mut ciphertext, Some(PAYMENT_METADATA_AAD));
65+
66+
let mut raw = Vec::with_capacity(tag.len() + ciphertext.len());
67+
raw.extend_from_slice(&tag);
68+
raw.extend_from_slice(&ciphertext);
69+
70+
EncryptedPaymentMetadata { raw }
71+
}
72+
}
73+
74+
impl EncryptedPaymentMetadata {
75+
pub(crate) fn from_raw(raw: Vec<u8>) -> Self {
76+
Self { raw }
77+
}
78+
79+
pub(crate) fn decrypt(
80+
&self, keys: &PaymentMetadataKeys, payment_hash: &PaymentHash,
81+
payment_secret: &PaymentSecret,
82+
) -> Option<PaymentMetadata> {
83+
if self.raw.len() < PAYMENT_METADATA_TAG_LEN {
84+
return None;
85+
}
86+
87+
let mut tag = [0u8; PAYMENT_METADATA_TAG_LEN];
88+
tag.copy_from_slice(&self.raw[..PAYMENT_METADATA_TAG_LEN]);
89+
90+
let mut plaintext = self.raw[PAYMENT_METADATA_TAG_LEN..].to_vec();
91+
let nonce = keys.nonce(payment_hash, payment_secret);
92+
let cipher = ChaCha20Poly1305::new(Key::new(keys.encryption_key), Nonce::new(nonce));
93+
cipher.decrypt(&mut plaintext, tag, Some(PAYMENT_METADATA_AAD)).ok()?;
94+
95+
sealed::PaymentMetadataTlv::read(&mut &plaintext[..]).ok().map(Into::into)
96+
}
97+
}
98+
99+
fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
100+
let mut engine = HmacEngine::<sha256::Hash>::new(key);
101+
engine.input(data);
102+
Hmac::<sha256::Hash>::from_engine(engine).to_byte_array()
103+
}
104+
105+
mod sealed {
106+
use lightning::impl_writeable_tlv_based;
107+
108+
use crate::payment::metadata::PaymentMetadata;
109+
use crate::payment::store::LSPS2Parameters;
110+
111+
pub(super) struct PaymentMetadataTlv {
112+
pub(super) lsps2_parameters: Option<LSPS2Parameters>,
113+
}
114+
115+
impl_writeable_tlv_based!(PaymentMetadataTlv, {
116+
(0, lsps2_parameters, option),
117+
});
118+
119+
impl From<PaymentMetadata> for PaymentMetadataTlv {
120+
fn from(metadata: PaymentMetadata) -> Self {
121+
Self { lsps2_parameters: metadata.lsps2_parameters }
122+
}
123+
}
124+
125+
impl From<PaymentMetadataTlv> for PaymentMetadata {
126+
fn from(metadata: PaymentMetadataTlv) -> Self {
127+
Self { lsps2_parameters: metadata.lsps2_parameters }
128+
}
129+
}
130+
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
135+
136+
#[test]
137+
fn empty_metadata_encrypts_and_decrypts() {
138+
let metadata = PaymentMetadata { lsps2_parameters: None };
139+
let keys = PaymentMetadataKeys::new([42; 32]);
140+
let payment_hash = PaymentHash([7; 32]);
141+
let payment_secret = PaymentSecret([8; 32]);
142+
143+
let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
144+
let decrypted = encrypted.decrypt(&keys, &payment_hash, &payment_secret).unwrap();
145+
146+
assert_eq!(metadata, decrypted);
147+
}
148+
149+
#[test]
150+
fn lsps2_parameters_encrypt_and_decrypt() {
151+
let lsps2_parameters = LSPS2Parameters {
152+
max_total_opening_fee_msat: Some(42_000),
153+
max_proportional_opening_fee_ppm_msat: Some(17_000),
154+
};
155+
let metadata = PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) };
156+
let keys = PaymentMetadataKeys::new([42; 32]);
157+
let payment_hash = PaymentHash([7; 32]);
158+
let payment_secret = PaymentSecret([8; 32]);
159+
160+
let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
161+
let decrypted = encrypted.decrypt(&keys, &payment_hash, &payment_secret).unwrap();
162+
163+
assert_eq!(metadata, decrypted);
164+
}
165+
166+
#[test]
167+
fn encrypted_metadata_uses_deterministic_context_nonce() {
168+
let metadata = PaymentMetadata { lsps2_parameters: None };
169+
let keys = PaymentMetadataKeys::new([42; 32]);
170+
let payment_hash = PaymentHash([7; 32]);
171+
let payment_secret = PaymentSecret([8; 32]);
172+
173+
let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
174+
let encrypted_again = metadata.encrypt(&keys, &payment_hash, &payment_secret);
175+
176+
assert_eq!(encrypted.raw, encrypted_again.raw);
177+
assert_eq!(encrypted.decrypt(&keys, &payment_hash, &payment_secret), Some(metadata));
178+
}
179+
180+
#[test]
181+
fn encrypted_metadata_requires_matching_key_and_context() {
182+
let metadata = PaymentMetadata { lsps2_parameters: None };
183+
let keys = PaymentMetadataKeys::new([42; 32]);
184+
let wrong_keys = PaymentMetadataKeys::new([43; 32]);
185+
let payment_hash = PaymentHash([7; 32]);
186+
let wrong_payment_hash = PaymentHash([9; 32]);
187+
let payment_secret = PaymentSecret([8; 32]);
188+
let wrong_payment_secret = PaymentSecret([10; 32]);
189+
190+
let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret);
191+
192+
assert_eq!(encrypted.decrypt(&wrong_keys, &payment_hash, &payment_secret), None);
193+
assert_eq!(encrypted.decrypt(&keys, &wrong_payment_hash, &payment_secret), None);
194+
assert_eq!(encrypted.decrypt(&keys, &payment_hash, &wrong_payment_secret), None);
195+
assert_eq!(
196+
EncryptedPaymentMetadata::from_raw(vec![0; PAYMENT_METADATA_TAG_LEN + 1]).decrypt(
197+
&keys,
198+
&payment_hash,
199+
&payment_secret
200+
),
201+
None
202+
);
203+
}
204+
}

src/payment/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
pub(crate) mod asynchronous;
1111
mod bolt11;
1212
mod bolt12;
13+
mod metadata;
1314
mod onchain;
1415
pub(crate) mod pending_payment_store;
1516
mod spontaneous;
@@ -18,6 +19,7 @@ mod unified;
1819

1920
pub use bolt11::Bolt11Payment;
2021
pub use bolt12::Bolt12Payment;
22+
pub(crate) use metadata::{EncryptedPaymentMetadata, PaymentMetadata, PaymentMetadataKeys};
2123
pub use onchain::OnchainPayment;
2224
pub use pending_payment_store::PendingPaymentDetails;
2325
pub use spontaneous::SpontaneousPayment;

0 commit comments

Comments
 (0)