changeset ca703ea05557 in /home/hg/repos/gajim-plugins
author: tmolitor <thilo(a)eightysoft.de>
branches:
details:gajim-plugins?cmd=changeset;node=ca703ea05557
description: decrypt encrypted images (conversations' encryption mode) using aes_gcm.py from omemo plugin
warning: this can take very long (about 1 minute for 1 MB)
diffstat:
url_image_preview/aes_gcm.py | 155 +++++++++++++++++++++++++++++++++
url_image_preview/manifest.ini | 2 +-
url_image_preview/url_image_preview.py | 62 +++++++++++-
3 files changed, 210 insertions(+), 9 deletions(-)
diffs (269 lines):
diff -r 71138d5be41a -r ca703ea05557 url_image_preview/aes_gcm.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/url_image_preview/aes_gcm.py Mon Feb 08 20:25:57 2016 +0100
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Jonathan Zdziarski <jonathan(a)zdziarski.com>
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+from struct import pack, unpack
+
+from Crypto.Cipher import AES
+from Crypto.Util import strxor
+
+log = logging.getLogger('gajim.plugin_system.aes_gcm')
+
+
+def gcm_rightshift(vec):
+ for x in range(15, 0, -1):
+ c = vec[x] >> 1
+ c |= (vec[x - 1] << 7) & 0x80
+ vec[x] = c
+ vec[0] >>= 1
+ return vec
+
+
+def gcm_gf_mult(a, b):
+ mask = [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01]
+ poly = [0x00, 0xe1]
+
+ Z = [0] * 16
+ V = [c for c in a]
+
+ for x in range(128):
+ if b[x >> 3] & mask[x & 7]:
+ Z = [V[y] ^ Z[y] for y in range(16)]
+ bit = V[15] & 1
+ V = gcm_rightshift(V)
+ V[0] ^= poly[bit]
+ return Z
+
+
+def ghash(h, auth_data, data):
+ u = (16 - len(data)) % 16
+ v = (16 - len(auth_data)) % 16
+
+ x = auth_data + chr(0) * v + data + chr(0) * u
+ x += pack('>QQ', len(auth_data) * 8, len(data) * 8)
+
+ y = [0] * 16
+ vec_h = [ord(c) for c in h]
+
+ for i in range(0, len(x), 16):
+ block = [ord(c) for c in x[i:i + 16]]
+ y = [y[j] ^ block[j] for j in range(16)]
+ y = gcm_gf_mult(y, vec_h)
+
+ return ''.join(chr(c) for c in y)
+
+
+def inc32(block):
+ counter, = unpack('>L', block[12:])
+ counter += 1
+ return block[:12] + pack('>L', counter)
+
+
+def gctr(k, icb, plaintext):
+ y = ''
+ if len(plaintext) == 0:
+ return y
+
+ aes = AES.new(k)
+ cb = icb
+
+ for i in range(0, len(plaintext), aes.block_size):
+ cb = inc32(cb)
+ encrypted = aes.encrypt(cb)
+ plaintext_block = plaintext[i:i + aes.block_size]
+ y += strxor.strxor(plaintext_block, encrypted[:len(plaintext_block)])
+
+ return y
+
+
+def gcm_decrypt(k, iv, encrypted, auth_data, tag):
+ aes = AES.new(k)
+ h = aes.encrypt(chr(0) * aes.block_size)
+
+ if len(iv) == 12:
+ y0 = iv + "\x00\x00\x00\x01"
+ else:
+ y0 = ghash(h, '', iv)
+
+ decrypted = gctr(k, y0, encrypted)
+ s = ghash(h, auth_data, encrypted)
+
+ t = aes.encrypt(y0)
+ T = strxor.strxor(s, t)
+ if T != tag:
+ raise ValueError('Decrypted data is invalid')
+ else:
+ return decrypted
+
+
+def gcm_encrypt(k, iv, plaintext, auth_data):
+ aes = AES.new(k)
+ h = aes.encrypt(chr(0) * aes.block_size)
+
+ if len(iv) == 12:
+ y0 = iv + "\x00\x00\x00\x01"
+ else:
+ y0 = ghash(h, '', iv)
+
+ encrypted = gctr(k, y0, plaintext)
+ s = ghash(h, auth_data, encrypted)
+
+ t = aes.encrypt(y0)
+ T = strxor.strxor(s, t)
+ return (encrypted, T)
+
+
+def aes_encrypt(key, nonce, plaintext):
+ """ Use AES128 GCM with the given key and iv to encrypt the payload. """
+ c, t = gcm_encrypt(key, nonce, plaintext, '')
+ result = c + t
+ return result
+
+
+def aes_decrypt(key, nonce, payload):
+ """ Use AES128 GCM with the given key and iv to decrypt the payload. """
+ ciphertext = payload[:-16]
+ mac = payload[-16:]
+ return gcm_decrypt(key, nonce, ciphertext, '', mac)
diff -r 71138d5be41a -r ca703ea05557 url_image_preview/manifest.ini
--- a/url_image_preview/manifest.ini Fri Feb 05 04:53:55 2016 +0100
+++ b/url_image_preview/manifest.ini Mon Feb 08 20:25:57 2016 +0100
@@ -1,7 +1,7 @@
[info]
name: Url image preview
short_name: url_image_preview
-version: 0.6.5
+version: 0.7
description: Url image preview in chatbox.<br/>
Based on patch in <a href="http://trac.gajim.org/attachment/ticket/5300">ticket #5300</a>.
authors = Denis Fomin <fominde(a)gmail.com>
diff -r 71138d5be41a -r ca703ea05557 url_image_preview/url_image_preview.py
--- a/url_image_preview/url_image_preview.py Fri Feb 05 04:53:55 2016 +0100
+++ b/url_image_preview/url_image_preview.py Mon Feb 08 20:25:57 2016 +0100
@@ -7,6 +7,7 @@
import urllib2
from urlparse import urlparse
+import logging
import nbxmpp
from common import gajim
from common import ged
@@ -15,6 +16,9 @@
from plugins.helpers import log_calls, log
from plugins.gui import GajimPluginConfigDialog
from conversation_textview import TextViewImage
+from .aes_gcm import aes_decrypt
+
+log = logging.getLogger('gajim.plugin_system.url_image_preview')
ACCEPTED_MIME_TYPES = ('image/png','image/jpeg','image/gif','image/raw',
'image/svg+xml')
@@ -140,12 +144,54 @@
# URL is already displayed
return
- # Start downloading image
- gajim.thread_interface(helpers.download_image, [ self.textview.account, {
- 'src': url, 'max_size': self.plugin.config['MAX_FILE_SIZE'] } ],
- self._update_img, [url, file_mime, repl_start, repl_end])
+ # check for encryption (conversations mode)
+ urlparts = urlparse(url)
+ key = ''
+ iv = ''
+ if len(urlparts.fragment):
+ fragment = []
+ for i in range(0, len(urlparts.fragment), 2):
+ fragment.append( chr( int (urlparts.fragment[i:i+2], 16 ) ) )
+ fragment = ''.join(fragment)
+ key = fragment[16:]
+ iv = fragment[:16]
+
+ # decrypt if the encryption parameters are correct
+ if len(urlparts.fragment) and len(key)==32 and len(iv)==16:
+ def _decryptor((mem, alt), url, file_mime, repl_start, repl_end, key, iv):
+ if not mem:
+ log.error('Could not download image for URL: %s -- %s' % (url, alt))
+ return
+ # start self._decrypt_url() in own thread and self._update_img() afterwards
+ gajim.thread_interface(self._decrypt_url,
+ [(mem, alt), key, iv],
+ self._update_img,
+ [url, file_mime, repl_start, repl_end, True])
+
+ # Start downloading image (_decryptor is callback when download has finished)
+ gajim.thread_interface(helpers.download_image, [ self.textview.account, {
+ 'src': url, 'max_size': self.plugin.config['MAX_FILE_SIZE'] } ],
+ _decryptor, [url, file_mime, repl_start, repl_end, key, iv])
+ else:
+ # Start downloading image (self._update_img() is callback when download has finished)
+ gajim.thread_interface(helpers.download_image, [ self.textview.account, {
+ 'src': url, 'max_size': self.plugin.config['MAX_FILE_SIZE'] } ],
+ self._update_img, [url, file_mime, repl_start, repl_end])
- def _update_img(self, (mem, alt), url, file_mime, repl_start, repl_end):
+ def _decrypt_url(self, (mem, alt), key, iv):
+ try:
+ log.info("Before decrypt image")
+ mem = aes_decrypt(key, iv, mem)
+ log.info("After decrypt image")
+ except Exception:
+ log.error('Could not decrypt image for URL (exception raised): %s' % url)
+ raise
+ if not mem or not len(mem):
+ log.error('Could not decrypt image for URL: %s' % url)
+ return (None, alt)
+ return (mem, alt)
+
+ def _update_img(self, (mem, alt), url, file_mime, repl_start, repl_end, decrypted=False):
if mem:
try:
loader = gtk.gdk.PixbufLoader()
@@ -179,8 +225,9 @@
log.error('Could not display image for URL: %s' % url)
raise
else:
- # If image could not be downloaded, URL is already displayed
- log.error('Could not download image for URL: %s -- %s' % (url, alt))
+ if not decrypted:
+ # If image could not be downloaded, URL is already displayed
+ log.error('Could not download image for URL: %s -- %s' % (url, alt))
def _get_http_head (self, account, url):
# Check if proxy is used
@@ -345,4 +392,3 @@
def max_size_value_changed(self, widget):
self.plugin.config['MAX_FILE_SIZE'] = self.max_file_size[
self.max_size_combobox.get_active()]
-