matrix-nio/nio/crypto/device.py

210 lines
6.5 KiB
Python

# -*- coding: utf-8 -*-
# Copyright © 2020 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from collections import defaultdict
from dataclasses import asdict, dataclass, field
from enum import Enum
from typing import DefaultDict, Dict, Iterator, KeysView, Optional
# TODO document the values better.
class TrustState(Enum):
"""The device trust state.
An Enum holding differing values that a device trust state can be in.
"""
unset = 0
verified = 1
blacklisted = 2
ignored = 3
@dataclass
class OlmDevice:
"""Class holding info about users Olm devices.
OlmDevices represent user devices with which we can communicate in an
encrypted manner. To do so an OlmDevice needs to have its trust state set.
The trust state can be set to one of "verified", "ignored", or
"blacklisted".
Note that the trust state should never be moddified directly on an
OlmDevice, all the attributes here are read only.
The trust state can be changed by pasing the OlmDevice to a nio Client or a
MatrixStore class.
Attributes:
user_id (str): The id of the user that the device belongs to.
device_id (str): The device id that combined with the user id uniquely
identifies the device.
keys (Dict): A dictionary containing the type and the public part
of this devices encryption keys.
display_name (str): The human readable name of this device.
deleted (bool): A boolean signaling if this device has been deleted by
its owner.
trust_state (TrustState): The trust state of this device.
"""
user_id: str = field()
device_id: str = field()
keys: Dict[str, str] = field()
display_name: str = ""
deleted: bool = False
trust_state: TrustState = TrustState.unset
@property
def id(self) -> str:
"""The device id.
Same as the device_id attribute.
"""
return self.device_id
@property
def ed25519(self) -> str:
"""The ed25519 fingerprint key of the device."""
return self.keys["ed25519"]
@ed25519.setter
def ed25519(self, new_value):
self.keys["ed25519"] = new_value
@property
def curve25519(self) -> str:
"""The curve25519 key of the device."""
return self.keys["curve25519"]
@curve25519.setter
def curve25519(self, new_value):
self.keys["curve25519"] = new_value
def as_dict(self):
"""Convert the OlmDevice into a dictionary."""
device = asdict(self)
device["trust_state"] = self.trust_state.name
return device
@property
def verified(self) -> bool:
"""Is the device verified."""
return self.trust_state == TrustState.verified
@property
def ignored(self) -> bool:
"""Is the device ignored."""
return self.trust_state == TrustState.ignored
@property
def blacklisted(self) -> bool:
"""Is the device blacklisted."""
return self.trust_state == TrustState.blacklisted
class DeviceStore:
"""A store that holds olm devices in memory.
The DeviceStore class implements the iter method, devices can be iterated
over normaly using:
>>> for device in device_store:
... print(device.user_id, device.device_id)
To get only non-deleted devices of a user the active_user_devices method
can be used:
>>> for device in device_store.active_user_devices("@bob:example.org"):
... print(device.user_id, device.device_id)
"""
def __init__(self):
self._entries: DefaultDict[str, Dict[str, OlmDevice]] = defaultdict(dict)
def __iter__(self) -> Iterator[OlmDevice]:
for user_devices in self._entries.values():
for device in user_devices.values():
yield device
def __getitem__(self, user_id: str) -> Dict[str, OlmDevice]:
return self._entries[user_id]
def items(self):
"""List of tuples in the form (user id, dict(device_id, OlmDevice)."""
return self._entries.items()
def values(self):
"""List of devices in the form of a dict(device_id, OlmDevice)."""
return self._entries.values()
def active_user_devices(self, user_id: str) -> Iterator[OlmDevice]:
"""Get all the non-deleted devices of a user.
Args:
user_id (str): The user for which we would like to get the devices
for.
This returns an iterator over all the non-deleted devices of the given
user.
"""
for device in self._entries[user_id].values():
if not device.deleted:
yield device
def device_from_sender_key(
self, user_id: str, sender_key: str
) -> Optional[OlmDevice]:
"""Get a non-deleted device of a user with the matching sender key.
Args:
user_id (str): The user id of the device owner.
sender_key (str): The encryption key that is owned by the device,
usually a curve25519 public key.
"""
for device in self.active_user_devices(user_id):
if device.curve25519 == sender_key:
return device
return None
@property
def users(self) -> KeysView[str]:
"""Get the list of users that the device store knows about."""
return self._entries.keys()
def devices(self, user_id: str) -> KeysView[str]:
return self._entries[user_id].keys()
def add(self, device: OlmDevice) -> bool:
"""Add the given device to the store.
Args:
device (OlmDevice): The device that should be added to the store.
Returns True if the device was added to the store, False if it already
was in the store.
"""
if device in self:
return False
self._entries[device.user_id][device.id] = device
return True