mirror of https://github.com/arendst/Tasmota.git
Matter refactor PASE parameters (#18406)
This commit is contained in:
parent
3da96a55d5
commit
0c0ab855f3
|
@ -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_IM_Data.h"
|
||||||
#include "solidify/solidified_Matter_UDPServer.h"
|
#include "solidify/solidified_Matter_UDPServer.h"
|
||||||
#include "solidify/solidified_Matter_Expirable.h"
|
#include "solidify/solidified_Matter_Expirable.h"
|
||||||
|
#include "solidify/solidified_Matter_Fabric.h"
|
||||||
#include "solidify/solidified_Matter_Session.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_Data.h"
|
||||||
#include "solidify/solidified_Matter_Commissioning.h"
|
#include "solidify/solidified_Matter_Commissioning.h"
|
||||||
#include "solidify/solidified_Matter_Message.h"
|
#include "solidify/solidified_Matter_Message.h"
|
||||||
|
|
|
@ -35,38 +35,21 @@ class Matter_Commisioning_Context
|
||||||
|
|
||||||
var responder # reference to the caller, sending packets
|
var responder # reference to the caller, sending packets
|
||||||
var device # root device object
|
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)
|
def init(responder)
|
||||||
import crypto
|
import crypto
|
||||||
self.responder = responder
|
self.responder = responder
|
||||||
self.device = responder.device
|
self.device = responder.device
|
||||||
# generate y once
|
|
||||||
self.y = crypto.random(32)
|
|
||||||
end
|
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
|
import string
|
||||||
# create session object
|
# 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)
|
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)
|
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
|
end
|
||||||
|
|
||||||
def process_incoming(msg)
|
def process_incoming(msg)
|
||||||
|
@ -126,6 +109,7 @@ class Matter_Commisioning_Context
|
||||||
def parse_PBKDFParamRequest(msg)
|
def parse_PBKDFParamRequest(msg)
|
||||||
import crypto
|
import crypto
|
||||||
import string
|
import string
|
||||||
|
var session = msg.session
|
||||||
# sanity checks
|
# sanity checks
|
||||||
if msg.opcode != 0x20 || msg.local_session_id != 0 || msg.protocol_id != 0
|
if msg.opcode != 0x20 || msg.local_session_id != 0 || msg.protocol_id != 0
|
||||||
tasmota.log("MTR: invalid PBKDFParamRequest message", 2)
|
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)
|
var pbkdfparamreq = matter.PBKDFParamRequest().parse(msg.raw, msg.app_payload_idx)
|
||||||
msg.session.set_mode_PASE()
|
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
|
# sanity check for PBKDFParamRequest
|
||||||
if pbkdfparamreq.passcodeId != 0
|
if pbkdfparamreq.passcodeId != 0
|
||||||
|
@ -147,9 +131,9 @@ class Matter_Commisioning_Context
|
||||||
end
|
end
|
||||||
|
|
||||||
# record the initiator_session_id
|
# record the initiator_session_id
|
||||||
self.future_initiator_session_id = pbkdfparamreq.initiator_session_id
|
session.__future_initiator_session_id = pbkdfparamreq.initiator_session_id
|
||||||
self.future_local_session_id = self.device.sessions.gen_local_session_id()
|
session.__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)
|
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", session.__future_local_session_id, msg.remote_ip, msg.remote_port), 2)
|
||||||
|
|
||||||
# prepare response
|
# prepare response
|
||||||
var pbkdfparamresp = matter.PBKDFParamResponse()
|
var pbkdfparamresp = matter.PBKDFParamResponse()
|
||||||
|
@ -157,14 +141,14 @@ class Matter_Commisioning_Context
|
||||||
pbkdfparamresp.initiatorRandom = pbkdfparamreq.initiatorRandom
|
pbkdfparamresp.initiatorRandom = pbkdfparamreq.initiatorRandom
|
||||||
# generate 32 bytes random
|
# generate 32 bytes random
|
||||||
pbkdfparamresp.responderRandom = crypto.random(32)
|
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_salt = self.device.commissioning_salt
|
||||||
pbkdfparamresp.pbkdf_parameters_iterations = self.device.commissioning_iterations
|
pbkdfparamresp.pbkdf_parameters_iterations = self.device.commissioning_iterations
|
||||||
tasmota.log("MTR: pbkdfparamresp: " + str(matter.inspect(pbkdfparamresp)), 4)
|
tasmota.log("MTR: pbkdfparamresp: " + str(matter.inspect(pbkdfparamresp)), 4)
|
||||||
var pbkdfparamresp_raw = pbkdfparamresp.tlv2raw()
|
var pbkdfparamresp_raw = pbkdfparamresp.tlv2raw()
|
||||||
tasmota.log("MTR: pbkdfparamresp_raw: " + pbkdfparamresp_raw.tohex(), 4)
|
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 resp = msg.build_response(0x21 #-PBKDR Response-#, true)
|
||||||
var raw = resp.encode_frame(pbkdfparamresp_raw)
|
var raw = resp.encode_frame(pbkdfparamresp_raw)
|
||||||
|
@ -175,6 +159,7 @@ class Matter_Commisioning_Context
|
||||||
|
|
||||||
def parse_Pake1(msg)
|
def parse_Pake1(msg)
|
||||||
import crypto
|
import crypto
|
||||||
|
var session = msg.session
|
||||||
# sanity checks
|
# sanity checks
|
||||||
if msg.opcode != 0x22 || msg.local_session_id != 0 || msg.protocol_id != 0
|
if msg.opcode != 0x22 || msg.local_session_id != 0 || msg.protocol_id != 0
|
||||||
tasmota.log("MTR: invalid Pake1 message", 2)
|
tasmota.log("MTR: invalid Pake1 message", 2)
|
||||||
|
@ -184,67 +169,68 @@ class Matter_Commisioning_Context
|
||||||
end
|
end
|
||||||
var pake1 = matter.Pake1().parse(msg.raw, msg.app_payload_idx)
|
var pake1 = matter.Pake1().parse(msg.raw, msg.app_payload_idx)
|
||||||
|
|
||||||
self.pA = pake1.pA
|
var pA = pake1.pA
|
||||||
# tasmota.log("MTR: received pA=" + self.pA.tohex(), 4)
|
# tasmota.log("MTR: received pA=" + pA.tohex(), 4)
|
||||||
|
|
||||||
|
|
||||||
# tasmota.log("MTR: spake: " + matter.inspect(self.spake), 4)
|
|
||||||
# instanciate SPAKE
|
# instanciate SPAKE
|
||||||
# for testing purpose, we don't send `w1` to make sure
|
# 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
|
# compute pB
|
||||||
self.spake.compute_pB(self.y)
|
spake.compute_pB(y)
|
||||||
self.pB = self.spake.pB
|
# tasmota.log("MTR: y=" + y.tohex(), 4)
|
||||||
# tasmota.log("MTR: y=" + self.y.tohex(), 4)
|
# tasmota.log("MTR: pb=" + spake.pB.tohex(), 4)
|
||||||
# tasmota.log("MTR: pb=" + self.pB.tohex(), 4)
|
|
||||||
|
|
||||||
# compute ZV
|
# compute ZV
|
||||||
self.spake.compute_ZV_verifier(self.pA)
|
spake.compute_ZV_verifier(pA)
|
||||||
# tasmota.log("MTR: Z=" + self.spake.Z.tohex(), 4)
|
# tasmota.log("MTR: Z=" + spake.Z.tohex(), 4)
|
||||||
# tasmota.log("MTR: V=" + self.spake.V.tohex(), 4)
|
# tasmota.log("MTR: V=" + spake.V.tohex(), 4)
|
||||||
|
|
||||||
var context = crypto.SHA256()
|
var context = crypto.SHA256()
|
||||||
context.update(bytes().fromstring(self.Matter_Context_Prefix))
|
context.update(bytes().fromstring(self.Matter_Context_Prefix))
|
||||||
context.update(self.PBKDFParamRequest)
|
context.update(session.__Msg1)
|
||||||
context.update(self.PBKDFParamResponse)
|
context.update(session.__Msg2)
|
||||||
var context_hash = context.out()
|
var context_hash = context.out()
|
||||||
|
|
||||||
# tasmota.log("MTR: Context=" + context_hash.tohex(), 4)
|
# tasmota.log("MTR: Context=" + context_hash.tohex(), 4)
|
||||||
|
|
||||||
# add pA
|
# add pA
|
||||||
self.spake.pA = self.pA
|
spake.pA = pA
|
||||||
|
|
||||||
self.spake.set_context(context_hash)
|
spake.set_context(context_hash)
|
||||||
self.spake.compute_TT_hash(true) # `true` to indicate it's Matter variant to SPAKE2+
|
spake.compute_TT_hash(true) # `true` to indicate it's Matter variant to SPAKE2+
|
||||||
|
|
||||||
# tasmota.log("MTR: ------------------------------", 4)
|
# tasmota.log("MTR: ------------------------------", 4)
|
||||||
# tasmota.log("MTR: Context = " + self.spake.Context.tohex(), 4)
|
# tasmota.log("MTR: Context = " + spake.Context.tohex(), 4)
|
||||||
# tasmota.log("MTR: M = " + self.spake.M.tohex(), 4)
|
# tasmota.log("MTR: M = " + spake.M.tohex(), 4)
|
||||||
# tasmota.log("MTR: N = " + self.spake.N.tohex(), 4)
|
# tasmota.log("MTR: N = " + spake.N.tohex(), 4)
|
||||||
# tasmota.log("MTR: pA = " + self.spake.pA.tohex(), 4)
|
# tasmota.log("MTR: pA = " + spake.pA.tohex(), 4)
|
||||||
# tasmota.log("MTR: pB = " + self.spake.pB.tohex(), 4)
|
# tasmota.log("MTR: pB = " + spake.pB.tohex(), 4)
|
||||||
# tasmota.log("MTR: Z = " + self.spake.Z.tohex(), 4)
|
# tasmota.log("MTR: Z = " + spake.Z.tohex(), 4)
|
||||||
# tasmota.log("MTR: V = " + self.spake.V.tohex(), 4)
|
# tasmota.log("MTR: V = " + spake.V.tohex(), 4)
|
||||||
# tasmota.log("MTR: w0 = " + self.spake.w0.tohex(), 4)
|
# tasmota.log("MTR: w0 = " + spake.w0.tohex(), 4)
|
||||||
# tasmota.log("MTR: ------------------------------", 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: KcA =" + spake.KcA.tohex(), 4)
|
||||||
# tasmota.log("MTR: KcB =" + self.spake.KcB.tohex(), 4)
|
# tasmota.log("MTR: KcB =" + spake.KcB.tohex(), 4)
|
||||||
# tasmota.log("MTR: K_shared=" + self.spake.K_shared.tohex(), 4)
|
# tasmota.log("MTR: K_shared=" + spake.K_shared.tohex(), 4)
|
||||||
# tasmota.log("MTR: Ke =" + self.spake.Ke.tohex(), 4)
|
# tasmota.log("MTR: Ke =" + spake.Ke.tohex(), 4)
|
||||||
self.cB = self.spake.cB
|
# tasmota.log("MTR: cB=" + spake.cB.tohex(), 4)
|
||||||
self.Ke = self.spake.Ke
|
|
||||||
# tasmota.log("MTR: cB=" + self.cB.tohex(), 4)
|
|
||||||
|
|
||||||
var pake2 = matter.Pake2()
|
var pake2 = matter.Pake2()
|
||||||
pake2.pB = self.pB
|
pake2.pB = spake.pB
|
||||||
pake2.cB = self.cB
|
pake2.cB = spake.cB
|
||||||
# tasmota.log("MTR: pake2: " + matter.inspect(pake2), 4)
|
# tasmota.log("MTR: pake2: " + matter.inspect(pake2), 4)
|
||||||
var pake2_raw = pake2.tlv2raw()
|
var pake2_raw = pake2.tlv2raw()
|
||||||
# tasmota.log("MTR: pake2_raw: " + pake2_raw.tohex(), 4)
|
# tasmota.log("MTR: pake2_raw: " + pake2_raw.tohex(), 4)
|
||||||
|
|
||||||
|
session.__spake_cA = spake.cA
|
||||||
|
session.__spake_Ke = spake.Ke
|
||||||
|
|
||||||
# now package the response message
|
# now package the response message
|
||||||
var resp = msg.build_response(0x23 #-pake-2-#, true) # no reliable flag
|
var resp = msg.build_response(0x23 #-pake-2-#, true) # no reliable flag
|
||||||
|
@ -256,6 +242,7 @@ class Matter_Commisioning_Context
|
||||||
|
|
||||||
def parse_Pake3(msg)
|
def parse_Pake3(msg)
|
||||||
import crypto
|
import crypto
|
||||||
|
var session = msg.session
|
||||||
# sanity checks
|
# sanity checks
|
||||||
if msg.opcode != 0x24 || msg.local_session_id != 0 || msg.protocol_id != 0
|
if msg.opcode != 0x24 || msg.local_session_id != 0 || msg.protocol_id != 0
|
||||||
tasmota.log("MTR: invalid Pake3 message", 2)
|
tasmota.log("MTR: invalid Pake3 message", 2)
|
||||||
|
@ -265,11 +252,11 @@ class Matter_Commisioning_Context
|
||||||
end
|
end
|
||||||
var pake3 = matter.Pake3().parse(msg.raw, msg.app_payload_idx)
|
var pake3 = matter.Pake3().parse(msg.raw, msg.app_payload_idx)
|
||||||
|
|
||||||
self.cA = pake3.cA
|
var cA = pake3.cA
|
||||||
# tasmota.log("MTR: received cA=" + self.cA.tohex(), 4)
|
# tasmota.log("MTR: received cA=" + cA.tohex(), 4)
|
||||||
|
|
||||||
# check the value against computed
|
# 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: invalid cA received", 2)
|
||||||
tasmota.log("MTR: StatusReport(General Code: FAILURE, ProtocolId: SECURE_CHANNEL, ProtocolCode: INVALID_PARAMETER)", 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)
|
var raw = self.send_status_report(msg, 0x01, 0x0000, 0x0002, false)
|
||||||
|
@ -277,23 +264,23 @@ class Matter_Commisioning_Context
|
||||||
end
|
end
|
||||||
|
|
||||||
# send PakeFinished and compute session key
|
# send PakeFinished and compute session key
|
||||||
self.created = tasmota.rtc()['utc']
|
var created = tasmota.rtc()['utc']
|
||||||
var session_keys = crypto.HKDF_SHA256().derive(self.Ke, bytes(), bytes().fromstring(self.SEKeys_Info), 48)
|
var session_keys = crypto.HKDF_SHA256().derive(session.__spake_Ke, bytes(), bytes().fromstring(self.SEKeys_Info), 48)
|
||||||
self.I2RKey = session_keys[0..15]
|
var I2RKey = session_keys[0..15]
|
||||||
self.R2IKey = session_keys[16..31]
|
var R2IKey = session_keys[16..31]
|
||||||
self.AttestationChallenge = session_keys[32..47]
|
var AttestationChallenge = session_keys[32..47]
|
||||||
|
|
||||||
# tasmota.log("MTR: ******************************", 4)
|
# tasmota.log("MTR: ******************************", 4)
|
||||||
# tasmota.log("MTR: session_keys=" + session_keys.tohex(), 4)
|
# tasmota.log("MTR: session_keys=" + session_keys.tohex(), 4)
|
||||||
# tasmota.log("MTR: I2RKey =" + self.I2RKey.tohex(), 4)
|
# tasmota.log("MTR: I2RKey =" + I2RKey.tohex(), 4)
|
||||||
# tasmota.log("MTR: R2IKey =" + self.R2IKey.tohex(), 4)
|
# tasmota.log("MTR: R2IKey =" + R2IKey.tohex(), 4)
|
||||||
# tasmota.log("MTR: AC =" + self.AttestationChallenge.tohex(), 4)
|
# tasmota.log("MTR: AC =" + AttestationChallenge.tohex(), 4)
|
||||||
# tasmota.log("MTR: ******************************", 4)
|
# tasmota.log("MTR: ******************************", 4)
|
||||||
|
|
||||||
# StatusReport(GeneralCode: SUCCESS, ProtocolId: SECURE_CHANNEL, ProtocolCode: SESSION_ESTABLISHMENT_SUCCESS)
|
# StatusReport(GeneralCode: SUCCESS, ProtocolId: SECURE_CHANNEL, ProtocolCode: SESSION_ESTABLISHMENT_SUCCESS)
|
||||||
var raw = self.send_status_report(msg, 0x00, 0x0000, 0x0000, false)
|
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
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -323,6 +310,7 @@ class Matter_Commisioning_Context
|
||||||
def parse_Sigma1(msg)
|
def parse_Sigma1(msg)
|
||||||
import crypto
|
import crypto
|
||||||
import string
|
import string
|
||||||
|
var session = msg.session
|
||||||
# sanity checks
|
# sanity checks
|
||||||
if msg.opcode != 0x30 || msg.local_session_id != 0 || msg.protocol_id != 0
|
if msg.opcode != 0x30 || msg.local_session_id != 0 || msg.protocol_id != 0
|
||||||
# tasmota.log("MTR: invalid Sigma1 message", 2)
|
# 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)
|
var sigma1 = matter.Sigma1().parse(msg.raw, msg.app_payload_idx)
|
||||||
tasmota.log(string.format("MTR: sigma1=%s", matter.inspect(sigma1)), 4)
|
tasmota.log(string.format("MTR: sigma1=%s", matter.inspect(sigma1)), 4)
|
||||||
|
|
||||||
self.initiatorEph_pub = sigma1.initiatorEphPubKey
|
session.__initiator_pub = sigma1.initiatorEphPubKey
|
||||||
|
|
||||||
# find session
|
# find session
|
||||||
var is_resumption = (sigma1.resumptionID != nil && sigma1.initiatorResumeMIC != nil)
|
var is_resumption = (sigma1.resumptionID != nil && sigma1.initiatorResumeMIC != nil)
|
||||||
|
@ -342,7 +330,6 @@ class Matter_Commisioning_Context
|
||||||
is_resumption = false
|
is_resumption = false
|
||||||
|
|
||||||
# Check that it's a resumption
|
# Check that it's a resumption
|
||||||
var session = msg.session
|
|
||||||
var session_resumption
|
var session_resumption
|
||||||
if is_resumption
|
if is_resumption
|
||||||
session_resumption = self.device.sessions.find_session_by_resumption_id(sigma1.resumptionID)
|
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.set_mode_CASE()
|
||||||
session.__future_initiator_session_id = sigma1.initiator_session_id # update initiator_session_id
|
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()
|
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)
|
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
|
# 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_initiator_session_id = sigma1.initiator_session_id # update initiator_session_id
|
||||||
session.__future_local_session_id = self.device.sessions.gen_local_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)
|
||||||
tasmota.log(string.format("MTR: +Session (%6i) from '[%s]:%i'", self.future_local_session_id, msg.remote_ip, msg.remote_port), 2)
|
|
||||||
|
|
||||||
tasmota.log("MTR: fabric="+matter.inspect(session._fabric), 4)
|
tasmota.log("MTR: fabric="+matter.inspect(session._fabric), 4)
|
||||||
tasmota.log("MTR: no_private_key="+session._fabric.no_private_key.tohex(), 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
|
# Compute Sigma2, p.162
|
||||||
session.resumption_id = crypto.random(16)
|
session.resumption_id = crypto.random(16)
|
||||||
self.ResponderEph_priv = crypto.random(32)
|
session.__responder_priv = crypto.random(32)
|
||||||
self.ResponderEph_pub = crypto.EC_P256().public_key(self.ResponderEph_priv)
|
session.__responder_pub = crypto.EC_P256().public_key(session.__responder_priv)
|
||||||
tasmota.log("MTR: ResponderEph_priv ="+self.ResponderEph_priv.tohex(), 4)
|
tasmota.log("MTR: ResponderEph_priv ="+session.__responder_priv.tohex(), 4)
|
||||||
tasmota.log("MTR: ResponderEph_pub ="+self.ResponderEph_pub.tohex(), 4)
|
tasmota.log("MTR: ResponderEph_pub ="+session.__responder_pub.tohex(), 4)
|
||||||
var responderRandom = crypto.random(32)
|
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()
|
var sigma2_tbsdata = matter.TLV.Matter_TLV_struct()
|
||||||
sigma2_tbsdata.add_TLV(1, matter.TLV.B2, session.get_noc())
|
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(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)
|
sigma2_tbsdata.add_TLV(4, matter.TLV.B2, sigma1.initiatorEphPubKey)
|
||||||
|
|
||||||
var TBSData2Signature = crypto.EC_P256().ecdsa_sign_sha256(session.get_pk(), sigma2_tbsdata.tlv2raw())
|
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
|
# Compute S2K, p.175
|
||||||
var s2k_info = bytes().fromstring(self.S2K_Info)
|
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)
|
var s2k = crypto.HKDF_SHA256().derive(session.shared_secret, s2k_salt, s2k_info, 16)
|
||||||
tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
|
tasmota.log("MTR: * SharedSecret = " + session.shared_secret.tohex(), 4)
|
||||||
|
@ -530,8 +515,8 @@ class Matter_Commisioning_Context
|
||||||
|
|
||||||
var sigma2 = matter.Sigma2()
|
var sigma2 = matter.Sigma2()
|
||||||
sigma2.responderRandom = responderRandom
|
sigma2.responderRandom = responderRandom
|
||||||
sigma2.responderSessionId = self.future_local_session_id
|
sigma2.responderSessionId = session.__future_local_session_id
|
||||||
sigma2.responderEphPubKey = self.ResponderEph_pub
|
sigma2.responderEphPubKey = session.__responder_pub
|
||||||
sigma2.encrypted2 = TBEData2Encrypted
|
sigma2.encrypted2 = TBEData2Encrypted
|
||||||
tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
|
tasmota.log("MTR: sigma2: " + matter.inspect(sigma2), 4)
|
||||||
var sigma2_raw = sigma2.tlv2raw()
|
var sigma2_raw = sigma2.tlv2raw()
|
||||||
|
@ -617,8 +602,8 @@ class Matter_Commisioning_Context
|
||||||
var sigma3_tbs = matter.TLV.Matter_TLV_struct()
|
var sigma3_tbs = matter.TLV.Matter_TLV_struct()
|
||||||
sigma3_tbs.add_TLV(1, matter.TLV.B1, initiatorNOC)
|
sigma3_tbs.add_TLV(1, matter.TLV.B1, initiatorNOC)
|
||||||
sigma3_tbs.add_TLV(2, matter.TLV.B1, initiatorICAC)
|
sigma3_tbs.add_TLV(2, matter.TLV.B1, initiatorICAC)
|
||||||
sigma3_tbs.add_TLV(3, matter.TLV.B1, self.initiatorEph_pub)
|
sigma3_tbs.add_TLV(3, matter.TLV.B1, session.__initiator_pub)
|
||||||
sigma3_tbs.add_TLV(4, matter.TLV.B1, self.ResponderEph_pub)
|
sigma3_tbs.add_TLV(4, matter.TLV.B1, session.__responder_pub)
|
||||||
tasmota.log("MTR: * sigma3_tbs = " + str(sigma3_tbs), 4)
|
tasmota.log("MTR: * sigma3_tbs = " + str(sigma3_tbs), 4)
|
||||||
var sigma3_tbs_raw = sigma3_tbs.tlv2raw()
|
var sigma3_tbs_raw = sigma3_tbs.tlv2raw()
|
||||||
tasmota.log("MTR: * sigma3_tbs_raw= " + sigma3_tbs_raw.tohex(), 4)
|
tasmota.log("MTR: * sigma3_tbs_raw= " + sigma3_tbs_raw.tohex(), 4)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
|
||||||
#
|
#
|
||||||
|
@ -19,276 +19,11 @@
|
||||||
|
|
||||||
import matter
|
import matter
|
||||||
|
|
||||||
#@ solidify:Matter_Fabric,weak
|
|
||||||
#@ solidify:Matter_Session,weak
|
#@ solidify:Matter_Session,weak
|
||||||
#@ solidify:Matter_Session_Store,weak
|
|
||||||
|
|
||||||
# for compilation
|
# for compilation
|
||||||
class Matter_Expirable end
|
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
|
# 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
|
# It can also be retrived by `source_node_id` when `local_session_id` is 0
|
||||||
#
|
#
|
||||||
# By convention, names starting with `_` are not persisted
|
# 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
|
class Matter_Session : Matter_Expirable
|
||||||
static var _PASE = 1 # PASE authentication in progress
|
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 _counter_snd_impl # implementation of counter_snd by matter.Counter()
|
||||||
var _exchange_id # exchange id for locally initiated transaction, non-persistent
|
var _exchange_id # exchange id for locally initiated transaction, non-persistent
|
||||||
# keep track of last known IP/Port of the fabric
|
# keep track of last known IP/Port of the fabric
|
||||||
var _ip # IP of the last received packet
|
var _ip # IP of the last received packet (string)
|
||||||
var _port # port of the last received packet
|
var _port # port of the last received packet (int)
|
||||||
var _message_handler # pointer to the message handler for this session
|
var _message_handler # pointer to the message handler for this session
|
||||||
# non-session counters
|
# non-session counters
|
||||||
var _counter_insecure_rcv # counter for incoming messages
|
var _counter_insecure_rcv # counter for incoming messages
|
||||||
|
@ -335,10 +71,15 @@ class Matter_Session : Matter_Expirable
|
||||||
var attestation_challenge # Attestation challenge
|
var attestation_challenge # Attestation challenge
|
||||||
var peer_node_id
|
var peer_node_id
|
||||||
# breadcrumb
|
# 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
|
# CASE
|
||||||
var resumption_id # bytes(16)
|
var resumption_id # bytes(16)
|
||||||
var shared_secret # ECDH shared secret used in CASE
|
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
|
# Previous CASE messages for Transcript hash
|
||||||
var __Msg1, __Msg2
|
var __Msg1, __Msg2
|
||||||
|
|
||||||
|
@ -702,372 +443,6 @@ end
|
||||||
matter.Session = Matter_Session
|
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
|
# Unit test
|
||||||
|
|
|
@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue