From c99e01ea8c14b931375be6dde6ddad3e5f7b36a0 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 10 Nov 2022 10:08:53 +0100 Subject: [PATCH] convert to fat errors --- crypto.go | 220 ++++++++++++++++++++++++++++++++++++++------ obfuscation_test.go | 3 + 2 files changed, 195 insertions(+), 28 deletions(-) diff --git a/crypto.go b/crypto.go index 939f9ec..e93d044 100644 --- a/crypto.go +++ b/crypto.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" - "errors" "fmt" "github.com/aead/chacha20" @@ -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)) } @@ -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{ @@ -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. // @@ -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 +} diff --git a/obfuscation_test.go b/obfuscation_test.go index dc476c8..968c535 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -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 "+