From f953ee0ac20a611b1daf6f82a90cb4e516197472 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 14:14:58 +1000 Subject: [PATCH 01/12] database: rename class fields Rename class fields to more accurately match the standard Infuse-IoT nomenclature. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 135 ++++++++++++++++--------------- src/infuse_iot/epacket/packet.py | 10 +-- src/infuse_iot/tools/gateway.py | 4 +- 3 files changed, 77 insertions(+), 72 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 78298d75..0bc95e6b 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -42,15 +42,15 @@ class DeviceState: def __init__( self, - address: int, + infuse_id: int, network_id: int | None = None, - device_id: int | None = None, + device_key_id: int | None = None, ): - self.address = address + self.infuse_id = infuse_id self.network_id = network_id - self.device_id = device_id + self.device_key_id = device_key_id self.bt_addr: InterfaceAddress.BluetoothLeAddr | None = None - self.public_key: bytes | None = None + self.device_public_key: bytes | None = None self.shared_key: bytes | None = None self._tx_gatt_seq = 0 @@ -66,43 +66,48 @@ def __init__(self) -> None: def observe_device( self, - address: int, + infuse_id: int, network_id: int | None = None, - device_id: int | None = None, + device_key_id: int | None = None, bt_addr: InterfaceAddress.BluetoothLeAddr | None = None, ) -> None: """Update device state based on observed packet""" if self.gateway is None: - self.gateway = address - if address not in self.devices: - self.devices[address] = self.DeviceState(address) + self.gateway = infuse_id + if infuse_id not in self.devices: + self.devices[infuse_id] = self.DeviceState(infuse_id) if network_id is not None: - self.devices[address].network_id = network_id - if device_id is not None: - if self.devices[address].device_id is not None and self.devices[address].device_id != device_id: - raise DeviceKeyChangedError(f"Device key for {address:016x} has changed") - self.devices[address].device_id = device_id + self.devices[infuse_id].network_id = network_id + if device_key_id is not None: + if ( + self.devices[infuse_id].device_key_id is not None + and self.devices[infuse_id].device_key_id != device_key_id + ): + raise DeviceKeyChangedError(f"Device key for {infuse_id:016x} has changed") + self.devices[infuse_id].device_key_id = device_key_id if bt_addr is not None: - self.bt_addr[bt_addr] = address - self.devices[address].bt_addr = bt_addr + self.bt_addr[bt_addr] = infuse_id + self.devices[infuse_id].bt_addr = bt_addr - def observe_security_state(self, address: int, cloud_key: bytes, device_key: bytes, network_id: int) -> None: + def observe_security_state( + self, infuse_id: int, cloud_pub_key: bytes, device_pub_key: bytes, network_id: int + ) -> None: """Update device state based on security_state response""" - if address not in self.devices: - self.devices[address] = self.DeviceState(address) - device_id = binascii.crc32(cloud_key + device_key) & 0x00FFFFFF - self.devices[address].device_id = device_id - self.devices[address].network_id = network_id - self.devices[address].public_key = device_key + if infuse_id not in self.devices: + self.devices[infuse_id] = self.DeviceState(infuse_id) + device_key_id = binascii.crc32(cloud_pub_key + device_pub_key) & 0x00FFFFFF + self.devices[infuse_id].device_key_id = device_key_id + self.devices[infuse_id].network_id = network_id + self.devices[infuse_id].device_public_key = device_pub_key client = Client(base_url="https://api.infuse-iot.com").with_headers({"x-api-key": f"Bearer {get_api_key()}"}) with client as client: - body = Key(base64.b64encode(device_key).decode("utf-8")) + body = Key(base64.b64encode(device_pub_key).decode("utf-8")) response = get_shared_secret.sync(client=client, body=body) if response is not None: key = base64.b64decode(response.key) - self.devices[address].shared_key = key + self.devices[infuse_id].shared_key = key def _network_key(self, network_id: int, interface: bytes, gps_time: int) -> bytes: if network_id not in self._network_keys: @@ -132,112 +137,112 @@ def _bt_gatt_key(self, base: bytes, time_idx: int) -> bytes: def _udp_key(self, base: bytes, time_idx: int) -> bytes: return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"udp") - def has_public_key(self, address: int) -> bool: + def has_public_key(self, infuse_id: int) -> bool: """Does the database have the public key for this device?""" - if address not in self.devices: + if infuse_id not in self.devices: return False - return self.devices[address].public_key is not None + return self.devices[infuse_id].device_public_key is not None - def has_network_id(self, address: int) -> bool: + def has_network_id(self, infuse_id: int) -> bool: """Does the database know the network ID for this device?""" - if address not in self.devices: + if infuse_id not in self.devices: return False - return self.devices[address].network_id is not None + return self.devices[infuse_id].network_id is not None def infuse_id_from_bluetooth(self, bt_addr: InterfaceAddress.BluetoothLeAddr) -> int | None: - """Get Bluetooth address associated with device""" + """Get Bluetooth infuse_id associated with device""" return self.bt_addr.get(bt_addr, None) - def serial_network_key(self, address: int, gps_time: int) -> bytes: + def serial_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for serial interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownNetworkKey - network_id = self.devices[address].network_id + network_id = self.devices[infuse_id].network_id if network_id is None: raise DeviceUnknownNetworkKey return self._network_key(network_id, b"serial", gps_time) - def serial_device_key(self, address: int, gps_time: int) -> bytes: + def serial_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for serial interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownDeviceKey - d = self.devices[address] - if d.device_id is None: + d = self.devices[infuse_id] + if d.device_key_id is None: raise DeviceUnknownDeviceKey - base = self.devices[address].shared_key + base = self.devices[infuse_id].shared_key if base is None: raise DeviceUnknownDeviceKey time_idx = gps_time // (60 * 60 * 24) return self._serial_key(base, time_idx) - def bt_adv_network_key(self, address: int, gps_time: int) -> bytes: + def bt_adv_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownNetworkKey - network_id = self.devices[address].network_id + network_id = self.devices[infuse_id].network_id if network_id is None: raise DeviceUnknownNetworkKey return self._network_key(network_id, b"bt_adv", gps_time) - def bt_adv_device_key(self, address: int, gps_time: int) -> bytes: + def bt_adv_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for Bluetooth advertising interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownDeviceKey - d = self.devices[address] - if d.device_id is None: + d = self.devices[infuse_id] + if d.device_key_id is None: raise DeviceUnknownDeviceKey - base = self.devices[address].shared_key + base = self.devices[infuse_id].shared_key if base is None: raise DeviceUnknownDeviceKey time_idx = gps_time // (60 * 60 * 24) return self._bt_adv_key(base, time_idx) - def bt_gatt_network_key(self, address: int, gps_time: int) -> bytes: + def bt_gatt_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownNetworkKey - network_id = self.devices[address].network_id + network_id = self.devices[infuse_id].network_id if network_id is None: raise DeviceUnknownNetworkKey return self._network_key(network_id, b"bt_gatt", gps_time) - def bt_gatt_device_key(self, address: int, gps_time: int) -> bytes: + def bt_gatt_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for Bluetooth advertising interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownDeviceKey - d = self.devices[address] - if d.device_id is None: + d = self.devices[infuse_id] + if d.device_key_id is None: raise DeviceUnknownDeviceKey - base = self.devices[address].shared_key + base = self.devices[infuse_id].shared_key if base is None: raise DeviceUnknownDeviceKey time_idx = gps_time // (60 * 60 * 24) return self._bt_gatt_key(base, time_idx) - def udp_network_key(self, address: int, gps_time: int) -> bytes: + def udp_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for UDP interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownNetworkKey - network_id = self.devices[address].network_id + network_id = self.devices[infuse_id].network_id if network_id is None: raise DeviceUnknownNetworkKey return self._network_key(network_id, b"udp", gps_time) - def udp_device_key(self, address: int, gps_time: int) -> bytes: + def udp_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for UDP interface""" - if address not in self.devices: + if infuse_id not in self.devices: raise DeviceUnknownDeviceKey - d = self.devices[address] - if d.device_id is None: + d = self.devices[infuse_id] + if d.device_key_id is None: raise DeviceUnknownDeviceKey - base = self.devices[address].shared_key + base = self.devices[infuse_id].shared_key if base is None: raise DeviceUnknownDeviceKey time_idx = gps_time // (60 * 60 * 24) diff --git a/src/infuse_iot/epacket/packet.py b/src/infuse_iot/epacket/packet.py index b8cdb078..300e7507 100644 --- a/src/infuse_iot/epacket/packet.py +++ b/src/infuse_iot/epacket/packet.py @@ -258,7 +258,7 @@ def to_serial(self, database: DeviceDatabase) -> bytes: key = database.serial_network_key(serial.infuse_id, gps_time) else: flags = Flags.ENCR_DEVICE - key_metadata = database.devices[serial.infuse_id].device_id + key_metadata = database.devices[serial.infuse_id].device_key_id key = database.serial_device_key(serial.infuse_id, gps_time) # Validation @@ -420,7 +420,7 @@ def hop_received(self) -> HopReceived: def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: - database.observe_device(header.device_id, device_id=header.key_metadata) + database.observe_device(header.device_id, device_key_id=header.key_metadata) key = database.serial_device_key(header.device_id, header.gps_time) else: database.observe_device(header.device_id, network_id=header.key_metadata) @@ -463,7 +463,7 @@ def encrypt( flags = 0 if auth == Auth.DEVICE: - key_meta = dev_state.device_id + key_meta = dev_state.device_key_id key = database.bt_gatt_device_key(infuse_id, gps_time) flags |= Flags.ENCR_DEVICE else: @@ -493,7 +493,7 @@ def encrypt( def decrypt(cls, database: DeviceDatabase, bt_addr: Address.BluetoothLeAddr | None, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: - database.observe_device(header.device_id, device_id=header.key_metadata, bt_addr=bt_addr) + database.observe_device(header.device_id, device_key_id=header.key_metadata, bt_addr=bt_addr) key = database.bt_gatt_device_key(header.device_id, header.gps_time) else: database.observe_device(header.device_id, network_id=header.key_metadata, bt_addr=bt_addr) @@ -508,7 +508,7 @@ class CtypeUdpFrame(CtypeV0UnversionedFrame): def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: - database.observe_device(header.device_id, device_id=header.key_metadata) + database.observe_device(header.device_id, device_key_id=header.key_metadata) key = database.udp_device_key(header.device_id, header.gps_time) else: database.observe_device(header.device_id, network_id=header.key_metadata) diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index 3a50dfe7..52f7d94d 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -401,8 +401,8 @@ def _handle_observed_devices(self): info = {} if state.network_id is not None: info["network_id"] = state.network_id - if state.device_id is not None: - info["device_id"] = state.device_id + if state.device_key_id is not None: + info["device_key_id"] = state.device_key_id if self._common.ddb.gateway == device: info["gateway"] = True observed_devices[device] = info From 172ddd93a0ccf1443b2aec54bd7680856e3cf775 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 16:36:13 +1000 Subject: [PATCH 02/12] database: extract common key internals Extract the common internals from key derivation functions to internal helpers. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 90 +++++++------------------------------- 1 file changed, 17 insertions(+), 73 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 0bc95e6b..2eeb5ebf 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -125,18 +125,6 @@ def _network_key(self, network_id: int, interface: bytes, gps_time: int) -> byte return self._derived_keys[key_id] - def _serial_key(self, base: bytes, time_idx: int) -> bytes: - return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"serial") - - def _bt_adv_key(self, base: bytes, time_idx: int) -> bytes: - return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"bt_adv") - - def _bt_gatt_key(self, base: bytes, time_idx: int) -> bytes: - return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"bt_gatt") - - def _udp_key(self, base: bytes, time_idx: int) -> bytes: - return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"udp") - def has_public_key(self, infuse_id: int) -> bool: """Does the database have the public key for this device?""" if infuse_id not in self.devices: @@ -153,18 +141,15 @@ def infuse_id_from_bluetooth(self, bt_addr: InterfaceAddress.BluetoothLeAddr) -> """Get Bluetooth infuse_id associated with device""" return self.bt_addr.get(bt_addr, None) - def serial_network_key(self, infuse_id: int, gps_time: int) -> bytes: - """Network key for serial interface""" + def _get_network_key(self, infuse_id: int, name: bytes, gps_time: int) -> bytes: if infuse_id not in self.devices: raise DeviceUnknownNetworkKey network_id = self.devices[infuse_id].network_id if network_id is None: raise DeviceUnknownNetworkKey + return self._network_key(network_id, name, gps_time) - return self._network_key(network_id, b"serial", gps_time) - - def serial_device_key(self, infuse_id: int, gps_time: int) -> bytes: - """Device key for serial interface""" + def _get_device_key(self, infuse_id: int, name: bytes, gps_time: int) -> bytes: if infuse_id not in self.devices: raise DeviceUnknownDeviceKey d = self.devices[infuse_id] @@ -174,77 +159,36 @@ def serial_device_key(self, infuse_id: int, gps_time: int) -> bytes: if base is None: raise DeviceUnknownDeviceKey time_idx = gps_time // (60 * 60 * 24) + return hkdf_derive(base, time_idx.to_bytes(4, "little"), name) + + def serial_network_key(self, infuse_id: int, gps_time: int) -> bytes: + """Network key for serial interface""" + return self._get_network_key(infuse_id, b"serial", gps_time) - return self._serial_key(base, time_idx) + def serial_device_key(self, infuse_id: int, gps_time: int) -> bytes: + """Device key for serial interface""" + return self._get_device_key(infuse_id, b"serial", gps_time) def bt_adv_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" - if infuse_id not in self.devices: - raise DeviceUnknownNetworkKey - network_id = self.devices[infuse_id].network_id - if network_id is None: - raise DeviceUnknownNetworkKey - - return self._network_key(network_id, b"bt_adv", gps_time) + return self._get_network_key(infuse_id, b"bt_adv", gps_time) def bt_adv_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for Bluetooth advertising interface""" - if infuse_id not in self.devices: - raise DeviceUnknownDeviceKey - d = self.devices[infuse_id] - if d.device_key_id is None: - raise DeviceUnknownDeviceKey - base = self.devices[infuse_id].shared_key - if base is None: - raise DeviceUnknownDeviceKey - time_idx = gps_time // (60 * 60 * 24) - - return self._bt_adv_key(base, time_idx) + return self._get_device_key(infuse_id, b"bt_adv", gps_time) def bt_gatt_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" - if infuse_id not in self.devices: - raise DeviceUnknownNetworkKey - network_id = self.devices[infuse_id].network_id - if network_id is None: - raise DeviceUnknownNetworkKey - - return self._network_key(network_id, b"bt_gatt", gps_time) + return self._get_network_key(infuse_id, b"bt_gatt", gps_time) def bt_gatt_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for Bluetooth advertising interface""" - if infuse_id not in self.devices: - raise DeviceUnknownDeviceKey - d = self.devices[infuse_id] - if d.device_key_id is None: - raise DeviceUnknownDeviceKey - base = self.devices[infuse_id].shared_key - if base is None: - raise DeviceUnknownDeviceKey - time_idx = gps_time // (60 * 60 * 24) - - return self._bt_gatt_key(base, time_idx) + return self._get_device_key(infuse_id, b"bt_gatt", gps_time) def udp_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for UDP interface""" - if infuse_id not in self.devices: - raise DeviceUnknownNetworkKey - network_id = self.devices[infuse_id].network_id - if network_id is None: - raise DeviceUnknownNetworkKey - - return self._network_key(network_id, b"udp", gps_time) + return self._get_network_key(infuse_id, b"udp", gps_time) def udp_device_key(self, infuse_id: int, gps_time: int) -> bytes: """Device key for UDP interface""" - if infuse_id not in self.devices: - raise DeviceUnknownDeviceKey - d = self.devices[infuse_id] - if d.device_key_id is None: - raise DeviceUnknownDeviceKey - base = self.devices[infuse_id].shared_key - if base is None: - raise DeviceUnknownDeviceKey - time_idx = gps_time // (60 * 60 * 24) - - return self._udp_key(base, time_idx) + return self._get_device_key(infuse_id, b"udp", gps_time) From 14d926fb63ed49aa22b4e0934989c86e946abb31 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 14:20:49 +1000 Subject: [PATCH 03/12] tools: gateway: accept local root key Enable providing a local root key to the gateway script, which is then passed to the underlying device database for key management. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 16 +++++++++++++++- src/infuse_iot/tools/gateway.py | 3 ++- src/infuse_iot/tools/native_bt.py | 6 +++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 2eeb5ebf..b97f6431 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -2,6 +2,12 @@ import base64 import binascii +import pathlib + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.types import ( + PrivateKeyTypes, +) from infuse_iot.api_client import Client from infuse_iot.api_client.api.key import get_shared_secret @@ -59,10 +65,18 @@ def gatt_sequence_num(self): self._tx_gatt_seq += 1 return self._tx_gatt_seq - def __init__(self) -> None: + def __init__(self, local_root: pathlib.Path | None) -> None: self.gateway: int | None = None self.devices: dict[int, DeviceDatabase.DeviceState] = {} self.bt_addr: dict[InterfaceAddress.BluetoothLeAddr, int] = {} + self._local_root: PrivateKeyTypes | None = None + if local_root: + with local_root.open() as f: + self._local_root = serialization.load_pem_private_key(f.read().encode("utf-8"), password=None) + + @property + def has_local_root(self) -> bool: + return self._local_root is not None def observe_device( self, diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index 52f7d94d..20689d4f 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -456,6 +456,7 @@ def add_parser(cls, parser): help="Save serial output to file", ) parser.add_argument("--baud", type=int, default=115200, help="Baudrate for serial port") + parser.add_argument("--root", type=ValidFile, help="Root identity certificate to use instead of cloud") def __init__(self, args: argparse.Namespace): self.port: SerialLike @@ -467,7 +468,7 @@ def __init__(self, args: argparse.Namespace): self.port = RttPort(args.rtt) elif args.pyocd is not None: self.port = PyOcdPort(args.pyocd) - self.ddb = DeviceDatabase() + self.ddb = DeviceDatabase(args.root) if args.display_only: self.server = None else: diff --git a/src/infuse_iot/tools/native_bt.py b/src/infuse_iot/tools/native_bt.py index dad447e8..a12ed19a 100644 --- a/src/infuse_iot/tools/native_bt.py +++ b/src/infuse_iot/tools/native_bt.py @@ -42,7 +42,7 @@ LocalServer, default_multicast_address, ) -from infuse_iot.util.argparse import BtLeAddress +from infuse_iot.util.argparse import BtLeAddress, ValidFile from infuse_iot.util.console import Console @@ -204,11 +204,11 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): - pass + parser.add_argument("--root", type=ValidFile, help="Root identity certificate to use instead of cloud") def __init__(self, args: argparse.Namespace): self.infuse_manu = 0x0DE4 - self.database = DeviceDatabase() + self.database = DeviceDatabase(args.root) self.server = LocalServer(default_multicast_address()) self.bleak_mapping: dict[int, BLEDevice] = {} self.unknown_networks: set[int] = set() From 320e0b34829b5cc33f5862b433d56d936aad7498 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 15:58:01 +1000 Subject: [PATCH 04/12] util: ctypes: fix `vla_counted_by` detection For `VLACompatLittleEndianStruct` subclasses, `vla_counted_by` always exists, but can be `None`. Ensure it is not `None` before attempting to use. Signed-off-by: Jordan Yates --- src/infuse_iot/util/ctypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infuse_iot/util/ctypes.py b/src/infuse_iot/util/ctypes.py index 489770c0..21cd1eae 100644 --- a/src/infuse_iot/util/ctypes.py +++ b/src/infuse_iot/util/ctypes.py @@ -52,8 +52,8 @@ class property is not `None`, it will consume the remainder of if issubclass(vla_field_type, ctypes.Array): array_base: ctypes._PyCSimpleType = vla_field_type._type_ # type: ignore - if hasattr(array_base, "vla_counted_by"): - # This is an array of VLA arrays where the sub-arrys define their own length + if hasattr(array_base, "vla_counted_by") and array_base.vla_counted_by: + # This is an array of VLA arrays where the sub-arrays define their own length vla_val = [] # Consume all remaining buffer bytes while len(remainder) > 0: From 6db74451e7bf6aeafd4f220c584a7fa38d13efd6 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 16:00:40 +1000 Subject: [PATCH 05/12] generated: update RPC definitions Update RPC definitions. Signed-off-by: Jordan Yates --- src/infuse_iot/generated/rpc_definitions.py | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py index b9f9e89f..18e362cb 100644 --- a/src/infuse_iot/generated/rpc_definitions.py +++ b/src/infuse_iot/generated/rpc_definitions.py @@ -222,6 +222,16 @@ class rpc_struct_data_logger_chunk(VLACompatLittleEndianStruct): _pack_ = 1 +class rpc_struct_public_key_info_256bit(VLACompatLittleEndianStruct): + """256 bit public key and identifier""" + + _fields_ = [ + ("id", ctypes.c_uint8), + ("key", 32 * ctypes.c_uint8), + ] + _pack_ = 1 + + class rpc_enum_bt_le_addr_type(enum.IntEnum): """Bluetooth LE address type""" @@ -273,6 +283,8 @@ class rpc_enum_key_id(enum.IntEnum): NETWORK_KEY = 0 SECONDARY_NETWORK_KEY = 1 SECONDARY_REMOTE_PUBLIC_KEY = 2 + DEVICE_PUBLIC_KEY = 3 + CLOUD_PUBLIC_KEY = 4 class rpc_enum_key_action(enum.IntEnum): @@ -1212,6 +1224,30 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class security_public_keys(RPCDefinitionBase): + """Query device public keys""" + + NAME = "security_public_keys" + HELP = "Query device public keys" + DESCRIPTION = "Query device public keys" + COMMAND_ID = 30002 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("skip", ctypes.c_uint8), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("keys_total", ctypes.c_uint8), + ("keys_included", ctypes.c_uint8), + ] + vla_field = ("public_keys", 0 * rpc_struct_public_key_info_256bit) + vla_counted_by = "keys_included" + _pack_ = 1 + + class data_sender(RPCDefinitionBase): """Send multiple INFUSE_RPC_DATA packets""" @@ -1310,6 +1346,7 @@ class response(VLACompatLittleEndianStruct): ubx_assist_now_ztp_creds.COMMAND_ID: ubx_assist_now_ztp_creds, security_state.COMMAND_ID: security_state, security_key_update.COMMAND_ID: security_key_update, + security_public_keys.COMMAND_ID: security_public_keys, data_sender.COMMAND_ID: data_sender, data_receiver.COMMAND_ID: data_receiver, echo.COMMAND_ID: echo, @@ -1333,6 +1370,7 @@ class response(VLACompatLittleEndianStruct): "rpc_struct_sockaddr", "rpc_struct_heap_info", "rpc_struct_data_logger_chunk", + "rpc_struct_public_key_info_256bit", "rpc_enum_bt_le_addr_type", "rpc_enum_file_action", "rpc_enum_infuse_bt_characteristic", @@ -1380,6 +1418,7 @@ class response(VLACompatLittleEndianStruct): "ubx_assist_now_ztp_creds", "security_state", "security_key_update", + "security_public_keys", "data_sender", "data_receiver", "echo", From 8730c886a36a069aa25eca41f60cdc4d9bafdaad Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 16:00:59 +1000 Subject: [PATCH 06/12] rpc_wrappers: security_public_keys: added Add wrapper for the `security_public_keys` RPC. Signed-off-by: Jordan Yates --- .../rpc_wrappers/security_public_keys.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/infuse_iot/rpc_wrappers/security_public_keys.py diff --git a/src/infuse_iot/rpc_wrappers/security_public_keys.py b/src/infuse_iot/rpc_wrappers/security_public_keys.py new file mode 100644 index 00000000..9d5903f8 --- /dev/null +++ b/src/infuse_iot/rpc_wrappers/security_public_keys.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import infuse_iot.definitions.rpc as defs +from infuse_iot.commands import InfuseRpcCommand +from infuse_iot.zephyr.errno import errno + + +class security_public_keys(InfuseRpcCommand, defs.security_public_keys): + @classmethod + def add_parser(cls, parser): + parser.add_argument("--skip", type=int, default=0, help="Skip first N keys") + + def __init__(self, args): + self._skip = args.skip + + def request_struct(self): + return self.request( + self._skip, + ) + + def handle_response(self, return_code, response): + if return_code != 0: + print(f"Failed to update key ({errno.strerror(-return_code)}, {-return_code})") + return + + for key in response.public_keys: + key_id = defs.rpc_enum_key_id(key.id) + key_str = bytes(key.key).hex() + print(f"{key_id.name:>30}: {key_str}") From 6b50ef3ef95156864d3d26e1d0c8c710515c3f28 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 22 Jan 2026 16:01:47 +1000 Subject: [PATCH 07/12] rpc_wrappers: security_key_update: added Add a wrapper for writing a secondary device root key. Signed-off-by: Jordan Yates --- .../rpc_wrappers/security_key_update.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/infuse_iot/rpc_wrappers/security_key_update.py diff --git a/src/infuse_iot/rpc_wrappers/security_key_update.py b/src/infuse_iot/rpc_wrappers/security_key_update.py new file mode 100644 index 00000000..e6eb612b --- /dev/null +++ b/src/infuse_iot/rpc_wrappers/security_key_update.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import x25519 + +import infuse_iot.definitions.rpc as defs +from infuse_iot.commands import InfuseRpcCommand +from infuse_iot.epacket.packet import Auth +from infuse_iot.util.argparse import ValidFile +from infuse_iot.util.ctypes import bytes_to_uint8 +from infuse_iot.zephyr.errno import errno + + +class security_key_update(InfuseRpcCommand, defs.security_key_update): + @classmethod + def add_parser(cls, parser): + parser.add_argument("--network-auth", action="store_true", help="Use network auth instead of device") + parser.add_argument("--delete", action="store_true", help="Delete instead of writing key") + parser.add_argument("--delay", type=int, default=2, help="Reboot delay (seconds)") + key_group = parser.add_mutually_exclusive_group(required=True) + key_group.add_argument("--secondary-root", type=ValidFile) + + def __init__(self, args): + self._auth = Auth.NETWORK if args.network_auth else Auth.DEVICE + self._key_action = defs.rpc_enum_key_action.KEY_DELETE if args.delete else defs.rpc_enum_key_action.KEY_WRITE + self._delay = args.delay + if args.secondary_root: + self._key_id = defs.rpc_enum_key_id.SECONDARY_REMOTE_PUBLIC_KEY + self._global_key_id = 0 + with args.secondary_root.open("r") as f: + private_key = serialization.load_pem_private_key(f.read().encode("utf-8"), password=None) + assert isinstance(private_key, x25519.X25519PrivateKey) + public_key = private_key.public_key() + self._key_bytes = public_key.public_bytes_raw() + else: + raise NotImplementedError("Unimplemented key type") + + def auth_level(self): + return self._auth + + def request_struct(self): + return self.request( + self._key_id, + self._key_action, + self._global_key_id, + bytes_to_uint8(self._key_bytes), + self._delay, + ) + + def handle_response(self, return_code, response): + if return_code != 0: + print(f"Failed to update key ({errno.strerror(-return_code)}, {-return_code})") + return + print(f"Updated key {self._key_id.name} on device") From e73fa27750d75cb6aa2da4ac441ec6206e100a3b Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 23 Jan 2026 13:05:18 +1000 Subject: [PATCH 08/12] gateway: compute secondary remote key If a local root identity is provided, query remote devices for secondary public keys and compute shared secrets if the keys match. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 30 +++++++++++++++++++++----- src/infuse_iot/tools/gateway.py | 37 +++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index b97f6431..33d2d4f9 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -5,9 +5,7 @@ import pathlib from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric.types import ( - PrivateKeyTypes, -) +from cryptography.hazmat.primitives.asymmetric import x25519 from infuse_iot.api_client import Client from infuse_iot.api_client.api.key import get_shared_secret @@ -58,6 +56,8 @@ def __init__( self.bt_addr: InterfaceAddress.BluetoothLeAddr | None = None self.device_public_key: bytes | None = None self.shared_key: bytes | None = None + self.secondary_device_key_id: int | None = None + self.local_shared_key: bytes | None = None self._tx_gatt_seq = 0 def gatt_sequence_num(self): @@ -69,15 +69,22 @@ def __init__(self, local_root: pathlib.Path | None) -> None: self.gateway: int | None = None self.devices: dict[int, DeviceDatabase.DeviceState] = {} self.bt_addr: dict[InterfaceAddress.BluetoothLeAddr, int] = {} - self._local_root: PrivateKeyTypes | None = None + self._local_root: x25519.X25519PrivateKey | None = None + self._local_root_public: bytes | None = None if local_root: with local_root.open() as f: - self._local_root = serialization.load_pem_private_key(f.read().encode("utf-8"), password=None) + private_key = serialization.load_pem_private_key(f.read().encode("utf-8"), password=None) + assert isinstance(private_key, x25519.X25519PrivateKey) + self._local_root = private_key + self._local_root_public = self._local_root.public_key().public_bytes_raw() @property def has_local_root(self) -> bool: return self._local_root is not None + def is_local_root(self, public_key: bytes) -> bool: + return public_key == self._local_root_public + def observe_device( self, infuse_id: int, @@ -103,6 +110,19 @@ def observe_device( self.bt_addr[bt_addr] = infuse_id self.devices[infuse_id].bt_addr = bt_addr + def observe_secondary_remote_public_key(self, infuse_id: int, secondary_pub_key: bytes): + if not self.is_local_root(secondary_pub_key): + return + if infuse_id not in self.devices: + return + dev = self.devices[infuse_id] + assert self._local_root is not None + assert self._local_root_public is not None + assert dev.device_public_key is not None + device_public_key = x25519.X25519PublicKey.from_public_bytes(dev.device_public_key) + dev.secondary_device_key_id = binascii.crc32(self._local_root_public + dev.device_public_key) & 0xFFFFFF + dev.local_shared_key = self._local_root.exchange(device_public_key) + def observe_security_state( self, infuse_id: int, cloud_pub_key: bytes, device_pub_key: bytes, network_id: int ) -> None: diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index 20689d4f..0d9a5a90 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -140,15 +140,34 @@ def security_state_done(pkt: PacketReceived, _: int, response: bytes): if cb_event is not None: cb_event.set() - # Generate security_state RPC - cmd_pkt = self.rpc.generate(30000, random.randbytes(16), Auth.NETWORK, security_state_done) - encrypted = cmd_pkt.to_serial(self.ddb) - # Write to serial port - Console.log_tx(cmd_pkt.ptype, len(encrypted)) - self.port.write(encrypted) - if cb_event is not None: - # Wait for the response - cb_event.wait(1.0) + def public_keys_done(pkt: PacketReceived, rc: int, response: bytes): + if rc != 0: + return + decoded = defs.security_public_keys.response.vla_from_buffer_copy(response) + for key in decoded.public_keys: + if key.id == defs.rpc_enum_key_id.SECONDARY_REMOTE_PUBLIC_KEY: + infuse_id = pkt.route[0].infuse_id + self.ddb.observe_secondary_remote_public_key(infuse_id, bytes(key.key)) + + def run_cmd_pkt(cmd: PacketOutputRouted): + encrypted = cmd_pkt.to_serial(self.ddb) + # Write to serial port + Console.log_tx(cmd_pkt.ptype, len(encrypted)) + self.port.write(encrypted) + if cb_event is not None: + # Wait for the response + cb_event.wait(1.0) + + # Run security_state RPC + cmd_pkt = self.rpc.generate( + defs.security_state.COMMAND_ID, random.randbytes(16), Auth.NETWORK, security_state_done + ) + run_cmd_pkt(cmd_pkt) + + if self.ddb.has_local_root: + # Query other public keys from the device + cmd_pkt = self.rpc.generate(defs.security_public_keys.COMMAND_ID, b"\x00", Auth.NETWORK, public_keys_done) + run_cmd_pkt(cmd_pkt) class SerialRxThread(SignaledThread): From 99a6218f1f35119cde7339ea717ae9f944cbe3d6 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 23 Jan 2026 13:11:15 +1000 Subject: [PATCH 09/12] database: option to specify device key ID Add the option to provide the device key ID used for the packet to the key retrieval service. Use this for decrypting data from devices. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 22 ++++++++++++---------- src/infuse_iot/epacket/packet.py | 6 +++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 33d2d4f9..c35d87a4 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -183,11 +183,13 @@ def _get_network_key(self, infuse_id: int, name: bytes, gps_time: int) -> bytes: raise DeviceUnknownNetworkKey return self._network_key(network_id, name, gps_time) - def _get_device_key(self, infuse_id: int, name: bytes, gps_time: int) -> bytes: + def _get_device_key(self, infuse_id: int, name: bytes, gps_time: int, key_id: int | None = None) -> bytes: if infuse_id not in self.devices: raise DeviceUnknownDeviceKey d = self.devices[infuse_id] - if d.device_key_id is None: + if key_id is None or key_id == d.device_key_id: + base = self.devices[infuse_id].shared_key + else: raise DeviceUnknownDeviceKey base = self.devices[infuse_id].shared_key if base is None: @@ -199,30 +201,30 @@ def serial_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for serial interface""" return self._get_network_key(infuse_id, b"serial", gps_time) - def serial_device_key(self, infuse_id: int, gps_time: int) -> bytes: + def serial_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: """Device key for serial interface""" - return self._get_device_key(infuse_id, b"serial", gps_time) + return self._get_device_key(infuse_id, b"serial", gps_time, key_id) def bt_adv_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" return self._get_network_key(infuse_id, b"bt_adv", gps_time) - def bt_adv_device_key(self, infuse_id: int, gps_time: int) -> bytes: + def bt_adv_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: """Device key for Bluetooth advertising interface""" - return self._get_device_key(infuse_id, b"bt_adv", gps_time) + return self._get_device_key(infuse_id, b"bt_adv", gps_time, key_id) def bt_gatt_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" return self._get_network_key(infuse_id, b"bt_gatt", gps_time) - def bt_gatt_device_key(self, infuse_id: int, gps_time: int) -> bytes: + def bt_gatt_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: """Device key for Bluetooth advertising interface""" - return self._get_device_key(infuse_id, b"bt_gatt", gps_time) + return self._get_device_key(infuse_id, b"bt_gatt", gps_time, key_id) def udp_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for UDP interface""" return self._get_network_key(infuse_id, b"udp", gps_time) - def udp_device_key(self, infuse_id: int, gps_time: int) -> bytes: + def udp_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: """Device key for UDP interface""" - return self._get_device_key(infuse_id, b"udp", gps_time) + return self._get_device_key(infuse_id, b"udp", gps_time, key_id) diff --git a/src/infuse_iot/epacket/packet.py b/src/infuse_iot/epacket/packet.py index 300e7507..617b0906 100644 --- a/src/infuse_iot/epacket/packet.py +++ b/src/infuse_iot/epacket/packet.py @@ -421,7 +421,7 @@ def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: database.observe_device(header.device_id, device_key_id=header.key_metadata) - key = database.serial_device_key(header.device_id, header.gps_time) + key = database.serial_device_key(header.device_id, header.gps_time, header.key_metadata) else: database.observe_device(header.device_id, network_id=header.key_metadata) key = database.serial_network_key(header.device_id, header.gps_time) @@ -494,7 +494,7 @@ def decrypt(cls, database: DeviceDatabase, bt_addr: Address.BluetoothLeAddr | No header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: database.observe_device(header.device_id, device_key_id=header.key_metadata, bt_addr=bt_addr) - key = database.bt_gatt_device_key(header.device_id, header.gps_time) + key = database.bt_gatt_device_key(header.device_id, header.gps_time, header.key_metadata) else: database.observe_device(header.device_id, network_id=header.key_metadata, bt_addr=bt_addr) key = database.bt_gatt_network_key(header.device_id, header.gps_time) @@ -509,7 +509,7 @@ def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: database.observe_device(header.device_id, device_key_id=header.key_metadata) - key = database.udp_device_key(header.device_id, header.gps_time) + key = database.udp_device_key(header.device_id, header.gps_time, header.key_metadata) else: database.observe_device(header.device_id, network_id=header.key_metadata) key = database.udp_network_key(header.device_id, header.gps_time) From 46d41707dee63064639e3dc735c9b3e354689ad1 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 23 Jan 2026 13:24:58 +1000 Subject: [PATCH 10/12] database: default to secondary local key if known If we know the shared key from the secondary (local) root key, use that instead of the cloud derived key. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index c35d87a4..0ebfd641 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -187,11 +187,19 @@ def _get_device_key(self, infuse_id: int, name: bytes, gps_time: int, key_id: in if infuse_id not in self.devices: raise DeviceUnknownDeviceKey d = self.devices[infuse_id] - if key_id is None or key_id == d.device_key_id: - base = self.devices[infuse_id].shared_key + if key_id is None: + if d.secondary_device_key_id: + key_id = d.secondary_device_key_id + base = d.local_shared_key + else: + key_id = d.device_key_id + base = d.shared_key + elif key_id == d.device_key_id: + base = d.shared_key + elif key_id == d.secondary_device_key_id: + base = d.local_shared_key else: raise DeviceUnknownDeviceKey - base = self.devices[infuse_id].shared_key if base is None: raise DeviceUnknownDeviceKey time_idx = gps_time // (60 * 60 * 24) From 025f90c7120475a321226376139918d788024fa2 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 23 Jan 2026 13:26:01 +1000 Subject: [PATCH 11/12] database: return chosen key identifier Return the key identifier associated with the returned key, as the caller no longer knows the value. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 15 +++++++++------ src/infuse_iot/epacket/packet.py | 13 ++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 0ebfd641..0f50f623 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -183,7 +183,9 @@ def _get_network_key(self, infuse_id: int, name: bytes, gps_time: int) -> bytes: raise DeviceUnknownNetworkKey return self._network_key(network_id, name, gps_time) - def _get_device_key(self, infuse_id: int, name: bytes, gps_time: int, key_id: int | None = None) -> bytes: + def _get_device_key( + self, infuse_id: int, name: bytes, gps_time: int, key_id: int | None = None + ) -> tuple[int, bytes]: if infuse_id not in self.devices: raise DeviceUnknownDeviceKey d = self.devices[infuse_id] @@ -202,14 +204,15 @@ def _get_device_key(self, infuse_id: int, name: bytes, gps_time: int, key_id: in raise DeviceUnknownDeviceKey if base is None: raise DeviceUnknownDeviceKey + assert key_id is not None time_idx = gps_time // (60 * 60 * 24) - return hkdf_derive(base, time_idx.to_bytes(4, "little"), name) + return key_id, hkdf_derive(base, time_idx.to_bytes(4, "little"), name) def serial_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for serial interface""" return self._get_network_key(infuse_id, b"serial", gps_time) - def serial_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: + def serial_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> tuple[int, bytes]: """Device key for serial interface""" return self._get_device_key(infuse_id, b"serial", gps_time, key_id) @@ -217,7 +220,7 @@ def bt_adv_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" return self._get_network_key(infuse_id, b"bt_adv", gps_time) - def bt_adv_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: + def bt_adv_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> tuple[int, bytes]: """Device key for Bluetooth advertising interface""" return self._get_device_key(infuse_id, b"bt_adv", gps_time, key_id) @@ -225,7 +228,7 @@ def bt_gatt_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for Bluetooth advertising interface""" return self._get_network_key(infuse_id, b"bt_gatt", gps_time) - def bt_gatt_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: + def bt_gatt_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> tuple[int, bytes]: """Device key for Bluetooth advertising interface""" return self._get_device_key(infuse_id, b"bt_gatt", gps_time, key_id) @@ -233,6 +236,6 @@ def udp_network_key(self, infuse_id: int, gps_time: int) -> bytes: """Network key for UDP interface""" return self._get_network_key(infuse_id, b"udp", gps_time) - def udp_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> bytes: + def udp_device_key(self, infuse_id: int, gps_time: int, key_id: int | None = None) -> tuple[int, bytes]: """Device key for UDP interface""" return self._get_device_key(infuse_id, b"udp", gps_time, key_id) diff --git a/src/infuse_iot/epacket/packet.py b/src/infuse_iot/epacket/packet.py index 617b0906..7d297cb8 100644 --- a/src/infuse_iot/epacket/packet.py +++ b/src/infuse_iot/epacket/packet.py @@ -258,8 +258,7 @@ def to_serial(self, database: DeviceDatabase) -> bytes: key = database.serial_network_key(serial.infuse_id, gps_time) else: flags = Flags.ENCR_DEVICE - key_metadata = database.devices[serial.infuse_id].device_key_id - key = database.serial_device_key(serial.infuse_id, gps_time) + key_metadata, key = database.serial_device_key(serial.infuse_id, gps_time) # Validation assert key_metadata is not None @@ -421,7 +420,7 @@ def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: database.observe_device(header.device_id, device_key_id=header.key_metadata) - key = database.serial_device_key(header.device_id, header.gps_time, header.key_metadata) + _, key = database.serial_device_key(header.device_id, header.gps_time, header.key_metadata) else: database.observe_device(header.device_id, network_id=header.key_metadata) key = database.serial_network_key(header.device_id, header.gps_time) @@ -460,11 +459,11 @@ def encrypt( ) -> bytes: dev_state = database.devices[infuse_id] gps_time = InfuseTime.gps_seconds_from_unix(int(time.time())) + key_meta: int | None flags = 0 if auth == Auth.DEVICE: - key_meta = dev_state.device_key_id - key = database.bt_gatt_device_key(infuse_id, gps_time) + key_meta, key = database.bt_gatt_device_key(infuse_id, gps_time) flags |= Flags.ENCR_DEVICE else: key_meta = dev_state.network_id @@ -494,7 +493,7 @@ def decrypt(cls, database: DeviceDatabase, bt_addr: Address.BluetoothLeAddr | No header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: database.observe_device(header.device_id, device_key_id=header.key_metadata, bt_addr=bt_addr) - key = database.bt_gatt_device_key(header.device_id, header.gps_time, header.key_metadata) + _, key = database.bt_gatt_device_key(header.device_id, header.gps_time, header.key_metadata) else: database.observe_device(header.device_id, network_id=header.key_metadata, bt_addr=bt_addr) key = database.bt_gatt_network_key(header.device_id, header.gps_time) @@ -509,7 +508,7 @@ def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: database.observe_device(header.device_id, device_key_id=header.key_metadata) - key = database.udp_device_key(header.device_id, header.gps_time, header.key_metadata) + _, key = database.udp_device_key(header.device_id, header.gps_time, header.key_metadata) else: database.observe_device(header.device_id, network_id=header.key_metadata) key = database.udp_network_key(header.device_id, header.gps_time) From 7585379e2ea6031c766a2fa67f393f142b9d9da7 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 23 Jan 2026 13:41:51 +1000 Subject: [PATCH 12/12] database: handle observing secondary device key Handle observing the secondary device key without raising an exception. Signed-off-by: Jordan Yates --- src/infuse_iot/database.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 0f50f623..2b4a2a63 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -97,18 +97,19 @@ def observe_device( self.gateway = infuse_id if infuse_id not in self.devices: self.devices[infuse_id] = self.DeviceState(infuse_id) + dev = self.devices[infuse_id] if network_id is not None: - self.devices[infuse_id].network_id = network_id + dev.network_id = network_id if device_key_id is not None: - if ( - self.devices[infuse_id].device_key_id is not None - and self.devices[infuse_id].device_key_id != device_key_id - ): + if device_key_id == dev.secondary_device_key_id: + pass + elif dev.device_key_id is not None and dev.device_key_id != device_key_id: raise DeviceKeyChangedError(f"Device key for {infuse_id:016x} has changed") - self.devices[infuse_id].device_key_id = device_key_id + else: + dev.device_key_id = device_key_id if bt_addr is not None: self.bt_addr[bt_addr] = infuse_id - self.devices[infuse_id].bt_addr = bt_addr + dev.bt_addr = bt_addr def observe_secondary_remote_public_key(self, infuse_id: int, secondary_pub_key: bytes): if not self.is_local_root(secondary_pub_key):