diff --git a/omemo/file_crypto.py b/omemo/file_crypto.py deleted file mode 100644 index 62c1837..0000000 --- a/omemo/file_crypto.py +++ /dev/null @@ -1,248 +0,0 @@ -# Copyright (C) 2019 Philipp Hörist -# -# This file is part of OMEMO Gajim Plugin. -# -# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published -# by the Free Software Foundation; version 3 only. -# -# OMEMO Gajim Plugin is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OMEMO Gajim Plugin. If not, see . - -import hashlib -import logging -import binascii -from pathlib import Path -from urllib.parse import urlparse -from urllib.parse import unquote - -from gi.repository import GLib -from gi.repository import Soup - -from gajim.common import configpaths -from gajim.common.helpers import write_file_async -from gajim.common.helpers import open_file -from gajim.common.const import URIType -from gajim.common.const import FTState -from gajim.common.filetransfer import FileTransfer -from gajim.plugins.plugins_i18n import _ -from gajim.gui.dialogs import DialogButton -from gajim.gui.dialogs import ConfirmationDialog -from gajim.gui.filetransfer_progress import FileTransferProgress - -from omemo.backend.aes import aes_decrypt_file - - -log = logging.getLogger('gajim.p.omemo.filedecryption') - -DIRECTORY = Path(configpaths.get('MY_DATA')) / 'downloads' - - -class FileDecryption: - def __init__(self, plugin): - self.plugin = plugin - self.window = None - self._session = Soup.Session() - - def hyperlink_handler(self, uri, instance, window): - if uri.type != URIType.WEB: - return - self.window = window - - urlparts = urlparse(uri.data) - if urlparts.scheme != 'aesgcm': - log.info('URL not encrypted: %s', uri.data) - return - - try: - key, iv = self._parse_fragment(urlparts.fragment) - except ValueError: - log.info('URL not encrypted: %s', uri.data) - return - - file_path = self._get_file_path(uri.data, urlparts) - if file_path.exists(): - instance.plugin_modified = True - self._show_file_open_dialog(file_path) - return - - file_path.parent.mkdir(mode=0o700, exist_ok=True) - - transfer = OMEMODownload(instance.account, - urlparts, - file_path, - key, - iv) - - transfer.connect('cancel', self._cancel_download) - FileTransferProgress(transfer) - - self._download_content(transfer) - instance.plugin_modified = True - - def _download_content(self, transfer): - log.info('Start downloading: %s', transfer.request_uri) - transfer.set_started() - message = transfer.get_soup_message() - message.connect('got-headers', self._on_got_headers, transfer) - message.connect('got-chunk', self._on_got_chunk, transfer) - - self._session.queue_message(message, self._on_finished, transfer) - - def _cancel_download(self, transfer, _signalname): - message = transfer.get_soup_message() - self._session.cancel_message(message, Soup.Status.CANCELLED) - transfer.set_cancelled() - - @staticmethod - def _on_got_headers(message, transfer): - transfer.set_in_progress() - size = message.props.response_headers.get_content_length() - transfer.size = size - - def _on_got_chunk(self, message, chunk, transfer): - transfer.set_chunk(chunk.get_data()) - if transfer.size: - # This gets called even when the requested file is not found - # So only update the progress if the file was actually found and - # we know the size - transfer.update_progress() - - self._session.pause_message(message) - GLib.idle_add(self._session.unpause_message, message) - - def _on_finished(self, _session, message, transfer): - if message.props.status_code == Soup.Status.CANCELLED: - log.info('Download cancelled') - return - - if message.status_code != Soup.Status.OK: - log.warning('Download failed: %s', transfer.request_uri) - log.warning(Soup.Status.get_phrase(message.status_code)) - error_text = _('Download failed: %s') % transfer.request_uri - transfer.set_error('http-error', error_text) - return - - data = message.props.response_body_data.get_data() - if data is None: - return - - decrypted_data = aes_decrypt_file(transfer.key, - transfer.iv, - data) - - write_file_async(transfer.path, - decrypted_data, - self._on_decrypted, - transfer) - - transfer.set_decrypting() - - def _on_decrypted(self, _result, error, transfer): - if error is not None: - log.error('%s: %s', transfer.path, error) - return - transfer.set_finished() - self._show_file_open_dialog(transfer.path) - - def _show_file_open_dialog(self, file_path): - def _open_file(): - open_file(file_path) - - def _open_folder(): - open_file(file_path.parent) - - ConfirmationDialog( - _('Open File'), - _('Open File?'), - _('Do you want to open %s?') % file_path.name, - [DialogButton.make('Cancel', - text=_('_No')), - DialogButton.make('OK', - text=_('Open _Folder'), - callback=_open_folder), - DialogButton.make('Accept', - text=_('_Open'), - callback=_open_file)], - transient_for=self.window).show() - - @staticmethod - def _parse_fragment(fragment): - if not fragment: - raise ValueError('Invalid fragment') - - fragment = binascii.unhexlify(fragment) - size = len(fragment) - # Clients started out with using a 16 byte IV but long term - # want to swtich to the more performant 12 byte IV - # We have to support both - if size == 48: - key = fragment[16:] - iv = fragment[:16] - elif size == 44: - key = fragment[12:] - iv = fragment[:12] - else: - raise ValueError('Invalid fragment size: %s' % size) - - return key, iv - - @staticmethod - def _get_file_path(uri, urlparts): - path = Path(unquote(urlparts.path)) - stem = path.stem - extension = path.suffix - - if len(stem) > 90: - # Many Filesystems have a limit on filename length - # Most have 255, some encrypted ones only 143 - # We add around 50 chars for the hash, - # so the filename should not exceed 90 - stem = stem[:90] - - name_hash = hashlib.sha1(str(uri).encode()).hexdigest() - - hash_filename = '%s_%s%s' % (stem, name_hash, extension) - - file_path = DIRECTORY / hash_filename - return file_path - - -class OMEMODownload(FileTransfer): - - _state_descriptions = { - FTState.DECRYPTING: _('Decrypting file…'), - FTState.STARTED: _('Downloading…'), - } - - def __init__(self, account, urlparts, path, key, iv): - FileTransfer.__init__(self, account) - - self._urlparts = urlparts - self.path = path - self.iv = iv - self.key = key - - self._message = None - - @property - def request_uri(self): - urlparts = self._urlparts._replace(scheme='https', fragment='') - return urlparts.geturl() - - @property - def filename(self): - return Path(self._urlparts.path).name - - def set_chunk(self, bytes_): - self._seen += len(bytes_) - - def get_soup_message(self): - if self._message is None: - self._message = Soup.Message.new('GET', self.request_uri) - return self._message diff --git a/omemo/plugin.py b/omemo/plugin.py index 754641b..662d817 100644 --- a/omemo/plugin.py +++ b/omemo/plugin.py @@ -62,7 +62,6 @@ except Exception as error: if not ERROR_MSG: try: from omemo.modules import omemo - from omemo import file_crypto from omemo.gtk.key import KeyDialog from omemo.gtk.config import OMEMOConfigDialog from omemo.backend.aes import aes_encrypt_file @@ -97,7 +96,6 @@ class OmemoPlugin(GajimPlugin): self.modules = [omemo] self.config_dialog = OMEMOConfigDialog(self) self.gui_extension_points = { - 'hyperlink_handler': (self._file_decryption, None), 'encrypt' + self.encryption_name: (self._encrypt_message, None), 'gc_encrypt' + self.encryption_name: ( self._muc_encrypt_message, None), @@ -207,10 +205,6 @@ class OmemoPlugin(GajimPlugin): return self.get_omemo(account).encrypt_message(conn, obj, callback, False) - def _file_decryption(self, uri, instance, window): - file_crypto.FileDecryption(self).hyperlink_handler( - uri, instance, window) - def encrypt_file(self, file, _account, callback): thread = threading.Thread(target=self._encrypt_file_thread, args=(file, callback))