Restructure + destinationId

This commit is contained in:
Scott Shawcroft 2024-09-27 16:01:11 -07:00
parent c406629605
commit 15c6b518cb
No known key found for this signature in database
11 changed files with 1477 additions and 1253 deletions

File diff suppressed because it is too large Load diff

109
circuitmatter/case.py Normal file
View file

@ -0,0 +1,109 @@
from . import crypto
from . import session
from . import tlv
class Sigma1(tlv.Structure):
initiatorRandom = tlv.OctetStringMember(1, 32)
initiatorSessionId = tlv.IntMember(2, signed=False, octets=2)
destinationId = tlv.OctetStringMember(3, crypto.HASH_LEN_BYTES)
initiatorEphPubKey = tlv.OctetStringMember(4, crypto.PUBLIC_KEY_SIZE_BYTES)
initiatorSessionParams = tlv.StructMember(
5, session.SessionParameterStruct, optional=True
)
resumptionID = tlv.OctetStringMember(6, 16, optional=True)
initiatorResumeMIC = tlv.OctetStringMember(
7, crypto.AEAD_MIC_LENGTH_BYTES, optional=True
)
class Sigma2TbsData(tlv.Structure):
responderNOC = tlv.OctetStringMember(1, 32)
responderICAC = tlv.OctetStringMember(2, crypto.CERTIFICATE_SIZE, optional=True)
responderEphPubKey = tlv.OctetStringMember(3, crypto.PUBLIC_KEY_SIZE_BYTES)
initiatorEphPubKey = tlv.OctetStringMember(4, crypto.PUBLIC_KEY_SIZE_BYTES)
class Sigma2TbeData(tlv.Structure):
responderNOC = tlv.OctetStringMember(1, 32)
responderICAC = tlv.OctetStringMember(2, crypto.CERTIFICATE_SIZE, optional=True)
signature = tlv.OctetStringMember(3, 64)
resumptionID = tlv.OctetStringMember(4, 16)
class Sigma2(tlv.Structure):
responderRandom = tlv.OctetStringMember(1, 32)
responderSessionId = tlv.IntMember(2, signed=False, octets=2)
responderEphPubKey = tlv.OctetStringMember(3, crypto.PUBLIC_KEY_SIZE_BYTES)
encrypted2 = tlv.OctetStringMember(4, Sigma2TbeData.max_length())
responderSessionParams = tlv.StructMember(
5, session.SessionParameterStruct, optional=True
)
class Sigma3TbsData(tlv.Structure):
initiatorNOC = tlv.OctetStringMember(1, 32)
initiatorICAC = tlv.OctetStringMember(2, crypto.CERTIFICATE_SIZE, optional=True)
initiatorEphPubKey = tlv.OctetStringMember(3, crypto.PUBLIC_KEY_SIZE_BYTES)
responderEphPubKey = tlv.OctetStringMember(4, crypto.PUBLIC_KEY_SIZE_BYTES)
class Sigma3TbeData(tlv.Structure):
initiatorNOC = tlv.OctetStringMember(1, 32)
initiatorICAC = tlv.OctetStringMember(2, crypto.CERTIFICATE_SIZE, optional=True)
signature = tlv.OctetStringMember(3, 64)
class Sigma3(tlv.Structure):
encrypted3 = tlv.OctetStringMember(1, Sigma3TbeData.max_length())
class Sigma2Resume(tlv.Structure):
resumptionID = tlv.OctetStringMember(1, 16)
sigma2ResumeMIC = tlv.OctetStringMember(2, 16)
responderSessionID = tlv.IntMember(3, signed=False, octets=2)
responderSessionParams = tlv.StructMember(
4, session.SessionParameterStruct, optional=True
)
def compute_destination_id(
root_public_key, fabric_id, node_id, initiator_random, identity_protection_key
):
print("root_public_key", len(root_public_key), "/", root_public_key.hex(":"))
print("fabric_id", len(fabric_id), "/", fabric_id.hex(":"))
print("node_id", len(node_id), "/", node_id.hex(":"))
print("initiator_random", len(initiator_random), "/", initiator_random.hex(":"))
print(
"identity_protection_key",
len(identity_protection_key),
"/",
identity_protection_key.hex(":"),
)
destination_message = b"".join(
(initiator_random, root_public_key, fabric_id, node_id)
)
return crypto.HMAC(identity_protection_key, destination_message)
if __name__ == "__main__":
root_public_key = bytes.fromhex(
"04:4a:9f:42:b1:ca:48:40:d3:72:92:bb:c7:f6:a7:e1:1e:22:20:0c:97:6f:c9:00:db:c9:8a:7a:38:3a:64:1c:b8:25:4a:2e:56:d4:e2:95:a8:47:94:3b:4e:38:97:c4:a7:73:e9:30:27:7b:4d:9f:be:de:8a:05:26:86:bf:ac:fa".replace(
":", ""
)
)
fabric_id = bytes.fromhex("62:d3:15:d1:08:c9:06:29".replace(":", ""))
node_id = bytes.fromhex("14:ef:13:7b:aa:44:55:cd".replace(":", ""))
initiator_random = bytes.fromhex(
"7e:17:12:31:56:8d:fa:17:20:6b:3a:cc:f8:fa:ec:2f:4d:21:b5:80:11:31:96:f4:7c:7c:4d:eb:81:0a:73:dc".replace(
":", ""
)
)
identity_protection_key = bytes.fromhex(
" 9b:c6:1c:d9:c6:2a:2d:f6:d6:4d:fc:aa:9d:c4:72:d4".replace(":", "")
)
destination_id = compute_destination_id(
root_public_key, fabric_id, node_id, initiator_random, identity_protection_key
)
print(destination_id.hex(":"))

View file

View file

