Skip to content
Snippets Groups Projects
Commit 67d6ed44 authored by Thilo Molitor's avatar Thilo Molitor
Browse files

initial omemo plugin commit

parent bcc4a7a8
No related branches found
No related tags found
No related merge requests found
[style]
based_on_style = pep8
align_closing_bracket_with_visual_indent = true
join_multiple_lines = true
This diff is collapsed.
# OMEMO Plugin for Gajim
This is an experimental plugin that adds support for the [OMEMO
Encryption](http://conversations.im/omemo) to [Gajim](https://gajim.org/). This
plugin is [free software](http://www.gnu.org/philosophy/free-sw.en.html)
distributed under the GNU General Public License version 3 or any later version.
**DO NOT rely on this plugin to protect sensitive information!**
## Dependencies
All dependencies can be installed with `pip`. (Depending on your setup you might
want to use `pip2` as Gajim is using python2.7)
* python-axolotl
## Installation
Clone the git repository into Gajim's plugin directory.
```shell
mkdir ~/.local/share/gajim/plugins -p
cd ~/.local/share/gajim/plugins
git clone git@github.com:kalkin/gajim-omemo.git
```
## Running
Enable *OMEMO Multi-End Message and Object Encryption* in the Plugin-Manager.
Before exchanging encrypted messages with a contact you have to hit the *Get
Device Keys* button. (Repeat that if you or your contact get new devices.)
Currently the plugin has no user interface for confirming the own and foreign
device keys. It uses trust on first use. This will be added in near future.
## Debugging
To see OMEMO related debug output start Gajim with the parameter `-l
gajim.plugin_system.omemo=DEBUG`.
## Support this project
I develop this project in my free time. Your donation allows me to spend more
time working on it and on free software generally.
My Bitcoin Address is: `1CnNM3Mree9hU8eRjCXrfCWV mX6oBnEfV1`
[![Support Me via Flattr](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=_kalkin&url=https://github.com/kalkin/gajim-omemo&title=gajim-omemo&language=en_US&tags=github&category=people)
## I found a bug
Please report it to the [issue
tracker](https://github.com/kalkin/gajim-omemo/issues). If you are experiencing
misbehaviour please provide detailed steps to reproduce and debugging output.
Always mention the exact Gajim version.
## Contact
You can contact me via email at `bahtiar@gadimov.de` or follow me on
[Twitter](https://twitter.com/_kalkin)
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright 2015 Daniel Gultsch <daniel@cgultsch.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/>.
#
import logging
from common import caps_cache, gajim, ged
from common.pep import SUPPORTED_PERSONAL_USER_EVENTS
from plugins import GajimPlugin
from plugins.helpers import log_calls
from .state import OmemoState
from .ui import Ui
from .xmpp import (
NS_NOTIFY, NS_OMEMO, BundleInformationAnnouncement, BundleInformationQuery,
DeviceListAnnouncement, DevicelistPEP, OmemoMessage, successful,
unpack_device_bundle, unpack_device_list_update, unpack_message)
iq_ids_to_callbacks = {}
log = logging.getLogger('gajim.plugin_system.omemo')
class OmemoPlugin(GajimPlugin):
omemo_states = {}
ui_list = {}
@log_calls('OmemoPlugin')
def init(self):
self.events_handlers = {
'message-received': (ged.PRECORE, self.message_received),
'pep-received': (ged.PRECORE, self.handle_device_list_update),
'raw-iq-received': (ged.PRECORE, self.handle_iq_received),
'signed-in': (ged.PRECORE, self.signed_in),
'stanza-message-outgoing':
(ged.PRECORE, self.handle_outgoing_msgs),
}
self.config_dialog = None
self.gui_extension_points = {'chat_control_base':
(self.connect_ui, None)}
SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP)
@log_calls('OmemoPlugin')
def get_omemo_state(self, account):
if account not in self.omemo_states:
self.omemo_states[account] = OmemoState(account)
return self.omemo_states[account]
@log_calls('OmemoPlugin')
def signed_in(self, show):
account = show.conn.name
state = self.get_omemo_state(account)
self.announce_support(state)
@log_calls('OmemoPlugin')
def activate(self):
if NS_NOTIFY not in gajim.gajim_common_features:
gajim.gajim_common_features.append(NS_NOTIFY)
self._compute_caps_hash()
@log_calls('OmemoPlugin')
def deactivate(self):
if NS_NOTIFY in gajim.gajim_common_features:
gajim.gajim_common_features.remove(NS_NOTIFY)
self._compute_caps_hash()
@log_calls('OmemoPlugin')
def _compute_caps_hash(self):
for a in gajim.connections:
gajim.caps_hash[a] = caps_cache.compute_caps_hash(
[
gajim.gajim_identity
],
gajim.gajim_common_features + gajim.gajim_optional_features[a])
# re-send presence with new hash
connected = gajim.connections[a].connected
if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
gajim.connections[a].status)
@log_calls('OmemoPlugin')
def message_received(self, msg):
if msg.stanza.getTag('encrypted', namespace=NS_OMEMO):
account = msg.conn.name
log.debug(account + ' ⇒ OMEMO msg received')
state = self.get_omemo_state(account)
if msg.forwarded and msg.sent:
from_jid = str(msg.stanza.getAttr('to')) # why gajim? why?
log.debug('message was forwarded doing magic')
else:
from_jid = str(msg.stanza.getAttr('from'))
msg_dict = unpack_message(msg.stanza)
msg_dict['sender_jid'] = gajim.get_jid_without_resource(from_jid)
plaintext = state.decrypt_msg(msg_dict)
if not plaintext:
return
msg.msgtxt = plaintext
msg.stanza.setBody(msg.msgtxt)
self.update_prekeys(account, msg_dict['sender_jid'])
contact_jid = gajim.get_jid_without_resource(msg.fjid)
if account in self.ui_list and \
contact_jid in self.ui_list[account]:
self.ui_list[account][contact_jid].activate_omemo()
return False
elif msg.stanza.getTag('body'):
account = msg.conn.name
from_jid = str(msg.stanza.getAttr('from'))
jid = gajim.get_jid_without_resource(from_jid)
gui = self.ui_list[account][jid]
if gui and gui.encryption_active():
gui.plain_warning()
@log_calls('OmemoPlugin')
def handle_device_list_update(self, event):
""" Check if the passed event is a device list update and store the new
device ids.
Parameters
----------
event : MessageReceivedEvent
Returns
-------
bool
True if the given event was a valid device list update event
See also
--------
4.2 Discovering peer support
http://conversations.im/xeps/multi-end.html#usecases-discovering
"""
if event.pep_type != 'headline':
return False
devices_list = unpack_device_list_update(event)
if len(devices_list) == 0:
return False
account = event.conn.name
contact_jid = gajim.get_jid_without_resource(event.fjid)
state = self.get_omemo_state(account)
my_jid = gajim.get_jid_from_account(account)
log.debug(account + ' ⇒ Received OMEMO pep for jid ' + contact_jid)
if contact_jid == my_jid:
log.debug(state.name + ' ⇒ Received own device_list ' + str(
devices_list))
state.add_own_devices(devices_list)
if not state.own_device_id_published() or anydup(
state.own_devices):
# Our own device_id is not in the list, it could be
# overwritten by some other client?
# also remove duplicates
devices_list = list(set(state.own_devices))
devices_list.append(state.own_device_id)
self.publish_own_devices_list(state)
else:
state.add_devices(contact_jid, devices_list)
if account in self.ui_list and contact_jid in self.ui_list[
account]:
self.ui_list[account][contact_jid].toggle_omemo(True)
self.update_prekeys(account, contact_jid)
return True
@log_calls('OmemoPlugin')
def publish_own_devices_list(self, state):
devices_list = state.own_devices
devices_list += [state.own_device_id]
log.debug(state.name + ' ⇒ Publishing own devices_list ' + str(
devices_list))
iq = DeviceListAnnouncement(devices_list)
gajim.connections[state.name].connection.send(iq)
id_ = str(iq.getAttr('id'))
iq_ids_to_callbacks[id_] = lambda event: log.debug(event)
@log_calls('OmemoPlugin')
def connect_ui(self, chat_control):
account = chat_control.contact.account.name
jid = chat_control.contact.jid
if account not in self.ui_list:
self.ui_list[account] = {}
self.ui_list[account][jid] = Ui(self, chat_control)
def are_keys_missing(self, contact):
""" Used by the ui to set the state of the PreKeyButton. """
account = contact.account.name
my_jid = gajim.get_jid_from_account(account)
state = self.get_omemo_state(account)
result = 0
result += len(state.devices_without_sessions(str(contact.jid)))
result += len(state.own_devices_without_sessions(my_jid))
return result
@log_calls('OmemoPlugin')
def handle_iq_received(self, event):
global iq_ids_to_callbacks
id_ = str(event.stanza.getAttr("id"))
if id_ in iq_ids_to_callbacks:
try:
iq_ids_to_callbacks[id_](event.stanza)
except:
raise
finally:
del iq_ids_to_callbacks[id_]
@log_calls('OmemoPlugin')
def query_prekey(self, contact):
account = contact.account.name
state = self.get_omemo_state(account)
to_jid = contact.jid
my_jid = gajim.get_jid_from_account(account)
for device_id in state.devices_without_sessions(to_jid):
self.fetch_device_bundle_information(state, to_jid, device_id)
for device_id in state.own_devices_without_sessions(my_jid):
self.fetch_device_bundle_information(state, my_jid, device_id)
@log_calls('OmemoPlugin')
def fetch_device_bundle_information(self, state, jid, device_id):
""" Fetch bundle information for specified jid, key, and create axolotl
session on success.
Parameters
----------
state : (OmemoState)
The OmemoState which is missing device bundle information
jid : str
The jid to query for bundle information
device_id : int
The device_id for which we are missing an axolotl session
"""
log.debug(state.name + '→ Fetch bundle information for dev ' + str(
device_id) + ' and jid ' + jid)
iq = BundleInformationQuery(jid, device_id)
iq_id = str(iq.getAttr('id'))
iq_ids_to_callbacks[iq_id] = \
lambda stanza: self.session_from_prekey_bundle(state, stanza,
jid, device_id)
gajim.connections[state.name].connection.send(iq)
@log_calls('OmemoPlugin')
def session_from_prekey_bundle(self, state, stanza, recipient_id,
device_id):
""" Starts a session when a bundle information announcement is received.
This method tries to build an axolotl session when a PreKey bundle
is fetched. If building the axolotl session is successful it tries
to update the ui by calling `self.update_prekeys()`.
If a session can not be build it will fail silently but log the a
warning.
See also
--------
4.3. Announcing bundle information:
http://conversations.im/xeps/multi-end.html#usecases-announcing
4.4 Building a session:
http://conversations.im/xeps/multi-end.html#usecases-building
Parameters:
-----------
state : (OmemoState)
The OmemoState used
stanza
The stanza object received from callback
recipient_id : str
The recipient jid
device_id : int
The device_id for which the bundle was queried
"""
bundle_dict = unpack_device_bundle(stanza, device_id)
if not bundle_dict:
log.warn('Failed requesting a bundle')
return
if state.build_session(recipient_id, device_id, bundle_dict):
self.update_prekeys(state.name, recipient_id)
@log_calls('OmemoPlugin')
def update_prekeys(self, account, recipient_id):
""" Updates the "Get Prekeys" Button in the ui.
Parameters:
----------
account : str
The account name
recipient_id : str
The recipient jid
"""
if account in self.ui_list:
if recipient_id in self.ui_list[account]:
self.ui_list[account][recipient_id].update_prekeys()
@log_calls('OmemoPlugin')
def announce_support(self, account):
""" Announce OMEMO support for an account via PEP.
In order for other clients/devices to be able to initiate a session
with gajim, it first has to announce itself by adding its device ID
to the devicelist PEP node.
Parameters
----------
account : str
The account name
See also
--------
4.3 Announcing bundle information:
http://conversations.im/xeps/multi-end.html#usecases-announcing
"""
state = self.get_omemo_state(account.name)
iq = BundleInformationAnnouncement(state.bundle, state.own_device_id)
gajim.connections[state.name].connection.send(iq)
id_ = str(iq.getAttr("id"))
log.debug(account.name + " → Announcing OMEMO support via PEP")
iq_ids_to_callbacks[id_] = lambda stanza: \
self.handle_announcement_result(stanza, state)
@log_calls('OmemoPlugin')
def handle_announcement_result(self, stanza, state):
""" Updates own device list if announcement was successfull.
If the OMEMO support announcement was successfull update own device
list if needed.
Parameters
----------
stanza
The stanza object received from callback
"""
account = state.name
state = self.get_omemo_state(account)
if successful(stanza):
log.debug(account + ' → Publishing bundle was successful')
if not state.own_device_id_published():
log.debug(account + ' → Device list needs updating')
self.publish_own_devices_list(state)
else:
log.debug(account + ' → Device list up to date')
else:
log.debug(account + ' → Publishing bundle was NOT successful')
@log_calls('OmemoPlugin')
def clear_device_list(self, contact):
account = contact.account.name
state = self.get_omemo_state(account)
devices_list = [state.own_device_id]
log.info(state.name + ' ⇒ Clearing devices_list ' + str(devices_list))
iq = DeviceListAnnouncement(devices_list)
gajim.connections[state.name].connection.send(iq)
id_ = str(iq.getAttr('id'))
iq_ids_to_callbacks[id_] = lambda event: log.info(event)
@log_calls('OmemoPlugin')
def handle_outgoing_msgs(self, event):
if not event.msg_iq.getTag('body'):
return
plaintext = event.msg_iq.getBody().encode('utf8')
account = event.conn.name
state = self.get_omemo_state(account)
full_jid = str(event.msg_iq.getAttr('to'))
to_jid = gajim.get_jid_without_resource(full_jid)
if to_jid not in state.omemo_enabled:
return False
try:
msg_dict = state.create_msg(
gajim.get_jid_from_account(account), to_jid, plaintext)
if not msg_dict:
return True
encrypted_node = OmemoMessage(msg_dict)
event.msg_iq.delChild('body')
event.msg_iq.addChild(node=encrypted_node)
log.debug(account + '' + str(event.msg_iq))
except:
return True
@log_calls('OmemoPlugin')
def is_omemo_enabled(self, contact):
account = contact.account.name
state = self.get_omemo_state(account)
return contact.jid in state.omemo_enabled
@log_calls('OmemoPlugin')
def omemo_enable_for(self, contact):
""" Used by the ui to enable omemo for a specified contact """
account = contact.account.name
state = self.get_omemo_state(account)
state.omemo_enabled |= {contact.jid}
@log_calls('OmemoPlugin')
def omemo_disable_for(self, contact):
""" Used by the ui to disable omemo for a specified contact """
account = contact.account.name
state = self.get_omemo_state(account)
state.omemo_enabled.remove(contact.jid)
@log_calls('OmemoPlugin')
def has_omemo(self, contact):
""" Used by the ui to find out if omemo controls shoudl be displayed for
the given contact.
Returns
-------
bool
True if there are known device_ids/clients supporting OMEMO
"""
account = contact.account.name
state = self.get_omemo_state(account)
if state.device_ids_for(contact):
return True
return False
@log_calls('OmemoPlugin')
def anydup(thelist):
seen = set()
for x in thelist:
if x in seen:
return True
seen.add(x)
return False
# -*- coding: utf-8 -*-
#
# Copyright 2014 Jonathan Zdziarski <jonathan@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.omemo')
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
log.info(result)
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)
class NoValidSessions(Exception):
pass
[info]
name: OMEMO Multi-End Message and Object Encryption
short_name: omemo
version: 0.1
description: OMEMO is an XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption. It is an open standard based on Axolotl and PEP which can be freely used and implemented by anyone.
authors: Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
homepage: http://github.com/kalkin/gajim-omemo.git
omemo/omemo.png

5.62 KiB

# -*- 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/>.
#
import logging
import os
from base64 import b64encode
from axolotl.ecc.djbec import DjbECPublicKey
from axolotl.identitykey import IdentityKey
from axolotl.invalidmessageexception import InvalidMessageException
from axolotl.invalidversionexception import (InvalidVersionException,
NoSessionException)
from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage
from axolotl.protocol.whispermessage import WhisperMessage
from axolotl.sessionbuilder import SessionBuilder
from axolotl.sessioncipher import SessionCipher
from axolotl.state.prekeybundle import PreKeyBundle
from axolotl.util.keyhelper import KeyHelper
from Crypto.Random import get_random_bytes
from common import gajim
from .aes_gcm import NoValidSessions, aes_decrypt, aes_encrypt
from .store.liteaxolotlstore import LiteAxolotlStore
DB_DIR = gajim.gajimpaths.data_root
log = logging.getLogger('gajim.plugin_system.omemo')
class OmemoState:
session_ciphers = {}
omemo_enabled = set()
device_ids = {}
own_devices = []
def __init__(self, name):
self.name = name
db_name = 'omemo_' + name + '.db'
db_file = os.path.join(DB_DIR, db_name)
self.store = LiteAxolotlStore(db_file)
def build_session(self, recipient_id, device_id, bundle_dict):
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:])
signedPreKeyPublic = DjbECPublicKey(bundle_dict['signedPreKeyPublic'][
1:])
identityKey = IdentityKey(DjbECPublicKey(bundle_dict['identityKey'][
1:]))
prekey_bundle = PreKeyBundle(
registration_id, device_id, bundle_dict['preKeyId'], preKeyPublic,
bundle_dict['signedPreKeyId'], signedPreKeyPublic,
bundle_dict['signedPreKeySignature'], identityKey)
sessionBuilder.processPreKeyBundle(prekey_bundle)
return self.get_session_cipher(recipient_id, device_id)
def add_devices(self, name, devices):
log.debug('Saving devices for ' + name + '' + str(devices))
self.device_ids[name] = devices
def add_own_devices(self, devices):
self.own_devices = devices
@property
def own_device_id(self):
reg_id = self.store.getLocalRegistrationId()
assert reg_id is not None, \
"Requested device_id but there is no generated"
return ((reg_id % 2147483646) + 1)
def own_device_id_published(self):
return self.own_device_id in self.own_devices
def device_ids_for(self, contact):
account = contact.account.name
log.debug(account + ' ⇒ Searching device_ids for contact ' +
contact.jid)
if contact.jid not in self.device_ids:
log.debug(contact.jid + '¬∈ devices_ids[' + account + ']')
return None
log.debug(account + ' ⇒ found device_ids ' + str(self.device_ids[
contact.jid]))
return self.device_ids[contact.jid]
@property
def bundle(self):
prekeys = [
(k.getId(), b64encode(k.getKeyPair().getPublicKey().serialize()))
for k in self.store.loadPreKeys()
]
identityKeyPair = self.store.getIdentityKeyPair()
signedPreKey = KeyHelper.generateSignedPreKey(
identityKeyPair, KeyHelper.getRandomSequence(65536))
self.store.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
result = {
'signedPreKeyId': signedPreKey.getId(),
'signedPreKeyPublic':
b64encode(signedPreKey.getKeyPair().getPublicKey().serialize()),
'signedPreKeySignature': b64encode(signedPreKey.getSignature()),
'identityKey':
b64encode(identityKeyPair.getPublicKey().serialize()),
'prekeys': prekeys
}
return result
def decrypt_msg(self, msg_dict):
own_id = self.own_device_id
if own_id not in msg_dict['keys']:
log.warn('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 = msg_dict['keys'][own_id]
try:
key = self.handlePreKeyWhisperMessage(sender_jid, sid,
encrypted_key)
except (InvalidVersionException, InvalidMessageException):
try:
key = self.handleWhisperMessage(sender_jid, sid, encrypted_key)
except (NoSessionException, InvalidMessageException) as e:
log.error('No Session found ' + e.message)
log.error('sender_jid → ' + str(sender_jid) + ' sid =>' + str(
sid))
return
result = aes_decrypt(key, iv, payload)
log.debug("Decrypted msg ⇒ " + result)
return result
def create_msg(self, from_jid, jid, plaintext):
key = get_random_bytes(16)
iv = get_random_bytes(16)
encrypted_keys = {}
devices_list = self.device_list_for(jid)
if len(devices_list) == 0:
log.error(self.name + ' → No known devices')
return
for dev in devices_list:
self.get_session_cipher(jid, dev)
session_ciphers = self.session_ciphers[jid]
if not session_ciphers:
log.warn('No session ciphers for ' + jid)
return
my_other_devices = set(self.own_devices) - set({self.own_device_id})
for dev in my_other_devices:
cipher = self.get_session_cipher(from_jid, dev)
encrypted_keys[dev] = cipher.encrypt(key).serialize()
for rid, cipher in session_ciphers.items():
try:
encrypted_keys[rid] = cipher.encrypt(key).serialize()
except:
log.warn(self.name + ' → Failed to find key for device ' + str(
rid))
if len(encrypted_keys) == 0:
log_msg = 'Encrypted keys empty'
log.error(log_msg)
raise NoValidSessions(log_msg)
payload = aes_encrypt(key, iv, plaintext)
log.info('Payload')
log.info(payload)
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
'jid': jid,
'iv': iv,
'payload': payload}
log.debug('encrypted message')
log.debug(result)
return result
def device_list_for(self, jid):
if jid not in self.device_ids:
return set()
return set(self.device_ids[jid])
def devices_without_sessions(self, jid):
""" List device_ids for the given jid which have no axolotl session.
Parameters
----------
jid : string
The contacts jid
Returns
-------
[int]
A list of device_ids
"""
known_devices = self.device_list_for(jid)
missing_devices = [dev
for dev in known_devices
if not self.store.containsSession(jid, dev)]
log.debug(self.name + ' → Missing device sessions: ' + str(
missing_devices))
return missing_devices
def own_devices_without_sessions(self, own_jid):
""" List own device_ids which have no axolotl session.
Parameters
----------
own_jid : string
Workaround for missing own jid in OmemoState
Returns
-------
[int]
A list of device_ids
"""
known_devices = set(self.own_devices) - {self.own_device_id}
missing_devices = [dev
for dev in known_devices
if not self.store.containsSession(own_jid, dev)]
log.debug(self.name + ' → Missing device sessions: ' + str(
missing_devices))
return missing_devices
def get_session_cipher(self, jid, device_id):
if jid not in self.session_ciphers:
self.session_ciphers[jid] = {}
if device_id not in self.session_ciphers[jid]:
cipher = SessionCipher(self.store, self.store, self.store,
self.store, jid, device_id)
self.session_ciphers[jid][device_id] = cipher
return self.session_ciphers[jid][device_id]
def handlePreKeyWhisperMessage(self, recipient_id, device_id, key):
preKeyWhisperMessage = PreKeyWhisperMessage(serialized=key)
sessionCipher = self.get_session_cipher(recipient_id, device_id)
key = sessionCipher.decryptPkmsg(preKeyWhisperMessage)
log.debug('PreKeyWhisperMessage -> ' + str(key))
return key
def handleWhisperMessage(self, recipient_id, device_id, key):
whisperMessage = WhisperMessage(serialized=key)
sessionCipher = self.get_session_cipher(recipient_id, device_id)
key = sessionCipher.decryptMsg(whisperMessage)
log.debug('WhisperMessage -> ' + str(key))
return key
__author__ = 'tarek'
# -*- coding: utf-8 -*-
#
# Copyright 2015 Tarek Galal <tare2.galal@gmail.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 logging
import sqlite3
from axolotl.state.axolotlstore import AxolotlStore
from axolotl.util.keyhelper import KeyHelper
from .liteidentitykeystore import LiteIdentityKeyStore
from .liteprekeystore import LitePreKeyStore
from .litesessionstore import LiteSessionStore
from .litesignedprekeystore import LiteSignedPreKeyStore
log = logging.getLogger('gajim.plugin_system.omemo')
DEFAULT_PREKEY_AMOUNT = 100
class LiteAxolotlStore(AxolotlStore):
def __init__(self, db):
log.debug('Opening the DB ' + str(db))
conn = sqlite3.connect(db, check_same_thread=False)
conn.text_factory = bytes
self.identityKeyStore = LiteIdentityKeyStore(conn)
self.preKeyStore = LitePreKeyStore(conn)
self.signedPreKeyStore = LiteSignedPreKeyStore(conn)
self.sessionStore = LiteSessionStore(conn)
if not self.getLocalRegistrationId():
log.info("Generating Axolotl keys for db" + str(db))
self._generate_axolotl_keys()
def _generate_axolotl_keys(self):
identityKeyPair = KeyHelper.generateIdentityKeyPair()
registrationId = KeyHelper.generateRegistrationId()
preKeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(),
DEFAULT_PREKEY_AMOUNT)
self.storeLocalData(registrationId, identityKeyPair)
for preKey in preKeys:
self.storePreKey(preKey.getId(), preKey)
def getIdentityKeyPair(self):
return self.identityKeyStore.getIdentityKeyPair()
def storeLocalData(self, registrationId, identityKeyPair):
self.identityKeyStore.storeLocalData(registrationId, identityKeyPair)
def getLocalRegistrationId(self):
return self.identityKeyStore.getLocalRegistrationId()
def saveIdentity(self, recepientId, identityKey):
self.identityKeyStore.saveIdentity(recepientId, identityKey)
def isTrustedIdentity(self, recepientId, identityKey):
return self.identityKeyStore.isTrustedIdentity(recepientId,
identityKey)
def loadPreKey(self, preKeyId):
return self.preKeyStore.loadPreKey(preKeyId)
def loadPreKeys(self):
return self.preKeyStore.loadPendingPreKeys()
def storePreKey(self, preKeyId, preKeyRecord):
self.preKeyStore.storePreKey(preKeyId, preKeyRecord)
def containsPreKey(self, preKeyId):
return self.preKeyStore.containsPreKey(preKeyId)
def removePreKey(self, preKeyId):
self.preKeyStore.removePreKey(preKeyId)
def loadSession(self, recepientId, deviceId):
return self.sessionStore.loadSession(recepientId, deviceId)
def getSubDeviceSessions(self, recepientId):
return self.sessionStore.getSubDeviceSessions(recepientId)
def storeSession(self, recepientId, deviceId, sessionRecord):
self.sessionStore.storeSession(recepientId, deviceId, sessionRecord)
def containsSession(self, recepientId, deviceId):
return self.sessionStore.containsSession(recepientId, deviceId)
def deleteSession(self, recepientId, deviceId):
self.sessionStore.deleteSession(recepientId, deviceId)
def deleteAllSessions(self, recepientId):
self.sessionStore.deleteAllSessions(recepientId)
def loadSignedPreKey(self, signedPreKeyId):
return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId)
def loadSignedPreKeys(self):
return self.signedPreKeyStore.loadSignedPreKeys()
def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
self.signedPreKeyStore.storeSignedPreKey(signedPreKeyId,
signedPreKeyRecord)
def containsSignedPreKey(self, signedPreKeyId):
return self.signedPreKeyStore.containsSignedPreKey(signedPreKeyId)
def removeSignedPreKey(self, signedPreKeyId):
self.signedPreKeyStore.removeSignedPreKey(signedPreKeyId)
# -*- coding: utf-8 -*-
#
# Copyright 2015 Tarek Galal <tare2.galal@gmail.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/>.
#
from axolotl.ecc.djbec import DjbECPrivateKey, DjbECPublicKey
from axolotl.identitykey import IdentityKey
from axolotl.identitykeypair import IdentityKeyPair
from axolotl.state.identitykeystore import IdentityKeyStore
class LiteIdentityKeyStore(IdentityKeyStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
dbConn.execute(
"CREATE TABLE IF NOT EXISTS identities (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," + "recipient_id TEXT," +
"registration_id INTEGER, public_key BLOB, private_key BLOB," +
"next_prekey_id NUMBER, timestamp NUMBER, trust NUMBER);")
def getIdentityKeyPair(self):
q = "SELECT public_key, private_key FROM identities " + \
"WHERE recipient_id = -1"
c = self.dbConn.cursor()
c.execute(q)
result = c.fetchone()
publicKey, privateKey = result
return IdentityKeyPair(
IdentityKey(DjbECPublicKey(publicKey[1:])),
DjbECPrivateKey(privateKey))
def getLocalRegistrationId(self):
q = "SELECT registration_id FROM identities WHERE recipient_id = -1"
c = self.dbConn.cursor()
c.execute(q)
result = c.fetchone()
return result[0] if result else None
def storeLocalData(self, registrationId, identityKeyPair):
q = "INSERT INTO identities( " + \
"recipient_id, registration_id, public_key, private_key) " + \
"VALUES(-1, ?, ?, ?)"
c = self.dbConn.cursor()
c.execute(q,
(registrationId,
identityKeyPair.getPublicKey().getPublicKey().serialize(),
identityKeyPair.getPrivateKey().serialize()))
self.dbConn.commit()
def saveIdentity(self, recipientId, identityKey):
q = "INSERT INTO identities (recipient_id, public_key) VALUES(?, ?)"
c = self.dbConn.cursor()
c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
self.dbConn.commit()
def isTrustedIdentity(self, recipientId, identityKey):
return True
# -*- coding: utf-8 -*-
#
# Copyright 2015 Tarek Galal <tare2.galal@gmail.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/>.
#
from axolotl.state.prekeyrecord import PreKeyRecord
from axolotl.state.prekeystore import PreKeyStore
class LitePreKeyStore(PreKeyStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
dbConn.execute("CREATE TABLE IF NOT EXISTS prekeys(" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN, " +
" record BLOB);")
def loadPreKey(self, preKeyId):
q = "SELECT record FROM prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, ))
result = cursor.fetchone()
if not result:
raise Exception("No such prekeyRecord!")
return PreKeyRecord(serialized=result[0])
def loadPendingPreKeys(self):
q = "SELECT record FROM prekeys"
cursor = self.dbConn.cursor()
cursor.execute(q)
result = cursor.fetchall()
return [PreKeyRecord(serialized=result[0]) for result in result]
# return [PreKeyRecord(serialized=result[0]) for r in result]
def storePreKey(self, preKeyId, preKeyRecord):
# self.removePreKey(preKeyId)
q = "INSERT INTO prekeys (prekey_id, record) VALUES(?,?)"
cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, preKeyRecord.serialize()))
self.dbConn.commit()
def containsPreKey(self, preKeyId):
q = "SELECT record FROM prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, ))
return cursor.fetchone() is not None
def removePreKey(self, preKeyId):
q = "DELETE FROM prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, ))
self.dbConn.commit()
# -*- coding: utf-8 -*-
#
# Copyright 2015 Tarek Galal <tare2.galal@gmail.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/>.
#
from axolotl.state.sessionrecord import SessionRecord
from axolotl.state.sessionstore import SessionStore
class LiteSessionStore(SessionStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
dbConn.execute("CREATE TABLE IF NOT EXISTS sessions (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"recipient_id TEXT," + "device_id INTEGER," +
"record BLOB," + "timestamp INTEGER, " +
"UNIQUE(recipient_id, device_id));")
def loadSession(self, recipientId, deviceId):
q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId, deviceId))
result = c.fetchone()
if result:
return SessionRecord(serialized=result[0])
else:
return SessionRecord()
def getSubDeviceSessions(self, recipientId):
q = "SELECT device_id from sessions WHERE recipient_id = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId, ))
result = c.fetchall()
deviceIds = [r[0] for r in result]
return deviceIds
def storeSession(self, recipientId, deviceId, sessionRecord):
self.deleteSession(recipientId, deviceId)
q = "INSERT INTO sessions(recipient_id, device_id, record) VALUES(?,?,?)"
c = self.dbConn.cursor()
c.execute(q, (recipientId, deviceId, sessionRecord.serialize()))
self.dbConn.commit()
def containsSession(self, recipientId, deviceId):
q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId, deviceId))
result = c.fetchone()
return result is not None
def deleteSession(self, recipientId, deviceId):
q = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?"
self.dbConn.cursor().execute(q, (recipientId, deviceId))
self.dbConn.commit()
def deleteAllSessions(self, recipientId):
q = "DELETE FROM sessions WHERE recipient_id = ?"
self.dbConn.cursor().execute(q, (recipientId, ))
self.dbConn.commit()
# -*- coding: utf-8 -*-
#
# Copyright 2015 Tarek Galal <tare2.galal@gmail.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/>.
#
from axolotl.invalidkeyidexception import InvalidKeyIdException
from axolotl.state.signedprekeyrecord import SignedPreKeyRecord
from axolotl.state.signedprekeystore import SignedPreKeyStore
class LiteSignedPreKeyStore(SignedPreKeyStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
dbConn.execute(
"CREATE TABLE IF NOT EXISTS signed_prekeys (" +
"_id INTEGER PRIMARY" + " KEY AUTOINCREMENT," +
"prekey_id INTEGER UNIQUE, timestamp INTEGER, record BLOB);")
def loadSignedPreKey(self, signedPreKeyId):
q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, ))
result = cursor.fetchone()
if not result:
raise InvalidKeyIdException("No such signedprekeyrecord! %s " %
signedPreKeyId)
return SignedPreKeyRecord(serialized=result[0])
def loadSignedPreKeys(self):
q = "SELECT record FROM signed_prekeys"
cursor = self.dbConn.cursor()
cursor.execute(q, )
result = cursor.fetchall()
results = []
for row in result:
results.append(SignedPreKeyRecord(serialized=row[0]))
return results
def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
q = "INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)"
cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, signedPreKeyRecord.serialize()))
self.dbConn.commit()
def containsSignedPreKey(self, signedPreKeyId):
q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, ))
return cursor.fetchone() is not None
def removeSignedPreKey(self, signedPreKeyId):
q = "DELETE FROM signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, ))
self.dbConn.commit()
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright 2015 Daniel Gultsch <daniel@cgultsch.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/>.
#
import logging
import gtk
from message_control import TYPE_CHAT, MessageControl
log = logging.getLogger('gajim.plugin_system.omemo')
# from plugins.helpers import log
class PreKeyButton(gtk.Button):
def __init__(self, plugin, contact):
super(PreKeyButton, self).__init__(label='Get Device Keys' + str(
plugin.are_keys_missing(contact)))
self.plugin = plugin
self.contact = contact
self.connect('clicked', self.on_click)
self.refresh()
def refresh(self):
amount = self.plugin.are_keys_missing(self.contact)
if amount == 0:
self.set_no_show_all(True)
self.hide()
else:
self.set_no_show_all(False)
self.show()
self.set_label('Get Device Keys ' + str(amount))
def on_click(self, widget):
self.plugin.query_prekey(self.contact)
class ClearDevicesButton(gtk.Button):
def __init__(self, plugin, contact):
super(ClearDevicesButton, self).__init__(label='Clear Devices')
self.plugin = plugin
self.contact = contact
self.connect('clicked', self.on_click)
def on_click(self, widget):
self.plugin.clear_device_list(self.contact)
class Checkbox(gtk.CheckButton):
def __init__(self, plugin, chat_control):
super(Checkbox, self).__init__(label='OMEMO')
self.chat_control = chat_control
self.contact = chat_control.contact
self.plugin = plugin
self.connect('clicked', self.on_click)
def on_click(self, widget):
enabled = self.get_active()
log.info('Clicked ' + str(enabled))
if enabled:
self.plugin.omemo_enable_for(self.contact)
self.chat_control._show_lock_image(True, 'OMEMO', True, True, True)
else:
self.plugin.omemo_disable_for(self.contact)
self.chat_control._show_lock_image(False, 'OMEMO', False, True,
False)
def _add_widget(widget, chat_control):
actions_hbox = chat_control.xml.get_object('actions_hbox')
send_button = chat_control.xml.get_object('send_button')
send_button_pos = actions_hbox.child_get_property(send_button, 'position')
actions_hbox.add_with_properties(widget, 'position', send_button_pos - 2,
'expand', False)
class Ui(object):
last_msg_plain = True
def __init__(self, plugin, chat_control):
contact = chat_control.contact
self.prekey_button = PreKeyButton(plugin, contact)
self.checkbox = Checkbox(plugin, chat_control)
self.clear_button = ClearDevicesButton(plugin, contact)
available = plugin.has_omemo(contact)
self.toggle_omemo(available)
self.checkbox.set_active(plugin.is_omemo_enabled(contact))
self.chat_control = chat_control
if chat_control.TYPE_ID == TYPE_CHAT:
_add_widget(self.prekey_button, chat_control)
_add_widget(self.checkbox, chat_control)
_add_widget(self.clear_button, chat_control)
def toggle_omemo(self, available):
if available:
self.checkbox.set_no_show_all(False)
self.checkbox.show()
else:
self.checkbox.set_no_show_all(True)
self.checkbox.hide()
def encryption_active(self):
return self.checkbox.get_active()
def encryption_disable(self):
return self.checkbox.set_active(False)
def activate_omemo(self):
if not self.checkbox.get_active():
self.chat_control.print_conversation_line(
'OMEMO encryption activated', 'status', '', None)
self.chat_control._show_lock_image(True, 'OMEMO', True, True, True)
self.checkbox.set_active(True)
elif self.last_msg_plain:
self.chat_control.print_conversation_line(
'OMEMO encryption activated', 'status', '', None)
self.last_msg_plain = False
def plain_warning(self):
if not self.last_msg_plain:
self.chat_control.print_conversation_line(
'Received plaintext message!', 'status', '', None)
self.last_msg_plain = True
def update_prekeys(self):
self.prekey_button.refresh()
# -*- 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 geting 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 common import gajim
from common.pep import AbstractPEP
from plugins.helpers import log_calls
NS_OMEMO = 'eu.siacs.conversations.axolotl'
NS_DEVICE_LIST = NS_OMEMO + '.devicelist'
NS_NOTIFY = NS_DEVICE_LIST + '+notify'
NS_BUNDLES = NS_OMEMO + '.bundles:'
log = logging.getLogger('gajim.plugin_system.omemo')
class PublishNode(Node):
def __init__(self, node_str, data):
assert node_str is not None and data is Node
Node.__init__(self, tag='publish', attrs={'node': node_str})
self.addChild('item').addChild(node=data)
class PubsubNode(Node):
def __init__(self, data):
assert data is Node
Node.__init__(self, tag='pubsub', attrs={'xmlns': NS_PUBSUB})
self.addChild(node=data)
class DeviceListAnnouncement(Iq):
def __init__(self, device_list):
id_ = gajim.get_an_id()
attrs = {'id': id_}
Iq.__init__(self, typ='set', attrs=attrs)
list_node = Node('list')
for device in device_list:
list_node.addChild('device').setAttr('id', device)
publish = PublishNode(NS_DEVICE_LIST, list_node)
pubsub = PubsubNode(publish)
self.addChild(node=pubsub)
class OmemoMessage(Node):
def __init__(self, msg_dict):
# , contact_jid, key, iv, payload, dev_id, my_dev_id):
log.debug('Creating_msg' + str(msg_dict))
Node.__init__(self, 'encrypted', attrs={'xmlns': NS_OMEMO})
header = Node('header', attrs={'sid': msg_dict['sid']})
for rid, key in msg_dict['keys'].items():
header.addChild('key', attrs={'rid': rid}).addData(b64encode(key))
header.addChild('iv').addData(b64encode(msg_dict['iv']))
self.addChild(node=header)
self.addChild('payload').addData(b64encode(msg_dict['payload']))
class BundleInformationQuery(Iq):
def __init__(self, contact_jid, device_id):
id_ = gajim.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)})
pubsub = PubsubNode(items)
self.addChild(node=pubsub)
class BundleInformationAnnouncement(Iq):
def __init__(self, state_bundle, device_id):
id_ = gajim.get_an_id()
attrs = {'id': id_}
Iq.__init__(self, typ='set', attrs=attrs)
bundle_node = self.make_bundle_node(state_bundle)
publish = PublishNode(NS_BUNDLES + str(device_id), bundle_node)
pubsub = PubsubNode(publish)
self.addChild(node=pubsub)
def make_bundle_node(self, 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'])
prekey_sig_node = result.addChild('signedPreKeySignature')
prekey_sig_node.addData(state_bundle['signedPreKeySignature'])
identity_key_node = result.addChild('identityKey')
identity_key_node.addData(state_bundle['identityKey'])
prekeys = result.addChild('prekeys')
for key in state_bundle['prekeys']:
prekeys.addChild('preKeyPublic',
attrs={'preKeyId': key[0]}).addData(key[1])
return result
class DevicelistPEP(AbstractPEP):
type_ = 'headline'
namespace = NS_DEVICE_LIST
def _extract_info(self, items):
return ({}, [])
@log_calls('OmemoPlugin')
def unpack_message(msg):
encrypted_node = msg.getTag('encrypted', namespace=NS_OMEMO)
if not encrypted_node:
log.debug('Message does not have encrypted node')
return unpack_encrypted(encrypted_node)
@log_calls('OmemoPlugin')
def unpack_device_bundle(bundle, device_id):
pubsub = bundle.getTag('pubsub', namespace=NS_PUBSUB)
if not pubsub:
log.warn('OMEMO device bundle has no pubsub node')
return
items = pubsub.getTag('items', attrs={'node': NS_BUNDLES + str(device_id)})
if not items:
log.warn('OMEMO device bundle has no items node')
return
item = items.getTag('item')
if not item:
log.warn('OMEMO device bundle has no item node')
return
bundle = item.getTag('bundle', namespace=NS_OMEMO)
if not bundle:
log.warn('OMEMO device bundle has no bundle node')
return
signed_prekey_node = bundle.getTag('signedPreKeyPublic')
if not signed_prekey_node:
log.warn('OMEMO device bundle has no signedPreKeyPublic node')
return
result = {}
result['signedPreKeyPublic'] = decode_data(signed_prekey_node)
if not result['signedPreKeyPublic']:
log.warn('OMEMO device bundle has no signedPreKeyPublic data')
return
if not signed_prekey_node.getAttr('signedPreKeyId'):
log.warn('OMEMO device bundle has no signedPreKeyId')
return
result['signedPreKeyId'] = int(signed_prekey_node.getAttr(
'signedPreKeyId'))
signed_signature_node = bundle.getTag('signedPreKeySignature')
if not signed_signature_node:
log.warn('OMEMO device bundle has no signedPreKeySignature node')
return
result['signedPreKeySignature'] = decode_data(signed_signature_node)
if not result['signedPreKeySignature']:
log.warn('OMEMO device bundle has no signedPreKeySignature data')
return
identity_key_node = bundle.getTag('identityKey')
if not identity_key_node:
log.warn('OMEMO device bundle has no identityKey node')
return
result['identityKey'] = decode_data(identity_key_node)
if not result['identityKey']:
log.warn('OMEMO device bundle has no identityKey data')
return
prekeys = bundle.getTag('prekeys')
if not prekeys or len(prekeys.getChildren()) == 0:
log.warn('OMEMO device bundle has no prekys')
return
picked_key_node = random.SystemRandom().choice(prekeys.getChildren())
if not picked_key_node.getAttr('preKeyId'):
log.warn('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.warn("Encrypted node with wrong NS")
return
header_node = encrypted_node.getTag('header')
if not header_node:
log.warn("OMEMO message without header")
return
if not header_node.getAttr('sid'):
log.warn("OMEMO message without sid in header")
return
sid = int(header_node.getAttr('sid'))
iv_node = header_node.getTag('iv')
if not iv_node:
log.warn("OMEMO message without iv")
return
iv = decode_data(iv_node)
if not iv:
log.warn("OMEMO message without iv data")
payload_node = encrypted_node.getTag('payload')
payload = None
if payload_node:
payload = decode_data(payload_node)
key_nodes = header_node.getTags('key')
if len(key_nodes) < 1:
log.warn("OMEMO message without keys")
return
keys = {}
for kn in key_nodes:
rid = kn.getAttr('rid')
if not rid:
log.warn('Omemo key without rid')
continue
if not kn.getData():
log.warn('Omemo key without data')
continue
keys[int(rid)] = decode_data(kn)
result = {'sid': sid, 'iv': iv, 'keys': keys, 'payload': payload}
log.debug('Parsed OMEMO message:' + str(result))
return result
def unpack_device_list_update(event):
""" Unpacks the device list update received in a MessageReceivedEvent.
Parameters
----------
event : MessageReceivedEvent
The event received from gajim
Returns
-------
[int]
List of device ids or empty list if nothing found
"""
event_node = event.stanza.getTag('event')
account = event.conn.name
result = []
if not event_node:
log.warn(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.debug(
account +
' → Device list update items node empty or not omemo device update'
)
return result
list_node = items.getChildren()[0].getTag('list')
if not items or len(items.getChildren()) == 0:
log.warn(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()
log.debug(data)
if not data:
log.warn("No node data")
return
try:
return b64decode(data)
except:
log.warn('b64decode broken')
return
def successful(stanza):
""" Check if stanza type is result. """
return stanza.getAttr('type') == 'result'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment