OMEMO GTK3 inital

This commit is contained in:
Philipp Hörist
2016-08-28 23:26:56 +02:00
parent 9457a18016
commit dabfbbf826
28 changed files with 5386 additions and 0 deletions

1
omemo/omemo/__init__.py Normal file
View File

@@ -0,0 +1 @@
__version__ = "0.1.0"

42
omemo/omemo/aes_gcm.py Normal file
View File

@@ -0,0 +1,42 @@
# -*- 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 logging
log = logging.getLogger('gajim.plugin_system.omemo')
try:
from .aes_gcm_native import aes_decrypt
from .aes_gcm_native import aes_encrypt
log.debug('Using fast cryptography')
except ImportError:
from .aes_gcm_fallback import aes_decrypt
from .aes_gcm_fallback import aes_encrypt
log.debug('Using slow cryptography')
def encrypt(key, iv, plaintext):
return aes_encrypt(key, iv, plaintext)
def decrypt(key, iv, ciphertext):
return aes_decrypt(key, iv, ciphertext)
class NoValidSessions(Exception):
pass

View File

@@ -0,0 +1,152 @@
# -*- 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.
from struct import pack, unpack
from Crypto.Cipher import AES
from Crypto.Util import strxor
def gcm_rightshift(vec):
for x in range(15, 0, -1):
c = vec[x] >> 1
c |= (vec[x - 1] << 7) & 0x80
vec[x] = c
vec[0] >>= 1
return vec
def gcm_gf_mult(a, b):
mask = [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01]
poly = [0x00, 0xe1]
Z = [0] * 16
V = [c for c in a]
for x in range(128):
if b[x >> 3] & mask[x & 7]:
Z = [V[y] ^ Z[y] for y in range(16)]
bit = V[15] & 1
V = gcm_rightshift(V)
V[0] ^= poly[bit]
return Z
def ghash(h, auth_data, data):
u = (16 - len(data)) % 16
v = (16 - len(auth_data)) % 16
x = auth_data + chr(0) * v + data + chr(0) * u
x += pack('>QQ', len(auth_data) * 8, len(data) * 8)
y = [0] * 16
vec_h = [ord(c) for c in h]
for i in range(0, len(x), 16):
block = [ord(c) for c in x[i:i + 16]]
y = [y[j] ^ block[j] for j in range(16)]
y = gcm_gf_mult(y, vec_h)
return ''.join(chr(c) for c in y)
def inc32(block):
counter, = unpack('>L', block[12:])
counter += 1
return block[:12] + pack('>L', counter)
def gctr(k, icb, plaintext):
y = ''
if len(plaintext) == 0:
return y
aes = AES.new(k)
cb = icb
for i in range(0, len(plaintext), aes.block_size):
cb = inc32(cb)
encrypted = aes.encrypt(cb)
plaintext_block = plaintext[i:i + aes.block_size]
y += strxor.strxor(plaintext_block, encrypted[:len(plaintext_block)])
return y
def gcm_decrypt(k, iv, encrypted, auth_data, tag):
aes = AES.new(k)
h = aes.encrypt(chr(0) * aes.block_size)
if len(iv) == 12:
y0 = iv + "\x00\x00\x00\x01"
else:
y0 = ghash(h, '', iv)
decrypted = gctr(k, y0, encrypted)
s = ghash(h, auth_data, encrypted)
t = aes.encrypt(y0)
T = strxor.strxor(s, t)
if T != tag:
raise ValueError('Decrypted data is invalid')
else:
return decrypted
def gcm_encrypt(k, iv, plaintext, auth_data):
aes = AES.new(k)
h = aes.encrypt(chr(0) * aes.block_size)
if len(iv) == 12:
y0 = iv + "\x00\x00\x00\x01"
else:
y0 = ghash(h, '', iv)
encrypted = gctr(k, y0, plaintext)
s = ghash(h, auth_data, encrypted)
t = aes.encrypt(y0)
T = strxor.strxor(s, t)
return (encrypted, T)
def aes_encrypt(key, nonce, plaintext):
""" Use AES128 GCM with the given key and iv to encrypt the payload. """
c, t = gcm_encrypt(key, nonce, plaintext, '')
result = c + t
return result
def aes_decrypt(key, nonce, payload):
""" Use AES128 GCM with the given key and iv to decrypt the payload. """
ciphertext = payload[:-16]
mac = payload[-16:]
return gcm_decrypt(key, nonce, ciphertext, '', mac)

