Matter refactor PASE parameters (#18406)

This commit is contained in:
s-hadinger 2023-04-13 22:21:33 +02:00 committed by GitHub
parent 3da96a55d5
commit 0c0ab855f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 6228 additions and 6208 deletions

View File

@ -133,7 +133,9 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
#include "solidify/solidified_Matter_IM_Data.h"
#include "solidify/solidified_Matter_UDPServer.h"
#include "solidify/solidified_Matter_Expirable.h"
#include "solidify/solidified_Matter_Fabric.h"
#include "solidify/solidified_Matter_Session.h"
#include "solidify/solidified_Matter_Session_Store.h"
#include "solidify/solidified_Matter_Commissioning_Data.h"
#include "solidify/solidified_Matter_Commissioning.h"
#include "solidify/solidified_Matter_Message.h"

View File

@ -35,38 +35,21 @@ class Matter_Commisioning_Context
var responder # reference to the caller, sending packets
var device # root device object
var spake
var future_initiator_session_id
var future_local_session_id
# used by TT hash
var PBKDFParamRequest, PBKDFParamResponse
# PAKE
var y # 32 bytes random known only by verifier
var pA, pB, cA, cB
var Ke
# CASE
var ResponderEph_priv, ResponderEph_pub
var initiatorEph_pub
# Session data
var created
var I2RKey, R2IKey, AttestationChallenge
def init(responder)
import crypto
self.responder = responder
self.device = responder.device
# generate y once
self.y = crypto.random(32)
end
#############################################################
def add_session(local_session_id, initiator_session_id, i2r, r2i, ac, created)
def add_session(local_session_id, initiator_session_id, i2r, r2i, ac)
import string
# create session object
tasmota.log(string.format("MTR: add_session local_session_id=%i initiator_session_id=%i", local_session_id, initiator_session_id), 3)
var session = self.device.sessions.create_session(local_session_id, initiator_session_id)
session.set_keys(i2r, r2i, ac, created)
session.set_keys(i2r, r2i, ac)
end
def process_incoming(msg)
@ -126,6 +109,7 @@ class Matter_Commisioning_Context
def parse_PBKDFParamRequest(msg)
import crypto
import string
var session = msg.session
# sanity checks
if msg.opcode != 0x20 || msg.local_session_id != 0 || msg.protocol_id != 0
tasmota.log("MTR: invalid PBKDFParamRequest message", 2)
@ -136,7 +120,7 @@ class Matter_Commisioning_Context
var pbkdfparamreq = matter.PBKDFParamRequest().parse(msg.raw, msg.app_payload_idx)
msg.session.set_mode_PASE()
self.PBKDFParamRequest = msg.raw[msg.app_payload_idx..]
session.__Msg1 = msg.raw[msg.app_payload_idx..]
# sanity check for PBKDFParamRequest
if pbkdfparamreq.passcodeId != 0
@ -147,9 +131,9 @@ class Matter_Commisioning_Context
end
# record the initiator_session_id
self.future_initiator_session_id = pbkdfparamreq.initiator_session_id
self.future_local_session_id = self.device.sessions.gen_local_session_id()
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", self.future_local_session_id, msg.remote_ip, msg.remote_port), 2)
session.__future_initiator_session_id = pbkdfparamreq.initiator_session_id
session.__future_local_session_id = self.device.sessions.gen_local_session_id()
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", session.__future_local_session_id, msg.remote_ip, msg.remote_port), 2)
# prepare response
var pbkdfparamresp = matter.PBKDFParamResponse()
@ -157,14 +141,14 @@ class Matter_Commisioning_Context
pbkdfparamresp.initiatorRandom = pbkdfparamreq.initiatorRandom
# generate 32 bytes random
pbkdfparamresp.responderRandom = crypto.random(32)
pbkdfparamresp.responderSessionId = self.future_local_session_id
pbkdfparamresp.responderSessionId = session.__future_local_session_id
pbkdfparamresp.pbkdf_parameters_salt = self.device.commissioning_salt
pbkdfparamresp.pbkdf_parameters_iterations = self.device.commissioning_iterations
tasmota.log("MTR: pbkdfparamresp: " + str(matter.inspect(pbkdfparamresp)), 4)
var pbkdfparamresp_raw = pbkdfparamresp.tlv2raw()
tasmota.log("MTR: pbkdfparamresp_raw: " + pbkdfparamresp_raw.tohex(), 4)
self.PBKDFParamResponse = pbkdfparamresp_raw
session.__Msg2 = pbkdfparamresp_raw
var resp = msg.build_response(0x21 #-PBKDR Response-#, true)
var raw = resp.encode_frame(pbkdfparamresp_raw)
@ -175,6 +159,7 @@ class Matter_Commisioning_Context
def parse_Pake1(msg)
import crypto
var session = msg.session
# sanity checks
if msg.opcode != 0x22 || msg.local_session_id != 0 || msg.protocol_id != 0
tasmota.log("MTR: invalid Pake1 message", 2)
@ -184,67 +169,68 @@ class Matter_Commisioning_Context
end
var pake1 = matter.Pake1().parse(msg.raw, msg.app_payload_idx)
self.pA = pake1.pA
# tasmota.log("MTR: received pA=" + self.pA.tohex(), 4)
var pA = pake1.pA
# tasmota.log("MTR: received pA=" + pA.tohex(), 4)
# tasmota.log("MTR: spake: " + matter.inspect(self.spake), 4)
# instanciate SPAKE
# for testing purpose, we don't send `w1` to make sure
self.spake = crypto.SPAKE2P_Matter(self.device.commissioning_w0, nil, self.device.commissioning_L)
var spake = crypto.SPAKE2P_Matter(self.device.commissioning_w0, nil, self.device.commissioning_L)
# generate `y` nonce (not persisted)
var y = crypto.random(32) # 32 bytes random known only by verifier
# compute pB
self.spake.compute_pB(self.y)
self.pB = self.spake.pB
# tasmota.log("MTR: y=" + self.y.tohex(), 4)
# tasmota.log("MTR: pb=" + self.pB.tohex(), 4)
spake.compute_pB(y)
# tasmota.log("MTR: y=" + y.tohex(), 4)
# tasmota.log("MTR: pb=" + spake.pB.tohex(), 4)
# compute ZV
self.spake.compute_ZV_verifier(self.pA)
# tasmota.log("MTR: Z=" + self.spake.Z.tohex(), 4)
# tasmota.log("MTR: V=" + self.spake.V.tohex(), 4)
spake.compute_ZV_verifier(pA)
# tasmota.log("MTR: Z=" + spake.Z.tohex(), 4)
# tasmota.log("MTR: V=" + spake.V.tohex(), 4)
var context = crypto.SHA256()
context.update(bytes().fromstring(self.Matter_Context_Prefix))
context.update(self.PBKDFParamRequest)
context.update(self.PBKDFParamResponse)
context.update(session.__Msg1)
context.update(session.__Msg2)
var context_hash = context.out()
# tasmota.log("MTR: Context=" + context_hash.tohex(), 4)
# add pA
self.spake.pA = self.pA
spake.pA = pA
self.spake.set_context(context_hash)
self.spake.compute_TT_hash(true) # `true` to indicate it's Matter variant to SPAKE2+
spake.set_context(context_hash)
spake.compute_TT_hash(true) # `true` to indicate it's Matter variant to SPAKE2+
# tasmota.log("MTR: ------------------------------", 4)
# tasmota.log("MTR: Context = " + self.spake.Context.tohex(), 4)
# tasmota.log("MTR: M = " + self.spake.M.tohex(), 4)
# tasmota.log("MTR: N = " + self.spake.N.tohex(), 4)
# tasmota.log("MTR: pA = " + self.spake.pA.tohex(), 4)
# tasmota.log("MTR: pB = " + self.spake.pB.tohex(), 4)
# tasmota.log("MTR: Z = " + self.spake.Z.tohex(), 4)
# tasmota.log("MTR: V = " + self.spake.V.tohex(), 4)
# tasmota.log("MTR: w0 = " + self.spake.w0.tohex(), 4)
# tasmota.log("MTR: Context = " + spake.Context.tohex(), 4)
# tasmota.log("MTR: M = " + spake.M.tohex(), 4)
# tasmota.log("MTR: N = " + spake.N.tohex(), 4)
# tasmota.log("MTR: pA = " + spake.pA.tohex(), 4)
# tasmota.log("MTR: pB = " + spake.pB.tohex(), 4)
# tasmota.log("MTR: Z = " + spake.Z.tohex(), 4)
# tasmota.log("MTR: V = " + spake.V.tohex(), 4)
# tasmota.log("MTR: w0 = " + spake.w0.tohex(), 4)
# tasmota.log("MTR: ------------------------------", 4)
# tasmota.log("MTR: Kmain =" + self.spake.Kmain.tohex(), 4)
# tasmota.log("MTR: Kmain =" + spake.Kmain.tohex(), 4)
# tasmota.log("MTR: KcA =" + self.spake.KcA.tohex(), 4)
# tasmota.log("MTR: KcB =" + self.spake.KcB.tohex(), 4)
# tasmota.log("MTR: K_shared=" + self.spake.K_shared.tohex(), 4)
# tasmota.log("MTR: Ke =" + self.spake.Ke.tohex(), 4)
self.cB = self.spake.cB
self.Ke = self.spake.Ke
# tasmota.log("MTR: cB=" + self.cB.tohex(), 4)
# tasmota.log("MTR: KcA =" + spake.KcA.tohex(), 4)
# tasmota.log("MTR: KcB =" + spake.KcB.tohex(), 4)
# tasmota.log("MTR: K_shared=" + spake.K_shared.tohex(), 4)
# tasmota.log("MTR: Ke =" + spake.Ke.tohex(), 4)
# tasmota.log("MTR: cB=" + spake.cB.tohex(), 4)
var pake2 = matter.Pake2()
pake2.pB = self.pB
pake2.cB = self.cB
pake2.pB = spake.pB
pake2.cB = spake.cB
# tasmota.log("MTR: pake2: " + matter.inspect(pake2), 4)
var pake2_raw = pake2.tlv2raw()
# tasmota.log("MTR: pake2_raw: " + pake2_raw.tohex(), 4)
session.__spake_cA = spake.cA
session.__spake_Ke = spake.Ke
# now package the response message
var resp = msg.build_response(0x23 #-pake-2-#, true) # no reliable flag
@ -256,6 +242,7 @@ class Matter_Commisioning_Context
def parse_Pake3(msg)
import crypto
var session = msg.session
# sanity checks
if msg.opcode != 0x24 || msg.local_session_id != 0 || msg.protocol_id != 0
tasmota.log("MTR: invalid Pake3 message", 2)
@ -265,11 +252,11 @@ class Matter_Commisioning_Context
end
var pake3 = matter.Pake3().parse(msg.raw, msg.app_payload_idx)
self.cA = pake3.cA
# tasmota.log("MTR: received cA=" + self.cA.tohex(), 4)
var cA = pake3.cA
# tasmota.log("MTR: received cA=" + cA.tohex(), 4)
# check the value against computed
if self.cA != self.spake.cA
if cA != session.__spake_cA
tasmota.log("MTR: invalid cA received", 2)
tasmota.log("MTR: StatusReport(General Code: FAILURE, ProtocolId: SECURE_CHANNEL, ProtocolCode: INVALID_PARAMETER)", 2)
var raw = self.send_status_report(msg, 0x01, 0x0000, 0x0002, false)
@ -277,23 +264,23 @@ class Matter_Commisioning_Context
end
# send PakeFinished and compute session key
self.created = tasmota.rtc()['utc']
var session_keys = crypto.HKDF_SHA256().derive(self.Ke, bytes(), bytes().fromstring(self.SEKeys_Info), 48)
self.I2RKey = session_keys[0..15]
self.R2IKey = session_keys[16..31]
self.AttestationChallenge = session_keys[32..47]
var created = tasmota.rtc()['utc']
var session_keys = crypto.HKDF_SHA256().derive(session.__spake_Ke, bytes(), bytes().fromstring(self.SEKeys_Info), 48)
var I2RKey = session_keys[0..15]
var R2IKey = session_keys[16..31]
var AttestationChallenge = session_keys[32..47]
# tasmota.log("MTR: ******************************", 4)
# tasmota.log("MTR: session_keys=" + session_keys.tohex(), 4)
# tasmota.log("MTR: I2RKey =" + self.I2RKey.tohex(), 4)
# tasmota.log("MTR: R2IKey =" + self.R2IKey.tohex(), 4)
# tasmota.log("MTR: AC =" + self.AttestationChallenge.tohex(), 4)
# tasmota.log("MTR: I2RKey =" + I2RKey.tohex(), 4)
# tasmota.log("MTR: R2IKey =" + R2IKey.tohex(), 4)
# tasmota.log("MTR: AC =" + AttestationChallenge.tohex(), 4)
# tasmota.log("MTR: ******************************", 4)
# StatusReport(GeneralCode: SUCCESS, ProtocolId: SECURE_CHANNEL, ProtocolCode: SESSION_ESTABLISHMENT_SUCCESS)
var raw = self.send_status_report(msg, 0x00, 0x0000, 0x0000, false)
self.add_session(self.future_local_session_id, self.future_initiator_session_id, self.I2RKey, self.R2IKey, self.AttestationChallenge, self.created)
self.add_session(session.__future_local_session_id, session.__future_initiator_session_id, I2RKey, R2IKey, AttestationChallenge, created)
return true
end
@ -323,6 +310,7 @@ class Matter_Commisioning_Context
def parse_Sigma1(msg)
import crypto
import string
var session = msg.session
# sanity checks
if msg.opcode != 0x30 || msg.local_session_id != 0 || msg.protocol_id != 0
# tasmota.log("MTR: invalid Sigma1 message", 2)
@ -333,7 +321,7 @@ class Matter_Commisioning_Context
var sigma1 = matter.Sigma1().parse(msg.raw, msg.app_payload_idx)
tasmota.log(string.format("MTR: sigma1=%s", matter.inspect(sigma1)), 4)
self.initiatorEph_pub = sigma1.initiatorEphPubKey
session.__initiator_pub = sigma1.initiatorEphPubKey
# find session
var is_resumption = (sigma1.resumptionID != nil && sigma1.initiatorResumeMIC != nil)
@ -342,7 +330,6 @@ class Matter_Commisioning_Context
is_resumption = false
# Check that it's a resumption
var session = msg.session
var session_resumption
if is_resumption
session_resumption = self.device.sessions.find_session_by_resumption_id(sigma1.resumptionID)
@ -378,7 +365,6 @@ class Matter_Commisioning_Context
session.set_mode_CASE()
session.__future_initiator_session_id = sigma1.initiator_session_id # update initiator_session_id
session.__future_local_session_id = self.device.sessions.gen_local_session_id()
# self.future_local_session_id = session.__future_local_session_id
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", session.__future_local_session_id, msg.remote_ip, msg.remote_port), 2)
# Generate and Send Sigma2_Resume
@ -467,8 +453,7 @@ class Matter_Commisioning_Context
session.__future_initiator_session_id = sigma1.initiator_session_id # update initiator_session_id
session.__future_local_session_id = self.device.sessions.gen_local_session_id()
self.future_local_session_id = session.__future_local_session_id
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", self.future_local_session_id, msg.remote_ip, msg.remote_port), 2)
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", session.__future_local_session_id, msg.remote_ip, msg.remote_port), 2)
tasmota.log("MTR: fabric="+matter.inspect(session._fabric), 4)
tasmota.log("MTR: no_private_key="+session._fabric.no_private_key.tohex(), 4)
@ -480,18 +465,18 @@ class Matter_Commisioning_Context
# Compute Sigma2, p.162
session.resumption_id = crypto.random(16)
self.ResponderEph_priv = crypto.random(32)
self.ResponderEph_pub = crypto.EC_P256().public_key(self.ResponderEph_priv)
tasmota.log("MTR: ResponderEph_priv ="+self.ResponderEph_priv.tohex(), 4)
tasmota.log("MTR: ResponderEph_pub ="+self.ResponderEph_pub.tohex(), 4)
session.__responder_priv = crypto.random(32)
session.__responder_pub = crypto.EC_P256().public_key(session.__responder_priv)
tasmota.log("MTR: ResponderEph_priv ="+session.__responder_priv.tohex(), 4)
tasmota.log("MTR: ResponderEph_pub ="+session.__responder_pub.tohex(), 4)
var responderRandom = crypto.random(32)
session.shared_secret = crypto.EC_P256().shared_key(self.ResponderEph_priv, sigma1.initiatorEphPubKey)
session.shared_secret = crypto.EC_P256().shared_key(session.__responder_priv, sigma1.initiatorEphPubKey)
var sigma2_tbsdata = matter.TLV.Matter_TLV_struct()
sigma2_tbsdata.add_TLV(1, matter.TLV.B2, session.get_noc())
sigma2_tbsdata.add_TLV(2, matter.TLV.B2, session.get_icac())
sigma2_tbsdata.add_TLV(3, matter.TLV.B2, self.ResponderEph_pub)
sigma2_tbsdata.add_TLV(3, matter.TLV.B2, session.__responder_pub)
sigma2_tbsdata.add_TLV(4, matter.TLV.B2, sigma1.initiatorEphPubKey)
var TBSData2Signature = crypto.EC_P256().ecdsa_sign_sha256(session.get_pk(), sigma2_tbsdata.tlv2raw())
@ -512,7 +497,7 @@ class Matter_Commisioning_Context
# Compute S2K, p.175
var s2k_info = bytes().fromstring(self.S2K_Info)
var s2k_salt = session.get_ipk_group_key() + responderRandom + self.ResponderEph_pub + TranscriptHash
var s2k_salt = session.get_ipk_group_key() + responderRandom + session.__responder_pub + TranscriptHash
var s2k = crypto.HKDF_SHA256().derive(session.shared_secret, s2k_salt, s2k_info, 16)
tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
@ -530,8 +515,8 @@ class Matter_Commisioning_Context
var sigma2 = matter.Sigma2()
sigma2.responderRandom = responderRandom
sigma2.responderSessionId = self.future_local_session_id
sigma2.responderEphPubKey = self.ResponderEph_pub
sigma2.responderSessionId = session.__future_local_session_id
sigma2.responderEphPubKey = session.__responder_pub
sigma2.encrypted2 = TBEData2Encrypted
tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
var sigma2_raw = sigma2.tlv2raw()
@ -617,8 +602,8 @@ class Matter_Commisioning_Context
var sigma3_tbs = matter.TLV.Matter_TLV_struct()
sigma3_tbs.add_TLV(1, matter.TLV.B1, initiatorNOC)
sigma3_tbs.add_TLV(2, matter.TLV.B1, initiatorICAC)
sigma3_tbs.add_TLV(3, matter.TLV.B1, self.initiatorEph_pub)
sigma3_tbs.add_TLV(4, matter.TLV.B1, self.ResponderEph_pub)
sigma3_tbs.add_TLV(3, matter.TLV.B1, session.__initiator_pub)
sigma3_tbs.add_TLV(4, matter.TLV.B1, session.__responder_pub)
tasmota.log("MTR: * sigma3_tbs = " + str(sigma3_tbs), 4)
var sigma3_tbs_raw = sigma3_tbs.tlv2raw()
tasmota.log("MTR: * sigma3_tbs_raw= " + sigma3_tbs_raw.tohex(), 4)

View File

@ -0,0 +1,288 @@
#
# Matter_Fabric.be - Support for Matter Fabric
#
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import matter
#@ solidify:Matter_Fabric,weak
# for solidification only
class Matter_Expirable end
#################################################################################
# Matter_Fabric class
#
# Record all information for a fabric that has provisioned
#
# By convetion:
# attributes with a normal name are persisted (unless they are instances)
# attributes starting with '_' are not persisted
# attributes starting with '__' are cleared when a new session is created
#################################################################################
class Matter_Fabric : Matter_Expirable
static var _MAX_CASE = 5 # maximum number of concurrent CASE sessions per fabric
static var _GROUP_SND_INCR = 32 # counter increased when persisting
# Group Key Derivation
static var _GROUP_KEY = "GroupKey v1.0" # starting with double `_` means it's not writable
var _store # reference back to session store
# timestamp
var created
# fabric-index
var fabric_index # index number for fabrics, starts with `1`
var fabric_parent # index of the parent fabric, i.e. the fabric that triggered the provisioning (if nested)
# list of active sessions
var _sessions # only active CASE sessions that need to be persisted
# our own private key
var no_private_key # private key of the device certificate (generated at commissioning)
# NOC information
var root_ca_certificate # root certificate of the initiator
var noc # Node Operational Certificate in TLV Matter Certificate
var icac # Initiator CA Certificate in TLV Matter Certificate
var ipk_epoch_key # timestamp
# Information extracted from `noc`
var fabric_id # fabric identifier as bytes(8) little endian
var fabric_compressed # comrpessed fabric_id identifier, hashed with root_ca public key
var device_id # our own device id bytes(8) little endian
var fabric_label # set by UpdateFabricLabel
# global group counters (send)
var counter_group_data_snd # counter for group data
var counter_group_ctrl_snd # counter for group command
var _counter_group_data_snd_impl# implementation of counter_group_data_snd by matter.Counter()
var _counter_group_ctrl_snd_impl# implementation of counter_group_ctrl_snd by matter.Counter()
# Admin info extracted from NOC/ICAC
var admin_subject
var admin_vendor
#############################################################
def init(store)
import crypto
self._store = store
self._sessions = matter.Expirable_list()
self.fabric_label = ""
self.created = tasmota.rtc()['utc']
# init group counters
self._counter_group_data_snd_impl = matter.Counter()
self._counter_group_ctrl_snd_impl = matter.Counter()
self.counter_group_data_snd = self._counter_group_data_snd_impl.next() + self._GROUP_SND_INCR
self.counter_group_ctrl_snd = self._counter_group_data_snd_impl.next() + self._GROUP_SND_INCR
end
def get_noc() return self.noc end
def get_icac() return self.icac end
def get_ipk_epoch_key() return self.ipk_epoch_key end
def get_fabric_id() return self.fabric_id end
def get_device_id() return self.device_id end
def get_fabric_compressed() return self.fabric_compressed end
def get_fabric_label() return self.fabric_label end
def get_admin_subject() return self.admin_subject end
def get_admin_vendor() return self.admin_vendor end
def get_ca() return self.root_ca_certificate end
def get_fabric_index() return self.fabric_index end
def set_fabric_index(v) self.fabric_index = v end
#############################################################
# When hydrating from persistance, update counters
def hydrate_post()
# reset counter_snd to highest known.
# We advance it only in case it is actually used
# This avoids updaing counters on dead sessions
self._counter_group_data_snd_impl.reset(self.counter_group_data_snd)
self._counter_group_ctrl_snd_impl.reset(self.counter_group_ctrl_snd)
self.counter_group_data_snd = self._counter_group_data_snd_impl.val()
self.counter_group_ctrl_snd = self._counter_group_ctrl_snd_impl.val()
end
#############################################################
# Management of security counters
#############################################################
# Provide the next counter value, and update the last know persisted if needed
#
def counter_group_data_snd_next()
import string
var next = self._counter_group_data_snd_impl.next()
tasmota.log(string.format("MTR: . Counter_group_data_snd=%i", next), 3)
if matter.Counter.is_greater(next, self.counter_group_data_snd)
self.counter_group_data_snd = next + self._GROUP_SND_INCR
if self.does_persist()
# the persisted counter is behind the actual counter
self.save()
end
end
return next
end
#############################################################
# Provide the next counter value, and update the last know persisted if needed
#
def counter_group_ctrl_snd_next()
import string
var next = self._counter_group_ctrl_snd_impl.next()
tasmota.log(string.format("MTR: . Counter_group_ctrl_snd=%i", next), 3)
if matter.Counter.is_greater(next, self.counter_group_ctrl_snd)
self.counter_group_ctrl_snd = next + self._GROUP_SND_INCR
if self.does_persist()
# the persisted counter is behind the actual counter
self.save()
end
end
return next
end
#############################################################
# Called before removal
def log_new_fabric()
import string
tasmota.log(string.format("MTR: +Fabric fab='%s'", self.get_fabric_id().copy().reverse().tohex()), 2)
end
#############################################################
# Called before removal
def before_remove()
import string
tasmota.log(string.format("MTR: -Fabric fab='%s' (removed)", self.get_fabric_id().copy().reverse().tohex()), 2)
end
#############################################################
# Operational Group Key Derivation, 4.15.2, p.182
def get_ipk_group_key()
if self.ipk_epoch_key == nil || self.fabric_compressed == nil return nil end
import crypto
var hk = crypto.HKDF_SHA256()
var info = bytes().fromstring(self._GROUP_KEY)
var hash = hk.derive(self.ipk_epoch_key, self.fabric_compressed, info, 16)
return hash
end
def get_ca_pub()
var ca = self.root_ca_certificate
if ca
var m = matter.TLV.parse(ca)
return m.findsubval(9)
end
end
#############################################################
# add session to list of persisted sessions
# check for duplicates
def add_session(s)
if self._sessions.find(s) == nil
while size(self._sessions) >= self._MAX_CASE
self._sessions.remove(self._sessions.find(self.get_oldest_session()))
end
self._sessions.push(s)
end
end
def get_oldest_session() return self.get_old_recent_session(true) end
def get_newest_session() return self.get_old_recent_session(false) end
# get the oldest or most recent session (oldest indicates direction)
def get_old_recent_session(oldest)
if size(self._sessions) == 0 return nil end
var session = self._sessions[0]
var timestamp = session.last_used
var idx = 1
while idx < size(self._sessions)
var time2 = self._sessions[idx].last_used
if (oldest ? time2 < timestamp : time2 > timestamp)
session = self._sessions[idx]
timestamp = time2
end
idx += 1
end
return session
end
#############################################################
# Fabric::tojson()
#
# convert a single entry as json
# returns a JSON string
#############################################################
def tojson()
import json
import string
import introspect
self.persist_pre()
var keys = []
for k : introspect.members(self)
var v = introspect.get(self, k)
if type(v) != 'function' && k[0] != '_' keys.push(k) end
end
keys = matter.sort(keys)
var r = []
for k : keys
var v = introspect.get(self, k)
if v == nil continue end
if isinstance(v, bytes) v = "$$" + v.tob64() end # bytes
r.push(string.format("%s:%s", json.dump(str(k)), json.dump(v)))
end
# add sessions
var s = []
for sess : self._sessions.persistables()
s.push(sess.tojson())
end
if size(s) > 0
var s_list = "[" + s.concat(",") + "]"
r.push('"_sessions":' + s_list)
end
self.persist_post()
return "{" + r.concat(",") + "}"
end
#############################################################
# fromjson()
#
# reads a map and load arguments
# returns an new instance of fabric
# don't load embedded session, this is done by store
# i.e. ignore any key starting with '_'
#############################################################
static def fromjson(store, values)
import string
import introspect
var self = matter.Fabric(store)
for k : values.keys()
if k[0] == '_' continue end # ignore if key starts with '_'
var v = values[k]
# standard values
if type(v) == 'string'
if string.find(v, "0x") == 0 # treat as bytes
introspect.set(self, k, bytes().fromhex(v[2..]))
elif string.find(v, "$$") == 0 # treat as bytes
introspect.set(self, k, bytes().fromb64(v[2..]))
else
introspect.set(self, k, v)
end
else
introspect.set(self, k, v)
end
end
self.hydrate_post()
return self
end
end
matter.Fabric = Matter_Fabric

View File

@ -1,5 +1,5 @@
#
# Matter_Session.be - Support for Matter Sessions and Session Store
# Matter_Session.be - Support for Matter Sessions
#
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
#
@ -19,276 +19,11 @@
import matter
#@ solidify:Matter_Fabric,weak
#@ solidify:Matter_Session,weak
#@ solidify:Matter_Session_Store,weak
# for compilation
class Matter_Expirable end
#################################################################################
# Matter_Fabric class
#
# Record all information for a fabric that has provisioned
#
# By convetion:
# attributes with a normal name are persisted (unless they are instances)
# attributes starting with '_' are not persisted
# attributes starting with '__' are cleared when a new session is created
#################################################################################
class Matter_Fabric : Matter_Expirable
static var _MAX_CASE = 5 # maximum number of concurrent CASE sessions per fabric
static var _GROUP_SND_INCR = 32 # counter increased when persisting
# Group Key Derivation
static var _GROUP_KEY = "GroupKey v1.0" # starting with double `_` means it's not writable
var _store # reference back to session store
# timestamp
var created
# fabric-index
var fabric_index # index number for fabrics, starts with `1`
var fabric_parent # index of the parent fabric, i.e. the fabric that triggered the provisioning (if nested)
# list of active sessions
var _sessions # only active CASE sessions that need to be persisted
# our own private key
var no_private_key # private key of the device certificate (generated at commissioning)
# NOC information
var root_ca_certificate # root certificate of the initiator
var noc # Node Operational Certificate in TLV Matter Certificate
var icac # Initiator CA Certificate in TLV Matter Certificate
var ipk_epoch_key # timestamp
# Information extracted from `noc`
var fabric_id # fabric identifier as bytes(8) little endian
var fabric_compressed # comrpessed fabric_id identifier, hashed with root_ca public key
var device_id # our own device id bytes(8) little endian
var fabric_label # set by UpdateFabricLabel
# global group counters (send)
var counter_group_data_snd # counter for group data
var counter_group_ctrl_snd # counter for group command
var _counter_group_data_snd_impl# implementation of counter_group_data_snd by matter.Counter()
var _counter_group_ctrl_snd_impl# implementation of counter_group_ctrl_snd by matter.Counter()
# Admin info extracted from NOC/ICAC
var admin_subject
var admin_vendor
#############################################################
def init(store)
import crypto
self._store = store
self._sessions = matter.Expirable_list()
self.fabric_label = ""
self.created = tasmota.rtc()['utc']
# init group counters
self._counter_group_data_snd_impl = matter.Counter()
self._counter_group_ctrl_snd_impl = matter.Counter()
self.counter_group_data_snd = self._counter_group_data_snd_impl.next() + self._GROUP_SND_INCR
self.counter_group_ctrl_snd = self._counter_group_data_snd_impl.next() + self._GROUP_SND_INCR
end
def get_noc() return self.noc end
def get_icac() return self.icac end
def get_ipk_epoch_key() return self.ipk_epoch_key end
def get_fabric_id() return self.fabric_id end
def get_device_id() return self.device_id end
def get_fabric_compressed() return self.fabric_compressed end
def get_fabric_label() return self.fabric_label end
def get_admin_subject() return self.admin_subject end
def get_admin_vendor() return self.admin_vendor end
def get_ca() return self.root_ca_certificate end
def get_fabric_index() return self.fabric_index end
def set_fabric_index(v) self.fabric_index = v end
#############################################################
# When hydrating from persistance, update counters
def hydrate_post()
# reset counter_snd to highest known.
# We advance it only in case it is actually used
# This avoids updaing counters on dead sessions
self._counter_group_data_snd_impl.reset(self.counter_group_data_snd)
self._counter_group_ctrl_snd_impl.reset(self.counter_group_ctrl_snd)
self.counter_group_data_snd = self._counter_group_data_snd_impl.val()
self.counter_group_ctrl_snd = self._counter_group_ctrl_snd_impl.val()
end
#############################################################
# Management of security counters
#############################################################
# Provide the next counter value, and update the last know persisted if needed
#
def counter_group_data_snd_next()
import string
var next = self._counter_group_data_snd_impl.next()
tasmota.log(string.format("MTR: . Counter_group_data_snd=%i", next), 3)
if matter.Counter.is_greater(next, self.counter_group_data_snd)
self.counter_group_data_snd = next + self._GROUP_SND_INCR
if self.does_persist()
# the persisted counter is behind the actual counter
self.save()
end
end
return next
end
#############################################################
# Provide the next counter value, and update the last know persisted if needed
#
def counter_group_ctrl_snd_next()
import string
var next = self._counter_group_ctrl_snd_impl.next()
tasmota.log(string.format("MTR: . Counter_group_ctrl_snd=%i", next), 3)
if matter.Counter.is_greater(next, self.counter_group_ctrl_snd)
self.counter_group_ctrl_snd = next + self._GROUP_SND_INCR
if self.does_persist()
# the persisted counter is behind the actual counter
self.save()
end
end
return next
end
#############################################################
# Called before removal
def log_new_fabric()
import string
tasmota.log(string.format("MTR: +Fabric fab='%s'", self.get_fabric_id().copy().reverse().tohex()), 2)
end
#############################################################
# Called before removal
def before_remove()
import string
tasmota.log(string.format("MTR: -Fabric fab='%s' (removed)", self.get_fabric_id().copy().reverse().tohex()), 2)
end
#############################################################
# Operational Group Key Derivation, 4.15.2, p.182
def get_ipk_group_key()
if self.ipk_epoch_key == nil || self.fabric_compressed == nil return nil end
import crypto
var hk = crypto.HKDF_SHA256()
var info = bytes().fromstring(self._GROUP_KEY)
var hash = hk.derive(self.ipk_epoch_key, self.fabric_compressed, info, 16)
return hash
end
def get_ca_pub()
var ca = self.root_ca_certificate
if ca
var m = matter.TLV.parse(ca)
return m.findsubval(9)
end
end
#############################################################
# add session to list of persisted sessions
# check for duplicates
def add_session(s)
if self._sessions.find(s) == nil
while size(self._sessions) >= self._MAX_CASE
self._sessions.remove(self._sessions.find(self.get_oldest_session()))
end
self._sessions.push(s)
end
end
def get_oldest_session() return self.get_old_recent_session(true) end
def get_newest_session() return self.get_old_recent_session(false) end
# get the oldest or most recent session (oldest indicates direction)
def get_old_recent_session(oldest)
if size(self._sessions) == 0 return nil end
var session = self._sessions[0]
var timestamp = session.last_used
var idx = 1
while idx < size(self._sessions)
var time2 = self._sessions[idx].last_used
if (oldest ? time2 < timestamp : time2 > timestamp)
session = self._sessions[idx]
timestamp = time2
end
idx += 1
end
return session
end
#############################################################
# Fabric::tojson()
#
# convert a single entry as json
# returns a JSON string
#############################################################
def tojson()
import json
import string
import introspect
self.persist_pre()
var keys = []
for k : introspect.members(self)
var v = introspect.get(self, k)
if type(v) != 'function' && k[0] != '_' keys.push(k) end
end
keys = matter.sort(keys)
var r = []
for k : keys
var v = introspect.get(self, k)
if v == nil continue end
if isinstance(v, bytes) v = "$$" + v.tob64() end # bytes
r.push(string.format("%s:%s", json.dump(str(k)), json.dump(v)))
end
# add sessions
var s = []
for sess : self._sessions.persistables()
s.push(sess.tojson())
end
if size(s) > 0
var s_list = "[" + s.concat(",") + "]"
r.push('"_sessions":' + s_list)
end
self.persist_post()
return "{" + r.concat(",") + "}"
end
#############################################################
# fromjson()
#
# reads a map and load arguments
# returns an new instance of fabric
# don't load embedded session, this is done by store
# i.e. ignore any key starting with '_'
#############################################################
static def fromjson(store, values)
import string
import introspect
var self = matter.Fabric(store)
for k : values.keys()
if k[0] == '_' continue end # ignore if key starts with '_'
var v = values[k]
# standard values
if type(v) == 'string'
if string.find(v, "0x") == 0 # treat as bytes
introspect.set(self, k, bytes().fromhex(v[2..]))
elif string.find(v, "$$") == 0 # treat as bytes
introspect.set(self, k, bytes().fromb64(v[2..]))
else
introspect.set(self, k, v)
end
else
introspect.set(self, k, v)
end
end
self.hydrate_post()
return self
end
end
matter.Fabric = Matter_Fabric
#################################################################################
# Matter_Session class
#
@ -296,6 +31,7 @@ matter.Fabric = Matter_Fabric
# It can also be retrived by `source_node_id` when `local_session_id` is 0
#
# By convention, names starting with `_` are not persisted
# Names starting with `__` are cleared when session is closed (transition from PASE to CASE or CASE finished)
#################################################################################
class Matter_Session : Matter_Expirable
static var _PASE = 1 # PASE authentication in progress
@ -322,8 +58,8 @@ class Matter_Session : Matter_Expirable
var _counter_snd_impl # implementation of counter_snd by matter.Counter()
var _exchange_id # exchange id for locally initiated transaction, non-persistent
# keep track of last known IP/Port of the fabric
var _ip # IP of the last received packet
var _port # port of the last received packet
var _ip # IP of the last received packet (string)
var _port # port of the last received packet (int)
var _message_handler # pointer to the message handler for this session
# non-session counters
var _counter_insecure_rcv # counter for incoming messages
@ -335,10 +71,15 @@ class Matter_Session : Matter_Expirable
var attestation_challenge # Attestation challenge
var peer_node_id
# breadcrumb
var _breadcrumb # breadcrumb attribute for this session, prefix `__` so that it is not persisted and untouched
var _breadcrumb # breadcrumb attribute for this session, prefix `_` so that it is not persisted and untouched
# CASE
var resumption_id # bytes(16)
var shared_secret # ECDH shared secret used in CASE
var __responder_priv, __responder_pub
var __initiator_pub
# PASE
var __spake_cA # crypto.SPAKE2P_Matter object, cA
var __spake_Ke # crypto.SPAKE2P_Matter object, Ke
# Previous CASE messages for Transcript hash
var __Msg1, __Msg2
@ -702,372 +443,6 @@ end
matter.Session = Matter_Session
#################################################################################
#################################################################################
#################################################################################
# Matter_Session_Store class
#################################################################################
#################################################################################
#################################################################################
class Matter_Session_Store
var sessions
var fabrics # list of provisioned fabrics
static var _FABRICS = "_matter_fabrics.json"
#############################################################
def init()
self.sessions = matter.Expirable_list()
self.fabrics = matter.Expirable_list()
end
#############################################################
# add provisioned fabric
def add_fabric(fabric)
if !isinstance(fabric, matter.Fabric) raise "value_error", "must be of class matter.Fabric" end
if self.fabrics.find(fabric) == nil
self.remove_redundant_fabric(fabric)
self.fabrics.push(fabric)
end
end
#############################################################
# remove fabric
def remove_fabric(fabric)
var idx = 0
while idx < size(self.sessions)
if self.sessions[idx]._fabric == fabric
self.sessions.remove(idx)
else
idx += 1
end
end
self.fabrics.remove(self.fabrics.find(fabric)) # fail safe
end
#############################################################
# Remove redudant fabric
#
# remove all other fabrics that have the same:
# fabric_id / device_id
def remove_redundant_fabric(f)
var i = 0
while i < size(self.fabrics)
var fabric = self.fabrics[i]
if fabric != f && fabric.fabric_id == f.fabric_id && fabric.device_id == f.device_id
self.fabrics.remove(i)
else
i += 1
end
end
end
#############################################################
# Returns an iterator on active fabrics
def active_fabrics()
self.remove_expired() # clean before
return self.fabrics.persistables()
end
#############################################################
# Count active fabrics
#
# Count the number of commissionned fabrics, i.e. persisted
def count_active_fabrics()
self.remove_expired() # clean before
return self.fabrics.count_persistables()
end
#############################################################
# Find fabric by index number
#
def find_fabric_by_index(fabric_index)
for fab : self.active_fabrics()
if fab.get_fabric_index() == fabric_index
return fab
end
end
return nil
end
#############################################################
# Find children fabrics
#
# Find all children fabrics recursively and collate in array
# includes the parent fabric as first element
#
# Ex:
# matter_device.sessions.fabrics[1].fabric_parent = 1
# matter_device.sessions.find_children_fabrics(1)
#
def find_children_fabrics(parent_index)
if parent_index == nil return [] end
var ret = [ parent_index ]
def find_children_fabrics_inner(index)
for fab: self.active_fabrics()
if fab.fabric_parent == index
# protect against infinite loops
if ret.find() == nil
var sub_index = fab.fabric_index
ret.push(sub_index)
find_children_fabrics_inner(sub_index)
end
end
end
end
find_children_fabrics_inner(parent_index)
# ret contains a list of indices
return ret
end
#############################################################
# Next fabric-idx
#
# starts at `1`, computes the next available fabric-idx
def next_fabric_idx()
self.remove_expired() # clean before
var next_idx = 1
for fab: self.active_fabrics()
var fab_idx = fab.fabric_index
if type(fab_idx) == 'int' && fab_idx >= next_idx
next_idx = fab_idx + 1
end
end
return next_idx
end
#############################################################
# add session
def create_session(local_session_id, initiator_session_id)
var session = self.get_session_by_local_session_id(local_session_id)
if session != nil self.remove_session(session) end
session = matter.Session(self, local_session_id, initiator_session_id)
self.sessions.push(session)
return session
end
#############################################################
# add session
def add_session(s, expires_in_seconds)
if expires_in_seconds != nil
s.set_expire_in_seconds(expires_in_seconds)
end
self.sessions.push(s)
end
#############################################################
def get_session_by_local_session_id(id)
if id == nil return nil end
var sz = size(self.sessions)
var i = 0
var sessions = self.sessions
while i < sz
var session = sessions[i]
if session.local_session_id == id
session.update()
return session
end
i += 1
end
end
#############################################################
def get_session_by_source_node_id(nodeid)
if nodeid == nil return nil end
var sz = size(self.sessions)
var i = 0
var sessions = self.sessions
while i < sz
var session = sessions[i]
if session._source_node_id == nodeid
session.update()
return session
end
i += 1
end
end
#############################################################
# Remove session by reference
#
def remove_session(s)
var i = 0
var sessions = self.sessions
while i < size(self.sessions)
if sessions[i] == s
sessions.remove(i)
else
i += 1
end
end
end
#############################################################
# Generate a new local_session_id
def gen_local_session_id()
import crypto
while true
var candidate_local_session_id = crypto.random(2).get(0, 2)
if self.get_session_by_local_session_id(candidate_local_session_id) == nil
return candidate_local_session_id
end
end
end
#############################################################
# remove_expired
#
def remove_expired()
self.sessions.every_second()
self.fabrics.every_second()
end
#############################################################
# call remove_expired every second
#
def every_second()
self.remove_expired()
end
#############################################################
# find or create a session for unencrypted traffic
# expires in `expire` seconds
def find_session_source_id_unsecure(source_node_id, expire)
var session = self.get_session_by_source_node_id(source_node_id)
if session == nil
session = matter.Session(self, 0, 0)
session._source_node_id = source_node_id
self.sessions.push(session)
session.set_expire_in_seconds(expire)
end
session.update()
return session
end
#############################################################
# find session by resumption id
def find_session_by_resumption_id(resumption_id)
import string
if !resumption_id return nil end
var i = 0
var sessions = self.sessions
while i < size(sessions)
var session = sessions[i]
tasmota.log(string.format("MTR: session.resumption_id=%s vs %s", str(session.resumption_id), str(resumption_id)))
if session.resumption_id == resumption_id && session.shared_secret != nil
tasmota.log(string.format("MTR: session.shared_secret=%s", str(session.shared_secret)))
session.update()
return session
end
i += 1
end
end
#############################################################
# list of sessions that are active, i.e. have been
# successfully commissioned
#
def sessions_active()
var ret = []
var idx = 0
while idx < size(self.sessions)
var session = self.sessions[idx]
if session.get_device_id() && session.get_fabric_id()
ret.push(session)
end
idx += 1
end
return ret
end
#############################################################
def save_fabrics()
import json
self.remove_expired() # clean before saving
var sessions_saved = 0
var fabs = []
for f : self.fabrics.persistables()
for _ : f._sessions.persistables() sessions_saved += 1 end # count persitable sessions
fabs.push(f.tojson())
end
var fabs_size = size(fabs)
fabs = "[" + fabs.concat(",") + "]"
try
import string
var f = open(self._FABRICS, "w")
f.write(fabs)
f.close()
tasmota.log(string.format("MTR: =Saved %i fabric(s) and %i session(s)", fabs_size, sessions_saved), 2)
except .. as e, m
tasmota.log("MTR: Session_Store::save Exception:" + str(e) + "|" + str(m), 2)
end
end
#############################################################
# load fabrics and associated sessions
def load_fabrics()
import string
try
self.sessions = matter.Expirable_list() # remove any left-over
self.fabrics = matter.Expirable_list() # remove any left-over
var f = open(self._FABRICS)
var file_content = f.read()
f.close()
import json
var file_json = json.load(file_content)
file_content = nil
tasmota.gc() # clean-up a potential long string
for v : file_json # iterate on values
# read fabric
var fabric = matter.Fabric.fromjson(self, v)
fabric.set_no_expiration()
fabric.set_persist(true)
# iterate on sessions
var sessions_json = v.find("_sessions", [])
for sess_json : sessions_json
var session = matter.Session.fromjson(self, sess_json, fabric)
if session != nil
session.set_no_expiration()
session.set_persist(true)
self.add_session(session)
fabric.add_session(session)
end
end
self.fabrics.push(fabric)
end
tasmota.log(string.format("MTR: Loaded %i fabric(s)", size(self.fabrics)), 2)
except .. as e, m
if e != "io_error"
tasmota.log("MTR: Session_Store::load Exception:" + str(e) + "|" + str(m), 2)
end
end
# persistables are normally not expiring
# if self.remove_expired() # clean after load
# self.save_fabrics()
# end
end
#############################################################
def create_fabric()
var fabric = matter.Fabric(self)
return fabric
end
end
matter.Session_Store = Matter_Session_Store
#-
# Unit test

View File

@ -0,0 +1,391 @@
#
# Matter_Session_Store.be - Support for Matter Session Store
#
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import matter
#@ solidify:Matter_Session_Store,weak
# for compilation
class Matter_Expirable end
#################################################################################
#################################################################################
#################################################################################
# Matter_Session_Store class
#################################################################################
#################################################################################
#################################################################################
class Matter_Session_Store
var sessions
var fabrics # list of provisioned fabrics
static var _FABRICS = "_matter_fabrics.json"
#############################################################
def init()
self.sessions = matter.Expirable_list()
self.fabrics = matter.Expirable_list()
end
#############################################################
# add provisioned fabric
def add_fabric(fabric)
if !isinstance(fabric, matter.Fabric) raise "value_error", "must be of class matter.Fabric" end
if self.fabrics.find(fabric) == nil
self.remove_redundant_fabric(fabric)
self.fabrics.push(fabric)
end
end
#############################################################
# remove fabric
def remove_fabric(fabric)
var idx = 0
while idx < size(self.sessions)
if self.sessions[idx]._fabric == fabric
self.sessions.remove(idx)
else
idx += 1
end
end
self.fabrics.remove(self.fabrics.find(fabric)) # fail safe
end
#############################################################
# Remove redudant fabric
#
# remove all other fabrics that have the same:
# fabric_id / device_id
def remove_redundant_fabric(f)
var i = 0
while i < size(self.fabrics)
var fabric = self.fabrics[i]
if fabric != f && fabric.fabric_id == f.fabric_id && fabric.device_id == f.device_id
self.fabrics.remove(i)
else
i += 1
end
end
end
#############################################################
# Returns an iterator on active fabrics
def active_fabrics()
self.remove_expired() # clean before
return self.fabrics.persistables()
end
#############################################################
# Count active fabrics
#
# Count the number of commissionned fabrics, i.e. persisted
def count_active_fabrics()
self.remove_expired() # clean before
return self.fabrics.count_persistables()
end
#############################################################
# Find fabric by index number
#
def find_fabric_by_index(fabric_index)
for fab : self.active_fabrics()
if fab.get_fabric_index() == fabric_index
return fab
end
end
return nil
end
#############################################################
# Find children fabrics
#
# Find all children fabrics recursively and collate in array
# includes the parent fabric as first element
#
# Ex:
# matter_device.sessions.fabrics[1].fabric_parent = 1
# matter_device.sessions.find_children_fabrics(1)
#
def find_children_fabrics(parent_index)
if parent_index == nil return [] end
var ret = [ parent_index ]
def find_children_fabrics_inner(index)
for fab: self.active_fabrics()
if fab.fabric_parent == index
# protect against infinite loops
if ret.find() == nil
var sub_index = fab.fabric_index
ret.push(sub_index)
find_children_fabrics_inner(sub_index)
end
end
end
end
find_children_fabrics_inner(parent_index)
# ret contains a list of indices
return ret
end
#############################################################
# Next fabric-idx
#
# starts at `1`, computes the next available fabric-idx
def next_fabric_idx()
self.remove_expired() # clean before
var next_idx = 1
for fab: self.active_fabrics()
var fab_idx = fab.fabric_index
if type(fab_idx) == 'int' && fab_idx >= next_idx
next_idx = fab_idx + 1
end
end
return next_idx
end
#############################################################
# add session
def create_session(local_session_id, initiator_session_id)
var session = self.get_session_by_local_session_id(local_session_id)
if session != nil self.remove_session(session) end
session = matter.Session(self, local_session_id, initiator_session_id)
self.sessions.push(session)
return session
end
#############################################################
# add session
def add_session(s, expires_in_seconds)
if expires_in_seconds != nil
s.set_expire_in_seconds(expires_in_seconds)
end
self.sessions.push(s)
end
#############################################################
def get_session_by_local_session_id(id)
if id == nil return nil end
var sz = size(self.sessions)
var i = 0
var sessions = self.sessions
while i < sz
var session = sessions[i]
if session.local_session_id == id
session.update()
return session
end
i += 1
end
end
#############################################################
def get_session_by_source_node_id(nodeid)
if nodeid == nil return nil end
var sz = size(self.sessions)
var i = 0
var sessions = self.sessions
while i < sz
var session = sessions[i]
if session._source_node_id == nodeid
session.update()
return session
end
i += 1
end
end
#############################################################
# Remove session by reference
#
def remove_session(s)
var i = 0
var sessions = self.sessions
while i < size(self.sessions)
if sessions[i] == s
sessions.remove(i)
else
i += 1
end
end
end
#############################################################
# Generate a new local_session_id
def gen_local_session_id()
import crypto
while true
var candidate_local_session_id = crypto.random(2).get(0, 2)
if self.get_session_by_local_session_id(candidate_local_session_id) == nil
return candidate_local_session_id
end
end
end
#############################################################
# remove_expired
#
def remove_expired()
self.sessions.every_second()
self.fabrics.every_second()
end
#############################################################
# call remove_expired every second
#
def every_second()
self.remove_expired()
end
#############################################################
# find or create a session for unencrypted traffic
# expires in `expire` seconds
def find_session_source_id_unsecure(source_node_id, expire)
var session = self.get_session_by_source_node_id(source_node_id)
if session == nil
session = matter.Session(self, 0, 0)
session._source_node_id = source_node_id
self.sessions.push(session)
session.set_expire_in_seconds(expire)
end
session.update()
return session
end
#############################################################
# find session by resumption id
def find_session_by_resumption_id(resumption_id)
import string
if !resumption_id return nil end
var i = 0
var sessions = self.sessions
while i < size(sessions)
var session = sessions[i]
tasmota.log(string.format("MTR: session.resumption_id=%s vs %s", str(session.resumption_id), str(resumption_id)))
if session.resumption_id == resumption_id && session.shared_secret != nil
tasmota.log(string.format("MTR: session.shared_secret=%s", str(session.shared_secret)))
session.update()
return session
end
i += 1
end
end
#############################################################
# list of sessions that are active, i.e. have been
# successfully commissioned
#
def sessions_active()
var ret = []
var idx = 0
while idx < size(self.sessions)
var session = self.sessions[idx]
if session.get_device_id() && session.get_fabric_id()
ret.push(session)
end
idx += 1
end
return ret
end
#############################################################
def save_fabrics()
import json
self.remove_expired() # clean before saving
var sessions_saved = 0
var fabs = []
for f : self.fabrics.persistables()
for _ : f._sessions.persistables() sessions_saved += 1 end # count persitable sessions
fabs.push(f.tojson())
end
var fabs_size = size(fabs)
fabs = "[" + fabs.concat(",") + "]"
try
import string
var f = open(self._FABRICS, "w")
f.write(fabs)
f.close()
tasmota.log(string.format("MTR: =Saved %i fabric(s) and %i session(s)", fabs_size, sessions_saved), 2)
except .. as e, m
tasmota.log("MTR: Session_Store::save Exception:" + str(e) + "|" + str(m), 2)
end
end
#############################################################
# load fabrics and associated sessions
def load_fabrics()
import string
try
self.sessions = matter.Expirable_list() # remove any left-over
self.fabrics = matter.Expirable_list() # remove any left-over
var f = open(self._FABRICS)
var file_content = f.read()
f.close()
import json
var file_json = json.load(file_content)
file_content = nil
tasmota.gc() # clean-up a potential long string
for v : file_json # iterate on values
# read fabric
var fabric = matter.Fabric.fromjson(self, v)
fabric.set_no_expiration()
fabric.set_persist(true)
# iterate on sessions
var sessions_json = v.find("_sessions", [])
for sess_json : sessions_json
var session = matter.Session.fromjson(self, sess_json, fabric)
if session != nil
session.set_no_expiration()
session.set_persist(true)
self.add_session(session)
fabric.add_session(session)
end
end
self.fabrics.push(fabric)
end
tasmota.log(string.format("MTR: Loaded %i fabric(s)", size(self.fabrics)), 2)
except .. as e, m
if e != "io_error"
tasmota.log("MTR: Session_Store::load Exception:" + str(e) + "|" + str(m), 2)
end
end
# persistables are normally not expiring
# if self.remove_expired() # clean after load
# self.save_fabrics()
# end
end
#############################################################
def create_fabric()
var fabric = matter.Fabric(self)
return fabric
end
end
matter.Session_Store = Matter_Session_Store

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff