Commit 21db440c authored by Philipp Hörist's avatar Philipp Hörist

Merge branch 'gtk3' into 'gtk3'

Port recent changes to OMEMO

See merge request !16
parents d38fe92d 6ecf3c91
This diff is collapsed.
[style]
based_on_style = pep8
align_closing_bracket_with_visual_indent = true
join_multiple_lines = true
1.0.1 / 2017-01-14
- Better XEP Compliance
- Bugfixes
1.0.0 / 2016-12-04
- Bugfixes
0.9.9 / 2016-12-01
- Bugfixes
0.9.8 / 2016-11-28
- Fix a Problem where OMEMO wouldnt activate after the plugin is updated
- Add QR Verification Code to Plugin Config
0.9.7 / 2016-11-12
- Bugfixes
0.9.6 / 2016-11-01
- Bugfixes
0.9.5 / 2016-10-10
- Add GroupChat BETA
- Add Option to delete Fingerprints
- Add Option to deactivate Accounts for OMEMO
0.9.0 / 2016-08-28
- Send INFO message to resources who dont support OMEMO
- Check dependencys and give correct error message
- Dont process PreKeyWhisperMessages without PreKey
- Dont process PGP messages
- Send INFO message to resources who dont support OMEMO
- Check dependencys and give correct error message
- Dont process PreKeyWhisperMessages without PreKey
- Dont process PGP messages
0.8.1 / 2016-08-05
- Query own Device Bundles on send button press
......
# OMEMO Plugin for Gajim
This Plugin 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.
## Installation
Before you open any issues please read our [Wiki](https://github.com/omemo/gajim-omemo/wiki) which addresses some problems that can occur during an install
### Linux
See [Linux Wiki](https://github.com/omemo/gajim-omemo/wiki/Installing-on-Linux)
### Windows
See [Windows Wiki](https://github.com/omemo/gajim-omemo/wiki/Installing-on-Windows)
### Via Package Manager
#### Arch
See [Arch Wiki](https://wiki.archlinux.org/index.php/Gajim#OMEMO_Support)
#### Gentoo
`layman -a flow && emerge gajim-omemo`
### Via PluginInstallerPlugin
Install the current stable version via the Gajim PluginManager. You *need* Gajim
version *0.16.5*. If your package manager does not provide an up to date version
you can install it from the official Mercurial repository. *DO NOT USE* gajim
0.16.4 it contains a vulnerability, which is fixed in 0.16.5.
```shell
hg clone https://hg.gajim.org/gajim
cd gajim
hg update gajim-0.16.5 --clean
```
**NOTE:** You *have* to install `python-axolotl` via `pip`. Depending on your setup you might
want to use `pip2` as Gajim is using python2.7. If you are using the official repository,
do not forget to install the `nbxmpp` dependency via pip or you package manager.
if you still have problems, we have written down the most common problems [here](https://github.com/omemo/gajim-omemo/wiki/It-doesnt-work,-what-should-i-do%3F-(Linux))
## Running
Enable *OMEMO Multi-End Message and Object Encryption* in the Plugin-Manager.
If your contact supports OMEMO you should see a new orange fish icon in the chat window.
Encryption will be enabled by default for contacts that support OMEMO.
If you open the chat window, the Plugin will tell you with a green status message if its *enabled* or *disabled*.
If you see no status message, your contact doesnt support OMEMO.
(**Beware**, every status message is green. A green message does not mean encryption is active. Read the message !)
You can also check if encryption is enabled/disabled, when you click on the OMEMO icon.
When you send your first message the Plugin will query your contacts encryption keys and you will
see them in a readable fingerprint format in the fingerprint window which pops up.
you have to trust at least **one** fingerprint to send messages.
you can receive messages from fingerprints where you didnt made a trust decision, but you cant
receive Messages from *not trusted* fingerprints
## Debugging
To see OMEMO related debug output start Gajim with the parameter `-l
gajim.plugin_system.omemo=DEBUG`.
## Hacking
This repository contains the current development version. If you want to
contribute clone the git repository into your Gajim's plugin directory.
```shell
mkdir ~/.local/share/gajim/plugins -p
cd ~/.local/share/gajim/plugins
git clone https://github.com/omemo/gajim-omemo
```
## 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: `1CnNM3Mree9hU8eRjCXrfCWVmX6oBnEfV1`
[![Support Me via Flattr](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/thing/5038679)
## I found a bug
Please report it to the [issue
tracker](https://github.com/omemo/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)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
[info]
name: OMEMO
short_name: omemo
version: 0.9.0
version: 1.0.1
description: OMEMO is an XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption based on Axolotl and PEP. You need to install some dependencys, you can find install instructions for your system in the Github Wiki.
authors: Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
Daniel Gultsch <daniel@gultsch.de>
Philipp Hörist <philipp@hoerist.com>
homepage: http://github.com/omemo/gajim-omemo.git
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/OmemoGajimPlugin
min_gajim_version: 0.16.9
max_gajim_version: 0.16.11
......@@ -18,6 +18,7 @@
#
import sys
import logging
log = logging.getLogger('gajim.plugin_system.omemo')
try:
......@@ -35,7 +36,11 @@ def encrypt(key, iv, plaintext):
def decrypt(key, iv, ciphertext):
return aes_decrypt(key, iv, ciphertext)
plaintext = aes_decrypt(key, iv, ciphertext).decode('utf-8')
if sys.version_info < (3, 0):
return unicode(plaintext)
else:
return plaintext
class NoValidSessions(Exception):
......
......@@ -29,11 +29,14 @@
# 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):
......@@ -140,13 +143,20 @@ def gcm_encrypt(k, iv, plaintext, auth_data):
def aes_encrypt(key, nonce, plaintext):
""" Use AES128 GCM with the given key and iv to encrypt the payload. """
c, t = gcm_encrypt(key, nonce, plaintext, '')
result = c + t
return result
return gcm_encrypt(key, nonce, plaintext, '')
def aes_decrypt(key, nonce, payload):
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:]
if len(_key) >= 32:
# XEP-0384
log.debug('XEP Compliant Key/Tag')
ciphertext = payload
key = _key[:16]
mac = _key[16:]
else:
# Legacy
log.debug('Legacy Key/Tag')
ciphertext = payload[:-16]
key = _key
mac = payload[-16:]
return gcm_decrypt(key, nonce, ciphertext, '', mac)
......@@ -19,6 +19,7 @@
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
......@@ -32,11 +33,22 @@ if os.name == 'nt':
else:
from cryptography.hazmat.backends import default_backend
log = logging.getLogger('gajim.plugin_system.omemo')
def aes_decrypt(key, iv, payload):
def aes_decrypt(_key, iv, payload):
""" Use AES128 GCM with the given key and iv to decrypt the payload. """
data = payload[:-16]
tag = payload[-16:]
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:
......@@ -58,4 +70,4 @@ def aes_encrypt(key, iv, plaintext):
algorithms.AES(key),
GCM(iv),
backend=_backend).encryptor()
return encryptor.update(plaintext) + encryptor.finalize() + encryptor.tag
return encryptor.update(plaintext) + encryptor.finalize(), encryptor.tag
......@@ -83,10 +83,16 @@ class LiteAxolotlStore(AxolotlStore):
def saveIdentity(self, recepientId, identityKey):
self.identityKeyStore.saveIdentity(recepientId, identityKey)
def deleteIdentity(self, recipientId, identityKey):
self.identityKeyStore.deleteIdentity(recipientId, identityKey)
def isTrustedIdentity(self, recepientId, identityKey):
return self.identityKeyStore.isTrustedIdentity(recepientId,
identityKey)
def setTrust(self, identityKey, trust):
return self.identityKeyStore.setTrust(identityKey, trust)
def getTrustedFingerprints(self, jid):
return self.identityKeyStore.getTrustedFingerprints(jid)
......@@ -127,6 +133,9 @@ class LiteAxolotlStore(AxolotlStore):
# TODO Reuse this
return self.sessionStore.getSubDeviceSessions(recepientId)
def getJidFromDevice(self, device_id):
return self.sessionStore.getJidFromDevice(device_id)
def storeSession(self, recepientId, deviceId, sessionRecord):
self.sessionStore.storeSession(recepientId, deviceId, sessionRecord)
......@@ -139,6 +148,15 @@ class LiteAxolotlStore(AxolotlStore):
def deleteAllSessions(self, recepientId):
self.sessionStore.deleteAllSessions(recepientId)
def getSessionsFromJid(self, recipientId):
return self.sessionStore.getSessionsFromJid(recipientId)
def getSessionsFromJids(self, recipientId):
return self.sessionStore.getSessionsFromJids(recipientId)
def getAllSessions(self):
return self.sessionStore.getAllSessions()
def loadSignedPreKey(self, signedPreKeyId):
return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId)
......
......@@ -86,6 +86,13 @@ class LiteIdentityKeyStore(IdentityKeyStore):
return result is not None
def deleteIdentity(self, recipientId, identityKey):
q = "DELETE FROM identities WHERE recipient_id = ? AND public_key = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId,
identityKey.getPublicKey().serialize()))
self.dbConn.commit()
def isTrustedIdentity(self, recipientId, identityKey):
q = "SELECT trust FROM identities WHERE recipient_id = ? " \
"AND public_key = ?"
......@@ -160,8 +167,8 @@ class LiteIdentityKeyStore(IdentityKeyStore):
c.execute(q, fingerprints)
self.dbConn.commit()
def setTrust(self, _id, trust):
q = "UPDATE identities SET trust = ? WHERE _id = ?"
def setTrust(self, identityKey, trust):
q = "UPDATE identities SET trust = ? WHERE public_key = ?"
c = self.dbConn.cursor()
c.execute(q, (trust, _id))
c.execute(q, (trust, identityKey.getPublicKey().serialize()))
self.dbConn.commit()
......@@ -48,6 +48,14 @@ class LiteSessionStore(SessionStore):
deviceIds = [r[0] for r in result]
return deviceIds
def getJidFromDevice(self, device_id):
q = "SELECT recipient_id from sessions WHERE device_id = ?"
c = self.dbConn.cursor()
c.execute(q, (device_id, ))
result = c.fetchone()
return result[0].decode('utf-8')
def getActiveDeviceTuples(self):
q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1"
c = self.dbConn.cursor()
......@@ -82,6 +90,33 @@ class LiteSessionStore(SessionStore):
self.dbConn.cursor().execute(q, (recipientId, ))
self.dbConn.commit()
def getAllSessions(self):
q = "SELECT _id, recipient_id, device_id, record, active from sessions"
c = self.dbConn.cursor()
result = []
for row in c.execute(q):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result
def getSessionsFromJid(self, recipientId):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" \
" WHERE recipient_id = ?"
c = self.dbConn.cursor()
result = []
for row in c.execute(q, (recipientId,)):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result
def getSessionsFromJids(self, recipientId):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" \
" WHERE recipient_id IN ({})" \
.format(', '.join(['?'] * len(recipientId)))
c = self.dbConn.cursor()
result = []
for row in c.execute(q, recipientId):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result
def setActiveState(self, deviceList, jid):
c = self.dbConn.cursor()
......@@ -96,28 +131,6 @@ class LiteSessionStore(SessionStore):
c.execute(q, deviceList)
self.dbConn.commit()
def getActiveSessionsKeys(self, recipientId):
q = "SELECT record FROM sessions WHERE active = 1 AND recipient_id = ?"
c = self.dbConn.cursor()
result = []
for row in c.execute(q, (recipientId,)):
public_key = (SessionRecord(serialized=row[0]).
getSessionState().getRemoteIdentityKey().
getPublicKey())
result.append(public_key.serialize())
return result
def getAllActiveSessionsKeys(self):
q = "SELECT record FROM sessions WHERE active = 1"
c = self.dbConn.cursor()
result = []
for row in c.execute(q):
public_key = (SessionRecord(serialized=row[0]).
getSessionState().getRemoteIdentityKey().
getPublicKey())
result.append(public_key.serialize())
return result
def getInactiveSessionsKeys(self, recipientId):
q = "SELECT record FROM sessions WHERE active = 0 AND recipient_id = ?"
c = self.dbConn.cursor()
......
......@@ -29,6 +29,14 @@ class SQLDatabase():
self.dbConn = dbConn
self.createDb()
self.migrateDb()
c = self.dbConn.cursor()
c.execute("PRAGMA synchronous=NORMAL;")
c.execute("PRAGMA journal_mode;")
mode = c.fetchone()[0]
# WAL is a persistent DB mode, dont override it if user has set it
if mode != 'wal':
c.execute("PRAGMA journal_mode=MEMORY;")
self.dbConn.commit()
def createDb(self):
if user_version(self.dbConn) == 0:
......
......@@ -200,8 +200,8 @@ class OmemoState:
key = self.handleWhisperMessage(sender_jid, sid, encrypted_key)
except (NoSessionException, InvalidMessageException) as e:
log.warning('No Session found ' + e.message)
log.warning('sender_jid => ' + str(sender_jid) +
' sid =>' + sid)
log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' +
str(sid))
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
......@@ -211,7 +211,7 @@ class OmemoState:
log.warning('Duplicate message found ' + str(e.args))
return
result = decrypt(key, iv, payload).decode('utf-8')
result = decrypt(key, iv, payload)
log.debug("Decrypted Message => " + result)
return result
......@@ -226,24 +226,97 @@ class OmemoState:
log.error('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.warning('No session ciphers for ' + jid)
return
payload, tag = encrypt(key, iv, plaintext)
# for XEP-384 Compliance uncomment
# key += tag
payload += tag
# Encrypt the message key with for each of receivers devices
for rid, cipher in session_ciphers.items():
for device in devices_list:
try:
if self.isTrusted(cipher) == TRUSTED:
encrypted_keys[rid] = cipher.encrypt(key).serialize()
if self.isTrusted(jid, device) == TRUSTED:
cipher = self.get_session_cipher(jid, device)
cipher_key = cipher.encrypt(key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped Device because Trust is: ' +
str(self.isTrusted(cipher)))
str(self.isTrusted(jid, device)))
except:
log.warning('Failed to find key for device ' + str(rid))
log.warning('Failed to find key for device ' + str(device))
if len(encrypted_keys) == 0:
log.error('Encrypted keys empty')
raise NoValidSessions('Encrypted keys empty')
my_other_devices = set(self.own_devices) - set({self.own_device_id})
# Encrypt the message key with for each of our own devices
for device in my_other_devices:
try:
if self.isTrusted(from_jid, device) == TRUSTED:
cipher = self.get_session_cipher(from_jid, device)
cipher_key = cipher.encrypt(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('Finished encrypting message')
return result
def create_gc_msg(self, from_jid, jid, plaintext):
key = get_random_bytes(16)
iv = get_random_bytes(16)
encrypted_keys = {}
room = jid
encrypted_jids = []
devices_list = self.device_list_for(jid, True)
if len(devices_list) == 0:
log.error('No known devices')
return
payload, tag = encrypt(key, iv, plaintext)
# for XEP-384 Compliance uncomment
# key += tag
payload += tag
for tup in devices_list:
self.get_session_cipher(tup[0], tup[1])
# Encrypt the message key with for each of receivers devices
for nick in self.plugin.groupchat[room]:
jid_to = self.plugin.groupchat[room][nick]
if jid_to == self.own_jid:
continue
if jid_to in encrypted_jids: # We already encrypted to this JID
continue
for rid, cipher in self.session_ciphers[jid_to].items():
try:
if self.isTrusted(jid_to, rid) == TRUSTED:
cipher_key = cipher.encrypt(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.exception('ERROR:')
log.warning('Failed to find key for device ' +
str(rid))
encrypted_jids.append(jid_to)
if len(encrypted_keys) == 0:
log_msg = 'Encrypted keys empty'
log.error(log_msg)
......@@ -254,16 +327,17 @@ class OmemoState:
for dev in my_other_devices:
try:
cipher = self.get_session_cipher(from_jid, dev)
if self.isTrusted(cipher) == TRUSTED:
encrypted_keys[dev] = cipher.encrypt(key).serialize()
if self.isTrusted(from_jid, dev) == TRUSTED:
cipher_key = cipher.encrypt(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(cipher)))
str(self.isTrusted(from_jid, dev)))
except:
log.exception('ERROR:')
log.warning('Failed to find key for device ' + str(dev))
payload = encrypt(key, iv, plaintext)
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
'jid': jid,
......@@ -273,14 +347,36 @@ class OmemoState:
log.debug('Finished encrypting message')
return result
def isTrusted(self, cipher):
self.cipher = cipher
self.state = self.cipher.sessionStore. \
loadSession(self.cipher.recipientId, self.cipher.deviceId). \
getSessionState()
self.key = self.state.getRemoteIdentityKey()
return self.store.identityKeyStore. \
isTrustedIdentity(self.cipher.recipientId, self.key)
def device_list_for(self, jid, gc=False):
""" Return a list of known device ids for the specified jid.
Parameters
----------
jid : string
The contacts jid
gc : bool
Groupchat Message
"""
if gc:
room = jid
devicelist = []
for nick in self.plugin.groupchat[room]:
jid_to = self.plugin.groupchat[room][nick]
if jid_to == self.own_jid:
continue
for device in self.device_ids[jid_to]:
devicelist.append((jid_to, device))
return devicelist
if jid == self.own_jid:
return set(self.own_devices) - set({self.own_device_id})
if jid not in self.device_ids:
return set()
return set(self.device_ids[jid])
def isTrusted(self, recipient_id, device_id):
record = self.store.loadSession(recipient_id, device_id)
identity_key = record.getSessionState().getRemoteIdentityKey()
return self.store.isTrustedIdentity(recipient_id, identity_key)
def getTrustedFingerprints(self, recipient_id):
inactive = self.store.getInactiveSessionsKeys(recipient_id)
......@@ -296,20 +392,6 @@ class OmemoState:
return undecided
def device_list_for(self, jid):
""" Return a list of known device ids for the specified jid.
Parameters
----------
jid : string
The contacts jid
"""
if jid == self.own_jid:
return set(self.own_devices) - set({self.own_device_id})
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.
......@@ -364,10 +446,10 @@ class OmemoState:
def handleWhisperMessage(self, recipient_id, device_id, key):