View File

@@ -0,0 +1,61 @@
# -*- 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
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 doesnt 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
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 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

15
omemo/omemo/db_helpers.py Normal file
View File

@@ -0,0 +1,15 @@
''' Database helper functions '''
def table_exists(db, name):
""" Check if the specified table exists in the db. """
query = """ SELECT name FROM sqlite_master
WHERE type='table' AND name=?;
"""
return db.execute(query, (name, )).fetchone() is not None
def user_version(db):
""" Return the value of PRAGMA user_version. """
return db.execute('PRAGMA user_version').fetchone()[0]

64
omemo/omemo/encryption.py Normal file
View File

@@ -0,0 +1,64 @@
# -*- 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/>.
#
class EncryptionState():
""" Used to store if OMEMO is enabled or not between gajim restarts """
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
def activate(self, jid):
q = """INSERT OR REPLACE INTO encryption_state (jid, encryption)
VALUES (?, 1) """
c = self.dbConn.cursor()
c.execute(q, (jid, ))
self.dbConn.commit()
def deactivate(self, jid):
q = """INSERT OR REPLACE INTO encryption_state (jid, encryption)
VALUES (?, 0)"""
c = self.dbConn.cursor()
c.execute(q, (jid, ))
self.dbConn.commit()
def is_active(self, jid):
q = 'SELECT encryption FROM encryption_state where jid = ?;'
c = self.dbConn.cursor()
c.execute(q, (jid, ))
result = c.fetchone()
if result is None:
return False
return result[0]
def exist(self, jid):
q = 'SELECT encryption FROM encryption_state where jid = ?;'
c = self.dbConn.cursor()
c.execute(q, (jid, ))
result = c.fetchone()
if result is None:
return False
else:
return True

View File

@@ -0,0 +1,168 @@
# -*- 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
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
from .encryption import EncryptionState
from .sql import SQLDatabase
log = logging.getLogger('gajim.plugin_system.omemo')
DEFAULT_PREKEY_AMOUNT = 100
MIN_PREKEY_AMOUNT = 80
SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days
SPK_CYCLE_TIME = 86400 # 24 Hours
class LiteAxolotlStore(AxolotlStore):
def __init__(self, connection):
try:
connection.text_factory = bytes
except(AttributeError):
raise AssertionError('Expected a sqlite3.Connection got ' +
str(connection))
self.sql = SQLDatabase(connection)
self.identityKeyStore = LiteIdentityKeyStore(connection)
self.preKeyStore = LitePreKeyStore(connection)
self.signedPreKeyStore = LiteSignedPreKeyStore(connection)
self.sessionStore = LiteSessionStore(connection)
self.encryptionStore = EncryptionState(connection)
if not self.getLocalRegistrationId():
log.info("Generating Axolotl keys")
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)
signedPreKey = KeyHelper.generateSignedPreKey(
identityKeyPair, KeyHelper.getRandomSequence(65536))
self.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
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 getTrustedFingerprints(self, jid):
return self.identityKeyStore.getTrustedFingerprints(jid)
def getUndecidedFingerprints(self, jid):
return self.identityKeyStore.getUndecidedFingerprints(jid)
def setShownFingerprints(self, jid):
return self.identityKeyStore.setShownFingerprints(jid)
def getNewFingerprints(self, jid):
return self.identityKeyStore.getNewFingerprints(jid)
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 getActiveDeviceTuples(self):
return self.sessionStore.getActiveDeviceTuples()
def getInactiveSessionsKeys(self, recipientId):
return self.sessionStore.getInactiveSessionsKeys(recipientId)
def getSubDeviceSessions(self, recepientId):
# TODO Reuse this
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)
def getNextSignedPreKeyId(self):
return self.signedPreKeyStore.getNextSignedPreKeyId()
def getCurrentSignedPreKeyId(self):
return self.signedPreKeyStore.getCurrentSignedPreKeyId()
def getSignedPreKeyTimestamp(self, signedPreKeyId):
return self.signedPreKeyStore.getSignedPreKeyTimestamp(signedPreKeyId)
def removeOldSignedPreKeys(self, timestamp):
self.signedPreKeyStore.removeOldSignedPreKeys(timestamp)

