diff --git a/attr_error_structure.go b/attr_error_structure.go new file mode 100644 index 0000000..05b52f2 --- /dev/null +++ b/attr_error_structure.go @@ -0,0 +1,153 @@ +package sphinx + +import ( + "crypto/hmac" + "crypto/sha256" + "io" +) + +// AttrErrorStructure contains the parameters that define the structure +// of the error message that is passed back. +type AttrErrorStructure struct { + // hopCount is the assumed maximum number of hops in the path. + hopCount int + + // fixedPayloadLen is the length of the payload data that each hop along + // the route can add. + fixedPayloadLen int + + // hmacSize is the number of bytes that is reserved for each hmac. + hmacSize int + + zeroHmac []byte +} + +// NewAttrErrorStructure creates an AttrErrorStructure with the defined +// parameters and returns it. +func NewAttrErrorStructure(hopCount int, fixedPayloadLen int, + hmacSize int) *AttrErrorStructure { + + return &AttrErrorStructure{ + hopCount: hopCount, + fixedPayloadLen: fixedPayloadLen, + hmacSize: hmacSize, + + zeroHmac: make([]byte, hmacSize), + } +} + +// HopCount returns the assumed maximum number of hops in the path. +func (o *AttrErrorStructure) HopCount() int { + return o.hopCount +} + +// FixedPayloadLen returns the length of the payload data that each hop along +// the route can add. +func (o *AttrErrorStructure) FixedPayloadLen() int { + return o.fixedPayloadLen +} + +// HmacSize returns the number of bytes that is reserved for each hmac. +func (o *AttrErrorStructure) HmacSize() int { + return o.hmacSize +} + +// totalHmacs is the total number of hmacs that is present in the failure +// message. Every hop adds HopCount hmacs to the message, but as the error +// back-propagates, downstream hmacs can be pruned. This results in the number +// of hmacs for each hop decreasing by one for each step that we move away from +// the current node. +func (o *AttrErrorStructure) totalHmacs() int { + return (o.hopCount * (o.hopCount + 1)) / 2 +} + +// allHmacsLen is the total length in the bytes of all hmacs in the failure +// message. +func (o *AttrErrorStructure) allHmacsLen() int { + return o.totalHmacs() * o.hmacSize +} + +// hmacsAndPayloadsLen is the total length in bytes of all hmacs and payloads +// together. +func (o *AttrErrorStructure) hmacsAndPayloadsLen() int { + return o.allHmacsLen() + o.allPayloadsLen() +} + +// allPayloadsLen is the total length in bytes of all payloads in the failure +// message. +func (o *AttrErrorStructure) allPayloadsLen() int { + return o.payloadLen() * o.hopCount +} + +// payloadLen is the size of the per-node payload. It consists of a 1-byte +// payload type followed by the payload data. +func (o *AttrErrorStructure) payloadLen() int { + return o.fixedPayloadLen +} + +// payloads returns a slice containing all payloads in the given failure +// data block. The payloads follow the message in the block. +func (o *AttrErrorStructure) payloads(data []byte) []byte { + dataLen := len(data) + + return data[dataLen-o.hmacsAndPayloadsLen() : dataLen-o.allHmacsLen()] +} + +// hmacs returns a slice containing all hmacs in the given failure data block. +// The hmacs are positioned at the end of the data block. +func (o *AttrErrorStructure) hmacs(data []byte) []byte { + return data[len(data)-o.allHmacsLen():] +} + +// 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 (o *AttrErrorStructure) calculateHmac(sharedSecret Hash256, + position int, message, payloads, hmacs []byte) []byte { + + umKey := generateKey("um", &sharedSecret) + hash := hmac.New(sha256.New, umKey[:]) + + // Include message. + _, _ = hash.Write(message) + + // Include payloads including our own. + _, _ = hash.Write(payloads[:(position+1)*o.payloadLen()]) + + // Include downstream hmacs. + writeDownstreamHmacs(position, o.hopCount, hmacs, o.hmacSize, hash) + + hmac := hash.Sum(nil) + + return hmac[:o.hmacSize] +} + +// writeDownstreamHmacs writes the hmacs of downstream nodes that are relevant +// for the given position to a writer instance. Position is expressed as the +// distance to the error source. The error source itself is at position 0. +func writeDownstreamHmacs(position, maxHops int, hmacs []byte, hmacBytes int, + w io.Writer) { + + // Track the index of the next hmac to write in a variable. The first + // maxHops slots are reserved for the hmacs of the current hop and can + // therefore be skipped. The first hmac to write is part of the block of + // hmacs that was written by the first downstream node. Which hmac + // exactly is determined by the assumed position of the current node. + var hmacIdx = maxHops + (maxHops - position - 1) + + // Iterate over all downstream nodes. + for j := 0; j < position; j++ { + _, _ = w.Write( + hmacs[hmacIdx*hmacBytes : (hmacIdx+1)*hmacBytes], + ) + + // Calculate the total number of hmacs in the block of the + // current downstream node. + blockSize := maxHops - j - 1 + + // Skip to the next block. The new hmac index will point to the + // hmac that corresponds to the next downstream node which is + // one step closer to the assumed error source. + hmacIdx += blockSize + } +} diff --git a/attr_error_test.go b/attr_error_test.go new file mode 100644 index 0000000..e48028a --- /dev/null +++ b/attr_error_test.go @@ -0,0 +1,454 @@ +package sphinx + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +var attributableErrorTestStructure = NewAttrErrorStructure(20, 4, 4) + +// TestAttributableOnionFailure checks the ability of sender of payment to +// decode the obfuscated onion error. +func TestAttributableOnionFailure(t *testing.T) { + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // Reduce the error path on one node, in order to check that we are + // able to receive the error not only from last hop. + errorPath := paymentPath[:len(paymentPath)-1] + + failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength) + sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) + require.NoError(t, err) + + // Emulate creation of the obfuscator on node where error have occurred. + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) + + // Emulate the situation when last hop creates the onion failure + // message and send it back. + holdTime := uint32(42) + var attrData, legacyData []byte + + legacyData, attrData, err = obfuscator.EncryptError( + true, failureData, attrData, holdTime, + ) + require.NoError(t, err) + payloads := []uint32{holdTime} + + // Emulate that failure message is backward obfuscated on every hop. + for i := len(errorPath) - 2; i >= 0; i-- { + // Emulate creation of the obfuscator on forwarding node which + // propagates the onion failure. + obfuscator = NewOnionErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + holdTimeIntermediate := uint32(100 + i) + legacyData, attrData, err = obfuscator.EncryptError( + false, legacyData, attrData, holdTimeIntermediate, + ) + require.NoError(t, err) + + payloads = append([]uint32{holdTimeIntermediate}, payloads...) + } + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, true, + ) + require.NoError(t, err) + + // We should understand the node from which error have been received. + require.Equal(t, + errorPath[len(errorPath)-1].SerializeCompressed(), + decryptedError.Sender.SerializeCompressed()) + + require.Equal(t, len(errorPath), decryptedError.SenderIdx) + + // Check that message have been properly de-obfuscated. + require.Equal(t, failureData, decryptedError.Message) + require.Equal(t, payloads, decryptedError.HoldTimes) +} + +// TestOnionFailureCorruption checks the ability of sender of payment to +// identify a node on the path that corrupted the failure message. +func TestOnionFailureCorruption(t *testing.T) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // Reduce the error path on one node, in order to check that we are + // able to receive the error not only from last hop. + errorPath := paymentPath[:len(paymentPath)-1] + + failureData := bytes.Repeat([]byte{'A'}, minOnionErrorLength) + sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) + require.NoError(t, err) + + // Emulate creation of the obfuscator on node where error have occurred. + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) + + // Emulate the situation when last hop creates the onion failure + // message and send it back. + holdTime := uint32(1) + var attrData []byte + legacyData, attrData, err := obfuscator.EncryptError( + true, failureData, attrData, holdTime, + ) + require.NoError(t, err) + + // Emulate that failure message is backward obfuscated on every hop. + for i := len(errorPath) - 2; i >= 0; i-- { + // Emulate creation of the obfuscator on forwarding node which + // propagates the onion failure. + obfuscator = NewOnionErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + holdTime := uint32(100 + i) + legacyData, attrData, err = obfuscator.EncryptError( + false, failureData, attrData, holdTime, + ) + require.NoError(t, err) + + // Hop 1 (the second hop from the sender pov) is corrupting the + // failure message. + if i == 1 { + attrData[0] ^= 255 + } + } + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, true, + ) + require.NoError(t, err) + + // Assert that the second hop is correctly identified as the error + // source. + require.Equal(t, 2, decryptedError.SenderIdx) + require.Nil(t, decryptedError.Message) +} + +type specHop struct { + SharedSecret string `json:"sharedSecret"` + EncryptedMessage string `json:"encryptedMessage"` +} + +type generatedData struct { + EncodedFailureMessage string `json:"encodedFailureMessage"` + + Hops []specHop `json:"hops"` +} + +type specVector struct { + generatedData `json:"generate"` +} + +// TestOnionFailureSpecVector checks that onion error corresponds to the +// specification. +func TestAttributableFailureSpecVector(t *testing.T) { + t.Parallel() + + vectorBytes, err := os.ReadFile("testdata/onion-test.json") + require.NoError(t, err) + + var vector specVector + require.NoError(t, json.Unmarshal(vectorBytes, &vector)) + + failureData, err := hex.DecodeString( + vector.generatedData.EncodedFailureMessage, + ) + require.NoError(t, err) + + paymentPath, err := getSpecPubKeys() + require.NoError(t, err) + + sessionKey, err := getSpecSessionKey() + require.NoError(t, err) + + var ( + legacyData []byte + attrData []byte + ) + sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) + require.NoError(t, err) + + for i, test := range vector.Hops { + // Decode the shared secret and check that it matchs with + // specification. + expectedSharedSecret, err := hex.DecodeString(test.SharedSecret) + require.NoError(t, err) + + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(sharedSecrets)-1-i], + attributableErrorTestStructure, + ) + + require.Equal( + t, expectedSharedSecret, obfuscator.sharedSecret[:], + ) + + holdTime := uint32(i + 1) + + if i == 0 { + // Emulate the situation when last hop creates the onion + // failure message and send it back. + legacyData, attrData, err = obfuscator.EncryptError( + true, failureData, attrData, holdTime, + ) + require.NoError(t, err) + } else { + // Emulate the situation when forward node obfuscates + // the onion failure. + legacyData, attrData, err = obfuscator.EncryptError( + false, legacyData, attrData, holdTime, + ) + require.NoError(t, err) + } + + // Decode the obfuscated data and check that it matches the + // specification. + expectedEncryptErrorData, err := hex.DecodeString( + test.EncryptedMessage, + ) + require.NoError(t, err) + + require.Equal(t, expectedEncryptErrorData, attrData) + } + + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receives the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, true, + ) + require.NoError(t, err) + + // Check that message have been properly de-obfuscated. + require.Equal(t, failureData, decryptedError.Message) + + // We should understand the node from which error have been received. + require.Equal(t, + paymentPath[len(paymentPath)-1].SerializeCompressed(), + decryptedError.Sender.SerializeCompressed(), + ) + + require.Equal(t, len(paymentPath), decryptedError.SenderIdx) + + // Now let's verify the attributable error fields. + require.Equal(t, decryptedError.Message, failureData) + + require.Equal(t, + paymentPath[len(paymentPath)-1].SerializeCompressed(), + decryptedError.Sender.SerializeCompressed(), + ) + + require.Equal(t, len(paymentPath), decryptedError.SenderIdx) +} + +// TestAttributableOnionFailureZeroesMessage checks that a garbage failure is +// attributed to the first hop. +func TestAttributableOnionFailureZeroesMessage(t *testing.T) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + obfuscatedData := make([]byte, 20000) + + decryptedError, err := deobfuscator.DecryptError( + obfuscatedData, nil, true, + ) + require.NoError(t, err) + + require.Equal(t, 1, decryptedError.SenderIdx) +} + +// TestAttributableOnionFailureShortMessage checks that too short failure is +// attributed to the first hop. +func TestAttributableOnionFailureShortMessage(t *testing.T) { + t.Parallel() + + // Create numHops random sphinx paymentPath. + sessionKey, paymentPath := generateRandomPath(t) + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, attributableErrorTestStructure) + + // Emulate that sender node receive the failure message and trying to + // unwrap it, by applying obfuscation and checking the hmac. + obfuscatedData := make([]byte, deobfuscator.hmacsAndPayloadsLen()-1) + failureMsg := bytes.Repeat([]byte{1}, minOnionErrorLength) + + decryptedError, err := deobfuscator.DecryptError( + failureMsg, obfuscatedData, true, + ) + require.NoError(t, err) + + require.Equal(t, 1, decryptedError.SenderIdx) + require.Equal(t, 1, decryptedError.SenderIdx) +} + +func generateRandomPath(t *testing.T) (*btcec.PrivateKey, []*btcec.PublicKey) { + paymentPath := make([]*btcec.PublicKey, 5) + for i := 0; i < len(paymentPath); i++ { + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + paymentPath[i] = privKey.PubKey() + } + + sessionKey, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{'A'}, 32)) + + return sessionKey, paymentPath +} + +func generateHashList(values ...int) []byte { + var b bytes.Buffer + for _, v := range values { + hash := [32]byte{byte(v)} + b.Write(hash[:]) + } + + return b.Bytes() +} + +const testMaxHops = 4 + +// Generate a list of 4+3+2+1 = 10 unique hmacs. The length of this list is +// fixed for the chosen maxHops. +func createTestHmacs() []byte { + return generateHashList( + 43, 42, 41, 40, + 32, 31, 30, + 21, 20, + 10, + ) +} + +const testHmacBytes = 32 + +func TestWriteDownstreamHmacs(t *testing.T) { + require := require.New(t) + + hmacs := createTestHmacs() + + test := func(position int, expectedValues []int) { + var b bytes.Buffer + writeDownstreamHmacs( + position, testMaxHops, hmacs, testHmacBytes, &b, + ) + + expectedHashes := generateHashList(expectedValues...) + require.Equal(expectedHashes, b.Bytes()) + } + + // Assuming the current node is in the position furthest away from the + // error source, we expect three downstream hmacs to be relevant. + test(3, []int{32, 21, 10}) + + // Assuming the current node is in positions closer to the error source, + // fewer hmacs become relevant. + test(2, []int{31, 20}) + test(1, []int{30}) + test(0, []int{}) +} + +func TestShiftHmacsRight(t *testing.T) { + require := require.New(t) + + hmacs := createTestHmacs() + + o := NewOnionErrorEncrypter( + Hash256{}, + NewAttrErrorStructure(testMaxHops, 0, 32), + ) + o.shiftHmacsRight(hmacs) + + expectedHmacs := generateHashList( + // Previous values are zeroed out. + 0, 0, 0, 0, + + // Previous first node hmacs minus the hmac representing the + // position farthest away from the error source. + 42, 41, 40, + + // And so on for the other nodes. + 31, 30, + 20, + ) + + require.Equal(expectedHmacs, hmacs) +} + +func TestShiftHmacsLeft(t *testing.T) { + require := require.New(t) + + hmacs := createTestHmacs() + + o := NewOnionErrorDecrypter( + nil, + NewAttrErrorStructure(testMaxHops, 0, 32), + ) + o.shiftHmacsLeft(hmacs) + + expectedHmacs := generateHashList( + // The hmacs of the second hop now become the first hop hmacs. + // The slot corresponding to the position farthest away from the + // error source remains empty. Because we are shifting, this can + // never be the position of the first hop. + 0, 32, 31, 30, + + // Continue this same scheme for the downstream hops. + 0, 21, 20, + 0, 10, + 0, + ) + + require.Equal(expectedHmacs, hmacs) +} diff --git a/crypto.go b/crypto.go index 6b1b49e..d1845bb 100644 --- a/crypto.go +++ b/crypto.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" + "encoding/binary" "errors" "fmt" @@ -18,6 +19,14 @@ const ( // the onion. Any value lower than 32 will truncate the HMAC both // during onion creation as well as during the verification. HMACSize = 32 + + // AMMAG is the string representation for the ammag key type. Used in + // cypher stream generation. + AMMAG = "ammag" + + // AMMAG_EXT is the string representation for the extended ammag key + // type. user in cypher stream generation. + AMMAG_EXT = "ammagext" ) // chaChaPolyZeroNonce is a slice of zero bytes used in the chacha20poly1305 @@ -97,6 +106,10 @@ type DecryptedError struct { // Message is the decrypted error message. Message []byte + + // HoldTimes is an array of hold times reported by each node on the error + // path. + HoldTimes []uint32 } // zeroHMAC is the special HMAC value that allows the final node to determine @@ -234,18 +247,18 @@ func chacha20polyDecrypt(key, cipherTxt []byte) ([]byte, error) { // // TODO(roasbef): rename? type sharedSecretGenerator interface { - // generateSharedSecret given a public key, generates a shared secret + // GenerateSharedSecret given a public key, generates a shared secret // using private data of the underlying sharedSecretGenerator. - generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) + GenerateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) } -// generateSharedSecret generates the shared secret using the given ephemeral +// GenerateSharedSecret generates the shared secret using the given ephemeral // pub key and the Router's private key. If a blindingPoint is provided then it // is used to tweak the Router's private key before creating the shared secret // with the ephemeral pub key. The blinding point is used to determine our // shared secret with the receiver. From that we can determine our shared // secret with the sender using the dhKey. -func (r *Router) generateSharedSecret(dhKey, +func (r *Router) GenerateSharedSecret(dhKey, blindingPoint *btcec.PublicKey) (Hash256, error) { // If no blinding point is provided, then the un-tweaked dhKey can @@ -301,37 +314,68 @@ func sharedSecret(priv SingleKeyECDH, pub *btcec.PublicKey) (Hash256, error) { // onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a // stream cipher, calling onionEncrypt on an already encrypted piece of data // will decrypt it. -func onionEncrypt(sharedSecret *Hash256, data []byte) []byte { +func onionEncrypt(keyType string, sharedSecret *Hash256, data []byte) []byte { p := make([]byte, len(data)) - ammagKey := generateKey("ammag", sharedSecret) + ammagKey := generateKey(keyType, sharedSecret) streamBytes := generateCipherStream(ammagKey, uint(len(data))) xor(p, data, streamBytes) return p } -// minOnionErrorLength is the minimally expected length of the onion error -// message. Including padding, all messages on the wire should be at least 256 -// bytes. We then add the size of the sha256 HMAC as well. -const minOnionErrorLength = 2 + 2 + 256 + sha256.Size +// minPaddedOnionErrorLength is the minimally expected length of the padded +// onion error message including two uint16s for the length of the message and +// the length of the padding. +const minPaddedOnionErrorLength = 2 + 2 + 256 + +// minOnionErrorLength is the minimally expected length of the complete onion +// error message including the HMAC. +const minOnionErrorLength = minPaddedOnionErrorLength + sha256.Size // DecryptError attempts to decrypt the passed encrypted error response. The // onion failure is encrypted in backward manner, starting from the node where // error have occurred. As a result, in order to decrypt the error we need get // all shared secret and apply decryption in the reverse order. A structure is -// returned that contains the decrypted error message and information on the -// sender. -func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( - *DecryptedError, error) { +// returned that contains the decrypted error message and information of the +// error sender. We also report the hold times in ms for each hop on the error +// path. +// +// The strictAttribution flag controls the behavior of the decryption logic +// surrounding the presence of attribution data: +// +// - If set, then the first node with bad attribution data will be blamed +// immediately. +// +// - If unset, decryption continues optimistically until a successful error +// message decryption occurs, regardless of attribution data validity. Hold +// times are still extracted for nodes that provided valid attribution data. +func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte, + attrData []byte, strictAttribution bool) (*DecryptedError, error) { - // Ensure the error message length is as expected. + // Ensure the error message field is present and has the correct length. if len(encryptedData) < minOnionErrorLength { return nil, fmt.Errorf("invalid error length: "+ "expected at least %v got %v", minOnionErrorLength, len(encryptedData)) } + validAttr := true + + // If we're decrypting with strict attribution, we need to have the + // correct attribution data present too. If strictAttribution is set + // then we immediately blame the first hop. + if len(attrData) < o.hmacsAndPayloadsLen() { + if strictAttribution { + return &DecryptedError{ + Sender: o.circuit.PaymentPath[0], + SenderIdx: 1, + }, nil + } else { + validAttr = false + } + } + sharedSecrets, _, err := generateSharedSecrets( o.circuit.PaymentPath, o.circuit.SessionKey, @@ -348,10 +392,16 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( ) copy(dummySecret[:], bytes.Repeat([]byte{1}, 32)) + // Copy the failure message data in a new variable. + failData := make([]byte, len(encryptedData)) + copy(failData, encryptedData) + + hopPayloads := make([]uint32, 0) + // 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. - for i := 0; i < NumMaxHops; i++ { + for i := 0; i < o.hopCount; i++ { var sharedSecret Hash256 // If we've already found the sender, then we'll use our dummy @@ -365,13 +415,74 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( } // With the shared secret, we'll now strip off a layer of - // encryption from the encrypted error payload. - encryptedData = onionEncrypt(&sharedSecret, encryptedData) + // encryption from the encrypted failure and attribution + // data. This needs to be done before parsing the attribution + // data, as the attribution data HMACs commit to it. + failData = onionEncrypt(AMMAG, &sharedSecret, failData) + + // If the attribution data are valid then do another round of + // attribution data decryption. + if validAttr { + attrData = onionEncrypt( + AMMAG_EXT, &sharedSecret, attrData, + ) + + payloads := o.payloads(attrData) + hmacs := o.hmacs(attrData) + + // Let's calculate the HMAC we expect for the + // corresponding payloads. + position := o.hopCount - i - 1 + expectedAttrHmac := o.calculateHmac( + sharedSecret, position, failData, payloads, + hmacs, + ) + + // Let's retrieve the actual HMAC from the correct + // position in the HMACs array. + actualAttrHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize] + + // If the hmac does not match up, exit with a nil + // message. This is not done for the dummy iterations. + if !bytes.Equal(actualAttrHmac, expectedAttrHmac) && + sender == 0 && i < len(o.circuit.PaymentPath) { + + switch strictAttribution { + case true: + sender = i + 1 + msg = nil + + case false: + // Flag the attribution data as invalid + // from this point onwards. This will + // prevent the loop from trying to + // extract anything from the attribution + // data. + validAttr = false + } + } + + // Extract the payload and exit with a nil message if it + // is invalid. + holdTime := o.extractPayload(payloads) + if sender == 0 && validAttr { + // Store hold time reported by this node. + hopPayloads = append(hopPayloads, holdTime) + + // Update the message. + msg = failData[sha256.Size:] + } + + // Shift payloads and hmacs to the left to prepare for + // the next iteration. + o.shiftPayloadsLeft(payloads) + o.shiftHmacsLeft(hmacs) + } - // 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:] + // Next, we'll need to separate the failure data, from the MAC + // itself so we can reconstruct and verify it. + expectedMac := failData[:sha256.Size] + data := failData[sha256.Size:] // With the data split, we'll now re-generate the MAC using its // specified key. @@ -395,12 +506,55 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( } return &DecryptedError{ - SenderIdx: sender, Sender: o.circuit.PaymentPath[sender-1], + SenderIdx: sender, Message: msg, + HoldTimes: hopPayloads, }, nil } +// extractPayload extracts the payload and payload origin information from the +// given byte slice. +func (o *OnionErrorDecrypter) extractPayload(payloadBytes []byte) uint32 { + // Extract payload. + holdTime := binary.BigEndian.Uint32(payloadBytes[0:o.payloadLen()]) + + return holdTime +} + +func (o *OnionErrorDecrypter) shiftPayloadsLeft(payloads []byte) { + copy(payloads, payloads[o.payloadLen():o.hopCount*o.payloadLen()]) +} + +func (o *OnionErrorDecrypter) shiftHmacsLeft(hmacs []byte) { + // Work from left to right to avoid overwriting data that is still + // needed later on in the shift operation. + srcIdx := o.hopCount + destIdx := 0 + copyLen := o.hopCount - 1 + for i := 0; i < o.hopCount-1; i++ { + // Clear first hmac slot. This slot is for the position farthest + // away from the error source. Because we are shifting, this + // cannot be relevant. + copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac) + + // The hmacs of the downstream hop become the remaining hmacs + // for the current hop. + copy( + hmacs[(destIdx+1)*o.hmacSize:], + hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize], + ) + + srcIdx += copyLen + destIdx += copyLen + 1 + copyLen-- + } + + // Clear the very last hmac slot. Because we just shifted, the most + // downstream hop can never be the error source. + copy(hmacs[destIdx*o.hmacSize:], o.zeroHmac) +} + // EncryptError is used to make data obfuscation using the generated shared // secret. // @@ -409,17 +563,144 @@ func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) ( // for backward failure obfuscation of the onion failure blob. By obfuscating // the onion failure on every node in the path we are adding additional step of // the security and barrier for malware nodes to retrieve valuable information. -// 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 { +// 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. +// Every node down the error path reports the recorded hold times for the HTLC, +// so this is also passed as an argument to this function in order for this node +// to append its own value. The attribution data is a structure which helps with +// identifying malicious intermediate hops that may have modified the failure +// data. +func (o *OnionErrorEncrypter) EncryptError(initial bool, legacyData []byte, + attrData []byte, holdTime uint32) ([]byte, []byte, error) { + + if initial && attrData != nil { + return nil, nil, fmt.Errorf("unable to encrypt, cannot " + + "initialize error with existing attribution data") + } + + if attrData == nil { + attrData = o.initializePayload(holdTime) + } + if initial { + if len(legacyData) < minPaddedOnionErrorLength { + return nil, nil, fmt.Errorf("initial data size less "+ + "than %v", minPaddedOnionErrorLength) + } + umKey := generateKey("um", &o.sharedSecret) hash := hmac.New(sha256.New, umKey[:]) - hash.Write(data) + hash.Write(legacyData) h := hash.Sum(nil) - data = append(h, data...) + legacyData = append(h, legacyData...) + } else { + if len(attrData) < o.hmacsAndPayloadsLen() { + return nil, nil, ErrInvalidAttrStructure + } + + // Add our hold time. + o.addIntermediatePayload(attrData, holdTime) + + // Shift hmacs to create space for the new hmacs. + o.shiftHmacsRight(o.hmacs(attrData)) + } + + // Update hmac block. + o.addHmacs(attrData, legacyData) + + legacy := onionEncrypt(AMMAG, &o.sharedSecret, legacyData) + attrError := onionEncrypt(AMMAG_EXT, &o.sharedSecret, attrData) + + return legacy, attrError, nil +} + +func (o *OnionErrorEncrypter) shiftHmacsRight(hmacs []byte) { + totalHmacs := (o.hopCount * (o.hopCount + 1)) / 2 + + // Work from right to left to avoid overwriting data that is still + // needed. + srcIdx := totalHmacs - 2 + destIdx := totalHmacs - 1 + + // The variable copyLen contains the number of hmacs to copy for the + // current hop. + copyLen := 1 + for i := 0; i < o.hopCount-1; i++ { + // Shift the hmacs to the right for the current hop. The hmac + // corresponding to the assumed position that is farthest away + // from the error source is discarded. + copy( + hmacs[destIdx*o.hmacSize:], + hmacs[srcIdx*o.hmacSize:(srcIdx+copyLen)*o.hmacSize], + ) + + // The number of hmacs to copy increases by one for each + // iteration. The further away from the error source, the more + // downstream hmacs exist that are relevant. + copyLen++ + + // Update indices backwards for the next iteration. + srcIdx -= copyLen + 1 + destIdx -= copyLen + } + + // Zero out the hmac slots corresponding to every possible position + // relative to the error source for the current hop. This is not + // strictly necessary as these slots are overwritten anyway, but we + // clear them for cleanliness. + for i := 0; i < o.hopCount; i++ { + copy(hmacs[i*o.hmacSize:], o.zeroHmac) + } +} + +// 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 []byte) { + payloads := o.payloads(data) + hmacs := o.hmacs(data) + + for i := 0; i < o.hopCount; i++ { + position := o.hopCount - i - 1 + hmac := o.calculateHmac( + o.sharedSecret, position, message, payloads, hmacs, + ) + + copy(hmacs[i*o.hmacSize:], hmac) } +} + +func (o *OnionErrorEncrypter) initializePayload(holdTime uint32) []byte { + + // Add space for payloads and hmacs. + data := make([]byte, o.hmacsAndPayloadsLen()) + + payloads := o.payloads(data) + + // Signal final hops in the payload. + addPayload(payloads, holdTime) + + return data +} + +func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte, + holdTime uint32) { + + payloads := o.payloads(data) + + // Shift payloads to create space for the new payload. + o.shiftPayloadsRight(payloads) + + // Signal intermediate hop in the payload. + addPayload(payloads, holdTime) +} + +func (o *OnionErrorEncrypter) shiftPayloadsRight(payloads []byte) { + copy(payloads[o.payloadLen():], payloads) +} + +func addPayload(payloads []byte, holdTime uint32) { - return onionEncrypt(&o.sharedSecret, data) + payload := make([]byte, 4) + binary.BigEndian.PutUint32(payload, holdTime) + copy(payloads, payload) } diff --git a/error.go b/error.go index a32c999..77a5e36 100644 --- a/error.go +++ b/error.go @@ -1,6 +1,8 @@ package sphinx -import "fmt" +import ( + "fmt" +) var ( // ErrReplayedPacket is an error returned when a packet is rejected @@ -24,4 +26,8 @@ var ( // ErrLogEntryNotFound is an error returned when a packet lookup in a replay // log fails because it is missing. ErrLogEntryNotFound = fmt.Errorf("sphinx packet is not in log") + + // ErrInvalidAttrStructure is an error that signals that the provided + // attribution data have an invalid length. + ErrInvalidAttrStructure = fmt.Errorf("invalid attribution data length") ) diff --git a/obfuscation.go b/obfuscation.go index a731a6c..a43f819 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -9,29 +9,19 @@ import ( // OnionErrorEncrypter is a struct that's used to implement onion error // encryption as defined within BOLT0004. type OnionErrorEncrypter struct { + *AttrErrorStructure sharedSecret Hash256 } -// NewOnionErrorEncrypter creates new instance of the onion encrypter backed by -// the passed router, with encryption to be done using the passed ephemeralKey. -func NewOnionErrorEncrypter(router *Router, ephemeralKey *btcec.PublicKey, - opts ...ProcessOnionOpt) (*OnionErrorEncrypter, error) { - - cfg := &processOnionCfg{} - for _, o := range opts { - o(cfg) - } - - sharedSecret, err := router.generateSharedSecret( - ephemeralKey, cfg.blindingPoint, - ) - if err != nil { - return nil, err - } +// NewOnionErrorEncrypter creates a new encrypter with the provided shared +// secret and attributable error structure. +func NewOnionErrorEncrypter(sharedSecret Hash256, + structure *AttrErrorStructure) *OnionErrorEncrypter { return &OnionErrorEncrypter{ - sharedSecret: sharedSecret, - }, nil + sharedSecret: sharedSecret, + AttrErrorStructure: structure, + } } // Encode writes the encrypter's shared secret to the provided io.Writer. @@ -121,12 +111,17 @@ func (c *Circuit) Encode(w io.Writer) error { // OnionErrorDecrypter is a struct that's used to decrypt onion errors in // response to failed HTLC routing attempts according to BOLT#4. type OnionErrorDecrypter struct { + *AttrErrorStructure circuit *Circuit } -// NewOnionErrorDecrypter creates new instance of onion decrypter. -func NewOnionErrorDecrypter(circuit *Circuit) *OnionErrorDecrypter { +// NewOnionErrorDecrypter creates new instance of onion decrypter with the +// provided circuit and attributable error structure. +func NewOnionErrorDecrypter(circuit *Circuit, + structure *AttrErrorStructure) *OnionErrorDecrypter { + return &OnionErrorDecrypter{ - circuit: circuit, + circuit: circuit, + AttrErrorStructure: structure, } } diff --git a/obfuscation_test.go b/obfuscation_test.go index 8c0af83..a76a108 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" ) // TestOnionFailure checks the ability of sender of payment to decode the @@ -35,33 +36,42 @@ func TestOnionFailure(t *testing.T) { } // Emulate creation of the obfuscator on node where error have occurred. - obfuscator := &OnionErrorEncrypter{ - sharedSecret: sharedSecrets[len(errorPath)-1], - } + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) // Emulate the situation when last hop creates the onion failure // message and send it back. - obfuscatedData := obfuscator.EncryptError(true, failureData) + legacyData, _, err := obfuscator.EncryptError( + true, failureData, nil, 0, + ) + require.NoError(t, err) // Emulate that failure message is backward obfuscated on every hop. for i := len(errorPath) - 2; i >= 0; i-- { // Emulate creation of the obfuscator on forwarding node which // propagates the onion failure. - obfuscator = &OnionErrorEncrypter{ - sharedSecret: sharedSecrets[i], - } - obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) + obfuscator = NewOnionErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + legacyData, _, err = obfuscator.EncryptError( + false, legacyData, nil, 1, + ) + require.NoError(t, err) } // Emulate creation of the deobfuscator on the receiving onion error side. deobfuscator := NewOnionErrorDecrypter(&Circuit{ SessionKey: sessionKey, PaymentPath: paymentPath, - }) + }, attributableErrorTestStructure) // Emulate that sender node receive the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(obfuscatedData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, nil, false, + ) if err != nil { t.Fatalf("unable to de-obfuscate the onion failure: %v", err) } @@ -195,11 +205,13 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to get specification session key: %v", err) } - var obfuscatedData []byte sharedSecrets, _, err := generateSharedSecrets(paymentPath, sessionKey) if err != nil { t.Fatalf("Unexpected error while generating secrets: %v", err) } + + var legacyData, attrData []byte + for i, test := range onionErrorData { // Decode the shared secret and check that it matchs with // specification. @@ -208,16 +220,17 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to decode spec shared secret: %v", err) } - obfuscator := &OnionErrorEncrypter{ - sharedSecret: sharedSecrets[len(sharedSecrets)-1-i], - } + obfuscator := NewOnionErrorEncrypter( + sharedSecrets[len(sharedSecrets)-1-i], + attributableErrorTestStructure, + ) var b bytes.Buffer if err := obfuscator.Encode(&b); err != nil { t.Fatalf("unable to encode obfuscator: %v", err) } - obfuscator2 := &OnionErrorEncrypter{} + obfuscator2 := NewOnionErrorEncrypter(Hash256{}, attributableErrorTestStructure) obfuscatorReader := bytes.NewReader(b.Bytes()) if err := obfuscator2.Decode(obfuscatorReader); err != nil { t.Fatalf("unable to decode obfuscator: %v", err) @@ -236,11 +249,13 @@ func TestOnionFailureSpecVector(t *testing.T) { if i == 0 { // Emulate the situation when last hop creates the onion failure // message and send it back. - obfuscatedData = obfuscator.EncryptError(true, failureData) + legacyData, attrData, err = obfuscator.EncryptError(true, failureData, nil, 0) + require.NoError(t, err) } else { // Emulate the situation when forward node obfuscates // the onion failure. - obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) + legacyData, attrData, err = obfuscator.EncryptError(false, legacyData, attrData, 0) + require.NoError(t, err) } // Decode the obfuscated data and check that it matches the @@ -250,21 +265,24 @@ func TestOnionFailureSpecVector(t *testing.T) { t.Fatalf("unable to decode spec obfusacted "+ "data: %v", err) } - if !bytes.Equal(expectedEncryptErrordData, obfuscatedData) { + if !bytes.Equal(expectedEncryptErrordData, legacyData) { t.Fatalf("obfuscated data not match spec: expected %x, "+ "got %x", expectedEncryptErrordData[:], - obfuscatedData[:]) + legacyData[:]) } } deobfuscator := NewOnionErrorDecrypter(&Circuit{ SessionKey: sessionKey, PaymentPath: paymentPath, - }) + }, attributableErrorTestStructure, + ) // Emulate that sender node receives the failure message and trying to // unwrap it, by applying obfuscation and checking the hmac. - decryptedError, err := deobfuscator.DecryptError(obfuscatedData) + decryptedError, err := deobfuscator.DecryptError( + legacyData, attrData, false, + ) if err != nil { t.Fatalf("unable to de-obfuscate the onion failure: %v", err) } diff --git a/sphinx.go b/sphinx.go index 8e16b23..423082e 100644 --- a/sphinx.go +++ b/sphinx.go @@ -546,7 +546,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret( + sharedSecret, err := r.GenerateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { @@ -587,7 +587,7 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte, } // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret( + sharedSecret, err := r.GenerateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { @@ -780,7 +780,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, } // Compute the shared secret for this onion packet. - sharedSecret, err := t.router.generateSharedSecret( + sharedSecret, err := t.router.GenerateSharedSecret( onionPkt.EphemeralKey, cfg.blindingPoint, ) if err != nil { diff --git a/testdata/onion-test.json b/testdata/onion-test.json index 193256f..5721d3e 100644 --- a/testdata/onion-test.json +++ b/testdata/onion-test.json @@ -1,28 +1,39 @@ { "comment": "A testcase for a variable length hop_payload. The third payload is 275 bytes long.", "generate": { + "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "session_key": "4141414141414141414141414141414141414141414141414141414141414141", "associated_data": "4242424242424242424242424242424242424242424242424242424242424242", "hops": [ { "pubkey": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", - "payload": "1202023a98040205dc06080000000000000001" + "payload": "1202023a98040205dc06080000000000000001", + "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", + "encryptedMessage": "d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252" }, { "pubkey": "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", - "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f" + "payload": "52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f", + "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", + "encryptedMessage": "1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb" }, { "pubkey": "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", - "payload": "12020230d4040204e206080000000000000003" + "payload": "12020230d4040204e206080000000000000003", + "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", + "encryptedMessage": "34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a" }, { "pubkey": "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", - "payload": "1202022710040203e806080000000000000004" + "payload": "1202022710040203e806080000000000000004", + "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", + "encryptedMessage": "74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1" }, { "pubkey": "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", - "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a" + "payload": "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", + "encryptedMessage": "84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad" } ] },