ci: Lint source
This commit is contained in:
parent
53994b77a7
commit
d28ca167f2
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
62
poetry.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:])
|
||||||
|
@ -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 stream’s 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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 server’s 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)
|
||||||
|
Loading…
Reference in New Issue
Block a user