[omemo] Refactor Plugin

- Adapt to nbxmpp supporting OMEMO
- Move python-axolotl code into backend folder
This commit is contained in:
Philipp Hörist
2019-02-11 22:32:27 +01:00
parent a6ed314941
commit 3c78b09fb2
24 changed files with 1460 additions and 2318 deletions

View File

@@ -1 +1 @@
from .omemoplugin import OmemoPlugin
from omemo.plugin import OmemoPlugin

86
omemo/backend/aes.py Normal file
View File

@@ -0,0 +1,86 @@
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# 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 <http://www.gnu.org/licenses/>.
import os
import logging
from collections import namedtuple
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')
EncryptionResult = namedtuple('EncryptionResult', 'payload key iv')
def _decrypt(key, iv, tag, data):
decryptor = Cipher(
algorithms.AES(key),
GCM(iv, tag=tag),
backend=default_backend()).decryptor()
return decryptor.update(data) + decryptor.finalize()
def aes_decrypt(_key, iv, payload):
if len(_key) >= 32:
# XEP-0384
log.debug('XEP Compliant Key/Tag')
data = payload
key = _key[:16]
tag = _key[16:]
else:
# Legacy
log.debug('Legacy Key/Tag')
data = payload[:-16]
key = _key
tag = payload[-16:]
return _decrypt(key, iv, tag, data).decode()
def aes_decrypt_file(key, iv, payload):
data = payload[:-16]
tag = payload[-16:]
return _decrypt(key, iv, tag, data)
def _encrypt(data, key_size, iv_size):
if isinstance(data, str):
data = data.encode()
key = os.urandom(key_size)
iv = os.urandom(iv_size)
encryptor = Cipher(
algorithms.AES(key),
GCM(iv),
backend=default_backend()).encryptor()
payload = encryptor.update(data) + encryptor.finalize()
return key, iv, encryptor.tag, payload
def aes_encrypt(plaintext):
key, iv, tag, payload = _encrypt(plaintext, 16, 16)
key += tag
return EncryptionResult(payload=payload, key=key, iv=iv)
def aes_encrypt_file(data):
key, iv, tag, payload, = _encrypt(data, 32, 16)
payload += tag
return EncryptionResult(payload=payload, key=key, iv=iv)

View File

