DoubleRatchet-0.6.0/0000755000175000017500000000000013405304506014220 5ustar useruser00000000000000DoubleRatchet-0.6.0/DoubleRatchet.egg-info/0000755000175000017500000000000013405304506020437 5ustar useruser00000000000000DoubleRatchet-0.6.0/DoubleRatchet.egg-info/not-zip-safe0000644000175000017500000000000113405225377022675 0ustar useruser00000000000000 DoubleRatchet-0.6.0/DoubleRatchet.egg-info/SOURCES.txt0000644000175000017500000000215313405304506022324 0ustar useruser00000000000000README.md setup.py DoubleRatchet.egg-info/PKG-INFO DoubleRatchet.egg-info/SOURCES.txt DoubleRatchet.egg-info/dependency_links.txt DoubleRatchet.egg-info/not-zip-safe DoubleRatchet.egg-info/requires.txt DoubleRatchet.egg-info/top_level.txt doubleratchet/__init__.py doubleratchet/aead.py doubleratchet/header.py doubleratchet/kdf.py doubleratchet/keypair.py doubleratchet/serializable.py doubleratchet/version.py doubleratchet/exceptions/__init__.py doubleratchet/exceptions/authenticationfailedexception.py doubleratchet/exceptions/missingkeyexception.py doubleratchet/exceptions/notinitializedexception.py doubleratchet/exceptions/toomanysavedmessagekeysexception.py doubleratchet/kdfchains/__init__.py doubleratchet/kdfchains/constkdfchain.py doubleratchet/kdfchains/kdfchain.py doubleratchet/ratchets/__init__.py doubleratchet/ratchets/dhratchet.py doubleratchet/ratchets/doubleratchet.py doubleratchet/ratchets/ratchet.py doubleratchet/ratchets/symmetrickeyratchet.py doubleratchet/recommended/__init__.py doubleratchet/recommended/cbchmacaead.py doubleratchet/recommended/chainkeykdf.py doubleratchet/recommended/rootkeykdf.pyDoubleRatchet-0.6.0/DoubleRatchet.egg-info/dependency_links.txt0000644000175000017500000000000113405304506024505 0ustar useruser00000000000000 DoubleRatchet-0.6.0/DoubleRatchet.egg-info/top_level.txt0000644000175000017500000000001613405304506023166 0ustar useruser00000000000000doubleratchet DoubleRatchet-0.6.0/DoubleRatchet.egg-info/requires.txt0000644000175000017500000000002413405304506023033 0ustar useruser00000000000000cryptography>=1.7.1 DoubleRatchet-0.6.0/DoubleRatchet.egg-info/PKG-INFO0000644000175000017500000000366413405304506021545 0ustar useruser00000000000000Metadata-Version: 2.1 Name: DoubleRatchet Version: 0.6.0 Summary: A python implementation of the Double Ratchet algorithm. Home-page: https://github.com/Syndace/python-doubleratchet Author: Tim Henkes Author-email: tim@cifg.io License: MIT Description: [![PyPI](https://img.shields.io/pypi/v/DoubleRatchet.svg)](https://pypi.org/project/DoubleRatchet/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/DoubleRatchet.svg)](https://pypi.org/project/DoubleRatchet/) [![Build Status](https://travis-ci.org/Syndace/python-doubleratchet.svg?branch=master)](https://travis-ci.org/Syndace/python-doubleratchet) # python-doubleratchet #### A python implementation of the Double Ratchet algorithm. This python library offers an implementation of the Double Ratchet algorithm as specified [here](https://signal.org/docs/specifications/doubleratchet/). The goal is to provide a configurable and independent implementation of the algorithm, while keeping the structure close to the specification and providing recommended settings. 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 DoubleRatchet-0.6.0/doubleratchet/0000755000175000017500000000000013405304506017045 5ustar useruser00000000000000DoubleRatchet-0.6.0/doubleratchet/aead.py0000644000175000017500000000243613372774414020332 0ustar useruser00000000000000class AEAD(object): """ Authenticated Encryption with Associated Data (AEAD). """ def encrypt(self, plaintext, key, ad): """ Encrypt given plaintext using given key and authenticating using given associated data. :param plaintext: A bytes-like object encoding the data to encrypt. :param key: A bytes-like object encoding the key to encrypt with. :param ad: A bytes-like object encoding the associated data to authenticate with. :returns: A bytes-like object encoding the encrypted data (the ciphertext). """ raise NotImplementedError def decrypt(self, ciphertext, key, ad): """ Decrypt given ciphertext using given key and check validity of the authentication using given associated data. :param ciphertext: A bytes-like object encoding the data to decrypt. :param key: A bytes-like object encoding the key to decrypt with. :param ad: A bytes-like object encoding the associated data to authenticate with. :returns: A bytes-like object encoding the decrypted data (the plaintext). :raises AuthenticationFailedException: If the message could not be authenticated using the associated data. """ raise NotImplementedError DoubleRatchet-0.6.0/doubleratchet/kdf.py0000644000175000017500000000110413372774414020173 0ustar useruser00000000000000class KDF(object): """ A key derivation function. """ def calculate(self, key, data, length): """ Use the key and the given input data to derive an output key of given length. :param key: A bytes-like object encoding the key to use for derivation. :param data: A bytes-like object encoding the data to use for derivation. :param length: The length of the key to derive, as an integer. :returns: A bytes-like object with the requested length encoding the derived key. """ raise NotImplementedError DoubleRatchet-0.6.0/doubleratchet/version.py0000644000175000017500000000002613405217606021106 0ustar useruser00000000000000__version__ = "0.6.0" DoubleRatchet-0.6.0/doubleratchet/exceptions/0000755000175000017500000000000013405304506021226 5ustar useruser00000000000000DoubleRatchet-0.6.0/doubleratchet/exceptions/authenticationfailedexception.py0000644000175000017500000000007113372774414027715 0ustar useruser00000000000000class AuthenticationFailedException(Exception): pass DoubleRatchet-0.6.0/doubleratchet/exceptions/toomanysavedmessagekeysexception.py0000644000175000017500000000007413372774414030506 0ustar useruser00000000000000class TooManySavedMessageKeysException(Exception): pass DoubleRatchet-0.6.0/doubleratchet/exceptions/__init__.py0000644000175000017500000000046213372774414023355 0ustar useruser00000000000000from __future__ import absolute_import from .authenticationfailedexception import AuthenticationFailedException from .missingkeyexception import MissingKeyException from .notinitializedexception import NotInitializedException from .toomanysavedmessagekeysexception import TooManySavedMessageKeysException DoubleRatchet-0.6.0/doubleratchet/exceptions/missingkeyexception.py0000644000175000017500000000005713372774414025717 0ustar useruser00000000000000class MissingKeyException(Exception): pass DoubleRatchet-0.6.0/doubleratchet/exceptions/notinitializedexception.py0000644000175000017500000000006313372774414026560 0ustar useruser00000000000000class NotInitializedException(Exception): pass DoubleRatchet-0.6.0/doubleratchet/kdfchains/0000755000175000017500000000000013405304506020777 5ustar useruser00000000000000DoubleRatchet-0.6.0/doubleratchet/kdfchains/kdfchain.py0000644000175000017500000000441613405170271023125 0ustar useruser00000000000000from __future__ import absolute_import from ..serializable import Serializable import base64 class KDFChain(Serializable): """ A key derivation function chain. A KDFChain is initialized with some data and a KDF, which are stored internally. KDFChains provide a "next" method, which takes some input data and uses this input data, the internally stored data and the KDF to derive new output data. One part of the output data becomes the new internally stored data, overriding the previously stored data. The other part becomes the output of the step. Because key derivation is a one-way process, KDFChains can move forward but never backward. """ def __init__(self, kdf, key): """ Initialize a KDFChain using the provided key derivation function and key. :param kdf: An instance of the KDF interface. :param key: A bytes-like object encoding the key to supply to the key derivation function. """ self.__length = 0 self.__kdf = kdf self.__key = key def serialize(self): return { "length" : self.__length, "key" : base64.b64encode(self.__key).decode("US-ASCII") } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = cls(*args, **kwargs) self.__length = serialized["length"] self.__key = base64.b64decode(serialized["key"].encode("US-ASCII")) return self def next(self, data): """ Derive a new set of internal and output data from given input data and the data stored internally. Use the key derivation function to derive new data. The kdf gets supplied with the current key and the data passed to this method. :param data: A bytes-like object encoding the data to pass to the key derivation function. :returns: A bytes-like object encoding the output material. """ self.__length += 1 result = self.__kdf.calculate(self.__key, data, 64) self.__key = result[:32] return result[32:] @property def length(self): """ :returns: The number of calls to the "next" method since initializing the chain. """ return self.__length DoubleRatchet-0.6.0/doubleratchet/kdfchains/constkdfchain.py0000644000175000017500000000215113405225575024176 0ustar useruser00000000000000from __future__ import absolute_import from .kdfchain import KDFChain class ConstKDFChain(KDFChain): """ An implementation of the Chain interface that uses a key derivation function to provide the chain step mechanism. In contrast to the KDFChain implementation, this implementation passes the same constant data to the key derivation function on every call to next. """ def __init__(self, constant, *args, **kwargs): """ Initialize a ConstKDFChain, which uses constant input data instead of passed data on chains steps. :param constant: The constant data to pass to the next method on each step. """ super(ConstKDFChain, self).__init__(*args, **kwargs) self.__constant = constant def next(self, data = None): """ Use the key derivation function to derive new data. The kdf gets supplied with the current key and the constant input data set using the constructor. :returns: A bytes-like object encoding the output material. """ return super(ConstKDFChain, self).next(self.__constant) DoubleRatchet-0.6.0/doubleratchet/kdfchains/__init__.py0000644000175000017500000000016013372774414023121 0ustar useruser00000000000000from __future__ import absolute_import from .constkdfchain import ConstKDFChain from .kdfchain import KDFChain DoubleRatchet-0.6.0/doubleratchet/ratchets/0000755000175000017500000000000013405304506020662 5ustar useruser00000000000000DoubleRatchet-0.6.0/doubleratchet/ratchets/symmetrickeyratchet.py0000644000175000017500000001210513405225360025333 0ustar useruser00000000000000from __future__ import absolute_import from ..exceptions import NotInitializedException from .ratchet import Ratchet class SymmetricKeyRatchet(Ratchet): """ An implementation of the Ratchet interface, which internally manages two chains: A chain to derive sending keys and a chain to derive receiving keys. The ratchet step alternately replaces the sending and the receiving chains with new ones. """ def __init__(self, sending_chain_class, receiving_chain_class): """ Initialize a new SymmetricKeyRatchet. :param sending_chain_class: An implementation of the Chain interface to be used for the sending chains. :param receiving_chain_class: An implementations of the Chain interface to be used for the receiving chains. """ super(SymmetricKeyRatchet, self).__init__() self.__SendingChain = sending_chain_class self.__ReceivingChain = receiving_chain_class self.__sending_chain = None self.__receiving_chain = None self.__previous_sending_chain_length = None def serialize(self): sending_chain = self.__sending_chain sending_chain = None if sending_chain == None else sending_chain.serialize() receiving_chain = self.__receiving_chain receiving_chain = None if receiving_chain == None else receiving_chain.serialize() return { "super" : super(SymmetricKeyRatchet, self).serialize(), "schain" : sending_chain, "rchain" : receiving_chain, "prev_schain_length" : self.__previous_sending_chain_length } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = super(SymmetricKeyRatchet, cls).fromSerialized( serialized["super"], *args, **kwargs ) if serialized["schain"] != None: self.__sending_chain = self.__SendingChain.fromSerialized( serialized["schain"], None ) if serialized["rchain"] != None: self.__receiving_chain = self.__ReceivingChain.fromSerialized( serialized["rchain"], None ) self.__previous_sending_chain_length = serialized["prev_schain_length"] return self def step(self, key, chain): """ Perform a rachted step, replacing one of the internally managed chains with a new one. :param key: A bytes-like object encoding the key to initialize the replacement chain with. :param chain: The chain to replace. This parameter must be one of the two strings "sending" and "receiving". """ if chain == "sending": self.__previous_sending_chain_length = self.sending_chain_length self.__sending_chain = self.__SendingChain(key) if chain == "receiving": self.__receiving_chain = self.__ReceivingChain(key) @property def previous_sending_chain_length(self): """ Get the length of the previous sending chain. :returns: Either an integer representing the length of the previous sending chain or None, if the current one is the first sending chain. """ return self.__previous_sending_chain_length @property def sending_chain_length(self): """ Get the length of the current sending chain. :returns: Either an integer representing the length of the current sending chain or None, if there is no sending chain. """ return None if self.__sending_chain == None else self.__sending_chain.length @property def receiving_chain_length(self): """ Get the length of the receiving chain. :returns: Either an integer representing the length of the receiving chain or None, if there is no receiving chain. """ return None if self.__receiving_chain == None else self.__receiving_chain.length def nextEncryptionKey(self): """ Use the sending chain to derive the next encryption key. :returns: A bytes-like object encoding the next encryption key. :raises NotInitializedException: If there is no sending chain yet. """ if self.__sending_chain == None: raise NotInitializedException( "Can not get the next encryption key from the symmetric key ratchet, " + "there is no sending chain yet." ) return self.__sending_chain.next() def nextDecryptionKey(self): """ Use the receiving chain to derive the next decryption key. :returns: A bytes-like object encoding the next decryption key. :raises NotInitializedException: If there is no receiving chain yet. """ if self.__receiving_chain == None: raise NotInitializedException( "Can not get the next decryption key from the symmetric key ratchet, " + "there is no receiving chain yet." ) return self.__receiving_chain.next() DoubleRatchet-0.6.0/doubleratchet/ratchets/ratchet.py0000644000175000017500000000115413405170304022664 0ustar useruser00000000000000from __future__ import absolute_import from ..serializable import Serializable class Ratchet(Serializable): """ A cryptographic ratchet. A ratchet manages some sort of internal state and offers a "step" method, which uses the current state and optional additional data to derive the next state. The new state overrides the old state. The derivation must be a one-way process, so that the ratchet can't go back to a previous state. """ def step(self, *args, **kwargs): """ Perform a ratchet step using provided arguments. """ raise NotImplementedError DoubleRatchet-0.6.0/doubleratchet/ratchets/__init__.py0000644000175000017500000000030413372774414023004 0ustar useruser00000000000000from __future__ import absolute_import from .dhratchet import DHRatchet from .doubleratchet import DoubleRatchet from .ratchet import Ratchet from .symmetrickeyratchet import SymmetricKeyRatchet DoubleRatchet-0.6.0/doubleratchet/ratchets/doubleratchet.py0000644000175000017500000002134413405221731024063 0ustar useruser00000000000000from __future__ import absolute_import from .dhratchet import DHRatchet from ..exceptions import TooManySavedMessageKeysException from ..header import Header import base64 import json class DoubleRatchet(DHRatchet): """ An implementation of the Ratchet interface, which builds the core of the DoubleRatchet protocol by linking all parts into one class. A double ratchet allows message encryption providing perfect forward secrecy. A double ratchet instance synchronizes with a second instance using Diffie-Hellman calculations, that are provided by the DHRatchet class. For details on how the protocol works, take a look at the specification by WhisperSystems: https://signal.org/docs/specifications/doubleratchet/ """ def __init__( self, aead, message_key_store_max, symmetric_key_ratchet, ad, *args, **kwargs ): """ Initialize a new DoubleRatchet. :param aead: An instance of an implementation of the AEAD interface, which is used to provice authenticated message encryption and is fed with the message keys derived using the symmetric key ratchet. :param message_key_store_max: An integer defining the maximum amount of message keys to store before raising an exception. This mechanism allows out-of-order messages, by storing message keys of out-of-order messages instead of discarding them. :param symmetric_key_ratchet: An instance of the SymmetricKeyRatchet class, which is used to derive en- and decryption keys for message exchange. :param ad: Some associated data to use for message authentication, encoded as a bytes-like object. """ self.__aead = aead self.__mks_max = message_key_store_max self.__skr = symmetric_key_ratchet self.__ad = ad self.__saved_message_keys = {} # The super constructor may already call the _onNewChainKey method, # that's why the skr must be stored into self, before the call can be made. super(DoubleRatchet, self).__init__(*args, **kwargs) def serialize(self): smks = {} for key, value in self.__saved_message_keys.items(): key = json.dumps({ "pub" : base64.b64encode(key[0]).decode("US-ASCII"), "index" : key[1] }) smks[key] = base64.b64encode(value).decode("US-ASCII") return { "super" : super(DoubleRatchet, self).serialize(), "skr" : self.__skr.serialize(), "ad" : base64.b64encode(self.__ad).decode("US-ASCII"), "smks" : smks } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = super(DoubleRatchet, cls).fromSerialized( serialized["super"], *args, **kwargs ) self.__skr = self.__skr.__class__.fromSerialized(serialized["skr"]) self.__ad = base64.b64decode(serialized["ad"].encode("US-ASCII")) smks = {} for key, value in serialized["smks"].items(): key = json.loads(key) pub = base64.b64decode(key["pub"].encode("US-ASCII")) index = key["index"] smks[(pub, index)] = base64.b64decode(value.encode("US-ASCII")) self.__saved_message_keys = smks return self def _onNewChainKey(self, key, chain): """ Update the symmetric key ratchet with the new key. """ self.__skr.step(key, chain) def decryptMessage(self, ciphertext, header, ad = None): """ Decrypt a message using this double ratchet session. :param ciphertext: A bytes-like object encoding the message to decrypt. :param header: An instance of the Header class. This should have been sent together with the ciphertext. :param ad: A bytes-like object encoding the associated data to use for message authentication. Pass None to use the associated data set during construction. :returns: The plaintext. :raises AuthenticationFailedException: If checking the authentication for this message failed. :raises NotInitializedException: If this double ratchet session is not yet initialized with a key pair, thus not prepared to decrypt an incoming message. :raises TooManySavedMessageKeysException: If more than message_key_store_max have to be stored to decrypt this message. """ if ad == None: ad = self.__ad # Try to decrypt the message using a previously saved message key plaintext = self.__decryptSavedMessage(ciphertext, header, ad) if plaintext: return plaintext # Check, whether the public key will trigger a dh ratchet step if self.triggersStep(header.dh_pub): # Save missed message keys for the current receiving chain self.__saveMessageKeys(header.pn) # Perform the step self.step(header.dh_pub) # Save missed message keys for the current receiving chain self.__saveMessageKeys(header.n) # Finally decrypt the message and return the plaintext return self.__decrypt( ciphertext, self.__skr.nextDecryptionKey(), header, ad ) def __decrypt(self, ciphertext, key, header, ad): return self.__aead.decrypt(ciphertext, key, self._makeAD(header, ad)) def __decryptSavedMessage(self, ciphertext, header, ad): try: # Search for a saved key for this message key = self.__saved_message_keys[(header.dh_pub, header.n)] except KeyError: # If there was no message key saved for this message, return None return None # Delete the entry del self.__saved_message_keys[(header.dh_pub, header.n)] # Finally decrypt the message and return the plaintext return self.__decrypt(ciphertext, key, header, ad) def __saveMessageKeys(self, target): if self.__skr.receiving_chain_length == None: return num_keys_to_save = target - self.__skr.receiving_chain_length # Check whether the mk_store_max value would get crossed saving these message keys if num_keys_to_save + len(self.__saved_message_keys) > self.__mks_max: raise TooManySavedMessageKeysException() # Save all message keys until the target chain length was reached while self.__skr.receiving_chain_length < target: next_key = self.__skr.nextDecryptionKey() key_index = self.__skr.receiving_chain_length - 1 self.__saved_message_keys[(self.other_pub, key_index)] = next_key def encryptMessage(self, message, ad = None): """ Encrypt a message using this double ratchet session. :param message: A bytes-like object encoding the message to encrypt. :param ad: A bytes-like object encoding the associated data to use for message authentication. Pass None to use the associated data set during construction. :returns: A dictionary containing the message header and ciphertext. The header is required to synchronize the double ratchet of the receiving party. Send it along with the ciphertext. The returned dictionary consists of two keys: "header", which includes an instance of the Header class and "ciphertext", which includes the encrypted message encoded as a bytes-like object. :raises NotInitializedException: If this double ratchet session is not yet initialized with the other parties public key, thus not ready to encrypt a message to that party. """ if ad == None: ad = self.__ad # Prepare the header for this message header = Header( self.pub, self.__skr.sending_chain_length, self.__skr.previous_sending_chain_length ) # Encrypt the message ciphertext = self.__aead.encrypt( message, self.__skr.nextEncryptionKey(), self._makeAD(header, ad) ) return { "header" : header, "ciphertext" : ciphertext } def _makeAD(self, header, ad): """ Construct specific associated data for this message from the message header and the general associated data. :param header: An instance of the Header class. :param ad: A bytes-like object encoding the general associated data. :returns: A bytes-like object encoding the message-specific associated data. """ raise NotImplementedError DoubleRatchet-0.6.0/doubleratchet/ratchets/dhratchet.py0000644000175000017500000001031713405220332023176 0ustar useruser00000000000000from __future__ import absolute_import from .ratchet import Ratchet class DHRatchet(Ratchet): """ An implementation of the Ratchet interface, which implements a Diffie-Hellman ratchet. A Diffie-Hellman ratchet performs its step by calculating shared secrets between Diffie-Hellman keys. The Diffie-Hellman ratchet is designed so that two instances can synchronize by exchanging the new public keys that are generated in each step. For more information, visit the specification by WhisperSystems: https://signal.org/docs/specifications/doubleratchet/#diffie-hellman-ratchet """ def __init__( self, key_pair_class, root_chain, own_key = None, other_pub = None ): """ Initialize a new Diffie-Hellman ratchet. :param key_pair_class: An implementations of the KeyPair interface which is used for the Diffie-Hellman key management and shared secret calculations. :param root_chain: A KDFChain, which receives the Diffie-Hellman key exchange output to derive a new chain key. :param own_key: An instance of key_pair_class holding the first key pair to initialize this ratchet with or None. :param other_pub: A bytes-like object encoding the public key of the other Diffie-Hellman ratchet to synchronize with or None. """ super(DHRatchet, self).__init__() self._KeyPair = key_pair_class self.__root_chain = root_chain if own_key: self.__key = own_key else: self.__newRatchetKey() self.__wrapOtherPub(other_pub) if self.__other.pub != None: self.__newRootKey("sending") def serialize(self): return { "super" : super(DHRatchet, self).serialize(), "root_chain" : self.__root_chain.serialize(), "own_key" : self.__key.serialize(), "other_pub" : self.__other.serialize() } @classmethod def fromSerialized(cls, serialized, *args, **kwargs): self = super(DHRatchet, cls).fromSerialized( serialized["super"], *args, **kwargs ) RootChain = self.__root_chain.__class__ self.__root_chain = RootChain.fromSerialized(serialized["root_chain"]) self.__key = self._KeyPair.fromSerialized(serialized["own_key"]) self.__other = self._KeyPair.fromSerialized(serialized["other_pub"]) return self def step(self, other_pub): """ Perform a rachted step, calculating a new shared secret from the public key and deriving new chain keys from this secret. New Diffie-Hellman calculations are only performed if the public key is different from the previous one. :param other_pub: A bytes-like object encoding the public key of the other Diffie-Hellman ratchet to synchronize with. """ if self.triggersStep(other_pub): self.__wrapOtherPub(other_pub) self.__newRootKey("receiving") self.__newRatchetKey() self.__newRootKey("sending") def __wrapOtherPub(self, other_pub): self.__other = self._KeyPair(pub = other_pub) def __newRatchetKey(self): self.__key = self._KeyPair.generate() def triggersStep(self, other_pub): """ :returns: A boolean indicating whether calling next with this public key would trigger a ratchet step. """ return other_pub != self.__other.pub def __newRootKey(self, chain): self._onNewChainKey( self.__root_chain.next(self.__key.getSharedSecret(self.__other)), chain ) def _onNewChainKey(self, key, chain): raise NotImplementedError @property def pub(self): """ :returns: A bytes-like object encoding the public key of the current internally managed key pair. """ return self.__key.pub @property def other_pub(self): """ :returns: A bytes-like object encoding the public key of the other Diffie-Hellman ratchet to synchronize with. """ return self.__other.pub DoubleRatchet-0.6.0/doubleratchet/__init__.py0000644000175000017500000000047013405170662021163 0ustar useruser00000000000000from __future__ import absolute_import from .version import __version__ from . import exceptions from . import kdfchains from . import ratchets from . import recommended from .aead import AEAD from .header import Header from .kdf import KDF from .keypair import KeyPair from .serializable import Serializable DoubleRatchet-0.6.0/doubleratchet/header.py0000644000175000017500000000251013372774414020661 0ustar useruser00000000000000class Header(object): """ Each message encrypted with the double ratchet protocol comes with a header in addition to the ciphertext body. """ def __init__(self, dh_pub, n, pn): """ Initiate a message header. :param dh_pub: A bytes-like object encoding the new public key of the senders diffie-hellman ratchet. :param n: The current length of the senders sending chain, as an integer. :param pn: The length of the senders previous sending chain, as an integer. This enables the receiver to store keys for skipped messages of the previous chain. """ self.__dh_pub = dh_pub self.__n = n self.__pn = pn @property def dh_pub(self): """ :returns: A bytes-like object encoding the new public key of the senders diffie-hellman ratchet. """ return self.__dh_pub @property def n(self): """ :returns: The current length of the senders sending chain, as an integer. """ return self.__n @property def pn(self): """ :returns: The length of the senders previous sending chain, as an integer. This enables the receiver to store keys for skipped messages of the previous chain. """ return self.__pn DoubleRatchet-0.6.0/doubleratchet/recommended/0000755000175000017500000000000013405304506021327 5ustar useruser00000000000000DoubleRatchet-0.6.0/doubleratchet/recommended/rootkeykdf.py0000644000175000017500000000355113372774414024102 0ustar useruser00000000000000from __future__ import absolute_import from ..kdf import KDF from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.hkdf import HKDF class RootKeyKDF(KDF): """ An implementations of the KDF interface based on a recommendation by WhisperSystems: This function is recommended to be implemented using HKDF with SHA-256 or SHA-512, using rk as HKDF salt, dh_out as HKDF input key material, and an application-specific byte sequence as HKDF info. The info value should be chosen to be distinct from other uses of HKDF in the application. """ CRYPTOGRAPHY_BACKEND = default_backend() HASH_FUNCTIONS = { "SHA-256": hashes.SHA256, "SHA-512": hashes.SHA512 } def __init__(self, hash_function, info_string): """ Prepare a RootKeyKDF, following a recommendation by WhisperSystems. :param hash_function: One of (the strings) "SHA-256" and "SHA-512". :param info_string: A bytes-like object encoding a string unique to this usage within the application. """ super(RootKeyKDF, self).__init__() if not hash_function in RootKeyKDF.HASH_FUNCTIONS: raise ValueError("Invalid value passed for the hash_function parameter.") if not isinstance(info_string, bytes): raise TypeError("Wrong type passed for the info_string parameter.") self.__hash_function = RootKeyKDF.HASH_FUNCTIONS[hash_function] self.__info_string = info_string def calculate(self, key, data, length): return HKDF( algorithm = self.__hash_function(), length = length, salt = key, info = self.__info_string, backend = self.__class__.CRYPTOGRAPHY_BACKEND ).derive(data) DoubleRatchet-0.6.0/doubleratchet/recommended/__init__.py0000644000175000017500000000022613372774414023454 0ustar useruser00000000000000from __future__ import absolute_import from .cbchmacaead import CBCHMACAEAD from .chainkeykdf import ChainKeyKDF from .rootkeykdf import RootKeyKDF DoubleRatchet-0.6.0/doubleratchet/recommended/cbchmacaead.py0000644000175000017500000001202013372774414024103 0ustar useruser00000000000000from __future__ import absolute_import from ..aead import AEAD from ..exceptions import AuthenticationFailedException from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, hmac, padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.kdf.hkdf import HKDF class CBCHMACAEAD(AEAD): """ An implementation of the AEAD interface based on a recommendation by WhisperSystems: HKDF is used with SHA-256 or SHA-512 to generate 80 bytes of output. The HKDF salt is set to a zero-filled byte sequence equal to the hash's output length. HKDF input key material is set to mk. HKDF info is set to an application-specific byte sequence distinct from other uses of HKDF in the application. The HKDF output is divided into a 32-byte encryption key, a 32-byte authentication key, and a 16-byte IV. The plaintext is encrypted using AES-256 in CBC mode with PKCS#7 padding, using the encryption key and IV from the previous step. HMAC is calculated using the authentication key and the same hash function as above. The HMAC input is the associated_data prepended to the ciphertext. The HMAC output is appended to the ciphertext. """ CRYPTOGRAPHY_BACKEND = default_backend() HASH_FUNCTIONS = { "SHA-256": hashes.SHA256, "SHA-512": hashes.SHA512 } def __init__(self, hash_function, info_string): """ Prepare a CBCHMACAEAD, following a recommendation by WhisperSystems. :param hash_function: One of (the strings) "SHA-256" and "SHA-512". :param info_string: A bytes-like object encoding a string unique to this usage within the application. """ super(CBCHMACAEAD, self).__init__() if not hash_function in CBCHMACAEAD.HASH_FUNCTIONS: raise ValueError("Invalid value passed for the hash_function parameter.") if not isinstance(info_string, bytes): raise TypeError("Wrong type passed for the info_string parameter.") self.__hash_function = CBCHMACAEAD.HASH_FUNCTIONS[hash_function] self.__digest_size = self.__hash_function().digest_size self.__info_string = info_string def __getHKDFOutput(self, message_key): # Prepare the salt, which should be a string of 0x00 bytes with the length of # the hash digest salt = b"\x00" * self.__digest_size # Get 80 bytes from the HKDF calculation hkdf_out = HKDF( algorithm = self.__hash_function(), length = 80, salt = salt, info = self.__info_string, backend = self.__class__.CRYPTOGRAPHY_BACKEND ).derive(message_key) # Split these 80 bytes in three parts return hkdf_out[:32], hkdf_out[32:64], hkdf_out[64:] def __getAES(self, key, iv): return Cipher( algorithms.AES(key), modes.CBC(iv), backend = self.__class__.CRYPTOGRAPHY_BACKEND ) def encrypt(self, plaintext, message_key, ad): encryption_key, authentication_key, iv = self.__getHKDFOutput(message_key) # Prepare PKCS#7 padded plaintext padder = padding.PKCS7(128).padder() plaintext = padder.update(plaintext) + padder.finalize() # Encrypt the plaintext using AES-256 (the 256 bit are implied by the key size), # CBC mode and the previously created key and iv aes_cbc = self.__getAES(encryption_key, iv).encryptor() ciphertext = aes_cbc.update(plaintext) + aes_cbc.finalize() # Build the authentication auth = hmac.HMAC( authentication_key, self.__hash_function(), backend = self.__class__.CRYPTOGRAPHY_BACKEND ) auth.update(ad + ciphertext) # Append the authentication to the ciphertext return ciphertext + auth.finalize() def decrypt(self, ciphertext, message_key, ad): auth = ciphertext[-self.__digest_size:] ciphertext = ciphertext[:-self.__digest_size] decryption_key, authentication_key, iv = self.__getHKDFOutput(message_key) new_auth = hmac.HMAC( authentication_key, self.__hash_function(), backend = self.__class__.CRYPTOGRAPHY_BACKEND ) new_auth.update(ad + ciphertext) try: new_auth.verify(auth) except InvalidSignature: raise AuthenticationFailedException() # Decrypt the plaintext using AES-256 (the 256 bit are implied by the key size), # CBC mode and the previously created key and iv aes_cbc = self.__getAES(decryption_key, iv).decryptor() plaintext = aes_cbc.update(ciphertext) + aes_cbc.finalize() # Remove the PKCS#7 padding from the plaintext unpadder = padding.PKCS7(128).unpadder() plaintext = unpadder.update(plaintext) + unpadder.finalize() return plaintext DoubleRatchet-0.6.0/doubleratchet/recommended/chainkeykdf.py0000644000175000017500000000507413372774414024203 0ustar useruser00000000000000from __future__ import absolute_import from ..kdf import KDF from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, hmac class ChainKeyKDF(KDF): """ An implementations of the KDF interface based on a recommendation by WhisperSystems: HMAC with SHA-256 or SHA-512 is recommended, using ck as the HMAC key and using separate constants as input (e.g. a single byte 0x01 as input to produce the message key, and a single byte 0x02 as input to produce the next chain key). Notice: Following these recommendations, the calculate method ignored both the data and the length parameters. """ CRYPTOGRAPHY_BACKEND = default_backend() HASH_FUNCTIONS = { "SHA-256": hashes.SHA256, "SHA-512": hashes.SHA512 } def __init__(self, hash_function, ck_constant = b"\x02", mk_constant = b"\x01"): """ Prepare a ChainKeyKDF, following a recommendation by WhisperSystems. :param hash_function: One of (the strings) "SHA-256" and "SHA-512". :param ck_constant: A single byte used for derivation of the next chain key. :param mk_constant: A single byte used for derivation of the next message key. """ super(ChainKeyKDF, self).__init__() if not hash_function in ChainKeyKDF.HASH_FUNCTIONS: raise ValueError("Invalid value passed for the hash_function parameter.") if not isinstance(ck_constant, bytes): raise TypeError("Wrong type passed for the ck_constant parameter.") if len(ck_constant) != 1: raise ValueError("Invalid value passed for the ck_constant parameter.") if not isinstance(mk_constant, bytes): raise TypeError("Wrong type passed for the mk_constant parameter.") if len(mk_constant) != 1: raise ValueError("Invalid value passed for the mk_constant parameter.") self.__hash_function = ChainKeyKDF.HASH_FUNCTIONS[hash_function] self.__ck_constant = ck_constant self.__mk_constant = mk_constant def calculate(self, key, data = None, length = None): chain_key = hmac.HMAC( key, self.__hash_function(), backend = self.__class__.CRYPTOGRAPHY_BACKEND ) chain_key.update(self.__ck_constant) message_key = hmac.HMAC( key, self.__hash_function(), backend = self.__class__.CRYPTOGRAPHY_BACKEND ) message_key.update(self.__mk_constant) return chain_key.finalize() + message_key.finalize() DoubleRatchet-0.6.0/doubleratchet/serializable.py0000644000175000017500000000176713405167576022116 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) DoubleRatchet-0.6.0/doubleratchet/keypair.py0000644000175000017500000000335113405167734021077 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 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 DoubleRatchet-0.6.0/setup.cfg0000644000175000017500000000004613405304506016041 0ustar useruser00000000000000[egg_info] tag_build = tag_date = 0 DoubleRatchet-0.6.0/README.md0000644000175000017500000000163713372774414015522 0ustar useruser00000000000000[![PyPI](https://img.shields.io/pypi/v/DoubleRatchet.svg)](https://pypi.org/project/DoubleRatchet/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/DoubleRatchet.svg)](https://pypi.org/project/DoubleRatchet/) [![Build Status](https://travis-ci.org/Syndace/python-doubleratchet.svg?branch=master)](https://travis-ci.org/Syndace/python-doubleratchet) # python-doubleratchet #### A python implementation of the Double Ratchet algorithm. This python library offers an implementation of the Double Ratchet algorithm as specified [here](https://signal.org/docs/specifications/doubleratchet/). The goal is to provide a configurable and independent implementation of the algorithm, while keeping the structure close to the specification and providing recommended settings. This library was developed as part of [python-omemo](https://github.com/Syndace/python-omemo), a pretty cool end-to-end encryption protocol. DoubleRatchet-0.6.0/PKG-INFO0000644000175000017500000000366413405304506015326 0ustar useruser00000000000000Metadata-Version: 2.1 Name: DoubleRatchet Version: 0.6.0 Summary: A python implementation of the Double Ratchet algorithm. Home-page: https://github.com/Syndace/python-doubleratchet Author: Tim Henkes Author-email: tim@cifg.io License: MIT Description: [![PyPI](https://img.shields.io/pypi/v/DoubleRatchet.svg)](https://pypi.org/project/DoubleRatchet/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/DoubleRatchet.svg)](https://pypi.org/project/DoubleRatchet/) [![Build Status](https://travis-ci.org/Syndace/python-doubleratchet.svg?branch=master)](https://travis-ci.org/Syndace/python-doubleratchet) # python-doubleratchet #### A python implementation of the Double Ratchet algorithm. This python library offers an implementation of the Double Ratchet algorithm as specified [here](https://signal.org/docs/specifications/doubleratchet/). The goal is to provide a configurable and independent implementation of the algorithm, while keeping the structure close to the specification and providing recommended settings. 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 DoubleRatchet-0.6.0/setup.py0000644000175000017500000000277313405217241015742 0ustar useruser00000000000000from setuptools import setup, find_packages import os import sys version_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "doubleratchet", "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 = "DoubleRatchet", version = version["__version__"], description = "A python implementation of the Double Ratchet algorithm.", long_description = long_description, long_description_content_type = "text/markdown", url = "https://github.com/Syndace/python-doubleratchet", author = "Tim Henkes", author_email = "tim@cifg.io", license = "MIT", packages = find_packages(), install_requires = [ "cryptography>=1.7.1" ], 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" ] )