ci: Lint source

This commit is contained in:
Gergely Polonkai 2023-10-29 09:55:39 +01:00
parent 53994b77a7
commit d28ca167f2
No known key found for this signature in database
GPG Key ID: 2D2885533B869ED4
14 changed files with 267 additions and 65 deletions

View File

@ -24,3 +24,9 @@ repos:
language: system language: system
require_serial: true require_serial: true
types_or: [python, pyi] types_or: [python, pyi]
- id: pylint
name: pylint
entry: poetry run pylint
language: system
types: [python]
require_serial: true

View File

@ -1,3 +1,5 @@
"""Example SHS client"""
import os import os
from asyncio import get_event_loop from asyncio import get_event_loop
from base64 import b64decode from base64 import b64decode
@ -7,11 +9,13 @@ from nacl.signing import SigningKey
from secret_handshake import SHSClient 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) config = yaml.safe_load(f)
async def main(): async def main():
"""Main function to run"""
server_pub_key = b64decode(config["public"][:-8]) server_pub_key = b64decode(config["public"][:-8])
client = SHSClient("localhost", 8008, SigningKey.generate(), server_pub_key) client = SHSClient("localhost", 8008, SigningKey.generate(), server_pub_key)
await client.open() await client.open()

View File

@ -1,3 +1,5 @@
"""Example SHS server"""
import os import os
from asyncio import get_event_loop from asyncio import get_event_loop
from base64 import b64decode from base64 import b64decode
@ -7,7 +9,7 @@ from nacl.signing import SigningKey
from secret_handshake import SHSServer 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) config = yaml.safe_load(f)
@ -17,6 +19,8 @@ async def _on_connect(conn):
async def main(): async def main():
"""Main function to run"""
server_keypair = SigningKey(b64decode(config["private"][:-8])[:32]) server_keypair = SigningKey(b64decode(config["private"][:-8])[:32])
server = SHSServer("localhost", 8008, server_keypair) server = SHSServer("localhost", 8008, server_keypair)
server.on_connect(_on_connect) server.on_connect(_on_connect)

62
poetry.lock generated
View File

