From c52b7866c81b6500577440a8cd0a8b92984360f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?poljar=20=28Damir=20Jeli=C4=87=29?= Date: Mon, 5 Mar 2018 19:34:51 +0100 Subject: [PATCH] Add olm account loading/creation and a initial olm command. --- main.py | 1 + matrix/commands.py | 3 + matrix/encryption.py | 176 +++++++++++++++++++++++++++++++++++++++++++ matrix/globals.py | 1 + matrix/server.py | 29 +++++++ 5 files changed, 210 insertions(+) create mode 100644 matrix/encryption.py diff --git a/main.py b/main.py index d14ed1b..eab1e41 100644 --- a/main.py +++ b/main.py @@ -36,6 +36,7 @@ from matrix.colors import Formatted from matrix.utf import utf8_decode from matrix.http import HttpResponse from matrix.api import MatrixSendMessage +from matrix.encryption import matrix_olm_command_cb # Weechat searches for the registered callbacks in the scope of the main script # file, import the callbacks here so weechat can find them. diff --git a/matrix/commands.py b/matrix/commands.py index dd7a440..958f180 100644 --- a/matrix/commands.py +++ b/matrix/commands.py @@ -32,6 +32,7 @@ from matrix.utils import key_from_value, tags_from_line_data from matrix.plugin_options import DebugType from matrix.server import MatrixServer from matrix.colors import Formatted +from matrix.encryption import matrix_hook_olm_command def hook_commands(): @@ -98,6 +99,8 @@ def hook_commands(): "matrix_me_command_cb", "") + matrix_hook_olm_command() + W.hook_command_run('/topic', 'matrix_command_topic_cb', '') W.hook_command_run('/buffer clear', 'matrix_command_buf_clear_cb', '') W.hook_command_run('/join', 'matrix_command_join_cb', '') diff --git a/matrix/encryption.py b/matrix/encryption.py new file mode 100644 index 0000000..9291b5f --- /dev/null +++ b/matrix/encryption.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +# Weechat Matrix Protocol Script +# Copyright © 2018 Damir Jelić +# +# 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 __future__ import unicode_literals + +import os + +# pylint: disable=redefined-builtin +from builtins import str + +from functools import wraps +from future.moves.itertools import zip_longest + +import matrix.globals + +try: + from olm.account import Account, OlmAccountError +except ImportError: + matrix.globals.ENCRYPTION = False + +from matrix.globals import W, SERVERS +from matrix.utf import utf8_decode + + +def own_buffer(f): + + @wraps(f) + def wrapper(data, buffer, *args, **kwargs): + + for server in SERVERS.values(): + if buffer in server.buffers.values(): + return f(server.name, buffer, *args, **kwargs) + elif buffer == server.server_buffer: + return f(server.name, buffer, *args, **kwargs) + + return W.WEECHAT_RC_OK + + return wrapper + + +def encrypt_enabled(f): + + @wraps(f) + def wrapper(*args, **kwds): + if matrix.globals.ENCRYPTION: + return f(*args, **kwds) + return None + + return wrapper + + +@encrypt_enabled +def matrix_hook_olm_command(): + W.hook_command( + # Command name and short description + "olm", + "Matrix olm encryption command", + # Synopsis + ("info all|blacklisted|private|unverified|verified ||" + "blacklist ||" + "unverify ||" + "verify "), + # Description + (" info: show info about known devices and their keys\n" + "blacklist: blacklist a device\n" + " unverify: unverify a device\n" + " verify: verify a device\n\n" + "Examples:\n"), + # Completions + ('info all|blacklisted|private|unverified|verified ||' + 'blacklist %(device_ids) ||' + 'unverify %(device_ids) ||' + 'verify %(device_ids)'), + # Function name + 'matrix_olm_command_cb', + '') + + +def olm_cmd_parse_args(args): + split_args = args.split() + + command = split_args.pop(0) if split_args else "info" + + rest_args = split_args if split_args else [] + + return command, rest_args + + +def grouper(iterable, n, fillvalue=None): + "Collect data into fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return zip_longest(*args, fillvalue=fillvalue) + + +def partition_key(key): + groups = grouper(key, 4, " ") + return ' '.join(''.join(g) for g in groups) + + +@own_buffer +@utf8_decode +def matrix_olm_command_cb(server_name, buffer, args): + server = SERVERS[server_name] + command, args = olm_cmd_parse_args(args) + + if not command or command == "info": + olm = server.olm + device_msg = (" - Device ID: {}\n".format(server.device_id) + if server.device_id else "") + id_key = partition_key(olm.account.identity_keys()["curve25519"]) + fp_key = partition_key(olm.account.identity_keys()["ed25519"]) + message = ("{prefix}matrix: Identity keys:\n" + " - User: {user}\n" + "{device_msg}" + " - Identity key: {id_key}\n" + " - Fingerprint key: {fp_key}\n").format( + prefix=W.prefix("network"), + user=server.user, + device_msg=device_msg, + id_key=id_key, + fp_key=fp_key) + W.prnt(server.server_buffer, message) + else: + message = ("{prefix}matrix: Command not implemented.".format( + prefix=W.prefix("error"))) + W.prnt(server.server_buffer, message) + + return W.WEECHAT_RC_OK + + +class EncryptionError(Exception): + pass + + +class Olm(): + + @encrypt_enabled + def __init__(self, server, account=None): + # type: (Server, Account) -> None + self.server = server + if account: + self.account = account + else: + self.account = Account() + + @classmethod + @encrypt_enabled + def from_session_dir(cls, server): + # type: (Server) -> Olm + account_file_name = "{}_{}.account".format(server.user, + server.device_id) + session_path = server.get_session_path() + path = os.path.join(session_path, account_file_name) + + try: + with open(path, "rb") as f: + pickle = f.read() + account = Account.from_pickle(pickle) + return cls(server, account) + except OlmAccountError as error: + raise EncryptionError(error) diff --git a/matrix/globals.py b/matrix/globals.py index 9c125b7..e7f7a29 100644 --- a/matrix/globals.py +++ b/matrix/globals.py @@ -31,3 +31,4 @@ except ImportError: OPTIONS = PluginOptions() # type: PluginOptions SERVERS = dict() # type: Dict[str, MatrixServer] CONFIG = None # type: weechat.config +ENCRYPTION = True # type: bool diff --git a/matrix/server.py b/matrix/server.py index 668f797..4902ab5 100644 --- a/matrix/server.py +++ b/matrix/server.py @@ -35,6 +35,13 @@ from matrix.globals import W, SERVERS, OPTIONS import matrix.api as API from matrix.api import MatrixClient, MatrixSyncMessage, MatrixLoginMessage +from matrix.encryption import Olm, EncryptionError + +try: + FileNotFoundError +except NameError: + FileNotFoundError = IOError + class MatrixServer: # pylint: disable=too-many-instance-attributes @@ -48,6 +55,7 @@ class MatrixServer: self.options = dict() # type: Dict[str, weechat.config] self.device_name = "Weechat Matrix" # type: str self.device_id = "" # type: str + self.olm = None # type: Olm self.user = "" # type: str self.password = "" # type: str @@ -126,6 +134,24 @@ class MatrixServer: with open(path, 'w') as f: f.write(self.device_id) + def _load_olm(self): + try: + self.olm = Olm.from_session_dir(self) + except FileNotFoundError: + message = ("{prefix}matrix: Creating new Olm identity for {user}" + " on {server} for device {device}.").format( + prefix=W.prefix("network"), + user=self.user, + server=self.name, + device=self.device_id) + W.prnt("", message) + self.olm = Olm(self) + except EncryptionError as error: + message = ("{prefix}matrix: Error loading Olm" + "account: {error}.").format( + prefix=W.prefix("error"), error=error) + W.prnt("", message) + def _create_options(self, config_file): options = [ Option('autoconnect', 'boolean', '', 0, 0, 'off', @@ -200,6 +226,9 @@ class MatrixServer: self._load_device_id() + if self.device_id: + self._load_olm() + elif option_name == "password": value = W.config_string(option) self.password = W.string_eval_expression(value, {}, {}, {})