Skip to content

Commit

Permalink
convert to fat errors
Browse files Browse the repository at this point in the history
  • Loading branch information
joostjager committed Nov 21, 2022
1 parent b62f49f commit 5783ac8
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 35 deletions.
271 changes: 240 additions & 31 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"

Expand All @@ -19,6 +20,8 @@ const (
HMACSize = 32
)

var byteOrder = binary.BigEndian

// Hash256 is a statically sized, 32-byte array, typically containing
// the output of a SHA256 hash.
type Hash256 [sha256.Size]byte
Expand Down Expand Up @@ -92,6 +95,10 @@ type DecryptedError struct {

// Message is the decrypted error message.
Message []byte

// HoldTimesMs is an array of millisecond durations reported by each node on
// the (error) path.
HoldTimesMs []uint64
}

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

// Ensure the error message length is as expected.
if len(encryptedData) != onionErrorLength {
// Ensure the error message length is enough to contain the payloads and
// hmacs blocks.
if len(encryptedData) < hmacsAndPayloadsLen {
return nil, fmt.Errorf("invalid error length: "+
"expected %v got %v", onionErrorLength,
"expected at least %v got %v", hmacsAndPayloadsLen,
len(encryptedData))
}

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

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

// Next, we'll need to separate the data, from the MAC itself
// so we can reconstruct and verify it.
expectedMac := encryptedData[:sha256.Size]
data := encryptedData[sha256.Size:]

// With the data split, we'll now re-generate the MAC using its
// specified key.
umKey := generateKey("um", &sharedSecret)
h := hmac.New(sha256.New, umKey[:])
h.Write(data)

// If the MAC matches up, then we've found the sender of the
// error and have also obtained the fully decrypted message.
realMac := h.Sum(nil)
if hmac.Equal(realMac, expectedMac) && sender == 0 {
message, payloads, hmacs := getMsgComponents(encryptedData)

expectedHmac := calculateHmac(sharedSecret, i, message, payloads, hmacs)
actualHmac := hmacs[i*sha256.Size : (i+1)*sha256.Size]

// If the hmac does not match up, exit with a nil message.
if !bytes.Equal(actualHmac, expectedHmac[:]) && sender == 0 {
sender = i + 1
msg = data
msg = nil
}

// Extract the payload and exit with a nil message if it is invalid.
payloadType, holdTimeMs, err := extractPayload(payloads)
if sender == 0 {
if err != nil {
sender = i + 1
msg = nil
}

// Store hold time reported by this node.
holdTimesMs = append(holdTimesMs, holdTimeMs)

// If we are at the node that is the source of the error, we can now
// save the message in our return variable.
if payloadType == payloadFinal {
sender = i + 1
msg = message
}
}

// Shift payloads and hmacs to the left to prepare for the next
// iteration.
shiftPayloadsLeft(payloads)
shiftHmacsLeft(hmacs)
}

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

return &DecryptedError{
SenderIdx: sender,
Sender: o.circuit.PaymentPath[sender-1],
Message: msg,
SenderIdx: sender,
Sender: o.circuit.PaymentPath[sender-1],
Message: msg,
HoldTimesMs: holdTimesMs,
}, nil
}

const (
totalHmacs = (NumMaxHops * (NumMaxHops + 1)) / 2
allHmacsLen = totalHmacs * sha256.Size
hmacsAndPayloadsLen = allHmacsLen + allPayloadsLen

// payloadLen is the size of the per-node payload. It consists of a 1-byte
// payload type and an 8-byte hold time.
payloadLen = 1 + 8

allPayloadsLen = payloadLen * NumMaxHops

payloadFinal = 1
payloadIntermediate = 0
)

func shiftHmacsRight(hmacs []byte) {
if len(hmacs) != allHmacsLen {
panic("invalid hmac block length")
}

srcIdx := totalHmacs - 2
destIdx := totalHmacs - 1
copyLen := 1
for i := 0; i < NumMaxHops-1; i++ {
copy(hmacs[destIdx*sha256.Size:], hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size])

copyLen++

srcIdx -= copyLen + 1
destIdx -= copyLen
}
}

func shiftHmacsLeft(hmacs []byte) {
if len(hmacs) != allHmacsLen {
panic("invalid hmac block length")
}

srcIdx := NumMaxHops
destIdx := 1
copyLen := NumMaxHops - 1
for i := 0; i < NumMaxHops-1; i++ {
copy(hmacs[destIdx*sha256.Size:], hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size])

srcIdx += copyLen
destIdx += copyLen + 1
copyLen--
}
}

func shiftPayloadsRight(payloads []byte) {
if len(payloads) != allPayloadsLen {
panic("invalid payload block length")
}

copy(payloads[payloadLen:], payloads)
}

