diff options
author | Philipp Hörist <forenjunkie@chello.at> | 2017-03-26 22:09:24 +0300 |
---|---|---|
committer | Philipp Hörist <forenjunkie@chello.at> | 2017-03-26 22:09:24 +0300 |
commit | 750528068f142341b730d430735ffd70a9f030ce (patch) | |
tree | 7f5a76cddf89b5171f0599b063bf9d9da5fde6ca /omemo | |
parent | 65e2f437a5c7d0296ccf4422d03d6e3c84741a5a (diff) |
[omemo] Add file decryption
Diffstat (limited to 'omemo')
-rw-r--r-- | omemo/file_decryption.py | 272 | ||||
-rw-r--r-- | omemo/omemoplugin.py | 8 | ||||
-rw-r--r-- | omemo/upload_progress_dialog.ui | 106 |
3 files changed, 385 insertions, 1 deletions
diff --git a/omemo/file_decryption.py b/omemo/file_decryption.py new file mode 100644 index 0000000..d0c8236 --- /dev/null +++ b/omemo/file_decryption.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Philipp Hörist <philipp@hoerist.com> +# +# This file is part of Gajim-OMEMO plugin. +# +# The Gajim-OMEMO 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, either version 3 of the License, or (at your option) any +# later version. +# +# Gajim-OMEMO 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 +# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import hashlib +import logging +import socket +import threading +import platform +import subprocess +import binascii +from urllib.request import urlopen +from urllib.error import URLError +from urllib.parse import urlparse, urldefrag +from io import BufferedWriter, FileIO, BytesIO + +from gi.repository import GLib +import gtkgui_helpers +from common import configpaths +from dialogs import ErrorDialog, YesNoDialog +if os.name == 'nt': + import certifi + +log = logging.getLogger('gajim.plugin_system.omemo.filedecryption') + +ERROR = False +try: + from cryptography.hazmat.primitives.ciphers import Cipher + from cryptography.hazmat.primitives.ciphers import algorithms + from cryptography.hazmat.primitives.ciphers.modes import GCM + from cryptography.hazmat.backends import default_backend +except ImportError: + log.exception('ImportError') + ERROR = True + +DIRECTORY = os.path.join(configpaths.gajimpaths['MY_DATA'], 'downloads') + +try: + if not os.path.exists(DIRECTORY): + os.makedirs(DIRECTORY) +except Exception: + ERROR = True + log.exception('Error') + + +class File: + def __init__(self, url): + self.url, self.fragment = urldefrag(url) + self.key = None + self.iv = None + self.filepath = None + self.filename = None + + +class FileDecryption: + def __init__(self, plugin): + self.plugin = plugin + self.window = None + + def hyperlink_handler(self, url, kind, instance, window): + if ERROR or kind != 'url': + return + self.window = window + urlparts = urlparse(url) + file = File(urlparts.geturl()) + + if urlparts.scheme not in ["https"] or not urlparts.netloc: + log.info("Not accepting URL for decryption: %s", url) + return + + if not self.is_encrypted(file): + log.info('Url not encrypted: %s', url) + return + + self.create_paths(file) + + if os.path.exists(file.filepath): + instance.plugin_modified = True + self.finished(file) + return + + event = threading.Event() + progressbar = ProgressWindow(self.plugin, self.window, event) + thread = threading.Thread(target=Download, + args=(file, progressbar, self.window, + event, self)) + thread.daemon = True + thread.start() + instance.plugin_modified = True + + def is_encrypted(self, file): + if file.fragment: + try: + fragment = binascii.unhexlify(file.fragment) + file.key = fragment[16:] + file.iv = fragment[:16] + if len(file.key) == 32 and len(file.iv) == 16: + return True + except: + return False + return False + + def create_paths(self, file): + file.filename = os.path.basename(file.url) + ext = os.path.splitext(file.filename)[1] + name = os.path.splitext(file.filename)[0] + urlhash = hashlib.sha1(file.url.encode('utf-8')).hexdigest() + newfilename = name + '_' + urlhash[:10] + ext + file.filepath = os.path.join(DIRECTORY, newfilename) + + def finished(self, file): + question = 'Do you want to open %s' % file.filename + YesNoDialog('Open File', question, + transient_for=self.window, + on_response_yes=(self.open_file, file.filepath)) + return False + + def open_file(self, checked, path): + if platform.system() == "Windows": + os.startfile(path) + elif platform.system() == "Darwin": + subprocess.Popen(["open", path]) + else: + subprocess.Popen(["xdg-open", path]) + + +class Download: + def __init__(self, file, progressbar, window, event, base): + self.file = file + self.progressbar = progressbar + self.window = window + self.event = event + self.base = base + self.download() + + def download(self): + GLib.idle_add(self.progressbar.set_text, 'Downloading...') + data = self.load_url() + if isinstance(data, str): + GLib.idle_add(self.progressbar.close_dialog) + GLib.idle_add(self.error, data) + return + + GLib.idle_add(self.progressbar.set_text, 'Decrypting...') + decrypted_data = self.aes_decrypt(data) + + GLib.idle_add( + self.progressbar.set_text, 'Writing file to harddisk...') + self.write_file(decrypted_data) + + GLib.idle_add(self.progressbar.close_dialog) + + GLib.idle_add(self.base.finished, self.file) + + def load_url(self): + try: + stream = BytesIO() + if os.name == 'nt': + get_request = urlopen( + self.file.url, cafile=certifi.where(), timeout=30) + else: + get_request = urlopen(self.file.url, timeout=30) + size = get_request.info()['Content-Length'] + if not size: + errormsg = 'Content-Length not found in header' + log.error(errormsg) + return errormsg + while True: + try: + if self.event.isSet(): + raise UploadAbortedException + temp = get_request.read(10000) + GLib.idle_add( + self.progressbar.update_progress, len(temp), size) + except socket.timeout: + errormsg = 'Request timeout' + log.error(errormsg) + return errormsg + if temp: + stream.write(temp) + else: + return stream + except UploadAbortedException as error: + log.info('Upload Aborted') + errormsg = error + except URLError as error: + log.exception('URLError') + errormsg = error.reason + except Exception as error: + log.exception('Error') + errormsg = error + stream.close() + return str(errormsg) + + def aes_decrypt(self, payload): + # Use AES128 GCM with the given key and iv to decrypt the payload. + payload = payload.getvalue() + data = payload[:-16] + tag = payload[-16:] + decryptor = Cipher( + algorithms.AES(self.file.key), + GCM(self.file.iv, tag=tag), + backend=default_backend()).decryptor() + return decryptor.update(data) + decryptor.finalize() + + def write_file(self, data): + log.info('Writing data to %s', self.file.filepath) + try: + with BufferedWriter(FileIO(self.file.filepath, "wb")) as output: + output.write(data) + output.close() + except Exception: + log.exception('Failed to write file') + + def error(self, error): + ErrorDialog(_('Error'), error, transient_for=self.window) + return False + + +class ProgressWindow: + def __init__(self, plugin, window, event): + self.plugin = plugin + self.event = event + self.xml = gtkgui_helpers.get_gtk_builder( + self.plugin.local_file_path('upload_progress_dialog.ui')) + self.dialog = self.xml.get_object('progress_dialog') + self.dialog.set_transient_for(window) + self.label = self.xml.get_object('label') + self.progressbar = self.xml.get_object('progressbar') + self.progressbar.set_text("") + self.dialog.show_all() + self.xml.connect_signals(self) + self.seen = 0 + + def set_text(self, text): + self.label.set_markup('<big>%s</big>' % text) + return False + + def update_progress(self, seen, total): + self.seen += seen + pct = (self.seen / float(total)) * 100.0 + self.progressbar.set_fraction(self.seen / float(total)) + self.progressbar.set_text(str(int(pct)) + "%") + return False + + def close_dialog(self, *args): + self.dialog.destroy() + return False + + def on_destroy(self, *args): + self.event.set() + + +class UploadAbortedException(Exception): + def __str__(self): + return _('Upload Aborted') diff --git a/omemo/omemoplugin.py b/omemo/omemoplugin.py index 06d8839..055ffb7 100644 --- a/omemo/omemoplugin.py +++ b/omemo/omemoplugin.py @@ -30,6 +30,7 @@ from plugins import GajimPlugin from plugins.helpers import log_calls from nbxmpp.simplexml import Node from nbxmpp import NS_CORRECT, NS_ADDRESS +from .file_decryption import FileDecryption from .xmpp import ( NS_NOTIFY, NS_OMEMO, NS_EME, BundleInformationAnnouncement, @@ -115,7 +116,9 @@ class OmemoPlugin(GajimPlugin): self.gui_extension_points = {'chat_control': (self.connect_ui, self.disconnect_ui), 'groupchat_control': (self.connect_ui, - self.disconnect_ui)} + self.disconnect_ui), + 'hyperlink_handler': (self.file_decryption, + None)} SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP) self.plugin = self self.announced = [] @@ -179,6 +182,9 @@ class OmemoPlugin(GajimPlugin): 'enable_esessions', False) log.info(str(account) + " => Gajim E2E encryption disabled") + def file_decryption(self, url, kind, instance, window): + FileDecryption(self).hyperlink_handler(url, kind, instance, window) + @log_calls('OmemoPlugin') def signed_in(self, event): """ Method called on SignIn diff --git a/omemo/upload_progress_dialog.ui b/omemo/upload_progress_dialog.ui new file mode 100644 index 0000000..d7021b7 --- /dev/null +++ b/omemo/upload_progress_dialog.ui @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.20.0 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="progress_dialog"> + <property name="can_focus">True</property> + <property name="title" translatable="yes">Download</property> + <property name="resizable">False</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="icon_name">go-down</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox"> + <property name="width_request">250</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">4</property> + <property name="right_padding">3</property> + <child> + <object class="GtkButton" id="close_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="close_dialog" swapped="no"/> + <signal name="destroy" handler="on_destroy" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">8</property> + <property name="bottom_padding">4</property> + <property name="left_padding">8</property> + <property name="right_padding">8</property> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">4</property> + <property name="bottom_padding">4</property> + <property name="left_padding">8</property> + <property name="right_padding">8</property> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pulse_step">0.10000000149</property> + <property name="show_text">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> +</interface> |