OMEMO-0.10.3/0000755000175000017500000000000013411752554012434 5ustar useruser00000000000000OMEMO-0.10.3/omemo/0000755000175000017500000000000013411752554013550 5ustar useruser00000000000000OMEMO-0.10.3/omemo/version.py0000644000175000017500000000002713411672170015601 0ustar useruser00000000000000__version__ = "0.10.3" OMEMO-0.10.3/omemo/sessionmanagerasyncio.py0000644000175000017500000000240613401707610020520 0ustar useruser00000000000000from __future__ import absolute_import import asyncio from .promise import Promise from .sessionmanager import SessionManager def wrap(attr): def _wrap(*args, **kwargs): result = attr(*args, **kwargs) if isinstance(result, Promise): future = asyncio.Future() result.then(future.set_result, future.set_exception) return future else: return result return _wrap class SessionManagerAsyncIO(SessionManager): @classmethod def create(cls, *args, **kwargs): result = super(SessionManagerAsyncIO, cls).create(*args, **kwargs) if isinstance(result, Promise): future = asyncio.Future() result.then(future.set_result, future.set_exception) return future else: return result def __getattribute__(self, attr_name): attr = super(SessionManagerAsyncIO, self).__getattribute__(attr_name) if attr_name in [ "encryptMessage", "encryptKeyTransportMessage", "buildSession", "decryptMessage", "newDeviceList", "getDevices", "public_bundle", "republish_bundle" ]: return wrap(attr) return attr OMEMO-0.10.3/omemo/extendeddoubleratchet.py0000644000175000017500000000207113411547344020467 0ustar useruser00000000000000from __future__ import absolute_import import base64 def make(backend): class ExtendedDoubleRatchet(backend.DoubleRatchet): def __init__(self, other_ik, *args, **kwargs): super(ExtendedDoubleRatchet, self).__init__(*args, **kwargs) self.__other_ik = other_ik def serialize(self): return { "super" : super(ExtendedDoubleRatchet, self).serialize(), "other_ik" : base64.b64encode(self.__other_ik).decode("US-ASCII") } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = super(ExtendedDoubleRatchet, cls).fromSerialized( serialized["super"], *args, ad = None, # TODO: This is ugly root_key = None, **kwargs ) self.__other_ik = base64.b64decode(serialized["other_ik"].encode("US-ASCII")) return self @property def ik(self): return self.__other_ik return ExtendedDoubleRatchet OMEMO-0.10.3/omemo/exceptions/0000755000175000017500000000000013411752554015731 5ustar useruser00000000000000OMEMO-0.10.3/omemo/exceptions/untrustedexception.py0000644000175000017500000000165513411514060022252 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class UntrustedException(SessionManagerException): def __init__(self, bare_jid, device, ik): self.__bare_jid = bare_jid self.__device = device self.__ik = ik @property def bare_jid(self): return self.__bare_jid @property def device(self): return self.__device @property def ik(self): return self.__ik def __eq__(self, other): return ( isinstance(other, UntrustedException) and other.bare_jid == self.bare_jid and other.device == self.device and other.ik == self.ik ) def __hash__(self): return hash((self.bare_jid, self.device, self.ik)) def __str__(self): return ( "The key {} of {} on device {} is untrusted." .format(self.__ik, self.__bare_jid, self.__device) ) OMEMO-0.10.3/omemo/exceptions/encryptionproblemsexception.py0000644000175000017500000000121513411514525024151 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class EncryptionProblemsException(SessionManagerException): def __init__(self, problems): self.__problems = problems @property def problems(self): return self.__problems def __str__(self): if len(self.__problems) == 1: return ( "There was a problem during message encryption: {}" .format(self.__problems[0]) ) else: return ( "There were {} problems during message encryption: {}" .format(len(self.__problems), self.__problems) ) OMEMO-0.10.3/omemo/exceptions/missingbundleexception.py0000644000175000017500000000143213411514564023062 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class MissingBundleException(SessionManagerException): def __init__(self, bare_jid, device): self.__bare_jid = bare_jid self.__device = device @property def bare_jid(self): return self.__bare_jid @property def device(self): return self.__device def __eq__(self, other): return ( isinstance(other, MissingBundleException) and other.bare_jid == self.bare_jid and other.device == self.device ) def __hash__(self): return hash((self.bare_jid, self.device)) def __str__(self): return ( "Missing bundle for {} on device {}." .format(self.__bare_jid, self.__device) ) OMEMO-0.10.3/omemo/exceptions/backendexception.py0000644000175000017500000000013513411515223021576 0ustar useruser00000000000000from .omemoexception import OMEMOException class BackendException(OMEMOException): pass OMEMO-0.10.3/omemo/exceptions/__init__.py0000644000175000017500000000137513411515314020037 0ustar useruser00000000000000from __future__ import absolute_import from .backendexception import BackendException from .encryptionproblemsexception import EncryptionProblemsException from .inconsistentinfoexception import InconsistentInfoException from .keyexchangeexception import KeyExchangeException from .missingbundleexception import MissingBundleException from .nodevicesexception import NoDevicesException from .noeligibledevicesexception import NoEligibleDevicesException from .nosessionexception import NoSessionException from .omemoexception import OMEMOException from .sessionmanagerexception import SessionManagerException from .unknownkeyexception import UnknownKeyException from .untrustedexception import UntrustedException from .wireformatexception import WireFormatException OMEMO-0.10.3/omemo/exceptions/omemoexception.py0000644000175000017500000000005213411515104021317 0ustar useruser00000000000000class OMEMOException(Exception): pass OMEMO-0.10.3/omemo/exceptions/inconsistentinfoexception.py0000644000175000017500000000020113411514213023573 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class InconsistentInfoException(SessionManagerException): pass OMEMO-0.10.3/omemo/exceptions/keyexchangeexception.py0000644000175000017500000000154613411514632022514 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class KeyExchangeException(SessionManagerException): def __init__(self, bare_jid, device, message): self.__bare_jid = bare_jid self.__device = device self.__message = message @property def bare_jid(self): return self.__bare_jid @property def device(self): return self.__device def __eq__(self, other): return ( isinstance(other, KeyExchangeException) and other.bare_jid == self.bare_jid and other.device == self.device ) def __hash__(self): return hash((self.bare_jid, self.device)) def __str__(self): return ( "The initial key exchange with {} on device {} failed: {}" .format(self.__bare_jid, self.__device, self.__message) ) OMEMO-0.10.3/omemo/exceptions/sessionmanagerexception.py0000644000175000017500000000014413411515276023235 0ustar useruser00000000000000from .omemoexception import OMEMOException class SessionManagerException(OMEMOException): pass OMEMO-0.10.3/omemo/exceptions/noeligibledevicesexception.py0000644000175000017500000000126513411514675023702 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class NoEligibleDevicesException(SessionManagerException): def __init__(self, bare_jid): self.__bare_jid = bare_jid @property def bare_jid(self): return self.__bare_jid def __eq__(self, other): return ( isinstance(other, NoEligibleDevicesException) and other.bare_jid == self.bare_jid ) def __hash__(self): return hash(self.bare_jid) def __str__(self): return ( "Encryption failed for every single device of {}. {} will not receive the " "message at all.".format(self.__bare_jid, self.__bare_jid) ) OMEMO-0.10.3/omemo/exceptions/nosessionexception.py0000644000175000017500000000151313411513700022226 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class NoSessionException(SessionManagerException): def __init__(self, bare_jid, device): self.__bare_jid = bare_jid self.__device = device @property def bare_jid(self): return self.__bare_jid @property def device(self): return self.__device def __eq__(self, other): return ( isinstance(other, NoSessionException) and other.bare_jid == self.bare_jid and other.device == self.device ) def __hash__(self): return hash((self.bare_jid, self.device)) def __str__(self): return ( "Tried to decrypt a message from {} on device {}, but there is no session " "with that device.".format(self.__bare_jid, self.__device) ) OMEMO-0.10.3/omemo/exceptions/wireformatexception.py0000644000175000017500000000014613411512767022402 0ustar useruser00000000000000from .backendexception import BackendException class WireFormatException(BackendException): pass OMEMO-0.10.3/omemo/exceptions/nodevicesexception.py0000644000175000017500000000101113411514572022166 0ustar useruser00000000000000from .sessionmanagerexception import SessionManagerException class NoDevicesException(SessionManagerException): def __init__(self, bare_jid): self.__bare_jid = bare_jid @property def bare_jid(self): return self.__bare_jid def __eq__(self, other): return isinstance(other, NoDevicesException) and other.bare_jid == self.bare_jid def __hash__(self): return hash(self.bare_jid) def __str__(self): return "No known devices for {}.".format(self.__bare_jid) OMEMO-0.10.3/omemo/exceptions/unknownkeyexception.py0000644000175000017500000000014013411515037022416 0ustar useruser00000000000000from .omemoexception import OMEMOException class UnknownKeyException(OMEMOException): pass OMEMO-0.10.3/omemo/storage.py0000644000175000017500000002024513411670475015572 0ustar useruser00000000000000class Storage(object): """ The interface used by the SessionManager to persist data between runs. There are two possible ways to implement the Storage class: synchronous or asynchronous. The mode is determined by the result of the is_async method. If the implementation is synchronous, the callback parameter is None. If the implementation is asynchronous, the callback parameter is a function that takes two arguments: - success: True or False - result: The result of the operation if success is True or the error if success is False Note: The SessionManager does caching to reduce the number of calls to a minimum. There should be no need to add caching or any other logic in here, just plain storing and loading. """ def loadOwnData(self, callback): """ Load the own data. Return a dictionary of following structure: { "own_bare_jid" : string, "own_device_id" : int } or None, if no own data was stored previously. """ raise NotImplementedError def storeOwnData(self, callback, own_bare_jid, own_device_id): """ Store given own data, overwriting previously stored data. """ raise NotImplementedError def loadState(self, callback): """ Load the state. Return the stored structure or None, if no state was stored previously. """ raise NotImplementedError def storeState(self, callback, state): """ Store the state, overwriting the old state, if it exists. state is passed as a serializable object, that means it consist of a combination of the following types: - dictionaries - lists - strings - integers - floats - booleans - None You can dump this object using for example the json module. For more information on how the state object is structured, look at the omemo.X3DHDoubleRatchet.serialize method. """ raise NotImplementedError def loadSession(self, callback, bare_jid, device_id): """ Load a session with given bare_jid and device id. bare_jid is passed as a string, device_id as an integer. Return either the structure previously stored or None. """ raise NotImplementedError def loadSessions(self, callback, bare_jid, device_ids): """ Return a dict containing the session for each device id. By default, this method calls loadSession for each device id. """ if self.is_async: self.__loadSessionsAsync(callback, bare_jid, device_ids, {}) else: return self.__loadSessionsSync(bare_jid, device_ids) def __loadSessionsAsync(self, callback, bare_jid, device_ids, result): if len(device_ids) == 0: return callback(True, result) device_id = device_ids[0] def __loadSessionCallback(success, trust): if success: result[device_id] = trust self.__loadSessionsAsync(callback, bare_jid, device_ids[1:], result) else: callback(False, trust) self.loadSession(__loadSessionCallback, bare_jid, device_id) def __loadSessionsSync(self, bare_jid, device_ids): return { device: self.loadSession(None, bare_jid, device) for device in device_ids } def storeSession(self, callback, bare_jid, device_id, session): """ Store a session for given bare_jid and device id, overwriting the previous session, if it exists. bare_jid is passed as string, device_id as an integer. session is passed as a serializable object, that means it consist of a combination of the following types: - dictionaries - lists - strings - integers - floats - booleans - None You can dump this object using for example the json module. """ raise NotImplementedError def deleteSession(self, callback, bare_jid, device_id): """ Completely wipe the session associated with given bare_jid and device_id from the storage. bare_jid is passed as string, device_id as an integer. """ raise NotImplementedError def loadActiveDevices(self, callback, bare_jid): """ Load the list of active devices for a given bare_jid. An "active device" is a device, which is listed in the most recent version of the device list pep node. bare_jid is passed as a string, the result is expected to be a list of integers. """ raise NotImplementedError def loadInactiveDevices(self, callback, bare_jid): """ Load the list of inactive devices for a given bare_jid. An "inactive device" is a device, which was listed in an older version of the device list pep node, but is NOT listed in the most recent version. bare_jid is passed as a string, the result is expected to be a dict mapping from int to int, where the keys are device ids and the values are timestamps (seconds since epoch). """ raise NotImplementedError def storeActiveDevices(self, callback, bare_jid, devices): """ Store the active devices for given bare_jid, overwriting the old stored list, if it exists. bare_jid is passed as a string, devices as a list of integers. """ raise NotImplementedError def storeInactiveDevices(self, callback, bare_jid, devices): """ Store the inactive devices for given bare_jid, overwriting the old stored list, if it exists. bare_jid is passed as a string, devices as a dict mapping from int to int, where the keys are device ids and the values are timestamps (seconds since epoch). """ raise NotImplementedError def storeTrust(self, callback, bare_jid, device_id, trust): """ bare_jid: string device_id: int trust: { "key" : string (Base64 encoded bytes), "trusted" : bool } """ raise NotImplementedError def loadTrust(self, callback, bare_jid, device_id): """ """ raise NotImplementedError def loadTrusts(self, callback, bare_jid, device_ids): """ Return a dict containing the trust status for each device id. By default, this method calls loadTrust for each device id. """ if self.is_async: self.__loadTrustsAsync(callback, bare_jid, device_ids, {}) else: return self.__loadTrustsSync(bare_jid, device_ids) def __loadTrustsAsync(self, callback, bare_jid, device_ids, result): if len(device_ids) == 0: return callback(True, result) device_id = device_ids[0] def __loadTrustCallback(success, trust): if success: result[device_id] = trust self.__loadTrustsAsync(callback, bare_jid, device_ids[1:], result) else: callback(False, trust) self.loadTrust(__loadTrustCallback, bare_jid, device_id) def __loadTrustsSync(self, bare_jid, device_ids): return { device: self.loadTrust(None, bare_jid, device) for device in device_ids } def listJIDs(self, callback): """ List all bare jids that have associated device lists stored in the storage. It doesn't matter if the lists are empty or not. Return a list of strings. """ raise NotImplementedError def deleteJID(self, callback, bare_jid): """ Delete all data associated with given bare_jid. This includes the active and inactive devices, all sessions stored for that jid and all information about trusted keys. """ raise NotImplementedError @property def is_async(self): """ Return, whether this implementation is asynchronous. Read the introduction to this module above for details on what this value changes. """ raise NotImplementedError OMEMO-0.10.3/omemo/extendedpublicbundle.py0000644000175000017500000000661413374637156020332 0ustar useruser00000000000000from __future__ import absolute_import from .exceptions import UnknownKeyException class ExtendedPublicBundle(object): """ This class looks exactly the same as the PublicBundle class, but the types of the fields are a bit different: The spk field is not a key, but a dictionary containing the key and the id: spk = { "key" : key, "id" : id } The otpks field is not an array of keys, but an array of dictionaries containing the key and the id: otpks = [ { "key" : key, "id" : id }, { "key" : key, "id" : id }, ... ] """ def __init__(self, ik, spk, spk_signature, otpks): self.__ik = ik self.__spk = spk self.__spk_signature = spk_signature self.__otpks = otpks @classmethod def parse(cls, backend, ik, spk, spk_signature, otpks): """ Use this method when creating a bundle from data you retrieved directly from some PEP node. This method applies an additional decoding step to the public keys in the bundle. Pass the same structure as the constructor expects. """ ik = backend.decodePublicKey(ik)[0] spk["key"] = backend.decodePublicKey(spk["key"])[0] otpks = list(map(lambda otpk: { "key" : backend.decodePublicKey(otpk["key"])[0], "id" : otpk["id"] }, otpks)) return cls(ik, spk, spk_signature, otpks) def serialize(self, backend): """ Use this method to prepare the data to be uploaded directly to some PEP node. This method applies an additional encoding step to the public keys in the bundle. The result is a dictionary with the keys ik, spk, spk_signature and otpks. The values are structured the same way as the inputs of the constructor. """ return { "ik": backend.encodePublicKey(self.ik, "25519"), "spk": { "id" : self.spk["id"], "key" : backend.encodePublicKey(self.spk["key"], "25519"), }, "spk_signature": self.spk_signature, "otpks": list(map(lambda otpk: { "id" : otpk["id"], "key" : backend.encodePublicKey(otpk["key"], "25519") }, self.otpks)) } @property def ik(self): return self.__ik @property def spk(self): return self.__spk @property def spk_signature(self): return self.__spk_signature @property def otpks(self): return self.__otpks def findOTPKId(self, otpk): otpks = list(filter(lambda x: x["key"] == otpk, self.otpks)) if len(otpks) != 1: raise UnknownKeyException("Tried to get the id of an unknown OTPK.") return otpks[0]["id"] def findSPKId(self, spk): # If the requested spk is the one contained in this bundle... if self.spk["key"] == spk: # ...return the id return self.spk["id"] raise UnknownKeyException("Tried to get the id of an unknown SPK.") def __eq__(self, other): try: return ( self.ik == other.ik and self.spk == other.spk and self.spk_signature == other.spk_signature and self.otpks == other.otpks ) except: return False OMEMO-0.10.3/omemo/__init__.py0000644000175000017500000000115613405436741015664 0ustar useruser00000000000000from __future__ import absolute_import from .version import __version__ import sys from . import backends from . import promise from . import util from .defaultotpkpolicy import DefaultOTPKPolicy from .extendeddoubleratchet import make as make_ExtendedDoubleRatchet from .extendedpublicbundle import ExtendedPublicBundle from .otpkpolicy import OTPKPolicy from .sessionmanager import SessionManager from .state import make as make_State from .storage import Storage from .x3dhdoubleratchet import make as make_X3DHDoubleRatchet if sys.version_info[0] == 3: from .sessionmanagerasyncio import SessionManagerAsyncIO OMEMO-0.10.3/omemo/state.py0000644000175000017500000001332013373511516015237 0ustar useruser00000000000000from __future__ import absolute_import import base64 import x3dh from .exceptions import UnknownKeyException from .extendedpublicbundle import ExtendedPublicBundle def make(backend): class State(backend.X3DHState): def __init__(self): super(State, self).__init__() self.__spk_id = 0 self.__spk_pub = None self.__otpk_id_counter = 0 self.__otpk_ids = {} def serialize(self): spk = self.__spk_pub spk = None if spk == None else base64.b64encode(spk).decode("US-ASCII") otpk_ids = {} for key, value in self.__otpk_ids.items(): otpk_ids[base64.b64encode(key).decode("US-ASCII")] = value return { "super": super(State, self).serialize(), "spk_id": self.__spk_id, "spk_pub": spk, "otpk_id_counter": self.__otpk_id_counter, "otpk_ids": otpk_ids } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = super(State, cls).fromSerialized( serialized["super"], *args, **kwargs ) spk = serialized["spk_pub"] spk = None if spk == None else base64.b64decode(spk.encode("US-ASCII")) otpk_ids = {} for key, value in serialized["otpk_ids"].items(): otpk_ids[base64.b64decode(key.encode("US-ASCII"))] = value self.__spk_id = serialized["spk_id"] self.__spk_pub = spk self.__otpk_id_counter = serialized["otpk_id_counter"] self.__otpk_ids = otpk_ids return self def getPublicBundle(self): """ The current OMEMO standard works with ids instead of sending full public keys whenever possible, probably to reduce traffic. This is not part of the core specification though, so it has to be added here. It is added in the getPublicBundle method, because this method is the only way to get public data and is the perfect spot to update ids. """ bundle = super(State, self).getPublicBundle() self.__updateIDs() return self.__extendBundle(bundle) def __updateIDs(self): # Check, whether the spk has changed and assign it the next id in that case if self.spk.pub != self.__spk_pub: self.__spk_pub = self.spk.pub self.__spk_id += 1 otpks = [ otpk.pub for otpk in self.otpks ] hidden_otpks = [ otpk.pub for otpk in self.hidden_otpks ] # Synchronize the list of OTPKs. # First, remove all entries in the current dict, # that were removed from the official list. for otpk in list(self.__otpk_ids): if not (otpk in otpks or otpk in hidden_otpks): del self.__otpk_ids[otpk] # Second, add new OTPKs to the dict and assign them ids for otpk in otpks: if not otpk in self.__otpk_ids: self.__otpk_id_counter += 1 self.__otpk_ids[otpk] = self.__otpk_id_counter def __extendBundle(self, bundle): """ Extend the bundle, adding the ids of the respective keys to all entries. """ ik = bundle.ik spk = { "key": bundle.spk, "id": self.getSPKID(bundle.spk) } spk_signature = bundle.spk_signature otpks = [ { "key": otpk, "id": self.getOTPKID(otpk) } for otpk in bundle.otpks ] return ExtendedPublicBundle(ik, spk, spk_signature, otpks) def __reduceBundle(self, bundle): """ Reduce the bundle, removing all ids of the respective keys from all entries. """ ik = bundle.ik spk = bundle.spk["key"] spk_signature = bundle.spk_signature otpks = [ otpk["key"] for otpk in bundle.otpks ] return x3dh.PublicBundle(ik, spk, spk_signature, otpks) def getSPKID(self, spk): self.__updateIDs() # If the requested spk is the most recent one... if self.__spk_pub == spk: # ...return the id return self.__spk_id raise UnknownKeyException("Tried to get the id of an unknown SPK.") def getSPK(self, spk_id): self.__updateIDs() # If the requested spk id is the one contained in this bundle... if self.__spk_id == spk_id: # ...return the key return self.__spk_pub raise UnknownKeyException("Tried to get the SPK for an unknown id.") def getOTPKID(self, otpk): self.__updateIDs() otpk_id = self.__otpk_ids.get(otpk) if otpk_id == None: raise UnknownKeyException("Tried to get the id of an unknown OTPK.") return otpk_id def getOTPK(self, otpk_id): self.__updateIDs() otpks = [ x[0] for x in self.__otpk_ids.items() if x[1] == otpk_id ] if len(otpks) != 1: raise UnknownKeyException("Tried to get the OTPK for an unknown id.") return otpks[0] def getSharedSecretActive(self, other_public_bundle, *args, **kwargs): other_public_bundle = self.__reduceBundle(other_public_bundle) return super(State, self).getSharedSecretActive( other_public_bundle, *args, **kwargs ) return State OMEMO-0.10.3/omemo/defaultotpkpolicy.py0000644000175000017500000000461213405437226017666 0ustar useruser00000000000000from __future__ import absolute_import from __future__ import division import copy from .otpkpolicy import OTPKPolicy class DefaultOTPKPolicy(OTPKPolicy): """ An implementation of the OTPKPolicy with a default ruleset that slightly prefers usability over security. These are the rules: * Never delete an OTPK because of messages that came from some sort of storage mechanism like MAM * Never delete an OTPK as long as you have not sent a single answer * Only delete an OTPK if at least two answers were sent with a delay of at least 24 hours between them With this ruleset possible attackers are prevented from permanently reusing an OTPK, while real-world use-cases should never result in lost messages because of deleted OTPKs. You can use the additional_information parameter of the SessionManager.decryptMessage method to declare whether a message came from some storage mechanism like MAM or not. To do so, pass additional_information like this:: additional_information = { "from_storage": boolean } """ @staticmethod def decideOTPK(preKeyMessages): pkms = copy.deepcopy(preKeyMessages) # Normalize the additional_information to contain an empty dict instead of None for pkm in pkms: if pkm["additional_information"] == None: pkm["additional_information"] = {} # Normalize the from_storage information for pkm in pkms: if not "from_storage" in pkm["additional_information"]: pkm["additional_information"]["from_storage"] = False # Filter out messages that were retreived from storage mechanisms pkms = list(filter( lambda pkm: not pkm["additional_information"]["from_storage"], pkms )) # Collect all answers answers = [] for pkm in pkms: answers += pkm["answers"] # Check whether at least two answers were sent if len(answers) < 2: return True # Check whether at least 24 hours passed between the two answers elapsed_seconds = max(answers) - min(answers) elapsed_minutes = elapsed_seconds / 60 elapsed_hours = elapsed_minutes / 60 if elapsed_hours < 24: return True # Otherwise, all conditions are met to delete the OTPK return False OMEMO-0.10.3/omemo/backends/0000755000175000017500000000000013411752554015322 5ustar useruser00000000000000OMEMO-0.10.3/omemo/backends/backend.py0000644000175000017500000000133713374636131017267 0ustar useruser00000000000000class Backend(object): def __init__(self, WireFormat, X3DHState, X3DHPKEncoder, DoubleRatchet): self.__WireFormat = WireFormat self.__X3DHState = X3DHState self.__X3DHPKEncoder = X3DHPKEncoder self.__DoubleRatchet = DoubleRatchet @property def WireFormat(self): return self.__WireFormat @property def X3DHState(self): return self.__X3DHState def encodePublicKey(self, *args, **kwargs): return self.__X3DHPKEncoder.encodePublicKey(*args, **kwargs) def decodePublicKey(self, *args, **kwargs): return self.__X3DHPKEncoder.decodePublicKey(*args, **kwargs) @property def DoubleRatchet(self): return self.__DoubleRatchet OMEMO-0.10.3/omemo/backends/wireformat.py0000644000175000017500000000117713374630420020054 0ustar useruser00000000000000class WireFormat(object): @staticmethod def messageFromWire(obj): raise NotImplementedError @staticmethod def finalizeMessageFromWire(obj, additional): raise NotImplementedError @staticmethod def messageToWire(ciphertext, header, additional): raise NotImplementedError @staticmethod def preKeyMessageFromWire(obj): raise NotImplementedError @staticmethod def finalizePreKeyMessageFromWire(obj, additional): raise NotImplementedError @staticmethod def preKeyMessageToWire(session_init_data, message, additional): raise NotImplementedError OMEMO-0.10.3/omemo/backends/__init__.py0000644000175000017500000000022113374634542017432 0ustar useruser00000000000000from __future__ import absolute_import from .backend import Backend from .wireformat import WireFormat from .x3dhpkencoder import X3DHPKEncoder OMEMO-0.10.3/omemo/backends/x3dhpkencoder.py0000644000175000017500000000226013374632230020431 0ustar useruser00000000000000from __future__ import absolute_import import x3dh class X3DHPKEncoder(x3dh.PublicKeyEncoder): @staticmethod def encodePublicKey(key, key_type): """ Encode given (Montgomery) public key and the type of the key into a sequence of bytes. :param key: The public key to encode, as a bytes-like object. :param key_type: Identification of the curve that this key is used with. Currently the only allowed value is (the string) "25519". :returns: A bytes-like object, which encodes the public key and possibly its type. """ raise NotImplementedError @staticmethod def decodePublicKey(key_encoded): """ Decode given (Montgomery) public key and the type of the key from a sequence of bytes. :param key_encoded: The public key and the type of the key to decode, as a bytes-like object. :returns: A tuple consisting of a bytes-like object encoding the public key and a string identifying the type of the curve this key is used with. Currently the only allowed value is (the string) "25519". """ raise NotImplementedError OMEMO-0.10.3/omemo/storagewrapper.py0000644000175000017500000000245713375753255017207 0ustar useruser00000000000000from __future__ import absolute_import from .promise import Promise from .storage import Storage def makeCallbackPromise(function, *args, **kwargs): """ Take a function that reports its result using a callback and return a Promise that listenes for this callback. The function must accept a callback as its first parameter. The callback must take two arguments: - success : True or False - result : The result of the operation if success is True or the error otherwise. """ def _resolver(resolve, reject): function( lambda success, result: resolve(result) if success else reject(result), *args, **kwargs ) return Promise(_resolver) def wrap(is_async, attr): def _wrap(*args, **kwargs): if is_async: return makeCallbackPromise(attr, *args, **kwargs) else: return attr(None, *args, **kwargs) return _wrap class StorageWrapper(object): def __init__(self, wrapped): self._wrapped = wrapped def __getattribute__(self, attr): if attr == "_wrapped": return super(StorageWrapper, self).__getattribute__(attr) if attr == "is_async": return self._wrapped.is_async return wrap(self.is_async, getattr(self._wrapped, attr)) OMEMO-0.10.3/omemo/sessionmanager.py0000644000175000017500000010420613411741141017131 0ustar useruser00000000000000from __future__ import absolute_import from __future__ import division import base64 import copy import logging import os import sys import time from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from . import promise from . import storagewrapper from .exceptions import * from .extendeddoubleratchet import make as make_ExtendedDoubleRatchet from .x3dhdoubleratchet import make as make_X3DHDoubleRatchet import x3dh # This makes me sad if sys.version_info[0] == 3: string_type = str else: string_type = basestring def d(*args, **kwargs): logging.getLogger("omemo.SessionManager").debug(*args, **kwargs) def w(*args, **kwargs): logging.getLogger("omemo.SessionManager").warning(*args, **kwargs) def e(*args, **kwargs): logging.getLogger("omemo.SessionManager").error(*args, **kwargs) def checkSelf(self, *args, **kwargs): return self._storage.is_async def checkPositionalArgument(position): def _checkPositionalArgument(*args, **kwargs): return args[position].is_async return _checkPositionalArgument class SessionManager(object): ################################ # construction and preparation # ################################ @classmethod @promise.maybe_coroutine(checkPositionalArgument(1)) def create( cls, storage, otpk_policy, backend, my_bare_jid, my_device_id, inactive_per_jid_max = 15, inactive_global_max = 0, inactive_max_age = 0 ): self = cls() # Store the parameters self._storage = storagewrapper.StorageWrapper(storage) self.__otpk_policy = otpk_policy self.__backend = backend self.__X3DHDoubleRatchet = make_X3DHDoubleRatchet(self.__backend) self.__ExtendedDoubleRatchet = make_ExtendedDoubleRatchet(self.__backend) self.__my_bare_jid = my_bare_jid self.__my_device_id = my_device_id self.__inactive_per_jid_max = inactive_per_jid_max self.__inactive_global_max = inactive_global_max self.__inactive_max_age = inactive_max_age # Prepare the caches self.__state = None self.__sessions_cache = {} self.__devices_cache = {} self.__trust_cache = {} yield self.__prepare() promise.returnValue(self) @promise.maybe_coroutine(checkSelf) def __prepare(self): state = yield self._storage.loadState() if state == None: self.__state = self.__X3DHDoubleRatchet() yield self._storage.storeState(self.__state.serialize()) yield self.__storeActiveDevices(self.__my_bare_jid, [ self.__my_device_id ]) else: self.__state = self.__X3DHDoubleRatchet.fromSerialized(state) own_data = yield self._storage.loadOwnData() if own_data == None: yield self._storage.storeOwnData(self.__my_bare_jid, self.__my_device_id) else: if (not self.__my_bare_jid == own_data["own_bare_jid"] or not self.__my_device_id == own_data["own_device_id"]): raise InconsistentInfoException( "Given storage is only usable for jid {} on device {}." .format(own_data["own_bare_jid"], own_data["own_device_id"]) ) ############## # encryption # ############## @promise.maybe_coroutine(checkSelf) def encryptMessage( self, bare_jids, plaintext, bundles = None, expect_problems = None ): # Dirty hack to access ciphertext from _encryptMessage ciphertext = [] def _encryptMessage(aes_gcm): ciphertext.append(aes_gcm.update(plaintext) + aes_gcm.finalize()) encrypted = yield self.encryptKeyTransportMessage( bare_jids, _encryptMessage, bundles, expect_problems ) encrypted["payload"] = ciphertext[0] promise.returnValue(encrypted) @promise.maybe_coroutine(checkSelf) def encryptRatchetForwardingMessage( self, bare_jids, bundles = None, expect_problems = None ): encrypted = yield self.encryptKeyTransportMessage( bare_jids, lambda aes_gcm: aes_gcm.finalize(), bundles, expect_problems ) promise.returnValue(encrypted) @promise.maybe_coroutine(checkSelf) def encryptKeyTransportMessage( self, bare_jids, encryption_callback, bundles = None, expect_problems = None ): """ bare_jids: iterable encryption_callback: A function which is called using an instance of cryptography.hazmat.primitives.ciphers.CipherContext, which you can use to encrypt any sort of data. You don't have to return anything. bundles: { [bare_jid: string] => { [device_id: int] => ExtendedPublicBundle } } expect_problems: { [bare_jid: string] => iterable } returns: { iv: bytes, sid: int, keys: { [bare_jid: string] => { [device: int] => { "data" : bytes, "pre_key" : boolean } } } } """ yield self.runInactiveDeviceCleanup() ######################### # parameter preparation # ######################### if isinstance(bare_jids, string_type): bare_jids = set([ bare_jids ]) else: bare_jids = set(bare_jids) if bundles == None: bundles = {} if expect_problems == None: expect_problems = {} else: for bare_jid in expect_problems: expect_problems[bare_jid] = set(expect_problems[bare_jid]) # Add the own bare jid to the set of jids bare_jids.add(self.__my_bare_jid) ######################################################## # check all preconditions and prepare missing sessions # ######################################################## problems = [] # Prepare the lists of devices to encrypt for encrypt_for = {} for bare_jid in bare_jids: devices = yield self.__loadActiveDevices(bare_jid) if len(devices) == 0: problems.append(NoDevicesException(bare_jid)) else: encrypt_for[bare_jid] = devices # Remove the sending devices from the list encrypt_for[self.__my_bare_jid].remove(self.__my_device_id) # Check whether all required bundles are available for bare_jid, devices in encrypt_for.items(): missing_bundles = set() # Load all sessions sessions = yield self.__loadSessions(bare_jid, devices) for device in devices: session = sessions[device] if session == None: if not device in bundles.get(bare_jid, {}): missing_bundles.add(device) devices -= missing_bundles for device in missing_bundles: if not device in expect_problems.get(bare_jid, set()): problems.append(MissingBundleException(bare_jid, device)) # Check for missing sessions and simulate the key exchange for bare_jid, devices in encrypt_for.items(): key_exchange_problems = {} # Load all sessions sessions = yield self.__loadSessions(bare_jid, devices) for device in devices: session = sessions[device] # If no session exists, create a new session if session == None: # Get the required bundle bundle = bundles[bare_jid][device] try: # Build the session, discarding the result afterwards. This is # just to check that the key exchange works. self.__state.getSharedSecretActive(bundle) except x3dh.exceptions.KeyExchangeException as e: key_exchange_problems[device] = str(e) encrypt_for[bare_jid] -= set(key_exchange_problems.keys()) for device, message in key_exchange_problems.items(): if not device in expect_problems.get(bare_jid, set()): problems.append(KeyExchangeException( bare_jid, device, message )) # Check the trust for each device for bare_jid, devices in encrypt_for.items(): # Load all trust trusts = yield self.__loadTrusts(bare_jid, devices) # Load all sessions sessions = yield self.__loadSessions(bare_jid, devices) untrusted = [] for device in devices: trust = trusts[device] session = sessions[device] # Get the identity key of the recipient other_ik = bundles[bare_jid][device].ik if session == None else session.ik if not (yield self.__checkTrust(bare_jid, device, other_ik, trust)): untrusted.append((device, other_ik)) devices -= set(map(lambda x: x[0], untrusted)) for device, other_ik in untrusted: if not device in expect_problems.get(bare_jid, set()): problems.append(UntrustedException(bare_jid, device, other_ik)) # Check for jids with no eligible devices for bare_jid, devices in list(encrypt_for.items()): # Skip this check for my own bare jid if bare_jid == self.__my_bare_jid: continue if len(devices) == 0: problems.append(NoEligibleDevicesException(bare_jid)) del encrypt_for[bare_jid] # If there were and problems, raise an Exception with a list of those. if len(problems) > 0: raise EncryptionProblemsException(problems) ############## # encryption # ############## # Prepare AES-GCM key and IV aes_gcm_iv = os.urandom(16) aes_gcm_key = os.urandom(16) # Create the AES-GCM instance aes_gcm = Cipher( algorithms.AES(aes_gcm_key), modes.GCM(aes_gcm_iv), backend=default_backend() ).encryptor() # Encrypt the plain data encryption_callback(aes_gcm) # Store the tag aes_gcm_tag = aes_gcm.tag # { # [bare_jid: string] => { # [device: int] => { # "data" : bytes, # "pre_key" : boolean # } # } # } encrypted_keys = {} for bare_jid, devices in encrypt_for.items(): encrypted_keys[bare_jid] = {} for device in devices: # Note whether this is a response to a PreKeyMessage if self.__state.hasBoundOTPK(bare_jid, device): self.__state.respondedTo(bare_jid, device) yield self._storage.storeState(self.__state.serialize()) # Load the session session = yield self.__loadSession(bare_jid, device) # If no session exists, this will be a PreKeyMessage pre_key = session == None # Create a new session if pre_key: # Get the required bundle bundle = bundles[bare_jid][device] # Build the session session_and_init_data = self.__state.getSharedSecretActive(bundle) session = session_and_init_data["dr"] session_init_data = session_and_init_data["to_other"] # Encrypt the AES GCM key and tag encrypted_data = session.encryptMessage(aes_gcm_key + aes_gcm_tag) # Store the new/changed session yield self.__storeSession(bare_jid, device, session) # Serialize the data into a simple message format serialized = self.__backend.WireFormat.messageToWire( encrypted_data["ciphertext"], encrypted_data["header"], { "DoubleRatchet": encrypted_data["additional"] } ) # If it is a PreKeyMessage, apply an additional step to the serialization. if pre_key: serialized = self.__backend.WireFormat.preKeyMessageToWire( session_init_data, serialized, { "DoubleRatchet": encrypted_data["additional"] } ) # Add the final encrypted and serialized data. encrypted_keys[bare_jid][device] = { "data" : serialized, "pre_key" : pre_key } promise.returnValue({ "iv" : aes_gcm_iv, "sid" : self.__my_device_id, "keys" : encrypted_keys }) ############## # decryption # ############## @promise.maybe_coroutine(checkSelf) def decryptMessage( self, bare_jid, device, iv, message, is_pre_key_message, ciphertext, additional_information = None, allow_untrusted = False ): aes_gcm = yield self.decryptKeyTransportMessage( bare_jid, device, iv, message, is_pre_key_message, additional_information, allow_untrusted ) promise.returnValue(aes_gcm.update(ciphertext) + aes_gcm.finalize()) @promise.maybe_coroutine(checkSelf) def decryptRatchetForwardingMessage( self, bare_jid, device, iv, message, is_pre_key_message, additional_information = None, allow_untrusted = False ): aes_gcm = yield self.decryptKeyTransportMessage( bare_jid, device, iv, message, is_pre_key_message, additional_information, allow_untrusted ) aes_gcm.finalize() @promise.maybe_coroutine(checkSelf) def decryptKeyTransportMessage( self, bare_jid, device, iv, message, is_pre_key_message, additional_information = None, allow_untrusted = False ): yield self.runInactiveDeviceCleanup() if is_pre_key_message: # Unpack the pre key message data message_and_init_data = self.__backend.WireFormat.preKeyMessageFromWire( message ) other_ik = message_and_init_data["session_init_data"]["ik"] # Before doing anything else, check the trust if not allow_untrusted: if not (yield self.__checkTrust(bare_jid, device, other_ik)): raise UntrustedException(bare_jid, device, other_ik) # Prepare the DoubleRatchet try: session = self.__state.getSharedSecretPassive( message_and_init_data["session_init_data"], bare_jid, device, self.__otpk_policy, additional_information ) except x3dh.exceptions.KeyExchangeException as e: raise KeyExchangeException(bare_jid, device, str(e)) # Store the changed state yield self._storage.storeState(self.__state.serialize()) # Store the new session yield self.__storeSession(bare_jid, device, session) # Unpack the "normal" message that was wrapped into the PreKeyMessage message = message_and_init_data["message"] # Load the session session = yield self.__loadSession(bare_jid, device) if session == None: raise NoSessionException(bare_jid, device) # Before doing anything else, check the trust if not allow_untrusted: if not (yield self.__checkTrust(bare_jid, device, session.ik)): raise UntrustedException(bare_jid, device, session.ik) # Now that the trust was checked, go on with normal processing if not is_pre_key_message: # If this is not part of a PreKeyMessage, we received a normal Message and can # safely delete the OTPK bound to this bare_jid+device. self.__state.deleteBoundOTPK(bare_jid, device) yield self._storage.storeState(self.__state.serialize()) # Unpack the message data message_data = self.__backend.WireFormat.messageFromWire(message) # Get the concatenation of the AES GCM key and tag plaintext = session.decryptMessage( message_data["ciphertext"], message_data["header"] ) # Check the authentication self.__backend.WireFormat.finalizeMessageFromWire( message, { "WireFormat": message_data["additional"], "DoubleRatchet": plaintext["additional"] } ) # Store the changed session yield self.__storeSession(bare_jid, device, session) plaintext = plaintext["plaintext"] aes_gcm_key = plaintext[:16] aes_gcm_tag = plaintext[16:] promise.returnValue(Cipher( algorithms.AES(aes_gcm_key), modes.GCM(iv, aes_gcm_tag), backend=default_backend() ).decryptor()) ############################ # public bundle management # ############################ @property def public_bundle(self): """ Fill a PublicBundle object with the public bundle data of this State. :returns: An instance of ExtendedPublicBundle, filled with the public data of this State. """ return self.__state.getPublicBundle() @property def republish_bundle(self): """ Read, whether this State has changed since it was loaded/since this flag was last cleared. :returns: A boolean indicating, whether the public bundle data has changed since last reading this flag. Clears the flag when reading. """ return self.__state.changed ###################### # session management # ###################### @promise.maybe_coroutine(checkSelf) def __loadSession(self, bare_jid, device): self.__sessions_cache[bare_jid] = self.__sessions_cache.get(bare_jid, {}) if not (device in self.__sessions_cache[bare_jid]): session = yield self._storage.loadSession(bare_jid, device) if not session == None: session = self.__ExtendedDoubleRatchet.fromSerialized(session, None) self.__sessions_cache[bare_jid][device] = session promise.returnValue(self.__sessions_cache[bare_jid][device]) @promise.maybe_coroutine(checkSelf) def __loadSessions(self, bare_jid, devices): self.__sessions_cache[bare_jid] = self.__sessions_cache.get(bare_jid, {}) missing_sessions = set(devices) - set(self.__sessions_cache[bare_jid].keys()) sessions = yield self._storage.loadSessions(bare_jid, list(missing_sessions)) for device in missing_sessions: session = sessions[device] if not session == None: session = self.__ExtendedDoubleRatchet.fromSerialized(session, None) self.__sessions_cache[bare_jid][device] = session promise.returnValue({ device: copy.deepcopy(self.__sessions_cache[bare_jid][device]) for device in devices }) @promise.maybe_coroutine(checkSelf) def __storeSession(self, bare_jid, device, session): self.__sessions_cache[bare_jid] = self.__sessions_cache.get(bare_jid, {}) self.__sessions_cache[bare_jid][device] = session yield self._storage.storeSession(bare_jid, device, session.serialize()) @promise.maybe_coroutine(checkSelf) def __deleteSession(self, bare_jid, device): self.__sessions_cache[bare_jid] = self.__sessions_cache.get(bare_jid, {}) self.__sessions_cache[bare_jid].pop(device, None) yield self._storage.deleteSession(bare_jid, device) def deleteSession(self, bare_jid, device): return self.__deleteSession(bare_jid, device) ##################### # device management # ##################### @promise.maybe_coroutine(checkSelf) def __loadActiveDevices(self, bare_jid): self.__devices_cache[bare_jid] = self.__devices_cache.get(bare_jid, {}) if not "active" in self.__devices_cache[bare_jid]: devices = yield self._storage.loadActiveDevices(bare_jid) self.__devices_cache[bare_jid]["active"] = set(devices) promise.returnValue(copy.deepcopy(self.__devices_cache[bare_jid]["active"])) @promise.maybe_coroutine(checkSelf) def __loadInactiveDevices(self, bare_jid): self.__devices_cache[bare_jid] = self.__devices_cache.get(bare_jid, {}) if not "inactive" in self.__devices_cache[bare_jid]: devices = yield self._storage.loadInactiveDevices(bare_jid) devices = copy.deepcopy(devices) self.__devices_cache[bare_jid]["inactive"] = devices promise.returnValue(copy.deepcopy(self.__devices_cache[bare_jid]["inactive"])) @promise.maybe_coroutine(checkSelf) def __storeActiveDevices(self, bare_jid, devices): self.__devices_cache[bare_jid] = self.__devices_cache.get(bare_jid, {}) self.__devices_cache[bare_jid]["active"] = set(devices) yield self._storage.storeActiveDevices(bare_jid, devices) @promise.maybe_coroutine(checkSelf) def __storeInactiveDevices(self, bare_jid, devices): self.__devices_cache[bare_jid] = self.__devices_cache.get(bare_jid, {}) self.__devices_cache[bare_jid]["inactive"] = copy.deepcopy(devices) yield self._storage.storeInactiveDevices(bare_jid, devices) @promise.maybe_coroutine(checkSelf) def newDeviceList(self, bare_jid, active_new): active_new = set(active_new) if bare_jid == self.__my_bare_jid: # The own device can never become inactive active_new |= set([ self.__my_device_id ]) active_old = yield self.__loadActiveDevices(bare_jid) inactive_old = yield self.__loadInactiveDevices(bare_jid) devices_old = active_old | set(inactive_old.keys()) inactive_new = devices_old - active_new now = time.time() inactive_new = { device: inactive_old.get(device, now) for device in inactive_new } yield self.__storeActiveDevices(bare_jid, active_new) yield self.__storeInactiveDevices(bare_jid, inactive_new) yield self.runInactiveDeviceCleanup() @promise.maybe_coroutine(checkSelf) def getDevices(self, bare_jid = None): yield self.runInactiveDeviceCleanup() if bare_jid == None: bare_jid = self.__my_bare_jid active = yield self.__loadActiveDevices(bare_jid) inactive = yield self.__loadInactiveDevices(bare_jid) promise.returnValue({ "active" : active, "inactive" : inactive }) @promise.maybe_coroutine(checkSelf) def __deleteInactiveDevices(self, bare_jid, delete_devices): for device in delete_devices: yield self.__deleteSession(bare_jid, device) inactive_devices = yield self.__loadInactiveDevices(bare_jid) for device in delete_devices: inactive_devices.pop(device, None) yield self.__storeInactiveDevices(bare_jid, inactive_devices) @promise.maybe_coroutine(checkSelf) def deleteInactiveDevicesByQuota(self, per_jid_max = 15, global_max = 0): """ Delete inactive devices by setting a quota. With per_jid_max you can define the amount of inactive devices that are kept for each jid, with global_max you can define a global maximum for inactive devices. If any of the quotas is reached, inactive devices are deleted on an LRU basis. This also deletes the corresponding sessions, so if a device comes active again and tries to send you an encrypted message you will not be able to decrypt it. The value "0" means no limitations/keep all inactive devices. It is recommended to always restrict the amount of per-jid inactive devices. If storage space limitations don't play a role, it is recommended to not restrict the global amount of inactive devices. Otherwise, the global_max can be used to control the amount of storage that can be used up by inactive sessions. The default of 15 per-jid devices is very permissive, but it is not recommended to decrease that number without a good reason. This is the recommended way to handle inactive device deletion. For a time-based alternative, look at the deleteInactiveDevicesByAge method. """ if per_jid_max < 1 and global_max < 1: return if per_jid_max < 1: per_jid_max = None if global_max < 1: global_max = None bare_jids = yield self._storage.listJIDs() if not per_jid_max == None: for bare_jid in bare_jids: devices = yield self.__loadInactiveDevices(bare_jid) if len(devices) > per_jid_max: # This sorts the devices from smaller to bigger timestamp, which means # from old to young. devices = sorted(devices.items(), key = lambda device: device[1]) # This gets the first (=oldest) n entries, so that only the # per_jid_max youngest entries are left. devices = devices[:-per_jid_max] # Get the device ids and discard the timestamps. devices = list(map(lambda device: device[0], devices)) yield self.__deleteInactiveDevices(bare_jid, devices) if not global_max == None: all_inactive_devices = [] for bare_jid in bare_jids: devices = yield self.__loadInactiveDevices(bare_jid) all_inactive_devices.extend(map( lambda device: (bare_jid, device[0], device[1]), devices.items() )) if len(all_inactive_devices) > global_max: # This sorts the devices from smaller to bigger timestamp, which means # from old to young. devices = sorted(all_inactive_devices, key = lambda device: device[2]) # This gets the first (=oldest) n entries, so that only the global_max # youngest entries are left. devices = devices[:-global_max] # Get the list of devices to delete for each jid delete_devices = {} for device in devices: bare_jid = device[0] device_id = device[1] delete_devices[bare_jid] = delete_devices.get(bare_jid, []) delete_devices[bare_jid].append(device_id) # Now, delete the devices for bare_jid, devices in delete_devices.items(): yield self.__deleteInactiveDevices(bare_jid, devices) @promise.maybe_coroutine(checkSelf) def deleteInactiveDevicesByAge(self, age_days): """ Delete all inactive devices from the device list storage and cache that are older then a given number of days. This also deletes the corresponding sessions, so if a device comes active again and tries to send you an encrypted message you will not be able to decrypt it. You are not allowed to delete inactive devices that were inactive for less than a day. Thus, the minimum value for age_days is 1. It is recommended to keep inactive devices for a longer period of time (e.g. multiple months), as it reduces the chance for message loss and doesn't require a lot of storage. The recommended alternative to deleting inactive devices by age is to delete them by count/quota. Look at the deleteInactiveDevicesByQuota method for that variant. """ if age_days < 1: return now = time.time() bare_jids = yield self._storage.listJIDs() for bare_jid in bare_jids: devices = yield self.__loadInactiveDevices(bare_jid) delete_devices = [] for device, timestamp in list(devices.items()): elapsed_s = now - timestamp elapsed_m = elapsed_s / 60 elapsed_h = elapsed_m / 60 elapsed_d = elapsed_h / 24 if elapsed_d >= age_days: delete_devices.append(device) if len(delete_devices) > 0: yield self.__deleteInactiveDevices(bare_jid, delete_devices) @promise.maybe_coroutine(checkSelf) def runInactiveDeviceCleanup(self): """ Runs both the deleteInactiveDevicesByAge and the deleteInactiveDevicesByQuota methods with the configuration that was set when calling create. """ yield self.deleteInactiveDevicesByQuota( self.__inactive_per_jid_max, self.__inactive_global_max ) yield self.deleteInactiveDevicesByAge(self.__inactive_max_age) #################### # trust management # #################### @promise.maybe_coroutine(checkSelf) def __loadTrust(self, bare_jid, device): self.__trust_cache[bare_jid] = self.__trust_cache.get(bare_jid, {}) if not device in self.__trust_cache[bare_jid]: trust = yield self._storage.loadTrust(bare_jid, device) self.__trust_cache[bare_jid][device] = None if trust == None else { "key" : base64.b64decode(trust["key"].encode("US-ASCII")), "trusted" : trust["trusted"] } promise.returnValue(copy.deepcopy(self.__trust_cache[bare_jid][device])) @promise.maybe_coroutine(checkSelf) def __loadTrusts(self, bare_jid, devices): self.__trust_cache[bare_jid] = self.__trust_cache.get(bare_jid, {}) missing_trusts = set(devices) - set(self.__trust_cache[bare_jid].keys()) trusts = yield self._storage.loadTrusts(bare_jid, list(missing_trusts)) for device in missing_trusts: trust = trusts[device] self.__trust_cache[bare_jid][device] = None if trust == None else { "key" : base64.b64decode(trust["key"].encode("US-ASCII")), "trusted" : trust["trusted"] } promise.returnValue({ device: copy.deepcopy(self.__trust_cache[bare_jid][device]) for device in devices }) @promise.maybe_coroutine(checkSelf) def __storeTrust(self, bare_jid, device, trust): self.__trust_cache[bare_jid] = self.__trust_cache.get(bare_jid, {}) self.__trust_cache[bare_jid][device] = copy.deepcopy(trust) yield self._storage.storeTrust( bare_jid, device, { "key" : base64.b64encode(trust["key"]).decode("US-ASCII"), "trusted" : trust["trusted"] } ) @promise.maybe_coroutine(checkSelf) def __checkTrust(self, bare_jid, device, key, trust = False): if trust == False: trust = yield self.__loadTrust(bare_jid, device) if trust == None: promise.returnValue(False) if not trust["key"] == key: promise.returnValue(False) promise.returnValue(trust["trusted"]) @promise.maybe_coroutine(checkSelf) def trust(self, bare_jid, device, key): yield self.__storeTrust(bare_jid, device, { "key" : key, "trusted" : True }) @promise.maybe_coroutine(checkSelf) def distrust(self, bare_jid, device, key): yield self.__storeTrust(bare_jid, device, { "key" : key, "trusted" : False }) def getTrustForDevice(self, bare_jid, device): """ Get trust information for a single device. The result is structured like this: { "key" : a bytes-like object encoding the public key, "trusted" : boolean } or None, if no trust was stored for that device. """ return self.__loadTrust(bare_jid, device) @promise.maybe_coroutine(checkSelf) def getTrustForJID(self, bare_jid): """ All-in-one trust information for all devices of a bare jid. The result is structured like this: { "active" : { device: int => trust_info } "inactive" : { device: int => trust_info } } where trust_info is the structure returned by getTrustForDevice. """ result = { "active" : {}, "inactive" : {} } devices = yield self.__loadActiveDevices(bare_jid) for device in devices: result["active"][device] = yield self.getTrustForDevice(bare_jid, device) devices = yield self.__loadInactiveDevices(bare_jid) for device in devices: result["inactive"][device] = yield self.getTrustForDevice(bare_jid, device) promise.returnValue(result) ######### # other # ######### def listJIDs(self): return self._storage.listJIDs() @promise.maybe_coroutine(checkSelf) def deleteJID(self, bare_jid): """ Delete all data associated with a JID. This includes the list of active/inactive devices, all sessions with that JID and all information about trusted keys. """ yield self.runInactiveDeviceCleanup() self.__sessions_cache.pop(bare_jid, None) self.__devices_cache.pop(bare_jid, None) self.__trust_cache.pop(bare_jid, None) yield self._storage.deleteJID(bare_jid) OMEMO-0.10.3/omemo/util.py0000644000175000017500000000061413372774430015103 0ustar useruser00000000000000from __future__ import absolute_import import os import struct DEVICE_ID_MIN = 1 DEVICE_ID_MAX = 2 ** 31 - 1 def generateDeviceID(blacklist = []): while True: device_id = struct.unpack(">L", os.urandom(4))[0] if device_id < DEVICE_ID_MIN or device_id > DEVICE_ID_MAX: continue if device_id in blacklist: continue return device_id OMEMO-0.10.3/omemo/otpkpolicy.py0000644000175000017500000000274313373511516016323 0ustar useruser00000000000000class OTPKPolicy(object): @staticmethod def decideOTPK(preKeyMessages): """ Use the data passed to this method to decide, whether to keep an OTPK or not. Return True to keep the OTPK and False to delete it. The preKeyMessages parameter is a list of dictionaries with following structure: { # The UNIX timestamp that PreKeyMessage was received on "timestamp": int, # A list of UNIX timestamps, for each Message that answered this PreKeyMessage "answers": list, # This key can be used by implementations to store any sort of additional # information about the message, which can be used for more complex logic to # decide whether to keep the one-time pre key. One example that would make a # lot of sense is a flag, which indicates whether the message was retrieved # from some storage mechanism like mam. Messages retrieved from mam should # probably not trigger one-time pre key deletion, because there might be more # pre key messages waiting in the mam catch-up that use the same one-time pre # key. # The value of this key must consist of Python primitives like ints, floats, # strings, booleans, lists, dictionaries or None (basically everything # json-serializable). "additional_information": any } """ raise NotImplementedError OMEMO-0.10.3/omemo/promise.py0000644000175000017500000002327413411747740015611 0ustar useruser00000000000000from __future__ import absolute_import import functools import threading import types class ReturnValueException(Exception): def __init__(self, value): self.__value = value @property def value(self): return self.__value class RejectedException(Exception): def __init__(self, reason): self.__reason = reason def __eq__(self, other): if isinstance(other, RejectedException): return self.__reason == other.reason return False def __hash__(self): return hash(self.__reason) @property def reason(self): return self.__reason class InvalidCoroutineException(Exception): def __init__(self, reason): self.__reason = reason def __eq__(self, other): if isinstance(other, InvalidCoroutineException): return self.__reason == other.reason return False def __hash__(self): return hash(self.__reason) @property def reason(self): return self.__reason class Promise(object): PENDING = "PENDING" FULFILLED = "FULFILLED" REJECTED = "REJECTED" def __init__(self, code): self.__state = Promise.PENDING self.__value = None self.__reason = None self.__onfulfilled = [] self.__onrejected = [] self.__code = code threading.Thread(target = self.__run).start() def __run(self): try: self.__code(self.__resolve, self.__reject) except BaseException as e: self.__reject(e) def __resolve(self, value): if self.__state == Promise.PENDING: self.__value = value self.__state = Promise.FULFILLED while len(self.__onfulfilled) > 0: listener = self.__onfulfilled.pop(0) listener(self.__value) def __reject(self, reason): if self.__state == Promise.PENDING: self.__reason = reason self.__state = Promise.REJECTED while len(self.__onrejected) > 0: listener = self.__onrejected.pop(0) listener(self.__reason) def then(self, onfulfilled, onrejected): if callable(onfulfilled): if self.__state == Promise.PENDING: self.__onfulfilled.append(onfulfilled) if self.__state == Promise.FULFILLED: onfulfilled(self.__value) if callable(onrejected): if self.__state == Promise.PENDING: self.__onrejected.append(onrejected) if self.__state == Promise.REJECTED: onrejected(self.__reason) @property def done(self): return not self.__state == Promise.PENDING @property def fulfilled(self): return self.__state == Promise.FULFILLED @property def rejected(self): return self.__state == Promise.REJECTED def __str__(self): if self.__state == Promise.PENDING: return "Pending Promise object" if self.__state == Promise.FULFILLED: return "Fulfilled Promise object with value: " + str(self.__value) if self.__state == Promise.REJECTED: return "Rejected Promise object with reason: " + str(self.__reason) @property def state(self): return self.__state @property def value(self): return self.__value @property def reason(self): return self.__reason @classmethod def resolve(cls, value): return cls(lambda resolve, reject: resolve(value)) @classmethod def reject(cls, reason): return cls(lambda resolve, reject: reject(reason)) def returnValue(value): """ In Python 2 we are not allowed to return from a generator function. Instead, we have to raise the StopIteration exceptions ourselves to return from the coroutine. Python 3.7 for some fucking reason changed it so you can't raise a StopIteration exception yourself. Really fucking great idea. Let's make it harder every version to write software for Python 2 and 3. For this awesome reason, we don't raise a StopIteration exception but a self-made exception called ReturnValueException. """ raise ReturnValueException(value) def coroutine(f): """ Implementation of a coroutine. Use as a decorator: @coroutine def foo(): result = yield somePromise The function passed should be a generator yielding instances of the Promise class (or compatible). The coroutine waits for the Promise to resolve and sends the result (or the error) back into the generator function. This simulates sequential execution which in reality can be asynchonous. """ @functools.wraps(f) def _coroutine(*args, **kwargs): def _resolver(resolve, reject): try: generator = f(*args, **kwargs) except BaseException as e: # Special case for a function that throws immediately reject(e) else: # Special case for a function that returns immediately if not isinstance(generator, types.GeneratorType): resolve(generator) else: def _step(previous, previous_type): element = None try: if previous_type == None: element = next(generator) elif previous_type: element = generator.send(previous) else: if not isinstance(previous, BaseException): previous = RejectedException(previous) element = generator.throw(previous) except StopIteration as e: resolve(getattr(e, "value", None)) except ReturnValueException as e: resolve(e.value) except BaseException as e: reject(e) else: try: element.then( lambda value : _step(value, True), lambda reason : _step(reason, False) ) except AttributeError: reject(InvalidCoroutineException(element)) _step(None, None) return Promise(_resolver) return _coroutine def no_coroutine(f): """ This is not a coroutine ;) Use as a decorator: @no_coroutine def foo(): five = yield 5 print(yield "hello") The function passed should be a generator yielding whatever you feel like. The yielded values instantly get passed back into the generator. It's basically the same as if you didn't use yield at all. The example above is equivalent to: def foo(): five = 5 print("hello") Why? This is the counterpart to coroutine used by maybe_coroutine below. """ @functools.wraps(f) def _no_coroutine(*args, **kwargs): generator = f(*args, **kwargs) # Special case for a function that returns immediately if not isinstance(generator, types.GeneratorType): return generator previous = None first = True while True: element = None try: if first: element = next(generator) else: element = generator.send(previous) except StopIteration as e: return getattr(e, "value", None) except ReturnValueException as e: return e.value else: previous = element first = False return _no_coroutine def maybe_coroutine(decide): """ Either be a coroutine or not. Use as a decorator: @maybe_coroutine(lambda maybeAPromise: return isinstance(maybeAPromise, Promise)) def foo(maybeAPromise): result = yield maybeAPromise print("hello") return result The function passed should be a generator yielding either only Promises or whatever you feel like. The decide parameter must be a function which gets called with the same parameters as the function to decide whether this is a coroutine or not. Using this it is possible to either make the function a coroutine or not based on a parameter to the function call. Let's explain the example above: # If the maybeAPromise is an instance of Promise, # we want the foo function to act as a coroutine. # If the maybeAPromise is not an instance of Promise, # we want the foo function to act like any other normal synchronous function. @maybe_coroutine(lambda maybeAPromise: return isinstance(maybeAPromise, Promise)) def foo(maybeAPromise): # If isinstance(maybeAPromise, Promise), foo behaves like a coroutine, # thus maybeAPromise will get resolved asynchronously and the result will be # pushed back here. # Otherwise, foo behaves like no_coroutine, # just pushing the exact value of maybeAPromise back into the generator. result = yield maybeAPromise print("hello") return result """ def _maybe_coroutine(f): @functools.wraps(f) def __maybe_coroutine(*args, **kwargs): if decide(*args, **kwargs): return coroutine(f)(*args, **kwargs) else: return no_coroutine(f)(*args, **kwargs) return __maybe_coroutine return _maybe_coroutine OMEMO-0.10.3/omemo/x3dhdoubleratchet.py0000644000175000017500000002256213411212107017526 0ustar useruser00000000000000from __future__ import absolute_import from x3dh.exceptions import KeyExchangeException from .exceptions import UnknownKeyException from .extendeddoubleratchet import make as make_ExtendedDoubleRatchet from .state import make as make_State from .version import __version__ import base64 import copy import time def make(backend): class X3DHDoubleRatchet(make_State(backend)): def __init__(self): super(X3DHDoubleRatchet, self).__init__() self.__bound_otpks = {} self.__pre_key_messages = {} self.__ExtendedDoubleRatchet = make_ExtendedDoubleRatchet(backend) def serialize(self): bound_otpks = {} for bare_jid in self.__bound_otpks: bound_otpks[bare_jid] = {} for device in self.__bound_otpks[bare_jid]: otpk = self.__bound_otpks[bare_jid][device] bound_otpks[bare_jid][device] = { "otpk" : base64.b64encode(otpk["otpk"]).decode("US-ASCII"), "id" : otpk["id"] } pk_messages = {} for otpk, value in self.__pre_key_messages.items(): otpk = base64.b64encode(otpk).decode("US-ASCII") pk_messages[otpk] = copy.deepcopy(value) return { "super" : super(X3DHDoubleRatchet, self).serialize(), "bound_otpks" : bound_otpks, "pk_messages" : pk_messages, "version" : __version__ } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): version = serialized["version"] # Add code to upgrade the state here self = super(X3DHDoubleRatchet, cls).fromSerialized( serialized["super"], *args, **kwargs ) bound_otpks = {} for bare_jid in serialized["bound_otpks"]: bound_otpks[bare_jid] = {} for device in serialized["bound_otpks"][bare_jid]: otpk = serialized["bound_otpks"][bare_jid][device] bound_otpks[bare_jid][device] = { "otpk" : base64.b64decode(otpk["otpk"].encode("US-ASCII")), "id" : otpk["id"] } pk_messages = {} for otpk, value in serialized["pk_messages"].items(): otpk = base64.b64decode(otpk.encode("US-ASCII")) pk_messages[otpk] = copy.deepcopy(value) self.__bound_otpks = bound_otpks self.__pre_key_messages = pk_messages return self def getSharedSecretActive( self, other_public_bundle, *args, **kwargs ): session_init_data = super(X3DHDoubleRatchet, self).getSharedSecretActive( other_public_bundle, *args, **kwargs ) # When actively initializing a session # - The shared secret becomes the root key # - The public SPK used for X3DH becomes the other's enc for the dh ratchet # - The associated data calculated by X3DH becomes the ad used by the # double ratchet encryption/decryption session_init_data["dr"] = self.__ExtendedDoubleRatchet( other_public_bundle.ik, session_init_data["ad"], session_init_data["sk"], own_key = None, other_pub = other_public_bundle.spk["key"] ) # The shared secret and ad values are now irrelevant del session_init_data["sk"] del session_init_data["ad"] self.__compressSessionInitData(session_init_data, other_public_bundle) return session_init_data def getSharedSecretPassive( self, session_init_data, bare_jid, device, otpk_policy, additional_information = None ): self.__decompressSessionInitData(session_init_data, bare_jid, device) self.__preKeyMessageReceived( session_init_data["otpk"], additional_information ) session_data = super(X3DHDoubleRatchet, self).getSharedSecretPassive( session_init_data, keep_otpk = True ) # Decide whether to keep this OTPK self.__decideBoundOTPK(bare_jid, device, otpk_policy) # When passively initializing the session # - The shared secret becomes the root key # - The public SPK used by the active part for X3DH becomes the own dh ratchet # key # - The associated data calculated by X3DH becomes the ad used by the double # ratchet encryption/decryption return self.__ExtendedDoubleRatchet( session_init_data["ik"], session_data["ad"], session_data["sk"], own_key = self.spk ) def __compressSessionInitData(self, session_init_data, bundle): """ Compress the session initialization data by replacing keys with their ids. """ to_other = session_init_data["to_other"] to_other["otpk_id"] = bundle.findOTPKId(to_other["otpk"]) to_other["spk_id"] = bundle.findSPKId(to_other["spk"]) del to_other["otpk"] del to_other["spk"] def __decompressSessionInitData(self, session_init_data, bare_jid, device): """ Decompress the session initialization data by replacing key ids with the keys. """ session_init_data["spk"] = self.getSPK(session_init_data["spk_id"]) del session_init_data["spk_id"] otpk_id = self.getBoundOTPKId(bare_jid, device) # Check, whether the bare_jid+device combination is already bound to some OTPK if otpk_id: # If it is, check whether the OTPK ids match if otpk_id == session_init_data["otpk_id"]: session_init_data["otpk"] = self.getBoundOTPK(bare_jid, device) # If the OTPK ids don't match, consider the old bound OTPK as deleteable # and bind the new OTPK else: self.deleteBoundOTPK(bare_jid, device) session_init_data["otpk"] = self.__bindOTPK( bare_jid, device, session_init_data["otpk_id"] ) else: # If it is not, get the OTPK from the id and bind the bare_jid+device # combination to it session_init_data["otpk"] = self.__bindOTPK( bare_jid, device, session_init_data["otpk_id"] ) del session_init_data["otpk_id"] def __preKeyMessageReceived(self, otpk, additional_information = None): # Add an entry to the received PreKeyMessage data self.__pre_key_messages[otpk] = self.__pre_key_messages.get(otpk, []) self.__pre_key_messages[otpk].append({ "timestamp": time.time(), "answers": [], "additional_information": additional_information, }) def getBoundOTPK(self, bare_jid, device): try: return self.__bound_otpks[bare_jid][device]["otpk"] except KeyError: return None def getBoundOTPKId(self, bare_jid, device): try: return self.__bound_otpks[bare_jid][device]["id"] except KeyError: return None def hasBoundOTPK(self, bare_jid, device): return True if self.getBoundOTPK(bare_jid, device) else False def respondedTo(self, bare_jid, device): bound_otpk_id = self.getBoundOTPK(bare_jid, device) self.__pre_key_messages[bound_otpk_id][-1]["answers"].append(time.time()) def __decideBoundOTPK(self, bare_jid, device, otpk_policy): bound_otpk_id = self.getBoundOTPK(bare_jid, device) if not otpk_policy.decideOTPK(self.__pre_key_messages[bound_otpk_id]): self.deleteBoundOTPK(bare_jid, device) def deleteBoundOTPK(self, bare_jid, device): otpk = self.getBoundOTPK(bare_jid, device) if otpk: del self.__pre_key_messages[otpk] del self.__bound_otpks[bare_jid][device] self.deleteOTPK(otpk) def __bindOTPK(self, bare_jid, device, otpk_id): try: otpk = self.getOTPK(otpk_id) except UnknownKeyException: raise KeyExchangeException( "The OTPK used for this session initialization has been deleted, " + "the session can not be initiated." ) self.__bound_otpks[bare_jid] = self.__bound_otpks.get(bare_jid, {}) self.__bound_otpks[bare_jid][device] = { "otpk": otpk, "id": otpk_id } self.__pre_key_messages[otpk] = [] self.hideFromPublicBundle(otpk) return otpk return X3DHDoubleRatchet OMEMO-0.10.3/setup.cfg0000644000175000017500000000004613411752554014255 0ustar useruser00000000000000[egg_info] tag_build = tag_date = 0 OMEMO-0.10.3/README.md0000644000175000017500000001657613411005737013724 0ustar useruser00000000000000[![PyPI](https://img.shields.io/pypi/v/OMEMO.svg)](https://pypi.org/project/OMEMO/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/OMEMO.svg)](https://pypi.org/project/OMEMO/) [![Build Status](https://travis-ci.org/Syndace/python-omemo.svg?branch=master)](https://travis-ci.org/Syndace/python-omemo) # python-omemo #### A Python implementation of the OMEMO Multi-End Message and Object Encryption protocol. This python library offers an open implementation of the OMEMO Multi-End Message and Object Encryption protocol as specified [here](https://xmpp.org/extensions/xep-0384.html). Goals of this implementation are: - Do not depend on libsignal but offer a solid alternative to it - Stay away from GPL (this repo will soon switch to MIT) - Be flexible to changes that might happen to the OMEMO protocol - Keep the structure close to the spec - Provide the parts of the protocol (X3DH, Double Ratchet) as own projects This library uses the [X3DH](https://github.com/Syndace/python-x3dh) and [DoubleRatchet](https://github.com/Syndace/python-doubleratchet) libraries, configures them with the parameters that OMEMO uses and manages all encryption sessions for you. This library does NOT manage XML/stanzas. ## Installation ### pip You can install this library and all of its dependencies via pip: ```Bash $ pip install OMEMO ``` ### AUR Ppjet6 kindly maintains AUR packages of both the current master and the latest release: | Release/Branch | Python Version | Link | |:-------------- |:--------------:|:----------------------------------------------------------------------------:| | current master | 2 | [*\*click\**](https://aur.archlinux.org/packages/python2-omemo-syndace/) | | latest release | 2 | [*\*click\**](https://aur.archlinux.org/packages/python2-omemo-syndace-git/) | | current master | 3 | [*\*click\**](https://aur.archlinux.org/packages/python-omemo-syndace/) | | latest release | 3 | [*\*click\**](https://aur.archlinux.org/packages/python-omemo-syndace-git/) | ## Usage ### Choose a backend To use this library you have to choose a backend first. Currently, you don't have a lot of choice: The only available backend is a backend offering libsignal compatibility, found [here](https://github.com/Syndace/python-omemo-backend-signal). Install your backend of choice and proceed to the next step. ### Implement the Storage interface The library has a lot of state/data that it has to persist between runs. To be as flexible as possible the library leaves it open for you to decide how to store the data. Simply implement the `Storage` interface found in `storage.py`. The file contains more info about how to implement the interface. ### Decide on a one-time pre key policy This part is kind of tricky as it requires a lot of knowledge about how the protocol works. Basically the key exchange mechanism used by the protocol assumes guaranteed message delivery and a response to the first message before another message is sent. Both conditions are not quite given in all environments, especially not in XMPP, which is the main use-case for this library. For that reason the library has to "relax" some of the protocols rules. Instead of always instantly deleting the keys used in the key exchange, it is now up to you to decide whether to keep keys or not. To do so, implement the `OTPKPolicy` interface found in `otpkpolicy.py` or use the default implementation `DefaultOTPKPolicy`. If you decide to implement the interface yourself, the `otpkpolicy.py` file contains more information on how to implement the interface. ### Create a SessionManager Now that you have selected a backend, decided on how to store the data and when to delete the key exchange keys, it's time to create an instance of the core class of this library: the SessionManager. The SessionManager handles message en- and decryption with all your contacts, trying to make it as easy as possible for you. The file `examples/sessions.py` contains a lot of well-commented code that shows how to create and use a SessionManager. ## Specific information for usage in XMPP/Jabber ### 1. Device list management #### 1.1. Device lists of your contacts The first thing you have to set up is the device list management. To do so, subscribe to (or in [XEP-0163](https://xmpp.org/extensions/xep-0163.html) speak: announce interest in) the "eu.siacs.conversations.axolotl.devicelist" node. You will now receive updates to the device lists of all your OMEMO-enabled contacts. Upon receiving such an update, pass the contained list into the "newDeviceList" method of your SessionManager. Some pseudocode: ```Python DEVICELIST_NODE = "eu.siacs.conversations.axolotl.devicelist" def __init__(): xep0163.announce_interest(DEVICELIST_NODE) def onPEPUpdate(node, item, sender_jid): if node == DEVICELIST_NODE: devices = unpackDeviceList(item) sessionMgr.newDeviceList(devices, sender_jid) ``` The SessionManager takes care of caching the device list and also remembers inactive devices for you. You can ask the SessionManager for stored device lists using the "getDevices" method. #### 1.2. Your own device list The next thing to set up is the management of you own device list. The rule is quite simple: always make sure, that your own device id is contained in your device list. Whenever you load your OMEMO-using software, download the device list of your own jid and make sure your own device id is contained. After following the steps in 1.1., you will now also receive PEP updates about changes to your own device list. Use these updates to assert that your own device id is still contained in the list. Some more pseudocode: ```Python def __init__(): own_device_list = xep0163.load_latest_entry(own_jid, DEVICELIST_NODE) manageOwnDeviceList(own_device_list) sessionMgr.newDeviceList(own_device_list, own_jid) def onPEPUpdate(node, item, sender_jid): if node == DEVICELIST_NODE: devices = unpackDeviceList(item) if sender_jid == own_jid: manageOwnDeviceList(devices) sessionMgr.newDeviceList(devices, sender_jid) def manageOwnDeviceList(devices): if not own_device in devices: devices.append(own_device) item = packDeviceList(devices) xep0163.publish(DEVICELIST_NODE, item) ``` ### 2. Bundle management The next thing you need to manager are the bundles used for the X3DH key exchange. Each device publishes its own bundle to a unique PEP node. **WIP** ### 3. Decryption ### 4. Encryption ### 5. A note about trust management ### 6. A note about fingerprints Fingerprints initially were part of the library but I decided to remove them. Fingerprints are not specified at all, that's why I leave it open for the client dev to decide on a way to build and show fingerprints. Some implementations simply take the public part of the identity key and show it as a QR-code or encoded as hex bytes. Pseudocode: ```Python # Get the ik public part from some bundle ik_pub = some_bundle.ik # Show a qr code somehow... showQRCode(ik_pub) # ...or create a hex byte representation # Wanted format: 01:23:45:67:89:AB:CD:EF # This is surprisingly tricky: import codecs import re ik_pub_hex = codecs.getencoder("hex")(ik_pub)[0].decode("US-ASCII").upper() ik_pub_hex = ":".join(re.findall("..?", ik_pub_hex)) ``` ### 7. A note about asynchronism OMEMO-0.10.3/PKG-INFO0000644000175000017500000002267613411752554013546 0ustar useruser00000000000000Metadata-Version: 2.1 Name: OMEMO Version: 0.10.3 Summary: A Python implementation of the OMEMO Multi-End Message and Object Encryption protocol. Home-page: https://github.com/Syndace/python-omemo Author: Tim Henkes Author-email: tim@cifg.io License: GPLv3 Description: [![PyPI](https://img.shields.io/pypi/v/OMEMO.svg)](https://pypi.org/project/OMEMO/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/OMEMO.svg)](https://pypi.org/project/OMEMO/) [![Build Status](https://travis-ci.org/Syndace/python-omemo.svg?branch=master)](https://travis-ci.org/Syndace/python-omemo) # python-omemo #### A Python implementation of the OMEMO Multi-End Message and Object Encryption protocol. This python library offers an open implementation of the OMEMO Multi-End Message and Object Encryption protocol as specified [here](https://xmpp.org/extensions/xep-0384.html). Goals of this implementation are: - Do not depend on libsignal but offer a solid alternative to it - Stay away from GPL (this repo will soon switch to MIT) - Be flexible to changes that might happen to the OMEMO protocol - Keep the structure close to the spec - Provide the parts of the protocol (X3DH, Double Ratchet) as own projects This library uses the [X3DH](https://github.com/Syndace/python-x3dh) and [DoubleRatchet](https://github.com/Syndace/python-doubleratchet) libraries, configures them with the parameters that OMEMO uses and manages all encryption sessions for you. This library does NOT manage XML/stanzas. ## Installation ### pip You can install this library and all of its dependencies via pip: ```Bash $ pip install OMEMO ``` ### AUR Ppjet6 kindly maintains AUR packages of both the current master and the latest release: | Release/Branch | Python Version | Link | |:-------------- |:--------------:|:----------------------------------------------------------------------------:| | current master | 2 | [*\*click\**](https://aur.archlinux.org/packages/python2-omemo-syndace/) | | latest release | 2 | [*\*click\**](https://aur.archlinux.org/packages/python2-omemo-syndace-git/) | | current master | 3 | [*\*click\**](https://aur.archlinux.org/packages/python-omemo-syndace/) | | latest release | 3 | [*\*click\**](https://aur.archlinux.org/packages/python-omemo-syndace-git/) | ## Usage ### Choose a backend To use this library you have to choose a backend first. Currently, you don't have a lot of choice: The only available backend is a backend offering libsignal compatibility, found [here](https://github.com/Syndace/python-omemo-backend-signal). Install your backend of choice and proceed to the next step. ### Implement the Storage interface The library has a lot of state/data that it has to persist between runs. To be as flexible as possible the library leaves it open for you to decide how to store the data. Simply implement the `Storage` interface found in `storage.py`. The file contains more info about how to implement the interface. ### Decide on a one-time pre key policy This part is kind of tricky as it requires a lot of knowledge about how the protocol works. Basically the key exchange mechanism used by the protocol assumes guaranteed message delivery and a response to the first message before another message is sent. Both conditions are not quite given in all environments, especially not in XMPP, which is the main use-case for this library. For that reason the library has to "relax" some of the protocols rules. Instead of always instantly deleting the keys used in the key exchange, it is now up to you to decide whether to keep keys or not. To do so, implement the `OTPKPolicy` interface found in `otpkpolicy.py` or use the default implementation `DefaultOTPKPolicy`. If you decide to implement the interface yourself, the `otpkpolicy.py` file contains more information on how to implement the interface. ### Create a SessionManager Now that you have selected a backend, decided on how to store the data and when to delete the key exchange keys, it's time to create an instance of the core class of this library: the SessionManager. The SessionManager handles message en- and decryption with all your contacts, trying to make it as easy as possible for you. The file `examples/sessions.py` contains a lot of well-commented code that shows how to create and use a SessionManager. ## Specific information for usage in XMPP/Jabber ### 1. Device list management #### 1.1. Device lists of your contacts The first thing you have to set up is the device list management. To do so, subscribe to (or in [XEP-0163](https://xmpp.org/extensions/xep-0163.html) speak: announce interest in) the "eu.siacs.conversations.axolotl.devicelist" node. You will now receive updates to the device lists of all your OMEMO-enabled contacts. Upon receiving such an update, pass the contained list into the "newDeviceList" method of your SessionManager. Some pseudocode: ```Python DEVICELIST_NODE = "eu.siacs.conversations.axolotl.devicelist" def __init__(): xep0163.announce_interest(DEVICELIST_NODE) def onPEPUpdate(node, item, sender_jid): if node == DEVICELIST_NODE: devices = unpackDeviceList(item) sessionMgr.newDeviceList(devices, sender_jid) ``` The SessionManager takes care of caching the device list and also remembers inactive devices for you. You can ask the SessionManager for stored device lists using the "getDevices" method. #### 1.2. Your own device list The next thing to set up is the management of you own device list. The rule is quite simple: always make sure, that your own device id is contained in your device list. Whenever you load your OMEMO-using software, download the device list of your own jid and make sure your own device id is contained. After following the steps in 1.1., you will now also receive PEP updates about changes to your own device list. Use these updates to assert that your own device id is still contained in the list. Some more pseudocode: ```Python def __init__(): own_device_list = xep0163.load_latest_entry(own_jid, DEVICELIST_NODE) manageOwnDeviceList(own_device_list) sessionMgr.newDeviceList(own_device_list, own_jid) def onPEPUpdate(node, item, sender_jid): if node == DEVICELIST_NODE: devices = unpackDeviceList(item) if sender_jid == own_jid: manageOwnDeviceList(devices) sessionMgr.newDeviceList(devices, sender_jid) def manageOwnDeviceList(devices): if not own_device in devices: devices.append(own_device) item = packDeviceList(devices) xep0163.publish(DEVICELIST_NODE, item) ``` ### 2. Bundle management The next thing you need to manager are the bundles used for the X3DH key exchange. Each device publishes its own bundle to a unique PEP node. **WIP** ### 3. Decryption ### 4. Encryption ### 5. A note about trust management ### 6. A note about fingerprints Fingerprints initially were part of the library but I decided to remove them. Fingerprints are not specified at all, that's why I leave it open for the client dev to decide on a way to build and show fingerprints. Some implementations simply take the public part of the identity key and show it as a QR-code or encoded as hex bytes. Pseudocode: ```Python # Get the ik public part from some bundle ik_pub = some_bundle.ik # Show a qr code somehow... showQRCode(ik_pub) # ...or create a hex byte representation # Wanted format: 01:23:45:67:89:AB:CD:EF # This is surprisingly tricky: import codecs import re ik_pub_hex = codecs.getencoder("hex")(ik_pub)[0].decode("US-ASCII").upper() ik_pub_hex = ":".join(re.findall("..?", ik_pub_hex)) ``` ### 7. A note about asynchronism Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Communications :: Chat Classifier: Topic :: Security :: Cryptography Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4 Description-Content-Type: text/markdown OMEMO-0.10.3/OMEMO.egg-info/0000755000175000017500000000000013411752554015002 5ustar useruser00000000000000OMEMO-0.10.3/OMEMO.egg-info/not-zip-safe0000644000175000017500000000000113405445116017224 0ustar useruser00000000000000 OMEMO-0.10.3/OMEMO.egg-info/SOURCES.txt0000644000175000017500000000223713411752554016672 0ustar useruser00000000000000README.md setup.py OMEMO.egg-info/PKG-INFO OMEMO.egg-info/SOURCES.txt OMEMO.egg-info/dependency_links.txt OMEMO.egg-info/not-zip-safe OMEMO.egg-info/requires.txt OMEMO.egg-info/top_level.txt omemo/__init__.py omemo/defaultotpkpolicy.py omemo/extendeddoubleratchet.py omemo/extendedpublicbundle.py omemo/otpkpolicy.py omemo/promise.py omemo/sessionmanager.py omemo/sessionmanagerasyncio.py omemo/state.py omemo/storage.py omemo/storagewrapper.py omemo/util.py omemo/version.py omemo/x3dhdoubleratchet.py omemo/backends/__init__.py omemo/backends/backend.py omemo/backends/wireformat.py omemo/backends/x3dhpkencoder.py omemo/exceptions/__init__.py omemo/exceptions/backendexception.py omemo/exceptions/encryptionproblemsexception.py omemo/exceptions/inconsistentinfoexception.py omemo/exceptions/keyexchangeexception.py omemo/exceptions/missingbundleexception.py omemo/exceptions/nodevicesexception.py omemo/exceptions/noeligibledevicesexception.py omemo/exceptions/nosessionexception.py omemo/exceptions/omemoexception.py omemo/exceptions/sessionmanagerexception.py omemo/exceptions/unknownkeyexception.py omemo/exceptions/untrustedexception.py omemo/exceptions/wireformatexception.pyOMEMO-0.10.3/OMEMO.egg-info/dependency_links.txt0000644000175000017500000000000113411752554021050 0ustar useruser00000000000000 OMEMO-0.10.3/OMEMO.egg-info/top_level.txt0000644000175000017500000000000613411752554017530 0ustar useruser00000000000000omemo OMEMO-0.10.3/OMEMO.egg-info/requires.txt0000644000175000017500000000004113411752554017375 0ustar useruser00000000000000X3DH<0.6,>=0.5.6 cryptography>=2 OMEMO-0.10.3/OMEMO.egg-info/PKG-INFO0000644000175000017500000002267613411752554016114 0ustar useruser00000000000000Metadata-Version: 2.1 Name: OMEMO Version: 0.10.3 Summary: A Python implementation of the OMEMO Multi-End Message and Object Encryption protocol. Home-page: https://github.com/Syndace/python-omemo Author: Tim Henkes Author-email: tim@cifg.io License: GPLv3 Description: [![PyPI](https://img.shields.io/pypi/v/OMEMO.svg)](https://pypi.org/project/OMEMO/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/OMEMO.svg)](https://pypi.org/project/OMEMO/) [![Build Status](https://travis-ci.org/Syndace/python-omemo.svg?branch=master)](https://travis-ci.org/Syndace/python-omemo) # python-omemo #### A Python implementation of the OMEMO Multi-End Message and Object Encryption protocol. This python library offers an open implementation of the OMEMO Multi-End Message and Object Encryption protocol as specified [here](https://xmpp.org/extensions/xep-0384.html). Goals of this implementation are: - Do not depend on libsignal but offer a solid alternative to it - Stay away from GPL (this repo will soon switch to MIT) - Be flexible to changes that might happen to the OMEMO protocol - Keep the structure close to the spec - Provide the parts of the protocol (X3DH, Double Ratchet) as own projects This library uses the [X3DH](https://github.com/Syndace/python-x3dh) and [DoubleRatchet](https://github.com/Syndace/python-doubleratchet) libraries, configures them with the parameters that OMEMO uses and manages all encryption sessions for you. This library does NOT manage XML/stanzas. ## Installation ### pip You can install this library and all of its dependencies via pip: ```Bash $ pip install OMEMO ``` ### AUR Ppjet6 kindly maintains AUR packages of both the current master and the latest release: | Release/Branch | Python Version | Link | |:-------------- |:--------------:|:----------------------------------------------------------------------------:| | current master | 2 | [*\*click\**](https://aur.archlinux.org/packages/python2-omemo-syndace/) | | latest release | 2 | [*\*click\**](https://aur.archlinux.org/packages/python2-omemo-syndace-git/) | | current master | 3 | [*\*click\**](https://aur.archlinux.org/packages/python-omemo-syndace/) | | latest release | 3 | [*\*click\**](https://aur.archlinux.org/packages/python-omemo-syndace-git/) | ## Usage ### Choose a backend To use this library you have to choose a backend first. Currently, you don't have a lot of choice: The only available backend is a backend offering libsignal compatibility, found [here](https://github.com/Syndace/python-omemo-backend-signal). Install your backend of choice and proceed to the next step. ### Implement the Storage interface The library has a lot of state/data that it has to persist between runs. To be as flexible as possible the library leaves it open for you to decide how to store the data. Simply implement the `Storage` interface found in `storage.py`. The file contains more info about how to implement the interface. ### Decide on a one-time pre key policy This part is kind of tricky as it requires a lot of knowledge about how the protocol works. Basically the key exchange mechanism used by the protocol assumes guaranteed message delivery and a response to the first message before another message is sent. Both conditions are not quite given in all environments, especially not in XMPP, which is the main use-case for this library. For that reason the library has to "relax" some of the protocols rules. Instead of always instantly deleting the keys used in the key exchange, it is now up to you to decide whether to keep keys or not. To do so, implement the `OTPKPolicy` interface found in `otpkpolicy.py` or use the default implementation `DefaultOTPKPolicy`. If you decide to implement the interface yourself, the `otpkpolicy.py` file contains more information on how to implement the interface. ### Create a SessionManager Now that you have selected a backend, decided on how to store the data and when to delete the key exchange keys, it's time to create an instance of the core class of this library: the SessionManager. The SessionManager handles message en- and decryption with all your contacts, trying to make it as easy as possible for you. The file `examples/sessions.py` contains a lot of well-commented code that shows how to create and use a SessionManager. ## Specific information for usage in XMPP/Jabber ### 1. Device list management #### 1.1. Device lists of your contacts The first thing you have to set up is the device list management. To do so, subscribe to (or in [XEP-0163](https://xmpp.org/extensions/xep-0163.html) speak: announce interest in) the "eu.siacs.conversations.axolotl.devicelist" node. You will now receive updates to the device lists of all your OMEMO-enabled contacts. Upon receiving such an update, pass the contained list into the "newDeviceList" method of your SessionManager. Some pseudocode: ```Python DEVICELIST_NODE = "eu.siacs.conversations.axolotl.devicelist" def __init__(): xep0163.announce_interest(DEVICELIST_NODE) def onPEPUpdate(node, item, sender_jid): if node == DEVICELIST_NODE: devices = unpackDeviceList(item) sessionMgr.newDeviceList(devices, sender_jid) ``` The SessionManager takes care of caching the device list and also remembers inactive devices for you. You can ask the SessionManager for stored device lists using the "getDevices" method. #### 1.2. Your own device list The next thing to set up is the management of you own device list. The rule is quite simple: always make sure, that your own device id is contained in your device list. Whenever you load your OMEMO-using software, download the device list of your own jid and make sure your own device id is contained. After following the steps in 1.1., you will now also receive PEP updates about changes to your own device list. Use these updates to assert that your own device id is still contained in the list. Some more pseudocode: ```Python def __init__(): own_device_list = xep0163.load_latest_entry(own_jid, DEVICELIST_NODE) manageOwnDeviceList(own_device_list) sessionMgr.newDeviceList(own_device_list, own_jid) def onPEPUpdate(node, item, sender_jid): if node == DEVICELIST_NODE: devices = unpackDeviceList(item) if sender_jid == own_jid: manageOwnDeviceList(devices) sessionMgr.newDeviceList(devices, sender_jid) def manageOwnDeviceList(devices): if not own_device in devices: devices.append(own_device) item = packDeviceList(devices) xep0163.publish(DEVICELIST_NODE, item) ``` ### 2. Bundle management The next thing you need to manager are the bundles used for the X3DH key exchange. Each device publishes its own bundle to a unique PEP node. **WIP** ### 3. Decryption ### 4. Encryption ### 5. A note about trust management ### 6. A note about fingerprints Fingerprints initially were part of the library but I decided to remove them. Fingerprints are not specified at all, that's why I leave it open for the client dev to decide on a way to build and show fingerprints. Some implementations simply take the public part of the identity key and show it as a QR-code or encoded as hex bytes. Pseudocode: ```Python # Get the ik public part from some bundle ik_pub = some_bundle.ik # Show a qr code somehow... showQRCode(ik_pub) # ...or create a hex byte representation # Wanted format: 01:23:45:67:89:AB:CD:EF # This is surprisingly tricky: import codecs import re ik_pub_hex = codecs.getencoder("hex")(ik_pub)[0].decode("US-ASCII").upper() ik_pub_hex = ":".join(re.findall("..?", ik_pub_hex)) ``` ### 7. A note about asynchronism Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Communications :: Chat Classifier: Topic :: Security :: Cryptography Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4 Description-Content-Type: text/markdown OMEMO-0.10.3/setup.py0000644000175000017500000000313513405226600014137 0ustar useruser00000000000000from setuptools import setup, find_packages import os import sys version_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "omemo", "version.py" ) version = {} try: execfile(version_file_path, version) except: with open(version_file_path) as fp: exec(fp.read(), version) with open("README.md") as f: long_description = f.read() setup( name = "OMEMO", version = version["__version__"], description = ( "A Python implementation of the OMEMO Multi-End Message and Object Encryption " + "protocol." ), long_description = long_description, long_description_content_type = "text/markdown", url = "https://github.com/Syndace/python-omemo", author = "Tim Henkes", author_email = "tim@cifg.io", license = "GPLv3", packages = find_packages(), install_requires = [ "X3DH>=0.5.6,<0.6", "cryptography>=2" ], python_requires = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", zip_safe = False, classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Topic :: Communications :: Chat", "Topic :: Security :: Cryptography", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ] )