View File

@@ -0,0 +1,167 @@
# -*- 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
UNDECIDED = 2
TRUSTED = 1
UNTRUSTED = 0
class LiteIdentityKeyStore(IdentityKeyStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
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, trust) " \
"VALUES(?, ?, ?)"
c = self.dbConn.cursor()
if not self.getIdentity(recipientId, identityKey):
c.execute(q, (recipientId,
identityKey.getPublicKey().serialize(),
UNDECIDED))
self.dbConn.commit()
def getIdentity(self, recipientId, identityKey):
q = "SELECT * FROM identities WHERE recipient_id = ? " \
"AND public_key = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
result = c.fetchone()
return result is not None
def isTrustedIdentity(self, recipientId, identityKey):
q = "SELECT trust FROM identities WHERE recipient_id = ? " \
"AND public_key = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
result = c.fetchone()
states = [UNTRUSTED, TRUSTED, UNDECIDED]
if result and result[0] in states:
return result[0]
else:
return True
def getAllFingerprints(self):
q = "SELECT _id, recipient_id, public_key, trust FROM identities " \
"WHERE recipient_id != -1 ORDER BY recipient_id ASC"
c = self.dbConn.cursor()
result = []
for row in c.execute(q):
result.append((row[0], row[1], row[2], row[3]))
return result
def getFingerprints(self, jid):
q = "SELECT _id, recipient_id, public_key, trust FROM identities " \
"WHERE recipient_id =? ORDER BY trust ASC"
c = self.dbConn.cursor()
result = []
c.execute(q, (jid,))
rows = c.fetchall()
for row in rows:
result.append((row[0], row[1], row[2], row[3]))
return result
def getTrustedFingerprints(self, jid):
q = "SELECT public_key FROM identities WHERE recipient_id = ? AND trust = ?"
c = self.dbConn.cursor()
result = []
c.execute(q, (jid, TRUSTED))
rows = c.fetchall()
for row in rows:
result.append(row[0])
return result
def getUndecidedFingerprints(self, jid):
q = "SELECT trust FROM identities WHERE recipient_id = ? AND trust = ?"
c = self.dbConn.cursor()
result = []
c.execute(q, (jid, UNDECIDED))
result = c.fetchall()
return result
def getNewFingerprints(self, jid):
q = "SELECT _id FROM identities WHERE shown = 0 AND " \
"recipient_id = ?"
c = self.dbConn.cursor()
result = []
for row in c.execute(q, (jid,)):
result.append(row[0])
return result
def setShownFingerprints(self, fingerprints):
q = "UPDATE identities SET shown = 1 WHERE _id IN ({})" \
.format(', '.join(['?'] * len(fingerprints)))
c = self.dbConn.cursor()
c.execute(q, fingerprints)
self.dbConn.commit()
def setTrust(self, _id, trust):
q = "UPDATE identities SET trust = ? WHERE _id = ?"
c = self.dbConn.cursor()
c.execute(q, (trust, _id))
self.dbConn.commit()

View File

@@ -0,0 +1,87 @@
# -*- 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
from axolotl.util.keyhelper import KeyHelper
class LitePreKeyStore(PreKeyStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
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=r[0]) for r in result]
def storePreKey(self, preKeyId, preKeyRecord):
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()
def getCurrentPreKeyId(self):
q = "SELECT MAX(prekey_id) FROM prekeys"
cursor = self.dbConn.cursor()
cursor.execute(q)
return cursor.fetchone()[0]
def getPreKeyCount(self):
q = "SELECT COUNT(prekey_id) FROM prekeys"
cursor = self.dbConn.cursor()
cursor.execute(q)
return cursor.fetchone()[0]
def generateNewPreKeys(self, count):
startId = self.getCurrentPreKeyId() + 1
preKeys = KeyHelper.generatePreKeys(startId, count)
for preKey in preKeys:
self.storePreKey(preKey.getId(), preKey)

