Skip to content

Commit 5783ac8

Browse files
committed
convert to fat errors
1 parent b62f49f commit 5783ac8

File tree

4 files changed

+334
-35
lines changed

4 files changed

+334
-35
lines changed

crypto.go

+240-31
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/hmac"
66
"crypto/sha256"
7+
"encoding/binary"
78
"errors"
89
"fmt"
910

@@ -19,6 +20,8 @@ const (
1920
HMACSize = 32
2021
)
2122

23+
var byteOrder = binary.BigEndian
24+
2225
// Hash256 is a statically sized, 32-byte array, typically containing
2326
// the output of a SHA256 hash.
2427
type Hash256 [sha256.Size]byte
@@ -92,6 +95,10 @@ type DecryptedError struct {
9295

9396
// Message is the decrypted error message.
9497
Message []byte
98+
99+
// HoldTimesMs is an array of millisecond durations reported by each node on
100+
// the (error) path.
101+
HoldTimesMs []uint64
95102
}
96103

97104
// zeroHMAC is the special HMAC value that allows the final node to determine
@@ -249,10 +256,11 @@ const onionErrorLength = 2 + 2 + 256 + sha256.Size
249256
func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
250257
*DecryptedError, error) {
251258

252-
// Ensure the error message length is as expected.
253-
if len(encryptedData) != onionErrorLength {
259+
// Ensure the error message length is enough to contain the payloads and
260+
// hmacs blocks.
261+
if len(encryptedData) < hmacsAndPayloadsLen {
254262
return nil, fmt.Errorf("invalid error length: "+
255-
"expected %v got %v", onionErrorLength,
263+
"expected at least %v got %v", hmacsAndPayloadsLen,
256264
len(encryptedData))
257265
}
258266

@@ -275,6 +283,7 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
275283
// We'll iterate a constant amount of hops to ensure that we don't give
276284
// away an timing information pertaining to the position in the route
277285
// that the error emanated from.
286+
holdTimesMs := make([]uint64, 0)
278287
for i := 0; i < NumMaxHops; i++ {
279288
var sharedSecret Hash256
280289

@@ -292,39 +301,185 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
292301
// encryption from the encrypted error payload.
293302
encryptedData = onionEncrypt(&sharedSecret, encryptedData)
294303

295-
// Next, we'll need to separate the data, from the MAC itself
296-
// so we can reconstruct and verify it.
297-
expectedMac := encryptedData[:sha256.Size]
298-
data := encryptedData[sha256.Size:]
299-
300-
// With the data split, we'll now re-generate the MAC using its
301-
// specified key.
302-
umKey := generateKey("um", &sharedSecret)
303-
h := hmac.New(sha256.New, umKey[:])
304-
h.Write(data)
305-
306-
// If the MAC matches up, then we've found the sender of the
307-
// error and have also obtained the fully decrypted message.
308-
realMac := h.Sum(nil)
309-
if hmac.Equal(realMac, expectedMac) && sender == 0 {
304+
message, payloads, hmacs := getMsgComponents(encryptedData)
305+
306+
expectedHmac := calculateHmac(sharedSecret, i, message, payloads, hmacs)
307+
actualHmac := hmacs[i*sha256.Size : (i+1)*sha256.Size]
308+
309+
// If the hmac does not match up, exit with a nil message.
310+
if !bytes.Equal(actualHmac, expectedHmac[:]) && sender == 0 {
310311
sender = i + 1
311-
msg = data
312+
msg = nil
313+
}
314+
315+
// Extract the payload and exit with a nil message if it is invalid.
316+
payloadType, holdTimeMs, err := extractPayload(payloads)
317+
if sender == 0 {
318+
if err != nil {
319+
sender = i + 1
320+
msg = nil
321+
}
322+
323+
// Store hold time reported by this node.
324+
holdTimesMs = append(holdTimesMs, holdTimeMs)
325+
326+
// If we are at the node that is the source of the error, we can now
327+
// save the message in our return variable.
328+
if payloadType == payloadFinal {
329+
sender = i + 1
330+
msg = message
331+
}
312332
}
333+
334+
// Shift payloads and hmacs to the left to prepare for the next
335+
// iteration.
336+
shiftPayloadsLeft(payloads)
337+
shiftHmacsLeft(hmacs)
313338
}
314339

315-
// If the sender index is still zero, then we haven't found the sender,
316-
// meaning we've failed to decrypt.
340+
// If the sender index is still zero, all hmacs checked out but none of the
341+
// payloads was a final payload. In this case we must be dealing with a max
342+
// length route and a final hop that returned an intermediate payload. Blame
343+
// the final hop.
317344
if sender == 0 {
318-
return nil, errors.New("unable to retrieve onion failure")
345+
sender = NumMaxHops
346+
msg = nil
319347
}
320348

321349
return &DecryptedError{
322-
SenderIdx: sender,
323-
Sender: o.circuit.PaymentPath[sender-1],
324-
Message: msg,
350+
SenderIdx: sender,
351+
Sender: o.circuit.PaymentPath[sender-1],
352+
Message: msg,
353+
HoldTimesMs: holdTimesMs,
325354
}, nil
326355
}
327356

357+
const (
358+
totalHmacs = (NumMaxHops * (NumMaxHops + 1)) / 2
359+
allHmacsLen = totalHmacs * sha256.Size
360+
hmacsAndPayloadsLen = allHmacsLen + allPayloadsLen
361+
362+
// payloadLen is the size of the per-node payload. It consists of a 1-byte
363+
// payload type and an 8-byte hold time.
364+
payloadLen = 1 + 8
365+
366+
allPayloadsLen = payloadLen * NumMaxHops
367+
368+
payloadFinal = 1
369+
payloadIntermediate = 0
370+
)
371+
372+
func shiftHmacsRight(hmacs []byte) {
373+
if len(hmacs) != allHmacsLen {
374+
panic("invalid hmac block length")
375+
}
376+
377+
srcIdx := totalHmacs - 2
378+
destIdx := totalHmacs - 1
379+
copyLen := 1
380+
for i := 0; i < NumMaxHops-1; i++ {
381+
copy(hmacs[destIdx*sha256.Size:], hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size])
382+
383+
copyLen++
384+
385+
srcIdx -= copyLen + 1
386+
destIdx -= copyLen
387+
}
388+
}
389+
390+
func shiftHmacsLeft(hmacs []byte) {
391+
if len(hmacs) != allHmacsLen {
392+
panic("invalid hmac block length")
393+
}
394+
395+
srcIdx := NumMaxHops
396+
destIdx := 1
397+
copyLen := NumMaxHops - 1
398+
for i := 0; i < NumMaxHops-1; i++ {
399+
copy(hmacs[destIdx*sha256.Size:], hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size])
400+
401+
srcIdx += copyLen
402+
destIdx += copyLen + 1
403+
copyLen--
404+
}
405+
}
406+
407+
func shiftPayloadsRight(payloads []byte) {
408+
if len(payloads) != allPayloadsLen {
409+
panic("invalid payload block length")
410+
}
411+
412+
copy(payloads[payloadLen:], payloads)
413+
}
414+
415+
func shiftPayloadsLeft(payloads []byte) {
416+
if len(payloads) != allPayloadsLen {
417+
panic("invalid payload block length")
418+
}
419+
420+
copy(payloads, payloads[payloadLen:NumMaxHops*payloadLen])
421+
}
422+
423+
// getMsgComponents splits a complete failure message into its components
424+
// without re-allocating memory.
425+
func getMsgComponents(data []byte) ([]byte, []byte, []byte) {
426+
payloads := data[len(data)-hmacsAndPayloadsLen : len(data)-allHmacsLen]
427+
hmacs := data[len(data)-allHmacsLen:]
428+
message := data[:len(data)-hmacsAndPayloadsLen]
429+
430+
return message, payloads, hmacs
431+
}
432+
433+
// calculateHmac calculates an hmac given a shared secret and a presumed
434+
// position in the path. Position is expressed as the distance to the error
435+
// source. The error source itself is at position 0.
436+
func calculateHmac(sharedSecret Hash256, position int,
437+
message, payloads, hmacs []byte) []byte {
438+
439+
var dataToHmac []byte
440+
441+
// Include payloads including our own.
442+
dataToHmac = append(dataToHmac, payloads[:(NumMaxHops-position)*payloadLen]...)
443+
444+
// Include downstream hmacs.
445+
var downstreamHmacsIdx = position + NumMaxHops
446+
for j := 0; j < NumMaxHops-position-1; j++ {
447+
dataToHmac = append(dataToHmac, hmacs[downstreamHmacsIdx*sha256.Size:(downstreamHmacsIdx+1)*sha256.Size]...)
448+
449+
downstreamHmacsIdx += NumMaxHops - j - 1
450+
}
451+
452+
// Include message.
453+
dataToHmac = append(dataToHmac, message...)
454+
455+
// Calculate and return hmac.
456+
umKey := generateKey("um", &sharedSecret)
457+
hash := hmac.New(sha256.New, umKey[:])
458+
hash.Write(dataToHmac)
459+
460+
return hash.Sum(nil)
461+
}
462+
463+
// calculateHmac calculates an hmac using the shared secret for this
464+
// OnionErrorEncryptor instance.
465+
func (o *OnionErrorEncrypter) calculateHmac(position int,
466+
message, payloads, hmacs []byte) []byte {
467+
468+
return calculateHmac(o.sharedSecret, position, message, payloads, hmacs)
469+
}
470+
471+
// addHmacs updates the failure data with a series of hmacs corresponding to all
472+
// possible positions in the path for the current node.
473+
func (o *OnionErrorEncrypter) addHmacs(data []byte) {
474+
message, payloads, hmacs := getMsgComponents(data)
475+
476+
for i := 0; i < NumMaxHops; i++ {
477+
hmac := o.calculateHmac(i, message, payloads, hmacs)
478+
479+
copy(hmacs[i*sha256.Size:], hmac)
480+
}
481+
}
482+
328483
// EncryptError is used to make data obfuscation using the generated shared
329484
// secret.
330485
//
@@ -336,14 +491,68 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
336491
// The reason for using onion obfuscation is to not give
337492
// away to the nodes in the payment path the information about the exact
338493
// failure and its origin.
339-
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte {
494+
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte,
495+
holdTimeMs uint64) []byte {
496+
340497
if initial {
341-
umKey := generateKey("um", &o.sharedSecret)
342-
hash := hmac.New(sha256.New, umKey[:])
343-
hash.Write(data)
344-
h := hash.Sum(nil)
345-
data = append(h, data...)
498+
data = o.initializePayload(data, holdTimeMs)
499+
} else {
500+
o.addIntermediatePayload(data, holdTimeMs)
346501
}
347502

503+
// Update hmac block.
504+
o.addHmacs(data)
505+
506+
// Obfuscate.
348507
return onionEncrypt(&o.sharedSecret, data)
349508
}
509+
510+
func (o *OnionErrorEncrypter) initializePayload(message []byte,
511+
holdTimeMs uint64) []byte {
512+
513+
// Add space for payloads and hmacs.
514+
data := make([]byte, len(message)+hmacsAndPayloadsLen)
515+
copy(data, message)
516+
517+
_, payloads, _ := getMsgComponents(data)
518+
519+
// Signal final hops in the payload.
520+
addPayload(payloads, payloadFinal, holdTimeMs)
521+
522+
return data
523+
}
524+
525+
func addPayload(payloads []byte, payloadType PayloadType, holdTimeMs uint64) {
526+
byteOrder.PutUint64(payloads[1:], uint64(holdTimeMs))
527+
528+
payloads[0] = byte(payloadType)
529+
}
530+
531+
func extractPayload(payloads []byte) (PayloadType, uint64, error) {
532+
var payloadType PayloadType
533+
534+
switch payloads[0] {
535+
case payloadFinal, payloadIntermediate:
536+
payloadType = PayloadType(payloads[0])
537+
538+
default:
539+
return 0, 0, errors.New("invalid payload type")
540+
}
541+
542+
holdTimeMs := byteOrder.Uint64(payloads[1:])
543+
544+
return payloadType, holdTimeMs, nil
545+
}
546+
547+
func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte,
548+
holdTimeMs uint64) {
549+
550+
_, payloads, hmacs := getMsgComponents(data)
551+
552+
// Shift hmacs and payloads to create space for the payload.
553+
shiftPayloadsRight(payloads)
554+
shiftHmacsRight(hmacs)
555+
556+
// Signal intermediate hop in the payload.
557+
addPayload(payloads, payloadIntermediate, holdTimeMs)
558+
}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
99
github.com/davecgh/go-spew v1.1.1
1010
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
11+
github.com/stretchr/testify v1.8.1
1112
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
1213
)
1314

go.sum

+14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
2121
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
2222
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
2323
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2425
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2526
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2627
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
@@ -53,6 +54,15 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
5354
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
5455
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
5556
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
57+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
58+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
59+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
60+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
61+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
62+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
63+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
64+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
65+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
5666
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
5767
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
5868
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
@@ -85,9 +95,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
8595
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
8696
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
8797
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
98+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8899
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
89100
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
90101
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
91102
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
92103
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
93104
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
105+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
106+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
107+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)