@ -0,0 +1,330 @@
from .. import crypto
from .. import data_model
from .. import interaction_model
from .. import tlv
import ecdsa
from ecdsa import der
import hashlib
import pathlib
import struct
import time
TEST_CERTS = pathlib.Path(
"/home/tannewt/repos/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/"
)
TEST_PAI_CERT_DER = TEST_CERTS / "Chip-Test-PAI-FFF1-8000-Cert.der"
TEST_PAI_CERT_PEM = TEST_CERTS / "Chip-Test-PAI-FFF1-8000-Cert.pem"
TEST_DAC_CERT_DER = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Cert.der"
TEST_DAC_CERT_PEM = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Cert.pem"
TEST_DAC_KEY_DER = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Key.der"
TEST_DAC_KEY_PEM = TEST_CERTS / "Chip-Test-DAC-FFF1-8000-0000-Key.pem"
TEST_CD_CERT_DER = pathlib.Path("certification_declaration.der")
class GeneralCommissioningCluster(data_model.GeneralCommissioningCluster):
def __init__(self):
super().__init__()
basic_commissioning_info = (
data_model.GeneralCommissioningCluster.BasicCommissioningInfo()
)
basic_commissioning_info.FailSafeExpiryLengthSeconds = 10
basic_commissioning_info.MaxCumulativeFailsafeSeconds = 900
self.basic_commissioning_info = basic_commissioning_info
def arm_fail_safe(
self, session, args: data_model.GeneralCommissioningCluster.ArmFailSafe
) -> data_model.GeneralCommissioningCluster.ArmFailSafeResponse:
response = data_model.GeneralCommissioningCluster.ArmFailSafeResponse()
response.ErrorCode = data_model.CommissioningErrorEnum.OK
return response
def set_regulatory_config(
self, session, args: data_model.GeneralCommissioningCluster.SetRegulatoryConfig
) -> data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse:
response = data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse()
response.ErrorCode = data_model.CommissioningErrorEnum.OK
return response
class AttestationElements(tlv.Structure):
certification_declaration = tlv.OctetStringMember(0x01, max_length=400)
attestation_nonce = tlv.OctetStringMember(0x02, max_length=32)
timestamp = tlv.IntMember(0x03, signed=False, octets=4)
firmware_information = tlv.OctetStringMember(0x04, max_length=16, optional=True)
"""Used for secure boot. We don't support it."""
class NOCSRElements(tlv.Structure):
csr = tlv.OctetStringMember(0x01, max_length=1024)
CSRNonce = tlv.OctetStringMember(0x02, max_length=32)
# Skip vendor reserved
def encode_set(*encoded_pieces):
total_len = sum([len(p) for p in encoded_pieces])
return b"\x31" + der.encode_length(total_len) + b"".join(encoded_pieces)
def encode_utf8_string(s):
encoded = s.encode("utf-8")
return b"\x0c" + der.encode_length(len(encoded)) + encoded
class NodeOperationalCredentialsCluster(data_model.NodeOperationalCredentialsCluster):
def __init__(self, group_key_manager, mdns_server, port):
super().__init__()
self.group_key_manager = group_key_manager
self.dac_key = ecdsa.keys.SigningKey.from_der(
TEST_DAC_KEY_DER.read_bytes(), hashfunc=hashlib.sha256
)
self.new_key_for_update = False
self.pending_root_cert = None
self.pending_signing_key = None
self.nocs = []
self.fabrics = []
self.commissioned_fabrics = 0
self.supported_fabrics = 10
self.root_certs = []
self.compressed_fabric_ids = []
self.mdns_server = mdns_server
self.port = port
def certificate_chain_request(
self,
session,
args: data_model.NodeOperationalCredentialsCluster.CertificateChainRequest,
) -> data_model.NodeOperationalCredentialsCluster.CertificateChainResponse:
response = (
data_model.NodeOperationalCredentialsCluster.CertificateChainResponse()
)
if args.CertificateType == data_model.CertificateChainTypeEnum.PAI:
print("PAI")
response.Certificate = TEST_PAI_CERT_DER.read_bytes()
elif args.CertificateType == data_model.CertificateChainTypeEnum.DAC:
print("DAC")
response.Certificate = TEST_DAC_CERT_DER.read_bytes()
return response
def attestation_request(
self,
session,
args: data_model.NodeOperationalCredentialsCluster.AttestationRequest,
) -> data_model.NodeOperationalCredentialsCluster.AttestationResponse:
print("attestation")
elements = AttestationElements()
elements.certification_declaration = TEST_CD_CERT_DER.read_bytes()
elements.attestation_nonce = args.AttestationNonce
elements.timestamp = int(time.time())
elements = elements.encode()
print("elements", len(elements), elements[:3].hex(" "))
print(
"challeng",
len(session.attestation_challenge),
session.attestation_challenge[:3].hex(" "),
)
attestation_tbs = elements.tobytes() + session.attestation_challenge
response = data_model.NodeOperationalCredentialsCluster.AttestationResponse()
response.AttestationElements = elements
response.AttestationSignature = self.dac_key.sign_deterministic(
attestation_tbs,
hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_string,
)
return response
def csr_request(
self, session, args: data_model.NodeOperationalCredentialsCluster.CSRRequest
) -> data_model.NodeOperationalCredentialsCluster.CSRResponse:
# Section 6.4.6.1
# CSR stands for Certificate Signing Request. A NOCSR is a Node Operational Certificate Signing Request
self.new_key_for_update = args.IsForUpdateNOC
self.pending_signing_key = ecdsa.keys.SigningKey.generate(
curve=ecdsa.NIST256p, hashfunc=hashlib.sha256
)
# DER encode the request
# https://www.rfc-editor.org/rfc/rfc2986 Section 4.2
certification_request = []
certification_request_info = []
# Version
certification_request_info.append(der.encode_integer(0))
# subject
attribute_type = der.encode_oid(2, 5, 4, 10)
value = encode_utf8_string("CSA")
subject = der.encode_sequence(
encode_set(der.encode_sequence(attribute_type, value))
)
certification_request_info.append(subject)
# Subject Public Key Info
algorithm = der.encode_sequence(
der.encode_oid(1, 2, 840, 10045, 2, 1),
der.encode_oid(1, 2, 840, 10045, 3, 1, 7),
)
self.pending_public_key = self.pending_signing_key.verifying_key.to_string(
encoding="uncompressed"
)
public_key = der.encode_bitstring(self.pending_public_key, unused=0)
spki = der.encode_sequence(algorithm, public_key)
certification_request_info.append(spki)
# Extensions
extension_request = der.encode_sequence(
der.encode_oid(1, 2, 840, 113549, 1, 9, 14),
encode_set(der.encode_sequence()),
)
certification_request_info.append(der.encode_constructed(0, extension_request))
certification_request_info = der.encode_sequence(*certification_request_info)
certification_request.append(certification_request_info)
signature_algorithm = der.encode_sequence(
der.encode_oid(1, 2, 840, 10045, 4, 3, 2)
)
certification_request.append(signature_algorithm)
# Signature
signature = self.pending_signing_key.sign_deterministic(
certification_request_info,
hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_der_canonize,
)
certification_request.append(der.encode_bitstring(signature, unused=0))
# Generate a new key pair.
new_key_csr = der.encode_sequence(*certification_request)
# Create a CSR to reply back with. Sign it with the new private key.
elements = NOCSRElements()
elements.csr = new_key_csr
elements.CSRNonce = args.CSRNonce
elements = elements.encode()
nocsr_tbs = elements.tobytes() + session.attestation_challenge
# class CSRResponse(tlv.Structure):
# NOCSRElements = tlv.OctetStringMember(0, RESP_MAX)
# AttestationSignature = tlv.OctetStringMember(1, 64)
response = data_model.NodeOperationalCredentialsCluster.CSRResponse()
response.NOCSRElements = elements
response.AttestationSignature = self.dac_key.sign_deterministic(
nocsr_tbs, hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_string
)
return response
def add_trusted_root_certificate(
self,
session,
args: data_model.NodeOperationalCredentialsCluster.AddTrustedRootCertificate,
) -> interaction_model.StatusCode:
self.pending_root_cert = args.RootCACertificate
return interaction_model.StatusCode.SUCCESS
def add_noc(
self, session, args: data_model.NodeOperationalCredentialsCluster.AddNOC
) -> data_model.NodeOperationalCredentialsCluster.NOCResponse:
# Section 11.18.6.8
noc, _ = crypto.MatterCertificate.decode(
args.NOCValue[0], memoryview(args.NOCValue)[1:]
)
icac, _ = crypto.MatterCertificate.decode(
args.ICACValue[0], memoryview(args.ICACValue)[1:]
)
response = data_model.NodeOperationalCredentialsCluster.NOCResponse()
if noc.ec_pub_key != self.pending_public_key:
print(noc.ec_pub_key, self.pending_public_key)
response.StatusCode = (
data_model.NodeOperationalCertStatusEnum.INVALID_PUBLIC_KEY
)
return response
# Save info about the fabric.
new_fabric_index = len(self.fabrics)
if new_fabric_index >= self.supported_fabrics:
response.StatusCode = data_model.NodeOperationalCertStatusEnum.TABLE_FULL
return response
session.local_fabric_index = new_fabric_index
# Store the NOC.
noc_struct = data_model.NodeOperationalCredentialsCluster.NOCStruct()
noc_struct.NOC = args.NOCValue
noc_struct.ICAC = args.ICACValue
self.nocs.append(noc_struct)
# Store the fabric
new_fabric = (
data_model.NodeOperationalCredentialsCluster.FabricDescriptorStruct()
)
new_fabric.RootPublicKey = self.pending_root_cert
new_fabric.VendorID = args.AdminVendorId
new_fabric.FabricID = noc.subject.matter_fabric_id
new_fabric.NodeID = noc.subject.matter_node_id
self.fabrics.append(new_fabric)
new_group_key = data_model.GroupKeyManagementCluster.KeySetWrite()
key_set = data_model.GroupKeySetStruct()
key_set.GroupKeySetID = 0
key_set.GroupKeySecurityPolicy = (
data_model.GroupKeySetSecurityPolicyEnum.TRUST_FIRST
)
key_set.EpochKey0 = args.IPKValue
key_set.EpochStartTime0 = 0
new_group_key.GroupKeySet = key_set
self.group_key_manager.key_set_write(session, new_group_key)
self.commissioned_fabrics += 1
# Get the root cert public key so we can create the compressed fabric id.
root_cert, _ = crypto.MatterCertificate.decode(
self.pending_root_cert[0], memoryview(self.pending_root_cert)[1:]
)
self.root_certs.append(root_cert)
fabric_id = struct.pack(">Q", noc.subject.matter_fabric_id)
self.compressed_fabric_ids.append(
crypto.KDF(root_cert.ec_pub_key[1:], fabric_id, b"CompressedFabric", 64)
)
compressed_fabric_id = self.compressed_fabric_ids[-1].hex().upper()
node_id = struct.pack(">Q", new_fabric.NodeID).hex().upper()
instance_name = f"{compressed_fabric_id}-{node_id}"
self.mdns_server.advertise_service(
"_matter",
"_tcp",
self.port,
instance_name=instance_name,
subtypes=[
f"_I{compressed_fabric_id}._sub._matter._tcp",
],
)
response.StatusCode = data_model.NodeOperationalCertStatusEnum.OK
return response
class GroupKeyManagementCluster(data_model.GroupKeyManagementCluster):
def __init__(self):
super().__init__()
self.key_sets = []
def key_set_write(
self, session, args: data_model.GroupKeyManagementCluster.KeySetWrite
) -> interaction_model.StatusCode:
self.key_sets.append(args.GroupKeySet)
return interaction_model.StatusCode.SUCCESS

View file