View File

@@ -0,0 +1,130 @@
# -*- 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
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 getActiveDeviceTuples(self):
q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1"
c = self.dbConn.cursor()
result = []
for row in c.execute(q):
result.append((row[0].decode('utf-8'), row[1]))
return result
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()
def setActiveState(self, deviceList, jid):
c = self.dbConn.cursor()
q = "UPDATE sessions SET active = {} " \
"WHERE recipient_id = '{}' AND device_id IN ({})" \
.format(1, jid, ', '.join(['?'] * len(deviceList)))
c.execute(q, deviceList)
q = "UPDATE sessions SET active = {} " \
"WHERE recipient_id = '{}' AND device_id NOT IN ({})" \
.format(0, jid, ', '.join(['?'] * len(deviceList)))
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()
result = []
for row in c.execute(q, (recipientId,)):
public_key = (SessionRecord(serialized=row[0]).
getSessionState().getRemoteIdentityKey().
getPublicKey())
result.append(public_key.serialize())
return result

View File

@@ -0,0 +1,113 @@
# -*- 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
from axolotl.util.medium import Medium
class LiteSignedPreKeyStore(SignedPreKeyStore):
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
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()
def getNextSignedPreKeyId(self):
result = self.getCurrentSignedPreKeyId()
if not result:
return 1 # StartId if no SignedPreKeys exist
else:
return (result % (Medium.MAX_VALUE - 1)) + 1
def getCurrentSignedPreKeyId(self):
q = "SELECT MAX(prekey_id) FROM signed_prekeys"
cursor = self.dbConn.cursor()
cursor.execute(q)
result = cursor.fetchone()
if not result:
return None
else:
return result[0]
def getSignedPreKeyTimestamp(self, signedPreKeyId):
q = "SELECT strftime('%s', timestamp) 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 result[0]
def removeOldSignedPreKeys(self, timestamp):
q = "DELETE FROM signed_prekeys " \
"WHERE timestamp < datetime(?, 'unixepoch')"
cursor = self.dbConn.cursor()
cursor.execute(q, (timestamp, ))
self.dbConn.commit()

147
omemo/omemo/sql.py Normal file
View File

@@ -0,0 +1,147 @@
# -*- 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 .db_helpers import user_version
class SQLDatabase():
""" SQL Database """
def __init__(self, dbConn):
"""
:type dbConn: Connection
"""
self.dbConn = dbConn
self.createDb()
self.migrateDb()
def createDb(self):
if user_version(self.dbConn) == 0:
# Creates
# IdentityKeyStore
# PreKeyStore
# SignedPreKeyStore
# SessionStore
# EncryptionStore
create_tables = '''
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 INTEGER, timestamp INTEGER, trust INTEGER,
shown INTEGER DEFAULT 0);
CREATE UNIQUE INDEX IF NOT EXISTS
public_key_index ON identities (public_key, recipient_id);
CREATE TABLE IF NOT EXISTS prekeys(
_id INTEGER PRIMARY KEY AUTOINCREMENT,
prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN,
record BLOB);
CREATE TABLE IF NOT EXISTS signed_prekeys (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
prekey_id INTEGER UNIQUE,
timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB);
CREATE TABLE IF NOT EXISTS sessions (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
recipient_id TEXT, device_id INTEGER,
record BLOB, timestamp INTEGER, active INTEGER DEFAULT 1,
UNIQUE(recipient_id, device_id));
CREATE TABLE IF NOT EXISTS encryption_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT UNIQUE,
encryption INTEGER,
timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP
);
'''
create_db_sql = """
BEGIN TRANSACTION;
%s
PRAGMA user_version=5;
END TRANSACTION;
""" % (create_tables)
self.dbConn.executescript(create_db_sql)
def migrateDb(self):
""" Migrates the DB
"""
# Find all double entrys and delete them
if user_version(self.dbConn) < 2:
delete_dupes = """ DELETE FROM identities WHERE _id not in (
SELECT MIN(_id)
FROM identities
GROUP BY
recipient_id, public_key
);
"""
self.dbConn.executescript(""" BEGIN TRANSACTION;
%s
PRAGMA user_version=2;
END TRANSACTION;
""" % (delete_dupes))
if user_version(self.dbConn) < 3:
# Create a UNIQUE INDEX so every public key/recipient_id tuple
# can only be once in the db
add_index = """ CREATE UNIQUE INDEX IF NOT EXISTS
public_key_index
ON identities (public_key, recipient_id);
"""
self.dbConn.executescript(""" BEGIN TRANSACTION;
%s
PRAGMA user_version=3;
END TRANSACTION;
""" % (add_index))
if user_version(self.dbConn) < 4:
# Adds column "active" to the sessions table
add_active = """ ALTER TABLE sessions
ADD COLUMN active INTEGER DEFAULT 1;
"""
self.dbConn.executescript(""" BEGIN TRANSACTION;
%s
PRAGMA user_version=4;
END TRANSACTION;
""" % (add_active))
if user_version(self.dbConn) < 5:
# Adds DEFAULT Timestamp
add_timestamp = """
DROP TABLE signed_prekeys;
CREATE TABLE IF NOT EXISTS signed_prekeys (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
prekey_id INTEGER UNIQUE,
timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB);
ALTER TABLE identities ADD COLUMN shown INTEGER DEFAULT 0;
UPDATE identities SET shown = 1;
"""
self.dbConn.executescript(""" BEGIN TRANSACTION;
%s
PRAGMA user_version=5;
END TRANSACTION;
""" % (add_timestamp))