func shiftPayloadsLeft(payloads []byte) {
if len(payloads) != allPayloadsLen {
panic("invalid payload block length")
}

copy(payloads, payloads[payloadLen:NumMaxHops*payloadLen])
}

// getMsgComponents splits a complete failure message into its components
// without re-allocating memory.
func getMsgComponents(data []byte) ([]byte, []byte, []byte) {
payloads := data[len(data)-hmacsAndPayloadsLen : len(data)-allHmacsLen]
hmacs := data[len(data)-allHmacsLen:]
message := data[:len(data)-hmacsAndPayloadsLen]

return message, payloads, hmacs
}

// calculateHmac calculates an hmac given a shared secret and a presumed
// position in the path. Position is expressed as the distance to the error
// source. The error source itself is at position 0.
func calculateHmac(sharedSecret Hash256, position int,
message, payloads, hmacs []byte) []byte {

var dataToHmac []byte

// Include payloads including our own.
dataToHmac = append(dataToHmac, payloads[:(NumMaxHops-position)*payloadLen]...)

// Include downstream hmacs.
var downstreamHmacsIdx = position + NumMaxHops
for j := 0; j < NumMaxHops-position-1; j++ {
dataToHmac = append(dataToHmac, hmacs[downstreamHmacsIdx*sha256.Size:(downstreamHmacsIdx+1)*sha256.Size]...)

downstreamHmacsIdx += NumMaxHops - j - 1
}

// Include message.
dataToHmac = append(dataToHmac, message...)

// Calculate and return hmac.
umKey := generateKey("um", &sharedSecret)
hash := hmac.New(sha256.New, umKey[:])
hash.Write(dataToHmac)

return hash.Sum(nil)
}

// calculateHmac calculates an hmac using the shared secret for this
// OnionErrorEncryptor instance.
func (o *OnionErrorEncrypter) calculateHmac(position int,
message, payloads, hmacs []byte) []byte {

return calculateHmac(o.sharedSecret, position, message, payloads, hmacs)
}

// addHmacs updates the failure data with a series of hmacs corresponding to all
// possible positions in the path for the current node.
func (o *OnionErrorEncrypter) addHmacs(data []byte) {
message, payloads, hmacs := getMsgComponents(data)

for i := 0; i < NumMaxHops; i++ {
hmac := o.calculateHmac(i, message, payloads, hmacs)

copy(hmacs[i*sha256.Size:], hmac)
}
}

// EncryptError is used to make data obfuscation using the generated shared
// secret.
//
Expand All @@ -336,14 +491,68 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
// The reason for using onion obfuscation is to not give
// away to the nodes in the payment path the information about the exact
// failure and its origin.
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte {
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte,
holdTimeMs uint64) []byte {

if initial {
umKey := generateKey("um", &o.sharedSecret)
hash := hmac.New(sha256.New, umKey[:])
hash.Write(data)
h := hash.Sum(nil)
data = append(h, data...)
data = o.initializePayload(data, holdTimeMs)
} else {
o.addIntermediatePayload(data, holdTimeMs)
}

// Update hmac block.
o.addHmacs(data)

// Obfuscate.
return onionEncrypt(&o.sharedSecret, data)
}

func (o *OnionErrorEncrypter) initializePayload(message []byte,
holdTimeMs uint64) []byte {

// Add space for payloads and hmacs.
data := make([]byte, len(message)+hmacsAndPayloadsLen)
copy(data, message)

_, payloads, _ := getMsgComponents(data)

// Signal final hops in the payload.
addPayload(payloads, payloadFinal, holdTimeMs)

return data
}

func addPayload(payloads []byte, payloadType PayloadType, holdTimeMs uint64) {
byteOrder.PutUint64(payloads[1:], uint64(holdTimeMs))

payloads[0] = byte(payloadType)
}

func extractPayload(payloads []byte) (PayloadType, uint64, error) {
var payloadType PayloadType

switch payloads[0] {
case payloadFinal, payloadIntermediate:
payloadType = PayloadType(payloads[0])

default:
return 0, 0, errors.New("invalid payload type")
}

holdTimeMs := byteOrder.Uint64(payloads[1:])

return payloadType, holdTimeMs, nil
}

func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte,
holdTimeMs uint64) {

_, payloads, hmacs := getMsgComponents(data)

// Shift hmacs and payloads to create space for the payload.
shiftPayloadsRight(payloads)
shiftHmacsRight(hmacs)

// Signal intermediate hop in the payload.
addPayload(payloads, payloadIntermediate, holdTimeMs)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/stretchr/testify v1.8.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)

Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
Expand Down Expand Up @@ -53,6 +54,15 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
Expand Down Expand Up @@ -85,9 +95,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 5783ac8

Please sign in to comment.