@ -21,6 +21,9 @@ HASH_LEN_BITS = 256
HASH_LEN_BYTES = 32
HASH_BLOCK_LEN_BYTES = 64
# Upper limit for encoded certificate size.
CERTIFICATE_SIZE = 400
class DNAttribute(tlv.List):
# Section 6.5.6.1
@ -150,4 +153,4 @@ def HKDF_Expand(prk, info, length) -> bytes:
def KDF(input_key, salt, info, length):
if salt is None:
salt = b"\x00" * HASH_LEN_BYTES
return HKDF_Expand(HKDF_Extract(salt, input_key), info, length / 8)
return HKDF_Expand(HKDF_Extract(salt, input_key), info, length // 8)[: length // 8]

View file

@ -207,6 +207,20 @@ class Cluster:
return None
class DescriptorCluster(Cluster):
CLUSTER_ID = 0x001D
class DeviceTypeStruct(tlv.Structure):
devtype_id = tlv.IntMember(0, signed=False, octets=4)
revision = tlv.IntMember(1, signed=False, octets=2, minimum=1)
DeviceTypeList = ListAttribute(0x0000)
ServerList = ListAttribute(0x0001)
ClientList = ListAttribute(0x0002)
PartsList = ListAttribute(0x0003)
TagList = ListAttribute(0x0004)
class ProductFinish(enum.IntEnum):
OTHER = 0
MATTE = 1

96
circuitmatter/exchange.py Normal file
View file

@ -0,0 +1,96 @@
import time
from .message import Message, ExchangeFlags, ProtocolId
from .protocol import SecureProtocolOpcode
# Section 4.12.8
MRP_MAX_TRANSMISSIONS = 5
"""The maximum number of transmission attempts for a given reliable message. The sender MAY choose this value as it sees fit."""
MRP_BACKOFF_BASE = 1.6
"""The base number for the exponential backoff equation."""
MRP_BACKOFF_JITTER = 0.25
"""The scaler for random jitter in the backoff equation."""
MRP_BACKOFF_MARGIN = 1.1
"""The scaler margin increase to backoff over the peer idle interval."""
MRP_BACKOFF_THRESHOLD = 1
"""The number of retransmissions before transitioning from linear to exponential backoff."""
MRP_STANDALONE_ACK_TIMEOUT_MS = 200
"""Amount of time to wait for an opportunity to piggyback an acknowledgement on an outbound message before falling back to sending a standalone acknowledgement."""
class Exchange:
def __init__(self, session, initiator: bool, exchange_id: int, protocols):
self.initiator = initiator
self.exchange_id = exchange_id
self.protocols = protocols
self.session = session
self.pending_acknowledgement = None
"""Message number that is waiting for an ack from us"""
self.send_standalone_time = None
self.next_retransmission_time = None
"""When to next resend the message that hasn't been acked"""
self.pending_retransmission = None
"""Message that we've attempted to send but hasn't been acked"""
def send(self, protocol_id, protocol_opcode, application_payload=None):
message = Message()
message.exchange_flags = ExchangeFlags(0)
if self.initiator:
message.exchange_flags |= ExchangeFlags.I
if self.pending_acknowledgement is not None:
message.exchange_flags |= ExchangeFlags.A
self.send_standalone_time = None
message.acknowledged_message_counter = self.pending_acknowledgement
self.pending_acknowledgement = None
message.protocol_id = protocol_id
message.protocol_opcode = protocol_opcode
message.exchange_id = self.exchange_id
message.application_payload = application_payload
self.session.send(message)
def send_standalone(self):
self.send(
ProtocolId.SECURE_CHANNEL, SecureProtocolOpcode.MRP_STANDALONE_ACK, None
)
def receive(self, message) -> bool:
"""Process the message and return if the packet should be dropped."""
if message.protocol_id not in self.protocols:
# Drop messages that don't match the protocols we're waiting for.
return True
# Section 4.12.5.2.1
if message.exchange_flags & ExchangeFlags.A:
if message.acknowledged_message_counter is None:
# Drop messages that are missing an acknowledgement counter.
return True
if message.acknowledged_message_counter != self.pending_acknowledgement:
# Drop messages that have the wrong acknowledgement counter.
return True
self.pending_retransmission = None
self.next_retransmission_time = None
# Section 4.12.5.2.2
# Incoming packets that are marked Reliable.
if message.exchange_flags & ExchangeFlags.R:
if message.duplicate:
# Send a standalone acknowledgement.
return True
if self.pending_acknowledgement is not None:
# Send a standalone acknowledgement with the message counter we're about to overwrite.
pass
self.pending_acknowledgement = message.message_counter
self.send_standalone_time = (
time.monotonic() + MRP_STANDALONE_ACK_TIMEOUT_MS / 1000
)
if message.duplicate:
return True
return False

257
circuitmatter/message.py Normal file
View file

@ -0,0 +1,257 @@
import enum
import struct
from . import tlv
from .protocol import ProtocolId
from typing import Optional
class ExchangeFlags(enum.IntFlag):
V = 1 << 4
SX = 1 << 3
R = 1 << 2
A = 1 << 1
I = 1 << 0 # noqa: E741
class SecurityFlags(enum.IntFlag):
P = 1 << 7
C = 1 << 6
MX = 1 << 5
# This is actually 2 bits but the top bit is reserved and always zero.
GROUP = 1 << 0
class Message:
def __init__(self):
self.clear()
def clear(self):
self.flags: int = 0
self.session_id: int = 0
self.security_flags: SecurityFlags = SecurityFlags(0)
self.message_counter: Optional[int] = 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
# Filled in after the message payload is decrypted.
self.exchange_flags: ExchangeFlags = ExchangeFlags(0)
self.exchange_id: Optional[int] = None
self.protocol_vendor_id = 0
self.protocol_id = ProtocolId(0)
self.protocol_opcode: Optional[int] = None
self.acknowledged_message_counter = None
self.application_payload = None
self.source_ipaddress = None
self.header = None
def parse_protocol_header(self):
self.exchange_flags, self.protocol_opcode, self.exchange_id = (
struct.unpack_from("<BBH", self.payload)
)
self.exchange_flags = ExchangeFlags(self.exchange_flags)
decrypted_offset = 4
self.protocol_vendor_id = 0
if self.exchange_flags & ExchangeFlags.V:
self.protocol_vendor_id = struct.unpack_from(
"<H", self.payload, decrypted_offset
)[0]
decrypted_offset += 2
protocol_id = struct.unpack_from("<H", self.payload, decrypted_offset)[0]
decrypted_offset += 2
self.protocol_id = ProtocolId(protocol_id)
self.protocol_opcode = self.protocol_id.ProtocolOpcode(self.protocol_opcode)
self.acknowledged_message_counter = None
if self.exchange_flags & ExchangeFlags.A:
self.acknowledged_message_counter = struct.unpack_from(
"<I", self.payload, decrypted_offset
)[0]
decrypted_offset += 4
self.application_payload = self.payload[decrypted_offset:]
def decode(self, buffer):
self.clear()
self.buffer = buffer
self.flags, self.session_id, self.security_flags, self.message_counter = (
struct.unpack_from("<BHBI", buffer)
)
self.security_flags = SecurityFlags(self.security_flags)
offset = 8
if self.flags & (1 << 2):
self.source_node_id = struct.unpack_from("<Q", buffer, 8)[0]
offset += 8
else:
self.source_node_id = 0
if (self.flags >> 4) != 0:
raise RuntimeError("Incorrect version")
self.secure_session = not (
not (self.security_flags & SecurityFlags.GROUP) and self.session_id == 0
)
self.decrypted = not self.secure_session
self.header = memoryview(buffer)[:offset]
self.payload = memoryview(buffer)[offset:]
self.duplicate = None
def encode_into(self, buffer, cipher=None):
offset = 0
struct.pack_into(
"<BHBI",
buffer,
offset,
self.flags,
self.session_id,
self.security_flags,
self.message_counter,
)
nonce_start = 3
nonce_end = nonce_start + 1 + 4
offset += 8
if self.source_node_id > 0:
struct.pack_into("<Q", buffer, offset, self.source_node_id)
offset += 8
nonce_end += 8
if self.destination_node_id > 0:
if self.destination_node_id > 0xFFFF_FFFF_FFFF_0000:
struct.pack_into(
"<H", buffer, offset, self.destination_node_id & 0xFFFF
)
offset += 2
else:
struct.pack_into("<Q", buffer, offset, self.destination_node_id)
offset += 8
if cipher is not None:
unencrypted_buffer = memoryview(bytearray(1280))
unencrypted_offset = 0
else:
unencrypted_buffer = buffer
unencrypted_offset = offset
struct.pack_into(
"BBHH",
unencrypted_buffer,
unencrypted_offset,
self.exchange_flags,
self.protocol_opcode,
self.exchange_id,
self.protocol_id,
)
unencrypted_offset += 6
if self.acknowledged_message_counter is not None:
struct.pack_into(
"I",
unencrypted_buffer,
unencrypted_offset,
self.acknowledged_message_counter,
)
unencrypted_offset += 4
if self.application_payload is not None:
if isinstance(self.application_payload, tlv.Structure):
# Wrap the structure in an anonymous tag.
unencrypted_buffer[unencrypted_offset] = 0x15
unencrypted_offset += 1
unencrypted_offset = self.application_payload.encode_into(
unencrypted_buffer, unencrypted_offset
)
elif hasattr(self.application_payload, "encode_into"):
unencrypted_offset = self.application_payload.encode_into(
unencrypted_buffer, unencrypted_offset
)
else:
# Skip a copy operation if we're using a separate unencrypted buffer
if unencrypted_offset == 0:
unencrypted_buffer = self.application_payload
else:
unencrypted_buffer[
unencrypted_offset : unencrypted_offset
+ len(self.application_payload)
] = self.application_payload
unencrypted_offset += len(self.application_payload)
# print("unencrypted", unencrypted_buffer[:unencrypted_offset].hex(" "))
# Encrypt the payload
if cipher is not None:
# The message may not include the source_node_id so we encode the nonce separately.
nonce = struct.pack(
"<BIQ", self.security_flags, self.message_counter, self.source_node_id
)
additional = buffer[:offset]
self.payload = cipher.encrypt(
nonce, bytes(unencrypted_buffer[:unencrypted_offset]), bytes(additional)
)
buffer[offset : offset + len(self.payload)] = self.payload
offset += len(self.payload)
else:
offset = unencrypted_offset
return offset
@property
def source_node_id(self):
return self._source_node_id
@source_node_id.setter
def source_node_id(self, value):
self._source_node_id = value
if value > 0:
self.flags |= 1 << 2
else:
self.flags &= ~(1 << 2)
@property
def destination_node_id(self):
return self._destination_node_id
@destination_node_id.setter
def destination_node_id(self, value):
self._destination_node_id = value
# Clear the field
self.flags &= ~0x3
if value == 0:
pass
elif value > 0xFFFF_FFFF_FFFF_0000:
self.flags |= 2
elif value > 0:
self.flags |= 1
def __str__(self):
pieces = ["Message:"]
pieces.append(f"Message Flags: {self.flags}")
pieces.append(f"Session ID: {self.session_id}")
pieces.append(f"Security Flags: {self.security_flags}")
pieces.append(f"Message Counter: {self.message_counter}")
if self.source_node_id is not None:
pieces.append(f"Source Node ID: {self.source_node_id:x}")
if self.destination_node_id is not None:
pieces.append(f"Destination Node ID: {self.destination_node_id:x}")
payload_info = ["Payload: "]
payload_info.append(f"Exchange Flags: {self.exchange_flags!r}")
payload_info.append(f"Protocol Opcode: {self.protocol_opcode!r}")
payload_info.append(f"Exchange ID: {self.exchange_id}")
if self.protocol_vendor_id:
payload_info.append(f"Protocol Vendor ID: {self.protocol_vendor_id}")
payload_info.append(f"Protocol ID: {self.protocol_id!r}")
if self.acknowledged_message_counter is not None:
payload_info.append(
f"Acknowledged Message Counter: {self.acknowledged_message_counter}"
)
if self.application_payload is not None:
application_payload = str(self.application_payload).replace("\n", "\n ")
payload_info.append(f"Application Payload: {application_payload}")
pieces.append("\n ".join(payload_info))
return "\n ".join(pieces)

72
circuitmatter/protocol.py Normal file
View file

@ -0,0 +1,72 @@
import enum
class SecureProtocolOpcode(enum.IntEnum):
MSG_COUNTER_SYNC_REQ = 0x00
"""The Message Counter Synchronization Request message queries the current message counter from a peer to bootstrap replay protection."""
MSG_COUNTER_SYNC_RSP = 0x01
"""The Message Counter Synchronization Response message provides the current message counter from a peer to bootstrap replay protection."""
MRP_STANDALONE_ACK = 0x10
"""This message is dedicated for the purpose of sending a stand-alone acknowledgement when there is no other data message available to piggyback an acknowledgement on top of."""
PBKDF_PARAM_REQUEST = 0x20
"""The request for PBKDF parameters necessary to complete the PASE protocol."""
PBKDF_PARAM_RESPONSE = 0x21
"""The PBKDF parameters sent in response to PBKDF-ParamRequest during the PASE protocol."""
PASE_PAKE1 = 0x22
"""The first PAKE message of the PASE protocol."""
PASE_PAKE2 = 0x23
"""The second PAKE message of the PASE protocol."""
PASE_PAKE3 = 0x24
"""The third PAKE message of the PASE protocol."""
CASE_SIGMA1 = 0x30
"""The first message of the CASE protocol."""
CASE_SIGMA2 = 0x31
"""The second message of the CASE protocol."""
CASE_SIGMA3 = 0x32
"""The third message of the CASE protocol."""
CASE_SIGMA2_RESUME = 0x33
"""The second resumption message of the CASE protocol."""
STATUS_REPORT = 0x40
"""The Status Report message encodes the result of an operation in the Secure Channel as well as other protocols."""
ICD_CHECK_IN = 0x50
"""The Check-in message notifies a client that the ICD is available for communication."""
class InteractionModelOpcode(enum.IntEnum):
STATUS_RESPONSE = 0x01
READ_REQUEST = 0x02
SUBSCRIBE_REQUEST = 0x03
SUBSCRIBE_RESPONSE = 0x04
REPORT_DATA = 0x05
WRITE_REQUEST = 0x06
WRITE_RESPONSE = 0x07
INVOKE_REQUEST = 0x08
INVOKE_RESPONSE = 0x09
TIMED_REQUEST = 0x0A
class ProtocolId(enum.IntEnum):
SECURE_CHANNEL = 0
INTERACTION_MODEL = 1
BDX = 2
USER_DIRECTED_COMMISSIONING = 3
FOR_TESTING = 4
def ProtocolOpcode(self, opcode_id: int):
if self == self.SECURE_CHANNEL:
return SecureProtocolOpcode(opcode_id)
elif self == self.INTERACTION_MODEL:
return InteractionModelOpcode(opcode_id)

View file

@ -1,4 +1,23 @@
import enum
import json
import time
from . import case
from . import crypto
from . import protocol
from . import tlv
from .exchange import Exchange
from .message import ExchangeFlags, SecurityFlags
import cryptography
import pathlib
import struct
# Section 4.11.2
MSG_COUNTER_WINDOW_SIZE = 32
MSG_COUNTER_SYNC_REQ_JITTER_MS = 500
MSG_COUNTER_SYNC_TIMEOUT_MS = 400
class SessionParameterStruct(tlv.Structure):
@ -9,3 +28,496 @@ class SessionParameterStruct(tlv.Structure):
interaction_model_revision = tlv.IntMember(5, signed=False, octets=2)
specification_version = tlv.IntMember(6, signed=False, octets=4)
max_paths_per_invoke = tlv.IntMember(7, signed=False, octets=2)
class GeneralCode(enum.IntEnum):
SUCCESS = 0
"""Operation completed successfully."""
FAILURE = 1
"""Generic failure, additional details may be included in the protocol specific status."""
BAD_PRECONDITION = 2
"""Operation was rejected by the system because the system is in an invalid state."""
OUT_OF_RANGE = 3
"""A value was out of a required range"""
BAD_REQUEST = 4
"""A request was unrecognized or malformed"""
UNSUPPORTED = 5
"""An unrecognized or unsupported request was received"""
UNEXPECTED = 6
"""A request was not expected at this time"""
RESOURCE_EXHAUSTED = 7
"""Insufficient resources to process the given request"""
BUSY = 8
"""Device is busy and cannot handle this request at this time"""
TIMEOUT = 9
"""A timeout occurred"""
CONTINUE = 10
"""Context-specific signal to proceed"""
ABORTED = 11
"""Failure, may be due to a concurrency error."""
INVALID_ARGUMENT = 12
"""An invalid/unsupported argument was provided"""
NOT_FOUND = 13
"""Some requested entity was not found"""
ALREADY_EXISTS = 14
"""The sender attempted to create something that already exists"""
PERMISSION_DENIED = 15
"""The sender does not have sufficient permissions to execute the requested operations."""
DATA_LOSS = 16
"""Unrecoverable data loss or corruption has occurred."""
MESSAGE_TOO_LARGE = 17
"""Message size is larger than the recipient can handle."""
class SecureChannelProtocolCode(enum.IntEnum):
SESSION_ESTABLISHMENT_SUCCESS = 0x0000
"""Indication that the last session establishment message was successfully processed."""
NO_SHARED_TRUST_ROOTS = 0x0001
"""Failure to find a common set of shared roots."""
INVALID_PARAMETER = 0x0002
"""Generic failure during session establishment."""
CLOSE_SESSION = 0x0003
"""Indication that the sender will close the current session."""
BUSY = 0x0004
"""Indication that the sender cannot currently fulfill the request."""
class StatusReport:
def __init__(self):
self.clear()
def clear(self):
self.general_code: GeneralCode = 0
self.protocol_id = 0
self.protocol_code = 0
self.protocol_data = None
def __len__(self):
return 8 + len(self.protocol_data) if self.protocol_data else 0
def encode_into(self, buffer, offset=0) -> int:
struct.pack_into(
"<HIH",
buffer,
offset,
self.general_code,
self.protocol_id,
self.protocol_code,
)
offset += 8
if self.protocol_data:
buffer[offset : offset + len(self.protocol_data)] = self.protocol_data
offset += len(self.protocol_data)
return offset
def decode(self, buffer):
self.general_code, self.protocol_id, self.protocol_code = struct.unpack_from(
"<HIH", buffer
)
self.general_code = GeneralCode(self.general_code)
self.protocol_data = buffer[8:]
def __str__(self):
return f"StatusReport: General Code: {self.general_code!r}, Protocol ID: {self.protocol_id}, Protocol Code: {self.protocol_code}, Protocol Data: {self.protocol_data.hex() if self.protocol_data else None}"
class UnsecuredSessionContext:
def __init__(
self,
socket,
message_counter,
initiator,
ephemeral_initiator_node_id,
node_ipaddress,
):
self.socket = socket
self.initiator = initiator
self.ephemeral_initiator_node_id = ephemeral_initiator_node_id
self.message_reception_state = None
self.message_counter = message_counter
self.node_ipaddress = node_ipaddress
self.exchanges = {}
def send(self, message):
message.destination_node_id = self.ephemeral_initiator_node_id
if message.message_counter is None:
message.message_counter = next(self.message_counter)
buf = memoryview(bytearray(1280))
nbytes = message.encode_into(buf)
self.socket.sendto(buf[:nbytes], self.node_ipaddress)
class SecureSessionContext:
def __init__(self, random_source, socket, local_session_id):
self.session_type = None
"""Records whether the session was established using CASE or PASE."""
self.session_role_initiator = False
"""Records whether the node is the session initiator or responder."""
self.local_session_id = local_session_id
"""Individually selected by each participant in secure unicast communication during session establishment and used as a unique identifier to recover encryption keys, authenticate incoming messages and associate them to existing sessions."""
self.peer_session_id = None
"""Assigned by the peer during session establishment"""
self.i2r_key = None
"""Encrypts data in messages sent from the initiator of session establishment to the responder."""
self.r2i_key = None
"""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 = MessageCounter(random_source=random_source)
"""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 sessions Fabric, which MAY be used to look up Fabric metadata related to the Fabric for which this session context applies."""
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."""
self.session_timestamp = None
"""A timestamp indicating the time at which the last message was sent or received. This timestamp SHALL be initialized with the time the session was created."""
self.active_timestamp = None
"""A timestamp indicating the time at which the last message was received. This timestamp SHALL be initialized with the time the session was created."""
self.session_idle_interval = None
self.session_active_interval = None
self.session_active_threshold = None
self.exchanges = {}
self._nonce = bytearray(crypto.AEAD_NONCE_LENGTH_BYTES)
self.socket = socket
self.node_ipaddress = None
@property
def peer_active(self):
return (time.monotonic() - self.active_timestamp) < self.session_active_interval
def decrypt_and_verify(self, message):
cipher = self.i2r
if self.session_role_initiator:
cipher = self.r2i
try:
source_node_id = 0 # for secure unicast messages
# TODO: Support group messages
struct.pack_into(
"<BIQ",
self._nonce,
0,
message.security_flags,
message.message_counter,
source_node_id,
)
decrypted_payload = cipher.decrypt(
self._nonce, bytes(message.payload), bytes(message.header)
)
except cryptography.exceptions.InvalidTag:
return False
message.decrypted = True
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 MessageReceptionState:
def __init__(self, starting_value, rollover=True, encrypted=False):
"""Implements 4.6.5.1"""
self.message_counter = starting_value
self.window_bitmap = (1 << MSG_COUNTER_WINDOW_SIZE) - 1
self.mask = self.window_bitmap
self.encrypted = encrypted
self.rollover = rollover
def process_counter(self, counter) -> bool:
"""Returns True if the counter number is a duplicate"""
# Process the current window first. Behavior outside the window varies.
if counter == self.message_counter:
return True
if self.message_counter <= MSG_COUNTER_WINDOW_SIZE < counter:
# Window wraps
bit_position = 0xFFFFFFFF - counter + self.message_counter
else:
bit_position = self.message_counter - counter - 1
if 0 <= bit_position < MSG_COUNTER_WINDOW_SIZE:
if self.window_bitmap & (1 << bit_position) != 0:
# This is a duplicate message
return True
self.window_bitmap |= 1 << bit_position
return False
new_start = (self.message_counter + 1) & self.mask # Inclusive
new_end = (
self.message_counter - MSG_COUNTER_WINDOW_SIZE
) & self.mask # Exclusive
if not self.rollover:
new_end = (1 << MSG_COUNTER_WINDOW_SIZE) - 1
elif self.encrypted:
new_end = (
self.message_counter + (1 << (MSG_COUNTER_WINDOW_SIZE - 1))
) & self.mask
if new_start <= new_end:
if not (new_start <= counter < new_end):
return True
else:
if not (counter < new_end or new_start <= counter):
return True
# This is a new message
shift = counter - self.message_counter
if counter < self.message_counter:
shift += 0x100000000
if shift > MSG_COUNTER_WINDOW_SIZE:
self.window_bitmap = 0
else:
new_bitmap = (self.window_bitmap << shift) & self.mask
self.window_bitmap = new_bitmap
if 1 < shift < MSG_COUNTER_WINDOW_SIZE:
self.window_bitmap |= 1 << (shift - 1)
self.message_counter = counter
return False
class MessageCounter:
def __init__(self, starting_value=None, random_source=None):
if starting_value is None:
starting_value = random_source.urandom(4)
starting_value = struct.unpack("<I", starting_value)[0]
starting_value >>= 4
starting_value += 1
self.value = starting_value
def __next__(self):
self.value = (self.value + 1) % 0xFFFFFFFF
return self.value
class SessionManager:
def __init__(self, random_source, socket, node_credentials):
persist_path = pathlib.Path("counters.json")
if persist_path.exists():
self.nonvolatile = json.loads(persist_path.read_text())
else:
self.nonvolatile = {}
self.nonvolatile["check_in_counter"] = None
self.nonvolatile["group_encrypted_data_message_counter"] = None
self.nonvolatile["group_encrypted_control_message_counter"] = None
self.unencrypted_message_counter = MessageCounter(random_source=random_source)
self.group_encrypted_data_message_counter = MessageCounter(
self.nonvolatile["group_encrypted_data_message_counter"],
random_source=random_source,
)
self.group_encrypted_control_message_counter = MessageCounter(
self.nonvolatile["group_encrypted_control_message_counter"],
random_source=random_source,
)
self.check_in_counter = MessageCounter(
self.nonvolatile["check_in_counter"], random_source=random_source
)
self.unsecured_session_context = {}
self.secure_session_contexts = ["reserved"]
self.socket = socket
self.random = random_source
self.node_credentials = node_credentials
def _increment(self, value):
return (value + 1) % 0xFFFFFFFF
def get_session(self, message):
if message.secure_session:
if message.security_flags & SecurityFlags.GROUP:
if message.source_node_id is None:
return None
# 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] = (
UnsecuredSessionContext(
self.socket,
self.unencrypted_message_counter,
initiator=False,
ephemeral_initiator_node_id=message.source_node_id,
node_ipaddress=message.source_ipaddress,
)
)
session_context = self.unsecured_session_context[message.source_node_id]
return session_context
def mark_duplicate(self, message):
"""Implements 4.6.7"""
session_context = self.get_session(message)
if session_context.message_reception_state is None:
session_context.message_reception_state = MessageReceptionState(
message.message_counter,
rollover=False,
encrypted=message.secure_session,
)
message.duplicate = False
return
message.duplicate = session_context.message_reception_state.process_counter(
message.message_counter
)
def next_message_counter(self, message):
"""Implements 4.6.6"""
if not message.secure_session:
value = self.unencrypted_message_counter
self.unencrypted_message_counter = self._increment(
self.unencrypted_message_counter
)
return value
elif message.security_flags & SecurityFlags.GROUP:
if message.security_flags & SecurityFlags.C:
value = self.group_encrypted_control_message_counter
self.group_encrypted_control_message_counter = self._increment(
self.group_encrypted_control_message_counter
)
return value
else:
value = self.group_encrypted_data_message_counter
self.group_encrypted_data_message_counter = self._increment(
self.group_encrypted_data_message_counter
)
return value
session = self.secure_session_contexts[message.session_id]
value = session.local_message_counter
next_value = self._increment(value)
session.local_message_counter = next_value
if next_value == 0:
# TODO expire the encryption key
raise NotImplementedError("Expire the encryption key 4.6.6")
return next_value
def new_context(self):
if None not in self.secure_session_contexts:
self.secure_session_contexts.append(None)
session_id = self.secure_session_contexts.index(None)
self.secure_session_contexts[session_id] = SecureSessionContext(
self.random, self.socket, session_id
)
return self.secure_session_contexts[session_id]
def process_exchange(self, message):
session = self.get_session(message)
if session is None:
return None
# Step 1 of 4.12.5.2
if (
message.exchange_flags & (ExchangeFlags.R | ExchangeFlags.A)
and not message.security_flags & SecurityFlags.C
and message.security_flags & SecurityFlags.GROUP
):
# Drop illegal combination of flags.
return None
if message.exchange_id not in session.exchanges:
# Section 4.10.5.2
initiator = message.exchange_flags & ExchangeFlags.I
if initiator and not message.duplicate:
session.exchanges[message.exchange_id] = Exchange(
session, not initiator, message.exchange_id, [message.protocol_id]
)
# Drop because the message isn't from an initiator.
elif message.exchange_flags & ExchangeFlags.R:
# Send a bare acknowledgement back.
raise NotImplementedError("Send a bare acknowledgement back")
return None
else:
# Just drop it.
return None
exchange = session.exchanges[message.exchange_id]
if exchange.receive(message):
# If we want to drop the message, then return None.
return None
return exchange
def reply_to_sigma1(self, sigma1):
if sigma1.resumptionID is None != sigma1.initiatorResumeMIC is None:
print("Invalid resumption ID")
error_status = StatusReport()
error_status.general_code = GeneralCode.FAILURE
error_status.protocol_id = protocol.ProtocolId.SECURE_CHANNEL
error_status.protocol_code = SecureChannelProtocolCode.INVALID_PARAMETER
return error_status
if sigma1.resumptionID is not None:
# Resume
raise NotImplementedError()
matching_noc = None
for i, fabric in enumerate(self.node_credentials.fabrics):
root_public_key = self.node_credentials.root_certs[i].ec_pub_key
key_set = self.node_credentials.group_key_manager.key_sets[i]
compressed_fabric_id = self.node_credentials.compressed_fabric_ids[i]
ipk_epoch_key = key_set.EpochKey0
identity_protection_key = crypto.KDF(
ipk_epoch_key,
compressed_fabric_id,
b"GroupKey v1.0",
crypto.SYMMETRIC_KEY_LENGTH_BITS,
)
fabric_id = struct.pack("<Q", fabric.FabricID)
node_id = struct.pack("<Q", fabric.NodeID)
candidate_destination_id = case.compute_destination_id(
root_public_key,
fabric_id,
node_id,
sigma1.initiatorRandom,
identity_protection_key,
)
print(candidate_destination_id.hex(), sigma1.destinationId.hex())
if sigma1.destinationId == candidate_destination_id:
print("matched!")
matching_noc = i
break
else:
print("didn't match")
if matching_noc is None:
error_status = StatusReport()
error_status.general_code = GeneralCode.FAILURE
error_status.protocol_id = protocol.ProtocolId.SECURE_CHANNEL
error_status.protocol_code = SecureChannelProtocolCode.NO_SHARED_TRUST_ROOTS
return error_status
sigma2 = case.Sigma2()
return sigma2

