diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db88ec5..8d45122 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,3 +24,9 @@ repos: language: system require_serial: true types_or: [python, pyi] + - id: pylint + name: pylint + entry: poetry run pylint + language: system + types: [python] + require_serial: true diff --git a/examples/test_client.py b/examples/test_client.py index f9521b7..1101b6e 100644 --- a/examples/test_client.py +++ b/examples/test_client.py @@ -1,3 +1,5 @@ +"""Example SHS client""" + import os from asyncio import get_event_loop from base64 import b64decode @@ -7,11 +9,13 @@ from nacl.signing import SigningKey from secret_handshake import SHSClient -with open(os.path.expanduser("~/.ssb/secret")) as f: +with open(os.path.expanduser("~/.ssb/secret"), encoding="utf-8") as f: config = yaml.safe_load(f) async def main(): + """Main function to run""" + server_pub_key = b64decode(config["public"][:-8]) client = SHSClient("localhost", 8008, SigningKey.generate(), server_pub_key) await client.open() diff --git a/examples/test_server.py b/examples/test_server.py index 25681af..a331dbb 100644 --- a/examples/test_server.py +++ b/examples/test_server.py @@ -1,3 +1,5 @@ +"""Example SHS server""" + import os from asyncio import get_event_loop from base64 import b64decode @@ -7,7 +9,7 @@ from nacl.signing import SigningKey from secret_handshake import SHSServer -with open(os.path.expanduser("~/.ssb/secret")) as f: +with open(os.path.expanduser("~/.ssb/secret"), encoding="utf-8") as f: config = yaml.safe_load(f) @@ -17,6 +19,8 @@ async def _on_connect(conn): async def main(): + """Main function to run""" + server_keypair = SigningKey(b64decode(config["private"][:-8])[:32]) server = SHSServer("localhost", 8008, server_keypair) server.on_connect(_on_connect) diff --git a/poetry.lock b/poetry.lock index 3799f1f..ceb3525 100644 --- a/poetry.lock +++ b/poetry.lock @@ -25,6 +25,17 @@ files = [ [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] +[[package]] +name = "astroid" +version = "3.0.1" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"}, + {file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"}, +] + [[package]] name = "babel" version = "2.13.1" @@ -431,6 +442,20 @@ files = [ {file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"}, ] +[[package]] +name = "dill" +version = "0.3.7" +description = "serialize all of Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + [[package]] name = "distlib" version = "0.3.7" @@ -638,6 +663,17 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -789,6 +825,30 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pylint" +version = "3.0.2" +description = "python code static checker" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "pylint-3.0.2-py3-none-any.whl", hash = "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"}, + {file = "pylint-3.0.2.tar.gz", hash = "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496"}, +] + +[package.dependencies] +astroid = ">=3.0.1,<=3.1.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + [[package]] name = "pynacl" version = "1.5.0" @@ -1249,4 +1309,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "b578043d7c76e0f421f1e0557bbea78870cca9fb9f8713dd3b7299f08823546b" +content-hash = "82c05c7c22b990f45fae70d36d0bed6f1c414b30af9e40318dfc383ebab9d86a" diff --git a/pyproject.toml b/pyproject.toml index b971e04..c53392e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,14 @@ pytest-mock = "^3.12.0" pre-commit = "^3.5.0" commitizen = "^3.12.0" black = "^23.10.1" +pylint = "^3.0.2" [tool.poetry.group.docs.dependencies] sphinx = "^7.2.6" +[tool.poetry.group.examples.dependencies] +pyyaml = "^6.0.1" + [tool.black] line-length = 120 @@ -47,6 +51,9 @@ skip_covered = true fail_under = 91 omit = ["examples/*"] +[tool.pylint.format] +max-line-length = 120 + [tool.pytest.ini_options] addopts = "--cov=. --no-cov-on-fail" ignore = "examples" diff --git a/secret_handshake/__init__.py b/secret_handshake/__init__.py index 288645a..dbff0e0 100644 --- a/secret_handshake/__init__.py +++ b/secret_handshake/__init__.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Secret Handshake""" from .network import SHSClient, SHSServer diff --git a/secret_handshake/boxstream.py b/secret_handshake/boxstream.py index d248c59..67695a0 100644 --- a/secret_handshake/boxstream.py +++ b/secret_handshake/boxstream.py @@ -1,5 +1,8 @@ -import struct +"""Box stream utilities""" + from asyncio import IncompleteReadError +import struct +from typing import Tuple from nacl.secret import SecretBox @@ -10,11 +13,9 @@ MAX_SEGMENT_SIZE = 4 * 1024 TERMINATION_HEADER = b"\x00" * 18 -def get_stream_pair(reader, writer, **kwargs): - """Return a tuple with `(unbox_stream, box_stream)` (reader/writer). +def get_stream_pair(reader, writer, **kwargs) -> Tuple["UnboxStream", "BoxStream"]: + """Create a new duplex box stream""" - :return: (:class:`secret_handshake.boxstream.UnboxStream`, - :class:`secret_handshake.boxstream.BoxStream`)""" box_args = { "key": kwargs["encrypt_key"], "nonce": kwargs["encrypt_nonce"], @@ -26,7 +27,9 @@ def get_stream_pair(reader, writer, **kwargs): return UnboxStream(reader, **unbox_args), BoxStream(writer, **box_args) -class UnboxStream(object): +class UnboxStream: + """Unboxing stream""" + def __init__(self, reader, key, nonce): self.reader = reader self.key = key @@ -34,6 +37,8 @@ class UnboxStream(object): self.closed = False async def read(self): + """Read data from the stream""" + try: data = await self.reader.readexactly(HEADER_LENGTH) except IncompleteReadError: @@ -70,7 +75,9 @@ class UnboxStream(object): return data -class BoxStream(object): +class BoxStream: + """Box stream""" + def __init__(self, writer, key, nonce): self.writer = writer self.key = key @@ -78,6 +85,8 @@ class BoxStream(object): self.nonce = nonce def write(self, data): + """Write data to the box stream""" + for chunk in split_chunks(data, MAX_SEGMENT_SIZE): body = self.box.encrypt(chunk, inc_nonce(self.nonce))[24:] header = struct.pack(">H", len(body) - 16) + body[:16] @@ -89,4 +98,6 @@ class BoxStream(object): self.writer.write(body[16:]) def close(self): + """Close the box stream""" + self.writer.write(self.box.encrypt(b"\x00" * 18, self.nonce)[24:]) diff --git a/secret_handshake/crypto.py b/secret_handshake/crypto.py index 0f61c3d..879b272 100644 --- a/secret_handshake/crypto.py +++ b/secret_handshake/crypto.py @@ -18,10 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Cryptography functions""" +from base64 import b64decode import hashlib import hmac -from base64 import b64decode +from typing import Optional from nacl.bindings import crypto_box_afternm, crypto_box_open_afternm, crypto_scalarmult from nacl.exceptions import CryptoError @@ -34,13 +36,19 @@ APPLICATION_KEY = b64decode("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=") class SHSError(Exception): """A SHS exception.""" - pass +class SHSCryptoBase: # pylint: disable=too-many-instance-attributes + """Base functions for SHS cryptography""" -class SHSCryptoBase(object): def __init__(self, local_key, ephemeral_key=None, application_key=None): self.local_key = local_key self.application_key = application_key or APPLICATION_KEY + self.shared_hash = None + self.remote_ephemeral_key = None + self.shared_secret = None + self.remote_app_hmac = None + self.remote_pub_key = None + self.box_secret = None self._reset_keys(ephemeral_key or PrivateKey.generate()) def _reset_keys(self, ephemeral_key): @@ -71,12 +79,16 @@ class SHSCryptoBase(object): return ok def clean(self, new_ephemeral_key=None): + """Clean internal data""" + self._reset_keys(new_ephemeral_key or PrivateKey.generate()) self.shared_secret = None self.shared_hash = None self.remote_ephemeral_key = None def get_box_keys(self): + """Get the box stream’s keys""" + shared_secret = hashlib.sha256(self.box_secret).digest() return { "shared_secret": shared_secret, @@ -88,7 +100,18 @@ class SHSCryptoBase(object): class SHSServerCrypto(SHSCryptoBase): + """SHS server crypto algorithm""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.b_alice = None + self.hello = None + self.box_secret = None + self.remote_pub_key = None + def verify_client_auth(self, data): + """Verify client authentication data""" + assert len(data) == 112 a_bob = crypto_scalarmult(bytes(self.local_key.to_curve25519_private_key()), self.remote_ephemeral_key) box_secret = hashlib.sha256(self.application_key + self.shared_secret + a_bob).digest() @@ -107,12 +130,13 @@ class SHSServerCrypto(SHSCryptoBase): return True def generate_accept(self): + """Generate an accept message""" + okay = self.local_key.sign(self.application_key + self.hello + self.shared_hash).signature - d = crypto_box_afternm(okay, b"\x00" * 24, self.box_secret) - return d + return crypto_box_afternm(okay, b"\x00" * 24, self.box_secret) def clean(self, new_ephemeral_key=None): - super(SHSServerCrypto, self).clean(new_ephemeral_key=new_ephemeral_key) + super().clean(new_ephemeral_key=new_ephemeral_key) self.hello = None self.b_alice = None @@ -120,19 +144,29 @@ class SHSServerCrypto(SHSCryptoBase): class SHSClientCrypto(SHSCryptoBase): """An object that encapsulates all the SHS client-side crypto. - :param local_key: the keypair used by the client (:class:`nacl.public.PrivateKey` object) - :param server_pub_key: the server's public key (``byte`` string) - :param ephemeral_key: a fresh local :class:`nacl.public.PrivateKey` - :param application_key: the unique application key (``byte`` string), defaults to SSB's + :param local_key: the keypair used by the client + :param server_pub_key: the server's public key + :param ephemeral_key: a fresh local private key + :param application_key: the unique application key, defaults to SSB's """ - def __init__(self, local_key, server_pub_key, ephemeral_key, application_key=None): - super(SHSClientCrypto, self).__init__(local_key, ephemeral_key, application_key) + def __init__( + self, + local_key: PrivateKey, + server_pub_key: bytes, + ephemeral_key: PrivateKey, + application_key: Optional[bytes] = None, + ): + super().__init__(local_key, ephemeral_key, application_key) self.remote_pub_key = VerifyKey(server_pub_key) + self.b_alice = None + self.a_bob = None + self.hello = None + self.box_secret = None def verify_server_challenge(self, data): """Verify the correctness of challenge sent from the server.""" - assert super(SHSClientCrypto, self).verify_challenge(data) + assert super().verify_challenge(data) curve_pkey = self.remote_pub_key.to_curve25519_public_key() # a_bob is (a * B) @@ -168,8 +202,8 @@ class SHSClientCrypto(SHSCryptoBase): try: # let's use the box secret to unbox our encrypted message signature = crypto_box_open_afternm(data, nonce, self.box_secret) - except CryptoError: - raise SHSError("Error decrypting server acceptance message") + except CryptoError as exc: + raise SHSError("Error decrypting server acceptance message") from exc # we should have received sign(B)[K | H | hash(a * b)] # let's see if that signature can verify the reconstructed data on our side @@ -177,6 +211,6 @@ class SHSClientCrypto(SHSCryptoBase): return True def clean(self, new_ephemeral_key=None): - super(SHSClientCrypto, self).clean(new_ephemeral_key=new_ephemeral_key) + super().clean(new_ephemeral_key=new_ephemeral_key) self.a_bob = None self.b_alice = None diff --git a/secret_handshake/network.py b/secret_handshake/network.py index e4719b8..1e665df 100644 --- a/secret_handshake/network.py +++ b/secret_handshake/network.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Networking functionality""" import asyncio @@ -26,22 +27,30 @@ from .crypto import SHSClientCrypto, SHSServerCrypto class SHSClientException(Exception): - pass + """Base exception class for client errors""" -class SHSDuplexStream(object): +class SHSDuplexStream: + """SHS duplex stream""" + def __init__(self): self.write_stream = None self.read_stream = None self.is_connected = False def write(self, data): + """Write data to the write stream""" + self.write_stream.write(data) async def read(self): + """Read data from the read stream""" + return await self.read_stream.read() def close(self): + """Close the duplex stream""" + self.write_stream.close() self.read_stream.close() self.is_connected = False @@ -58,21 +67,27 @@ class SHSDuplexStream(object): return msg -class SHSEndpoint(object): +class SHSEndpoint: + """SHS endpoint""" + def __init__(self): self._on_connect = None self.crypto = None def on_connect(self, cb): + """Set the function to be called when a new connection arrives""" self._on_connect = cb def disconnect(self): + """Disconnect the endpoint""" raise NotImplementedError class SHSServer(SHSEndpoint): + """SHS server""" + def __init__(self, host, port, server_kp, application_key=None): - super(SHSServer, self).__init__() + super().__init__() self.host = host self.port = port self.crypto = SHSServerCrypto(server_kp, application_key=application_key) @@ -92,6 +107,8 @@ class SHSServer(SHSEndpoint): writer.write(self.crypto.generate_accept()) async def handle_connection(self, reader, writer): + """Handle incoming connections""" + self.crypto.clean() await self._handshake(reader, writer) keys = self.crypto.get_box_keys() @@ -104,6 +121,8 @@ class SHSServer(SHSEndpoint): asyncio.ensure_future(self._on_connect(conn)) async def listen(self): + """Listen for connections""" + await asyncio.start_server(self.handle_connection, self.host, self.port) def disconnect(self): @@ -112,23 +131,33 @@ class SHSServer(SHSEndpoint): class SHSServerConnection(SHSDuplexStream): + """SHS server connection""" + def __init__(self, read_stream, write_stream): - super(SHSServerConnection, self).__init__() + super().__init__() self.read_stream = read_stream self.write_stream = write_stream @classmethod def from_byte_streams(cls, reader, writer, **keys): + """Create a server connection from an existing byte stream""" + reader, writer = get_stream_pair(reader, writer, **keys) + return cls(reader, writer) class SHSClient(SHSDuplexStream, SHSEndpoint): - def __init__(self, host, port, client_kp, server_pub_key, ephemeral_key=None, application_key=None): + """SHS client""" + + def __init__( # pylint: disable=too-many-arguments + self, host, port, client_kp, server_pub_key, ephemeral_key=None, application_key=None + ): SHSDuplexStream.__init__(self) SHSEndpoint.__init__(self) self.host = host self.port = port + self.writer = None self.crypto = SHSClientCrypto( client_kp, server_pub_key, ephemeral_key=ephemeral_key, application_key=application_key ) @@ -147,6 +176,8 @@ class SHSClient(SHSDuplexStream, SHSEndpoint): raise SHSClientException("Server accept is not valid") async def open(self): + """Open the TCP connection""" + reader, writer = await asyncio.open_connection(self.host, self.port) await self._handshake(reader, writer) @@ -156,6 +187,7 @@ class SHSClient(SHSDuplexStream, SHSEndpoint): self.read_stream, self.write_stream = get_stream_pair(reader, writer, **keys) self.writer = writer self.is_connected = True + if self._on_connect: await self._on_connect() diff --git a/secret_handshake/util.py b/secret_handshake/util.py index d13f99f..69e17d2 100644 --- a/secret_handshake/util.py +++ b/secret_handshake/util.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Utility functions""" import struct @@ -26,11 +27,16 @@ MAX_NONCE = 8 * NONCE_SIZE def inc_nonce(nonce): + """Increment nonce""" + num = bytes_to_long(nonce) + 1 + if num > 2**MAX_NONCE: num = 0 + bnum = long_to_bytes(num) bnum = b"\x00" * (NONCE_SIZE - len(bnum)) + bnum + return bnum @@ -44,6 +50,8 @@ def split_chunks(seq, n): # Stolen from PyCypto (Public Domain) def b(s): + """Shorthand for s.encode("latin-1")""" + return s.encode("latin-1") # utf-8 would cause some side-effects we don't want @@ -61,8 +69,8 @@ def long_to_bytes(n, blocksize=0): s = pack(">I", n & 0xFFFFFFFF) + s n = n >> 32 # strip off leading zeros - for i in range(len(s)): - if s[i] != b("\000")[0]: + for i, c in enumerate(s): + if c != b("\000")[0]: break else: # only happens when n == 0 diff --git a/tests/helpers.py b/tests/helpers.py index 5f0420b..714d11f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -26,8 +26,9 @@ from io import BytesIO class AsyncBuffer(BytesIO): """Just a BytesIO with an async read method.""" - async def read(self, n=None): - v = super(AsyncBuffer, self).read(n) + async def read(self, n=None): # pylint: disable=invalid-overridden-method + v = super().read(n) + return v readexactly = read diff --git a/tests/test_boxstream.py b/tests/test_boxstream.py index 6605995..a92d02a 100644 --- a/tests/test_boxstream.py +++ b/tests/test_boxstream.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Tests for the box stream""" import pytest @@ -27,17 +28,18 @@ from .helpers import AsyncBuffer, async_comprehend from .test_crypto import CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE MESSAGE_1 = ( - b"\xcev\xedE\x06l\x02\x13\xc8\x17V\xfa\x8bZ?\x88B%O\xb0L\x9f\x8e\x8c0y\x1dv\xc0\xc9\xf6\x9d\xc2\xdf\xdb" b"\xee\x9d" + b"\xcev\xedE\x06l\x02\x13\xc8\x17V\xfa\x8bZ?\x88B%O\xb0L\x9f\x8e\x8c0y\x1dv\xc0\xc9\xf6\x9d\xc2\xdf\xdb\xee\x9d" ) MESSAGE_2 = b"\x141\xd63\x13d\xd1\xecZ\x9b\xd0\xd4\x03\xcdR?'\xaa.\x89I\x92I\xf9guL\xaa\x06?\xea\xca/}\x88*\xb2" MESSAGE_3 = ( - b'\xcbYY\xf1\x0f\xa5O\x13r\xa6"\x15\xc5\x9d\r.*\x0b\x92\x10m\xa6(\x0c\x0c\xc61\x80j\x81)\x800\xed\xda' b"\xad\xa1" + b'\xcbYY\xf1\x0f\xa5O\x13r\xa6"\x15\xc5\x9d\r.*\x0b\x92\x10m\xa6(\x0c\x0c\xc61\x80j\x81)\x800\xed\xda\xad\xa1' ) MESSAGE_CLOSED = b"\xb1\x14hU'\xb5M\xa6\"\x03\x9duy\xa1\xd4evW,\xdcE\x18\xe4+ C4\xe8h\x96\xed\xc5\x94\x80" @pytest.mark.asyncio async def test_boxstream(): + """Test stream boxing""" buffer = AsyncBuffer() box_stream = BoxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) box_stream.write(b"foo") @@ -62,6 +64,8 @@ async def test_boxstream(): @pytest.mark.asyncio async def test_unboxstream(): + """Test stream unboxing""" + buffer = AsyncBuffer(MESSAGE_1 + MESSAGE_2 + MESSAGE_3 + MESSAGE_CLOSED) buffer.seek(0) @@ -73,6 +77,8 @@ async def test_unboxstream(): @pytest.mark.asyncio async def test_long_packets(): + """Test for receiving long packets""" + data_size = 6 * 1024 data = bytes(n % 256 for n in range(data_size)) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 7981940..511e941 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Tests for the crypto components""" import hashlib @@ -34,18 +35,24 @@ SERVER_EPH_KEY_SEED = b"ed\x1c\x01\x03s\x04\xdc\x8e`\xd6Z\xd0u;\xcbX\x91\xd8ZO\x CLIENT_EPH_KEY_SEED = b"u8\xd0\xe3\x85d_Pz\x0c\xf5\xfd\x15\xce2p#\xb0\xf0\x9f\xe6!\xe1\xcb\xf6\x93\t\xebr{1\x8b" -@pytest.fixture() +@pytest.fixture def server(): + """A testing SHS server""" + server_key = SigningKey(SERVER_KEY_SEED) server_eph_key = PrivateKey(SERVER_EPH_KEY_SEED) + return SHSServerCrypto(server_key, server_eph_key, application_key=APP_KEY) -@pytest.fixture() +@pytest.fixture def client(): + """A testing SHS client""" + client_key = SigningKey(CLIENT_KEY_SEED) server_key = SigningKey(SERVER_KEY_SEED) client_eph_key = PrivateKey(CLIENT_EPH_KEY_SEED) + return SHSClientCrypto(client_key, bytes(server_key.verify_key), client_eph_key, application_key=APP_KEY) @@ -55,19 +62,19 @@ CLIENT_CHALLENGE = ( b"\xe5\xc6K\xae\x94\xdbVt\x84\xdc\x1c@+D\x1c%" ) CLIENT_AUTH = ( - b"\xf2\xaf?z\x15\x10\xd0\xf0\xdf\xe3\x91\xfe\x14\x1c}z\xab\xeey\xf5\xef\xfc\xa1EdV\xf2T\x95s[!$z" - b"\xeb\x8f\x1b\x96JP\x17^\x92\xc8\x9e\xb4*5`\xf2\x8fI.\x93\xb9\x14:\xca@\x06\xff\xd1\xf1J\xc8t\xc4" - b"\xd8\xc3$[\xc5\x94je\x83\x00%\x99\x10\x16\xb1\xa2\xb2\xb7\xbf\xc9\x88\x14\xb9\xbb^\tzq\xa4\xef\xc5" - b"\xf5\x1f7#\xed\x92X\xb2\xe3\xe5\x8b[t3" + b"\xf2\xaf?z\x15\x10\xd0\xf0\xdf\xe3\x91\xfe\x14\x1c}z\xab\xeey\xf5\xef\xfc\xa1EdV\xf2T\x95s[!$" + b"z\xeb\x8f\x1b\x96JP\x17^\x92\xc8\x9e\xb4*5`\xf2\x8fI.\x93\xb9\x14:\xca@\x06\xff\xd1\xf1J\xc8t" + b"\xc4\xd8\xc3$[\xc5\x94je\x83\x00%\x99\x10\x16\xb1\xa2\xb2\xb7\xbf\xc9\x88\x14\xb9\xbb^\tzq" + b"\xa4\xef\xc5\xf5\x1f7#\xed\x92X\xb2\xe3\xe5\x8b[t3" ) SERVER_CHALLENGE = ( - b"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9\xeawl\xb9}\x85\xc3ik" - b"\x0c ($E\xb4\x8ax\xc4)t<\xd7\x8b\xd6\x07\xb7\xecw\x84\r\xe1-Iz`\xeb\x04\x89\xd6{" + b"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9\xeawl\xb9}\x85\xc3" + b"ik\x0c ($E\xb4\x8ax\xc4)t<\xd7\x8b\xd6\x07\xb7\xecw\x84\r\xe1-Iz`\xeb\x04\x89\xd6{" ) SERVER_ACCEPT = ( - b'\xb4\xd0\xea\xfb\xfb\xf6s\xcc\x10\xc4\x99\x95"\x13 y\xa6\xea.G\xeed\x8d=t9\x88|\x94\xd1\xbcK\xd47' - b"\xd8\xbcG1h\xac\xd0\xeb*\x1f\x8d\xae\x0b\x91G\xa1\xe6\x96b\xf2\xda90u\xeb_\xab\xdb\xcb%d7}\xb5\xce" - b"(k\x15\xe3L\x9d)\xd5\xa1|:" + b'\xb4\xd0\xea\xfb\xfb\xf6s\xcc\x10\xc4\x99\x95"\x13 y\xa6\xea.G\xeed\x8d=t9\x88|\x94\xd1\xbcK' + b"\xd47\xd8\xbcG1h\xac\xd0\xeb*\x1f\x8d\xae\x0b\x91G\xa1\xe6\x96b\xf2\xda90u\xeb_\xab\xdb\xcb%d" + b"7}\xb5\xce(k\x15\xe3L\x9d)\xd5\xa1|:" ) INTER_SHARED_SECRET = ( b"vf\xd82\xaeU\xda]\x08\x9eZ\xd6\x06\xcc\xd3\x99\xfd\xce\xc5\x16e8n\x9a\x04\x04\x84\xc5\x1a" b"\x8f\xf2M" @@ -83,7 +90,9 @@ CLIENT_ENCRYPT_NONCE = b"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x CLIENT_DECRYPT_NONCE = b"d\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7" -def test_handshake(client, server): +def test_handshake(client, server): # pylint: disable=redefined-outer-name + """Test the handshake procedure""" + client_challenge = client.generate_challenge() assert client_challenge == CLIENT_CHALLENGE assert server.verify_challenge(client_challenge) diff --git a/tests/test_network.py b/tests/test_network.py index b5bf94b..e6ba9a7 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -18,40 +18,59 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Tests for the networking components""" + import os from asyncio import Event, wait_for import pytest from nacl.signing import SigningKey +from secret_handshake import SHSClient, SHSServer + from .helpers import AsyncBuffer -class DummyCrypto(object): +class DummyCrypto: """Dummy crypto module, pretends everything is fine.""" - def verify_server_challenge(self, data): + def verify_server_challenge(self, _): + """Verify the server challenge""" + return True - def verify_challenge(self, data): + def verify_challenge(self, _): + """Verify the challenge data""" + return True - def verify_server_accept(self, data): + def verify_server_accept(self, _): + """Verify server’s accept message""" return True def generate_challenge(self): + """Generate authentication challenge""" + return b"CHALLENGE" def generate_client_auth(self): + """Generate client authentication data""" + return b"AUTH" - def verify_client_auth(self, data): + def verify_client_auth(self, _): + """Verify client authentication data""" + return True def generate_accept(self): + """Generate an ACCEPT message""" + return b"ACCEPT" def get_box_keys(self): + """Get box keys""" + return { "encrypt_key": b"x" * 32, "encrypt_nonce": b"x" * 32, @@ -60,10 +79,10 @@ class DummyCrypto(object): } def clean(self): - return + """Clean up internal data""" -def _dummy_boxstream(stream, **kwargs): +def _dummy_boxstream(stream, **_): """Identity boxstream, no tansformation.""" return stream @@ -72,7 +91,7 @@ def _client_stream_mocker(): reader = AsyncBuffer(b"xxx") writer = AsyncBuffer(b"xxx") - async def _create_mock_streams(host, port): + async def _create_mock_streams(host, port): # pylint: disable=unused-argument return reader, writer return reader, writer, _create_mock_streams @@ -82,7 +101,7 @@ def _server_stream_mocker(): reader = AsyncBuffer(b"xxx") writer = AsyncBuffer(b"xxx") - async def _create_mock_server(cb, host, port): + async def _create_mock_server(cb, host, port): # pylint: disable=unused-argument await cb(reader, writer) return reader, writer, _create_mock_server @@ -90,13 +109,13 @@ def _server_stream_mocker(): @pytest.mark.asyncio async def test_client(mocker): - reader, writer, _create_mock_streams = _client_stream_mocker() + """Test the client""" + + reader, _, _create_mock_streams = _client_stream_mocker() mocker.patch("asyncio.open_connection", new=_create_mock_streams) mocker.patch("secret_handshake.boxstream.BoxStream", new=_dummy_boxstream) mocker.patch("secret_handshake.boxstream.UnboxStream", new=_dummy_boxstream) - from secret_handshake import SHSClient - client = SHSClient("shop.local", 1111, SigningKey.generate(), os.urandom(32)) client.crypto = DummyCrypto() @@ -108,15 +127,15 @@ async def test_client(mocker): @pytest.mark.asyncio async def test_server(mocker): - from secret_handshake import SHSServer + """Test the server""" resolve = Event() - async def _on_connect(conn): + async def _on_connect(_): server.disconnect() resolve.set() - reader, writer, _create_mock_server = _server_stream_mocker() + _, _, _create_mock_server = _server_stream_mocker() mocker.patch("asyncio.start_server", new=_create_mock_server) mocker.patch("secret_handshake.boxstream.BoxStream", new=_dummy_boxstream) mocker.patch("secret_handshake.boxstream.UnboxStream", new=_dummy_boxstream)