Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b0fa2a6
track the device list of users and download keys
Zil0 May 24, 2018
c8496d3
add device tracking tests
Zil0 Jun 24, 2018
69d300e
track devices of users in encrypted rooms
Zil0 Jun 12, 2018
9aadf1b
build doc for crypto.device_list
Zil0 Jun 29, 2018
165d4f4
add olm encryption, decryption and session
Zil0 May 23, 2018
c689ec1
add olm tests
Zil0 Jun 25, 2018
eb43d6c
add olm_ensure_sessions
Zil0 May 30, 2018
d3896b4
add megolm outbound session support
Zil0 May 28, 2018
f18f9b2
delete outbound sessions on leave
Zil0 Jun 18, 2018
de73b9d
send encrypted group messages
Zil0 Jun 5, 2018
c07ee43
automatically send encrypted messages in encrypted rooms
Zil0 Jun 5, 2018
24b5a40
add megolm outbound tests
Zil0 Jun 27, 2018
4ce7cdf
handle m.room.encryption properties
Zil0 Jun 29, 2018
71e8c52
build doc for crypto.megolm_outbound_session
Zil0 Jun 29, 2018
2515d57
pass rotation parameters when enabling encryption
Zil0 Jul 20, 2018
e649a23
add megolm inbound session support
Zil0 Jun 8, 2018
a28b29d
automatically decrypt encrypted events
Zil0 Jun 4, 2018
a377517
add megolm inbound tests
Zil0 Jun 28, 2018
ca08e86
persist olm account
Zil0 Jun 14, 2018
16ed294
build doc for crypto.crypto_store
Zil0 Jun 29, 2018
55e8e09
add account persistence tests
Zil0 Jun 29, 2018
9e0ad6f
prevent tests from using database
Zil0 Jun 29, 2018
05823ea
persist olm sessions
Zil0 Jun 14, 2018
33a06d8
add olm persistence tests
Zil0 Jun 30, 2018
f8487bd
persist megolm inbound sessions
Zil0 Jun 14, 2018
4bc2473
add megolm inbound persistence tests
Zil0 Jul 1, 2018
93f408a
persist megolm outbound sessions
Zil0 Jun 14, 2018
0ed22c1
add outbound sessions persistence tests
Zil0 Jul 2, 2018
46405db
add device keys persistence
Zil0 Jul 4, 2018
64134d7
add device keys persistence tests
Zil0 Jul 4, 2018
1804f7a
ignore optional dependency appdirs when building doc
Zil0 Jul 4, 2018
41e35d1
general improvement to CryptoStore
Zil0 Jul 21, 2018
2d7b271
better primary keys in crypto store
Zil0 Jul 25, 2018
cddd13a
restore device ID from user ID in CryptoStore
Zil0 Aug 3, 2018
bb10897
turn on SQLite secure_delete
Zil0 Aug 7, 2018
9567772
add m.file missing required key
Zil0 Jul 6, 2018
77d36f4
add encrypted attachments dependencies
Zil0 Jul 6, 2018
db8f378
encrypted attachments support
Zil0 Jul 6, 2018
b236ea2
plug-in encrypted attachments
Zil0 Jul 6, 2018
8d4f284
automatically send encrypted m.file messages
Zil0 Aug 16, 2018
656005b
add Device class
Zil0 Jul 15, 2018
6bce7b4
add devices attribute to User
Zil0 Jul 22, 2018
0c76655
make OlmDevice subclass Device
Zil0 Jul 15, 2018
f2b25e8
better device keys handling
Zil0 Jul 15, 2018
b27dfa8
remember signing key when establishing Megolm Inbound Session
Zil0 Jul 28, 2018
6d31cce
configure device verification
Zil0 Jul 28, 2018
c1c8aca
persist verification info
Zil0 Jul 28, 2018
e4f417f
alert of unknown devices when sending encrypted messages
Zil0 Jul 28, 2018
b5e1b7a
add device verification checks
Zil0 Jul 28, 2018
0492e95
persist device upon verification
Zil0 Aug 3, 2018
ba0950a
add Device class docstring
Zil0 Aug 7, 2018
93d7b0e
add Device class tests
Zil0 Aug 7, 2018
cdffc9f
add get_fingerprint method to client
Zil0 Aug 9, 2018
0fdfdd6
fixup! add devices attribute to User
Zil0 Sep 14, 2018
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
3 changes: 2 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,5 @@
'Miscellaneous'),
]