View file

@ -1,46 +1,37 @@
["urandom", 871507505707987, 4, "LYTFPw=="]
["urandom", 871507505724057, 4, "iY4sjA=="]
["urandom", 871507505736501, 4, "VGDJjA=="]
["urandom", 871507505744636, 4, "QuSDpw=="]
["urandom", 871507505760586, 8, "0PGaAxtzY2A="]
["receive", 871510714593623, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAMwUwQpGvSQkTU57tQUgS1gAABUwASCtkU/QXzijHlthCZ1aUI6G+OjmKqBQx5rWaOtXz1BIESUCh04kAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="]
["urandom", 871510741118477, 32, "ccncgbiV+n7AGVAQZjwSTlG+TIRHu3W2DbodunF9jdE="]
["urandom", 871510741140168, 4, "quQB6g=="]
["send", 871510741238353, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AQAAAERY/ANGvSQkTU57tQIhS1gAAMwUwQoVMAEgrZFP0F84ox5bYQmdWlCOhvjo5iqgUMea1mjrV89QSBEwAiBxydyBuJX6fsAZUBBmPBJOUb5MhEe7dbYNuh26cX2N0SQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="]
["receive", 871510747233066, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM0UwQpGvSQkTU57tQUiS1gAABUwAUEEV1RRuOlve0Df09KY83f47IzxiqUGFTZpEQD09obiPvcCjyOIhziiZ8W1cWHgCE4QUV/2H7yDRB0vDlaqXhvEeRg="]
["randbelow", 871510747348975, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 1372112454692769542481468369269276073942330241544482652254110676231987590818]
["send", 871510757117093, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AQAAAEVY/ANGvSQkTU57tQIjS1gAAM0UwQoVMAFBBJBlAvoBG5VgLXSdUtb3TwVwyNAp7T1EvRTKNU3nv4rv5KZtc1kaqYlQYkkpflmdMSYzYHp0k8RMlEkHXkie88MwAiAMY6gKq0gQezVlGFOhgvE2nsH7+QaTwTgDOPIpf7BhGRg="]
["receive", 871510757526795, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM4UwQpGvSQkTU57tQUkS1gAABUwASC09gAl9bruVi76LrlzQ+ybbYWti96RIl17zKb7cO4avBg="]
["send", 871510757606135, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AQAAAEZY/ANGvSQkTU57tQJAS1gAAM4UwQoAAAAAAAAAAA=="]
["receive", 871510757782357, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAN20VAGHsmD/rsF9BopTAFGhuQRhb+KBACHcwjhOztcLSGFIX7QSOUjpjYL2sRJRMpaYdoYeTriA/vhAlFMLOcivtVUP5LlheC03A2PJHp5fEypPdHkiTndhVGfk/qVf2Fx1dqdpQQDd6frYWP14J6gZhy2I9UYoEhDHiwi+lsE="]
["send", 871510758476055, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAEweoA4lVjKSA24mtNOw6eiRg7enlc1buKPr+8I2xCnewl7l1Y3uNxj0hZ787HWQdYkzPWbI/eKQDKAfT6hGrv+cIL7etjf+IEeA1kvEs++Fa8FK00ndAtIIlL79+u/4uVEqvcNaQMHUhC3FEyFiZ+u4SjVf4UDhWtkVdjnAn04e6Uk7JzfpGkR1tp3UEKv3cBlc749nzxvX6QQpaHFMtqY6D3BGVeVWffrqwwakEVoeoxBE6/6FPZOSrsjE4zwdJM1n+a9wHdhbeCwF1xFFNQyJ0rzGkqwk4P6viQL0Oo287TYJ0mY="]
["receive", 871510759404836, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAN60VAEJHWj3AEExQMElZbuNNvzahTZF6JIfUu/XKxFu0kw="]
["receive", 871510759500487, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAN+0VAE/TaX2/jtsVw1XhxSs2LX7uyxG+QYWt+gHMMQ3wK1dg2Q3j4oL+Rpi73UMr/csxk/mGA9Yo2gbWTwHoJc0jGiRxxdL8hTwTa+l5sFujg=="]
["send", 871510759738265, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAE0eoA6gySeKfHCuZ3uvuu2F43E9J71VFkGnKVVRtZvdHfGjzWdNgFd0uzbZmApNs0GjWyoG58fhQw=="]
["receive", 871510760001331, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOC0VAGkXHZzWisUrI5LQ38vo2kfoyG3uyzn5g/cTTp5qz4="]
["receive", 871510760102452, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOG0VAFACT6o+/nAZduzpi/uc7L5PTgxxZwUaAtv3DJmRIE8Z2Zgc09WT/7/BS9XYd+u5hBz2SuqY1MKNlM="]
["send", 871510760370297, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAE4eoA4FLe9QxePzbDzJW6fA9pcBjZmiQUTrFUwQpIkNRaDGuRuA4agq8riqVMtnwV+wD6N0vZNcUNxLT+usNg=="]
["receive", 871510760662057, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOK0VAGZpLer22Jco/RmWW37f1GCxvG4BC94UYQSB6Am8ZS3ax67xeodKK0pO1MEg9bdxeh1HL/G7eRHVXLV2wYIKQ=="]
["send", 871510760956863, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAE8eoA430KQCm/2QlvdZAeUNIFipPZf0+uPthKkiPwode3lXC7DCPdMQGFFvgum0errRC0h9AwSD4k8fCj+jjQ=="]
["receive", 871510761233805, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOO0VAExKRWtOgGCbgpgd78cO7rcxIOoZwmXGjeNZR4KGE1HnU2XCgz73CIUVo3Hnro9FSRz0vL9l6I="]
["send", 871510761593884, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFAeoA7QU5WNzHeYMdAJXWFAg3mqkaUtwC8HY5WAdtn7zy8hZP0JmecjVVL6OKfQdrqBIsFWAb2+ztLfvbn2pxrqLDaaWkqYNSQmUy4+roHylY5Yi/bDghMtw/uAp5eD0esliof0qvdEeCBJNxgVTnt9sW5oPrSJt8xGNyDbs2YV2ufPVNMKXPbcUr0k0Oo4HFkDLqeSbQPwMmW+3L/eV4nkiHGMlGoXRsQcleA8IoVXBo3dM/N+SNAaoziRwVfKfnZRuvmdkt57qmqemsvN9UEVfZl/PZQX/+pXaVPoO+7ulGOvs4nR7n3sy55uZA8XbVueYA8nPvCUJFQ2cnSye1h9GIc3xAeIQBVdVh3Phn9N0tZwVcwrRIzpnpEoJIqxgJq06vxWEJNboO8H/lW+VHAkooDvecq/gUh3EZViJ2SpnfJ1kJWWjXc2FV+LIaVzifwPJm/NbNDDvS6+1gRsoyjZsOHa5TGJtQWkDClPPg9HZP5YVnNwkFcFGt8n4MRfrYaHqgd5W7lWZSUC86a3i2B0pg4FTWIYpMRloR0R93DzKt4v7mSD6mzY+a1B3bLn0fo+NdgVeill8NodQ32fs0MarcVn4Dk15lEVkELswHw5tvpnMOIX7HNxtjinmvebwmHzbaHgnVxx2XXIIqq2ZVaEq5BIzApw0UsU1OyIPtwKKJHJgVEVFb+XCv65yZVwB4Yr8lcI"]
["receive", 871510761916101, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOS0VAG4u9A5duGe4zTqXeSDtC1dPUsjjFkMfiRhx7jL5OzRP7HeTnCn6KKvHyQDWKHGW7FM03uftps="]
["send", 871510762249089, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFEeoA5J3g7u/gjbmQ4vMlrrlojWEOoeIct/Q46/Qb7U0+2Bxogw/H36G6tPdjwm6WQmPXdxDi3/6BS6LhkAR7ZYyP4AXdBEsR0M3ErI9OIpXefieFGzOGRQZChgbdyVynfN+p9+b5asEnkznn0O80XUsoJavqMG071Exc3L2y/gQCuvqJUTLuxsUKQYKvR+tCn0Z96QcwFkITM+PQy00bsfeDYIz00ztGvJKhcoQnwz8SDljQ0VBUkAh+Cb1t50K5FNtoEitc1u7QfkMaRIcOq0/PiAS0tVPWyLWJ1+P0R9HWWkdpwJbt+lhYh0l3fIOnxYDLoiPTzXXxA3LiCwhhWozkz3MogR78w6jZlNugG4YtpwMZKiVFvJuUtQM3ZrfaADlPj4KNfP9xACalHBkbYMvAOEczp+LYSAqz7db7FZALZ6Z3tHb9gqSeZn9FRvsr33j5DxMNZ463FJtz4SLJRs1qCg+fj3NHHPVlEEr6v6IFhNol90N8k3C2HPeViu0zPne7qpTN8XLmcDwaIOsUqMo/pIU9xO2HRzIB9Z0W2oWKDYBNW1NQzC8qzo8iNpMThyw8xwAZaPSULS3iXcSZ9OssIXZhsnI2c9mHKrEnHHAK82SgLODw/Xo3Hew9pLoVlu8qH572kI5yN74GA5LFU+UMRfss8nGrZ1Y8ONbGb25xXHJXc9izimvEJtCCjSnJ+SDpvsrgsvxdjZDCu4IUwcDhcC8JUi3OiJ"]
["receive", 871510762616873, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOW0VAE6n+6ifjmAlz1XcgWVq4n42Em5+v5Q68VMnCxsbxfZRMIda0m9xQj5ACDvtzx5D3WpMwtUU1fR5e2KV/s0mFF3gYiOnEfoUtD7lGyZJtxGWGr2mvW1hg=="]
["send", 871510763454332, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFIeoA4sjyWsVHVw/cqQjFuHH/KrOkFLXQgUADVYqm5WjPnTiKFUh0WyQ2mgcd0aLcLqYyksMftwulxsjZWW1T1AR+Jn4m1BpdxVck2q8RGzDLTivtD3efPwMiOz0vb9uFoL4gUG2cxGif32XrY2hOL6ymVSOPbkpsF3rzez/qI0eiJIFEUM4d9Q+NALAo9qb7AIyN9WwqKvg9vv/cs2uDAL+xo4YaQRYA2MK8Ix41PytHq5gHOXCMYRCvsR1I1qt2frKjyr+8srV9c1evZZujz2g3ZEWMyq7vf6A8ITd3JGPRH9zM4UYIr1ej8tNII4htF+9RyLrO8jbO/ce5mH46kwj4g/+Ou2MBl0PuWJYqR47hONvFEIjCu6ygoVEnvcE3nyJ34RzjCn1pnTRir3clpatya6fU21eOlGQwPM9bVIHU54mM/OOAQwlv26YTcHvYXNssyOQ+V+7FjgpTuPXkUuIq0nq201iQY4Qs4wioHrgjJVTAO+BIDFTvGYyye6atcRKmE8N25B4g3GNUFfgQhwSpY8yg4bxhj9"]
["receive", 871510765694856, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOa0VAEblIYFdbSQgiGm6K9pubGrhB5cMn5tZHskzkukR2PuVz7m74+tiehJrsXvKm8UIv2tnSrgHyvo3+V2ebVnjwBzq8XYEMjVC3jk0yQv0xMWjeE5E5Bhjg=="]
["send", 871510767340209, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFMeoA7EKGToSc44Wi2zY+U/kGo+ThiPV+ZBE5D8MRHcynH532TJJxyQRVFKR0Vrfd1u6UVjQ+m1M1WzjXSsZwMLVpumdsj+mFcv2ZwJfL9iZ6cuXtZzZlTCjH6GQmBLGX3ju11ikadq+X1KxYEXJKJu5Q0lHll9w0DN9AhU3u2WrH0JQhwXL6dsx3UNShxkEvLLSLmr8JO7t3vShl+cE1dty6plEnYWOefVNB+lc+9ec7dPHWjZw7SGGN86gwuZuotYugPXoVsWjoNrduVYrAOZmJA9Wz0hA2SPIyCzpLDnp5ubUwjtTQVnGs/UKhl36cpK5eXuIJIATMb3E85fc47MPb7LZDLRrrQXzuqWolo2b7G0F/BDn7KAVWwvn4obNyUb1Gbg1eMv4qnbh5jIpdnJKx0ZtWmt4aQHjeAHkruyUgayE6hnKg5GcG+YgWQiSf9F9ceOmduGnJDMUpY/5lP+zcDPhQvDe9KPZn0bF8hyo7hkR6i2nKZ3CIkAQa4Yo5dC7LbW"]
["receive", 871510768445232, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOe0VAFgycfXhul85xkf8AYs+ZcAT7Mdsi618N6aMSirtQj/PfR/6yRWihgXAHED/dJdqS2cUY6VGnGuyJs1NX7T04SKndJltTf0s0Hz9h0AqekYWYoSynBpnDpp38VXFxUG8VGfLTs+XeYgr6KSSfIB7xCt+GiZSux/l+et1bt89UF1x8qG85vYEtxN4AvQ9vj/SLksMSYQtP4h3/GiiOEO8cHELM52re9OEvk5Zl0G/6T/Iue0xw4L//5vn9JLy6o6QyTQsC+0Bod1fS5Gq3EYF4SLmiDLck5nkhQay8a907GoJbCcg8z0iEsco/mWQ5BVXxyiMv3ZPi1r/wmT+4tBcKBo0pmhuvnUGXqdSNg9QkixLMW238mEuMi7uKi3nGg="]
["send", 871510768697378, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFQeoA5aiNXKMtETGYcmO46T1h42YB8NHr4ekIHwdqZ8rt92b8lIZ2/vJsX/SaXdIZXAm5V95SVY/rIgjg=="]
["receive", 871510769016680, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOi0VAFrBiW7zpzdiH84slfkh2uGmwGj5INszOz8ekypDkR4ReJSXKI95yT9UKvjc+S/uSgWNNO1c6x1o5blV2JsyKPxd0ktshbmPGX1DIh3r0fjwSfHO/Nj0QAx0t363pfAYShDSENGhAII9qvqDkKyTSaf2zZaubm0JuZqCNTWDDSYtxXRVo74jArQ604uxMvab+K4llXiKQ7n95ySfx8eRgjJRAHzXBYl6wi5jnJv/AHC7iv/vId3BZDRicecUm2MjS6RBg6XfOpmY2xzNPl7YwF6HN8wI1QIO8fRbrYOhfzbfEM4MWkGZGd2edm63iuesAO1QZOtYomhTzhK+mA7orCF62GkWAKuj6kZ7XX6Re5pcgeWD6wW8zn0TpTOuikJcqDDR2Qq9nz/P7sI4xBWtadydeZPUnVWPJK3buZOjQ+YeoiyPBDpGUz/s7TSAwI8+swQ8AiFnKMY+TyrUiGgRZ+EMKRPiZqT6Nd9WovvUImxtFZOgEcoYqPeWD0ceoOaEejMeojIXK1dHxqCRRJBRvC4nBqmIFkejTWKyxkf4vgeqFhjaMHi0LRSGSc6uMAAci9K2kx7tnk51RTSOjuHvAhXhGcT1c2H2JbJz9vlVOyVaetpxAn9ePvTsID+qgdgG4rBGau6OqpFidUEnCrdDNSPLuZEAFrsKawZHQ2AncsMrou93Lnx4oEdjw8gzDhHGAF95gCcahS3mqAmLvh5xfczAChXWqRHcbZpKSa1lx8="]
["send", 871510770152451, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFUeoA4xoDLCiedCkANLEtEuGcUfk7bs3HGt8LPWIhOEXacFheZq30bwav224n1OyuWWg6Cfn1E97PjGgA=="]
["receive", 871511536348884, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871512192149091, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871512745462688, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871513688495730, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871515359269685, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAAM8UwQpxjzD0bHCsFgUwVlgAABUwASBKyNYEGmInBE82JnNmFvOx71nNjBq+UHjWwfjSA9ITwCUCiE4wAyBUam0B+lHC/wWmC4AOPXmnCS/x0qNZQ99DVwOgoH6F6DAEQQTmkTT3k166A66xuqJOapYfWEtvyN64Lj/18LH2p3UihjjOTjG9nuPZCv5dXBdvoGEL7xP80UzeIYmZ2GRxEuwiNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871520579725115, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AAEAAOm0VAE4x+ZcR5C+Oxzkj66sGTrE2m1D/wWLvGGxHHg/Tan0R+lDBCjdZCkpaNde5ASGSLcaO6HCqzgv4dw="]
["send", 871520580169533, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "AIdOAFYeoA55LpHDPsHKqvP2S5hDNMtXNPdTmTxcPBzAlsT2XJQEpFpyIbr4VqUL9rnfzO3Wc3uyPGNEsuq1/cfHnw=="]
["receive", 871521781534473, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAANAUwQpfiPFpqjxOPgUwWFgAABUwASBQ0v2kJcKoo0TtVGWhF6P2PUCqhUoCkfXehTfq+VsTsiUCiU4wAyBsgV46uBS95RUxIhVJGj8AOgIh6ntK7a2x6upS7C7d4TAEQQRNB93DQrP0H9vH2Da5pdHl4GrH7JOE86WhpcLA9YjOPGX55SVIktysfVV3cXjUV1nExBDrpcazQnMx6KomBeUANQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871522417258946, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAANAUwQpfiPFpqjxOPgUwWFgAABUwASBQ0v2kJcKoo0TtVGWhF6P2PUCqhUoCkfXehTfq+VsTsiUCiU4wAyBsgV46uBS95RUxIhVJGj8AOgIh6ntK7a2x6upS7C7d4TAEQQRNB93DQrP0H9vH2Da5pdHl4GrH7JOE86WhpcLA9YjOPGX55SVIktysfVV3cXjUV1nExBDrpcazQnMx6KomBeUANQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["receive", 871523033966922, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 35019, 0, 0], "BAAAANAUwQpfiPFpqjxOPgUwWFgAABUwASBQ0v2kJcKoo0TtVGWhF6P2PUCqhUoCkfXehTfq+VsTsiUCiU4wAyBsgV46uBS95RUxIhVJGj8AOgIh6ntK7a2x6upS7C7d4TAEQQRNB93DQrP0H9vH2Da5pdHl4GrH7JOE86WhpcLA9YjOPGX55SVIktysfVV3cXjUV1nExBDrpcazQnMx6KomBeUANQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]
["urandom", 957477626050872, 4, "RxtNIw=="]
["urandom", 957477626066922, 4, "s/06iA=="]
["urandom", 957477626080397, 4, "CI8bZQ=="]
["urandom", 957477626089004, 4, "EByYzw=="]
["urandom", 957477626104703, 8, "wAonXmJPoFQ="]
["receive", 957481403668137, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "BAAAAByGpAcSl0j30JKozgUg+xwAABUwASAC6zsxstcRZTjqCrnXXdqS3RjGwbYxgcSG9uLvM2mCtCUCG9EkAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="]
["urandom", 957481429839885, 32, "12DeZjfWTicUctGPRTirKUyNU4Sgx5jsBkN8exw9MRY="]
["urandom", 957481429864491, 4, "+Y7Bag=="]
["send", 957481429964690, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AQAAALbRNAISl0j30JKozgIh+xwAAByGpAcVMAEgAus7MbLXEWU46gq5113akt0YxsG2MYHEhvbi7zNpgrQwAiDXYN5mN9ZOJxRy0Y9FOKspTI1ThKDHmOwGQ3x7HD0xFiQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="]
["receive", 957481436093225, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "BAAAAB2GpAcSl0j30JKozgUi+xwAABUwAUEE0LgRrhwTIxsCw5a+zavc73eSswrrt/lXke1N/OqPA51RG/zxsPGURJdl76rrVJ6o4iDypDg7TQJX7vJzS5equRg="]
["randbelow", 957481436206749, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 96523657396848248187256499613648568144896092570516211989401494846616576231763]
["send", 957481446089629, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AQAAALfRNAISl0j30JKozgIj+xwAAB2GpAcVMAFBBAYUPACafby10s50hSlddNjOz37MeCyZd7PgwTtmz300fF+eT56oy2fepbcbkLnt2Nqzd2eLLfmu8BpBSN6MdXowAiAfq6LLmOmr9BOR95o2+8Tqld92HAKD28MymeV7IS9KyBg="]
["receive", 957481446479895, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "BAAAAB6GpAcSl0j30JKozgUk+xwAABUwASDJDeYTlQDz6yOi9DNf6dIXP7r7kV3JnWz16miY8UnGJxg="]
["send", 957481446556319, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AQAAALjRNAISl0j30JKozgJA+xwAAB6GpAcAAAAAAAAAAA=="]
["receive", 957481446727843, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAD12+w0IUiaWKaQJeUehIApijNFRoaGK/+Ty8AvlUOgn+AB1v+JZT8q1JdFns8+kMNrb6aObW44Cc65UomsxaXTc6AFPDY3Vfz6DEhBYS1MjR4iuynyUPySexe3PwkerFWBVPey/kuM4akFBYl/lqtzxEWnQbC3lP3W5rhBjnLk="]
["send", 957481447387297, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPEYrAb66sK7ZsCIfLRaVLccbPUU6JBfFnE309MlIy8e1kerZ3jFiQdAitx/BZw5vlf3amhtjwLjsmoRWT8p+IxmEcfzCZdwUN2SISIwKWY1pdLrO6IafnNejPynTjl3JL/pEeMdz7WE6xJ1QUO+D1mXGBijSO5WtIX2QPPvglU+TbYO8AlLGCHe0pWv7lzOOyEUp4N4fq3gVTemIWm69rZPBkpY06IOGX+Ctri3YDOeoMTR3qj+RW32oFrZUQS2xNH8JUwxQYYfoOTFgVFNWykHeCObaSoMpDioojR+X+5a6jOubEQ="]
["receive", 957481448319757, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAD52+w2+0GEx+vlO4KI5oOnD058DXTaIWVAePxXtVdn4RrU="]
["receive", 957481448415798, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAD92+w3Ozg4f3PW3NID2eDOq+n8tcNsuxqHhXycE2SFtlLvMtbSfI59mC6MX1ZCkGFWt/IlMr7Nl+y1CynbPW/deah2BUDg8HOLx728vlwoKlQ=="]
["send", 957481448632407, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPIYrAYxai/HXjbkj0HOYoxKGr1249TBNqLkOxJZbYvdqh7nGIL6E/fndryX0T1vIJ2nDWn5MSEdQA=="]
["receive", 957481449245444, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEB2+w1LfSabrwMUXPeliNlCHW+8NRSqGbEZXpN/ad1eRNI="]
["receive", 957481449370901, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEF2+w2Qx1KJB4iKKrjLepQt4ToSy0TrydpaO/SbgWXHJhgq0eaPxnikZbGlLGPHbTKEn1os/YqrwDz4Y7U="]
["send", 957481449613198, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPMYrAZitQ5h5+VcUw36eEypmXhtiIntnRG1+uzY2AJ10Lq5bqkXwPUkPmPz6UkY42PWIc+PfceNbA/Dvp9VOA=="]
["receive", 957481449936388, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEJ2+w3b4Wp2R9auEXxwA9uHTZhP2vfmTIfZW2aGJZoIjNI1LBGPWZ4WeGDHj2wFlV18LbWhGX+hkgTfY7fL9JDwPA=="]
["send", 957481450207510, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPQYrAYswhOaMsqZVaD1Yoeuz3Y9qIjjqIYKZXgHgYGV0xf1yLsdnK2nj47hqB2qHO1RLAKVF5g6o4uQlxDiKg=="]
["receive", 957481450535238, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEN2+w3oFLUBS9v+3BLhSb/LQx1P5QvF8ZvB1GtelgKG70VVFafvqGS1gAv7RHbeeKttUDghuk85wXI="]
["send", 957481451053125, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPUYrAaEkYIHGSG/sg0IQC8n2aysQulzDN5kZlhXFjz4nyJogra8CvF0vy54Hu0bfwIjVo+PaEJyeiw2m8/+nO5+Z0v2ceDwLjJY97F3P7rUdosn+qhcNv58yhcSTFS/+rGIAiMFylyhdVXeQZjU0r+E2suSXHavXNEzj/cxGteyPl58sAY0zX6LL0RXfQvdyJdkD/BiWePfEHAbowpTwF1BZt+7vrGlkreKcHPXDoTOAr7MlXVA+7ZUtWhqp1jyFtGO1HBC3e/WzmxrPRmuOVomhYmI27cGDOvDc7ZW9n3s1jTpve4FF0SQb+3+PS4eI5ha3xxMsGnl2EaelVu8kg2eE34Oti7gJPLtGjysXIbN3ZVzrDjoGoG5UfBjSq/dlBR/9cwJBM+eAAREOqtXcfaytB4SUfeP8F/5q/6sPOku1qDQIv7AR48WytRAWF1WzZRB7kv0bfWJZoa//7lnB/3ZxCvTTMAK9NAVzIQmh3GThTsN7x18GxCo9T1fXnQqp+wB8QwdDKRAU7cNzXMOOPTHJgwIqCjOvZ6ORk2DdAMkvQz5qhNgVVY0FxZNBwJ9QHgJWfvqkWsejZSg4bw0BbRkR3NL+Mbk71ke0xCYleLawKwiHaRFnYr43wLnUqy03/m3qalOII+eRvBqwNahLi0mTeKWuS+Aq4WzcvlPelxl8VplZ53YUNDJ3fHqpe37zj2TBMrZ"]
["receive", 957481451421050, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAER2+w3eqEmalZu79q3Z5E0nj/Joq4ogk3hd6ocszliH3aijLsPpkzEliNGlw6+mUPhgW0ZjabMzNZ4="]
["send", 957481451743187, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPYYrAZxroyd1LgZkmGRVIs9zl0WSJvbOICIoRIa6kxB47fglnQg2A/9b4rt8gbKjJczrqxiTKS8Pb+8lVetPekEg22G19rw335eJtgVnA3wQq6FyB/uqBMXZ4HJz7i8DpPgWCE7PN2ECYdjCwNld/oP/mcpitxgx/DVFNgF4zKRnWb2olyvBDQwR/KiZzzWZ7u7LVo7Jjs/s3V1wiBA0lMnKkl7QQKsv8D4EzKXgaT1AnuXG4yiPnL+/v5ov3zIJPwg2AMlMNcQ2e9rKCZKMMuLKGx3Am6qCYDALbvAo/o1oLlVG2dU8SHGJBGsvG0xweTwW8m6HtYAmQ6as0voo6INy0x8kLIZiAvw/gc7Xr8KplZnZ/BVVLkJjWVqluw11VK9c19qeTEf1MLyQITlhht4eVHwcsU1oUKIuZKuxuT9dVKbgIzqoZBwvopoi9NEMcJCmK3lNjxfLtMx/k6EWidx9HdzuqvUOSHmGxQtYJAFmHOyH1bw53cZ2YaU4NKSBbv8TsRs2jKHK03Qi999/DalB4DHsVVrmpK3x/sXwjYazY+FTVPUkaM/wW9rX7A2ro7nTYgQjLD/bqr01xwwnZiVdySFLYD/f/hNHyNoh3RM/PxqBWgrpE9t0ZGcCivw+wnY9eWjRfyBei14WsE9gRUL7k/X+4JDoNUavV9QxUIYp56yh2BTtqFuYtig9kX/EJZEaVQ3Y9bCyGM0x+3bvWJyYptgx0pAWFXk"]
["receive", 957481452113787, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEV2+w09bgxPWaw/air5Glif3b0ouz38xvghBWQaAVlPnkya1cZ7FgAiISRD6+cPld62eRNl23muRXN8uz00pccXDtP7RYeWA/kUHHsMJxHb9E/L7sCKZXuKfA=="]
["send", 957481452926150, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPcYrAY/cTERAw8tSqtl4B/0zX4xpv8ls1Xa9xapK5u8xrfcVfwMmPc6zDIIXRm5N+Be3O45VGGXiXBTqJQThMVkkmSWrb3c461/qcOkTErYvwozsLGlsYj4wb5htzz8+u0dR+QbS2S1kZjZgA9VtEaxoDuJIfAxtX4Xp5LwOIA391Iu+Tx3fWcZl3qfFZDiGOyFIvqpYnbGi76ndEvUwwFQi8eTQcBUxE7gBbC17Y4X7EmW6Ba+1JzTbWpuOz1K1UcouFXKTtOlUB55oHRlLh+nLFqcgvhjXgNOHWJJVQ8apEsguY0qnswkmkg+ARgOJ0Iqf3ZfZ+qC5XFtGgcnXGono8SEs8xC4IodCJ/6FdXqXfN3DgghHVYAgPV5BPb/TNxMGkseO02mdxdMMucGHCGkvsH/VAgt41WVNeqUh1FIWiKdm+LvCAx9hFc6Wpa5UIyyVn+2Qr69jrU6N767bOnwA6Ddj3cnwkmSjWs3NKmz26li3CHhlNESckfkyadFHNZVFNi7hAwi6shJLUKa/GnxhJjQFhugnbzH"]
["receive", 957481455078231, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEZ2+w34DqabLugWquFRGk2dwsY17UDpzeRP2XZZGaD+KEk+zMbsEwTc5xL/IWgnoKJ1UwPYJoavuajr04gdfnRsiTnHPVp0S4rc+r9MNszRdEQ7I8Y/5yI/xQ=="]
["send", 957481456616744, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPgYrAaYS+op+sXu51xgPZyQblEhDbX/YmCmTkaX/C8fidWqgcg0rBfx9faWiDkdZ6ozUsm6GJagI9uc7ku9JPJynngqGb10rkOd3dB1C5HEH9764lJlgJ2RQESHlX+fRvmg33yMdomEjSUiehJ166f5NaMal4oFQUhdcllQ9+xojhkABOkmN2q7NuBxYUQ/vIbyAciMnsG14ynIdwgR3NHB+qrc1bog/wm6lgvCgUlE9Iff8VuIIKjDh5uWJ5EqCDwI+Wme/+wpNZQ44bCpycSNcB3JYZH4o1jW3Y7Mbwa3sIXih4cT/PdkRUDbHHBeRUDZW7fHmT/7KNvv/F1KGZlFkZpHpVtjlBOFycSEnLNr/Bt7erISm9HclQcHwLLFK/2MEK+TwBDTSQmavkMYkrzJaFhE0kuDVZGBVefeaCmlN13U5NUTLeZ1J719ybIQavFHAlHGSYDVpXTaOrT+RBIMpJhgqdN4fopTUVoThyA6ctZF8ZdiWZ0dATolutjCMawX3tMlrg=="]
["receive", 957481457736527, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEd2+w0oPja3y0GRhV0DCmWqIzB7l0SB8AuS04+sRYi5ze5T23BKUOjWg4iiccdm8jL74SuQaKBRgmbALoj2T4Lkzz3JhB98cy5YS3ka4ic76mOSTHWOkzHpzbxtuv3j4HQyVbciKp8v5gStDyCVPAQJ7RapUR2038RQN4jcWgGi+4swnw8Inp06y3BHctCKLUlmqxcZTWanzzdBUMujA6sS0x+0AeasOQVAgF7aU66SUCYz1+s6tZ5kRmSAoJiG+rch+SXdFOwotUOIivgbjR1XFULzNzuT1WOh3jp5PQGCZHNMSyAZwwOxPrQDGdLNRkShdfBo5AV/q2TuW37KPJn/zMg4zFS099xQkvZFHSTPQ9yOj8VZPQJd9TxFEaF6T9o="]
["send", 957481457974156, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPkYrAZh0wZJtdBpui9Ya0ZMJsoDvxqCchdPAusDDy9vR0cmd6RccbC0gwVy/hplw5EWPqiMbv9fWFRRmA=="]
["receive", 957481458299810, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "AAEAAEh2+w2AAwvmfRNG+5VNueG/SJT93Q+UfmUm1WBTaMIi8eyNKBm18ELhH0Ohru0uVZdQEohP22sCmshvE9sXNd89WGVfbxF1LrAsEpVCDMFJBav7pHM6R6i72dfMjS8JgzQtqsiQFRNyRYTTicZ5uU9+yyhP6hOgl5lRQWpmNLHYMvvjm37/LAIVN/Y/nP3d0PB3POY2nBywiOYCr5WXKaw5YaUXtHGi6C2FHwB+gTnurZjJM9D4wNsm/qnu0obzqAj6GdYUO+Ag8GwGkjQVpDXwiBH/zocFblX78+a9pdd1ReGi+xTbYnYnQ3e27YSQP4Gn+UNKXcAY0/Zl+l87i57rgqUZ5eyAX78UvAKvLrenzzoGRZ+7t5n5QlQtSqbqSvzSnRhE3iZghq3rIOHm1RDJuX8HDGjfmE/s3Y3lNxZb/D2QA1ngGFodOkITWOVRs47CPrcQ9YiyZHCZXN2uyJ/nOFUydIii6XlqX99jObTgySWnfMlu7PJh4OLPiGj2Kbi1smGd77kuSQXBK9T1CP/ptMVXEImx2mJVMYyiYCjqzrMRD5uCmlqVgEPCeuvcS7DNTYSmbc1QtH2peBpsUAcxR+bMah34SSnNTH18iER0+OuZSQqWiKVsSORzUUBMdgK//8T2awEVK9IgI4wZ7hDsp/taLVTay/GSiziQYRzaTsX7peRVVq9Et9ZxKoj1ZgECZWKdRepYH5L5M4i1I1V/zt1fwyUCPg7LXy5m9Oxtn1I="]
["send", 957481459125087, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "ABvRAPoYrAZIIDlhRP5354g/w0Gv21d1GaA+MUumNUw7jp8tOeUMzmPP65OiYOo528onSAbYM8HL8kuVDhnUXw=="]
["receive", 957482453663651, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 47844, 0, 0], "BAAAAB+GpAdejAxZG7iGlwUwBh0AABUwASAEROPgEISULXCn20VG8hxBxQygu7ethlezIXbN2+gN8SUCHNEwAyDIlEQTUwT68oaWfxabeB3nKaR+u+B5Y3rzpFpWztF3EzAEQQTwNP4+3G4VIvKvtxF6PJix6eMqJxh2SKfxUyxUpMXuK/li5+5ljpu5mVqSi3Y/xLP9WtBKK0LuNfoJmz2kVDUHNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="]