diff --git a/.gitignore b/.gitignore index 5996501..5bdb4cc 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ nosetests.xml coverage.xml *,cover .hypothesis/ +.pytest_cache/ # Translations *.mo diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5022499..72a37cd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +.. _v0-3-0: + +0.3.0 - 2019-02-24 +~~~~~~~~~~~~~~~~~~ + +* Added support for non-default crypto backends +* Loosened restriction on Cryptography version in requirements.txt and bumped version to 2.5 +* Moved away from custom x448 implementation in favor of OpenSSL implementation (for default backend) + .. _v0-2-2: 0.2.2 - 2018-03-21 @@ -35,4 +44,4 @@ Changelog 0.1.1 - 2017-09-12 ~~~~~~~~~~~~~~~~~~ -Initial release. \ No newline at end of file +Initial release. diff --git a/README.md b/README.md index 67e9bd1..bae17c9 100755 --- a/README.md +++ b/README.md @@ -10,10 +10,6 @@ Compatible with revisions 32 and 33. Master branch contains latest version released. Trunk branch is an active development branch. -### Warning -This package shall not be used (yet) for production purposes. There was little to none peer review done so far. -Use common sense while using - until this package becomes stable. - ## Documentation Available on [Read the Docs](https://noiseprotocol.readthedocs.io). For now it provides basic documentation on HandshakeState, CipherState and SymmetricState. Refer to the rest of the README below for more information. @@ -138,12 +134,12 @@ pytest ### Todo-list for the project: -- [ ] custom crypto backends +- [x] add non-default crypto algorithms support, as requested - [ ] fallback patterns support - [ ] scripts for keypair generation (+ console entry points) - [ ] "echo" (noise-c like) example - [ ] extensive logging -- [ ] move away from custom ed448 implementation +- [x] move away from custom ed448 implementation - [ ] implement countermeasures for side-channel attacks - [ ] **get peer review of the code** diff --git a/docs/conf.py b/docs/conf.py index bfc8e94..996cf2a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,9 +58,9 @@ author = 'Piotr Lizonczyk' # built documents. # # The short X.Y version. -version = '0.2' +version = '0.3' # The full version, including alpha/beta/rc tags. -release = '0.2.0' +release = '0.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/noise/backends/__init__.py b/noise/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/noise/backends/default/__init__.py b/noise/backends/default/__init__.py new file mode 100644 index 0000000..bd0305e --- /dev/null +++ b/noise/backends/default/__init__.py @@ -0,0 +1,3 @@ +from .backend import DefaultNoiseBackend + +noise_backend = DefaultNoiseBackend() diff --git a/noise/backends/default/backend.py b/noise/backends/default/backend.py new file mode 100644 index 0000000..e6bc780 --- /dev/null +++ b/noise/backends/default/backend.py @@ -0,0 +1,38 @@ +from noise.backends.default.ciphers import ChaCha20Cipher, AESGCMCipher +from noise.backends.default.diffie_hellmans import ED25519, ED448 +from noise.backends.default.hashes import hmac_hash, BLAKE2sHash, BLAKE2bHash, SHA256Hash, SHA512Hash +from noise.backends.default.keypairs import KeyPair25519, KeyPair448 +from noise.backends.noise_backend import NoiseBackend + + +class DefaultNoiseBackend(NoiseBackend): + """ + Contains all the crypto methods endorsed by Noise Protocol specification, using Cryptography as backend + """ + + def __init__(self): + super(DefaultNoiseBackend, self).__init__() + + self.diffie_hellmans = { + '25519': ED25519, + '448': ED448 + } + + self.ciphers = { + 'AESGCM': AESGCMCipher, + 'ChaChaPoly': ChaCha20Cipher + } + + self.hashes = { + 'BLAKE2s': BLAKE2sHash, + 'BLAKE2b': BLAKE2bHash, + 'SHA256': SHA256Hash, + 'SHA512': SHA512Hash + } + + self.keypairs = { + '25519': KeyPair25519, + '448': KeyPair448 + } + + self.hmac = hmac_hash diff --git a/noise/backends/default/ciphers.py b/noise/backends/default/ciphers.py new file mode 100644 index 0000000..9360125 --- /dev/null +++ b/noise/backends/default/ciphers.py @@ -0,0 +1,35 @@ +import abc + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 + +from noise.functions.cipher import Cipher + + +class CryptographyCipher(Cipher, metaclass=abc.ABCMeta): + def encrypt(self, k, n, ad, plaintext): + return self.cipher.encrypt(nonce=self.format_nonce(n), data=plaintext, associated_data=ad) + + def decrypt(self, k, n, ad, ciphertext): + return self.cipher.decrypt(nonce=self.format_nonce(n), data=ciphertext, associated_data=ad) + + @abc.abstractmethod + def format_nonce(self, n): + raise NotImplementedError + + +class AESGCMCipher(CryptographyCipher): + @property + def klass(self): + return AESGCM + + def format_nonce(self, n): + return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='big') + + +class ChaCha20Cipher(CryptographyCipher): + @property + def klass(self): + return ChaCha20Poly1305 + + def format_nonce(self, n): + return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='little') diff --git a/noise/backends/default/diffie_hellmans.py b/noise/backends/default/diffie_hellmans.py new file mode 100644 index 0000000..5ddbc65 --- /dev/null +++ b/noise/backends/default/diffie_hellmans.py @@ -0,0 +1,45 @@ +from cryptography.hazmat.primitives.asymmetric import x25519, x448 + +from noise.backends.default.keypairs import KeyPair25519, KeyPair448 +from noise.exceptions import NoiseValueError +from noise.functions.dh import DH + + +class ED25519(DH): + @property + def klass(self): + return KeyPair25519 + + @property + def dhlen(self): + return 32 + + def generate_keypair(self) -> 'KeyPair': + private_key = x25519.X25519PrivateKey.generate() + public_key = private_key.public_key() + return KeyPair25519(private_key, public_key, public_key.public_bytes()) + + def dh(self, private_key, public_key) -> bytes: + if not isinstance(private_key, x25519.X25519PrivateKey) or not isinstance(public_key, x25519.X25519PublicKey): + raise NoiseValueError('Invalid keys! Must be x25519.X25519PrivateKey and x25519.X25519PublicKey instances') + return private_key.exchange(public_key) + + +class ED448(DH): + @property + def klass(self): + return KeyPair448 + + @property + def dhlen(self): + return 56 + + def generate_keypair(self) -> 'KeyPair': + private_key = x448.X448PrivateKey.generate() + public_key = private_key.public_key() + return KeyPair448(private_key, public_key, public_key.public_bytes()) + + def dh(self, private_key, public_key) -> bytes: + if not isinstance(private_key, x448.X448PrivateKey) or not isinstance(public_key, x448.X448PublicKey): + raise NoiseValueError('Invalid keys! Must be x448.X448PrivateKey and x448.X448PublicKey instances') + return private_key.exchange(public_key) diff --git a/noise/backends/default/hashes.py b/noise/backends/default/hashes.py new file mode 100644 index 0000000..4fae13c --- /dev/null +++ b/noise/backends/default/hashes.py @@ -0,0 +1,80 @@ +import abc +from functools import partial + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.hmac import HMAC + +from noise.functions.hash import Hash + +cryptography_backend = default_backend() + + +class CryptographyHash(Hash, metaclass=abc.ABCMeta): + def hash(self, data): + digest = hashes.Hash(self.fn(), cryptography_backend) + digest.update(data) + return digest.finalize() + + +class SHA256Hash(CryptographyHash): + @property + def fn(self): + return hashes.SHA256 + + @property + def hashlen(self): + return 32 + + @property + def blocklen(self): + return 64 + + +class SHA512Hash(CryptographyHash): + @property + def fn(self): + return hashes.SHA512 + + @property + def hashlen(self): + return 64 + + @property + def blocklen(self): + return 128 + + +class BLAKE2sHash(CryptographyHash): + @property + def fn(self): + return partial(hashes.BLAKE2s, digest_size=self.hashlen) + + @property + def hashlen(self): + return 32 + + @property + def blocklen(self): + return 64 + + +class BLAKE2bHash(CryptographyHash): + @property + def fn(self): + return partial(hashes.BLAKE2b, digest_size=self.hashlen) + + @property + def hashlen(self): + return 64 + + @property + def blocklen(self): + return 128 + + +def hmac_hash(key, data, algorithm): + # Applies HMAC using the HASH() function. + hmac = HMAC(key=key, algorithm=algorithm(), backend=cryptography_backend) + hmac.update(data=data) + return hmac.finalize() diff --git a/noise/backends/default/keypairs.py b/noise/backends/default/keypairs.py new file mode 100644 index 0000000..5e74ddf --- /dev/null +++ b/noise/backends/default/keypairs.py @@ -0,0 +1,39 @@ +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import x25519, x448 + +from noise.exceptions import NoiseValueError +from noise.functions.keypair import KeyPair + + +class KeyPair25519(KeyPair): + @classmethod + def from_private_bytes(cls, private_bytes): + if len(private_bytes) != 32: + raise NoiseValueError('Invalid length of private_bytes! Should be 32') + private = x25519.X25519PrivateKey.from_private_bytes(private_bytes) + public = private.public_key() + return cls(private=private, public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)) + + @classmethod + def from_public_bytes(cls, public_bytes): + if len(public_bytes) != 32: + raise NoiseValueError('Invalid length of public_bytes! Should be 32') + public = x25519.X25519PublicKey.from_public_bytes(public_bytes) + return cls(public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)) + + +class KeyPair448(KeyPair): + @classmethod + def from_private_bytes(cls, private_bytes): + if len(private_bytes) != 56: + raise NoiseValueError('Invalid length of private_bytes! Should be 56') + private = x448.X448PrivateKey.from_private_bytes(private_bytes) + public = private.public_key() + return cls(private=private, public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)) + + @classmethod + def from_public_bytes(cls, public_bytes): + if len(public_bytes) != 56: + raise NoiseValueError('Invalid length of private_bytes! Should be 56') + public = x448.X448PublicKey.from_public_bytes(public_bytes) + return cls(public=public, public_bytes=public.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)) diff --git a/noise/backends/experimental/__init__.py b/noise/backends/experimental/__init__.py new file mode 100644 index 0000000..08f0344 --- /dev/null +++ b/noise/backends/experimental/__init__.py @@ -0,0 +1,3 @@ +from noise.backends.experimental.backend import ExperimentalNoiseBackend + +noise_backend = ExperimentalNoiseBackend() diff --git a/noise/backends/experimental/backend.py b/noise/backends/experimental/backend.py new file mode 100644 index 0000000..e065b5f --- /dev/null +++ b/noise/backends/experimental/backend.py @@ -0,0 +1,8 @@ +from noise.backends.default import DefaultNoiseBackend + + +class ExperimentalNoiseBackend(DefaultNoiseBackend): + """ + Contains all the default crypto methods, but also methods not directly endorsed by Noise Protocol specification + """ + pass diff --git a/noise/backends/noise_backend.py b/noise/backends/noise_backend.py new file mode 100644 index 0000000..d172036 --- /dev/null +++ b/noise/backends/noise_backend.py @@ -0,0 +1,64 @@ +from noise.exceptions import NoiseProtocolNameError +from noise.functions.hash import hkdf +from noise.patterns import (PatternN, PatternK, PatternX, PatternNN, PatternKN, PatternNK, PatternKK, PatternNX, + PatternKX, PatternXN, PatternIN, PatternXK, PatternIK, PatternXX, PatternIX) + + +class NoiseBackend: + """ + Base for creating backends. + Implementing classes must define supported crypto methods in appropriate dict (diffie_hellmans, ciphers, etc.) + HMAC function must be defined as well. + + Dicts use convention for keys - they must match the string that occurs in Noise Protocol name. + """ + def __init__(self): + self.patterns = { + 'N': PatternN, + 'K': PatternK, + 'X': PatternX, + 'NN': PatternNN, + 'KN': PatternKN, + 'NK': PatternNK, + 'KK': PatternKK, + 'NX': PatternNX, + 'KX': PatternKX, + 'XN': PatternXN, + 'IN': PatternIN, + 'XK': PatternXK, + 'IK': PatternIK, + 'XX': PatternXX, + 'IX': PatternIX, + } + + self.diffie_hellmans = {} + self.ciphers = {} + self.hashes = {} + self.keypairs = {} + self.hmac = None + + self.hkdf = hkdf + + @property + def methods(self): + return { + 'pattern': self.patterns, + 'dh': self.diffie_hellmans, + 'cipher': self.ciphers, + 'hash': self.hashes, + 'keypair': self.keypairs + } + + def map_protocol_name_to_crypto(self, unpacked_name): + mappings = {} + # Validate if we know everything that Noise Protocol is supposed to use and map appropriate functions + for method, map_dict in self.methods.items(): + looked_up_func = getattr(unpacked_name, method) + func = map_dict.get(looked_up_func) + if not func: + raise NoiseProtocolNameError('Unknown {} in Noise Protocol name, given {}, known {}'.format( + method, looked_up_func, " ".join(map_dict))) + mappings[method] = func + + return mappings + diff --git a/noise/connection.py b/noise/connection.py index 3f5dd58..3e64c7f 100644 --- a/noise/connection.py +++ b/noise/connection.py @@ -3,6 +3,7 @@ from typing import Union, List from cryptography.exceptions import InvalidTag +from noise.backends.default import noise_backend from noise.constants import MAX_MESSAGE_LEN from noise.exceptions import NoisePSKError, NoiseValueError, NoiseHandshakeError, NoiseInvalidMessage from .noise_protocol import NoiseProtocol @@ -21,6 +22,7 @@ _keypairs = {Keypair.STATIC: 's', Keypair.REMOTE_STATIC: 'rs', class NoiseConnection(object): def __init__(self): + self.backend = None self.noise_protocol = None self.protocol_name = None self.handshake_finished = False @@ -28,14 +30,14 @@ class NoiseConnection(object): self._next_fn = None @classmethod - def from_name(cls, name: Union[str, bytes]): + def from_name(cls, name: Union[str, bytes], backend=noise_backend): instance = cls() # Forgiving passing string. Bytes are good too, anything else will fail inside NoiseProtocol try: instance.protocol_name = name.encode('ascii') if isinstance(name, str) else name except ValueError: raise NoiseValueError('If passing string as protocol name, it must contain only ASCII characters') - instance.noise_protocol = NoiseProtocol(protocol_name=name) + instance.noise_protocol = NoiseProtocol(protocol_name=name, backend=backend) return instance def set_psks(self, psk: Union[bytes, str] = None, psks: List[Union[str, bytes]] = None): @@ -74,21 +76,21 @@ class NoiseConnection(object): def set_keypair_from_private_bytes(self, keypair: Keypair, private_bytes: bytes): self.noise_protocol.keypairs[_keypairs[keypair]] = \ - self.noise_protocol.dh_fn.keypair_cls.from_private_bytes(private_bytes) + self.noise_protocol.dh_fn.klass.from_private_bytes(private_bytes) def set_keypair_from_public_bytes(self, keypair: Keypair, private_bytes: bytes): self.noise_protocol.keypairs[_keypairs[keypair]] = \ - self.noise_protocol.dh_fn.keypair_cls.from_public_bytes(private_bytes) + self.noise_protocol.dh_fn.klass.from_public_bytes(private_bytes) def set_keypair_from_private_path(self, keypair: Keypair, path: str): with open(path, 'rb') as fd: self.noise_protocol.keypairs[_keypairs[keypair]] = \ - self.noise_protocol.dh_fn.keypair_cls.from_private_bytes(fd.read()) + self.noise_protocol.dh_fn.klass.from_private_bytes(fd.read()) def set_keypair_from_public_path(self, keypair: Keypair, path: str): with open(path, 'rb') as fd: self.noise_protocol.keypairs[_keypairs[keypair]] = \ - self.noise_protocol.dh_fn.keypair_cls.from_public_bytes(fd.read()) + self.noise_protocol.dh_fn.klass.from_public_bytes(fd.read()) def start_handshake(self): self.noise_protocol.validate() diff --git a/noise/crypto.py b/noise/crypto.py deleted file mode 100644 index c47f522..0000000 --- a/noise/crypto.py +++ /dev/null @@ -1,110 +0,0 @@ -DHLEN = 56 -P = 2 ** 448 - 2 ** 224 - 1 -A24 = 39081 - - -class X448(object): - # Based on RFC 7748 and heavily relying on it - https://tools.ietf.org/html/rfc7748#section-5 - # Modified mostly to fulfill python3 changes - # Almost surely unsafe from side-channel attacks. - # Should be replaced with safer implementation (most likely one from OpenSSL and/or pyca/Cryptography) - @staticmethod - def decode_little_endian(b): - assert len(b) == DHLEN - return sum([b[i] << 8 * i for i in range(DHLEN)]) - - @staticmethod - def decode_u_coordinate(u): - u[-1] &= (1 << 56) - 1 - return X448.decode_little_endian(u) - - @staticmethod - def encode_u_coordinate(u): - return bytes([(u >> 8 * i) & 255 for i in range(DHLEN)]) - - @staticmethod - def decode_scalar448(k): - k = [b for b in k] - k[0] &= 252 - k[55] |= 128 - return X448.decode_little_endian(k) - - @staticmethod - def cswap(swap, x2, x3): - dummy = (swap * (x2 - x3)) % P - x2 = (x2 - dummy) % P - x3 = (x3 + dummy) % P - return x2, x3 - - @staticmethod - def x448(k, u): - x1 = u - x2 = 1 - z2 = 0 - x3 = u - z3 = 1 - swap = 0 - - for t in range(448-1, -1, -1): - k_t = (k >> t) & 1 - swap ^= k_t - x2, x3 = X448.cswap(swap, x2, x3) - z2, z3 = X448.cswap(swap, z2, z3) - swap = k_t - - a = (x2 + z2) % P - aa = (a * a) % P - b = (x2 - z2) % P - bb = (b * b) % P - e = (aa - bb) % P - c = (x3 + z3) % P - d = (x3 - z3) % P - da = (d * a) % P - cb = (c * b) % P - x3 = pow((da + cb) % P, 2, P) - z3 = (x1 * pow((da - cb) % P, 2, P)) % P - x2 = (aa * bb) % P - z2 = (e * ((aa + (A24 * e) % P) % P)) % P - - x2, x3 = X448.cswap(swap, x2, x3) - z2, z3 = X448.cswap(swap, z2, z3) - - return (x2 * pow(z2, P - 2, P)) % P - - @staticmethod - def mul(n, p): - return X448.encode_u_coordinate(X448.x448(X448.decode_scalar448(n), X448.decode_little_endian(p))) - - @staticmethod - def mul_5(n): - return X448.encode_u_coordinate(X448.x448(X448.decode_scalar448(n), 5)) - - -# Self-test -# Test vectors taken from RFC 7748 section 5.2 and 6.2 -scalar1 = bytes.fromhex( - '203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f') -u1 = bytes.fromhex( - '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db') -assert X448.mul(scalar1, u1) == bytes.fromhex( - '884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d') - -scalar2 = bytes.fromhex( - '3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3') -u2 = bytes.fromhex( - '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086') -assert X448.mul(scalar2, u2) == bytes.fromhex( - 'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f') - -alice_priv = bytes.fromhex( - '9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b') -alice_pub = bytes.fromhex( - '9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0') -bob_priv = bytes.fromhex( - '1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d') -bob_pub = bytes.fromhex( - '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609') -assert alice_pub == X448.mul_5(alice_priv) -assert bob_pub == X448.mul_5(bob_priv) -assert X448.mul(alice_priv, bob_pub) == X448.mul(bob_priv, alice_pub) == bytes.fromhex( - '07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d') diff --git a/noise/functions.py b/noise/functions.py deleted file mode 100644 index 72e48a2..0000000 --- a/noise/functions.py +++ /dev/null @@ -1,251 +0,0 @@ -import abc -import warnings -from functools import partial -import os - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import x25519 -from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 -from cryptography.hazmat.primitives.hmac import HMAC -from noise.constants import MAX_NONCE -from noise.exceptions import NoiseValueError -from .crypto import X448 - -backend = default_backend() - - -class DH(object): - def __init__(self, method): - if method == 'ed25519': - self.method = method - self.dhlen = 32 - self.keypair_cls = KeyPair25519 - self.generate_keypair = self._25519_generate_keypair - self.dh = self._25519_dh - elif method == 'ed448': - self.method = method - self.dhlen = 56 - self.keypair_cls = KeyPair448 - self.generate_keypair = self._448_generate_keypair - self.dh = self._448_dh - else: - raise NotImplementedError('DH method: {}'.format(method)) - - def _25519_generate_keypair(self) -> '_KeyPair': - private_key = x25519.X25519PrivateKey.generate() - public_key = private_key.public_key() - return KeyPair25519(private_key, public_key, public_key.public_bytes()) - - def _25519_dh(self, private_key: 'x25519.X25519PrivateKey', public_key: 'x25519.X25519PublicKey') -> bytes: - if not isinstance(private_key, x25519.X25519PrivateKey) or not isinstance(public_key, x25519.X25519PublicKey): - raise NoiseValueError('Invalid keys! Must be x25519.X25519PrivateKey and x25519.X25519PublicKey instances') - return private_key.exchange(public_key) - - def _448_generate_keypair(self) -> '_KeyPair': - return KeyPair448.new() - - def _448_dh(self, private_key: bytes, public_key: bytes) -> bytes: - if len(private_key) != self.dhlen or len(public_key) != self.dhlen: - raise NoiseValueError('Invalid length of keys! Should be {}'.format(self.dhlen)) - return X448.mul(private_key, public_key) - - -class Cipher(object): - def __init__(self, method): - if method == 'AESGCM': - self._cipher = AESGCM - self.encrypt = self._aesgcm_encrypt - self.decrypt = self._aesgcm_decrypt - self.rekey = self._default_rekey - elif method == 'ChaCha20': - self._cipher = ChaCha20Poly1305 - self.encrypt = self._chacha20_encrypt - self.decrypt = self._chacha20_decrypt - self.rekey = self._default_rekey - else: - raise NotImplementedError('Cipher method: {}'.format(method)) - self.cipher = None - - def _aesgcm_encrypt(self, k, n, ad, plaintext): - return self.cipher.encrypt(nonce=self._aesgcm_nonce(n), data=plaintext, associated_data=ad) - - def _aesgcm_decrypt(self, k, n, ad, ciphertext): - return self.cipher.decrypt(nonce=self._aesgcm_nonce(n), data=ciphertext, associated_data=ad) - - def _aesgcm_nonce(self, n): - return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='big') - - def _chacha20_encrypt(self, k, n, ad, plaintext): - return self.cipher.encrypt(nonce=self._chacha20_nonce(n), data=plaintext, associated_data=ad) - - def _chacha20_decrypt(self, k, n, ad, ciphertext): - return self.cipher.decrypt(nonce=self._chacha20_nonce(n), data=ciphertext, associated_data=ad) - - def _chacha20_nonce(self, n): - return b'\x00\x00\x00\x00' + n.to_bytes(length=8, byteorder='little') - - def _default_rekey(self, k): - return self.encrypt(k, MAX_NONCE, b'', b'\x00' * 32)[:32] - - def initialize(self, key): - self.cipher = self._cipher(key) - - -class Hash(object): - def __init__(self, method): - if method == 'SHA256': - self.hashlen = 32 - self.blocklen = 64 - self.hash = self._hash_sha256 - self.fn = hashes.SHA256 - elif method == 'SHA512': - self.hashlen = 64 - self.blocklen = 128 - self.hash = self._hash_sha512 - self.fn = hashes.SHA512 - elif method == 'BLAKE2s': - self.hashlen = 32 - self.blocklen = 64 - self.hash = self._hash_blake2s - self.fn = partial(hashes.BLAKE2s, digest_size=self.hashlen) - elif method == 'BLAKE2b': - self.hashlen = 64 - self.blocklen = 128 - self.hash = self._hash_blake2b - self.fn = partial(hashes.BLAKE2b, digest_size=self.hashlen) - else: - raise NotImplementedError('Hash method: {}'.format(method)) - - def _hash_sha256(self, data): - digest = hashes.Hash(hashes.SHA256(), backend) - digest.update(data) - return digest.finalize() - - def _hash_sha512(self, data): - digest = hashes.Hash(hashes.SHA512(), backend) - digest.update(data) - return digest.finalize() - - def _hash_blake2s(self, data): - digest = hashes.Hash(hashes.BLAKE2s(digest_size=self.hashlen), backend) - digest.update(data) - return digest.finalize() - - def _hash_blake2b(self, data): - digest = hashes.Hash(hashes.BLAKE2b(digest_size=self.hashlen), backend) - digest.update(data) - return digest.finalize() - - -class _KeyPair(object): - __metaclass__ = abc.ABCMeta - - def __init__(self, private=None, public=None, public_bytes=None): - self.private = private - self.public = public - self.public_bytes = public_bytes - - @classmethod - @abc.abstractmethod - def from_private_bytes(cls, private_bytes): - raise NotImplementedError - - @classmethod - @abc.abstractmethod - def from_public_bytes(cls, public_bytes): - raise NotImplementedError - - -class KeyPair25519(_KeyPair): - @classmethod - def from_private_bytes(cls, private_bytes): - if len(private_bytes) != 32: - raise NoiseValueError('Invalid length of private_bytes! Should be 32') - private = x25519.X25519PrivateKey.from_private_bytes(private_bytes) - public = private.public_key() - return cls(private=private, public=public, public_bytes=public.public_bytes()) - - @classmethod - def from_public_bytes(cls, public_bytes): - if len(public_bytes) != 32: - raise NoiseValueError('Invalid length of public_bytes! Should be 32') - public = x25519.X25519PublicKey.from_public_bytes(public_bytes) - return cls(public=public, public_bytes=public.public_bytes()) - - -class KeyPair448(_KeyPair): - def __init__(self, *args, **kwargs): - super(KeyPair448, self).__init__(*args, **kwargs) - warnings.warn('This implementation of ed448 is likely to be very insecure! USE ONLY FOR TESTING!') - - @classmethod - def from_private_bytes(cls, private_bytes): - if len(private_bytes) != 56: - raise NoiseValueError('Invalid length of private_bytes! Should be 56') - private = private_bytes - public = X448.mul_5(private) - return cls(private=private, public=public, public_bytes=public) - - @classmethod - def from_public_bytes(cls, public_bytes): - if len(public_bytes) != 56: - raise NoiseValueError('Invalid length of private_bytes! Should be 56') - return cls(public=public_bytes, public_bytes=public_bytes) - - @classmethod - def new(cls): - private = os.urandom(56) - public = X448.mul_5(private) - return cls(private=private, public=public, public_bytes=public) - - -dh_map = { - '25519': DH('ed25519'), - '448': DH('ed448') -} - -cipher_map = { - 'AESGCM': partial(Cipher, 'AESGCM'), - 'ChaChaPoly': partial(Cipher, 'ChaCha20') -} - -hash_map = { - 'BLAKE2s': Hash('BLAKE2s'), - 'BLAKE2b': Hash('BLAKE2b'), - 'SHA256': Hash('SHA256'), - 'SHA512': Hash('SHA512') -} - -keypair_map = { - '25519': KeyPair25519, - '448': KeyPair448 -} - - -def hmac_hash(key, data, algorithm): - # Applies HMAC using the HASH() function. - hmac = HMAC(key=key, algorithm=algorithm(), backend=backend) - hmac.update(data=data) - return hmac.finalize() - - -def hkdf(chaining_key, input_key_material, num_outputs, hmac_hash_fn): - # Sets temp_key = HMAC-HASH(chaining_key, input_key_material). - temp_key = hmac_hash_fn(chaining_key, input_key_material) - - # Sets output1 = HMAC-HASH(temp_key, byte(0x01)). - output1 = hmac_hash_fn(temp_key, b'\x01') - - # Sets output2 = HMAC-HASH(temp_key, output1 || byte(0x02)). - output2 = hmac_hash_fn(temp_key, output1 + b'\x02') - - # If num_outputs == 2 then returns the pair (output1, output2). - if num_outputs == 2: - return output1, output2 - - # Sets output3 = HMAC-HASH(temp_key, output2 || byte(0x03)). - output3 = hmac_hash_fn(temp_key, output2 + b'\x03') - - # Returns the triple (output1, output2, output3). - return output1, output2, output3 diff --git a/noise/functions/__init__.py b/noise/functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/noise/functions/cipher.py b/noise/functions/cipher.py new file mode 100644 index 0000000..823410a --- /dev/null +++ b/noise/functions/cipher.py @@ -0,0 +1,27 @@ +import abc + +from noise.constants import MAX_NONCE + + +class Cipher(metaclass=abc.ABCMeta): + def __init__(self): + self.cipher = None + + @property + @abc.abstractmethod + def klass(self): + raise NotImplementedError + + @abc.abstractmethod + def encrypt(self, k, n, ad, plaintext): + raise NotImplementedError + + @abc.abstractmethod + def decrypt(self, k, n, ad, ciphertext): + raise NotImplementedError + + def rekey(self, k): + return self.encrypt(k, MAX_NONCE, b'', b'\x00' * 32)[:32] + + def initialize(self, key): + self.cipher = self.klass(key) diff --git a/noise/functions/dh.py b/noise/functions/dh.py new file mode 100644 index 0000000..6b98639 --- /dev/null +++ b/noise/functions/dh.py @@ -0,0 +1,21 @@ +import abc + + +class DH(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def klass(self): + raise NotImplementedError + + @property + @abc.abstractmethod + def dhlen(self): + raise NotImplementedError + + @abc.abstractmethod + def generate_keypair(self) -> 'KeyPair': + raise NotImplementedError + + @abc.abstractmethod + def dh(self, private_key, public_key) -> bytes: + raise NotImplementedError diff --git a/noise/functions/hash.py b/noise/functions/hash.py new file mode 100644 index 0000000..76c533c --- /dev/null +++ b/noise/functions/hash.py @@ -0,0 +1,43 @@ +import abc + + +class Hash(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def fn(self): + raise NotImplementedError + + @property + @abc.abstractmethod + def hashlen(self): + raise NotImplementedError + + @property + @abc.abstractmethod + def blocklen(self): + raise NotImplementedError + + @abc.abstractmethod + def hash(self, data): + raise NotImplementedError + + +def hkdf(chaining_key, input_key_material, num_outputs, hmac_hash_fn): + # Sets temp_key = HMAC-HASH(chaining_key, input_key_material). + temp_key = hmac_hash_fn(chaining_key, input_key_material) + + # Sets output1 = HMAC-HASH(temp_key, byte(0x01)). + output1 = hmac_hash_fn(temp_key, b'\x01') + + # Sets output2 = HMAC-HASH(temp_key, output1 || byte(0x02)). + output2 = hmac_hash_fn(temp_key, output1 + b'\x02') + + # If num_outputs == 2 then returns the pair (output1, output2). + if num_outputs == 2: + return output1, output2 + + # Sets output3 = HMAC-HASH(temp_key, output2 || byte(0x03)). + output3 = hmac_hash_fn(temp_key, output2 + b'\x03') + + # Returns the triple (output1, output2, output3). + return output1, output2, output3 \ No newline at end of file diff --git a/noise/functions/keypair.py b/noise/functions/keypair.py new file mode 100644 index 0000000..fa18ff4 --- /dev/null +++ b/noise/functions/keypair.py @@ -0,0 +1,18 @@ +import abc + + +class KeyPair(metaclass=abc.ABCMeta): + def __init__(self, private=None, public=None, public_bytes=None): + self.private = private + self.public = public + self.public_bytes = public_bytes + + @classmethod + @abc.abstractmethod + def from_private_bytes(cls, private_bytes): + raise NotImplementedError + + @classmethod + @abc.abstractmethod + def from_public_bytes(cls, public_bytes): + raise NotImplementedError diff --git a/noise/functions/patterns.py b/noise/functions/patterns.py new file mode 100644 index 0000000..b8c4613 --- /dev/null +++ b/noise/functions/patterns.py @@ -0,0 +1,78 @@ +from typing import List + +from noise.constants import TOKEN_PSK + + +class Pattern(object): + """ + TODO document + """ + def __init__(self): + # As per specification, if both parties have pre-messages, the initiator is listed first. To reduce complexity, + # pre_messages shall be a list of two lists: + # the first for the initiator's pre-messages, the second for the responder + self.pre_messages = [ + [], + [] + ] + + # List of lists of valid tokens, alternating between tokens for initiator and responder + self.tokens = [] + + self.name = '' + self.one_way = False + self.psk_count = 0 + + def has_pre_messages(self): + return any(map(lambda x: len(x) > 0, self.pre_messages)) + + def get_initiator_pre_messages(self) -> list: + return self.pre_messages[0].copy() + + def get_responder_pre_messages(self) -> list: + return self.pre_messages[1].copy() + + def apply_pattern_modifiers(self, modifiers: List[str]) -> None: + # Applies given pattern modifiers to self.tokens of the Pattern instance. + for modifier in modifiers: + if modifier.startswith('psk'): + try: + index = int(modifier.replace('psk', '', 1)) + except ValueError: + raise ValueError('Improper psk modifier {}'.format(modifier)) + + if index // 2 > len(self.tokens): + raise ValueError('Modifier {} cannot be applied - pattern has not enough messages'.format(modifier)) + + # Add TOKEN_PSK in the correct place in the correct message + if index == 0: # if 0, insert at the beginning of first message + self.tokens[0].insert(0, TOKEN_PSK) + else: # if bigger than zero, append at the end of first, second etc. + self.tokens[index - 1].append(TOKEN_PSK) + self.psk_count += 1 + + elif modifier == 'fallback': + raise NotImplementedError # TODO implement + + else: + raise ValueError('Unknown pattern modifier {}'.format(modifier)) + + def get_required_keypairs(self, initiator: bool) -> list: + required = [] + if initiator: + if self.name[0] in ('K', 'X', 'I'): + required.append('s') + if self.one_way or self.name[1] == 'K': + required.append('rs') + else: + if self.name[0] == 'K': + required.append('rs') + if self.one_way or self.name[1] in ['K', 'X']: + required.append('s') + return required + + +class OneWayPattern(Pattern): + def __init__(self): + super(OneWayPattern, self).__init__() + self.one_way = True diff --git a/noise/noise_protocol.py b/noise/noise_protocol.py index c3ad8da..3eb065e 100644 --- a/noise/noise_protocol.py +++ b/noise/noise_protocol.py @@ -5,48 +5,37 @@ from typing import Tuple from noise.exceptions import NoiseProtocolNameError, NoisePSKError, NoiseValidationError from noise.state import HandshakeState from .constants import MAX_PROTOCOL_NAME_LEN, Empty -from .functions import dh_map, cipher_map, hash_map, keypair_map, hmac_hash, hkdf -from .patterns import patterns_map class NoiseProtocol(object): """ TODO: Document """ - methods = { - 'pattern': patterns_map, - 'dh': dh_map, - 'cipher': cipher_map, - 'hash': hash_map, - 'keypair': keypair_map - } - - def __init__(self, protocol_name: bytes): - if not isinstance(protocol_name, bytes): - raise NoiseProtocolNameError('Protocol name has to be of type "bytes" not {}'.format(type(protocol_name))) - if len(protocol_name) > MAX_PROTOCOL_NAME_LEN: - raise NoiseProtocolNameError('Protocol name too long, has to be at most ' - '{} chars long'.format(MAX_PROTOCOL_NAME_LEN)) - + def __init__(self, protocol_name: bytes, backend: 'NoiseBackend'): self.name = protocol_name - mappings, pattern_modifiers = self._parse_protocol_name() + self.backend = backend + unpacked_name = UnpackedName.from_protocol_name(self.name) + mappings = self.backend.map_protocol_name_to_crypto(unpacked_name) # A valid Pattern instance (see Section 7 of specification (rev 32)) self.pattern = mappings['pattern']() - self.pattern_modifiers = pattern_modifiers + self.pattern_modifiers = unpacked_name.pattern_modifiers if self.pattern_modifiers: - self.pattern.apply_pattern_modifiers(pattern_modifiers) + self.pattern.apply_pattern_modifiers(self.pattern_modifiers) # Handle PSK handshake options self.psks = None self.is_psk_handshake = any([modifier.startswith('psk') for modifier in self.pattern_modifiers]) - self.dh_fn = mappings['dh'] - self.cipher_fn = mappings['cipher'] - self.hash_fn = mappings['hash'] - self.keypair_fn = mappings['keypair'] - self.hmac = partial(hmac_hash, algorithm=self.hash_fn.fn) - self.hkdf = partial(hkdf, hmac_hash_fn=self.hmac) + # Preinitialized + self.dh_fn = mappings['dh']() + self.hash_fn = mappings['hash']() + self.hmac = partial(backend.hmac, algorithm=self.hash_fn.fn) + self.hkdf = partial(backend.hkdf, hmac_hash_fn=self.hmac) + + # Initialized where needed + self.cipher_class = mappings['cipher'] + self.keypair_class = mappings['keypair'] self.prologue = None self.initiator = None @@ -60,42 +49,6 @@ class NoiseProtocol(object): self.keypairs = {'s': None, 'e': None, 'rs': None, 're': None} - def _parse_protocol_name(self) -> Tuple[dict, list]: - unpacked = self.name.decode().split('_') - if unpacked[0] != 'Noise': - raise NoiseProtocolNameError('Noise Protocol name shall begin with Noise! Provided: {}'.format(self.name)) - - # Extract pattern name and pattern modifiers - pattern = '' - modifiers_str = None - for i, char in enumerate(unpacked[1]): - if char.isupper(): - pattern += char - else: - # End of pattern, now look for modifiers - modifiers_str = unpacked[1][i:] # Will be empty string if it exceeds string size - break - modifiers = modifiers_str.split('+') if modifiers_str else [] - - data = {'pattern': 'Pattern' + pattern, - 'dh': unpacked[2], - 'cipher': unpacked[3], - 'hash': unpacked[4], - 'keypair': unpacked[2], - 'pattern_modifiers': modifiers} - - mapped_data = {} - - # Validate if we know everything that Noise Protocol is supposed to use and map appropriate functions - for key, map_dict in self.methods.items(): - func = map_dict.get(data[key]) - if not func: - raise NoiseProtocolNameError('Unknown {} in Noise Protocol name, given {}, known {}'.format( - key, data[key], " ".join(map_dict))) - mapped_data[key] = func - - return mapped_data, modifiers - def handshake_done(self): if self.pattern.one_way: if self.initiator: @@ -110,7 +63,7 @@ class NoiseProtocol(object): del self.initiator del self.dh_fn del self.hash_fn - del self.keypair_fn + del self.keypair_class def validate(self): if self.is_psk_handshake: @@ -141,3 +94,46 @@ class NoiseProtocol(object): kwargs[keypair] = value self.handshake_state = HandshakeState.initialize(self, **kwargs) self.symmetric_state = self.handshake_state.symmetric_state + + +class UnpackedName: + def __init__(self, pattern, dh, cipher, hash, keypair, pattern_modifiers): + self.pattern = pattern + self.dh = dh + self.cipher = cipher + self.hash = hash + self.keypair = keypair + self.pattern_modifiers = pattern_modifiers + + @classmethod + def from_protocol_name(cls, name): + if not isinstance(name, bytes): + raise NoiseProtocolNameError('Protocol name has to be of type "bytes" not {}'.format(type(name))) + if len(name) > MAX_PROTOCOL_NAME_LEN: + raise NoiseProtocolNameError('Protocol name too long, has to be at most ' + '{} chars long'.format(MAX_PROTOCOL_NAME_LEN)) + + unpacked = name.decode().split('_') + if unpacked[0] != 'Noise': + raise NoiseProtocolNameError('Noise Protocol name shall begin with Noise! Provided: {}'.format(name)) + + # Extract pattern name and pattern modifiers + pattern = '' + modifiers_str = None + for i, char in enumerate(unpacked[1]): + if char.isupper(): + pattern += char + else: + # End of pattern, now look for modifiers + modifiers_str = unpacked[1][i:] # Will be empty string if it exceeds string size + break + modifiers = modifiers_str.split('+') if modifiers_str else [] + + return cls( + pattern=pattern, + dh=unpacked[2], + cipher=unpacked[3], + hash=unpacked[4], + keypair=unpacked[2], + pattern_modifiers=modifiers + ) diff --git a/noise/patterns.py b/noise/patterns.py index cca285a..e481f77 100644 --- a/noise/patterns.py +++ b/noise/patterns.py @@ -1,85 +1,9 @@ -from typing import List - -from .constants import TOKEN_E, TOKEN_S, TOKEN_EE, TOKEN_ES, TOKEN_SE, TOKEN_SS, TOKEN_PSK - - -class Pattern(object): - """ - TODO document - """ - def __init__(self): - # As per specification, if both parties have pre-messages, the initiator is listed first. To reduce complexity, - # pre_messages shall be a list of two lists: - # the first for the initiator's pre-messages, the second for the responder - self.pre_messages = [ - [], - [] - ] - - # List of lists of valid tokens, alternating between tokens for initiator and responder - self.tokens = [] - - self.name = '' - self.one_way = False - self.psk_count = 0 - - def has_pre_messages(self): - return any(map(lambda x: len(x) > 0, self.pre_messages)) - - def get_initiator_pre_messages(self) -> list: - return self.pre_messages[0].copy() - - def get_responder_pre_messages(self) -> list: - return self.pre_messages[1].copy() - - def apply_pattern_modifiers(self, modifiers: List[str]) -> None: - # Applies given pattern modifiers to self.tokens of the Pattern instance. - for modifier in modifiers: - if modifier.startswith('psk'): - try: - index = int(modifier.replace('psk', '', 1)) - except ValueError: - raise ValueError('Improper psk modifier {}'.format(modifier)) - - if index // 2 > len(self.tokens): - raise ValueError('Modifier {} cannot be applied - pattern has not enough messages'.format(modifier)) - - # Add TOKEN_PSK in the correct place in the correct message - if index == 0: # if 0, insert at the beginning of first message - self.tokens[0].insert(0, TOKEN_PSK) - else: # if bigger than zero, append at the end of first, second etc. - self.tokens[index - 1].append(TOKEN_PSK) - self.psk_count += 1 - - elif modifier == 'fallback': - raise NotImplementedError # TODO implement - - else: - raise ValueError('Unknown pattern modifier {}'.format(modifier)) - - def get_required_keypairs(self, initiator: bool) -> list: - required = [] - if initiator: - if self.name[0] in ('K', 'X', 'I'): - required.append('s') - if self.one_way or self.name[1] == 'K': - required.append('rs') - else: - if self.name[0] == 'K': - required.append('rs') - if self.one_way or self.name[1] in ['K', 'X']: - required.append('s') - return required +from noise.constants import TOKEN_S, TOKEN_E, TOKEN_ES, TOKEN_SS, TOKEN_EE, TOKEN_SE +from noise.functions.patterns import OneWayPattern, Pattern # One-way patterns -class OneWayPattern(Pattern): - def __init__(self): - super(OneWayPattern, self).__init__() - self.one_way = True - - class PatternN(OneWayPattern): def __init__(self): super(PatternN, self).__init__() @@ -281,22 +205,3 @@ class PatternIX(Pattern): [TOKEN_E, TOKEN_S], [TOKEN_E, TOKEN_EE, TOKEN_SE, TOKEN_S, TOKEN_ES] ] - - -patterns_map = { - 'PatternN': PatternN, - 'PatternK': PatternK, - 'PatternX': PatternX, - 'PatternNN': PatternNN, - 'PatternKN': PatternKN, - 'PatternNK': PatternNK, - 'PatternKK': PatternKK, - 'PatternNX': PatternNX, - 'PatternKX': PatternKX, - 'PatternXN': PatternXN, - 'PatternIN': PatternIN, - 'PatternXK': PatternXK, - 'PatternIK': PatternIK, - 'PatternXX': PatternXX, - 'PatternIX': PatternIX, -} diff --git a/noise/state.py b/noise/state.py index 63e53b6..312550f 100644 --- a/noise/state.py +++ b/noise/state.py @@ -16,7 +16,7 @@ class CipherState(object): def __init__(self, noise_protocol): self.k = Empty() self.n = None - self.cipher = noise_protocol.cipher_fn() + self.cipher = noise_protocol.cipher_class() def initialize_key(self, key): """ @@ -363,7 +363,7 @@ class HandshakeState(object): for token in message_pattern: if token == TOKEN_E: # Sets re to the next DHLEN bytes from the message. Calls MixHash(re.public_key). - self.re = self.noise_protocol.keypair_fn.from_public_bytes(bytes(message[:dhlen])) + self.re = self.noise_protocol.keypair_class.from_public_bytes(bytes(message[:dhlen])) message = message[dhlen:] self.symmetric_state.mix_hash(self.re.public_bytes) if self.noise_protocol.is_psk_handshake: @@ -378,7 +378,9 @@ class HandshakeState(object): else: temp = bytes(message[:dhlen]) message = message[dhlen:] - self.rs = self.noise_protocol.keypair_fn.from_public_bytes(self.symmetric_state.decrypt_and_hash(temp)) + self.rs = self.noise_protocol.keypair_class.from_public_bytes( + self.symmetric_state.decrypt_and_hash(temp) + ) elif token == TOKEN_EE: # Calls MixKey(DH(e, re)). diff --git a/requirements.txt b/requirements.txt index 20fa3b5..34d60e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -cryptography==2.1.4 +cryptography>=2.5 diff --git a/setup.py b/setup.py index 0187754..964f35d 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ except (IOError, ImportError): setup( name='noiseprotocol', - version='0.2.2', + version='0.3.0', description='Implementation of Noise Protocol Framework', long_description=long_description, url='https://github.com/plizonczyk/noiseprotocol', @@ -35,6 +35,6 @@ setup( ], keywords='cryptography noiseprotocol noise security', packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']), - install_requires=['cryptography>=2.1.4'], + install_requires=['cryptography>=2.5'], python_requires='~=3.5', # we like 3.5, 3.6, and beyond, but not 4.0 ) diff --git a/tests/test_rev33_compat.py b/tests/test_rev33_compat.py index 13eeb21..ec070c8 100644 --- a/tests/test_rev33_compat.py +++ b/tests/test_rev33_compat.py @@ -1,3 +1,4 @@ +from noise.backends.default import noise_backend from noise.noise_protocol import NoiseProtocol from noise.state import CipherState, SymmetricState @@ -8,12 +9,14 @@ class TestRevision33Compatibility(object): fn = None noise_name = b"Noise_NN_25519_AESGCM_SHA3/256" + + modified_backend = noise_backend + modified_backend.hashes['SHA3/256'] = FakeSHA3_256 # Add callable to hash functions mapping modified_class = NoiseProtocol - modified_class.methods['hash']['SHA3/256'] = FakeSHA3_256 # Add callable to hash functions mapping - modified_class(noise_name) + modified_class(noise_name, modified_backend) def test_cipher_state_set_nonce(self): - noise_protocol = NoiseProtocol(b"Noise_NN_25519_AESGCM_SHA256") + noise_protocol = NoiseProtocol(b"Noise_NN_25519_AESGCM_SHA256", backend=noise_backend) cipher_state = CipherState(noise_protocol) cipher_state.initialize_key(b'\x00'*32) assert cipher_state.n == 0