@@ -6,8 +6,10 @@ import fr.acinq.bitcoin.io.ByteArrayOutput
66import fr.acinq.bitcoin.io.Input
77import fr.acinq.bitcoin.io.Output
88import fr.acinq.lightning.MilliSatoshi
9+ import fr.acinq.lightning.crypto.ChaCha20Poly1305
910import fr.acinq.lightning.utils.msat
1011import fr.acinq.lightning.wire.LightningCodecs
12+ import fr.acinq.lightning.wire.OfferTypes
1113
1214/* *
1315 * The flow for Bolt 12 offer payments is the following:
@@ -37,6 +39,7 @@ sealed class OfferPaymentMetadata {
3739 LightningCodecs .writeByte(this .version.toInt(), out )
3840 when (this ) {
3941 is V1 -> this .write(out )
42+ is V2 -> this .write(out )
4043 }
4144 return out .toByteArray().byteVector()
4245 }
@@ -48,6 +51,25 @@ sealed class OfferPaymentMetadata {
4851 val signature = Crypto .sign(Crypto .sha256(encoded), nodeKey)
4952 encoded + signature
5053 }
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+ }
5173 }
5274
5375 /* * 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 {
86108 }
87109 }
88110
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+
89174 companion object {
90175 /* *
91176 * Decode an [OfferPaymentMetadata] encoded using [encode] (e.g. from our payments DB).
@@ -95,6 +180,7 @@ sealed class OfferPaymentMetadata {
95180 val input = ByteArrayInput (encoded.toByteArray())
96181 return when (val version = LightningCodecs .byte(input)) {
97182 1 -> V1 .read(input)
183+ 2 -> V2 .read(input)
98184 else -> throw IllegalArgumentException (" unknown offer payment metadata version: $version " )
99185 }
100186 }
@@ -103,7 +189,7 @@ sealed class OfferPaymentMetadata {
103189 * Decode an [OfferPaymentMetadata] stored in a blinded path's path_id field.
104190 * @return null if the path_id doesn't contain valid data created by us.
105191 */
106- fun fromPathId (nodeId : PublicKey , pathId : ByteVector ): OfferPaymentMetadata ? {
192+ fun fromPathId (nodeKey : PrivateKey , pathId : ByteVector , paymentHash : ByteVector32 ): OfferPaymentMetadata ? {
107193 if (pathId.isEmpty()) return null
108194 val input = ByteArrayInput (pathId.toByteArray())
109195 when (LightningCodecs .byte(input)) {
@@ -113,10 +199,23 @@ sealed class OfferPaymentMetadata {
113199 val metadata = LightningCodecs .bytes(input, metadataSize)
114200 val signature = LightningCodecs .bytes(input, 64 ).byteVector64()
115201 // 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
117203 // This call is safe since we verified that we have the right number of bytes and the signature was valid.
118204 return V1 .read(ByteArrayInput (metadata))
119205 }
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+ }
120219 else -> return null
121220 }
122221 }
0 commit comments