Restructure + destinationId
This commit is contained in:
parent
c406629605
commit
15c6b518cb
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
109
circuitmatter/case.py
Normal 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(":"))
|
||||
0
circuitmatter/clusters/__init__.py
Normal file
0
circuitmatter/clusters/__init__.py
Normal file
330
circuitmatter/clusters/core.py
Normal file
330
circuitmatter/clusters/core.py
Normal 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
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
96
circuitmatter/exchange.py
Normal 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
257
circuitmatter/message.py
Normal 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
72
circuitmatter/protocol.py
Normal 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)
|
||||
|
|
@ -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 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 = 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
|
||||
|
|
|
|||
|
|
@ -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=="]
|
||||
|
|
|
|||
Loading…
Reference in a new issue