@ -25,6 +25,17 @@ files = [
[package.extras] [package.extras]
test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] 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]] [[package]]
name = "babel" name = "babel"
version = "2.13.1" version = "2.13.1"
@ -431,6 +442,20 @@ files = [
{file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"}, {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]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.7" version = "0.3.7"
@ -638,6 +663,17 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, {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]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "1.0.0"
@ -789,6 +825,30 @@ files = [
[package.extras] [package.extras]
plugins = ["importlib-metadata"] 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]] [[package]]
name = "pynacl" name = "pynacl"
version = "1.5.0" version = "1.5.0"
@ -1249,4 +1309,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "b578043d7c76e0f421f1e0557bbea78870cca9fb9f8713dd3b7299f08823546b" content-hash = "82c05c7c22b990f45fae70d36d0bed6f1c414b30af9e40318dfc383ebab9d86a"

View File

@ -23,10 +23,14 @@ pytest-mock = "^3.12.0"
pre-commit = "^3.5.0" pre-commit = "^3.5.0"
commitizen = "^3.12.0" commitizen = "^3.12.0"
black = "^23.10.1" black = "^23.10.1"
pylint = "^3.0.2"
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
sphinx = "^7.2.6" sphinx = "^7.2.6"
[tool.poetry.group.examples.dependencies]
pyyaml = "^6.0.1"
[tool.black] [tool.black]
line-length = 120 line-length = 120
@ -47,6 +51,9 @@ skip_covered = true
fail_under = 91 fail_under = 91
omit = ["examples/*"] omit = ["examples/*"]
[tool.pylint.format]
max-line-length = 120
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--cov=. --no-cov-on-fail" addopts = "--cov=. --no-cov-on-fail"
ignore = "examples" ignore = "examples"

View File

@ -18,6 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Secret Handshake"""
from .network import SHSClient, SHSServer from .network import SHSClient, SHSServer

View File

@ -1,5 +1,8 @@
import struct """Box stream utilities"""
from asyncio import IncompleteReadError from asyncio import IncompleteReadError
import struct
from typing import Tuple
from nacl.secret import SecretBox from nacl.secret import SecretBox
@ -10,11 +13,9 @@ MAX_SEGMENT_SIZE = 4 * 1024
TERMINATION_HEADER = b"\x00" * 18 TERMINATION_HEADER = b"\x00" * 18
def get_stream_pair(reader, writer, **kwargs): def get_stream_pair(reader, writer, **kwargs) -> Tuple["UnboxStream", "BoxStream"]:
"""Return a tuple with `(unbox_stream, box_stream)` (reader/writer). """Create a new duplex box stream"""
:return: (:class:`secret_handshake.boxstream.UnboxStream`,
:class:`secret_handshake.boxstream.BoxStream`)"""
box_args = { box_args = {
"key": kwargs["encrypt_key"], "key": kwargs["encrypt_key"],
"nonce": kwargs["encrypt_nonce"], "nonce": kwargs["encrypt_nonce"],
@ -26,7 +27,9 @@ def get_stream_pair(reader, writer, **kwargs):
return UnboxStream(reader, **unbox_args), BoxStream(writer, **box_args) return UnboxStream(reader, **unbox_args), BoxStream(writer, **box_args)
class UnboxStream(object): class UnboxStream:
"""Unboxing stream"""
def __init__(self, reader, key, nonce): def __init__(self, reader, key, nonce):
self.reader = reader self.reader = reader
self.key = key self.key = key
@ -34,6 +37,8 @@ class UnboxStream(object):
self.closed = False self.closed = False
async def read(self): async def read(self):
"""Read data from the stream"""
try: try:
data = await self.reader.readexactly(HEADER_LENGTH) data = await self.reader.readexactly(HEADER_LENGTH)
except IncompleteReadError: except IncompleteReadError:
@ -70,7 +75,9 @@ class UnboxStream(object):
return data return data
class BoxStream(object): class BoxStream:
"""Box stream"""
def __init__(self, writer, key, nonce): def __init__(self, writer, key, nonce):
self.writer = writer self.writer = writer
self.key = key self.key = key
@ -78,6 +85,8 @@ class BoxStream(object):
self.nonce = nonce self.nonce = nonce
def write(self, data): def write(self, data):
"""Write data to the box stream"""
for chunk in split_chunks(data, MAX_SEGMENT_SIZE): for chunk in split_chunks(data, MAX_SEGMENT_SIZE):
body = self.box.encrypt(chunk, inc_nonce(self.nonce))[24:] 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]
@ -89,4 +98,6 @@ class BoxStream(object):
self.writer.write(body[16:]) self.writer.write(body[16:])
def close(self): def close(self):
"""Close the box stream"""
self.writer.write(self.box.encrypt(b"\x00" * 18, self.nonce)[24:]) self.writer.write(self.box.encrypt(b"\x00" * 18, self.nonce)[24:])

View File

@ -18,10 +18,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Cryptography functions"""
from base64 import b64decode
import hashlib import hashlib
import hmac 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.bindings import crypto_box_afternm, crypto_box_open_afternm, crypto_scalarmult
from nacl.exceptions import CryptoError from nacl.exceptions import CryptoError
@ -34,13 +36,19 @@ APPLICATION_KEY = b64decode("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=")
class SHSError(Exception): class SHSError(Exception):
"""A SHS 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): def __init__(self, local_key, ephemeral_key=None, application_key=None):
self.local_key = local_key self.local_key = local_key
self.application_key = application_key or APPLICATION_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()) self._reset_keys(ephemeral_key or PrivateKey.generate())
def _reset_keys(self, ephemeral_key): def _reset_keys(self, ephemeral_key):
@ -71,12 +79,16 @@ class SHSCryptoBase(object):
return ok return ok
def clean(self, new_ephemeral_key=None): def clean(self, new_ephemeral_key=None):
"""Clean internal data"""
self._reset_keys(new_ephemeral_key or PrivateKey.generate()) self._reset_keys(new_ephemeral_key or PrivateKey.generate())
self.shared_secret = None self.shared_secret = None
self.shared_hash = None self.shared_hash = None
self.remote_ephemeral_key = None self.remote_ephemeral_key = None
def get_box_keys(self): def get_box_keys(self):
"""Get the box streams keys"""
shared_secret = hashlib.sha256(self.box_secret).digest() shared_secret = hashlib.sha256(self.box_secret).digest()
return { return {
"shared_secret": shared_secret, "shared_secret": shared_secret,
@ -88,7 +100,18 @@ class SHSCryptoBase(object):
class SHSServerCrypto(SHSCryptoBase): 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): def verify_client_auth(self, data):
"""Verify client authentication data"""
assert len(data) == 112 assert len(data) == 112
a_bob = crypto_scalarmult(bytes(self.local_key.to_curve25519_private_key()), self.remote_ephemeral_key) 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() box_secret = hashlib.sha256(self.application_key + self.shared_secret + a_bob).digest()
@ -107,12 +130,13 @@ class SHSServerCrypto(SHSCryptoBase):
return True return True
def generate_accept(self): def generate_accept(self):
"""Generate an accept message"""
okay = self.local_key.sign(self.application_key + self.hello + self.shared_hash).signature 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 crypto_box_afternm(okay, b"\x00" * 24, self.box_secret)
return d
def clean(self, new_ephemeral_key=None): 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.hello = None
self.b_alice = None self.b_alice = None
@ -120,19 +144,29 @@ class SHSServerCrypto(SHSCryptoBase):
class SHSClientCrypto(SHSCryptoBase): class SHSClientCrypto(SHSCryptoBase):
"""An object that encapsulates all the SHS client-side crypto. """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 local_key: the keypair used by the client
:param server_pub_key: the server's public key (``byte`` string) :param server_pub_key: the server's public key
:param ephemeral_key: a fresh local :class:`nacl.public.PrivateKey` :param ephemeral_key: a fresh local private key
:param application_key: the unique application key (``byte`` string), defaults to SSB's :param application_key: the unique application key, defaults to SSB's
""" """
def __init__(self, local_key, server_pub_key, ephemeral_key, application_key=None): def __init__(
super(SHSClientCrypto, self).__init__(local_key, ephemeral_key, application_key) 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.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): def verify_server_challenge(self, data):
"""Verify the correctness of challenge sent from the server.""" """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() curve_pkey = self.remote_pub_key.to_curve25519_public_key()
# a_bob is (a * B) # a_bob is (a * B)
@ -168,8 +202,8 @@ class SHSClientCrypto(SHSCryptoBase):
try: try:
# let's use the box secret to unbox our encrypted message # let's use the box secret to unbox our encrypted message
signature = crypto_box_open_afternm(data, nonce, self.box_secret) signature = crypto_box_open_afternm(data, nonce, self.box_secret)
except CryptoError: except CryptoError as exc:
raise SHSError("Error decrypting server acceptance message") raise SHSError("Error decrypting server acceptance message") from exc
# we should have received sign(B)[K | H | hash(a * b)] # 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 # let's see if that signature can verify the reconstructed data on our side
@ -177,6 +211,6 @@ class SHSClientCrypto(SHSCryptoBase):
return True return True
def clean(self, new_ephemeral_key=None): 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.a_bob = None
self.b_alice = None self.b_alice = None

View File

@ -18,6 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Networking functionality"""
import asyncio import asyncio
@ -26,22 +27,30 @@ from .crypto import SHSClientCrypto, SHSServerCrypto
class SHSClientException(Exception): class SHSClientException(Exception):
pass """Base exception class for client errors"""
class SHSDuplexStream(object): class SHSDuplexStream:
"""SHS duplex stream"""
def __init__(self): def __init__(self):
self.write_stream = None self.write_stream = None
self.read_stream = None self.read_stream = None
self.is_connected = False self.is_connected = False
def write(self, data): def write(self, data):
"""Write data to the write stream"""
self.write_stream.write(data) self.write_stream.write(data)
async def read(self): async def read(self):
"""Read data from the read stream"""
return await self.read_stream.read() return await self.read_stream.read()
def close(self): def close(self):
"""Close the duplex stream"""
self.write_stream.close() self.write_stream.close()
self.read_stream.close() self.read_stream.close()
self.is_connected = False self.is_connected = False
@ -58,21 +67,27 @@ class SHSDuplexStream(object):
return msg return msg
class SHSEndpoint(object): class SHSEndpoint:
"""SHS endpoint"""
def __init__(self): def __init__(self):
self._on_connect = None self._on_connect = None
self.crypto = None self.crypto = None
def on_connect(self, cb): def on_connect(self, cb):
"""Set the function to be called when a new connection arrives"""
self._on_connect = cb self._on_connect = cb
def disconnect(self): def disconnect(self):
"""Disconnect the endpoint"""
raise NotImplementedError raise NotImplementedError
class SHSServer(SHSEndpoint): class SHSServer(SHSEndpoint):
"""SHS server"""
def __init__(self, host, port, server_kp, application_key=None): def __init__(self, host, port, server_kp, application_key=None):
super(SHSServer, self).__init__() super().__init__()
self.host = host self.host = host
self.port = port self.port = port
self.crypto = SHSServerCrypto(server_kp, application_key=application_key) self.crypto = SHSServerCrypto(server_kp, application_key=application_key)
@ -92,6 +107,8 @@ class SHSServer(SHSEndpoint):
writer.write(self.crypto.generate_accept()) writer.write(self.crypto.generate_accept())
async def handle_connection(self, reader, writer): async def handle_connection(self, reader, writer):
"""Handle incoming connections"""
self.crypto.clean() self.crypto.clean()
await self._handshake(reader, writer) await self._handshake(reader, writer)
keys = self.crypto.get_box_keys() keys = self.crypto.get_box_keys()
@ -104,6 +121,8 @@ class SHSServer(SHSEndpoint):
asyncio.ensure_future(self._on_connect(conn)) asyncio.ensure_future(self._on_connect(conn))
async def listen(self): async def listen(self):
"""Listen for connections"""
await asyncio.start_server(self.handle_connection, self.host, self.port) await asyncio.start_server(self.handle_connection, self.host, self.port)
def disconnect(self): def disconnect(self):
@ -112,23 +131,33 @@ class SHSServer(SHSEndpoint):
class SHSServerConnection(SHSDuplexStream): class SHSServerConnection(SHSDuplexStream):
"""SHS server connection"""
def __init__(self, read_stream, write_stream): def __init__(self, read_stream, write_stream):
super(SHSServerConnection, self).__init__() super().__init__()
self.read_stream = read_stream self.read_stream = read_stream
self.write_stream = write_stream self.write_stream = write_stream
@classmethod @classmethod
def from_byte_streams(cls, reader, writer, **keys): 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) reader, writer = get_stream_pair(reader, writer, **keys)
return cls(reader, writer) return cls(reader, writer)
class SHSClient(SHSDuplexStream, SHSEndpoint): 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) SHSDuplexStream.__init__(self)
SHSEndpoint.__init__(self) SHSEndpoint.__init__(self)
self.host = host self.host = host
self.port = port self.port = port
self.writer = None
self.crypto = SHSClientCrypto( self.crypto = SHSClientCrypto(
client_kp, server_pub_key, ephemeral_key=ephemeral_key, application_key=application_key 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") raise SHSClientException("Server accept is not valid")
async def open(self): async def open(self):
"""Open the TCP connection"""
reader, writer = await asyncio.open_connection(self.host, self.port) reader, writer = await asyncio.open_connection(self.host, self.port)
await self._handshake(reader, writer) 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.read_stream, self.write_stream = get_stream_pair(reader, writer, **keys)
self.writer = writer self.writer = writer
self.is_connected = True self.is_connected = True
if self._on_connect: if self._on_connect:
await self._on_connect() await self._on_connect()

View File

@ -18,6 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Utility functions"""
import struct import struct
@ -26,11 +27,16 @@ MAX_NONCE = 8 * NONCE_SIZE
def inc_nonce(nonce): def inc_nonce(nonce):
"""Increment nonce"""
num = bytes_to_long(nonce) + 1 num = bytes_to_long(nonce) + 1
if num > 2**MAX_NONCE: if num > 2**MAX_NONCE:
num = 0 num = 0
bnum = long_to_bytes(num) bnum = long_to_bytes(num)
bnum = b"\x00" * (NONCE_SIZE - len(bnum)) + bnum bnum = b"\x00" * (NONCE_SIZE - len(bnum)) + bnum
return bnum return bnum
@ -44,6 +50,8 @@ def split_chunks(seq, n):
# Stolen from PyCypto (Public Domain) # Stolen from PyCypto (Public Domain)
def b(s): 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 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 s = pack(">I", n & 0xFFFFFFFF) + s
n = n >> 32 n = n >> 32
# strip off leading zeros # strip off leading zeros
for i in range(len(s)): for i, c in enumerate(s):
if s[i] != b("\000")[0]: if c != b("\000")[0]:
break break
else: else:
# only happens when n == 0 # only happens when n == 0

View File

@ -26,8 +26,9 @@ from io import BytesIO
class AsyncBuffer(BytesIO): class AsyncBuffer(BytesIO):
"""Just a BytesIO with an async read method.""" """Just a BytesIO with an async read method."""
async def read(self, n=None): async def read(self, n=None): # pylint: disable=invalid-overridden-method
v = super(AsyncBuffer, self).read(n) v = super().read(n)
return v return v
readexactly = read readexactly = read

View File

@ -18,6 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Tests for the box stream"""
import pytest import pytest
@ -27,17 +28,18 @@ from .helpers import AsyncBuffer, async_comprehend
from .test_crypto import CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE from .test_crypto import CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE
MESSAGE_1 = ( 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_2 = b"\x141\xd63\x13d\xd1\xecZ\x9b\xd0\xd4\x03\xcdR?'\xaa.\x89I\x92I\xf9guL\xaa\x06?\xea\xca/}\x88*\xb2"
MESSAGE_3 = ( 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" MESSAGE_CLOSED = b"\xb1\x14hU'\xb5M\xa6\"\x03\x9duy\xa1\xd4evW,\xdcE\x18\xe4+ C4\xe8h\x96\xed\xc5\x94\x80"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_boxstream(): async def test_boxstream():
"""Test stream boxing"""
buffer = AsyncBuffer() buffer = AsyncBuffer()
box_stream = BoxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE) box_stream = BoxStream(buffer, CLIENT_ENCRYPT_KEY, CLIENT_ENCRYPT_NONCE)
box_stream.write(b"foo") box_stream.write(b"foo")
@ -62,6 +64,8 @@ async def test_boxstream():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_unboxstream(): async def test_unboxstream():
"""Test stream unboxing"""
buffer = AsyncBuffer(MESSAGE_1 + MESSAGE_2 + MESSAGE_3 + MESSAGE_CLOSED) buffer = AsyncBuffer(MESSAGE_1 + MESSAGE_2 + MESSAGE_3 + MESSAGE_CLOSED)
buffer.seek(0) buffer.seek(0)
@ -73,6 +77,8 @@ async def test_unboxstream():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_long_packets(): async def test_long_packets():
"""Test for receiving long packets"""
data_size = 6 * 1024 data_size = 6 * 1024
data = bytes(n % 256 for n in range(data_size)) data = bytes(n % 256 for n in range(data_size))

View File

@ -18,6 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Tests for the crypto components"""
import hashlib 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" 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(): def server():
"""A testing SHS server"""
server_key = SigningKey(SERVER_KEY_SEED) server_key = SigningKey(SERVER_KEY_SEED)
server_eph_key = PrivateKey(SERVER_EPH_KEY_SEED) server_eph_key = PrivateKey(SERVER_EPH_KEY_SEED)
return SHSServerCrypto(server_key, server_eph_key, application_key=APP_KEY) return SHSServerCrypto(server_key, server_eph_key, application_key=APP_KEY)
@pytest.fixture() @pytest.fixture
def client(): def client():
"""A testing SHS client"""
client_key = SigningKey(CLIENT_KEY_SEED) client_key = SigningKey(CLIENT_KEY_SEED)
server_key = SigningKey(SERVER_KEY_SEED) server_key = SigningKey(SERVER_KEY_SEED)
client_eph_key = PrivateKey(CLIENT_EPH_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) 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%" b"\xe5\xc6K\xae\x94\xdbVt\x84\xdc\x1c@+D\x1c%"
) )
CLIENT_AUTH = ( 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"\xf2\xaf?z\x15\x10\xd0\xf0\xdf\xe3\x91\xfe\x14\x1c}z\xab\xeey\xf5\xef\xfc\xa1EdV\xf2T\x95s[!$"
b"\xeb\x8f\x1b\x96JP\x17^\x92\xc8\x9e\xb4*5`\xf2\x8fI.\x93\xb9\x14:\xca@\x06\xff\xd1\xf1J\xc8t\xc4" b"z\xeb\x8f\x1b\x96JP\x17^\x92\xc8\x9e\xb4*5`\xf2\x8fI.\x93\xb9\x14:\xca@\x06\xff\xd1\xf1J\xc8t"
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"\xc4\xd8\xc3$[\xc5\x94je\x83\x00%\x99\x10\x16\xb1\xa2\xb2\xb7\xbf\xc9\x88\x14\xb9\xbb^\tzq"
b"\xf5\x1f7#\xed\x92X\xb2\xe3\xe5\x8b[t3" b"\xa4\xef\xc5\xf5\x1f7#\xed\x92X\xb2\xe3\xe5\x8b[t3"
) )
SERVER_CHALLENGE = ( 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"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9\xeawl\xb9}\x85\xc3"
b"\x0c ($E\xb4\x8ax\xc4)t<\xd7\x8b\xd6\x07\xb7\xecw\x84\r\xe1-Iz`\xeb\x04\x89\xd6{" b"ik\x0c ($E\xb4\x8ax\xc4)t<\xd7\x8b\xd6\x07\xb7\xecw\x84\r\xe1-Iz`\xeb\x04\x89\xd6{"
) )
SERVER_ACCEPT = ( 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'\xb4\xd0\xea\xfb\xfb\xf6s\xcc\x10\xc4\x99\x95"\x13 y\xa6\xea.G\xeed\x8d=t9\x88|\x94\xd1\xbcK'
b"\xd8\xbcG1h\xac\xd0\xeb*\x1f\x8d\xae\x0b\x91G\xa1\xe6\x96b\xf2\xda90u\xeb_\xab\xdb\xcb%d7}\xb5\xce" b"\xd47\xd8\xbcG1h\xac\xd0\xeb*\x1f\x8d\xae\x0b\x91G\xa1\xe6\x96b\xf2\xda90u\xeb_\xab\xdb\xcb%d"
b"(k\x15\xe3L\x9d)\xd5\xa1|:" b"7}\xb5\xce(k\x15\xe3L\x9d)\xd5\xa1|:"
) )
INTER_SHARED_SECRET = ( 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" 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" 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() client_challenge = client.generate_challenge()
assert client_challenge == CLIENT_CHALLENGE assert client_challenge == CLIENT_CHALLENGE
assert server.verify_challenge(client_challenge) assert server.verify_challenge(client_challenge)

View File

@ -18,40 +18,59 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
"""Tests for the networking components"""
import os import os
from asyncio import Event, wait_for from asyncio import Event, wait_for
import pytest import pytest
from nacl.signing import SigningKey from nacl.signing import SigningKey
from secret_handshake import SHSClient, SHSServer
from .helpers import AsyncBuffer from .helpers import AsyncBuffer
class DummyCrypto(object): class DummyCrypto:
"""Dummy crypto module, pretends everything is fine.""" """Dummy crypto module, pretends everything is fine."""
def verify_server_challenge(self, data): def verify_server_challenge(self, _):
"""Verify the server challenge"""
return True return True
def verify_challenge(self, data): def verify_challenge(self, _):
"""Verify the challenge data"""
return True return True
def verify_server_accept(self, data): def verify_server_accept(self, _):
"""Verify servers accept message"""
return True return True
def generate_challenge(self): def generate_challenge(self):
"""Generate authentication challenge"""
return b"CHALLENGE" return b"CHALLENGE"
def generate_client_auth(self): def generate_client_auth(self):
"""Generate client authentication data"""
return b"AUTH" return b"AUTH"
def verify_client_auth(self, data): def verify_client_auth(self, _):
"""Verify client authentication data"""
return True return True
def generate_accept(self): def generate_accept(self):
"""Generate an ACCEPT message"""
return b"ACCEPT" return b"ACCEPT"
def get_box_keys(self): def get_box_keys(self):
"""Get box keys"""
return { return {
"encrypt_key": b"x" * 32, "encrypt_key": b"x" * 32,
"encrypt_nonce": b"x" * 32, "encrypt_nonce": b"x" * 32,
@ -60,10 +79,10 @@ class DummyCrypto(object):
} }
def clean(self): def clean(self):
return """Clean up internal data"""
def _dummy_boxstream(stream, **kwargs): def _dummy_boxstream(stream, **_):
"""Identity boxstream, no tansformation.""" """Identity boxstream, no tansformation."""
return stream return stream
@ -72,7 +91,7 @@ def _client_stream_mocker():
reader = AsyncBuffer(b"xxx") reader = AsyncBuffer(b"xxx")
writer = 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
return reader, writer, _create_mock_streams return reader, writer, _create_mock_streams
@ -82,7 +101,7 @@ def _server_stream_mocker():
reader = AsyncBuffer(b"xxx") reader = AsyncBuffer(b"xxx")
writer = 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) await cb(reader, writer)
return reader, writer, _create_mock_server return reader, writer, _create_mock_server
@ -90,13 +109,13 @@ def _server_stream_mocker():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_client(mocker): 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("asyncio.open_connection", new=_create_mock_streams)
mocker.patch("secret_handshake.boxstream.BoxStream", new=_dummy_boxstream) mocker.patch("secret_handshake.boxstream.BoxStream", new=_dummy_boxstream)
mocker.patch("secret_handshake.boxstream.UnboxStream", 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() client.crypto = DummyCrypto()
@ -108,15 +127,15 @@ async def test_client(mocker):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_server(mocker): async def test_server(mocker):
from secret_handshake import SHSServer """Test the server"""
resolve = Event() resolve = Event()
async def _on_connect(conn): async def _on_connect(_):
server.disconnect() server.disconnect()
resolve.set() 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("asyncio.start_server", new=_create_mock_server)
mocker.patch("secret_handshake.boxstream.BoxStream", new=_dummy_boxstream) mocker.patch("secret_handshake.boxstream.BoxStream", new=_dummy_boxstream)
mocker.patch("secret_handshake.boxstream.UnboxStream", new=_dummy_boxstream) mocker.patch("secret_handshake.boxstream.UnboxStream", new=_dummy_boxstream)