@@ -19,16 +19,14 @@
import logging
import time
import os
from base64 import b64encode
from nbxmpp.structs import OMEMOBundle
from nbxmpp.structs import OMEMOMessage
from axolotl.ecc.djbec import DjbECPublicKey
from axolotl.identitykey import IdentityKey
from axolotl.duplicatemessagexception import DuplicateMessageException
from axolotl.invalidmessageexception import InvalidMessageException
from axolotl.invalidversionexception import InvalidVersionException
from axolotl.untrustedidentityexception import UntrustedIdentityException
from axolotl.nosessionexception import NoSessionException
from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage
from axolotl.protocol.whispermessage import WhisperMessage
from axolotl.sessionbuilder import SessionBuilder
@@ -36,13 +34,12 @@ from axolotl.sessioncipher import SessionCipher
from axolotl.state.prekeybundle import PreKeyBundle
from axolotl.util.keyhelper import KeyHelper
from .aes_gcm import NoValidSessions, decrypt, encrypt
from omemo.backend.aes import aes_decrypt, aes_encrypt
from .liteaxolotlstore import (LiteAxolotlStore, DEFAULT_PREKEY_AMOUNT,
MIN_PREKEY_AMOUNT, SPK_CYCLE_TIME,
SPK_ARCHIVE_TIME)
log = logging.getLogger('gajim.plugin_system.omemo')
logAxolotl = logging.getLogger('axolotl')
UNTRUSTED = 0
@@ -78,23 +75,24 @@ class OmemoState:
str(self.store.preKeyStore.getPreKeyCount()) +
' PreKeys available')
def build_session(self, recipient_id, device_id, bundle_dict):
def build_session(self, recipient_id, device_id, bundle):
sessionBuilder = SessionBuilder(self.store, self.store, self.store,
self.store, recipient_id, device_id)
registration_id = self.store.getLocalRegistrationId()
preKeyPublic = DjbECPublicKey(bundle_dict['preKeyPublic'][1:])
prekey = bundle.pick_prekey()
preKeyPublic = DjbECPublicKey(prekey['key'][1:])
signedPreKeyPublic = DjbECPublicKey(bundle_dict['signedPreKeyPublic'][
1:])
identityKey = IdentityKey(DjbECPublicKey(bundle_dict['identityKey'][
1:]))
signedPreKeyPublic = DjbECPublicKey(bundle.spk['key'][1:])
identityKey = IdentityKey(DjbECPublicKey(bundle.ik[1:]))
prekey_bundle = PreKeyBundle(
registration_id, device_id, bundle_dict['preKeyId'], preKeyPublic,
bundle_dict['signedPreKeyId'], signedPreKeyPublic,
bundle_dict['signedPreKeySignature'], identityKey)
registration_id, device_id,
prekey['id'], preKeyPublic,
bundle.spk['id'], signedPreKeyPublic,
bundle.spk_signature,
identityKey)
sessionBuilder.processPreKeyBundle(prekey_bundle)
return self.get_session_cipher(recipient_id, device_id)
@@ -153,101 +151,85 @@ class OmemoState:
@property
def bundle(self):
self.checkPreKeyAmount()
prekeys = [
(k.getId(), b64encode(k.getKeyPair().getPublicKey().serialize()))
for k in self.store.loadPreKeys()
]
bundle = {'otpks': []}
for k in self.store.loadPreKeys():
key = k.getKeyPair().getPublicKey().serialize()
bundle['otpks'].append({'key': key, 'id': k.getId()})
identityKeyPair = self.store.getIdentityKeyPair()
bundle['ik'] = identityKeyPair.getPublicKey().serialize()
self.cycleSignedPreKey(identityKeyPair)
signedPreKey = self.store.loadSignedPreKey(
self.store.getCurrentSignedPreKeyId())
bundle['spk_signature'] = signedPreKey.getSignature()
bundle['spk'] = {'key': signedPreKey.getKeyPair().getPublicKey().serialize(),
'id': signedPreKey.getId()}
result = {
'signedPreKeyId': signedPreKey.getId(),
'signedPreKeyPublic':
b64encode(signedPreKey.getKeyPair().getPublicKey().serialize()),
'signedPreKeySignature': b64encode(signedPreKey.getSignature()),
'identityKey':
b64encode(identityKeyPair.getPublicKey().serialize()),
'prekeys': prekeys
}
return result
return OMEMOBundle(**bundle)
def decrypt_msg(self, msg_dict):
def decrypt_msg(self, omemo_message, jid):
own_id = self.own_device_id
if msg_dict['sid'] == own_id:
if omemo_message.sid == own_id:
log.info('Received previously sent message by us')
return
if own_id not in msg_dict['keys']:
if own_id not in omemo_message.keys:
log.warning('OMEMO message does not contain our device key')
return
iv = msg_dict['iv']
sid = msg_dict['sid']
sender_jid = msg_dict['sender_jid']
payload = msg_dict['payload']
encrypted_key, prekey = omemo_message.keys[own_id]
encrypted_key = msg_dict['keys'][own_id]
try:
key = self.handlePreKeyWhisperMessage(sender_jid, sid,
encrypted_key)
except (InvalidVersionException, InvalidMessageException):
if prekey:
try:
key = self.handleWhisperMessage(sender_jid, sid, encrypted_key)
except (NoSessionException, InvalidMessageException) as e:
log.warning(e)
log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' +
str(sid))
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
key = self.handlePreKeyWhisperMessage(
jid, omemo_message.sid, encrypted_key)
except Exception as error:
log.warning(error)
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
return
else:
try:
key = self.handleWhisperMessage(
jid, omemo_message.sid, encrypted_key)
except Exception as error:
log.warning(error)
return
if payload is None:
if omemo_message.payload is None:
result = None
log.debug("Decrypted Key Exchange Message")
else:
result = decrypt(key, iv, payload)
log.debug("Decrypted Message => " + result)
result = aes_decrypt(key, omemo_message.iv, omemo_message.payload)
log.debug("Decrypted Message => %s", result)
return result
def create_msg(self, from_jid, jid, plaintext):
key = os.urandom(16)
iv = os.urandom(16)
encrypted_keys = {}
devices_list = self.device_list_for(jid)
if len(devices_list) == 0:
if not devices_list:
log.error('No known devices')
return
payload, tag = encrypt(key, iv, plaintext)
key += tag
result = aes_encrypt(plaintext)
# Encrypt the message key with for each of receivers devices
for device in devices_list:
try:
if self.isTrusted(jid, device) == TRUSTED:
cipher = self.get_session_cipher(jid, device)
cipher_key = cipher.encrypt(key)
cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped Device because Trust is: ' +
str(self.isTrusted(jid, device)))
except:
log.warning('Failed to find key for device ' + str(device))
log.debug('Skipped Device because Trust is: %s',
self.isTrusted(jid, device))
except Exception:
log.warning('Failed to find key for device: %s', device)
if len(encrypted_keys) == 0:
if not encrypted_keys:
log.error('Encrypted keys empty')
raise NoValidSessions('Encrypted keys empty')
@@ -257,36 +239,29 @@ class OmemoState:
try:
if self.isTrusted(from_jid, device) == TRUSTED:
cipher = self.get_session_cipher(from_jid, device)
cipher_key = cipher.encrypt(key)
cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped own Device because Trust is: ' +
str(self.isTrusted(from_jid, device)))
except:
log.warning('Failed to find key for device ' + str(device))
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
'jid': jid,
'iv': iv,
'payload': payload}
log.debug('Skipped own Device because Trust is: %s',
self.isTrusted(from_jid, device))
except Exception:
log.warning('Failed to find key for device: %s', device)
log.debug('Finished encrypting message')
return result
return OMEMOMessage(sid=self.own_device_id,
keys=encrypted_keys,
iv=result.iv,
payload=result.payload)
def create_gc_msg(self, from_jid, jid, plaintext):
key = os.urandom(16)
iv = os.urandom(16)
encrypted_keys = {}
room = jid
encrypted_jids = []
devices_list = self.device_list_for(jid, True)
payload, tag = encrypt(key, iv, plaintext)
key += tag
result = aes_encrypt(plaintext, append_tag=True)
for tup in devices_list:
self.get_session_cipher(tup[0], tup[1])
@@ -303,16 +278,15 @@ class OmemoState:
for rid, cipher in self.session_ciphers[jid_to].items():
try:
if self.isTrusted(jid_to, rid) == TRUSTED:
cipher_key = cipher.encrypt(key)
cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[rid] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped Device because Trust is: ' +
str(self.isTrusted(jid_to, rid)))
except:
log.debug('Skipped Device because Trust is: %s',
self.isTrusted(jid_to, rid))
except Exception:
log.exception('ERROR:')
log.warning('Failed to find key for device ' +
str(rid))
log.warning('Failed to find key for device %s', rid)
encrypted_jids.append(jid_to)
my_other_devices = set(self.own_devices) - set({self.own_device_id})
@@ -321,28 +295,25 @@ class OmemoState:
try:
cipher = self.get_session_cipher(from_jid, dev)
if self.isTrusted(from_jid, dev) == TRUSTED:
cipher_key = cipher.encrypt(key)
cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[dev] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped own Device because Trust is: ' +
str(self.isTrusted(from_jid, dev)))
except:
log.debug('Skipped own Device because Trust is: %s',
self.isTrusted(from_jid, dev))
except Exception:
log.exception('ERROR:')
log.warning('Failed to find key for device ' + str(dev))
log.warning('Failed to find key for device: %s', dev)
if not encrypted_keys:
log.error('Encrypted keys empty')
raise NoValidSessions('Encrypted keys empty')
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
'jid': jid,
'iv': iv,
'payload': payload}
log.debug('Finished encrypting message')
return result
return OMEMOMessage(sid=self.own_device_id,
keys=encrypted_keys,
iv=result.iv,
payload=result.payload)
def device_list_for(self, jid, gc=False):
""" Return a list of known device ids for the specified jid.
@@ -439,7 +410,7 @@ class OmemoState:
key = sessionCipher.decryptPkmsg(preKeyWhisperMessage)
# Publish new bundle after PreKey has been used
# for building a new Session
self.xmpp_con.publish_bundle()
self.xmpp_con.set_bundle()
self.add_device(recipient_id, device_id)
return key
except UntrustedIdentityException as e:
@@ -495,3 +466,7 @@ class OmemoState:
# Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME
timestamp = now - SPK_ARCHIVE_TIME
self.store.removeOldSignedPreKeys(timestamp)
class NoValidSessions(Exception):
pass

View File

@@ -1,21 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# Copyright 2017 Philipp Hörist <philipp@hoerist.com>
# This file is part of OMEMO Gajim Plugin.
#
# This file is part of Gajim-OMEMO 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.
#
# 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/>.
# 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 <http://www.gnu.org/licenses/>.
import os
import hashlib
@@ -32,29 +29,23 @@ from urllib.parse import urlparse, urldefrag
from io import BufferedWriter, FileIO, BytesIO
from gi.repository import GLib
from gajim.common import app
from gajim.common import configpaths
from gajim.plugins.plugins_i18n import _
from gajim.gtk.dialogs import ErrorDialog, YesNoDialog
from omemo.gtk.progress import ProgressWindow
from omemo.backend.aes import aes_decrypt_file
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
from omemo.omemo.aes_gcm_native import aes_encrypt
except ImportError:
log.exception('ImportError')
ERROR = True
DIRECTORY = os.path.join(configpaths.get('MY_DATA'), 'downloads')
ERROR = False
try:
if not os.path.exists(DIRECTORY):
os.makedirs(DIRECTORY)
@@ -63,15 +54,6 @@ except Exception:
log.exception('Error')
def encrypt_file(data):
key = os.urandom(32)
iv = os.urandom(16)
payload, tag = aes_encrypt(key, iv, data)
encrypted_data = payload + tag
return (encrypted_data, key, iv)
class File:
def __init__(self, url, account):
self.account = account
@@ -105,7 +87,7 @@ class FileDecryption:
if not self.is_encrypted(file):
log.info('Url not encrypted: %s', url)
return
print('ADASD')
self.create_paths(file)
if os.path.exists(file.filepath):
@@ -181,7 +163,10 @@ class Download:
return
GLib.idle_add(self.progressbar.set_text, _('Decrypting...'))
decrypted_data = self.aes_decrypt(data)
decrypted_data = aes_decrypt_file(self.file.key,
self.file.iv,
data.getvalue())
GLib.idle_add(
self.progressbar.set_text, _('Writing file to harddisk...'))
@@ -203,11 +188,11 @@ class Download:
log.warning('CERT Verification disabled')
get_request = urlopen(self.file.url, timeout=30, context=context)
else:
cafile = None
if os.name == 'nt':
get_request = urlopen(
self.file.url, cafile=certifi.where(), timeout=30)
else:
get_request = urlopen(self.file.url, timeout=30)
cafile = certifi.where()
context = ssl.create_default_context(cafile=cafile)
get_request = urlopen(self.file.url, timeout=30, context=context)
size = get_request.info()['Content-Length']
if not size:
@@ -241,17 +226,6 @@ class Download:
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:

View File

@@ -1,22 +1,20 @@
'''
Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
Copyright 2015 Daniel Gultsch <daniel@cgultsch.de>
Copyright 2016 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/>.
'''
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
#
# 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 <http://www.gnu.org/licenses/>.
import binascii
import logging
@@ -171,7 +169,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
def cleardevice_button_clicked_cb(self, button, *args):
active = self._ui.get_object('account_combobox').get_active()
account = self.account_store[active][0]
app.connections[account].get_module('OMEMO').publish_own_devices_list(new=True)
app.connections[account].get_module('OMEMO').set_devicelist(new=True)
self.update_context_list()
def refresh_button_clicked_cb(self, button, *args):

View File

@@ -1,18 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
# This file is part of OMEMO Gajim Plugin.
#
# Gajim is free software; you can redistribute it and/or modify
# 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.
#
# Gajim is distributed in the hope that it will be useful,
# 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging
import binascii

View File

@@ -1,18 +1,19 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
# This file is part of OMEMO Gajim Plugin.
#
# Gajim is free software; you can redistribute it and/or modify
# 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.
#
# Gajim is distributed in the hope that it will be useful,
# 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from gajim.plugins.helpers import get_builder

View File

@@ -1,16 +1,18 @@
# This file is part of Gajim-OMEMO.
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# Gajim-OMEMO is free software; you can redistribute it and/or modify
# 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.
#
# Gajim-OMEMO is distributed in the hope that it will be useful,
# 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 Gajim-OMEMO. If not, see <http://www.gnu.org/licenses/>.
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from collections import namedtuple

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +0,0 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of OMEMO.
#
# OMEMO 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 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. If not, see <http://www.gnu.org/licenses/>.
# XEP-0384: OMEMO Encryption
import logging
import nbxmpp
from gajim.common import app
from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
from omemo.modules.util import NS_OMEMO
from omemo.modules.util import NS_DEVICE_LIST
from omemo.modules.util import unpack_devicelist
log = logging.getLogger('gajim.plugin_system.omemo.pep')
# Module name
name = 'OMEMODevicelist'
zeroconf = False
class OMEMODevicelistData(AbstractPEPData):
type_ = 'omemo-devicelist'
class OMEMODevicelist(AbstractPEPModule):
'''
<item>
<list xmlns='eu.siacs.conversations.axolotl'>
<device id='12345' />
<device id='4223' />
</list>
</item>
'''
name = 'omemo-devicelist'
namespace = NS_DEVICE_LIST
pep_class = OMEMODevicelistData
store_publish = True
_log = log
@staticmethod
def _extract_info(item):
return unpack_devicelist(item)
def _notification_received(self, jid, devicelist):
con = app.connections[self._account]
con.get_module('OMEMO').device_list_received(devicelist.data,
jid.getStripped())
@staticmethod
def _build_node(devicelist):
list_node = nbxmpp.Node('list', {'xmlns': NS_OMEMO})
if devicelist is None:
return list_node
for device in devicelist:
list_node.addChild('device', attrs={'id': device})
return list_node
def get_instance(*args, **kwargs):
return OMEMODevicelist(*args, **kwargs), 'OMEMODevicelist'

View File

@@ -1,46 +1,30 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of OMEMO.
# This file is part of OMEMO Gajim Plugin.
#
# OMEMO is free software; you can redistribute it and/or modify
# 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 is distributed in the hope that it will be useful,
# 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. If not, see <http://www.gnu.org/licenses/>.
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0384: OMEMO Encryption
import logging
import nbxmpp
from gajim.common.exceptions import StanzaMalformed
def prepare_stanza(stanza, plaintext):
delete_nodes(stanza, 'encrypted', nbxmpp.NS_OMEMO_TEMP)
delete_nodes(stanza, 'body')
stanza.setBody(plaintext)
log = logging.getLogger('gajim.plugin_system.omemo')
NS_OMEMO = 'eu.siacs.conversations.axolotl'
NS_DEVICE_LIST = NS_OMEMO + '.devicelist'
def unpack_devicelist(item):
list_ = item.getTag('list', namespace=NS_OMEMO)
if list_ is None:
raise StanzaMalformed('No list node')
device_list = list_.getTags('device')
devices = []
for device in device_list:
id_ = device.getAttr('id')
if id_ is None:
raise StanzaMalformed('No id for device found')
devices.append(int(id_))
return devices
def delete_nodes(stanza, name, namespace=None):
nodes = stanza.getTags(name, namespace=namespace)
for node in nodes:
stanza.delChild(node)

View File

@@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# This file is part of python-omemo library.
#
# The python-omemo library 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.
#
# python-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 python-omemo library. If not, see <http://www.gnu.org/licenses/>.
#
import sys
import logging
from .aes_gcm_native import aes_decrypt
from .aes_gcm_native import aes_encrypt
log = logging.getLogger('gajim.plugin_system.omemo')
def encrypt(key, iv, plaintext):
return aes_encrypt(key, iv, plaintext)
def decrypt(key, iv, ciphertext):
return aes_decrypt(key, iv, ciphertext).decode('utf-8')
class NoValidSessions(Exception):
pass

View File

@@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# This file is part of python-omemo library.
#
# The python-omemo library 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.
#
# python-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 python-omemo library. If not, see <http://www.gnu.org/licenses/>.
#
import os
import logging
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers.modes import GCM
# On Windows we have to import a specific backend because the
# default_backend() mechanism doesn't work in Gajim for Windows.
# Its because of how Gajim is build with cx_freeze
if os.name == 'nt':
from cryptography.hazmat.backends.openssl import backend
else:
from cryptography.hazmat.backends import default_backend
log = logging.getLogger('gajim.plugin_system.omemo')
def aes_decrypt(_key, iv, payload):
""" Use AES128 GCM with the given key and iv to decrypt the payload. """
if len(_key) >= 32:
# XEP-0384
log.debug('XEP Compliant Key/Tag')
data = payload
key = _key[:16]
tag = _key[16:]
else:
# Legacy
log.debug('Legacy Key/Tag')
data = payload[:-16]
key = _key
tag = payload[-16:]
if os.name == 'nt':
_backend = backend
else:
_backend = default_backend()
decryptor = Cipher(
algorithms.AES(key),
GCM(iv, tag=tag),
backend=_backend).decryptor()
return decryptor.update(data) + decryptor.finalize()
def aes_encrypt(key, iv, plaintext):
""" Use AES128 GCM with the given key and iv to encrypt the plaintext. """
if os.name == 'nt':
_backend = backend
else:
_backend = default_backend()
encryptor = Cipher(
algorithms.AES(key),
GCM(iv),
backend=_backend).encryptor()
return encryptor.update(plaintext) + encryptor.finalize(), encryptor.tag

View File

@@ -1,24 +1,20 @@
# -*- coding: utf-8 -*-
'''
Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
Copyright 2015 Daniel Gultsch <daniel@cgultsch.de>
Copyright 2016 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/>.
'''
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
#
# 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 <http://www.gnu.org/licenses/>.
import logging
import binascii
@@ -36,11 +32,12 @@ from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
from gajim.groupchat_control import GroupchatControl
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
CRYPTOGRAPHY_MISSING = 'You are missing Python3-Cryptography'
AXOLOTL_MISSING = 'You are missing Python3-Axolotl or use an outdated version'
PROTOBUF_MISSING = "OMEMO can't import Google Protobuf, you can find help in " \
"the GitHub Wiki"
@@ -48,12 +45,11 @@ ERROR_MSG = ''
log = logging.getLogger('gajim.plugin_system.omemo')
try:
from omemo import file_crypto
except Exception as error:
log.exception(error)
ERROR_MSG = CRYPTOGRAPHY_MISSING
if log.getEffectiveLevel() == logging.DEBUG:
log_axolotl = logging.getLogger('axolotl')
log_axolotl.setLevel(logging.DEBUG)
log_axolotl.addHandler(logging.StreamHandler())
log_axolotl.propagate = False
try:
import google.protobuf
@@ -70,14 +66,10 @@ except Exception as error:
if not ERROR_MSG:
try:
from omemo.modules import omemo
from omemo.modules import omemo_devicelist
except Exception as error:
log.error(error)
ERROR_MSG = 'Error: %s' % error
# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init
@unique
class UserMessages(IntEnum):
@@ -87,7 +79,6 @@ class UserMessages(IntEnum):
class OmemoPlugin(GajimPlugin):
def init(self):
""" Init """
if ERROR_MSG:
self.activatable = False
self.available_text = ERROR_MSG
@@ -99,17 +90,13 @@ class OmemoPlugin(GajimPlugin):
'signed-in': (ged.PRECORE, self.signed_in),
'omemo-new-fingerprint': (ged.PRECORE, self._on_new_fingerprints),
}
self.modules = [
omemo,
omemo_devicelist,
]
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._gc_encrypt_message, None),
'decrypt': (self._message_received, None),
'send_message' + self.encryption_name: (
self.before_sendmessage, None),
'encryption_dialog' + self.encryption_name: (
@@ -198,11 +185,6 @@ class OmemoPlugin(GajimPlugin):
return False
return True
def _message_received(self, conn, obj, callback):
if conn.name == 'Local':
return
app.connections[conn.name].get_module('OMEMO').message_received(conn, obj, callback)
def _gc_encrypt_message(self, conn, obj, callback):
if conn.name == 'Local':
return
@@ -225,12 +207,11 @@ class OmemoPlugin(GajimPlugin):
@staticmethod
def _encrypt_file_thread(file, callback, *args, **kwargs):
encrypted_data, key, iv = file_crypto.encrypt_file(
file.get_data(full=True))
result = aes_encrypt_file(file.get_data(full=True))
file.encrypted = True
file.size = len(encrypted_data)
file.user_data = binascii.hexlify(iv + key).decode('utf-8')
file.data = encrypted_data
file.size = len(result.payload)
file.user_data = binascii.hexlify(result.iv + result.key).decode()
file.data = result.payload
if file.event.isSet():
return
GLib.idle_add(callback, file)
@@ -267,7 +248,7 @@ class OmemoPlugin(GajimPlugin):
else:
# check if we have devices for the contact
if not self.get_omemo(account).device_list_for(contact.jid):
con.query_devicelist(contact.jid, True)
con.request_devicelist(contact.jid, True)
self.print_message(chat_control, UserMessages.QUERY_DEVICES)
chat_control.sendmessage = False
return

View File

@@ -1,308 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# 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/>.
#
""" This module handles all the XMPP logic like creating different kind of
stanza nodes and getting data from stanzas.
"""
import logging
import random
from base64 import b64decode, b64encode
from nbxmpp.protocol import NS_PUBSUB, Iq
from nbxmpp.simplexml import Node
from gajim.common import app # pylint: disable=import-error
from gajim.plugins.helpers import log_calls # pylint: disable=import-error
NS_PUBSUB_EVENT = NS_PUBSUB + '#event'
NS_EME = 'urn:xmpp:eme:0'
NS_OMEMO = 'eu.siacs.conversations.axolotl'
NS_DEVICE_LIST = NS_OMEMO + '.devicelist'
NS_NOTIFY = NS_DEVICE_LIST + '+notify'
NS_BUNDLES = NS_OMEMO + '.bundles:'
NS_HINTS = 'urn:xmpp:hints'
log = logging.getLogger('gajim.plugin_system.omemo')
class PubsubNode(Node):
def __init__(self, data):
assert isinstance(data, Node)
Node.__init__(self, tag='pubsub', attrs={'xmlns': NS_PUBSUB})
self.addChild(node=data)
class OmemoMessage(Node):
def __init__(self, msg_dict):
# , contact_jid, key, iv, payload, dev_id, my_dev_id):
Node.__init__(self, 'encrypted', attrs={'xmlns': NS_OMEMO})
header = Node('header', attrs={'sid': msg_dict['sid']})
for rid, (key, prekey) in msg_dict['keys'].items():
if prekey:
child = header.addChild('key',
attrs={'prekey': 'true', 'rid': rid})
else:
child = header.addChild('key',
attrs={'rid': rid})
child.addData(b64encode(key).decode('utf-8'))
header.addChild('iv').addData(b64encode(msg_dict['iv']).decode('utf-8'))
self.addChild(node=header)
self.addChild('payload').addData(b64encode(msg_dict['payload'])
.decode('utf-8'))
class BundleInformationQuery(Iq):
def __init__(self, contact_jid, device_id):
assert isinstance(device_id, int)
id_ = app.get_an_id()
attrs = {'id': id_}
Iq.__init__(self, typ='get', attrs=attrs, to=contact_jid)
items = Node('items', attrs={'node': NS_BUNDLES + str(device_id),
'max_items': 1})
pubsub = PubsubNode(items)
self.addChild(node=pubsub)
class DevicelistQuery(Iq):
def __init__(self, contact_jid):
id_ = app.get_an_id()
attrs = {'id': id_}
Iq.__init__(self, typ='get', attrs=attrs, to=contact_jid)
items = Node('items', attrs={'node': NS_DEVICE_LIST, 'max_items': 1})
pubsub = PubsubNode(items)
self.addChild(node=pubsub)
def make_bundle(state_bundle):
result = Node('bundle', attrs={'xmlns': NS_OMEMO})
prekey_pub_node = result.addChild(
'signedPreKeyPublic',
attrs={'signedPreKeyId': state_bundle['signedPreKeyId']})
prekey_pub_node.addData(state_bundle['signedPreKeyPublic']
.decode('utf-8'))
prekey_sig_node = result.addChild('signedPreKeySignature')
prekey_sig_node.addData(state_bundle['signedPreKeySignature']
.decode('utf-8'))
identity_key_node = result.addChild('identityKey')
identity_key_node.addData(state_bundle['identityKey'].decode('utf-8'))
prekeys = result.addChild('prekeys')
for key in state_bundle['prekeys']:
prekeys.addChild('preKeyPublic',
attrs={'preKeyId': key[0]}) \
.addData(key[1].decode('utf-8'))
return result
@log_calls('OmemoPlugin')
def unpack_device_bundle(bundle, device_id):
pubsub = bundle.getTag('pubsub', namespace=NS_PUBSUB)
if not pubsub:
log.warning('OMEMO device bundle has no pubsub node')
return
items = pubsub.getTag('items', attrs={'node': NS_BUNDLES + str(device_id)})
if not items:
log.warning('OMEMO device bundle has no items node')
return
item = items.getTag('item', namespace=NS_PUBSUB)
if not item:
log.warning('OMEMO device bundle has no item node')
return
bundle = item.getTag('bundle', namespace=NS_OMEMO)
if not bundle:
log.warning('OMEMO device bundle has no bundle node')
return
signed_prekey_node = bundle.getTag('signedPreKeyPublic',
namespace=NS_OMEMO)
if not signed_prekey_node:
log.warning('OMEMO device bundle has no signedPreKeyPublic node')
return
result = {}
result['signedPreKeyPublic'] = decode_data(signed_prekey_node)
if not result['signedPreKeyPublic']:
log.warning('OMEMO device bundle has no signedPreKeyPublic data')
return
if not signed_prekey_node.getAttr('signedPreKeyId'):
log.warning('OMEMO device bundle has no signedPreKeyId')
return
result['signedPreKeyId'] = int(signed_prekey_node.getAttr(
'signedPreKeyId'))
signed_signature_node = bundle.getTag('signedPreKeySignature',
namespace=NS_OMEMO)
if not signed_signature_node:
log.warning('OMEMO device bundle has no signedPreKeySignature node')
return
result['signedPreKeySignature'] = decode_data(signed_signature_node)
if not result['signedPreKeySignature']:
log.warning('OMEMO device bundle has no signedPreKeySignature data')
return
identity_key_node = bundle.getTag('identityKey', namespace=NS_OMEMO)
if not identity_key_node:
log.warning('OMEMO device bundle has no identityKey node')
return
result['identityKey'] = decode_data(identity_key_node)
if not result['identityKey']:
log.warning('OMEMO device bundle has no identityKey data')
return
prekeys = bundle.getTag('prekeys', namespace=NS_OMEMO)
if not prekeys or len(prekeys.getChildren()) == 0:
log.warning('OMEMO device bundle has no prekys')
return
picked_key_node = random.SystemRandom().choice(prekeys.getChildren())
if not picked_key_node.getAttr('preKeyId'):
log.warning('OMEMO PreKey has no id set')
return
result['preKeyId'] = int(picked_key_node.getAttr('preKeyId'))
result['preKeyPublic'] = decode_data(picked_key_node)
if not result['preKeyPublic']:
return
return result
@log_calls('OmemoPlugin')
def unpack_encrypted(encrypted_node):
""" Unpacks the encrypted node, decodes the data and returns a msg_dict.
"""
if not encrypted_node.getNamespace() == NS_OMEMO:
log.warning("Encrypted node with wrong NS")
return
header_node = encrypted_node.getTag('header', namespace=NS_OMEMO)
if not header_node:
log.warning("OMEMO message without header")
return
if not header_node.getAttr('sid'):
log.warning("OMEMO message without sid in header")
return
sid = int(header_node.getAttr('sid'))
iv_node = header_node.getTag('iv', namespace=NS_OMEMO)
if not iv_node:
log.warning("OMEMO message without iv")
return
iv = decode_data(iv_node)
if not iv:
log.warning("OMEMO message without iv data")
payload_node = encrypted_node.getTag('payload', namespace=NS_OMEMO)
payload = None
if payload_node:
payload = decode_data(payload_node)
key_nodes = header_node.getTags('key')
if len(key_nodes) < 1:
log.warning("OMEMO message without keys")
return
keys = {}
for kn in key_nodes:
rid = kn.getAttr('rid')
if not rid:
log.warning('Omemo key without rid')
continue
if not kn.getData():
log.warning('Omemo key without data')
continue
keys[int(rid)] = decode_data(kn)
result = {'sid': sid, 'iv': iv, 'keys': keys, 'payload': payload}
return result
def unpack_device_list_update(stanza, account):
""" Unpacks the device list from a stanza
Parameters
----------
stanza
Returns
-------
[int]
List of device ids or empty list if nothing found
"""
event_node = stanza.getTag('event', namespace=NS_PUBSUB_EVENT)
if not event_node:
event_node = stanza.getTag('pubsub', namespace=NS_PUBSUB)
result = []
if not event_node:
log.warning(account + ' => Device list update event node empty!')
return result
items = event_node.getTag('items', {'node': NS_DEVICE_LIST})
if not items or len(items.getChildren()) != 1:
log.warning(
account +
' => Device list update items node empty or not omemo device update')
return result
list_node = items.getChildren()[0].getTag('list')
if not list_node or len(list_node.getChildren()) == 0:
log.warning(account + ' => Device list update list node empty!')
return result
devices_nodes = list_node.getChildren()
for dn in devices_nodes:
_id = dn.getAttr('id')
if _id:
result.append(int(_id))
return result
def decode_data(node):
""" Fetch the data from specified node and b64decode it. """
data = node.getData()
if not data:
log.warning("No node data")
return
try:
return b64decode(data)
except:
log.warning('b64decode broken')
return
def successful(stanza):
""" Check if stanza type is result. """
return stanza.getAttr('type') == 'result'