diff options
author | Philipp Hörist <forenjunkie@chello.at> | 2017-03-26 17:37:59 +0300 |
---|---|---|
committer | Philipp Hörist <forenjunkie@chello.at> | 2017-03-26 17:37:59 +0300 |
commit | 6ace7341378f11d83d2685ae85fb938b7bc5a463 (patch) | |
tree | 5f334932a4203980c9bbbdb099363022d406332d | |
parent | 573c88a3c58dfe8122e8f4882cace2b41f9cd210 (diff) | |
parent | e72763eefc79a7cf050e15c9d8497004db00e721 (diff) |
Merge branch 'master' into 'master'
OMEMO Version 1.1.0
See merge request !31
-rw-r--r-- | omemo/CHANGELOG | 3 | ||||
-rw-r--r-- | omemo/file_decryption.py | 286 | ||||
-rw-r--r-- | omemo/manifest.ini | 2 | ||||
-rw-r--r-- | omemo/omemoplugin.py | 2 | ||||
-rw-r--r-- | omemo/upload_progress_dialog.ui | 108 |
5 files changed, 400 insertions, 1 deletions
diff --git a/omemo/CHANGELOG b/omemo/CHANGELOG index a7d5817..9558f0f 100644 --- a/omemo/CHANGELOG +++ b/omemo/CHANGELOG @@ -1,3 +1,6 @@ +1.1.0 / 2017-03-26 +- Add file decryption + 1.0.4 / 2017-03-01 - Use correct tag name for EME diff --git a/omemo/file_decryption.py b/omemo/file_decryption.py new file mode 100644 index 0000000..8f80471 --- /dev/null +++ b/omemo/file_decryption.py @@ -0,0 +1,286 @@ +# -*- 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 urllib2 +import socket +import threading +import platform +import subprocess +import binascii +from io import BufferedWriter, FileIO, BytesIO +from urlparse import urlparse, urldefrag + +import gtk +import gobject +import gtkgui_helpers +from common import configpaths +from dialogs import ErrorDialog, YesNoDialog + +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 + +log = logging.getLogger('gajim.plugin_system.omemo.filedecryption') + +DIRECTORY = os.path.join(configpaths.gajimpaths['MY_DATA'], 'downloads') +try: + if not os.path.exists(DIRECTORY): + os.makedirs(DIRECTORY) + ERROR = False +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.chat_control = None + self.orig_handler = None + self.tv = None + self.progress_windows = {} + + def activate(self, chat_control): + if ERROR: + return + self.tv = chat_control.conv_textview.tv + self.chat_control = chat_control + self.orig_handler = self.tv.hyperlink_handler + self.tv.hyperlink_handler = self.hyperlink_handler + + def deactivate(self): + self.tv.hyperlink_handler = self.orig_handler + + def hyperlink_handler(self, texttag, widget, event, iter_, kind): + if event.type != gtk.gdk.BUTTON_PRESS: + return + begin_iter = iter_.copy() + # we get the begining of the tag + while not begin_iter.begins_tag(texttag): + begin_iter.backward_char() + end_iter = iter_.copy() + # we get the end of the tag + while not end_iter.ends_tag(texttag): + end_iter.forward_char() + + url = self.tv.get_buffer().get_text(begin_iter, end_iter, True) + 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) + self.orig_handler(texttag, widget, event, iter_, kind) + return + + if not self.is_encrypted(file): + log.info('Url not encrypted: %s', url) + self.orig_handler(texttag, widget, event, iter_, kind) + return + + self.create_paths(file) + + if os.path.exists(file.filepath): + self.finished(file) + return + + event = threading.Event() + progressbar = ProgressWindow(self.plugin, self.chat_control, event) + thread = threading.Thread(target=Download, + args=(file, progressbar, self.chat_control, + event, self)) + thread.daemon = True + thread.start() + + 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).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.chat_control.parent_win.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, chat_control, event, base): + self.file = file + self.progressbar = progressbar + self.chat_control = chat_control + self.event = event + self.base = base + self.download() + + def download(self): + gobject.idle_add(self.progressbar.set_text, 'Downloading...') + data = self.load_url() + if isinstance(data, str): + gobject.idle_add(self.progressbar.close_dialog) + gobject.idle_add(self.error, data) + return + + gobject.idle_add(self.progressbar.set_text, 'Decrypting...') + decrypted_data = self.aes_decrypt(data) + + gobject.idle_add( + self.progressbar.set_text, 'Writing file to harddisk...') + self.write_file(decrypted_data) + + gobject.idle_add(self.progressbar.close_dialog) + + gobject.idle_add(self.base.finished, self.file) + + def load_url(self): + try: + get_request = urllib2.urlopen(self.file.url, timeout=30) + size = get_request.info().getheader('Content-Length').strip() + if not size: + errormsg = 'Content-Length not found in header' + log.error(errormsg) + return errormsg + stream = BytesIO() + while True: + try: + if self.event.isSet(): + raise UploadAbortedException + temp = get_request.read(10000) + gobject.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 urllib2.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.chat_control.parent_win.window) + return False + + +class ProgressWindow: + def __init__(self, plugin, chat_control, 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(chat_control.parent_win.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/manifest.ini b/omemo/manifest.ini index ab38078..9696093 100644 --- a/omemo/manifest.ini +++ b/omemo/manifest.ini @@ -1,7 +1,7 @@ [info] name: OMEMO short_name: omemo -version: 1.0.4 +version: 1.1.0 description: OMEMO is an XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption based on Axolotl and PEP. You need to install some dependencys, you can find install instructions for your system in the Gitlab Wiki. authors: Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de> Daniel Gultsch <daniel@gultsch.de> diff --git a/omemo/omemoplugin.py b/omemo/omemoplugin.py index 03c06c5..9b8167a 100644 --- a/omemo/omemoplugin.py +++ b/omemo/omemoplugin.py @@ -37,6 +37,7 @@ from .xmpp import ( DevicelistPEP, OmemoMessage, successful, unpack_device_bundle, unpack_device_list_update, unpack_encrypted) +from .file_decryption import FileDecryption from common import demandimport demandimport.enable() demandimport.ignore += ['_imp', '_thread', 'axolotl', 'PIL', @@ -832,6 +833,7 @@ class OmemoPlugin(GajimPlugin): chat_control : ChatControl Gajim ChatControl object """ + FileDecryption(self).activate(chat_control) account = chat_control.contact.account.name if account in self.disabled_accounts: return diff --git a/omemo/upload_progress_dialog.ui b/omemo/upload_progress_dialog.ui new file mode 100644 index 0000000..c5e0b5a --- /dev/null +++ b/omemo/upload_progress_dialog.ui @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <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="GtkVBox" 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="GtkHButtonBox" 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="use_action_appearance">False</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="destroy" handler="on_destroy" swapped="no"/> + <signal name="clicked" handler="close_dialog" 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="xalign">0</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="show_text">True</property> + <property name="pulse_step">0.10000000149</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> |