diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6deafc2..0000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 120 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4302ed4..db88ec5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,3 +16,11 @@ repos: pass_filenames: false language: system stages: [push] + - id: black + name: black + description: "Black: The uncompromising Python code formatter" + entry: poetry run black + args: ["--diff"] + language: system + require_serial: true + types_or: [python, pyi] diff --git a/examples/test_client.py b/examples/test_client.py index f2699c4..94fbac9 100644 --- a/examples/test_client.py +++ b/examples/test_client.py @@ -7,13 +7,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")) as f: config = yaml.safe_load(f) async def main(): - server_pub_key = b64decode(config['public'][:-8]) - client = SHSClient('localhost', 8008, SigningKey.generate(), server_pub_key) + server_pub_key = b64decode(config["public"][:-8]) + client = SHSClient("localhost", 8008, SigningKey.generate(), server_pub_key) await client.open() async for msg in client: diff --git a/examples/test_server.py b/examples/test_server.py index edbbf6d..54125a6 100644 --- a/examples/test_server.py +++ b/examples/test_server.py @@ -7,7 +7,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")) as f: config = yaml.safe_load(f) @@ -17,11 +17,12 @@ async def _on_connect(conn): async def main(): - server_keypair = SigningKey(b64decode(config['private'][:-8])[:32]) - server = SHSServer('localhost', 8008, server_keypair) + server_keypair = SigningKey(b64decode(config["private"][:-8])[:32]) + server = SHSServer("localhost", 8008, server_keypair) server.on_connect(_on_connect) await server.listen() + loop = get_event_loop() loop.run_until_complete(main()) loop.run_forever() diff --git a/poetry.lock b/poetry.lock index 2ae594b..98f0353 100644 --- a/poetry.lock +++ b/poetry.lock @@ -53,6 +53,46 @@ setuptools = {version = "*", markers = "python_version >= \"3.12\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +[[package]] +name = "black" +version = "23.10.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "build" version = "1.0.3" @@ -278,6 +318,20 @@ setuptools = "*" [package.extras] test = ["mock (>=3.0.0)", "pytest"] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -595,6 +649,17 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "nodeenv" version = "1.8.0" @@ -620,6 +685,17 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + [[package]] name = "platformdirs" version = "3.11.0" @@ -1184,4 +1260,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "ea2b635a53716be43ebfd83e9e810998913f10f5da1cd5bf778c57b92c6836bb" +content-hash = "4f9ec52a576209d59ea926bd2e332f5f06b4b2158c2ea348185704d21c63bd73" diff --git a/pyproject.toml b/pyproject.toml index d72f880..ef91b18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,14 @@ pytest-asyncio = "^0.21.1" pytest-mock = "^3.12.0" pre-commit = "^3.5.0" commitizen = "^3.12.0" +black = "^23.10.1" [tool.poetry.group.docs.dependencies] sphinx = "^7.2.6" +[tool.black] +line-length = 120 + [tool.commitizen] name = "cz_conventional_commits" tag_format = "$version" diff --git a/secret_handshake/__init__.py b/secret_handshake/__init__.py index 040f65c..288645a 100644 --- a/secret_handshake/__init__.py +++ b/secret_handshake/__init__.py @@ -21,4 +21,4 @@ from .network import SHSClient, SHSServer -__all__ = ('SHSClient', 'SHSServer') +__all__ = ("SHSClient", "SHSServer") diff --git a/secret_handshake/boxstream.py b/secret_handshake/boxstream.py index 04a42a1..bd2267e 100644 --- a/secret_handshake/boxstream.py +++ b/secret_handshake/boxstream.py @@ -8,21 +8,21 @@ from .util import inc_nonce, split_chunks HEADER_LENGTH = 2 + 16 + 16 MAX_SEGMENT_SIZE = 4 * 1024 -TERMINATION_HEADER = (b'\x00' * 18) +TERMINATION_HEADER = b"\x00" * 18 def get_stream_pair(reader, writer, **kwargs): """Return a tuple with `(unbox_stream, box_stream)` (reader/writer). :return: (:class:`secret_handshake.boxstream.UnboxStream`, - :class:`secret_handshake.boxstream.BoxStream`) """ + :class:`secret_handshake.boxstream.BoxStream`)""" box_args = { - 'key': kwargs['encrypt_key'], - 'nonce': kwargs['encrypt_nonce'], + "key": kwargs["encrypt_key"], + "nonce": kwargs["encrypt_nonce"], } unbox_args = { - 'key': kwargs['decrypt_key'], - 'nonce': kwargs['decrypt_nonce'], + "key": kwargs["decrypt_key"], + "nonce": kwargs["decrypt_nonce"], } return UnboxStream(reader, **unbox_args), BoxStream(writer, **box_args) @@ -49,7 +49,7 @@ class UnboxStream(object): self.closed = True return None - length = struct.unpack('>H', header[:2])[0] + length = struct.unpack(">H", header[:2])[0] mac = header[2:] data = await self.reader.readexactly(length) @@ -78,7 +78,7 @@ class BoxStream(object): def write(self, data): 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] + header = struct.pack(">H", len(body) - 16) + body[:16] hdrbox = self.box.encrypt(header, self.nonce)[24:] self.writer.write(hdrbox) @@ -87,4 +87,4 @@ class BoxStream(object): self.writer.write(body[16:]) def close(self): - self.writer.write(self.box.encrypt(b'\x00' * 18, self.nonce)[24:]) + 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 05b1315..0f61c3d 100644 --- a/secret_handshake/crypto.py +++ b/secret_handshake/crypto.py @@ -23,17 +23,17 @@ import hashlib import hmac from base64 import b64decode -from nacl.bindings import (crypto_box_afternm, crypto_box_open_afternm, - crypto_scalarmult) +from nacl.bindings import crypto_box_afternm, crypto_box_open_afternm, crypto_scalarmult from nacl.exceptions import CryptoError from nacl.public import PrivateKey from nacl.signing import VerifyKey -APPLICATION_KEY = b64decode('1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=') +APPLICATION_KEY = b64decode("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=") class SHSError(Exception): """A SHS exception.""" + pass @@ -45,8 +45,9 @@ class SHSCryptoBase(object): def _reset_keys(self, ephemeral_key): self.local_ephemeral_key = ephemeral_key - self.local_app_hmac = (hmac.new(self.application_key, bytes(ephemeral_key.public_key), digestmod='sha512') - .digest()[:32]) + self.local_app_hmac = hmac.new( + self.application_key, bytes(ephemeral_key.public_key), digestmod="sha512" + ).digest()[:32] def generate_challenge(self): """Generate and return a challenge to be sent to the server.""" @@ -57,7 +58,7 @@ class SHSCryptoBase(object): assert len(data) == 64 sent_hmac, remote_ephemeral_key = data[:32], data[32:] - h = hmac.new(self.application_key, remote_ephemeral_key, digestmod='sha512') + h = hmac.new(self.application_key, remote_ephemeral_key, digestmod="sha512") self.remote_app_hmac = h.digest()[:32] ok = self.remote_app_hmac == sent_hmac @@ -78,11 +79,11 @@ class SHSCryptoBase(object): def get_box_keys(self): shared_secret = hashlib.sha256(self.box_secret).digest() return { - 'shared_secret': shared_secret, - 'encrypt_key': hashlib.sha256(shared_secret + bytes(self.remote_pub_key)).digest(), - 'decrypt_key': hashlib.sha256(shared_secret + bytes(self.local_key.verify_key)).digest(), - 'encrypt_nonce': self.remote_app_hmac[:24], - 'decrypt_nonce': self.local_app_hmac[:24] + "shared_secret": shared_secret, + "encrypt_key": hashlib.sha256(shared_secret + bytes(self.remote_pub_key)).digest(), + "decrypt_key": hashlib.sha256(shared_secret + bytes(self.local_key.verify_key)).digest(), + "encrypt_nonce": self.remote_app_hmac[:24], + "decrypt_nonce": self.local_app_hmac[:24], } @@ -91,7 +92,7 @@ class SHSServerCrypto(SHSCryptoBase): 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() - self.hello = crypto_box_open_afternm(data, b'\x00' * 24, box_secret) + self.hello = crypto_box_open_afternm(data, b"\x00" * 24, box_secret) signature, public_key = self.hello[:64], self.hello[64:] signed = self.application_key + bytes(self.local_key.verify_key) + self.shared_hash pkey = VerifyKey(public_key) @@ -99,14 +100,15 @@ class SHSServerCrypto(SHSCryptoBase): # will raise an exception if verification fails pkey.verify(signed, signature) self.remote_pub_key = pkey - b_alice = crypto_scalarmult(bytes(self.local_ephemeral_key), - bytes(self.remote_pub_key.to_curve25519_public_key())) + b_alice = crypto_scalarmult( + bytes(self.local_ephemeral_key), bytes(self.remote_pub_key.to_curve25519_public_key()) + ) self.box_secret = hashlib.sha256(self.application_key + self.shared_secret + a_bob + b_alice).digest()[:32] return True def generate_accept(self): 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) + d = crypto_box_afternm(okay, b"\x00" * 24, self.box_secret) return d def clean(self, new_ephemeral_key=None): @@ -159,8 +161,7 @@ class SHSClientCrypto(SHSCryptoBase): b_alice = crypto_scalarmult(bytes(curve_lkey), self.remote_ephemeral_key) self.b_alice = b_alice # this is hash(K | a * b | a * B | A * b) - self.box_secret = hashlib.sha256(self.application_key + self.shared_secret + self.a_bob + - b_alice).digest() + self.box_secret = hashlib.sha256(self.application_key + self.shared_secret + self.a_bob + b_alice).digest() nonce = b"\x00" * 24 @@ -168,7 +169,7 @@ class SHSClientCrypto(SHSCryptoBase): # 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') + raise SHSError("Error decrypting server acceptance message") # 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 diff --git a/secret_handshake/network.py b/secret_handshake/network.py index 5416957..db907ee 100644 --- a/secret_handshake/network.py +++ b/secret_handshake/network.py @@ -77,13 +77,13 @@ class SHSServer(SHSEndpoint): async def _handshake(self, reader, writer): data = await reader.readexactly(64) if not self.crypto.verify_challenge(data): - raise SHSClientException('Client challenge is not valid') + raise SHSClientException("Client challenge is not valid") writer.write(self.crypto.generate_challenge()) data = await reader.readexactly(112) if not self.crypto.verify_client_auth(data): - raise SHSClientException('Client auth is not valid') + raise SHSClientException("Client auth is not valid") writer.write(self.crypto.generate_accept()) @@ -125,21 +125,22 @@ class SHSClient(SHSDuplexStream, SHSEndpoint): SHSEndpoint.__init__(self) self.host = host self.port = port - self.crypto = SHSClientCrypto(client_kp, server_pub_key, ephemeral_key=ephemeral_key, - application_key=application_key) + self.crypto = SHSClientCrypto( + client_kp, server_pub_key, ephemeral_key=ephemeral_key, application_key=application_key + ) async def _handshake(self, reader, writer): writer.write(self.crypto.generate_challenge()) data = await reader.readexactly(64) if not self.crypto.verify_server_challenge(data): - raise SHSClientException('Server challenge is not valid') + raise SHSClientException("Server challenge is not valid") writer.write(self.crypto.generate_client_auth()) data = await reader.readexactly(80) if not self.crypto.verify_server_accept(data): - raise SHSClientException('Server accept is not valid') + raise SHSClientException("Server accept is not valid") async def open(self): reader, writer = await asyncio.open_connection(self.host, self.port) diff --git a/secret_handshake/test_boxstream.py b/secret_handshake/test_boxstream.py index d035b58..395d222 100644 --- a/secret_handshake/test_boxstream.py +++ b/secret_handshake/test_boxstream.py @@ -26,29 +26,31 @@ from secret_handshake.util 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') +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" +) 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') -MESSAGE_CLOSED = b'\xb1\x14hU\'\xb5M\xa6"\x03\x9duy\xa1\xd4evW,\xdcE\x18\xe4+ C4\xe8h\x96\xed\xc5\x94\x80' +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" +) +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(): buffer = AsyncBuffer() box_stream = BoxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) - box_stream.write(b'foo') + box_stream.write(b"foo") buffer.seek(0) assert await buffer.read() == MESSAGE_1 pos = buffer.tell() - box_stream.write(b'foo') + box_stream.write(b"foo") buffer.seek(pos) assert await buffer.read() == MESSAGE_2 pos = buffer.tell() - box_stream.write(b'bar') + box_stream.write(b"bar") buffer.seek(pos) assert await buffer.read() == MESSAGE_3 @@ -65,7 +67,7 @@ async def test_unboxstream(): unbox_stream = UnboxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) assert not unbox_stream.closed - assert (await async_comprehend(unbox_stream)) == [b'foo', b'foo', b'bar'] + assert (await async_comprehend(unbox_stream)) == [b"foo", b"foo", b"bar"] assert unbox_stream.closed diff --git a/secret_handshake/test_crypto.py b/secret_handshake/test_crypto.py index 05a2147..7981940 100644 --- a/secret_handshake/test_crypto.py +++ b/secret_handshake/test_crypto.py @@ -27,11 +27,11 @@ from nacl.signing import SigningKey from secret_handshake.crypto import SHSClientCrypto, SHSServerCrypto -APP_KEY = hashlib.sha256(b'app_key').digest() -SERVER_KEY_SEED = b'\xcaw\x01\xc2cQ\xfd\x94\x9f\x14\x84\x0c0\x12\\\x96\xcd\x9b\x0c\x02z&\x96!\xe0\xa2' -CLIENT_KEY_SEED = b'\xbf\x02<\xd3e\x9d\xac-\xd1\x9e-{\xe5q\x90\x03\x11\xba\x8cSQ\xa0\xc3p~\x89\xe6\xeeb\xaa\x1c\x17' +APP_KEY = hashlib.sha256(b"app_key").digest() +SERVER_KEY_SEED = b"\xcaw\x01\xc2cQ\xfd\x94\x9f\x14\x84\x0c0\x12\\\x96\xcd\x9b\x0c\x02z&\x96!\xe0\xa2" +CLIENT_KEY_SEED = b"\xbf\x02<\xd3e\x9d\xac-\xd1\x9e-{\xe5q\x90\x03\x11\xba\x8cSQ\xa0\xc3p~\x89\xe6\xeeb\xaa\x1c\x17" SERVER_EPH_KEY_SEED = b"ed\x1c\x01\x03s\x04\xdc\x8e`\xd6Z\xd0u;\xcbX\x91\xd8ZO\xf8\xf0\xd6'\xd5\xb1Yy\x13yH" -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' +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() @@ -49,28 +49,38 @@ def client(): return SHSClientCrypto(client_key, bytes(server_key.verify_key), client_eph_key, application_key=APP_KEY) -CLIENT_CHALLENGE = (b'd\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7j' - b'\xd7\xf0uc\xf0r\xf3\x7f\t\x18\xec\x8c\xf7\xff\x8e\xa9\xc83\x13\x18R\x16\x1d' - 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') -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{') -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|:') -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') -BOX_SECRET = b'\x03\xfe\xe3\x8c u\xbcl^\x17eD\x96\xa3\xa6\x880f\x11\x7f\x85\xf2:\xa3[`\x06[#l\xbcr' +CLIENT_CHALLENGE = ( + b"d\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7j" + b"\xd7\xf0uc\xf0r\xf3\x7f\t\x18\xec\x8c\xf7\xff\x8e\xa9\xc83\x13\x18R\x16\x1d" + 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" +) +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{" +) +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|:" +) +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" +) +BOX_SECRET = b"\x03\xfe\xe3\x8c u\xbcl^\x17eD\x96\xa3\xa6\x880f\x11\x7f\x85\xf2:\xa3[`\x06[#l\xbcr" -SHARED_SECRET = b'UV\xad*\x8e\xce\x88\xf2\x87l\x13iZ\x12\xd7\xa6\xd1\x9c-\x9d\x07\xf5\xa96\x03w\x11\xe5\x96$m\x1d' -CLIENT_ENCRYPT_KEY = (b'\xec\x1f,\x82\x9f\xedA\xc0\xda\x87[\xf9u\xbf\xac\x9cI\xa5T\xd1\x91\xff\xa8.\xd0 \xfbU\xc7\x14' - b')\xc7') -CLIENT_DECRYPT_KEY = b'\xf9e\xa0As\xb2=\xb7P~\xf3\xf9(\xfd\x7f\xfe\xb7TZhn\xd7\x8c=\xea.o\x9e\x8c9)\x10' -CLIENT_ENCRYPT_NONCE = b'S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9' -CLIENT_DECRYPT_NONCE = b'd\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7' +SHARED_SECRET = b"UV\xad*\x8e\xce\x88\xf2\x87l\x13iZ\x12\xd7\xa6\xd1\x9c-\x9d\x07\xf5\xa96\x03w\x11\xe5\x96$m\x1d" +CLIENT_ENCRYPT_KEY = ( + b"\xec\x1f,\x82\x9f\xedA\xc0\xda\x87[\xf9u\xbf\xac\x9cI\xa5T\xd1\x91\xff\xa8.\xd0 \xfbU\xc7\x14" b")\xc7" +) +CLIENT_DECRYPT_KEY = b"\xf9e\xa0As\xb2=\xb7P~\xf3\xf9(\xfd\x7f\xfe\xb7TZhn\xd7\x8c=\xea.o\x9e\x8c9)\x10" +CLIENT_ENCRYPT_NONCE = b"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9" +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): @@ -100,12 +110,12 @@ def test_handshake(client, server): client_keys = client.get_box_keys() server_keys = server.get_box_keys() - assert client_keys['shared_secret'] == SHARED_SECRET - assert client_keys['encrypt_key'] == CLIENT_ENCRYPT_KEY - assert client_keys['decrypt_key'] == CLIENT_DECRYPT_KEY - assert client_keys['encrypt_nonce'] == CLIENT_ENCRYPT_NONCE - assert client_keys['decrypt_nonce'] == CLIENT_DECRYPT_NONCE + assert client_keys["shared_secret"] == SHARED_SECRET + assert client_keys["encrypt_key"] == CLIENT_ENCRYPT_KEY + assert client_keys["decrypt_key"] == CLIENT_DECRYPT_KEY + assert client_keys["encrypt_nonce"] == CLIENT_ENCRYPT_NONCE + assert client_keys["decrypt_nonce"] == CLIENT_DECRYPT_NONCE - assert client_keys['shared_secret'] == server_keys['shared_secret'] - assert client_keys['encrypt_key'] == server_keys['decrypt_key'] - assert client_keys['encrypt_nonce'] == server_keys['decrypt_nonce'] + assert client_keys["shared_secret"] == server_keys["shared_secret"] + assert client_keys["encrypt_key"] == server_keys["decrypt_key"] + assert client_keys["encrypt_nonce"] == server_keys["decrypt_nonce"] diff --git a/secret_handshake/test_network.py b/secret_handshake/test_network.py index f19081d..bece634 100644 --- a/secret_handshake/test_network.py +++ b/secret_handshake/test_network.py @@ -29,6 +29,7 @@ from secret_handshake.util import AsyncBuffer class DummyCrypto(object): """Dummy crypto module, pretends everything is fine.""" + def verify_server_challenge(self, data): return True @@ -39,23 +40,23 @@ class DummyCrypto(object): return True def generate_challenge(self): - return b'CHALLENGE' + return b"CHALLENGE" def generate_client_auth(self): - return b'AUTH' + return b"AUTH" def verify_client_auth(self, data): return True def generate_accept(self): - return b'ACCEPT' + return b"ACCEPT" def get_box_keys(self): return { - 'encrypt_key': b'x' * 32, - 'encrypt_nonce': b'x' * 32, - 'decrypt_key': b'x' * 32, - 'decrypt_nonce': b'x' * 32 + "encrypt_key": b"x" * 32, + "encrypt_nonce": b"x" * 32, + "decrypt_key": b"x" * 32, + "decrypt_nonce": b"x" * 32, } def clean(self): @@ -68,8 +69,8 @@ def _dummy_boxstream(stream, **kwargs): def _client_stream_mocker(): - reader = AsyncBuffer(b'xxx') - writer = AsyncBuffer(b'xxx') + reader = AsyncBuffer(b"xxx") + writer = AsyncBuffer(b"xxx") async def _create_mock_streams(host, port): return reader, writer @@ -78,8 +79,8 @@ def _client_stream_mocker(): def _server_stream_mocker(): - reader = AsyncBuffer(b'xxx') - writer = AsyncBuffer(b'xxx') + reader = AsyncBuffer(b"xxx") + writer = AsyncBuffer(b"xxx") async def _create_mock_server(cb, host, port): await cb(reader, writer) @@ -90,18 +91,18 @@ def _server_stream_mocker(): @pytest.mark.asyncio async def test_client(mocker): reader, writer, _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) + 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 = SHSClient("shop.local", 1111, SigningKey.generate(), os.urandom(32)) client.crypto = DummyCrypto() await client.open() - reader.append(b'TEST') - assert (await client.read()) == b'TEST' + reader.append(b"TEST") + assert (await client.read()) == b"TEST" client.disconnect() @@ -116,11 +117,11 @@ async def test_server(mocker): resolve.set() reader, writer, _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) + 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) - server = SHSServer('shop.local', 1111, SigningKey.generate(), os.urandom(32)) + server = SHSServer("shop.local", 1111, SigningKey.generate(), os.urandom(32)) server.crypto = DummyCrypto() server.on_connect(_on_connect) diff --git a/secret_handshake/util.py b/secret_handshake/util.py index 0b4667e..c5e0053 100644 --- a/secret_handshake/util.py +++ b/secret_handshake/util.py @@ -23,14 +23,16 @@ import struct from io import BytesIO NONCE_SIZE = 24 -MAX_NONCE = (8 * NONCE_SIZE) +MAX_NONCE = 8 * NONCE_SIZE class AsyncBuffer(BytesIO): """Just a BytesIO with an async read method.""" + async def read(self, n=None): v = super(AsyncBuffer, self).read(n) return v + readexactly = read def append(self, data): @@ -50,10 +52,10 @@ async def async_comprehend(generator): def inc_nonce(nonce): num = bytes_to_long(nonce) + 1 - if num > 2 ** MAX_NONCE: + if num > 2**MAX_NONCE: num = 0 bnum = long_to_bytes(num) - bnum = b'\x00' * (NONCE_SIZE - len(bnum)) + bnum + bnum = b"\x00" * (NONCE_SIZE - len(bnum)) + bnum return bnum @@ -78,24 +80,24 @@ def long_to_bytes(n, blocksize=0): blocksize. """ # after much testing, this algorithm was deemed to be the fastest - s = b('') + s = b("") pack = struct.pack while n > 0: - s = pack('>I', n & 0xffffffff) + s + 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]: + if s[i] != b("\000")[0]: break else: # only happens when n == 0 - s = b('\000') + s = b("\000") i = 0 s = s[i:] # add back some pad bytes. this could be done more efficiently w.r.t. the # de-padding being done above, but sigh... if blocksize > 0 and len(s) % blocksize: - s = (blocksize - len(s) % blocksize) * b('\000') + s + s = (blocksize - len(s) % blocksize) * b("\000") + s return s @@ -108,9 +110,9 @@ def bytes_to_long(s): unpack = struct.unpack length = len(s) if length % 4: - extra = (4 - length % 4) - s = b('\000') * extra + s + extra = 4 - length % 4 + s = b("\000") * extra + s length = length + extra for i in range(0, length, 4): - acc = (acc << 32) + unpack('>I', s[i:i+4])[0] + acc = (acc << 32) + unpack(">I", s[i : i + 4])[0] return acc