Skip to content

Commit 83d4580

Browse files
committed
add fat error encryption and decryption
1 parent ca23184 commit 83d4580

File tree

6 files changed

+670
-2
lines changed

6 files changed

+670
-2
lines changed

crypto.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ func (p *PrivKeyECDH) PubKey() *btcec.PublicKey {
6161
// k is our private key, and P is the public key, we perform the following
6262
// operation:
6363
//
64-
// sx := k*P
65-
// s := sha256(sx.SerializeCompressed())
64+
// sx := k*P
65+
// s := sha256(sx.SerializeCompressed())
6666
//
6767
// NOTE: This is part of the SingleKeyECDH interface.
6868
func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) {
@@ -92,6 +92,10 @@ type DecryptedError struct {
9292

9393
// Message is the decrypted error message.
9494
Message []byte
95+
96+
// HoldTimesMs is an array of millisecond durations reported by each node on
97+
// the (error) path.
98+
HoldTimesMs []uint64
9599
}
96100

97101
// zeroHMAC is the special HMAC value that allows the final node to determine

fat_error_crypto.go

+326
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package sphinx
2+
3+
import (
4+
"bytes"
5+
"crypto/hmac"
6+
"crypto/sha256"
7+
"encoding/binary"
8+
"errors"
9+
"fmt"
10+
)
11+
12+
var byteOrder = binary.BigEndian
13+
14+
// DecryptError attempts to decrypt the passed encrypted error response. The
15+
// onion failure is encrypted in backward manner, starting from the node where
16+
// error have occurred. As a result, in order to decrypt the error we need get
17+
// all shared secret and apply decryption in the reverse order. A structure is
18+
// returned that contains the decrypted error message and information on the
19+
// sender.
20+
func (o *OnionErrorDecrypter) DecryptFatError(encryptedData []byte) (
21+
*DecryptedError, error) {
22+
23+
// Ensure the error message length is enough to contain the payloads and
24+
// hmacs blocks. Otherwise blame the first hop.
25+
if len(encryptedData) < 256+hmacsAndPayloadsLen {
26+
return &DecryptedError{
27+
SenderIdx: 1,
28+
Sender: o.circuit.PaymentPath[0],
29+
}, nil
30+
}
31+
32+
sharedSecrets, err := generateSharedSecrets(
33+
o.circuit.PaymentPath,
34+
o.circuit.SessionKey,
35+
)
36+
if err != nil {
37+
return nil, fmt.Errorf("error generating shared secret: %w", err)
38+
}
39+
40+
var (
41+
sender int
42+
msg []byte
43+
dummySecret Hash256
44+
)
45+
copy(dummySecret[:], bytes.Repeat([]byte{1}, 32))
46+
47+
// We'll iterate a constant amount of hops to ensure that we don't give
48+
// away an timing information pertaining to the position in the route
49+
// that the error emanated from.
50+
holdTimesMs := make([]uint64, 0)
51+
for i := 0; i < NumMaxHops; i++ {
52+
var sharedSecret Hash256
53+
54+
// If we've already found the sender, then we'll use our dummy
55+
// secret to continue decryption attempts to fill out the rest
56+
// of the loop. Otherwise, we'll use the next shared secret in
57+
// line.
58+
if sender != 0 || i > len(sharedSecrets)-1 {
59+
sharedSecret = dummySecret
60+
} else {
61+
sharedSecret = sharedSecrets[i]
62+
}
63+
64+
// With the shared secret, we'll now strip off a layer of
65+
// encryption from the encrypted error payload.
66+
encryptedData = onionEncrypt(&sharedSecret, encryptedData)
67+
68+
message, payloads, hmacs := getMsgComponents(encryptedData)
69+
70+
expectedHmac := calculateHmac(sharedSecret, i, message, payloads, hmacs)
71+
actualHmac := hmacs[i*sha256.Size : (i+1)*sha256.Size]
72+
73+
// If the hmac does not match up, exit with a nil message.
74+
if !bytes.Equal(actualHmac, expectedHmac) && sender == 0 {
75+
sender = i + 1
76+
msg = nil
77+
}
78+
79+
// Extract the payload and exit with a nil message if it is invalid.
80+
payloadType, holdTimeMs, err := extractPayload(payloads)
81+
if sender == 0 {
82+
if err != nil {
83+
sender = i + 1
84+
msg = nil
85+
}
86+
87+
// Store hold time reported by this node.
88+
holdTimesMs = append(holdTimesMs, holdTimeMs)
89+
90+
// If we are at the node that is the source of the error, we can now
91+
// save the message in our return variable.
92+
if payloadType == payloadFinal {
93+
sender = i + 1
94+
msg = message
95+
}
96+
}
97+
98+
// Shift payloads and hmacs to the left to prepare for the next
99+
// iteration.
100+
shiftPayloadsLeft(payloads)
101+
shiftHmacsLeft(hmacs)
102+
}
103+
104+
// If the sender index is still zero, all hmacs checked out but none of the
105+
// payloads was a final payload. In this case we must be dealing with a max
106+
// length route and a final hop that returned an intermediate payload. Blame
107+
// the final hop.
108+
if sender == 0 {
109+
sender = NumMaxHops
110+
msg = nil
111+
}
112+
113+
return &DecryptedError{
114+
SenderIdx: sender,
115+
Sender: o.circuit.PaymentPath[sender-1],
116+
Message: msg,
117+
HoldTimesMs: holdTimesMs,
118+
}, nil
119+
}
120+
121+
const (
122+
totalHmacs = (NumMaxHops * (NumMaxHops + 1)) / 2
123+
allHmacsLen = totalHmacs * sha256.Size
124+
hmacsAndPayloadsLen = allHmacsLen + allPayloadsLen
125+
126+
// payloadLen is the size of the per-node payload. It consists of a 1-byte
127+
// payload type and an 8-byte hold time.
128+
payloadLen = 1 + 8
129+
130+
allPayloadsLen = payloadLen * NumMaxHops
131+
132+
payloadFinal = 1
133+
payloadIntermediate = 0
134+
)
135+
136+
func shiftHmacsRight(hmacs []byte) {
137+
if len(hmacs) != allHmacsLen {
138+
panic("invalid hmac block length")
139+
}
140+
141+
srcIdx := totalHmacs - 2
142+
destIdx := totalHmacs - 1
143+
copyLen := 1
144+
for i := 0; i < NumMaxHops-1; i++ {
145+
copy(
146+
hmacs[destIdx*sha256.Size:],
147+
hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size],
148+
)
149+
150+
copyLen++
151+
152+
srcIdx -= copyLen + 1
153+
destIdx -= copyLen
154+
}
155+
}
156+
157+
func shiftHmacsLeft(hmacs []byte) {
158+
if len(hmacs) != allHmacsLen {
159+
panic("invalid hmac block length")
160+
}
161+
162+
srcIdx := NumMaxHops
163+
destIdx := 1
164+
copyLen := NumMaxHops - 1
165+
for i := 0; i < NumMaxHops-1; i++ {
166+
copy(
167+
hmacs[destIdx*sha256.Size:],
168+
hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size],
169+
)
170+
171+
srcIdx += copyLen
172+
destIdx += copyLen + 1
173+
copyLen--
174+
}
175+
}
176+
177+
func shiftPayloadsRight(payloads []byte) {
178+
if len(payloads) != allPayloadsLen {
179+
panic("invalid payload block length")
180+
}
181+
182+
copy(payloads[payloadLen:], payloads)
183+
}
184+
185+
func shiftPayloadsLeft(payloads []byte) {
186+
if len(payloads) != allPayloadsLen {
187+
panic("invalid payload block length")
188+
}
189+
190+
copy(payloads, payloads[payloadLen:NumMaxHops*payloadLen])
191+
}
192+
193+
// getMsgComponents splits a complete failure message into its components
194+
// without re-allocating memory.
195+
func getMsgComponents(data []byte) ([]byte, []byte, []byte) {
196+
payloads := data[len(data)-hmacsAndPayloadsLen : len(data)-allHmacsLen]
197+
hmacs := data[len(data)-allHmacsLen:]
198+
message := data[:len(data)-hmacsAndPayloadsLen]
199+
200+
return message, payloads, hmacs
201+
}
202+
203+
// calculateHmac calculates an hmac given a shared secret and a presumed
204+
// position in the path. Position is expressed as the distance to the error
205+
// source. The error source itself is at position 0.
206+
func calculateHmac(sharedSecret Hash256, position int,
207+
message, payloads, hmacs []byte) []byte {
208+
209+
umKey := generateKey("um", &sharedSecret)
210+
hash := hmac.New(sha256.New, umKey[:])
211+
212+
// Include message.
213+
_, _ = hash.Write(message)
214+
215+
// Include payloads including our own.
216+
_, _ = hash.Write(payloads[:(NumMaxHops-position)*payloadLen])
217+
218+
// Include downstream hmacs.
219+
var hmacsIdx = position + NumMaxHops
220+
for j := 0; j < NumMaxHops-position-1; j++ {
221+
_, _ = hash.Write(
222+
hmacs[hmacsIdx*sha256.Size : (hmacsIdx+1)*sha256.Size],
223+
)
224+
225+
hmacsIdx += NumMaxHops - j - 1
226+
}
227+
228+
return hash.Sum(nil)
229+
}
230+
231+
// calculateHmac calculates an hmac using the shared secret for this
232+
// OnionErrorEncryptor instance.
233+
func (o *OnionErrorEncrypter) calculateHmac(position int,
234+
message, payloads, hmacs []byte) []byte {
235+
236+
return calculateHmac(o.sharedSecret, position, message, payloads, hmacs)
237+
}
238+
239+
// addHmacs updates the failure data with a series of hmacs corresponding to all
240+
// possible positions in the path for the current node.
241+
func (o *OnionErrorEncrypter) addHmacs(data []byte) {
242+
message, payloads, hmacs := getMsgComponents(data)
243+
244+
for i := 0; i < NumMaxHops; i++ {
245+
hmac := o.calculateHmac(i, message, payloads, hmacs)
246+
247+
copy(hmacs[i*sha256.Size:], hmac)
248+
}
249+
}
250+
251+
// EncryptError is used to make data obfuscation using the generated shared
252+
// secret.
253+
//
254+
// In context of Lightning Network is either used by the nodes in order to make
255+
// initial obfuscation with the creation of the hmac or by the forwarding nodes
256+
// for backward failure obfuscation of the onion failure blob. By obfuscating
257+
// the onion failure on every node in the path we are adding additional step of
258+
// the security and barrier for malware nodes to retrieve valuable information.
259+
// The reason for using onion obfuscation is to not give
260+
// away to the nodes in the payment path the information about the exact
261+
// failure and its origin.
262+
func (o *OnionErrorEncrypter) EncryptFatError(initial bool, data []byte,
263+
holdTimeMs uint64) []byte {
264+
265+
if initial {
266+
data = o.initializePayload(data, holdTimeMs)
267+
} else {
268+
o.addIntermediatePayload(data, holdTimeMs)
269+
}
270+
271+
// Update hmac block.
272+
o.addHmacs(data)
273+
274+
// Obfuscate.
275+
return onionEncrypt(&o.sharedSecret, data)
276+
}
277+
278+
func (o *OnionErrorEncrypter) initializePayload(message []byte,
279+
holdTimeMs uint64) []byte {
280+
281+
// Add space for payloads and hmacs.
282+
data := make([]byte, len(message)+hmacsAndPayloadsLen)
283+
copy(data, message)
284+
285+
_, payloads, _ := getMsgComponents(data)
286+
287+
// Signal final hops in the payload.
288+
addPayload(payloads, payloadFinal, holdTimeMs)
289+
290+
return data
291+
}
292+
293+
func addPayload(payloads []byte, payloadType PayloadType, holdTimeMs uint64) {
294+
byteOrder.PutUint64(payloads[1:], holdTimeMs)
295+
296+
payloads[0] = byte(payloadType)
297+
}
298+
299+
func extractPayload(payloads []byte) (PayloadType, uint64, error) {
300+
var payloadType PayloadType
301+
302+
switch payloads[0] {
303+
case payloadFinal, payloadIntermediate:
304+
payloadType = PayloadType(payloads[0])
305+
306+
default:
307+
return 0, 0, errors.New("invalid payload type")
308+
}
309+
310+
holdTimeMs := byteOrder.Uint64(payloads[1:])
311+
312+
return payloadType, holdTimeMs, nil
313+
}
314+
315+
func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte,
316+
holdTimeMs uint64) {
317+
318+
_, payloads, hmacs := getMsgComponents(data)
319+
320+
// Shift hmacs and payloads to create space for the payload.
321+
shiftPayloadsRight(payloads)
322+
shiftHmacsRight(hmacs)
323+
324+
// Signal intermediate hop in the payload.
325+
addPayload(payloads, payloadIntermediate, holdTimeMs)
326+
}

0 commit comments

Comments
 (0)