X3DH-0.5.8/0000755000175000017500000000000013411750576012221 5ustar useruser00000000000000X3DH-0.5.8/X3DH.egg-info/0000755000175000017500000000000013411750576014421 5ustar useruser00000000000000X3DH-0.5.8/X3DH.egg-info/not-zip-safe0000644000175000017500000000000113405212117016632 0ustar useruser00000000000000 X3DH-0.5.8/X3DH.egg-info/SOURCES.txt0000644000175000017500000000076013411750576016310 0ustar useruser00000000000000README.md setup.py X3DH.egg-info/PKG-INFO X3DH.egg-info/SOURCES.txt X3DH.egg-info/dependency_links.txt X3DH.egg-info/not-zip-safe X3DH.egg-info/requires.txt X3DH.egg-info/top_level.txt x3dh/__init__.py x3dh/keypair.py x3dh/publicbundle.py x3dh/publickeyencoder.py x3dh/serializable.py x3dh/state.py x3dh/version.py x3dh/exceptions/__init__.py x3dh/exceptions/keyexchangeexception.py x3dh/exceptions/missingkeyexception.py x3dh/implementations/__init__.py x3dh/implementations/keypaircurve25519.pyX3DH-0.5.8/X3DH.egg-info/dependency_links.txt0000644000175000017500000000000113411750576020467 0ustar useruser00000000000000 X3DH-0.5.8/X3DH.egg-info/top_level.txt0000644000175000017500000000000513411750576017146 0ustar useruser00000000000000x3dh X3DH-0.5.8/X3DH.egg-info/requires.txt0000644000175000017500000000004713411750576017022 0ustar useruser00000000000000cryptography>=1.7.1 XEdDSA<0.5,>=0.4.5 X3DH-0.5.8/X3DH.egg-info/PKG-INFO0000644000175000017500000000362413411750576015523 0ustar useruser00000000000000Metadata-Version: 2.1 Name: X3DH Version: 0.5.8 Summary: A python implementation of the Extended Triple Diffie-Hellman key agreement protocol. Home-page: https://github.com/Syndace/python-x3dh Author: Tim Henkes Author-email: tim@cifg.io License: MIT Description: [![PyPI](https://img.shields.io/pypi/v/X3DH.svg)](https://pypi.org/project/X3DH/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/X3DH.svg)](https://pypi.org/project/X3DH/) [![Build Status](https://travis-ci.org/Syndace/python-x3dh.svg?branch=master)](https://travis-ci.org/Syndace/python-x3dh) # python-x3dh #### A python implementation of the Extended Triple Diffie-Hellman key agreement protocol. This python library offers an implementation of the Extended Triple Diffie-Hellman key agreement protocol (X3DH) as specified [here](https://signal.org/docs/specifications/x3dh/). The goal is to provide a configurable and independent implementation of the protocol, while keeping the structure close to the specification. This library was developed as part of [python-omemo](https://github.com/Syndace/python-omemo), a pretty cool end-to-end encryption protocol. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Communications :: Chat Classifier: Topic :: Security :: Cryptography Classifier: License :: OSI Approved :: MIT License 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 X3DH-0.5.8/setup.cfg0000644000175000017500000000004613411750576014042 0ustar useruser00000000000000[egg_info] tag_build = tag_date = 0 X3DH-0.5.8/README.md0000644000175000017500000000156413372774375013517 0ustar useruser00000000000000[![PyPI](https://img.shields.io/pypi/v/X3DH.svg)](https://pypi.org/project/X3DH/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/X3DH.svg)](https://pypi.org/project/X3DH/) [![Build Status](https://travis-ci.org/Syndace/python-x3dh.svg?branch=master)](https://travis-ci.org/Syndace/python-x3dh) # python-x3dh #### A python implementation of the Extended Triple Diffie-Hellman key agreement protocol. This python library offers an implementation of the Extended Triple Diffie-Hellman key agreement protocol (X3DH) as specified [here](https://signal.org/docs/specifications/x3dh/). The goal is to provide a configurable and independent implementation of the protocol, while keeping the structure close to the specification. This library was developed as part of [python-omemo](https://github.com/Syndace/python-omemo), a pretty cool end-to-end encryption protocol. X3DH-0.5.8/PKG-INFO0000644000175000017500000000362413411750576013323 0ustar useruser00000000000000Metadata-Version: 2.1 Name: X3DH Version: 0.5.8 Summary: A python implementation of the Extended Triple Diffie-Hellman key agreement protocol. Home-page: https://github.com/Syndace/python-x3dh Author: Tim Henkes Author-email: tim@cifg.io License: MIT Description: [![PyPI](https://img.shields.io/pypi/v/X3DH.svg)](https://pypi.org/project/X3DH/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/X3DH.svg)](https://pypi.org/project/X3DH/) [![Build Status](https://travis-ci.org/Syndace/python-x3dh.svg?branch=master)](https://travis-ci.org/Syndace/python-x3dh) # python-x3dh #### A python implementation of the Extended Triple Diffie-Hellman key agreement protocol. This python library offers an implementation of the Extended Triple Diffie-Hellman key agreement protocol (X3DH) as specified [here](https://signal.org/docs/specifications/x3dh/). The goal is to provide a configurable and independent implementation of the protocol, while keeping the structure close to the specification. This library was developed as part of [python-omemo](https://github.com/Syndace/python-omemo), a pretty cool end-to-end encryption protocol. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Communications :: Chat Classifier: Topic :: Security :: Cryptography Classifier: License :: OSI Approved :: MIT License 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 X3DH-0.5.8/x3dh/0000755000175000017500000000000013411750576013067 5ustar useruser00000000000000X3DH-0.5.8/x3dh/version.py0000644000175000017500000000002613411750550015114 0ustar useruser00000000000000__version__ = "0.5.8" X3DH-0.5.8/x3dh/exceptions/0000755000175000017500000000000013411750576015250 5ustar useruser00000000000000X3DH-0.5.8/x3dh/exceptions/__init__.py0000644000175000017500000000022413372774375017370 0ustar useruser00000000000000from __future__ import absolute_import from .keyexchangeexception import KeyExchangeException from .missingkeyexception import MissingKeyException X3DH-0.5.8/x3dh/exceptions/missingkeyexception.py0000644000175000017500000000005713372774375021736 0ustar useruser00000000000000class MissingKeyException(Exception): pass X3DH-0.5.8/x3dh/exceptions/keyexchangeexception.py0000644000175000017500000000006013372774375022041 0ustar useruser00000000000000class KeyExchangeException(Exception): pass X3DH-0.5.8/x3dh/__init__.py0000644000175000017500000000046513405211376015176 0ustar useruser00000000000000from __future__ import absolute_import from .version import __version__ from . import exceptions from . import implementations from .keypair import KeyPair from .publicbundle import PublicBundle from .publickeyencoder import PublicKeyEncoder from .serializable import Serializable from .state import State X3DH-0.5.8/x3dh/implementations/0000755000175000017500000000000013411750576016277 5ustar useruser00000000000000X3DH-0.5.8/x3dh/implementations/__init__.py0000644000175000017500000000013113372774375020414 0ustar useruser00000000000000from __future__ import absolute_import from .keypaircurve25519 import KeyPairCurve25519 X3DH-0.5.8/x3dh/implementations/keypaircurve25519.py0000644000175000017500000000742213411750511021762 0ustar useruser00000000000000from __future__ import absolute_import import base64 from ..exceptions import MissingKeyException from ..keypair import KeyPair from nacl.bindings import crypto_box_NONCEBYTES from nacl.bindings.crypto_scalarmult import crypto_scalarmult from nacl.public import Box from nacl.public import PrivateKey as Curve25519DecryptionKey from nacl.public import PublicKey as Curve25519EncryptionKey from nacl.utils import random from xeddsa.implementations import XEdDSA25519 class KeyPairCurve25519(KeyPair): """ An implementation of the KeyPair interface for Montgomery Curve25519 key pairs. """ def __init__(self, priv = None, pub = None): wrap = self.__class__.__wrap self.__priv = wrap(priv, Curve25519DecryptionKey) self.__pub = wrap(pub, Curve25519EncryptionKey) if self.__priv != None and self.__pub == None: self.__pub = self.__priv.public_key self.__priv_bytes = None if self.__priv == None else bytes(self.__priv) self.__pub_bytes = None if self.__pub == None else bytes(self.__pub) @classmethod def generate(cls): return cls(priv = XEdDSA25519.generate_mont_priv()) @staticmethod def __wrap(key, cls): if key == None: return None if isinstance(key, cls): return key return cls(key) def serialize(self): priv = self.priv pub = self.pub return { "super" : super(KeyPairCurve25519, self).serialize(), "priv" : None if priv == None else base64.b64encode(priv).decode("US-ASCII"), "pub" : None if pub == None else base64.b64encode(pub).decode("US-ASCII") } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): wrap = cls.__wrap self = super(KeyPairCurve25519, cls).fromSerialized( serialized["super"], *args, **kwargs ) priv = serialized["priv"] pub = serialized["pub"] priv = None if priv == None else base64.b64decode(priv.encode("US-ASCII")) pub = None if pub == None else base64.b64decode(pub.encode("US-ASCII")) self.__priv = wrap(priv, Curve25519DecryptionKey) self.__pub = wrap(pub, Curve25519EncryptionKey) self.__priv_bytes = None if self.__priv == None else bytes(self.__priv) self.__pub_bytes = None if self.__pub == None else bytes(self.__pub) return self @property def priv(self): return self.__priv_bytes @property def pub(self): return self.__pub_bytes def encrypt(self, data, other): return bytes(self.__getBox(other).encrypt(data, random(crypto_box_NONCEBYTES))) def decrypt(self, data, other): return bytes(self.__getBox(other).decrypt(data)) def __getBox(self, other): if self.__priv == None: raise MissingKeyException( "Cannot get a shared secret using this KeyPairCurve25519, private key " + "missing." ) if other.__pub == None: raise MissingKeyException( "Cannot get a shared secret using the other KeyPairCurve25519, public " + "key missing." ) return Box(self.__priv, other.__pub) def getSharedSecret(self, other): if self.__priv == None: raise MissingKeyException( "Cannot get a shared secret using this KeyPairCurve25519, private key " + "missing." ) if other.__pub == None: raise MissingKeyException( "Cannot get a shared secret using the other KeyPairCurve25519, public " + "key missing." ) return crypto_scalarmult( self.priv, other.pub ) X3DH-0.5.8/x3dh/state.py0000644000175000017500000004417213405211616014557 0ustar useruser00000000000000from __future__ import absolute_import import base64 from functools import wraps import os import time from .exceptions import KeyExchangeException from .implementations import KeyPairCurve25519 from .publicbundle import PublicBundle from .serializable import Serializable from xeddsa.implementations import XEdDSA25519 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.backends import default_backend def changes(f): @wraps(f) def _changes(*args, **kwargs): args[0]._changed = True return f(*args, **kwargs) return _changes class State(Serializable): """ The state is the core of the X3DH protocol. It manages a collection of key pairs and signatures and offers methods to do key exchanges with other parties. """ CRYPTOGRAPHY_BACKEND = default_backend() HASH_FUNCTIONS = { "SHA-256": hashes.SHA256, "SHA-512": hashes.SHA512 } def __init__( self, info_string, curve, hash_function, spk_timeout, min_num_otpks, max_num_otpks, public_key_encoder_class ): """ Prepare an X3DH state to provide asynchronous key exchange using a set of public keys called "public bundle". :param info_string: A bytes-like object encoding a string unique to this usage within the application. :param curve: The type of the curve. Allowed values: (the string) "25519" ("448" might follow soon). :param hash_function: The hash function to use. Allowed values: (the strings) "SHA-256" and "SHA-512". :param spk_timeout: Rotate the SPK after this amount of time in seconds. :param min_num_otpks: Minimum number of OTPKs that must always be available. :param max_num_otpks: Maximum number of OTPKs that may be available. :param public_key_encoder_class: A sub class of PublicKeyEncoder. """ if not isinstance(info_string, bytes): raise TypeError("Wrong type passed for the info_string parameter.") if not curve in [ "25519" ]: raise ValueError("Invalid value passed for the curve parameter.") if not hash_function in State.HASH_FUNCTIONS: raise ValueError("Invalid value passed for the hash_function parameter.") self.__info_string = info_string self.__curve = curve self.__hash_function = State.HASH_FUNCTIONS[hash_function] self.__spk_timeout = spk_timeout self.__min_num_otpks = min_num_otpks self.__max_num_otpks = max_num_otpks self.__PublicKeyEncoder = public_key_encoder_class # Load the configuration if self.__curve == "25519": self.__KeyPair = KeyPairCurve25519 self.__XEdDSA = XEdDSA25519 # Track whether this State has somehow changed since loading it # This can be used e.g. to republish the public bundle if something has changed self._changed = False # Keep a list of OTPKs that have been hidden from the public bundle. self.__hidden_otpks = [] self.__generateIK() self.__generateSPK() self.__generateOTPKs() ################# # serialization # ################# def serialize(self): spk = { "key" : self.__spk["key"].serialize(), "signature" : base64.b64encode(self.__spk["signature"]).decode("US-ASCII"), "timestamp" : self.__spk["timestamp"] } return { "changed" : self._changed, "ik" : self.__ik.serialize(), "spk" : spk, "otpks" : [ otpk.serialize() for otpk in self.__otpks ], "hidden_otpks" : [ otpk.serialize() for otpk in self.__hidden_otpks ] } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = cls(*args, **kwargs) parseKeyPair = self.__KeyPair.fromSerialized self._changed = serialized["changed"] self.__ik = parseKeyPair(serialized["ik"]) spk = serialized["spk"] self.__spk = { "key" : parseKeyPair(spk["key"]), "signature" : base64.b64decode(spk["signature"].encode("US-ASCII")), "timestamp" : spk["timestamp"] } self.__otpks = [ parseKeyPair(x) for x in serialized["otpks"] ] self.__hidden_otpks = [ parseKeyPair(x) for x in serialized["hidden_otpks"] ] return self ################## # key generation # ################## @changes def __generateIK(self): """ Generate an IK. This should only be done once. """ self.__ik = self.__KeyPair.generate() @changes def __generateSPK(self): """ Generate a new PK and sign its public key using the IK, add the timestamp aswell to allow for periodic rotations. """ key = self.__KeyPair.generate() key_serialized = self.__PublicKeyEncoder.encodePublicKey( key.pub, self.__curve ) signature = self.__XEdDSA(mont_priv = self.__ik.priv).sign(key_serialized) self.__spk = { "key": key, "signature": signature, "timestamp": time.time() } @changes def __generateOTPKs(self, num_otpks = None): """ Generate the given amount of OTPKs. :param num_otpks: Either an integer or None. If the value of num_otpks is None, set it to the max_num_otpks value of the configuration. """ if num_otpks == None: num_otpks = self.__max_num_otpks otpks = [] for _ in range(num_otpks): otpks.append(self.__KeyPair.generate()) try: self.__otpks.extend(otpks) except AttributeError: self.__otpks = otpks #################### # internal helpers # #################### def __kdf(self, secret_key_material): """ :param secret_key_material: A bytes-like object encoding the secret key material. :returns: A bytes-like object encoding the shared secret key. """ salt = b"\x00" * self.__hash_function().digest_size if self.__curve == "25519": input_key_material = b"\xFF" * 32 if self.__curve == "448": input_key_material = b"\xFF" * 57 input_key_material += secret_key_material hkdf = HKDF( algorithm=self.__hash_function(), length=32, salt=salt, info=self.__info_string, backend=self.__class__.CRYPTOGRAPHY_BACKEND ) return hkdf.derive(input_key_material) ################## # key management # ################## def __checkSPKTimestamp(self): """ Check whether the SPK is too old and generate a new one in that case. """ if time.time() - self.__spk["timestamp"] > self.__spk_timeout: self.__generateSPK() def __refillOTPKs(self): """ If the amount of available OTPKs fell under the minimum, refills the OTPKs up to the maximum limit again. """ remainingOTPKs = len(self.__otpks) if remainingOTPKs < self.__min_num_otpks: self.__generateOTPKs(self.__max_num_otpks - remainingOTPKs) @changes def hideFromPublicBundle(self, otpk_pub): """ Hide a one-time pre key from the public bundle. :param otpk_pub: The public key of the one-time pre key to hide, encoded as a bytes-like object. """ self.__checkSPKTimestamp() for otpk in self.__otpks: if otpk.pub == otpk_pub: self.__otpks.remove(otpk) self.__hidden_otpks.append(otpk) self.__refillOTPKs() @changes def deleteOTPK(self, otpk_pub): """ Delete a one-time pre key, either publicly visible or hidden. :param otpk_pub: The public key of the one-time pre key to delete, encoded as a bytes-like object. """ self.__checkSPKTimestamp() for otpk in self.__otpks: if otpk.pub == otpk_pub: self.__otpks.remove(otpk) for otpk in self.__hidden_otpks: if otpk.pub == otpk_pub: self.__hidden_otpks.remove(otpk) self.__refillOTPKs() ############################ # public bundle management # ############################ def getPublicBundle(self): """ Fill a PublicBundle object with the public bundle data of this State. :returns: An instance of PublicBundle, filled with the public data of this State. """ self.__checkSPKTimestamp() ik_pub = self.__ik.pub spk_pub = self.__spk["key"].pub spk_sig = self.__spk["signature"] otpk_pubs = [ otpk.pub for otpk in self.__otpks ] return PublicBundle(ik_pub, spk_pub, spk_sig, otpk_pubs) @property def changed(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. """ self.__checkSPKTimestamp() changed = self._changed self._changed = False return changed ################ # key exchange # ################ def getSharedSecretActive( self, other_public_bundle, allow_zero_otpks = False ): """ Do the key exchange, as the active party. This involves selecting keys from the passive parties' public bundle. :param other_public_bundle: An instance of PublicBundle, filled with the public data of the passive party. :param allow_zero_otpks: A flag indicating whether bundles with no one-time pre keys are allowed or throw an error. False is the recommended default. :returns: A dictionary containing the shared secret, the shared associated data and the data the passive party needs to finalize the key exchange. The returned structure looks like this:: { "to_other": { # The public key of the active parties' identity key pair "ik": bytes, # The public key of the active parties' ephemeral key pair "ek": bytes, # The public key of the used passive parties' one-time pre key or None "otpk": bytes or None, # The public key of the passive parties' signed pre key pair "spk": bytes }, "ad": bytes, # The shared associated data "sk": bytes # The shared secret } :raises KeyExchangeException: If an error occurs during the key exchange. The exception message will contain (human-readable) details. """ self.__checkSPKTimestamp() other_ik = self.__KeyPair(pub = other_public_bundle.ik) other_spk = { "key": self.__KeyPair(pub = other_public_bundle.spk), "signature": other_public_bundle.spk_signature } other_otpks = [ self.__KeyPair(pub = otpk) for otpk in other_public_bundle.otpks ] if len(other_otpks) == 0 and not allow_zero_otpks: raise KeyExchangeException( "The other public bundle does not contain any OTPKs, which is not " + "allowed." ) other_spk_serialized = self.__PublicKeyEncoder.encodePublicKey( other_spk["key"].pub, self.__curve ) if not self.__XEdDSA(mont_pub = other_ik.pub).verify( other_spk_serialized, other_spk["signature"] ): raise KeyExchangeException( "The signature of this public bundle's spk could not be verifified." ) ek = self.__KeyPair.generate() dh1 = self.__ik.getSharedSecret(other_spk["key"]) dh2 = ek.getSharedSecret(other_ik) dh3 = ek.getSharedSecret(other_spk["key"]) dh4 = b"" otpk = None if len(other_otpks) > 0: otpk_index = ord(os.urandom(1)) % len(other_otpks) otpk = other_otpks[otpk_index] dh4 = ek.getSharedSecret(otpk) sk = self.__kdf(dh1 + dh2 + dh3 + dh4) ik_pub_serialized = self.__PublicKeyEncoder.encodePublicKey( self.__ik.pub, self.__curve ) other_ik_pub_serialized = self.__PublicKeyEncoder.encodePublicKey( other_ik.pub, self.__curve ) ad = ik_pub_serialized + other_ik_pub_serialized return { "to_other": { "ik": self.__ik.pub, "ek": ek.pub, "otpk": otpk.pub if otpk else None, "spk": other_spk["key"].pub }, "ad": ad, "sk": sk } def getSharedSecretPassive( self, passive_exchange_data, allow_no_otpk = False, keep_otpk = False ): """ Do the key exchange, as the passive party. This involves retrieving data about the key exchange from the active party. :param passive_exchange_data: A structure generated by the active party, which contains data requried to complete the key exchange. See the "to_other" part of the structure returned by "getSharedSecretActive". :param allow_no_otpk: A boolean indicating whether to allow key exchange, even if the active party did not use a one-time pre key. The recommended default is False. :param keep_otpk: Keep the one-time pre key after using it, instead of deleting it. See the notes below. :returns: A dictionary containing the shared secret and the shared associated data. The returned structure looks like this:: { "ad": bytes, # The shared associated data "sk": bytes # The shared secret } The specification of X3DH dictates to delete one-time pre keys as soon as they are used. This behaviour provides security but may lead to considerable usability downsides in some environments. For that reason the keep_otpk flag exists. If set to True, the one-time pre key is not automatically deleted. USE WITH CARE, THIS MAY INTRODUCE SECURITY LEAKS IF USED INCORRECTLY. If you decide to set the flag and to keep the otpks, you have to manage deleting them yourself, e.g. by subclassing this class and overriding this method. :raises KeyExchangeException: If an error occurs during the key exchange. The exception message will contain (human-readable) details. """ self.__checkSPKTimestamp() other_ik = self.__KeyPair(pub = passive_exchange_data["ik"]) other_ek = self.__KeyPair(pub = passive_exchange_data["ek"]) if self.__spk["key"].pub != passive_exchange_data["spk"]: raise KeyExchangeException( "The SPK used for this key exchange has been rotated, the key exchange " + "can not be completed." ) my_otpk = None if "otpk" in passive_exchange_data: for otpk in self.__otpks: if otpk.pub == passive_exchange_data["otpk"]: my_otpk = otpk break for otpk in self.__hidden_otpks: if otpk.pub == passive_exchange_data["otpk"]: my_otpk = otpk break if not my_otpk: raise KeyExchangeException( "The OTPK used for this key exchange has been deleted, the key " + "exchange can not be completed." ) elif not allow_no_otpk: raise KeyExchangeException( "This key exchange data does not contain an OTPK, which is not allowed." ) dh1 = self.__spk["key"].getSharedSecret(other_ik) dh2 = self.__ik.getSharedSecret(other_ek) dh3 = self.__spk["key"].getSharedSecret(other_ek) dh4 = b"" if my_otpk: dh4 = my_otpk.getSharedSecret(other_ek) sk = self.__kdf(dh1 + dh2 + dh3 + dh4) other_ik_pub_serialized = self.__PublicKeyEncoder.encodePublicKey( other_ik.pub, self.__curve ) ik_pub_serialized = self.__PublicKeyEncoder.encodePublicKey( self.__ik.pub, self.__curve ) ad = other_ik_pub_serialized + ik_pub_serialized if my_otpk and not keep_otpk: self.deleteOTPK(my_otpk.pub) return { "ad": ad, "sk": sk } @property def spk(self): """ :returns: The signed pre key pair as an instance of KeyPair. """ self.__checkSPKTimestamp() return self.__spk["key"] @property def spk_signature(self): """ :returns: The signature that was created using the identity key to sign the encoded public key of the signed pre key pair. The signature is encoded as a bytes-like object. """ self.__checkSPKTimestamp() return self.__spk["signature"] @property def ik(self): """ :returns: The identity key pair as an instance of KeyPair. """ self.__checkSPKTimestamp() return self.__ik @property def otpks(self): """ :returns: A list of all public one-time pre keys, as instances of KeyPair. """ self.__checkSPKTimestamp() return self.__otpks @property def hidden_otpks(self): """ :returns: A list of all hidden one-time pre keys, as instances of KeyPair. """ self.__checkSPKTimestamp() return self.__hidden_otpks X3DH-0.5.8/x3dh/serializable.py0000644000175000017500000000176713405211346016110 0ustar useruser00000000000000class Serializable(object): def serialize(self): """ :returns: A serializable Python structure, which contains all the state information of this object. Use together with the fromSerialized method. Here, "serializable" means, that the structure consists of any combination of the following types: * dictionaries * lists * strings * integers * floats * booleans * None """ return None @classmethod def fromSerialized(cls, serialized, *args, **kwargs): """ :param serialized: A serializable Python object. :returns: Return a new instance that was set to the state that was saved into the serialized object. Use together with the serialize method. Notice: You have to pass all positional parameters required by the constructor of the class you call fromSerialized on. """ return cls(*args, **kwargs) X3DH-0.5.8/x3dh/keypair.py0000644000175000017500000000566213405211446015105 0ustar useruser00000000000000from __future__ import absolute_import from .serializable import Serializable class KeyPair(Serializable): """ The interface of a key pair. A key pair is a pair consisting of a private and a public key used for en- and decryption. """ def __init__(self, priv = None, pub = None): """ Initiate a KeyPair instance using the key information provided as parameters. :param priv: The private key as a bytes-like object or None. :param pub: The public key as a bytes-like object or None. """ raise NotImplementedError @classmethod def generate(cls): """ :returns: A new key pair with private and public key set. """ raise NotImplementedError @property def priv(self): """ :returns: A bytes-like object encoding the private key of this key pair instance. """ raise NotImplementedError @property def pub(self): """ :returns: A bytes-like object encoding the public key of this key pair instance. """ raise NotImplementedError def encrypt(self, data, other): """ Encrypt given data using the private key stored by this KeyPair instance, for the public key stored by the other instance. :param data: The data to encrypt. A bytes-like object. :param other: An instance of the KeyPair class. The public key to encrypt the data for. :returns: The encrypted data. :raises MissingKeyException: If any key is missing to complete this operation. The exception message will contain (human-readable) details. """ raise NotImplementedError def decrypt(self, data, other): """ Decrypt the encrypted data using the private key stored by this KeyPair instance, for the public key stored by the other instance. :param data: The data to decrypt. A bytes-like object. :param other: An instance of the KeyPair class. The public key to decrypt the data from. :returns: The decrypted plain data. :raises MissingKeyException: If any key is missing to complete this operation. The exception message will contain (human-readable) details. """ raise NotImplementedError def getSharedSecret(self, other): """ Get a shared secret between the keys stored by this instance and the keys stored by the other instance. The shared secrets are generated, so that following equation is True: :: shared_secret(A.priv, B.pub) == shared_secret(B.priv, A.pub) :param other: An instance of the KeyPair class. :returns: The shared secret, as a bytes-like object. :raises MissingKeyException: If any key is missing to complete this operation. The exception message will contain (human-readable) details. """ raise NotImplementedError X3DH-0.5.8/x3dh/publicbundle.py0000644000175000017500000000356313372774375016131 0ustar useruser00000000000000class PublicBundle(object): """ The public bundle is an important part of the X3DH protocol. It contains a collection of public keys and signatures, that each party somehow has to publish. Other parties can use the contents of this bundle to initiate the key exchange. """ def __init__(self, ik, spk, spk_signature, otpks): """ Create a new public bundle. :param ik: The public key of the identity key pair, encoded as a bytes-like object. :param spk: The public key of the signed pre key pair, encoded as a bytes-like object. :param spk_signature: A bytes-like object encoding the signature, that was created by signing the public key of the signed pre key pair using the identity key. :param otpks: A list of public keys, one for each one-time pre key pair, each encoded as a bytes-like object. """ self.__ik = ik self.__spk = spk self.__spk_signature = spk_signature self.__otpks = otpks @property def ik(self): """ :returns: The public key of the identity key pair, encoded as a bytes-like object. """ return self.__ik @property def spk(self): """ :returns: The public key of the signed pre key pair, encoded as a bytes-like object. """ return self.__spk @property def spk_signature(self): """ :returns: A bytes-like object encoding the signature, that was created by signing the public key of the signed pre key pair using the identity key. """ return self.__spk_signature @property def otpks(self): """ :returns: A list of public keys, one for each one-time pre key pair, each encoded as a bytes-like object. """ return self.__otpks X3DH-0.5.8/x3dh/publickeyencoder.py0000644000175000017500000000106513372774375017003 0ustar useruser00000000000000class PublicKeyEncoder(object): @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 X3DH-0.5.8/setup.py0000644000175000017500000000306013402762072013724 0ustar useruser00000000000000from setuptools import setup, find_packages import os import sys version_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "x3dh", "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 = "X3DH", version = version["__version__"], description = ( "A python implementation of the Extended Triple Diffie-Hellman key agreement " + "protocol." ), long_description = long_description, long_description_content_type = "text/markdown", url = "https://github.com/Syndace/python-x3dh", author = "Tim Henkes", author_email = "tim@cifg.io", license = "MIT", packages = find_packages(), install_requires = [ "cryptography>=1.7.1", "XEdDSA>=0.4.5,<0.5" ], 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 :: MIT License", "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" ] )