From 39c27d0b552afaab5be108c1c0d564ed6f94d1df Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Tue, 17 Sep 2024 16:04:19 -0700 Subject: [PATCH] Encrypts and sends but isn't decryptable --- circuitmatter/__init__.py | 142 +++++++++++++++++++++++------ circuitmatter/data_model.py | 112 +++++++++++++++++------ circuitmatter/interaction_model.py | 18 +--- circuitmatter/tlv.py | 19 ++-- test_data/recorded_packets.jsonl | 25 +++-- 5 files changed, 225 insertions(+), 91 deletions(-) diff --git a/circuitmatter/__init__.py b/circuitmatter/__init__.py index bfa4a53..b4225ae 100644 --- a/circuitmatter/__init__.py +++ b/circuitmatter/__init__.py @@ -305,7 +305,7 @@ def send(self, message): class SecureSessionContext: - def __init__(self, local_session_id): + def __init__(self, socket, local_session_id): self.session_type = None """Records whether the session was established using CASE or PASE.""" self.session_role_initiator = False @@ -320,13 +320,13 @@ def __init__(self, local_session_id): """Encrypts data in messages sent from the session establishment responder to the initiator.""" self.shared_secret = None """Computed during the CASE protocol execution and re-used when CASE session resumption is implemented.""" - self.local_message_counter = None + self.local_message_counter = MessageCounter() """Secure Session Message Counter for outbound messages.""" self.message_reception_state = None """Provides tracking for the Secure Session Message Counter of the remote""" self.local_fabric_index = None """Records the local Index for the session’s Fabric, which MAY be used to look up Fabric metadata related to the Fabric for which this session context applies.""" - self.peer_node_id = None + self.peer_node_id = 0 """Records the authenticated node ID of the remote peer, when available.""" self.resumption_id = None """The ID used when resuming a session between the local and remote peer.""" @@ -340,6 +340,8 @@ def __init__(self, local_session_id): self.exchanges = {} self._nonce = bytearray(session.CRYPTO_AEAD_NONCE_LENGTH_BYTES) + self.socket = socket + self.node_ipaddress = None @property def peer_active(self): @@ -370,6 +372,22 @@ def decrypt_and_verify(self, message): message.payload = decrypted_payload return True + def send(self, message): + message.session_id = self.peer_session_id + cipher = self.r2i + if self.session_role_initiator: + cipher = self.i2r + + self.session_timestamp = time.monotonic() + + message.destination_node_id = self.peer_node_id + if message.message_counter is None: + message.message_counter = next(self.local_message_counter) + + buf = memoryview(bytearray(1280)) + nbytes = message.encode_into(buf, cipher) + self.socket.sendto(buf[:nbytes], self.node_ipaddress) + class Message: def __init__(self): @@ -380,8 +398,8 @@ def clear(self): self.session_id: int = 0 self.security_flags: SecurityFlags = SecurityFlags(0) self.message_counter: Optional[int] = None - self.source_node_id = None - self.destination_node_id = None + self.source_node_id = 0 + self.destination_node_id = 0 self.secure_session: Optional[bool] = None self.payload = None self.duplicate: Optional[bool] = None @@ -440,7 +458,7 @@ def decode(self, buffer): self.source_node_id = struct.unpack_from("> 4) != 0: raise RuntimeError("Incorrect version") @@ -453,7 +471,7 @@ def decode(self, buffer): self.payload = memoryview(buffer)[offset:] self.duplicate = None - def encode_into(self, buffer): + def encode_into(self, buffer, cipher=None): offset = 0 struct.pack_into( " 0: struct.pack_into(" 0: if self.destination_node_id > 0xFFFF_FFFF_FFFF_0000: struct.pack_into( " 0: self.flags |= 1 << 2 else: self.flags &= ~(1 << 2) @@ -528,7 +587,7 @@ def destination_node_id(self, value): self._destination_node_id = value # Clear the field self.flags &= ~0x3 - if value is None: + if value == 0: pass elif value > 0xFFFF_FFFF_FFFF_0000: self.flags |= 2 @@ -708,6 +767,7 @@ def get_session(self, message): # TODO: Get MRS for source node id and message type else: session_context = self.secure_session_contexts[message.session_id] + session_context.node_ipaddress = message.source_ipaddress else: if message.source_node_id not in self.unsecured_session_context: self.unsecured_session_context[message.source_node_id] = ( @@ -774,7 +834,9 @@ def new_context(self): self.secure_session_contexts.append(None) session_id = self.secure_session_contexts.index(None) - self.secure_session_contexts[session_id] = SecureSessionContext(session_id) + self.secure_session_contexts[session_id] = SecureSessionContext( + self.socket, session_id + ) return self.secure_session_contexts[session_id] def process_exchange(self, message): @@ -814,7 +876,15 @@ def process_exchange(self, message): class CircuitMatter: - def __init__(self, socketpool, mdns_server, random_source, state_filename): + def __init__( + self, + socketpool, + mdns_server, + random_source, + state_filename, + vendor_id=0xFFF1, + product_id=0, + ): self.socketpool = socketpool self.mdns_server = mdns_server self.random = random_source @@ -851,9 +921,21 @@ def __init__(self, socketpool, mdns_server, random_source, state_filename): self.start_commissioning() self._endpoints = {} - self.add_cluster(0, data_model.BasicInformationCluster()) - self.add_cluster(0, data_model.NetworkCommissioningCluster()) - self.add_cluster(0, data_model.GeneralCommissioningCluster()) + basic_info = data_model.BasicInformationCluster() + basic_info.vendor_id = vendor_id + basic_info.product_id = product_id + self.add_cluster(0, basic_info) + network_info = data_model.NetworkCommissioningCluster() + network_info.connect_max_time_seconds = 10 + self.add_cluster(0, network_info) + general_commissioning = data_model.GeneralCommissioningCluster() + basic_commissioning_info = ( + data_model.GeneralCommissioningCluster.BasicCommissioningInfo() + ) + basic_commissioning_info.FailSafeExpiryLengthSeconds = 10 + basic_commissioning_info.MaxCumulativeFailsafeSeconds = 900 + general_commissioning.basic_commissioning_info = basic_commissioning_info + self.add_cluster(0, general_commissioning) def start_commissioning(self): descriminator = self.nonvolatile["descriminator"] @@ -903,6 +985,8 @@ def get_report(self, cluster, path): astatus = interaction_model.AttributeStatusIB() astatus.Path = path status = interaction_model.StatusIB() + status.Status = 0 + status.ClusterStatus = 0 astatus.Status = status report.AttributeStatus = astatus report.AttributeData = cluster.get_attribute_data(path) @@ -1093,7 +1177,11 @@ def process_packet(self, address, data): print(f"Cluster 0x{path.Cluster:02x} not found") response = interaction_model.ReportDataMessage() response.AttributeReports = attribute_reports - print(response) + exchange.send( + ProtocolId.INTERACTION_MODEL, + InteractionModelOpcode.REPORT_DATA, + response, + ) if protocol_opcode == InteractionModelOpcode.INVOKE_REQUEST: print("Received Invoke Request") elif protocol_opcode == InteractionModelOpcode.INVOKE_RESPONSE: diff --git a/circuitmatter/data_model.py b/circuitmatter/data_model.py index 282fc45..a7e3d9a 100644 --- a/circuitmatter/data_model.py +++ b/circuitmatter/data_model.py @@ -1,11 +1,20 @@ import enum import random +import struct from typing import Iterable from . import interaction_model from . import tlv +class Enum8(enum.IntEnum): + pass + + +class Enum16(enum.IntEnum): + pass + + class Attribute: def __init__(self, _id, default=None): self.id = _id @@ -18,7 +27,7 @@ def __get__(self, instance, cls): return v def __set__(self, instance, value): - old_value = instance._attribute_values[self.id] + old_value = instance._attribute_values.get(self.id, None) if old_value == value: return instance._attribute_values[self.id] = value @@ -28,16 +37,52 @@ def encode(self, value): raise NotImplementedError() -class FeatureMap(Attribute): - def __init__(self): - super().__init__(0xFFFC) +class NumberAttribute(Attribute): + def __init__(self, _id, *, signed, bits, default=None): + self.signed = signed + self.bits = bits + self.id = _id + self.default = default + super().__init__(_id, default=default) + + @staticmethod + def encode_number(value, *, signed=True): + bit_length = value.bit_length() + format_string = None + if signed: + type = tlv.ElementType.SIGNED_INT + else: + type = tlv.ElementType.UNSIGNED_INT + length = 0 # in power of two + if bit_length <= 8: + format_string = "Bb" if signed else "BB" + length = 0 + elif bit_length <= 16: + format_string = "Bh" if signed else "BH" + length = 1 + elif bit_length <= 32: + format_string = "Bi" if signed else "BI" + length = 2 + else: + format_string = "Bq" if signed else "BQ" + length = 3 + + return struct.pack(format_string, type | length, value) def encode(self, value): - raise NotImplementedError() + return NumberAttribute.encode_number(value, signed=self.signed) -class NumberAttribute(Attribute): - pass +class FeatureMap(NumberAttribute): + def __init__(self): + super().__init__(0xFFFC, signed=False, bits=32, default=0) + + +class EnumAttribute(NumberAttribute): + def __init__(self, _id, enum_type, default=None): + self.enum_type = enum_type + bits = 8 if issubclass(enum_type, Enum8) else 16 + super().__init__(_id, signed=False, bits=bits, default=default) class ListAttribute(Attribute): @@ -53,11 +98,12 @@ def __init__(self, _id, struct_type): self.struct_type = struct_type super().__init__(_id) - -class EnumAttribute(Attribute): - def __init__(self, _id, enum_type): - self.enum_type = enum_type - super().__init__(_id) + def encode(self, value) -> memoryview: + buffer = memoryview(bytearray(value.max_length() + 2)) + buffer[0] = tlv.ElementType.STRUCTURE + end = value.encode_into(buffer, 1) + buffer[end] = tlv.ElementType.END_OF_CONTAINER + return buffer[: end + 1] class OctetStringAttribute(Attribute): @@ -98,13 +144,13 @@ def _attributes(cls) -> Iterable[tuple[str, Attribute]]: def get_attribute_data(self, path) -> interaction_model.AttributeDataIB: print("get_attribute_data", path.Attribute) data = interaction_model.AttributeDataIB() + data.DataVersion = 0 data.Path = path found = False for field_name, descriptor in self._attributes(): if descriptor.id != path.Attribute: continue print("read", field_name, descriptor) - data._data_type = descriptor.data_type data.Data = descriptor.encode(getattr(self, field_name)) found = True break @@ -161,16 +207,16 @@ class ProductAppearance(tlv.TLVStructure): Finish = tlv.EnumMember(0, ProductFinish) PrimaryColor = tlv.EnumMember(1, Color) - data_model_revision = NumberAttribute(0x00) + data_model_revision = NumberAttribute(0x00, signed=False, bits=16) vendor_name = UTF8StringAttribute(0x01, max_length=32) - vendor_id = NumberAttribute(0x02) + vendor_id = NumberAttribute(0x02, signed=False, bits=16) product_name = UTF8StringAttribute(0x03, max_length=32) - product_id = NumberAttribute(0x04) + product_id = NumberAttribute(0x04, signed=False, bits=16) node_label = UTF8StringAttribute(0x05, max_length=32, default="") location = UTF8StringAttribute(0x06, max_length=2, default="XX") - hardware_version = NumberAttribute(0x07) + hardware_version = NumberAttribute(0x07, signed=False, bits=16) hardware_version_string = UTF8StringAttribute(0x08, min_length=1, max_length=64) - software_version = NumberAttribute(0x09) + software_version = NumberAttribute(0x09, signed=False, bits=32) software_version_string = UTF8StringAttribute(0x0A, min_length=1, max_length=64) manufacturing_date = UTF8StringAttribute(0x0B, min_length=8, max_length=16) part_number = UTF8StringAttribute(0x0C, max_length=32) @@ -182,8 +228,8 @@ class ProductAppearance(tlv.TLVStructure): unique_id = UTF8StringAttribute(0x12, max_length=32) capability_minima = StructAttribute(0x13, CapabilityMinima) product_appearance = StructAttribute(0x14, ProductAppearance) - specification_version = NumberAttribute(0x15, default=0) - max_paths_per_invoke = NumberAttribute(0x16, default=1) + specification_version = NumberAttribute(0x15, signed=False, bits=32, default=0) + max_paths_per_invoke = NumberAttribute(0x16, signed=False, bits=16, default=1) class GeneralCommissioningCluster(Cluster): @@ -198,11 +244,17 @@ class RegulatoryLocationType(enum.IntEnum): OUTDOOR = 1 INDOOR_OUTDOOR = 2 - breadcrumb = NumberAttribute(0) + bits = 8 + + breadcrumb = NumberAttribute(0, signed=False, bits=64, default=0) basic_commissioning_info = StructAttribute(1, BasicCommissioningInfo) - regulatory_config = EnumAttribute(2, RegulatoryLocationType) - location_capability = EnumAttribute(3, RegulatoryLocationType) - support_concurrent_connection = BoolAttribute(4) + regulatory_config = EnumAttribute( + 2, RegulatoryLocationType, default=RegulatoryLocationType.INDOOR_OUTDOOR + ) + location_capability = EnumAttribute( + 3, RegulatoryLocationType, default=RegulatoryLocationType.INDOOR_OUTDOOR + ) + support_concurrent_connection = BoolAttribute(4, default=True) class NetworkCommissioningCluster(Cluster): @@ -213,7 +265,7 @@ class FeatureBitmap(enum.IntFlag): THREAD_NETWORK_INTERFACE = 0b010 ETHERNET_NETWORK_INTERFACE = 0b100 - class NetworkCommissioningStatus(enum.IntEnum): + class NetworkCommissioningStatus(Enum8): SUCCESS = 0 """Ok, no error""" @@ -253,14 +305,14 @@ class NetworkCommissioningStatus(enum.IntEnum): UNKNOWN_ERROR = 12 """Unknown error""" - max_networks = NumberAttribute(0) + max_networks = NumberAttribute(0, signed=False, bits=8) networks = ListAttribute(1) - scan_max_time_seconds = NumberAttribute(2) - connect_max_time_seconds = NumberAttribute(3) + scan_max_time_seconds = NumberAttribute(2, signed=False, bits=8) + connect_max_time_seconds = NumberAttribute(3, signed=False, bits=8) interface_enabled = BoolAttribute(4) last_network_status = EnumAttribute(5, NetworkCommissioningStatus) last_network_id = OctetStringAttribute(6, min_length=1, max_length=32) - last_connect_error_value = NumberAttribute(7) + last_connect_error_value = NumberAttribute(7, signed=True, bits=32) supported_wifi_bands = ListAttribute(8) supported_thread_features = BitmapAttribute(9) - thread_version = NumberAttribute(10) + thread_version = NumberAttribute(10, signed=False, bits=16) diff --git a/circuitmatter/interaction_model.py b/circuitmatter/interaction_model.py index a758252..8ec50b4 100644 --- a/circuitmatter/interaction_model.py +++ b/circuitmatter/interaction_model.py @@ -49,13 +49,7 @@ class StatusIB(tlv.TLVStructure): class AttributeDataIB(tlv.TLVStructure): DataVersion = tlv.IntMember(0, signed=False, octets=4) Path = tlv.StructMember(1, AttributePathIB) - Data = tlv.AnythingMember( - 2, "_data_type" - ) # This is a weird one because the TLV type can be anything. - - def __init__(self): - self._data_type = None - super().__init__() + Data = tlv.AnythingMember(2) class AttributeStatusIB(tlv.TLVStructure): @@ -92,9 +86,7 @@ class EventDataIB(tlv.TLVStructure): DeltaEpochTimestamp = tlv.IntMember(5, signed=True, octets=8, optional=True) DeltaSystemTimestamp = tlv.IntMember(6, signed=True, octets=8, optional=True) - Data = tlv.AnythingMember( - 7, "_data_type" - ) # This is a weird one because the TLV type can be anything. + Data = tlv.AnythingMember(7) class EventReportIB(tlv.TLVStructure): @@ -103,8 +95,8 @@ class EventReportIB(tlv.TLVStructure): class ReportDataMessage(tlv.TLVStructure): - SubscriptionId = tlv.IntMember(0, signed=False, octets=4) - AttributeReports = tlv.ArrayMember(1, AttributeReportIB) - EventReports = tlv.ArrayMember(2, EventReportIB) + SubscriptionId = tlv.IntMember(0, signed=False, octets=4, optional=True) + AttributeReports = tlv.ArrayMember(1, AttributeReportIB, optional=True) + EventReports = tlv.ArrayMember(2, EventReportIB, optional=True) MoreChunkedMessages = tlv.BoolMember(3, optional=True) SuppressResponse = tlv.BoolMember(4, optional=True) diff --git a/circuitmatter/tlv.py b/circuitmatter/tlv.py index 1cd4893..d934431 100644 --- a/circuitmatter/tlv.py +++ b/circuitmatter/tlv.py @@ -219,6 +219,10 @@ def __init__( self.tag_length = 8 self._max_length = None self._default = None + self._name = None + + def __set_name__(self, owner, name): + self._name = name @property def max_length(self): @@ -282,7 +286,7 @@ def encode_into(self, obj: TLVStructure, buffer: bytearray, offset: int) -> int: # Value is None and the field is optional so skip it. return offset elif not self.nullable: - raise ValueError("Required field isn't set") + raise ValueError(f"{self._name} isn't set") tag_control = 0 if self.tag is not None: @@ -712,23 +716,16 @@ def __iter__(self): class AnythingMember(Member): - def __init__(self, tag, type_attribute_name): - self.type_attribute_name = type_attribute_name - self._element_type = None - super().__init__(tag, optional=False, nullable=True) + """Stores a TLV encoded value.""" def decode(self, buffer, length, offset=0): return None def _print(self, value): - return "???" - - def encode_into(self, obj: TLVStructure, buffer: bytearray, offset: int) -> int: - self._element_type = getattr(obj, self.type_attribute_name) - return super().encode_into(obj, buffer, offset) + return value.hex() def encode_element_type(self, value): - return self._element_type + return value[0] def encode_value_into(self, value, buffer: bytearray, offset: int) -> int: return offset diff --git a/test_data/recorded_packets.jsonl b/test_data/recorded_packets.jsonl index 64968da..ec5649e 100644 --- a/test_data/recorded_packets.jsonl +++ b/test_data/recorded_packets.jsonl @@ -1,10 +1,15 @@ -["urandom", 177305057188313, 8, "c6s/zmYDhPY="] -["receive", 177321168775239, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "BAAAAK9Ksg3O22AGIK7zjAUgb3gAABUwASCu6dFJ6yUncsf/vvRQcSAc7bU6iNcRaVG5epjckXnO4yUChWQkAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] -["urandom", 177321169021294, 32, "ygKFvF2pxqkE6rFrzTYk63cskRCnNvxkuEjbfL9TysQ="] -["send", 177321169150587, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "AQAAAF3jEgLO22AGIK7zjAIhb3gAAK9Ksg0VMAEgrunRSeslJ3LH/770UHEgHO21OojXEWlRuXqY3JF5zuMwAiDKAoW8XanGqQTqsWvNNiTrdyyREKc2/GS4SNt8v1PKxCUDAQA1BCYBECcAADACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] -["receive", 177321175047659, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "BAAAALBKsg3O22AGIK7zjAUib3gAABUwAUEEbaAYCys1TTrm1I4QudXvSdO8WvLzn76NUZw5Qj1s2THuX+/irxEDl7pUWTO2rLxS8GiuVkbXK6VgwbTD3UyQgxg="] -["randbelow", 177321175154802, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 69262736637335533582530128543114847108658688902246054750026796985292607836977] -["send", 177321191569502, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "AQAAAF7jEgLO22AGIK7zjAIjb3gAALBKsg0VMAFBBAigbrvmzaW2mggSrbl0bCWyIt8V+/snwC7e/zeqevdpbUbGp4jgni4i0gnpqGO9EscxnXDIsZh0fK0MFp15boUwAiAon7kz8k0TlGj8ta47ub/H9C0NbLGd4k2rs0qVvXJrnxg="] -["receive", 177321192011375, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "BAAAALFKsg3O22AGIK7zjAUkb3gAABUwASC7hTHoZNLFkqjU4yhiI04D5lz3qq8FDNoxOG0FhpDMEBg="] -["send", 177321192098540, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "AQAAAF/jEgLO22AGIK7zjAJAb3gAALFKsg0AAAAAAAAAAA=="] -["receive", 177321192252170, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 52596, 0, 0], "AAEAAJM+oQr8tN5pBnM2Ox1R6R5yCtrcBvtmQxfQY4mHkWTY6NSgIkrrn8PC7RVEJ6TH8VplZsK5gurZO4Ni5uBLFtQ9WKdmltAqBexKPtZlTS/Taluv4ZJIvMr+QfFJ4I9TMpw2ufgWXwePCcgeCQ2vr2GPG0WNC5iaUbjJmRvOOX0="] +["urandom", 93605477486462, 8, "gF88cM8Fdws="] +["receive", 93609109439051, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "BAAAAG1aTQypZZhQ61R6pwUg7l8AABUwASCAi8eYa+yxOHt1wczNj2sib00cVne73qdB4ACn2EL1tSUCfq0kAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] +["urandom", 93609138892087, 32, "aI0a5kAMERbOZkdcsYK1fyjy+SUis5vCMr/z5mVbFUI="] +["send", 93609139058692, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AQAAAAwHlwCpZZhQ61R6pwIh7l8AAG1aTQwVMAEggIvHmGvssTh7dcHMzY9rIm9NHFZ3u96nQeAAp9hC9bUwAiBojRrmQAwRFs5mR1yxgrV/KPL5JSKzm8Iyv/PmZVsVQiUDAQA1BCYBECcAADACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] +["receive", 93609145909196, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "BAAAAG5aTQypZZhQ61R6pwUi7l8AABUwAUEECQ1E55Ge/QdwIt9ApvlujnzwNkxWOAQo33o1VjxvjRlgF/gAgGR/qT/nHVjT/Y08DUw2BPybYQ8yiALpkh7cRRg="] +["randbelow", 93609146092151, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 113375977605381650694547482532764789231695354705278667510956348485473112713874] +["send", 93609162243032, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AQAAAA0HlwCpZZhQ61R6pwIj7l8AAG5aTQwVMAFBBJ89SvxsKCYUC4N3JxEmLyV7/2O3E+qYJkuajUaO48PKO42ty4VNcuo9hUtFF7RAAQdGyhHAucKUZNNRHZeRVVIwAiDU1KDBDAD9SI3uEZ6/nqB4EFZhIi7SeVFRvRvruGT7IRg="] +["receive", 93609162813558, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "BAAAAG9aTQypZZhQ61R6pwUk7l8AABUwASBxL8z46GInTI1DJ2HFO9zzGw9FKdZ7aYzPSaNcvwn2nRg="] +["send", 93609162944084, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AQAAAA4HlwCpZZhQ61R6pwJA7l8AAG9aTQwAAAAAAAAAAA=="] +["receive", 93609163134143, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AAEAABIkJQLjhJ21cI7QD9KovB/pAZsmra0uYJpejzRJeboWAbrUor9cDs0AQAkbvSzuEJOhzo47NVtkKqM1fIjL1P0FYWhFEryNZLrgxFuaz9b0V/JrTXAmcdnDslb+r36XCPPIPEJ17Rk0IgA1Hyfi8+z+hZVKjaf9wnJ1GbkNZTs="] +["send", 93609164255729, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AH6tALRcoQ0CBe9fAQASJCUCPC2ZWwg2ID2SHwL84Kb4/ZgcGR/3fJzb83+CE9H38SDdzsAvub8z61zZS4NpJvhMCUczsN1NolqQbAUV7PbQ/CrbIxgU21vOpsh0K6Q+4N/aQKrZG4rOOg2Iu4WH4rPSVdI58ry/f4OiC5eq8+yjeQWWBrgu0N3hwxBcwHpeBVmPXfkX9lwMsZaYoU+UiriKFMWyubmjaDqctl5z3i0yNRuJVFElqUSECY3VvxHaUKFdd8T460u/QTbHZOEPQpA/Mqnnpwe/bT5p0y3P2bCMQrzrl6w+ByABL2RNC52G7r5GTGN0x8rpSvDrZOYa4wfcA1FNFxWqqFvJFXCJ5kjpTKwh2QOAW3zVQugzl9+8RBIB4pS/XLpgaDWoMIEvKw8nlksWbfwGNVFMWnxd5GPOAv3FwQDQt4sRasgZAuC/wRqYKkWeCSsdS/NRuag+ruYX3jF/bnJp2oVisd3asu1VHjyqYCxtjfBs0WtX0cBls2r79XmrtO9qPfltYChvwYKLF6XM21wYBf33rWV/332qoWc35O1MjKz2CtsBBtEv0L0LH2XdgfE18tAm8DhWyhWvxeJ1NkhZM1O7aPBkHZzcnlDHooQMl32wBu7QIAdsuvOg62bGDN/kBLI/vqMu"] +["receive", 93609540233976, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AAEAABIkJQLjhJ21cI7QD9KovB/pAZsmra0uYJpejzRJeboWAbrUor9cDs0AQAkbvSzuEJOhzo47NVtkKqM1fIjL1P0FYWhFEryNZLrgxFuaz9b0V/JrTXAmcdnDslb+r36XCPPIPEJ17Rk0IgA1Hyfi8+z+hZVKjaf9wnJ1GbkNZTs="] +["receive", 93609920720629, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AAEAABIkJQLjhJ21cI7QD9KovB/pAZsmra0uYJpejzRJeboWAbrUor9cDs0AQAkbvSzuEJOhzo47NVtkKqM1fIjL1P0FYWhFEryNZLrgxFuaz9b0V/JrTXAmcdnDslb+r36XCPPIPEJ17Rk0IgA1Hyfi8+z+hZVKjaf9wnJ1GbkNZTs="] +["receive", 93610459358202, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AAEAABIkJQLjhJ21cI7QD9KovB/pAZsmra0uYJpejzRJeboWAbrUor9cDs0AQAkbvSzuEJOhzo47NVtkKqM1fIjL1P0FYWhFEryNZLrgxFuaz9b0V/JrTXAmcdnDslb+r36XCPPIPEJ17Rk0IgA1Hyfi8+z+hZVKjaf9wnJ1GbkNZTs="] +["receive", 93611345336845, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 45610, 0, 0], "AAEAABIkJQLjhJ21cI7QD9KovB/pAZsmra0uYJpejzRJeboWAbrUor9cDs0AQAkbvSzuEJOhzo47NVtkKqM1fIjL1P0FYWhFEryNZLrgxFuaz9b0V/JrTXAmcdnDslb+r36XCPPIPEJ17Rk0IgA1Hyfi8+z+hZVKjaf9wnJ1GbkNZTs="]