412
omemo/omemo/state.py Normal file
View File

@@ -0,0 +1,412 @@
# -*- 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 time
from base64 import b64encode
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
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 .aes_gcm import NoValidSessions, decrypt, 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
TRUSTED = 1
UNDECIDED = 2
class OmemoState:
def __init__(self, own_jid, connection, account, plugin):
""" Instantiates an OmemoState object.
:param connection: an :py:class:`sqlite3.Connection`
"""
self.account = account
self.plugin = plugin
self.session_ciphers = {}
self.own_jid = own_jid
self.device_ids = {}
self.own_devices = []
self.store = LiteAxolotlStore(connection)
self.encryption = self.store.encryptionStore
for jid, device_id in self.store.getActiveDeviceTuples():
if jid != own_jid:
self.add_device(jid, device_id)
else:
self.add_own_device(device_id)
log.info(self.account + ' => Roster devices after boot:' +
str(self.device_ids))
log.info(self.account + ' => Own devices after boot:' +
str(self.own_devices))
log.debug(self.account + ' => ' +
str(self.store.preKeyStore.getPreKeyCount()) +
' PreKeys available')
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 set_devices(self, name, devices):
""" Return a an.
Parameters
----------
jid : string
The contacts jid
devices: [int]
A list of devices
"""
self.device_ids[name] = devices
log.info(self.account + ' => Saved devices for ' + name)
def add_device(self, name, device_id):
if name not in self.device_ids:
self.device_ids[name] = [device_id]
elif device_id not in self.device_ids[name]:
self.device_ids[name].append(device_id)
def set_own_devices(self, devices):
""" Overwrite the current :py:attribute:`OmemoState.own_devices` with
the given devices.
Parameters
----------
devices : [int]
A list of device_ids
"""
self.own_devices = devices
log.info(self.account + ' => Saved own devices')
def add_own_device(self, device_id):
if device_id not in self.own_devices:
self.own_devices.append(device_id)
@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 `True` only if own device id was added via
:py:method:`OmemoState.set_own_devices()`.
"""
return self.own_device_id in self.own_devices
@property
def bundle(self):
self.checkPreKeyAmount()
prekeys = [
(k.getId(), b64encode(k.getKeyPair().getPublicKey().serialize()))
for k in self.store.loadPreKeys()
]
identityKeyPair = self.store.getIdentityKeyPair()
self.cycleSignedPreKey(identityKeyPair)
signedPreKey = self.store.loadSignedPreKey(
self.store.getCurrentSignedPreKeyId())
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 msg_dict['sid'] == own_id:
log.info('Received previously sent message by us')
return
if own_id not in msg_dict['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 = 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.warning('No Session found ' + e.message)
log.warning('sender_jid => ' + str(sender_jid) +
' sid =>' + sid)
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
return
result = decrypt(key, iv, payload).decode('utf-8')
log.debug("Decrypted Message => " + 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('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
# Encrypt the message key with for each of receivers devices
for rid, cipher in session_ciphers.items():
try:
if self.isTrusted(cipher) == TRUSTED:
encrypted_keys[rid] = cipher.encrypt(key).serialize()
else:
log.debug('Skipped Device because Trust is: ' +
str(self.isTrusted(cipher)))
except:
log.warning('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)
my_other_devices = set(self.own_devices) - set({self.own_device_id})
# Encrypt the message key with for each of our own devices
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()
else:
log.debug('Skipped own Device because Trust is: ' +
str(self.isTrusted(cipher)))
except:
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,
'iv': iv,
'payload': payload}
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 getTrustedFingerprints(self, recipient_id):
inactive = self.store.getInactiveSessionsKeys(recipient_id)
trusted = self.store.getTrustedFingerprints(recipient_id)
trusted = set(trusted) - set(inactive)
return trusted
def getUndecidedFingerprints(self, recipient_id):
inactive = self.store.getInactiveSessionsKeys(recipient_id)
undecided = self.store.getUndecidedFingerprints(recipient_id)
undecided = set(undecided) - set(inactive)
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.
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)]
if missing_devices:
log.info(self.account + ' => Missing device sessions for ' +
jid + ': ' + 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)
if not preKeyWhisperMessage.getPreKeyId():
raise Exception("Received PreKeyWhisperMessage without PreKey =>" +
recipient_id)
sessionCipher = self.get_session_cipher(recipient_id, device_id)
try:
log.debug(self.account +
" => Received PreKeyWhisperMessage from " +
recipient_id)
key = sessionCipher.decryptPkmsg(preKeyWhisperMessage)
# Publish new bundle after PreKey has been used
# for building a new Session
self.plugin.publish_bundle(self.account)
return key
except UntrustedIdentityException as e:
log.info(self.account + " => Received WhisperMessage " +
"from Untrusted Fingerprint! => " + e.getName())
def handleWhisperMessage(self, recipient_id, device_id, key):
whisperMessage = WhisperMessage(serialized=key)
sessionCipher = self.get_session_cipher(recipient_id, device_id)
log.debug(self.account + " => Received WhisperMessage from " +
recipient_id)
if self.isTrusted(sessionCipher) >= TRUSTED:
key = sessionCipher.decryptMsg(whisperMessage, textMsg=False)
return key
else:
raise Exception("Received WhisperMessage "
"from Untrusted Fingerprint! => " + recipient_id)
def checkPreKeyAmount(self):
# Check if enough PreKeys are available
preKeyCount = self.store.preKeyStore.getPreKeyCount()
if preKeyCount < MIN_PREKEY_AMOUNT:
newKeys = DEFAULT_PREKEY_AMOUNT - preKeyCount
self.store.preKeyStore.generateNewPreKeys(newKeys)
log.info(self.account + ' => ' + str(newKeys) +
' PreKeys created')
def cycleSignedPreKey(self, identityKeyPair):
# Publish every SPK_CYCLE_TIME a new SignedPreKey
# Delete all exsiting SignedPreKeys that are older
# then SPK_ARCHIVE_TIME
# Check if SignedPreKey exist and create if not
if not self.store.getCurrentSignedPreKeyId():
signedPreKey = KeyHelper.generateSignedPreKey(
identityKeyPair, self.store.getNextSignedPreKeyId())
self.store.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
log.debug(self.account +
' => New SignedPreKey created, because none existed')
# if SPK_CYCLE_TIME is reached, generate a new SignedPreKey
now = int(time.time())
timestamp = self.store.getSignedPreKeyTimestamp(
self.store.getCurrentSignedPreKeyId())
if int(timestamp) < now - SPK_CYCLE_TIME:
signedPreKey = KeyHelper.generateSignedPreKey(
identityKeyPair, self.store.getNextSignedPreKeyId())
self.store.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
log.debug(self.account + ' => Cycled SignedPreKey')
# Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME
timestamp = now - SPK_ARCHIVE_TIME
self.store.removeOldSignedPreKeys(timestamp)