Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/infuse_iot/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import enum


class InfuseType(enum.Enum):
class InfuseType(enum.IntEnum):
"""Infuse Data Types"""

ECHO_REQ = 0
Expand All @@ -15,7 +15,14 @@ class InfuseType(enum.Enum):
RPC_RSP = 6
RECEIVED_EPACKET = 7
ACK = 8
EPACKET_FORWARD = 9
SERIAL_LOG = 10
MEMFAULT_CHUNK = 30

KEY_IDS = 127


class InfuseID(enum.IntEnum):
"""Hardcoded Infuse IDs"""

GATEWAY = -1
12 changes: 10 additions & 2 deletions src/infuse_iot/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@ def __init__(self, address, network_id=None, device_id=None):
self.address = address
self.network_id = network_id
self.device_id = device_id
self.bt_addr: InterfaceAddress.BluetoothLeAddr | None = None
self.public_key = None
self.shared_key = None
self._tx_gatt_seq = 0

def gatt_sequence_num(self):
"""Persistent auto-incrementing sequence number for GATT"""
self._tx_gatt_seq += 1
return self._tx_gatt_seq

def __init__(self):
self.gateway = None
Expand All @@ -65,8 +72,6 @@ def observe_device(
"""Update device state based on observed packet"""
if self.gateway is None:
self.gateway = address
if bt_addr is not None:
self.bt_addr[bt_addr] = address
if address not in self.devices:
self.devices[address] = self.DeviceState(address)
if network_id is not None:
Expand All @@ -80,6 +85,9 @@ def observe_device(
f"Device key for {address:016x} has changed"
)
self.devices[address].device_id = device_id
if bt_addr is not None:
self.bt_addr[bt_addr] = address
self.devices[address].bt_addr = bt_addr

def observe_security_state(
self, address: int, cloud_key: bytes, device_key: bytes, network_id: int
Expand Down
6 changes: 6 additions & 0 deletions src/infuse_iot/epacket/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ def __str__(self) -> str:
def len(self):
return ctypes.sizeof(self.CtypesFormat)

def to_ctype(self) -> CtypesFormat:
"""Convert the address to the ctype format"""
return self.CtypesFormat(
self.addr_type, bytes_to_uint8(self.addr_val.to_bytes(6, "little"))
)

def to_json(self) -> Dict:
return {"i": "BT", "t": self.addr_type, "v": self.addr_val}

Expand Down
215 changes: 115 additions & 100 deletions src/infuse_iot/epacket/packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from infuse_iot.time import InfuseTime


class Auth(enum.Enum):
class Auth(enum.IntEnum):
"""Authorisation options"""

DEVICE = 0
Expand All @@ -30,89 +30,6 @@ class Flags(enum.IntEnum):
ENCR_NETWORK = 0x0000


class InterfaceAddress(Serializable):
class SerialAddr(Serializable):
def __str__(self):
return ""

def len(self):
return 0

def to_json(self) -> Dict:
return {"i": "SERIAL"}

@classmethod
def from_json(cls, values: Dict) -> Self:
return cls()

class BluetoothLeAddr(Serializable):
class CtypesFormat(ctypes.Structure):
_fields_ = [
("type", ctypes.c_uint8),
("addr", 6 * ctypes.c_uint8),
]
_pack_ = 1

def __init__(self, addr_type, addr_val):
self.addr_type = addr_type
self.addr_val = addr_val

def __hash__(self) -> int:
return (self.addr_type << 48) + self.addr_val

def __eq__(self, another) -> bool:
return (
self.addr_type == another.addr_type
and self.addr_val == another.addr_val
)

def __str__(self) -> str:
t = "random" if self.addr_type == 1 else "public"
v = ":".join([f"{x:02x}" for x in self.addr_val.to_bytes(6, "big")])
return f"{v} ({t})"

def len(self):
return ctypes.sizeof(self.CtypesFormat)

def to_json(self) -> Dict:
return {"i": "BT", "t": self.addr_type, "v": self.addr_val}

@classmethod
def from_json(cls, values: Dict) -> Self:
return cls(values["t"], values["v"])

def __init__(self, val):
self.val = val

def __str__(self):
return str(self.val)

def len(self):
return self.val.len()

def to_json(self) -> Dict:
return self.val.to_json()

@classmethod
def from_json(cls, values: Dict) -> Self:
if values["i"] == "BT":
return cls(cls.BluetoothLeAddr.from_json(values))
elif values["i"] == "SERIAL":
return cls(cls.SerialAddr())
raise NotImplementedError("Unknown address type")

@classmethod
def from_bytes(cls, interface: Interface, stream: bytes) -> Self:
assert interface in [
Interface.BT_ADV,
Interface.BT_PERIPHERAL,
Interface.BT_CENTRAL,
]

c = cls.BluetoothLeAddr.CtypesFormat.from_buffer_copy(stream)
return cls.BluetoothLeAddr(c.type, int.from_bytes(bytes(c.addr), "little"))


class HopOutput(Serializable):
def __init__(self, infuse_id: int, interface: Interface, auth: Auth):
self.infuse_id = infuse_id
Expand Down Expand Up @@ -145,7 +62,7 @@ def __init__(
self,
infuse_id: int,
interface: Interface,
interface_address: InterfaceAddress,
interface_address: Address,
auth: Auth,
key_identifier: int,
gps_time: int,
Expand Down Expand Up @@ -179,7 +96,7 @@ def from_json(cls, values: Dict) -> Self:
return cls(
infuse_id=values["id"],
interface=interface,
interface_address=InterfaceAddress.from_json(values["interface_addr"]),
interface_address=Address.from_json(values["interface_addr"]),
auth=Auth(values["auth"]),
key_identifier=values["key_id"],
gps_time=values["time"],
Expand Down Expand Up @@ -242,7 +159,7 @@ def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self
frame_type = decode_mapping[common_header.interface]

# Extract interface address (Only Bluetooth supported)
addr = InterfaceAddress.from_bytes(common_header.interface, packet_bytes)
addr = Address.from_bytes(common_header.interface, packet_bytes)
del packet_bytes[: addr.len()]

# Decrypting packet
Expand Down Expand Up @@ -307,8 +224,8 @@ def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self
return packets


class PacketOutput(Serializable):
"""ePacket to be transmitted by gateway"""
class PacketOutputRouted(Serializable):
"""ePacket to be transmitted by gateway with complete route"""

def __init__(self, route: List[HopOutput], ptype: InfuseType, payload: bytes):
# [Serial, hop, hop, final_hop]
Expand All @@ -319,23 +236,47 @@ def __init__(self, route: List[HopOutput], ptype: InfuseType, payload: bytes):
def to_serial(self, database: DeviceDatabase) -> bytes:
"""Encode and encrypt packet for serial transmission"""
gps_time = InfuseTime.gps_seconds_from_unix(int(time.time()))
# Multi hop not currently supported
assert len(self.route) == 1
route = self.route[0]

if route.auth == Auth.NETWORK:
if len(self.route) == 2:
# Two hops only supports Bluetooth central for now
final = self.route[1]
assert final.interface == Interface.BT_CENTRAL

# Forwarded payload
forward_payload = CtypeBtGattFrame.encrypt(
database, final.infuse_id, self.ptype, Auth.DEVICE, self.payload
)

# Forwarding header
forward_hdr = CtypeForwardHeaderBtGatt(
ctypes.sizeof(CtypeForwardHeaderBtGatt) + len(forward_payload),
Interface.BT_CENTRAL.value,
database.devices[final.infuse_id].bt_addr.to_ctype(),
)

ptype = InfuseType.EPACKET_FORWARD
payload = bytes(forward_hdr) + forward_payload
elif len(self.route) == 1:
ptype = self.ptype
payload = self.payload
else:
raise NotImplementedError(">2 hops currently not supported")

serial = self.route[0]

if serial.auth == Auth.NETWORK:
flags = Flags.ENCR_NETWORK
key_metadata = database.devices[route.infuse_id].network_id
key = database.serial_network_key(route.infuse_id, gps_time)
key_metadata = database.devices[serial.infuse_id].network_id
key = database.serial_network_key(serial.infuse_id, gps_time)
else:
flags = Flags.ENCR_DEVICE
key_metadata = database.devices[route.infuse_id].device_id
key = database.serial_device_key(route.infuse_id, gps_time)
key_metadata = database.devices[serial.infuse_id].device_id
key = database.serial_device_key(serial.infuse_id, gps_time)

# Create header
header = CtypeSerialFrame(
version=0,
_type=self.ptype.value,
_type=ptype,
flags=flags,
gps_time=gps_time,
sequence=0,
Expand All @@ -347,7 +288,7 @@ def to_serial(self, database: DeviceDatabase) -> bytes:
# Encrypt and return payload
header_bytes = bytes(header)
ciphertext = chachapoly_encrypt(
key, header_bytes[:11], header_bytes[11:], self.payload
key, header_bytes[:11], header_bytes[11:], payload
)
return header_bytes + ciphertext

Expand All @@ -367,6 +308,42 @@ def from_json(cls, values: Dict) -> Self:
)


class PacketOutput(PacketOutputRouted):
"""ePacket to be transmitted by gateway"""

def __init__(self, infuse_id: int, auth: Auth, ptype: InfuseType, payload: bytes):
self.infuse_id = infuse_id
self.auth = auth
self.ptype = ptype
self.payload = payload

def to_json(self) -> Dict:
return {
"infuse_id": self.infuse_id,
"auth": self.auth,
"type": self.ptype.value,
"payload": base64.b64encode(self.payload).decode("utf-8"),
}

@classmethod
def from_json(cls, values: Dict) -> Self:
return cls(
infuse_id=values["infuse_id"],
auth=Auth(values["auth"]),
ptype=InfuseType(values["type"]),
payload=base64.b64decode(values["payload"].encode("utf-8")),
)


class CtypeForwardHeaderBtGatt(ctypes.LittleEndianStructure):
_fields_ = [
("total_length", ctypes.c_uint16),
("interface", ctypes.c_uint8),
("address", Address.BluetoothLeAddr.CtypesFormat),
]
_pack_ = 1


class CtypeV0VersionedFrame(ctypes.LittleEndianStructure):
_fields_ = [
("version", ctypes.c_uint8),
Expand Down Expand Up @@ -419,7 +396,7 @@ def hop_received(self) -> HopReceived:
return HopReceived(
self.device_id,
Interface.SERIAL,
InterfaceAddress(InterfaceAddress.SerialAddr()),
Address(Address.SerialAddr()),
auth,
self.key_metadata,
self.gps_time,
Expand Down Expand Up @@ -464,6 +441,44 @@ def decrypt(
class CtypeBtGattFrame(CtypeV0VersionedFrame):
"""Bluetooth GATT packet header"""

@classmethod
def encrypt(
cls,
database: DeviceDatabase,
infuse_id: int,
ptype: InfuseType,
auth: Auth,
payload: bytes,
) -> bytes:
dev_state = database.devices[infuse_id]
gps_time = InfuseTime.gps_seconds_from_unix(int(time.time()))
flags = 0

if auth == Auth.DEVICE:
key_meta = dev_state.device_id
key = database.bt_gatt_device_key(infuse_id, gps_time)
flags |= Flags.ENCR_DEVICE
else:
key_meta = dev_state.network_id
key = database.bt_gatt_network_key(infuse_id, gps_time)

# Construct GATT header
header = cls()
header._type = ptype
header.flags = flags
header.device_id = infuse_id
header.key_metadata = key_meta
header.gps_time = gps_time
header.sequence = dev_state.gatt_sequence_num()
header.entropy = random.randint(0, 65535)

# Encrypt and return payload
header_bytes = bytes(header)
ciphertext = chachapoly_encrypt(
key, header_bytes[:11], header_bytes[11:], payload
)
return header_bytes + ciphertext

@classmethod
def decrypt(
cls, database: DeviceDatabase, bt_addr: Address.BluetoothLeAddr, frame: bytes
Expand Down
7 changes: 6 additions & 1 deletion src/infuse_iot/rpc_wrappers/time_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ def handle_response(self, return_code, response):

t_remote = InfuseTime.unix_time_from_epoch(response.epoch_time)
t_local = time.time()
sync_age = (
f"{response.sync_age} seconds ago"
if response.sync_age != 2**32 - 1
else "Never"
)

print(f"\t Source: {InfuseTimeSource(response.time_source)}")
print(f"\tRemote Time: {InfuseTime.utc_time_string(t_remote)}")
print(f"\t Local Time: {InfuseTime.utc_time_string(t_local)}")
print(f"\t Synced: {response.sync_age} seconds ago")
print(f"\t Synced: {sync_age}")
Loading