Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/omemo
diff options
context:
space:
mode:
authorPhilipp Hörist <forenjunkie@chello.at>2017-03-26 22:09:24 +0300
committerPhilipp Hörist <forenjunkie@chello.at>2017-03-26 22:09:24 +0300
commit750528068f142341b730d430735ffd70a9f030ce (patch)
tree7f5a76cddf89b5171f0599b063bf9d9da5fde6ca /omemo
parent65e2f437a5c7d0296ccf4422d03d6e3c84741a5a (diff)
[omemo] Add file decryption
Diffstat (limited to 'omemo')
-rw-r--r--omemo/file_decryption.py272
-rw-r--r--omemo/omemoplugin.py8
-rw-r--r--omemo/upload_progress_dialog.ui106
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>