autodoc_mock_imports = ["olm", "canonicaljson"]
autodoc_mock_imports = ["olm", "canonicaljson", "appdirs", "unpaddedbase64", "Crypto",
"Crypto.Cipher", "Crypto.Hash", "Crypto.Util"]
25 changes: 25 additions & 0 deletions docs/source/matrix_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,28 @@ matrix_client.crypto
:members:
:undoc-members:
:show-inheritance:

.. automodule:: matrix_client.crypto.device_list
:members:
:undoc-members:
:show-inheritance:

.. automodule:: matrix_client.crypto.sessions
:members:
:undoc-members:
:show-inheritance:

.. automodule:: matrix_client.crypto.crypto_store
:members:
:undoc-members:
:show-inheritance:

.. automodule:: matrix_client.crypto.encrypt_attachments
:members:
:undoc-members:
:show-inheritance:

.. automodule:: matrix_client.crypto.verified_event
:members:
:undoc-members:
:show-inheritance:
62 changes: 42 additions & 20 deletions matrix_client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,19 +329,43 @@ def redact_event(self, room_id, event_id, reason=None, txn_id=None, timestamp=No
# content_type can be a image,audio or video
# extra information should be supplied, see
# https://matrix.org/docs/spec/r0.0.1/client_server.html
def send_content(self, room_id, item_url, item_name, msg_type,
extra_information=None, timestamp=None):
def send_content(self, room_id, item_url, item_name, msg_type, filename=None,
extra_information=None, timestamp=None, encryption_info=None):
content_pack = self.get_content_body(item_url, item_name, msg_type, filename,
extra_information, encryption_info)
return self.send_message_event(room_id, "m.room.message", content_pack,
timestamp=timestamp)

def get_content_body(self, item_url, item_name, msg_type, filename=None,
extra_information=None, encryption_info=None):
if extra_information is None:
extra_information = {}

content_pack = {
"url": item_url,
"msgtype": msg_type,
"body": item_name,
"info": extra_information
}
return self.send_message_event(room_id, "m.room.message", content_pack,
timestamp=timestamp)
if msg_type == "m.file":
content_pack["filename"] = filename or item_name
if encryption_info:
encryption_info['url'] = item_url
content_pack['file'] = encryption_info
else:
content_pack['url'] = item_url
return content_pack

def get_location_body(self, geo_uri, name, thumb_url=None, thumb_info=None):
content_pack = {
"geo_uri": geo_uri,
"msgtype": "m.location",
"body": name,
}
if thumb_url:
content_pack["thumbnail_url"] = thumb_url
if thumb_info:
content_pack["thumbnail_info"] = thumb_info
return content_pack

# http://matrix.org/docs/spec/client_server/r0.2.0.html#m-location
def send_location(self, room_id, geo_uri, name, thumb_url=None, thumb_info=None,
Expand All @@ -356,15 +380,8 @@ def send_location(self, room_id, geo_uri, name, thumb_url=None, thumb_info=None,
thumb_info (dict): Metadata about the thumbnail, type ImageInfo.
timestamp (int): Set origin_server_ts (For application services only)
"""
content_pack = {
"geo_uri": geo_uri,
"msgtype": "m.location",
"body": name,
}
if thumb_url:
content_pack["thumbnail_url"] = thumb_url
if thumb_info:
content_pack["thumbnail_info"] = thumb_info
content_pack = self.get_location_body(
geo_uri, name, thumb_url, thumb_info)

return self.send_message_event(room_id, "m.room.message", content_pack,
timestamp=timestamp)
Expand Down Expand Up @@ -405,12 +422,11 @@ def send_notice(self, room_id, text_content, timestamp=None):
text_content (str): The m.notice body to send.
timestamp (int): Set origin_server_ts (For application services only)
"""
body = {
"msgtype": "m.notice",
"body": text_content
}
return self.send_message_event(room_id, "m.room.message", body,
timestamp=timestamp)
return self.send_message_event(
room_id, "m.room.message",
self.get_notice_body(text_content),
timestamp=timestamp
)

def get_room_messages(self, room_id, token, direction, limit=10, to=None):
"""Perform GET /rooms/{roomId}/messages.
Expand Down Expand Up @@ -675,6 +691,12 @@ def get_emote_body(self, text):
"body": text
}

def get_notice_body(self, text):
return {
"msgtype": "m.notice",
"body": text
}

def get_filter(self, user_id, filter_id):
return self._send("GET", "/user/{userId}/filter/{filterId}"
.format(userId=user_id, filterId=filter_id))
Expand Down
78 changes: 71 additions & 7 deletions matrix_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .api import MatrixHttpApi
from .checks import check_user_id
from .errors import MatrixRequestError, MatrixUnexpectedResponse
from .room import Room
from .user import User
Expand Down Expand Up @@ -63,6 +64,13 @@ class MatrixClient(object):
encryption_conf (dict): Optional. Configuration parameters for encryption.
Refer to :func:`~matrix_client.crypto.olm_device.OlmDevice` for supported
options, since it will be passed to this class.
restore_device_id (bool): Optional. Only valid when encryption is enabled. When
turned on, the device ID corresponding to the user ID will be retrieved from
the encryption database, if it exists.
verify_devices (bool): Optional. When enabled, sending a message will fail when
there are unknown devices in an encrypted room. A client will have to
inspect those, and resend its message. Note that this can be configured later
on a per room basis.

Returns:
`MatrixClient`
Expand Down Expand Up @@ -111,7 +119,8 @@ def global_callback(incoming_event):

def __init__(self, base_url, token=None, user_id=None,
valid_cert_check=True, sync_filter_limit=20,
cache_level=CACHE.ALL, encryption=False, encryption_conf=None):
cache_level=CACHE.ALL, encryption=False, encryption_conf=None,
restore_device_id=False, verify_devices=False):
if user_id:
warn(
"user_id is deprecated. "
Expand All @@ -121,6 +130,9 @@ def __init__(self, base_url, token=None, user_id=None,
if encryption and not ENCRYPTION_SUPPORT:
raise ValueError("Failed to enable encryption. Please make sure the olm "
"library is available.")
if restore_device_id and not encryption:
raise ValueError("restore_device_id only makes sense when encryption is "
"enabled.")

self.api = MatrixHttpApi(base_url, token)
self.api.validate_certificate(valid_cert_check)
Expand All @@ -133,6 +145,9 @@ def __init__(self, base_url, token=None, user_id=None,
self._encryption = encryption
self.encryption_conf = encryption_conf or {}
self.olm_device = None
self.first_sync = True
self.restore_device_id = restore_device_id
self.verify_devices = verify_devices
if isinstance(cache_level, CACHE):
self._cache_level = cache_level
else:
Expand Down Expand Up @@ -265,15 +280,32 @@ def login(self, username, password, limit=10, sync=True, device_id=None):
limit (int): Deprecated. How many messages to return when syncing.
This will be replaced by a filter API in a later release.
sync (bool): Optional. Whether to initiate a /sync request after logging in.
device_id (str): Optional. ID of the client device. The server will
auto-generate a device_id if this is not specified.
device_id (str): Optional. ID of the client device. If it is not specified,
the server will auto-generate one, or it may be retrieved
from database if ``restore_device_id`` is ``True``. If it is specified,
and ``restore_device_id`` is ``True``, the eventual encryption keys stored
along with a previous device ID of the current user are discarded.

Returns:
str: Access token

Raises:
MatrixRequestError
"""
if not device_id and self.restore_device_id:
try:
check_user_id(username)
except ValueError:
raise ValueError("When using restore_device_id, a full user ID "
"must be supplied when logging in.")
try:
self.olm_device = OlmDevice(
self.api, username, **self.encryption_conf)
device_id = self.olm_device.device_id
logger.info('Device ID was sucessfully retrieved from database.')
except ValueError:
pass

response = self.api.login(
"m.login.password", user=username, password=password, device_id=device_id
)
Expand All @@ -284,8 +316,9 @@ def login(self, username, password, limit=10, sync=True, device_id=None):
self.device_id = response["device_id"]

if self._encryption:
self.olm_device = OlmDevice(
self.api, self.user_id, self.device_id, **self.encryption_conf)
if not self.olm_device:
self.olm_device = OlmDevice(
self.api, self.user_id, self.device_id, **self.encryption_conf)
self.olm_device.upload_identity_keys()
self.olm_device.upload_one_time_keys()

Expand Down Expand Up @@ -566,7 +599,7 @@ def upload(self, content, content_type, filename=None):
)

def _mkroom(self, room_id):
room = Room(self, room_id)
room = Room(self, room_id, verify_devices=self.verify_devices)
if self._encryption:
try:
event = self.api.get_state_event(room_id, "m.room.encryption")
Expand All @@ -581,8 +614,21 @@ def _mkroom(self, room_id):
# TODO better handling of the blocking I/O caused by update_one_time_key_counts
def _sync(self, timeout_ms=30000):
response = self.api.sync(self.sync_token, timeout_ms, filter=self.sync_filter)

if self._encryption and 'device_lists' in response:
if response['device_lists'].get('changed'):
self.olm_device.device_list.update_user_device_keys(
response['device_lists']['changed'], self.sync_token)
if response['device_lists'].get('left'):
self.olm_device.device_list.stop_tracking_users(
response['device_lists']['left'])

self.sync_token = response["next_batch"]

if self._encryption and self.first_sync:
self.first_sync = False
self.olm_device.device_list.update_after_restart(self.sync_token)

for presence_update in response['presence']['events']:
for callback in self.presence_listeners.values():
callback(presence_update)
Expand All @@ -597,6 +643,11 @@ def _sync(self, timeout_ms=30000):
if room_id in self.rooms:
del self.rooms[room_id]

if 'to_device' in response:
for event in response['to_device']['events']:
if event['type'] == 'm.room.encrypted' and self._encryption:
self.olm_device.olm_handle_encrypted_event(event)

if self._encryption and 'device_one_time_keys_count' in response:
self.olm_device.update_one_time_key_counts(
response['device_one_time_keys_count'])
Expand Down Expand Up @@ -627,6 +678,10 @@ def _sync(self, timeout_ms=30000):
):
listener['callback'](event)

if self._encryption and room.encrypted:
# Track the new users in the room
self.olm_device.device_list.track_pending_users()

for event in sync_room['ephemeral']['events']:
event['room_id'] = room_id
room._put_ephemeral_event(event)
Expand All @@ -650,7 +705,7 @@ def get_user(self, user_id):
"""
warn("get_user is deprecated. Directly instantiate a User instead.",
DeprecationWarning)
return User(self.api, user_id)
return User(self, user_id)

# TODO: move to Room class
def remove_room_alias(self, room_alias):
Expand All @@ -667,3 +722,12 @@ def remove_room_alias(self, room_alias):
return True
except MatrixRequestError:
return False

def get_fingerprint(self):
"""Get the fingerprint of the current device.

This is used when verifying devices.
"""
if not self._encryption:
raise ValueError("Encryption is not enabled, this device has no fingerprint.")
return self.olm_device.ed25519
Loading