@@ -6,8 +6,10 @@ import fr.acinq.bitcoin.io.ByteArrayOutput
6
6
import fr.acinq.bitcoin.io.Input
7
7
import fr.acinq.bitcoin.io.Output
8
8
import fr.acinq.lightning.MilliSatoshi
9
+ import fr.acinq.lightning.crypto.ChaCha20Poly1305
9
10
import fr.acinq.lightning.utils.msat
10
11
import fr.acinq.lightning.wire.LightningCodecs
12
+ import fr.acinq.lightning.wire.OfferTypes
11
13
12
14
/* *
13
15
* The flow for Bolt 12 offer payments is the following:
@@ -37,6 +39,7 @@ sealed class OfferPaymentMetadata {
37
39
LightningCodecs .writeByte(this .version.toInt(), out )
38
40
when (this ) {
39
41
is V1 -> this .write(out )
42
+ is V2 -> this .write(out )
40
43
}
41
44
return out .toByteArray().byteVector()
42
45
}
@@ -48,6 +51,25 @@ sealed class OfferPaymentMetadata {
48
51
val signature = Crypto .sign(Crypto .sha256(encoded), nodeKey)
49
52
encoded + signature
50
53
}
54
+ is V2 -> {
55
+ // We only encrypt what comes after the version byte.
56
+ val encoded = run {
57
+ val out = ByteArrayOutput ()
58
+ this .write(out )
59
+ out .toByteArray()
60
+ }
61
+ val (encrypted, mac) = run {
62
+ val paymentHash = Crypto .sha256(this .preimage).byteVector32()
63
+ val priv = V2 .deriveKey(nodeKey, paymentHash)
64
+ val nonce = paymentHash.take(12 ).toByteArray()
65
+ ChaCha20Poly1305 .encrypt(priv.value.toByteArray(), nonce, encoded, paymentHash.toByteArray())
66
+ }
67
+ val out = ByteArrayOutput ()
68
+ out .write(2 ) // version
69
+ out .write(encrypted)
70
+ out .write(mac)
71
+ out .toByteArray().byteVector()
72
+ }
51
73
}
52
74
53
75
/* * In this first version, we simply sign the payment metadata to verify its authenticity when receiving the payment. */
@@ -86,6 +108,69 @@ sealed class OfferPaymentMetadata {
86
108
}
87
109
}
88
110
111
+ /* * In this version, we encrypt the payment metadata with a key derived from our seed. */
112
+ data class V2 (
113
+ override val offerId : ByteVector32 ,
114
+ override val amount : MilliSatoshi ,
115
+ override val preimage : ByteVector32 ,
116
+ val payerKey : PublicKey ,
117
+ val payerNote : String? ,
118
+ val quantity : Long ,
119
+ val contactSecret : ByteVector32 ? ,
120
+ val payerOffer : OfferTypes .Offer ? ,
121
+ val payerAddress : ContactAddress ? ,
122
+ override val createdAtMillis : Long
123
+ ) : OfferPaymentMetadata() {
124
+ override val version: Byte get() = 2
125
+
126
+ private fun writeOptionalBytes (data : ByteArray? , out : Output ) = when (data) {
127
+ null -> LightningCodecs .writeU16(0 , out )
128
+ else -> {
129
+ LightningCodecs .writeU16(data.size, out )
130
+ LightningCodecs .writeBytes(data, out )
131
+ }
132
+ }
133
+
134
+ fun write (out : Output ) {
135
+ LightningCodecs .writeBytes(offerId, out )
136
+ LightningCodecs .writeU64(amount.toLong(), out )
137
+ LightningCodecs .writeBytes(preimage, out )
138
+ LightningCodecs .writeBytes(payerKey.value, out )
139
+ writeOptionalBytes(payerNote?.encodeToByteArray(), out )
140
+ LightningCodecs .writeU64(quantity, out )
141
+ writeOptionalBytes(contactSecret?.toByteArray(), out )
142
+ writeOptionalBytes(payerOffer?.let { OfferTypes .Offer .tlvSerializer.write(it.records) }, out )
143
+ writeOptionalBytes(payerAddress?.toString()?.encodeToByteArray(), out )
144
+ LightningCodecs .writeU64(createdAtMillis, out )
145
+ }
146
+
147
+ companion object {
148
+ private fun readOptionalBytes (input : Input ): ByteArray? = when (val size = LightningCodecs .u16(input)) {
149
+ 0 -> null
150
+ else -> LightningCodecs .bytes(input, size)
151
+ }
152
+
153
+ fun read (input : Input ): V2 {
154
+ val offerId = LightningCodecs .bytes(input, 32 ).byteVector32()
155
+ val amount = LightningCodecs .u64(input).msat
156
+ val preimage = LightningCodecs .bytes(input, 32 ).byteVector32()
157
+ val payerKey = PublicKey (LightningCodecs .bytes(input, 33 ))
158
+ val payerNote = readOptionalBytes(input)?.decodeToString()
159
+ val quantity = LightningCodecs .u64(input)
160
+ val contactSecret = readOptionalBytes(input)?.byteVector32()
161
+ val payerOffer = readOptionalBytes(input)?.let { OfferTypes .Offer .tlvSerializer.read(it) }?.let { OfferTypes .Offer (it) }
162
+ val payerAddress = readOptionalBytes(input)?.decodeToString()?.let { ContactAddress .fromString(it) }
163
+ val createdAtMillis = LightningCodecs .u64(input)
164
+ return V2 (offerId, amount, preimage, payerKey, payerNote, quantity, contactSecret, payerOffer, payerAddress, createdAtMillis)
165
+ }
166
+
167
+ fun deriveKey (nodeKey : PrivateKey , paymentHash : ByteVector32 ): PrivateKey {
168
+ val tweak = Crypto .sha256(" offer_payment_metadata_v2" .encodeToByteArray() + paymentHash.toByteArray() + nodeKey.value.toByteArray())
169
+ return nodeKey * PrivateKey (tweak)
170
+ }
171
+ }
172
+ }
173
+
89
174
companion object {
90
175
/* *
91
176
* Decode an [OfferPaymentMetadata] encoded using [encode] (e.g. from our payments DB).
@@ -95,6 +180,7 @@ sealed class OfferPaymentMetadata {
95
180
val input = ByteArrayInput (encoded.toByteArray())
96
181
return when (val version = LightningCodecs .byte(input)) {
97
182
1 -> V1 .read(input)
183
+ 2 -> V2 .read(input)
98
184
else -> throw IllegalArgumentException (" unknown offer payment metadata version: $version " )
99
185
}
100
186
}
@@ -103,7 +189,7 @@ sealed class OfferPaymentMetadata {
103
189
* Decode an [OfferPaymentMetadata] stored in a blinded path's path_id field.
104
190
* @return null if the path_id doesn't contain valid data created by us.
105
191
*/
106
- fun fromPathId (nodeId : PublicKey , pathId : ByteVector ): OfferPaymentMetadata ? {
192
+ fun fromPathId (nodeKey : PrivateKey , pathId : ByteVector , paymentHash : ByteVector32 ): OfferPaymentMetadata ? {
107
193
if (pathId.isEmpty()) return null
108
194
val input = ByteArrayInput (pathId.toByteArray())
109
195
when (LightningCodecs .byte(input)) {
@@ -113,10 +199,23 @@ sealed class OfferPaymentMetadata {
113
199
val metadata = LightningCodecs .bytes(input, metadataSize)
114
200
val signature = LightningCodecs .bytes(input, 64 ).byteVector64()
115
201
// Note that the signature includes the version byte.
116
- if (! Crypto .verifySignature(Crypto .sha256(pathId.take(1 + metadataSize)), signature, nodeId )) return null
202
+ if (! Crypto .verifySignature(Crypto .sha256(pathId.take(1 + metadataSize)), signature, nodeKey.publicKey() )) return null
117
203
// This call is safe since we verified that we have the right number of bytes and the signature was valid.
118
204
return V1 .read(ByteArrayInput (metadata))
119
205
}
206
+ 2 -> {
207
+ val priv = V2 .deriveKey(nodeKey, paymentHash)
208
+ val nonce = paymentHash.take(12 ).toByteArray()
209
+ val encryptedSize = input.availableBytes - 16
210
+ return try {
211
+ val encrypted = LightningCodecs .bytes(input, encryptedSize)
212
+ val mac = LightningCodecs .bytes(input, 16 )
213
+ val decrypted = ChaCha20Poly1305 .decrypt(priv.value.toByteArray(), nonce, encrypted, paymentHash.toByteArray(), mac)
214
+ V2 .read(ByteArrayInput (decrypted))
215
+ } catch (_: Throwable ) {
216
+ null
217
+ }
218
+ }
120
219
else -> return null
121
220
}
122
221
}
0 commit comments