From b0bf831fe4bf656af13f2f139668ff294aef94c0 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 23 May 2023 14:52:52 +0200 Subject: [PATCH 1/4] add minPaddedOnionErrorLength constant --- crypto.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crypto.go b/crypto.go index 0930544..b4ae481 100644 --- a/crypto.go +++ b/crypto.go @@ -61,8 +61,8 @@ func (p *PrivKeyECDH) PubKey() *btcec.PublicKey { // k is our private key, and P is the public key, we perform the following // operation: // -// sx := k*P -// s := sha256(sx.SerializeCompressed()) +// sx := k*P +// s := sha256(sx.SerializeCompressed()) // // NOTE: This is part of the SingleKeyECDH interface. func (p *PrivKeyECDH) ECDH(pub *btcec.PublicKey) ([32]byte, error) { @@ -235,10 +235,14 @@ func onionEncrypt(sharedSecret *Hash256, data []byte) []byte { 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 From 7b827337175239c32b5b8df5d162654a298939d4 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 17 Jan 2023 13:44:32 +0100 Subject: [PATCH 2/4] initialize NewOnionErrorEncrypter with shared secret directly Allow for more flexible usage of the error encrypter. This is useful when upgrading an existing legacy error encrypter to fat errors in lnd. --- obfuscation.go | 11 ++--------- obfuscation_test.go | 16 ++++++---------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/obfuscation.go b/obfuscation.go index b8df7cc..14aabbe 100644 --- a/obfuscation.go +++ b/obfuscation.go @@ -15,17 +15,10 @@ type OnionErrorEncrypter struct { // NewOnionErrorEncrypter creates new instance of the onion encrypter backed by // the passed router, with encryption to be doing using the passed // ephemeralKey. -func NewOnionErrorEncrypter(router *Router, - ephemeralKey *btcec.PublicKey) (*OnionErrorEncrypter, error) { - - sharedSecret, err := router.generateSharedSecret(ephemeralKey) - if err != nil { - return nil, err - } - +func NewOnionErrorEncrypter(sharedSecret Hash256) *OnionErrorEncrypter { return &OnionErrorEncrypter{ sharedSecret: sharedSecret, - }, nil + } } // Encode writes the encrypter's shared secret to the provided io.Writer. diff --git a/obfuscation_test.go b/obfuscation_test.go index da62ed0..d8f3cbd 100644 --- a/obfuscation_test.go +++ b/obfuscation_test.go @@ -35,9 +35,7 @@ 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]) // Emulate the situation when last hop creates the onion failure // message and send it back. @@ -47,9 +45,7 @@ func TestOnionFailure(t *testing.T) { 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], - } + obfuscator = NewOnionErrorEncrypter(sharedSecrets[i]) obfuscatedData = obfuscator.EncryptError(false, obfuscatedData) } @@ -208,16 +204,16 @@ 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], + ) var b bytes.Buffer if err := obfuscator.Encode(&b); err != nil { t.Fatalf("unable to encode obfuscator: %v", err) } - obfuscator2 := &OnionErrorEncrypter{} + obfuscator2 := NewOnionErrorEncrypter(Hash256{}) obfuscatorReader := bytes.NewReader(b.Bytes()) if err := obfuscator2.Decode(obfuscatorReader); err != nil { t.Fatalf("unable to decode obfuscator: %v", err) From 78880cd09c878c645fe8bcaae13f4564bec411ac Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 17 Jan 2023 13:51:08 +0100 Subject: [PATCH 3/4] export GenerateSharedSecret --- crypto.go | 8 ++++---- sphinx.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crypto.go b/crypto.go index b4ae481..16acc66 100644 --- a/crypto.go +++ b/crypto.go @@ -204,13 +204,13 @@ func blindBaseElement(blindingFactor btcec.ModNScalar) *btcec.PublicKey { // // 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 by given ephemeral key. -func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { +// GenerateSharedSecret generates the shared secret by given ephemeral key. +func (r *Router) GenerateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) { var sharedSecret Hash256 // Ensure that the public key is on our curve. diff --git a/sphinx.go b/sphinx.go index 36e9a81..d59d0e1 100644 --- a/sphinx.go +++ b/sphinx.go @@ -534,7 +534,7 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) { // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.GenerateSharedSecret(onionPkt.EphemeralKey) if err != nil { return nil, err } @@ -568,7 +568,7 @@ func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket, assocData []byte) (*ProcessedPacket, error) { // Compute the shared secret for this onion packet. - sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey) + sharedSecret, err := r.GenerateSharedSecret(onionPkt.EphemeralKey) if err != nil { return nil, err } @@ -731,7 +731,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket, assocData []byte, incomingCltv uint32) error { // Compute the shared secret for this onion packet. - sharedSecret, err := t.router.generateSharedSecret( + sharedSecret, err := t.router.GenerateSharedSecret( onionPkt.EphemeralKey, ) if err != nil { From 787ad3d102b0b71bd7700b1c5a2263bdfa249f8f Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 10 Nov 2022 10:08:53 +0100 Subject: [PATCH 4/4] add attributable error encryption and decryption --- attributable_error_crypto.go | 170 ++++++++++++ attributable_error_crypto_test.go | 432 ++++++++++++++++++++++++++++++ attributable_error_decrypt.go | 211 +++++++++++++++ attributable_error_encrypt.go | 171 ++++++++++++ go.mod | 1 + go.sum | 14 + testdata/attributable_error.json | 25 ++ 7 files changed, 1024 insertions(+) create mode 100644 attributable_error_crypto.go create mode 100644 attributable_error_crypto_test.go create mode 100644 attributable_error_decrypt.go create mode 100644 attributable_error_encrypt.go create mode 100644 testdata/attributable_error.json diff --git a/attributable_error_crypto.go b/attributable_error_crypto.go new file mode 100644 index 0000000..1aad840 --- /dev/null +++ b/attributable_error_crypto.go @@ -0,0 +1,170 @@ +package sphinx + +import ( + "crypto/hmac" + "crypto/sha256" + "io" +) + +type payloadSource byte + +const ( + // payloadIntermediateNode is a marker to signal that this attributable + // error payload is originating from a node between the payer and the + // error source. + payloadIntermediateNode payloadSource = 0 + + // payloadErrorNode is a marker to signal that this attributable error + // payload is originating from the error source. + payloadErrorNode payloadSource = 1 +) + +// 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 +} + +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 1 + o.fixedPayloadLen +} + +// message returns a slice containing the message in the given failure data +// block. The message is positioned at the beginning of the block. +func (o *AttrErrorStructure) message(data []byte) []byte { + return data[:len(data)-o.hmacsAndPayloadsLen()] +} + +// 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/attributable_error_crypto_test.go b/attributable_error_crypto_test.go new file mode 100644 index 0000000..8b22264 --- /dev/null +++ b/attributable_error_crypto_test.go @@ -0,0 +1,432 @@ +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) { + t.Parallel() + + t.Run("32 byte hmac", func(t *testing.T) { testAttributableOnionFailure(t, 32) }) + t.Run("4 byte hmac", func(t *testing.T) { testAttributableOnionFailure(t, 4) }) +} + +// TestAttributableOnionFailure checks the ability of sender of payment to +// decode the obfuscated onion error. +func testAttributableOnionFailure(t *testing.T, hmacBytes int) { + t.Parallel() + + var structure = NewAttrErrorStructure(27, 8, hmacBytes) + + // 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 := NewOnionAttrErrorEncrypter( + sharedSecrets[len(errorPath)-1], structure, + ) + + // Emulate the situation when last hop creates the onion failure + // message and send it back. + finalPayload := [8]byte{1} + obfuscatedData, err := obfuscator.EncryptError( + true, failureData, finalPayload[:], + ) + require.NoError(t, err) + payloads := [][]byte{finalPayload[:]} + + // 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 = NewOnionAttrErrorEncrypter( + sharedSecrets[i], structure, + ) + + intermediatePayload := [8]byte{byte(100 + i)} + obfuscatedData, err = obfuscator.EncryptError( + false, obfuscatedData, intermediatePayload[:], + ) + require.NoError(t, err) + + payloads = append([][]byte{intermediatePayload[:]}, payloads...) + } + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionAttrErrorDecrypter(&Circuit{ + SessionKey: sessionKey, + PaymentPath: paymentPath, + }, structure) + + // 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) + 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.Payloads) +} + +// 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 := NewOnionAttrErrorEncrypter( + sharedSecrets[len(errorPath)-1], attributableErrorTestStructure, + ) + + // Emulate the situation when last hop creates the onion failure + // message and send it back. + payload := [4]byte{1} + obfuscatedData, err := obfuscator.EncryptError( + true, failureData, payload[:], + ) + 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 = NewOnionAttrErrorEncrypter( + sharedSecrets[i], attributableErrorTestStructure, + ) + + payload := [4]byte{byte(100 + i)} + obfuscatedData, err = obfuscator.EncryptError( + false, obfuscatedData, payload[:], + ) + require.NoError(t, err) + + // Hop 1 (the second hop from the sender pov) is corrupting the + // failure message. + if i == 1 { + obfuscatedData[0] ^= 255 + } + } + + // Emulate creation of the deobfuscator on the receiving onion error + // side. + deobfuscator := NewOnionAttrErrorDecrypter(&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) + 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 specVector struct { + EncodedFailureMessage string `json:"encodedFailureMessage"` + + Hops []specHop `json:"hops"` +} + +// TestOnionFailureSpecVector checks that onion error corresponds to the +// specification. +func TestAttributableFailureSpecVector(t *testing.T) { + t.Parallel() + + vectorBytes, err := os.ReadFile("testdata/attributable_error.json") + require.NoError(t, err) + + var vector specVector + require.NoError(t, json.Unmarshal(vectorBytes, &vector)) + + failureData, err := hex.DecodeString(vector.EncodedFailureMessage) + require.NoError(t, err) + + paymentPath, err := getSpecPubKeys() + require.NoError(t, err) + + sessionKey, err := getSpecSessionKey() + require.NoError(t, err) + + var obfuscatedData []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 := NewOnionAttrErrorEncrypter( + sharedSecrets[len(sharedSecrets)-1-i], + attributableErrorTestStructure, + ) + + require.Equal( + t, expectedSharedSecret, obfuscator.sharedSecret[:], + ) + + payload := [4]byte{0, 0, 0, byte(i + 1)} + + if i == 0 { + // Emulate the situation when last hop creates the onion + // failure message and send it back. + obfuscatedData, err = obfuscator.EncryptError( + true, failureData, payload[:], + ) + require.NoError(t, err) + } else { + // Emulate the situation when forward node obfuscates + // the onion failure. + obfuscatedData, err = obfuscator.EncryptError( + false, obfuscatedData, payload[:], + ) + 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, obfuscatedData) + } + + deobfuscator := NewOnionAttrErrorDecrypter(&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) + require.NoError(t, err) + + // Check that message have been properly de-obfuscated. + require.Equal(t, decryptedError.Message, failureData) + + // We should understand the node from which error have been received. + require.Equal(t, + decryptedError.Sender.SerializeCompressed(), + paymentPath[len(paymentPath)-1].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 := NewOnionAttrErrorDecrypter(&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) + 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 := NewOnionAttrErrorDecrypter(&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) + + decryptedError, err := deobfuscator.DecryptError(obfuscatedData) + require.NoError(t, err) + + 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 := NewOnionAttrErrorEncrypter( + 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 := NewOnionAttrErrorDecrypter( + 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/attributable_error_decrypt.go b/attributable_error_decrypt.go new file mode 100644 index 0000000..d85da4f --- /dev/null +++ b/attributable_error_decrypt.go @@ -0,0 +1,211 @@ +package sphinx + +import ( + "bytes" + "errors" + "fmt" +) + +// DecryptedAttrError contains the decrypted attributable error message +// and its sender. +type DecryptedAttrError struct { + DecryptedError + + // Payloads is an array of data blocks reported by each node on the + // (error) path. + Payloads [][]byte +} + +// OnionAttrErrorDecrypter is a struct that's used to decrypt +// attributable onion errors in response to failed HTLC routing attempts +// according to BOLT#4. +type OnionAttrErrorDecrypter struct { + AttrErrorStructure + + circuit *Circuit +} + +// NewOnionAttrErrorDecrypter creates new instance of an attributable +// error onion decrypter. +func NewOnionAttrErrorDecrypter(circuit *Circuit, + structure *AttrErrorStructure) *OnionAttrErrorDecrypter { + + return &OnionAttrErrorDecrypter{ + AttrErrorStructure: *structure, + circuit: circuit, + } +} + +// 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 *OnionAttrErrorDecrypter) DecryptError(encryptedData []byte) ( + *DecryptedAttrError, error) { + + // Ensure the error message length is enough to contain the payloads and + // hmacs blocks. Otherwise blame the first hop. + if len(encryptedData) < + minPaddedOnionErrorLength+o.hmacsAndPayloadsLen() { + + return &DecryptedAttrError{ + DecryptedError: DecryptedError{ + SenderIdx: 1, + Sender: o.circuit.PaymentPath[0], + }, + }, nil + } + + sharedSecrets, err := generateSharedSecrets( + o.circuit.PaymentPath, + o.circuit.SessionKey, + ) + if err != nil { + return nil, fmt.Errorf("error generating shared secret: "+ + "%w", err) + } + + var ( + sender int + msg []byte + dummySecret Hash256 + ) + copy(dummySecret[:], bytes.Repeat([]byte{1}, 32)) + + // 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. + hopPayloads := make([][]byte, 0) + for i := 0; i < o.hopCount; i++ { + var sharedSecret Hash256 + + // If we've already found the sender, then we'll use our dummy + // secret to continue decryption attempts to fill out the rest + // of the loop. Otherwise, we'll use the next shared secret in + // line. + if sender != 0 || i > len(sharedSecrets)-1 { + sharedSecret = dummySecret + } else { + sharedSecret = sharedSecrets[i] + } + + // With the shared secret, we'll now strip off a layer of + // encryption from the encrypted error payload. + encryptedData = onionEncrypt(&sharedSecret, encryptedData) + + message := o.message(encryptedData) + payloads := o.payloads(encryptedData) + hmacs := o.hmacs(encryptedData) + + position := o.hopCount - i - 1 + expectedHmac := o.calculateHmac( + sharedSecret, position, message, payloads, hmacs, + ) + actualHmac := hmacs[i*o.hmacSize : (i+1)*o.hmacSize] + + // If the hmac does not match up, exit with a nil message but + // only after finishing the constant number of iterations. + if !bytes.Equal(actualHmac, expectedHmac) && sender == 0 { + sender = i + 1 + msg = nil + } + + // Extract the payload and exit with a nil message if it is + // invalid. + source, payload, err := o.extractPayload(payloads) + if sender == 0 { + if err != nil { + sender = i + 1 + msg = nil + } + + // Store data reported by this node. + hopPayloads = append(hopPayloads, payload) + + // If we are at the node that is the source of the + // error, we can now save the message in our return + // variable. + if source == payloadErrorNode { + sender = i + 1 + msg = message + } + } + + // Shift payloads and hmacs to the left to prepare for the next + // iteration. + o.shiftPayloadsLeft(payloads) + o.shiftHmacsLeft(hmacs) + } + + // 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 { + sender = o.hopCount + msg = nil + } + + return &DecryptedAttrError{ + DecryptedError: DecryptedError{ + SenderIdx: sender, + Sender: o.circuit.PaymentPath[sender-1], + Message: msg, + }, + Payloads: hopPayloads, + }, nil +} + +func (o *OnionAttrErrorDecrypter) 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) +} + +func (o *OnionAttrErrorDecrypter) shiftPayloadsLeft(payloads []byte) { + copy(payloads, payloads[o.payloadLen():o.hopCount*o.payloadLen()]) +} + +// extractPayload extracts the payload and payload origin information from the +// given byte slice. +func (o *OnionAttrErrorDecrypter) extractPayload(payloadBytes []byte) ( + payloadSource, []byte, error) { + + source := payloadSource(payloadBytes[0]) + + // Validate source indicator. + if source != payloadErrorNode && source != payloadIntermediateNode { + return 0, nil, errors.New("invalid payload source indicator") + } + + // Extract payload. + payload := make([]byte, o.fixedPayloadLen) + copy(payload, payloadBytes[1:o.payloadLen()]) + + return source, payload, nil +} diff --git a/attributable_error_encrypt.go b/attributable_error_encrypt.go new file mode 100644 index 0000000..8bece10 --- /dev/null +++ b/attributable_error_encrypt.go @@ -0,0 +1,171 @@ +package sphinx + +import ( + "errors" + "fmt" +) + +// ErrInvalidStructure is returned when the failure message has an invalid +// structure. This is typically returned for messages that are shorter than the +// minimum length. +var ErrInvalidStructure = errors.New("failure message has invalid structure") + +// NewOnionAttrErrorEncrypter creates new instance of the onion +// encrypter backed by the passed shared secret. +func NewOnionAttrErrorEncrypter(sharedSecret Hash256, + structure *AttrErrorStructure) *OnionAttrErrorEncrypter { + + return &OnionAttrErrorEncrypter{ + AttrErrorStructure: *structure, + + sharedSecret: sharedSecret, + } +} + +// OnionAttrErrorEncrypter is a struct that's used to implement +// attributable onion error encryption as defined within BOLT0004. +type OnionAttrErrorEncrypter struct { + AttrErrorStructure + + sharedSecret Hash256 +} + +func (o *OnionAttrErrorEncrypter) 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) + } +} + +func (o *OnionAttrErrorEncrypter) shiftPayloadsRight(payloads []byte) { + copy(payloads[o.payloadLen():], payloads) +} + +// addHmacs updates the failure data with a series of hmacs corresponding to all +// possible positions in the path for the current node. +func (o *OnionAttrErrorEncrypter) addHmacs(data []byte) { + message := o.message(data) + 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) + } +} + +// EncryptError is used to make data obfuscation using the generated shared +// secret. +// +// In context of Lightning Network is either used by the nodes in order to make +// initial obfuscation with the creation of the hmac or by the forwarding nodes +// 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 *OnionAttrErrorEncrypter) EncryptError(initial bool, + data []byte, payload []byte) ([]byte, error) { + + if len(payload) > o.fixedPayloadLen { + return nil, fmt.Errorf("payload exceeds maximum length") + } + + if initial { + if len(data) < minPaddedOnionErrorLength { + return nil, fmt.Errorf( + "initial data size less than %v", + minPaddedOnionErrorLength, + ) + } + + data = o.initializePayload(data, payload) + } else { + if len(data) < + minPaddedOnionErrorLength+o.hmacsAndPayloadsLen() { + + return nil, ErrInvalidStructure + } + + o.addIntermediatePayload(data, payload) + + // Shift hmacs to create space for the new hmacs. + o.shiftHmacsRight(o.hmacs(data)) + } + + // Update hmac block. + o.addHmacs(data) + + // Obfuscate. + return onionEncrypt(&o.sharedSecret, data), nil +} + +func (o *OnionAttrErrorEncrypter) initializePayload(message []byte, + payload []byte) []byte { + + // Add space for payloads and hmacs. + data := make([]byte, len(message)+o.hmacsAndPayloadsLen()) + copy(data, message) + + payloads := o.payloads(data) + + // Signal final hops in the payload. + addPayload(payloads, payloadErrorNode, payload) + + return data +} + +func (o *OnionAttrErrorEncrypter) addIntermediatePayload(data []byte, + payload []byte) { + + payloads := o.payloads(data) + + // Shift payloads to create space for the new payload. + o.shiftPayloadsRight(payloads) + + // Signal intermediate hop in the payload. + addPayload(payloads, payloadIntermediateNode, payload) +} + +func addPayload(payloads []byte, source payloadSource, + payload []byte) { + + payloads[0] = byte(source) + copy(payloads[1:], payload) +} diff --git a/go.mod b/go.mod index d274362..f049b44 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index fe83050..449e1a6 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/testdata/attributable_error.json b/testdata/attributable_error.json new file mode 100644 index 0000000..218f9e3 --- /dev/null +++ b/testdata/attributable_error.json @@ -0,0 +1,25 @@ +{ + "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "hops": [ + { + "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", + "encryptedMessage": "e88935bf7c9a374ba64fd9da3aa2a05e6cf9e52cfb1f72698fd33b74b9c79346284931ea3571af54de5df341304833b0825fb7e8817fd82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d0c84ad72f642fc26919927b347808bade4b1c321b08bc363f20745ba2f97f0ced2996a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9e2a007228a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f92e2cf87a89ed786194104eba6b7973491f04d0b924c10e6fcff336d037b6e53fe763919da946a640960978994e8d0e5a2d555c9a897ce38a324c766fd01e9416acd91f1ea345c12aa1cfa5933c5c1230c5e45efb8c7e8d75bd9dd85ce8228cf80a52c915282375663690d1286ba0e70201af791a25715819dfd1035feb5239e3df7c230956cb3be858395094d3d99cbc2352cb8adc7a8fe8f4755a2f93bef3926f57bbcff17956c4031a2ae8c88d57dd9235b49a0253e86a7f173d96907aa2e162c82f1626dd5c07188e5e01d79724a546bed2b89a2084230b770ff97b2271158569ed7d00f967cdc51d216fa1578a9624f9142d8de1039b5d4f51de09324c91582f830c7730934feffbc7c51d5d87e8760a77e0712d947190ea6f896a4685045a3de3b8187490ee65f68a9c40cf708e03ab5f28a3b7e5e4a164c3cdb3a7a393b120a2306671a3e310419f873e1d978ff08535353a85eb1773e7476f1f102e3f2364036427a633d32cbf1c34ee0a223f696e69d9e296ac4981d64c99e9966d93eef673163ac774b2545e36e64816030b4ebb7775afeb77f88396c565d58bf2f2d07601dfc5338e5a5a71853dac2e42fc2d89a2da7bf913a8d5c1705ef3c869dc6a7d3a6a8ec7cde4e99380c0223b8d766506574f45cafbe96b25ebefd066945bffc1d2262d1e7d1057660ec2916055a493f2930afe8133de9e593b470e2d512ab5bedb363a6c9ac59fd82379f528d02bae1ade38fafe2d7ac7b097bb6fa4e00e43b087d3480f41402a5156379052526827da75486cf8703ed9ba34f38a9b445da35c3e5bd799156f25081e88051d54fe47fd0f7bca364d7cdeb01cb28aadc9b03fdd91712036f1da31a554e230a897e7142c0043c98e1d9a4bd996d26dfbce431bce8a6e29048783a7142c84483895c44df5d65c3bf8ca1bd6069ad305e525df7a4c584299525549bd3d013dcbfaebf18dcd82a9d29618a9b3e564b19cfdeac6eb7a6c4ad42268747fbd162c2f300aaad722d59dde7db179d93468b435724d97078df797d75d1728e75d0687e661fb603fa1b264466b8cedb75482ef042151f2ca045c795683a5e56dfff85a17f82cc5aba7e187784f159a996c3200b8b3f2a91de25d378557c5b33f2a17ec877b15cccc5615836899c30912cb83390e24902cbecb57cb95c0738d798debc43a07b09060347b145d5ccc150fcc46bc0a21f372622f25acae867946346e54498737be61c312e93086748b37633cbd8ab62433e6375914f117b9c1cb802513f" + }, + { + "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", + "encryptedMessage": "89f5945b1ed1f4bbe9a33a706089c10f7c0dc4c0995fff7f4b5d9db50e04aca120031a33e1148091d8d3a3138f25397a6048d7f022a71f751e44a72d9b3f79809c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab6960c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185cdc615a346e11eaf8f32cd44d5f1213d4738768f081978420697b454700ade1c093c02a6ca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be649cc7c5ebe91be307bcd3ef972eac04ee1411897667db31217e01aad868554ab56be9a21a0827e29cd8829428ddbe7bb86f23d0a46aa6d54aa36e9b61498e690229236bd5e7b25afbc5a31661cd9713b904e57c17187a147f2eafedd595db46b26e5a9f6393b82a55e81a93d72f4b050fbedcd3a527992e8b4f2f5a1ca36aff1037e645937f2633c27a3cd0902342527df7617d332efb2199718fc5646aaba27826267e79705f766e8a0d6f2249dad7f5814957c48d1ef27f94317ad4e523289315b34b4f47579814e6513bc55ea6535fc6963388dbcbe6caa8d708f1f800cdf4c6060bfb69e001d8664a02fb09698b4f1b8fc4a2787ef9080d22b0b4f19dafce139769bcf532ad1e21d5cb7a7e65061444030ca30726a69f09756c8e2e6a37feea37af317ff77f6303922472ca6b261de9ae33626c9880df0b48667dbba7b04b7e7836519dab7358567c7324d106c831657528d86ec2edfd845faae8ea94223254fbefb738db202e0eca9001b3313127d1a33f470c4b29b05280b3b2f7b4bfc9f33497ffef59e78a120513e63d743d60c1ff176f40283de63ce4d37d9cc99af46ed15dd58e61dc1669d8bfa5b0eda8feda7dc511b1d07c26452dacefb5ad5377669785ade7d3a8c53dedfacbfa4461416c7cb4a5dc9140194088d2672f4c0f7caa214d2d1705e4d73bc3632639b5ef15b49bd7c50de3f2ab66608c25d212711a23c6a0ef9f22b224fa1a2059d8174aad941094cfd05cc6cd5f634ec5d0f4cec8454d1ee4817c48b36d2089d8c5c0e970e9839ae33b10bd0969e8af37e38f4361de930546d07f3e3745dac990fe470fda1d3b1041d3eaad6d19ef6e70c1585ef9f3d14280a4d3730104dfa49d3fe40e51e7c49eef6db35011c7e7f4daca2f0f36fc87742306208f10e6b85415f53f3a2316a2f124136aa6b6a9a2a5a43bc755f481344d3067bd28810adfe60cba3dec1e11cb66c4648749b42108cab1dc85fdb5bf28d4c7c95e273cd8203a35831b35ce40f4153778f0ddd200ea2652032840a3ddb1e46a2ab383d18ef9b9db426d016c6bfb9a05068f04f2a54be9405cac27c2e601568bdbaafeb5353363b2ab07168758a94ae6bc903e57e0cd41a2c753d2ccc5b6988a77637efc22ed648e2fb6eeac5504dfc05553f3f611f4f56248f170eabf4ddb19b4d006cbf6358f384914f1bd586c48b63aea04e35bea4fa5e56d8ed6780f30aaee19171f804532d654663c66ff4593ce7b495525c66a10bb7935f068a0154ae2a13704ab80176d1283d3e92bbf663ee100425ac01c73137e36ab8d8c845a725c3ecabc6973d35dfd" + }, + { + "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", + "encryptedMessage": "e8bc60d044af7b8686febd7b1ae04f2f30fb809233ebb730ba837bc4f06751ea7f1862ddc1535f37c6eef4789bbed4d5e34d5875d2cd2e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c1155802b8866064f7bad07a50da5cf31f8c3151c4c52e525fb22ecf48f8fa39bb5adf932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca11bb685616be825941bd8b2cac7d37d1a28e5a4531601bfa9146c590e2bc0ce1fdabd288184b1dea153ad23f85402dc956e0b25f025dd2456f74b0d44b0d8e2d244fcaa04c65f1d027519158a8e5a83b00988d8c8bded5a8478326738e2b9ab467f27c42fecec7f6e48f5ee435266220b6c6ebfd9eb7a50b96d665f018adb86427a46208f39e63ab71876131d50bdb524367f5d538cfdba78a94cd1ca4e6381749f8823a100ea020169f16596f3d8cfff49dbda803c3afb89c7fcfec9dbe979b89882a347fa8e441b1d3bea00654e247912ccc4b85946c38f6a7c32d6b102c596dfa9bd78a24603d8ae93a713c342660036249d60fd0c3874c2e9545b3d099afaffe9dfced8e9123f4720de054e6d91329abcd91b07907af03673652823f9988039f87ebfa2ff156831c8881d16776771ec81b20ee37918b5fe81bef9e6710d4430c8e924ca15e8b5d061982e369923f819b1313ad94b9bc0e846df659367079f0113bb43f7c99dc5d5ae8958c605d7bafaed7c04a078c16d1cc8bd622a92676269452f06dcdc3a0891a091ed9403606d482d4cd7e99b067bb66ff3cf428a2fc4469bafc76ae172402338e3a59a3a607361293192385c141e31caa6a49615115ffaca858b400385be7e5472e35afb2a5b4b761b8c69767727516f974734f5d00b917deb2ec7a5ca05fb3683fc0faf155bec35854359707da65fa13df32ba237ef0ae21da9f60e68f157a196b1155e5fb33f43789ba9f43d2f6852c63c147f26a51dc5fc767326e2b56464efe376afb90c4a90d071d288051fd8af10ffb52421bbbdbe37f3d83dd6408f33241f54b135460ac285df9b27cd6ab2c109996f3fb4eb6244f9daa7920bb492c0958caa0f4239205e50c7297b575dbb9ce8f3154a5a5a9afa7eb80dd4b9de6143603b135fe4e0471a49fb8b364e0f828a32f57a21dec19a41d999e2f9b8c262437016d9d5cd62517a1bed4a8fb1d9d71e6058b2b22386e4cb5699ad04ab358a49359376cb9e360645758fcd28031705b48f393ca7d4cead494939e9a6956eaa5af0666897f0ce525dc6fff80b1c2b972ccc5d9a0b4bd710396c416991d6bbc545b2e0348c249ed2c3f3032e350252e05c974ee73d72341b466e40d0e89e132f17759ab9d0764e1c1e2f3464aa22cb928a9adc4216dd31a39e59ddc9d8b775d946bde0f651b2d5a32e6da41ffddbe7520a75329a5ac1c2e75f2a73d7b67ca41923b6517b7101b46304604607348ae458ca70de9f4fda848f9076145a0a6d8f26a1c6504a018c3f79f9d3a74c37a7ac5785acd87bf7ac39c148135f8d956b20eb396abe" + }, + { + "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", + "encryptedMessage": "e7aca8bf4139f65f977604e3f4a593f2cb031f7b9a711107e6c19691d0c5060a375d0d39d6920d2e08d72d59cee7827bccbacd62aeaee61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eb2a6bddfc370eb45c1699c856969e2d574fdd155945ed727fdf2aec4f056a4d49fdefc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc0369b609e1d6b7f9a98d6f139d335d34022afc76f3c22354b0b43dd865b1c211cf8b1850ac389a1ba507ecc961434b3b61e906895d17f34580cf3ceefd4d77c06c1560aea2b20798f3217c3fab544800b1b19538971f4650217c566f14bb6845e210a0339bed7b93a9d53ae8a6bdca8ede02544601bb6da225d0b138e4217c33c1be2b964a265a8065b893dbcc8fcf74a861311136c5f236b7c3cd2d28e5175789bcf6cbc9974b6ce6285fd958cb7f6f5e5d879a76b4fd6619c34d643120383791a2ce6bfc611885319a13f47c7ba2e8c1ac4c50da3714b5a3c63b0afaa73cbbebc3724383da6c044ff5480fc3ec4d8b43b45dbe206de176e443fcdd383fbacdf3e4256a13068b9750dd1ff605a7f340edd9dbad0f213d828d5ad10bc99189b8b3b3bfeea855c02050e51f66c402b588b9b7c354875b259e128702f6760cd6f77e9dfed0db82075aadb3ccbd5e7024d819e11f1fe57fee830d3b732a9d409d3ee39824857843b41c9d17b6e78bdd014209a2e9006a2823940bd7750ee94a1011e55428933ed90a588feefdb1bbb684ad5538eb4126488812dda96d8b68de37302458d2c06121877a33d2a0decf879407cc53f6a02b5b111b2a4504ac10a8c770e039a68cf50c5ba146dea36bbc8cef7cdcce348fbc607dca64214d9045adbc8987a8ab76666c6f4e219b2e53a53d46740153b25e3e872037ff0322f03159ce4309511d97b8b9d5ba43c8d49ef7962ceacbf04d5c704deb341917ca18fbf3ff6025ffb3b2b400a3ca6fa32a3672d29c3530eb68a5425f022c9958993f92ebcee24a64046ff1dde5ae330016eef4536e2e0a13aa2e13ff3c93fa241c5bcba94bd9493fe8b453b24497a41c832f2899b52b5eaba962c8456fac88b4d6857b2f6d45330d2447142c447038b46177ee57693b9387f5b2ac7a491df3e03094b36b6ccdf668abe836571850476540ac313269bdbed5417f0f6f56f1a35e0a37a28c85f6268b6a89e29ddbd45593f6d69e9ea550c41fc9d265a2a552e1423cc1005487ba118d1164871a16ebe3933b05fb3a3c8614adbf0a4513eaf5720b81794ecce8d9048c8856379760c7e382f8fb0ad7978070087fa1f8c064fa671269bb64c4b1fc9f860b84755a6fda1ef798c1033a1e7a4719c78201acadb2165bc97026f66a21b556d4d09b1e8b0e29e050f6f034a9e649a4a2d1d34f42263b24c4e257909ab62d259188aab42f77f1018490d1de38ef9af5b973335537b09e3816b117a8bb187cb7bd30a1779aec88cf70821a6a0ee4d76e220dbdbacb37e777dd863eccbfc86c04650916c0adbc40d3b7f80" + }, + { + "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", + "encryptedMessage": "d135558a6bad7c5a617272dfff6fba9a1ee9d5509b697129b42407b020959566ba84011d09efc5e187c148ba548bf7922e7a5d6c0ceb2ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822e4d8e15a7c521d67ce2cc836c49118f205c99f18570504504221e337a29e2716fb28671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7e3d387988960f41b82bdda83a4e39828a8644af0377cb3f1cfa6bb26cfa878a5d4599ad2cbd72475931259903165cccb17679e3d0aaa2577f798de15b227f4b0dbf92b8ef891d17c814ab2e4e599b01f4b66c830b7dbbcfb3ab33a9e0af7398c4076f2256986710f7ca41cd8b477579a2e45c32830a3498baf7607b2ead37b75242a7c58dfe486b9b959ff639d04c2b0398fd00a8f8ee76f531b72fd5a3f09ed79e0de573c228f32488179c302be2677576354e22079eeb0a3ffaef65c875078c714d16ed36cf10892352b76d14764f51f0ffbb62f98faf263f41e84ec1ab0d4f723d279879f655be2c5caef768ed6be313a751f0f2af9e3517107f23012c54f36bdcc96f0f1c721bcb8a927cf2ed82e886d0a83975d00e9a0ff849cb9b44d8c030c293c9a02a47df169f4684a84b03c843ac2955bd7566909bb93ef5ae14b3be4fb481358b0e0894a8b4ff6599e4fac7bfb48841aa660dd1b70193e492221ec5d4a28f4dad0818ed1d72ef08da12eb7583f29276586a3e69f10bbe13ba4afca584e86ce2ce599cc7df8b98318a8d448fb3c75ad94671f9d80bc7a1660dc716422176fb19fa1e959d204116d538d2b0ca478b2c9f0ade86cf515d54e95676b0b17a508c13f97deb70bdbf4bc45f4ead427951d7455f892ea20d6a9bac83446a2217607747287b2a990bc8a7a823884d94fbcca38aceb666ca2afc47e8b9d8a3eeea093c92876b83d4e41e032d30365eea2cd717dc70f79cc4c9b7695e81769d2a0c30ca02b98f834bfb968948f150670d1cec91500c27301fc0d01faeacaa96fe0800b177bae2b57eee7909000934b597eb8517ddb11136f5235ce8d7204c22a6784fe8c187121457715b7383ee699a42df1d58a413725c86730d704538af546e7d1a70a15a968f5e7aa9afedcaf8a8e4909d7af6840d9018f8c3ba42f697eca710d31344b0ec20c8e9020e271e67c3822021afdbbbe2b77ef8e2e5d1b6a47865bf5cf925758df8e3a68e0be4cd0ece8c1bf26bf41686a5632bf9f9c720999b62d7d0a6d69bc3afe3d3f202a51ab5e077f76f4f77b9533de8151fbc431e453ad6c1bd6dd65a5c034d9cedcbaef57f4bbeff247ce4d0d5bfba92442bbd34e823edc351080732df64abdac453d982d8cb305dd583b7ffb7296d9d6076c411c3a36f76f5965a78d01795000fa1311cf08a662c74a9aa5369fadd6c8cf29d3de62303b51473b403ffe12a7164b6dc756b3d57377d0528a793b4c478c15a1cb68fc13318182095cc1a756db47ff3e141e59d783a2a87a0c3822f6644cb96813645f62ea76878be6d2ae36ef14f80e" + } + ] +} \ No newline at end of file