Compare commits

...

11 Commits
main ... dev

13 changed files with 1143 additions and 551 deletions

View File

@ -16,3 +16,24 @@ 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]
- id: pylint
name: pylint
entry: poetry run pylint
language: system
types: [python]
require_serial: true
- id: isort
name: isort
args: ["--check", "--diff"]
entry: poetry run isort
language: system
require_serial: true
types_or: [python, pyi]

View File

@ -1,93 +1,108 @@
"""Example SSB Client"""
from asyncio import ensure_future, gather, get_event_loop
import base64
import hashlib
import logging
import struct
import time
from asyncio import get_event_loop, gather, ensure_future
from colorlog import ColoredFormatter
from nacl.signing import SigningKey
from secret_handshake.network import SHSClient
from ssb.muxrpc import MuxRPCAPI, MuxRPCAPIException
from ssb.muxrpc import MuxRPCAPI, MuxRPCAPIException, MuxRPCRequest
from ssb.packet_stream import PacketStream, PSMessageType
from ssb.util import load_ssb_secret
import hashlib
import base64
api = MuxRPCAPI()
@api.define('createHistoryStream')
def create_history_stream(connection, msg):
print('create_history_stream', msg)
@api.define("createHistoryStream")
def create_history_stream(connection: PacketStream, msg: MuxRPCRequest) -> None: # pylint: disable=unused-argument
"""Handle the createHistoryStream RPC call"""
print("create_history_stream", msg)
# msg = PSMessage(PSMessageType.JSON, True, stream=True, end_err=True, req=-req)
# connection.write(msg)
@api.define('blobs.createWants')
def create_wants(connection, msg):
print('create_wants', msg)
@api.define("blobs.createWants")
def create_wants(connection: PacketStream, msg: MuxRPCRequest) -> None: # pylint: disable=unused-argument
"""Handle the createWants RPC call"""
print("create_wants", msg)
async def test_client():
async for msg in api.call('createHistoryStream', [{
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
'seq': 1,
'live': False,
'keys': False
}], 'source'):
print('> RESPONSE:', msg)
async def test_client() -> None:
"""The actual client implementation"""
async for msg in api.call(
"createHistoryStream",
[{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}],
"source",
):
print("> RESPONSE:", msg)
try:
print('> RESPONSE:', await api.call('whoami', [], 'sync'))
print("> RESPONSE:", await api.call("whoami", [], "sync"))
except MuxRPCAPIException as e:
print(e)
handler = api.call('gossip.ping', [], 'duplex')
handler.send(struct.pack('l', int(time.time() * 1000)), msg_type=PSMessageType.BUFFER)
handler = api.call("gossip.ping", [], "duplex")
handler.send(struct.pack("l", int(time.time() * 1000)), msg_type=PSMessageType.BUFFER)
async for msg in handler:
print('> RESPONSE:', msg)
print("> RESPONSE:", msg)
handler.send(True, end=True)
break
img_data = b''
async for msg in api.call('blobs.get', ['&kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM=.sha256'], 'source'):
if msg.type.name == 'BUFFER':
img_data = b""
async for msg in api.call("blobs.get", ["&kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM=.sha256"], "source"):
assert msg
if msg.type.name == "BUFFER":
img_data += msg.data
if msg.type.name == 'JSON' and msg.data == b'true':
assert (base64.b64encode(hashlib.sha256(img_data).digest()) ==
b'kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM=')
with open('./ub1k.jpg', 'wb') as f:
if msg.type.name == "JSON" and msg.data == b"true":
assert (
base64.b64encode(hashlib.sha256(img_data).digest()) == b"kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM="
)
with open("./ub1k.jpg", "wb") as f:
f.write(img_data)
async def main():
client = SHSClient('127.0.0.1', 8008, keypair, bytes(keypair.verify_key))
async def main(keypair: SigningKey) -> None:
"""The main function to run"""
client = SHSClient("127.0.0.1", 8008, keypair, bytes(keypair.verify_key))
packet_stream = PacketStream(client)
await client.open()
api.add_connection(packet_stream)
await gather(ensure_future(api), test_client())
if __name__ == '__main__':
if __name__ == "__main__":
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# create formatter
formatter = ColoredFormatter('%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - '
'%(cyan)s%(message)s%(reset)s')
formatter = ColoredFormatter(
"%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - %(cyan)s%(message)s%(reset)s"
)
# add formatter to ch
ch.setFormatter(formatter)
# add ch to logger
logger = logging.getLogger('packet_stream')
logger = logging.getLogger("packet_stream")
logger.setLevel(logging.INFO)
logger.addHandler(ch)
keypair = load_ssb_secret()['keypair']
ssb_keypair = load_ssb_secret()["keypair"]
loop = get_event_loop()
loop.run_until_complete(main())
loop.run_until_complete(main(ssb_keypair))
loop.close()

View File

@ -1,45 +1,53 @@
"""Test SSB server"""
from asyncio import get_event_loop
import logging
from asyncio import gather, get_event_loop, ensure_future
from colorlog import ColoredFormatter
from secret_handshake import SHSServer
from ssb.packet_stream import PacketStream
from secret_handshake.network import SHSDuplexStream
from ssb.muxrpc import MuxRPCAPI
from ssb.packet_stream import PacketStream
from ssb.util import load_ssb_secret
api = MuxRPCAPI()
async def on_connect(conn):
async def on_connect(conn: SHSDuplexStream) -> None:
"""Incoming connection handler"""
packet_stream = PacketStream(conn)
api.add_connection(packet_stream)
print('connect', conn)
print("connect", conn)
async for msg in packet_stream:
print(msg)
async def main():
server = SHSServer('127.0.0.1', 8008, load_ssb_secret()['keypair'])
async def main() -> None:
"""The main function to run"""
server = SHSServer("127.0.0.1", 8008, load_ssb_secret()["keypair"])
server.on_connect(on_connect)
await server.listen()
if __name__ == '__main__':
if __name__ == "__main__":
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter
formatter = ColoredFormatter('%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - '
'%(cyan)s%(message)s%(reset)s')
formatter = ColoredFormatter(
"%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - %(cyan)s%(message)s%(reset)s"
)
# add formatter to ch
ch.setFormatter(formatter)
# add ch to logger
logger = logging.getLogger('packet_stream')
logger = logging.getLogger("packet_stream")
logger.setLevel(logging.DEBUG)
logger.addHandler(ch)

340
poetry.lock generated
View File

@ -26,16 +26,19 @@ files = [
test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
[[package]]
name = "async-generator"
version = "1.8"
description = "Async generators for Python 3.5"
name = "astroid"
version = "3.0.1"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = "*"
python-versions = ">=3.8.0"
files = [
{file = "async_generator-1.8-py3-none-any.whl", hash = "sha256:d9253336202cb9df50ba617893fe794c61394a7eb4b9054f285c860f395ac6ff"},
{file = "async_generator-1.8.zip", hash = "sha256:928b644cfc92be498f2d6c431e0082ae79ea736fbdf1ce4247881071dd525348"},
{file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"},
{file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"},
]
[package.dependencies]
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
[[package]]
name = "babel"
version = "2.13.1"
@ -53,6 +56,48 @@ 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"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[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 = "certifi"
version = "2023.7.22"
@ -244,6 +289,20 @@ toml = "*"
[package.extras]
test = ["mock"]
[[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"
@ -374,6 +433,20 @@ files = [
{file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"},
]
[[package]]
name = "dill"
version = "0.3.7"
description = "serialize all of Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
{file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "docutils"
version = "0.17.1"
@ -453,20 +526,20 @@ files = [
[[package]]
name = "isort"
version = "4.3.21"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=3.8.0"
files = [
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
pipfile = ["pipreqs", "requirementslib"]
pyproject = ["toml"]
requirements = ["pip-api", "pipreqs"]
xdg-home = ["appdirs (>=1.4.0)"]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jinja2"
@ -554,6 +627,74 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy"
version = "1.6.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"},
{file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"},
{file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"},
{file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"},
{file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"},
{file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"},
{file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"},
{file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"},
{file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"},
{file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"},
{file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"},
{file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"},
{file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"},
{file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"},
{file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"},
{file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"},
{file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"},
{file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"},
{file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"},
{file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"},
{file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"},
{file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"},
{file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"},
{file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"},
{file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"},
{file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"},
{file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.1.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
reports = ["lxml"]
[[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 = "packaging"
version = "23.2"
@ -565,6 +706,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 = "pep257"
version = "0.7.0"
@ -576,6 +728,21 @@ files = [
{file = "pep257-0.7.0.tar.gz", hash = "sha256:f3d67547f5617a9cfeb4b8097ed94a954888315defaf6e9b518ff1719363bf03"},
]
[[package]]
name = "platformdirs"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]]
name = "pluggy"
version = "1.3.0"
@ -630,49 +797,61 @@ files = [
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "pylint"
version = "3.0.2"
description = "python code static checker"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "pylint-3.0.2-py3-none-any.whl", hash = "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"},
{file = "pylint-3.0.2.tar.gz", hash = "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496"},
]
[package.dependencies]
astroid = ">=3.0.1,<=3.1.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
{version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
]
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
name = "pynacl"
version = "1.1.2"
version = "1.5.0"
description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false
python-versions = "*"
python-versions = ">=3.6"
files = [
{file = "PyNaCl-1.1.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:9558ef5c1ae45322c054d1d1151016e0463b4da8b5c746a675e99c5c7d8f4faa"},
{file = "PyNaCl-1.1.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:301c966c1e17950e50d174ab4b2e7ef3e98ff51ad7a591152a19fe2139281eed"},
{file = "PyNaCl-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d21d733a63637ddf41d0cab50135ec9f5224dd22fd10ebf5c5f5f946b833f84"},
{file = "PyNaCl-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:c93d151efcdd7d214b1b11d781c9f1b125f0208cd06d9762bddabdfeac1cedfc"},
{file = "PyNaCl-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1b4938a557b32e5c6b27fac79a94cf1abb70753b5462a0b577bd2a77e09dacd0"},
{file = "PyNaCl-1.1.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:99f91eb80b85fe16f70d362cfeae8eeeb108cd09a85f039fdab02164762f764b"},
{file = "PyNaCl-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57314a7bad4bd39501dc622942f9921923673e52e126b0fc4f0214b5d25d619a"},
{file = "PyNaCl-1.1.2-cp33-cp33m-macosx_10_6_intel.whl", hash = "sha256:506bc2591968a1a7b6577075bc29a591d8fff5bdfec03b0dd926f34b75b670e5"},
{file = "PyNaCl-1.1.2-cp33-cp33m-manylinux1_i686.whl", hash = "sha256:4c15d7cea1a313fff3f68222e682ee1f855e43c0865081cad7385066a6b57d75"},
{file = "PyNaCl-1.1.2-cp33-cp33m-manylinux1_x86_64.whl", hash = "sha256:c4ea0e3b9f3317ada56e12c7b37f6d0316900ae8b54a20d7b100d4e14350ac87"},
{file = "PyNaCl-1.1.2-cp33-cp33m-win32.whl", hash = "sha256:53d83faf274813a5778bba1cd4cb96b79f39e44a63b1c4a4dada01a2b0eeafe8"},
{file = "PyNaCl-1.1.2-cp33-cp33m-win_amd64.whl", hash = "sha256:5172395dea8203ae124fd282fef3d242aa75366d66aebc0f5aab0c4753eed97b"},
{file = "PyNaCl-1.1.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:877879903cddb5da317fe86d923f65eb28c62fd7feb79cd3402d166e401f9423"},
{file = "PyNaCl-1.1.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:123c41df1db119397f2e26e9c63ca2ea853d3663e26b1c389bd3859dc1b7178a"},
{file = "PyNaCl-1.1.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:ceb16b7977123713ad898450ca86a2dc6706a17fe4cf278ffb6b76929c186550"},
{file = "PyNaCl-1.1.2-cp34-cp34m-win32.whl", hash = "sha256:813d4170f62d68236bb041cf731e8d1f34fc1006a5e5d81139bead6ddaa9d169"},
{file = "PyNaCl-1.1.2-cp34-cp34m-win_amd64.whl", hash = "sha256:f01405a5c453b866e35338c53882f7ba7069c1f4e4045ce67513ad45c796f8a5"},
{file = "PyNaCl-1.1.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:4a3be9f884df08087996516707446ba55648bbefae8428bf578fa05f20fa2ed9"},
{file = "PyNaCl-1.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7d14f18f8bc43977691276097524b9713d21b9635fea9791311261a66e4fe296"},
{file = "PyNaCl-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9532aaa446840ece574c719ad3bbf25f60ca9871f48b5446e3f73e8b498e2398"},
{file = "PyNaCl-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:67b75a950dbc4025bfa549c183baa17db4096955912f385df31830e5a2121974"},
{file = "PyNaCl-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:dfc85c2b414dee700e32764559d560063825ec1470d3ee6c973e43c80a622e56"},
{file = "PyNaCl-1.1.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3fd984580cbea8e02fc531aa32ab9487b72c30127f9e4c8db9ba3fe8950ecc93"},
{file = "PyNaCl-1.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:75a427377b2088c29a44db08c796c75a9cde2f9725dd041903cfbc0f6034895c"},
{file = "PyNaCl-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ca2deb968135f1400105ca902f5cef24ba6984b6a4904756498afcb9077c76f9"},
{file = "PyNaCl-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:ffb74ac578b3b80b1d2d5a23a6dd7b1d6682e5fce6a7b3d21b46b180a5546055"},
{file = "PyNaCl-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b796d95704b674100bd99fc42bbde9f8f2ccddae8599a4d4bbcb518428dfbfed"},
{file = "PyNaCl-1.1.2.tar.gz", hash = "sha256:32f52b754abf07c319c04ce16905109cab44b0e7f7c79497431d3b2000f8af8c"},
{file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
{file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
{file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
{file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
]
[package.dependencies]
cffi = ">=1.4.1"
six = "*"
[package.extras]
tests = ["pytest"]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
[[package]]
name = "pytest"
@ -845,22 +1024,21 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "secret-handshake"
version = "0.1.0.dev3"
description = "A module that implements Secret Handshake as specified in \"Designing a Secret Handshake: Authenticated"
version = "0.1.0"
description = "A module that implements Secret Handshake"
optional = false
python-versions = "*"
files = [
{file = "secret-handshake-0.1.0.dev3.tar.gz", hash = "sha256:be1f812101c0eb84a82a08d119090d8f423230878e233b4bfc551fb708b4e32a"},
]
python-versions = "^3.9"
files = []
develop = false
[package.dependencies]
async-generator = "1.8"
pynacl = "1.1.2"
PyNaCl = "^1.5.0"
[package.extras]
all = ["Sphinx (>=1.6.2)", "check-manifest (>=0.25)", "coverage (==4.4.1)", "isort (>=4.2.2)", "pydocstyle (==2.1.1)", "pytest (==3.4.0)", "pytest-asyncio (==0.6.0)", "pytest-cov (==2.5.1)", "pytest-mock (==1.6.3)"]
docs = ["Sphinx (>=1.6.2)"]
tests = ["check-manifest (>=0.25)", "coverage (==4.4.1)", "isort (>=4.2.2)", "pydocstyle (==2.1.1)", "pytest (==3.4.0)", "pytest-asyncio (==0.6.0)", "pytest-cov (==2.5.1)", "pytest-mock (==1.6.3)"]
[package.source]
type = "git"
url = "https://gitea.polonkai.eu/gergely/PySecretHandshake"
reference = "main"
resolved_reference = "5a3af659277219536eeef4c64b8a991902f0acd2"
[[package]]
name = "setuptools"
@ -899,17 +1077,6 @@ files = [
{file = "simplejson-3.16.0.tar.gz", hash = "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5"},
]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "snowballstemmer"
version = "2.2.0"
@ -1091,6 +1258,39 @@ files = [
{file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"},
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.12"
description = "Typing stubs for PyYAML"
optional = false
python-versions = "*"
files = [
{file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"},
{file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"},
]
[[package]]
name = "types-simplejson"
version = "3.19.0.2"
description = "Typing stubs for simplejson"
optional = false
python-versions = "*"
files = [
{file = "types-simplejson-3.19.0.2.tar.gz", hash = "sha256:ebc81f886f89d99d6b80c726518aa2228bc77c26438f18fd81455e4f79f8ee1b"},
{file = "types_simplejson-3.19.0.2-py3-none-any.whl", hash = "sha256:8ba093dc7884f59b3e62aed217144085e675a269debc32678fd80e0b43b2b86f"},
]
[[package]]
name = "typing-extensions"
version = "4.8.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]]
name = "urllib3"
version = "2.0.7"
@ -1137,4 +1337,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "d61fa17ada60d932d45c104bf4d45b79411c743d3eb129f65751c8d57c4bea15"
content-hash = "d80cbfdf7923c50c95505a84d8ad75eae016ca81ae32a8b22d074569b0a0fcbd"

View File

@ -8,27 +8,34 @@ readme = "README.rst"
[tool.poetry.dependencies]
python = "^3.9"
async_generator = "^1.8"
PyNaCl = "^1.1.0"
PyYAML = "^6.0.1"
secret-handshake = { version = "0.1.0.dev3", allow-prereleases = true }
secret-handshake = { git = "https://gitea.polonkai.eu/gergely/PySecretHandshake", branch = "main" }
simplejson = "3.16.0"
colorlog = "^6.7.0"
[tool.poetry.group.dev.dependencies]
check-manifest = "^0.39"
coverage = "^7.3.2"
isort = "^4.3.20"
isort = "^5.12.0"
pep257 = "^0.7.0"
pytest = "^7.4.3"
pytest-asyncio = "^0.21.1"
pytest-cov = "^4.1.0"
pytest-mock = "^3.12.0"
commitizen = "^3.12.0"
black = "^23.10.1"
pylint = "^3.0.2"
mypy = "^1.6.1"
types-pyyaml = "^6.0.12.12"
types-simplejson = "^3.19.0.2"
[tool.poetry.group.docs.dependencies]
Sphinx = "^2.1.1"
[tool.black]
line-length = 120
[tool.coverage.run]
branch = true
@ -38,6 +45,14 @@ skip_covered = true
fail_under = 70
omit = ["examples/*"]
[tool.isort]
force_sort_within_sections = true
line_length = 120
profile = "black"
[tool.pylint.format]
max-line-length = 120
[tool.pytest.ini_options]
addopts = ["--cov=.", "--no-cov-on-fail"]
python_files = ["tests/test_*.py"]

View File

@ -1,3 +1,5 @@
from .models import Feed, LocalFeed, Message, LocalMessage, NoPrivateKeyException
"""Feed related functionality"""
__all__ = ('Feed', 'LocalFeed', 'Message', 'LocalMessage', 'NoPrivateKeyException')
from .models import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException
__all__ = ("Feed", "LocalFeed", "Message", "LocalMessage", "NoPrivateKeyException")

View File

@ -1,108 +1,172 @@
import datetime
from base64 import b64encode
from collections import namedtuple, OrderedDict
from hashlib import sha256
"""Feed models"""
from base64 import b64encode
from collections import OrderedDict, namedtuple
import datetime
from hashlib import sha256
from typing import Any, Dict, Optional
from nacl.signing import SigningKey, VerifyKey
from simplejson import dumps, loads
from typing_extensions import Self
from ssb.util import tag
OrderedMsg = namedtuple('OrderedMsg', ('previous', 'author', 'sequence', 'timestamp', 'hash', 'content'))
OrderedMsg = namedtuple("OrderedMsg", ("previous", "author", "sequence", "timestamp", "hash", "content"))
class NoPrivateKeyException(Exception):
pass
"""Exception to raise when a private key is not available"""
def to_ordered(data):
def to_ordered(data: Dict[str, Any]) -> OrderedDict[str, Any]:
"""Convert a dictionary to an ``OrderedDict``"""
smsg = OrderedMsg(**data)
return OrderedDict((k, getattr(smsg, k)) for k in smsg._fields)
def get_millis_1970():
def get_millis_1970() -> int:
"""Get the UNIX timestamp in milliseconds"""
return int(datetime.datetime.utcnow().timestamp() * 1000)
class Feed(object):
def __init__(self, public_key):
class Feed:
"""Base class for feeds"""
def __init__(self, public_key: VerifyKey):
self.public_key = public_key
@property
def id(self):
return tag(self.public_key).decode('ascii')
def id(self) -> str:
"""The identifier of the feed"""
def sign(self, msg):
raise NoPrivateKeyException('Cannot use remote identity to sign (no private key!)')
return tag(self.public_key).decode("ascii")
def sign(self, msg: "Message") -> bytes:
"""Sign a message"""
raise NoPrivateKeyException("Cannot use remote identity to sign (no private key!)")
class LocalFeed(Feed):
def __init__(self, private_key):
self.private_key = private_key
"""Class representing a local feed"""
def __init__(self, private_key: SigningKey): # pylint: disable=super-init-not-called
self.private_key: SigningKey = private_key
@property
def public_key(self):
def public_key(self) -> VerifyKey:
"""The public key of the feed"""
return self.private_key.verify_key
def sign(self, msg):
@public_key.setter
def public_key(self, _: VerifyKey) -> None:
raise TypeError("Cannot set just the public key of a local feed")
def sign(self, msg: "Message") -> bytes:
"""Sign a message for this feed"""
return self.private_key.sign(msg).signature
class Message(object):
def __init__(self, feed, content, signature, sequence=1, timestamp=None, previous=None):
class Message:
"""Base class for SSB messages"""
def __init__( # pylint: disable=too-many-arguments
self,
feed: Feed,
content: Dict[str, Any],
signature: Optional[str] = None,
sequence: int = 1,
timestamp: Optional[int] = None,
previous: Optional["Message"] = None,
):
self.feed = feed
self.content = content
if signature is None:
raise ValueError("signature can't be None")
self.signature = signature
self.previous = previous
if self.previous:
self.sequence = self.previous.sequence + 1
self.sequence: int = self.previous.sequence + 1
else:
self.sequence = sequence
self.timestamp = get_millis_1970() if timestamp is None else timestamp
@classmethod
def parse(cls, data, feed):
def parse(cls, data: bytes, feed: Feed) -> Self:
"""Parse raw message data"""
obj = loads(data, object_pairs_hook=OrderedDict)
msg = cls(feed, obj['content'], timestamp=obj['timestamp'])
msg = cls(feed, obj["content"], timestamp=obj["timestamp"])
return msg
def serialize(self, add_signature=True):
return dumps(self.to_dict(add_signature=add_signature), indent=2).encode('utf-8')
def serialize(self, add_signature: bool = True) -> bytes:
"""Serialize the message"""
def to_dict(self, add_signature=True):
obj = to_ordered({
'previous': self.previous.key if self.previous else None,
'author': self.feed.id,
'sequence': self.sequence,
'timestamp': self.timestamp,
'hash': 'sha256',
'content': self.content
})
return dumps(self.to_dict(add_signature=add_signature), indent=2).encode("utf-8")
def to_dict(self, add_signature: bool = True) -> OrderedDict[str, Any]:
"""Convert the message to a dictionary"""
obj = to_ordered(
{
"previous": self.previous.key if self.previous else None,
"author": self.feed.id,
"sequence": self.sequence,
"timestamp": self.timestamp,
"hash": "sha256",
"content": self.content,
}
)
if add_signature:
obj['signature'] = self.signature
obj["signature"] = self.signature
return obj
def verify(self, signature):
def verify(self, signature: str) -> bool:
"""Verify the signature of the message"""
return self.signature == signature
@property
def hash(self):
hash = sha256(self.serialize()).digest()
return b64encode(hash).decode('ascii') + '.sha256'
def hash(self) -> str:
"""The cryptographic hash of the message"""
hash_ = sha256(self.serialize()).digest()
return b64encode(hash_).decode("ascii") + ".sha256"
@property
def key(self):
return '%' + self.hash
def key(self) -> str:
"""The key of the message"""
return "%" + self.hash
class LocalMessage(Message):
def __init__(self, feed, content, signature=None, sequence=1, timestamp=None, previous=None):
"""Class representing a local message"""
def __init__( # pylint: disable=too-many-arguments,super-init-not-called
self,
feed: Feed,
content: Dict[str, Any],
signature: Optional[str] = None,
sequence: int = 1,
timestamp: Optional[int] = None,
previous: Optional[Message] = None,
):
self.feed = feed
self.content = content
@ -119,7 +183,8 @@ class LocalMessage(Message):
else:
self.signature = signature
def _sign(self):
def _sign(self) -> str:
# ensure ordering of keys and indentation of 2 characters, like ssb-keys
data = self.serialize(add_signature=False)
return (b64encode(bytes(self.feed.sign(data))) + b'.sig.ed25519').decode('ascii')
return (b64encode(bytes(self.feed.sign(data))) + b".sig.ed25519").decode("ascii")

View File

@ -1,140 +1,238 @@
from functools import wraps
"""MuxRPC"""
from async_generator import async_generator, yield_
from typing import Any, AsyncIterator, Callable, Dict, Generator, List, Literal, Optional, Union
from ssb.packet_stream import PSMessageType
from typing_extensions import Self
from .packet_stream import PacketStream, PSMessage, PSMessageType, PSRequestHandler, PSStreamHandler
MuxRPCJSON = Dict[str, Any]
MuxRPCCallType = Literal["async", "duplex", "sink", "source", "sync"]
MuxRPCRequestHandlerType = Callable[[PacketStream, "MuxRPCRequest"], None]
MuxRPCRequestParam = Union[bytes, str, MuxRPCJSON] # pylint: disable=invalid-name
class MuxRPCAPIException(Exception):
pass
"""Exception to raise on MuxRPC API errors"""
class MuxRPCHandler(object):
def check_message(self, msg):
class MuxRPCHandler: # pylint: disable=too-few-public-methods
"""Base MuxRPC handler class"""
def check_message(self, msg: PSMessage) -> None:
"""Check message validity"""
body = msg.body
if isinstance(body, dict) and 'name' in body and body['name'] == 'Error':
raise MuxRPCAPIException(body['message'])
if isinstance(body, dict) and "name" in body and body["name"] == "Error":
raise MuxRPCAPIException(body["message"])
def __await__(self) -> Generator[Optional[PSMessage], None, None]:
raise NotImplementedError()
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
raise NotImplementedError()
async def __anext__(self) -> Optional[PSMessage]:
raise NotImplementedError()
def send(self, msg: Any, msg_type: PSMessageType = PSMessageType.JSON, end: bool = False) -> None:
"""Send a message through the stream"""
raise NotImplementedError()
class MuxRPCRequestHandler(MuxRPCHandler):
def __init__(self, ps_handler):
class MuxRPCRequestHandler(MuxRPCHandler): # pylint: disable=abstract-method
"""Base class for MuxRPC request handlers"""
def __init__(self, ps_handler: PSRequestHandler):
self.ps_handler = ps_handler
def __await__(self):
msg = (yield from self.ps_handler.__await__())
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
return self
async def __anext__(self) -> Optional[PSMessage]:
msg = await self.ps_handler.__anext__()
assert msg
self.check_message(msg)
return msg
class MuxRPCSourceHandler(MuxRPCHandler):
def __init__(self, ps_handler):
class MuxRPCSourceHandler(MuxRPCHandler): # pylint: disable=abstract-method
"""MuxRPC handler for sources"""
def __init__(self, ps_handler: PSStreamHandler):
self.ps_handler = ps_handler
@async_generator
async def __aiter__(self):
async for msg in self.ps_handler:
try:
self.check_message(msg)
await yield_(msg)
except MuxRPCAPIException:
raise
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
return self
async def __anext__(self) -> Optional[PSMessage]:
msg = await self.ps_handler.__anext__()
assert msg
self.check_message(msg)
return msg
class MuxRPCSinkHandlerMixin(object):
class MuxRPCSinkHandlerMixin: # pylint: disable=too-few-public-methods
"""Mixin for sink-type MuxRPC handlers"""
connection: Optional[PacketStream]
req: Optional[int]
def send(self, msg: Any, msg_type: PSMessageType = PSMessageType.JSON, end: bool = False) -> None:
"""Send a message through the stream"""
assert self.connection
def send(self, msg, msg_type=PSMessageType.JSON, end=False):
self.connection.send(msg, stream=True, msg_type=msg_type, req=self.req, end_err=end)
class MuxRPCDuplexHandler(MuxRPCSinkHandlerMixin, MuxRPCSourceHandler):
def __init__(self, ps_handler, connection, req):
super(MuxRPCDuplexHandler, self).__init__(ps_handler)
class MuxRPCDuplexHandler(MuxRPCSinkHandlerMixin, MuxRPCSourceHandler): # pylint: disable=abstract-method
"""MuxRPC handler for duplex streams"""
def __init__(self, ps_handler: PSStreamHandler, connection: PacketStream, req: int):
super().__init__(ps_handler)
self.connection = connection
self.req = req
class MuxRPCSinkHandler(MuxRPCHandler, MuxRPCSinkHandlerMixin):
def __init__(self, connection, req):
class MuxRPCSinkHandler(MuxRPCHandler, MuxRPCSinkHandlerMixin): # pylint: disable=abstract-method
"""MuxRPC handler for sinks"""
def __init__(self, connection: PacketStream, req: int):
self.connection = connection
self.req = req
def _get_appropriate_api_handler(type_, connection, ps_handler, req):
if type_ in {'sync', 'async'}:
def _get_appropriate_api_handler(
type_: MuxRPCCallType, connection: PacketStream, ps_handler: Union[PSRequestHandler, PSStreamHandler], req: int
) -> MuxRPCHandler:
"""Find the appropriate MuxRPC handler"""
if type_ in {"sync", "async"}:
assert isinstance(ps_handler, PSRequestHandler)
return MuxRPCRequestHandler(ps_handler)
elif type_ == 'source':
if type_ == "source":
assert isinstance(ps_handler, PSStreamHandler)
return MuxRPCSourceHandler(ps_handler)
elif type_ == 'sink':
if type_ == "sink":
return MuxRPCSinkHandler(connection, req)
elif type_ == 'duplex':
if type_ == "duplex":
assert isinstance(ps_handler, PSStreamHandler)
return MuxRPCDuplexHandler(ps_handler, connection, req)
raise TypeError(f"Unknown request type {type_}")
class MuxRPCRequest:
"""MuxRPC request"""
class MuxRPCRequest(object):
@classmethod
def from_message(cls, message):
body = message.body
return cls('.'.join(body['name']), body['args'])
def from_message(cls, message: PSMessage) -> Self:
"""Initialise a request from a raw packet stream message"""
def __init__(self, name, args):
body = message.body
return cls(".".join(body["name"]), body["args"])
def __init__(self, name: str, args: List[MuxRPCRequestParam]):
self.name = name
self.args = args
def __repr__(self):
return '<MuxRPCRequest {0.name} {0.args}>'.format(self)
def __repr__(self) -> str:
return f"<MuxRPCRequest {self.name} {self.args}>"
class MuxRPCMessage(object):
class MuxRPCMessage:
"""MuxRPC message"""
@classmethod
def from_message(cls, message):
def from_message(cls, message: PSMessage) -> Self:
"""Initialise a MuxRPC message from a raw packet stream message"""
return cls(message.body)
def __init__(self, body):
def __init__(self, body: PSMessage):
self.body = body
def __repr__(self):
return '<MuxRPCMessage {0.body}}>'.format(self)
def __repr__(self) -> str:
return f"<MuxRPCMessage {self.body}>"
class MuxRPCAPI(object):
def __init__(self):
self.handlers = {}
self.connection = None
class MuxRPCAPI:
"""Generit MuxRPC API"""
async def __await__(self):
async for req_message in self.connection:
body = req_message.body
if req_message is None:
return
if isinstance(body, dict) and body.get('name'):
self.process(self.connection, MuxRPCRequest.from_message(req_message))
def __init__(self) -> None:
self.handlers: Dict[str, MuxRPCRequestHandlerType] = {}
self.connection: Optional[PacketStream] = None
def __aiter__(self) -> AsyncIterator[None]:
return self
async def __anext__(self) -> None:
assert self.connection
req_message = await self.connection.__anext__()
if req_message is None:
raise StopAsyncIteration()
body = req_message.body
if isinstance(body, dict) and body.get("name"):
self.process(self.connection, MuxRPCRequest.from_message(req_message))
def __await__(self) -> Generator[None, None, None]:
yield from self.__anext__().__await__()
def add_connection(self, connection: PacketStream) -> None:
"""Set the packet stream connection of this RPC API"""
def add_connection(self, connection):
self.connection = connection
def define(self, name):
def _handle(f):
def define(self, name: str) -> Callable[[MuxRPCRequestHandlerType], MuxRPCRequestHandlerType]:
"""Decorator to define an RPC method handler"""
def _handle(f: MuxRPCRequestHandlerType) -> MuxRPCRequestHandlerType:
self.handlers[name] = f
@wraps(f)
def _f(*args, **kwargs):
return f(*args, **kwargs)
return f
return _handle
def process(self, connection, request):
def process(self, connection: PacketStream, request: MuxRPCRequest) -> None:
"""Process an incoming request"""
handler = self.handlers.get(request.name)
if not handler:
raise MuxRPCAPIException('Method {} not found!'.format(request.name))
raise MuxRPCAPIException(f"Method {request.name} not found!")
handler(connection, request)
def call(self, name, args, type_='sync'):
def call(self, name: str, args: List[MuxRPCRequestParam], type_: MuxRPCCallType = "sync") -> MuxRPCHandler:
"""Call an RPC method"""
assert self.connection
if not self.connection.is_connected:
raise Exception('not connected')
raise Exception("not connected") # pylint: disable=broad-exception-raised
old_counter = self.connection.req_counter
ps_handler = self.connection.send({
'name': name.split('.'),
'args': args,
'type': type_
}, stream=type_ in {'sink', 'source', 'duplex'})
ps_handler = self.connection.send(
{"name": name.split("."), "args": args, "type": type_},
stream=type_ in {"sink", "source", "duplex"},
)
return _get_appropriate_api_handler(type_, self.connection, ps_handler, old_counter)

View File

@ -1,180 +1,255 @@
import logging
import struct
"""Packet streams"""
from asyncio import Event, Queue
from enum import Enum
from time import time
import logging
from math import ceil
import struct
from time import time
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Union
from secret_handshake.network import SHSDuplexStream
import simplejson
from async_generator import async_generator, yield_
from typing_extensions import Self
from secret_handshake import SHSClient, SHSServer
logger = logging.getLogger('packet_stream')
PSHandler = Union["PSRequestHandler", "PSStreamHandler"]
PSMessageData = Union[bytes, bool, Dict[str, Any], str]
logger = logging.getLogger("packet_stream")
class PSMessageType(Enum):
"""Available message types"""
BUFFER = 0
TEXT = 1
JSON = 2
class PSStreamHandler(object):
def __init__(self, req):
super(PSStreamHandler).__init__()
self.req = req
self.queue = Queue()
class PSStreamHandler:
"""Packet stream handler"""
def __init__(self, req: int):
self.req = req
self.queue: Queue[Optional["PSMessage"]] = Queue()
async def process(self, msg: "PSMessage") -> None:
"""Process a pending message"""
async def process(self, msg):
await self.queue.put(msg)
async def stop(self):
async def stop(self) -> None:
"""Stop a pending request"""
await self.queue.put(None)
@async_generator
async def __aiter__(self):
while True:
elem = await self.queue.get()
if not elem:
return
await yield_(elem)
def __aiter__(self) -> AsyncIterator[Optional["PSMessage"]]:
return self
async def __anext__(self) -> Optional["PSMessage"]:
elem = await self.queue.get()
if not elem:
raise StopAsyncIteration()
return elem
class PSRequestHandler(object):
def __init__(self, req):
super(PSRequestHandler).__init__()
class PSRequestHandler:
"""Packet stream request handler"""
def __init__(self, req: int):
self.req = req
self.event = Event()
self._msg = None
self._msg: Optional[PSMessage] = None
async def process(self, msg: "PSMessage") -> None:
"""Process a message request"""
async def process(self, msg):
self._msg = msg
self.event.set()
async def stop(self):
async def stop(self) -> None:
"""Stop a pending event request"""
if not self.event.is_set():
self.event.set()
def __await__(self):
def __aiter__(self):
return self
async def __anext__(self) -> Optional["PSMessage"]:
# wait until 'process' is called
yield from self.event.wait().__await__()
await self.event.wait()
return self._msg
class PSMessage(object):
class PSMessage:
"""Packet Stream message"""
@classmethod
def from_header_body(cls, flags, req, body):
def from_header_body(cls, flags: int, req: int, body: bytes) -> Self:
"""Parse a raw message"""
type_ = PSMessageType(flags & 0x03)
if type_ == PSMessageType.TEXT:
body = body.decode('utf-8')
body_s = body.decode("utf-8")
elif type_ == PSMessageType.JSON:
body = simplejson.loads(body)
body_s = simplejson.loads(body)
return cls(type_, body, bool(flags & 0x08), bool(flags & 0x04), req=req)
return cls(type_, body_s, bool(flags & 0x08), bool(flags & 0x04), req=req)
@property
def data(self):
def data(self) -> bytes:
"""The raw message data"""
if self.type == PSMessageType.TEXT:
return self.body.encode('utf-8')
elif self.type == PSMessageType.JSON:
return simplejson.dumps(self.body).encode('utf-8')
assert isinstance(self.body, str)
return self.body.encode("utf-8")
if self.type == PSMessageType.JSON:
return simplejson.dumps(self.body).encode("utf-8")
assert isinstance(self.body, bytes)
return self.body
def __init__(self, type_, body, stream, end_err, req=None):
def __init__(
self, type_: PSMessageType, body: Any, stream: bool, end_err: bool, req: Optional[int] = None
): # pylint: disable=too-many-arguments
self.stream = stream
self.end_err = end_err
self.type = type_
self.body = body
self.req = req
def __repr__(self):
def __repr__(self) -> str:
if self.type == PSMessageType.BUFFER:
body = '{} bytes'.format(len(self.body))
body = f"{len(self.body)} bytes"
else:
body = self.body
return '<PSMessage ({}): {}{} {}{}>'.format(self.type.name, body,
'' if self.req is None else ' [{}]'.format(self.req),
'~' if self.stream else '', '!' if self.end_err else '')
req = "" if self.req is None else f" [{self.req}]"
is_stream = "~" if self.stream else ""
err = "!" if self.end_err else ""
return f"<PSMessage ({self.type.name}): {body}{req} {is_stream}{err}>"
class PacketStream(object):
def __init__(self, connection):
class PacketStream:
"""SSB Packet stream"""
def __init__(self, connection: SHSDuplexStream):
self.connection = connection
self.req_counter = 1
self._event_map = {}
self._event_map: Dict[int, Tuple[float, PSHandler]] = {}
self._connected = False
def register_handler(self, handler: PSHandler) -> None:
"""Register an RPC handler"""
def register_handler(self, handler):
self._event_map[handler.req] = (time(), handler)
@property
def is_connected(self):
def is_connected(self) -> bool:
"""Check if the stream is connected"""
return self.connection.is_connected
@async_generator
async def __aiter__(self):
while True:
msg = await self.read()
if not msg:
return
# filter out replies
if msg.req >= 0:
await yield_(msg)
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
return self
async def __await__(self):
async def __anext__(self) -> Optional[PSMessage]:
msg = await self.read()
if not msg:
raise StopAsyncIteration()
if msg.req is not None and msg.req >= 0:
return msg
return None
async def __await__(self) -> None:
async for data in self:
logger.info('RECV: %r', data)
logger.info("RECV: %r", data)
if data is None:
return
async def _read(self):
async def _read(self) -> Optional[PSMessage]:
try:
header = await self.connection.read()
if not header or header == b'\x00' * 9:
return
flags, length, req = struct.unpack('>BIi', header)
if not header or header == b"\x00" * 9:
return None
flags, length, req = struct.unpack(">BIi", header)
n_packets = ceil(length / 4096)
body = b''
for n in range(n_packets):
body += await self.connection.read()
body = b""
for _ in range(n_packets):
read_data = await self.connection.read()
if read_data is not None:
body += read_data
logger.debug("READ %s %s", header, len(body))
logger.debug('READ %s %s', header, len(body))
return PSMessage.from_header_body(flags, req, body)
except StopAsyncIteration:
logger.debug('DISCONNECT')
logger.debug("DISCONNECT")
self.connection.disconnect()
return None
async def read(self):
async def read(self) -> Optional[PSMessage]:
"""Read data from the packet stream"""
msg = await self._read()
if not msg:
return None
# check whether it's a reply and handle accordingly
if msg.req < 0:
t, handler = self._event_map[-msg.req]
if msg.req is not None and msg.req < 0:
_, handler = self._event_map[-msg.req]
await handler.process(msg)
logger.info('RESPONSE [%d]: %r', -msg.req, msg)
logger.info("RESPONSE [%d]: %r", -msg.req, msg)
if msg.end_err:
await handler.stop()
del self._event_map[-msg.req]
logger.info('RESPONSE [%d]: EOS', -msg.req)
logger.info("RESPONSE [%d]: EOS", -msg.req)
return msg
def _write(self, msg):
logger.info('SEND [%d]: %r', msg.req, msg)
header = struct.pack('>BIi', (int(msg.stream) << 3) | (int(msg.end_err) << 2) | msg.type.value, len(msg.data),
msg.req)
def _write(self, msg: PSMessage) -> None:
logger.info("SEND [%d]: %r", msg.req, msg)
header = struct.pack(
">BIi", (int(msg.stream) << 3) | (int(msg.end_err) << 2) | msg.type.value, len(msg.data), msg.req
)
self.connection.write(header)
self.connection.write(msg.data)
logger.debug('WRITE HDR: %s', header)
logger.debug('WRITE DATA: %s', msg.data)
logger.debug("WRITE HDR: %s", header)
logger.debug("WRITE DATA: %s", msg.data)
def send( # pylint: disable=too-many-arguments
self,
data: Any,
msg_type: PSMessageType = PSMessageType.JSON,
stream: bool = False,
end_err: bool = False,
req: Optional[int] = None,
) -> PSHandler:
"""Send data through the packet stream"""
def send(self, data, msg_type=PSMessageType.JSON, stream=False, end_err=False, req=None):
update_counter = False
if req is None:
update_counter = True
req = self.req_counter
@ -185,15 +260,19 @@ class PacketStream(object):
self._write(msg)
if stream:
handler = PSStreamHandler(self.req_counter)
handler: PSHandler = PSStreamHandler(self.req_counter)
else:
handler = PSRequestHandler(self.req_counter)
self.register_handler(handler)
if update_counter:
self.req_counter += 1
return handler
def disconnect(self):
def disconnect(self) -> None:
"""Disconnect the stream"""
self._connected = False
self.connection.disconnect()

View File

@ -1,29 +1,39 @@
import os
import yaml
from base64 import b64decode, b64encode
"""Utility functions"""
from nacl.signing import SigningKey
from base64 import b64decode, b64encode
import os
from typing import TypedDict
from nacl.signing import SigningKey, VerifyKey
import yaml
class SSBSecret(TypedDict):
"""Dictionary type to hold an SSB secret identity"""
keypair: SigningKey
id: str
class ConfigException(Exception):
pass
"""Exception to raise if there is a problem with the configuration data"""
def tag(key):
"""Create tag from publick key."""
return b'@' + b64encode(bytes(key)) + b'.ed25519'
def tag(key: VerifyKey) -> bytes:
"""Create tag from public key."""
return b"@" + b64encode(bytes(key)) + b".ed25519"
def load_ssb_secret():
def load_ssb_secret() -> SSBSecret:
"""Load SSB keys from ~/.ssb"""
with open(os.path.expanduser('~/.ssb/secret')) as f:
with open(os.path.expanduser("~/.ssb/secret"), encoding="utf-8") as f:
config = yaml.load(f, Loader=yaml.SafeLoader)
if config['curve'] != 'ed25519':
raise ConfigException('Algorithm not known: ' + config['curve'])
if config["curve"] != "ed25519":
raise ConfigException("Algorithm not known: " + config["curve"])
server_prv_key = b64decode(config['private'][:-8])
return {
'keypair': SigningKey(server_prv_key[:32]),
'id': config['id']
}
server_prv_key = b64decode(config["private"][:-8])
return {"keypair": SigningKey(server_prv_key[:32]), "id": config["id"]}

View File

@ -1,11 +1,12 @@
"""Tests for the feed functionality"""
from base64 import b64decode
from collections import OrderedDict
import pytest
from nacl.signing import SigningKey, VerifyKey
import pytest
from ssb.feed import LocalMessage, LocalFeed, Feed, Message, NoPrivateKeyException
from ssb.feed import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException
SERIALIZED_M1 = b"""{
"previous": null,
@ -23,127 +24,147 @@ SERIALIZED_M1 = b"""{
}"""
@pytest.fixture()
def local_feed():
secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=')
@pytest.fixture
def local_feed() -> LocalFeed:
"""Fixture providing a local feed"""
secret = b64decode("Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=")
return LocalFeed(SigningKey(secret))
@pytest.fixture()
def remote_feed():
public = b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=')
@pytest.fixture
def remote_feed() -> Feed:
"""Fixture providing a remote feed"""
public = b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
return Feed(VerifyKey(public))
def test_local_feed():
secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=')
def test_local_feed() -> None:
"""Test a local feed"""
secret = b64decode("Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=")
feed = LocalFeed(SigningKey(secret))
assert bytes(feed.private_key) == secret
assert bytes(feed.public_key) == b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=')
assert feed.id == '@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519'
assert bytes(feed.public_key) == b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
def test_remote_feed():
public = b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=')
def test_remote_feed() -> None:
"""Test a remote feed"""
public = b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
feed = Feed(VerifyKey(public))
assert bytes(feed.public_key) == public
assert feed.id == '@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519'
assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
m1 = Message(feed, OrderedDict([
('type', 'about'),
('about', feed.id),
('name', 'neo'),
('description', 'The Chosen One')
]), 'foo', timestamp=1495706260190)
m1 = Message(
feed,
OrderedDict([("type", "about"), ("about", feed.id), ("name", "neo"), ("description", "The Chosen One")]),
"foo",
timestamp=1495706260190,
)
with pytest.raises(NoPrivateKeyException):
feed.sign(m1)
def test_local_message(local_feed):
m1 = LocalMessage(local_feed, OrderedDict([
('type', 'about'),
('about', local_feed.id),
('name', 'neo'),
('description', 'The Chosen One')
]), timestamp=1495706260190)
def test_local_message(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
"""Test a local message"""
m1 = LocalMessage(
local_feed,
OrderedDict([("type", "about"), ("about", local_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
timestamp=1495706260190,
)
assert m1.timestamp == 1495706260190
assert m1.previous is None
assert m1.sequence == 1
assert m1.signature == \
'lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519'
assert m1.key == '%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256'
assert m1.signature == (
"lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
)
assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
m2 = LocalMessage(local_feed, OrderedDict([
('type', 'about'),
('about', local_feed.id),
('name', 'morpheus'),
('description', 'Dude with big jaw')
]), previous=m1, timestamp=1495706447426)
m2 = LocalMessage(
local_feed,
OrderedDict(
[("type", "about"), ("about", local_feed.id), ("name", "morpheus"), ("description", "Dude with big jaw")]
),
previous=m1,
timestamp=1495706447426,
)
assert m2.timestamp == 1495706447426
assert m2.previous is m1
assert m2.sequence == 2
assert m2.signature == \
'3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519'
assert m2.key == '%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256'
assert m2.signature == (
"3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
)
assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
def test_remote_message(remote_feed):
signature = 'lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519'
m1 = Message(remote_feed, OrderedDict([
('type', 'about'),
('about', remote_feed.id),
('name', 'neo'),
('description', 'The Chosen One')
]), signature, timestamp=1495706260190)
def test_remote_message(remote_feed: Feed) -> None: # pylint: disable=redefined-outer-name
"""Test a remote message"""
signature = "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
m1 = Message(
remote_feed,
OrderedDict([("type", "about"), ("about", remote_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
signature,
timestamp=1495706260190,
)
assert m1.timestamp == 1495706260190
assert m1.previous is None
assert m1.sequence == 1
assert m1.signature == signature
assert m1.key == '%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256'
assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
signature = '3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519'
m2 = Message(remote_feed, OrderedDict([
('type', 'about'),
('about', remote_feed.id),
('name', 'morpheus'),
('description', 'Dude with big jaw')
]), signature, previous=m1, timestamp=1495706447426)
signature = "3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
m2 = Message(
remote_feed,
OrderedDict(
[("type", "about"), ("about", remote_feed.id), ("name", "morpheus"), ("description", "Dude with big jaw")]
),
signature,
previous=m1,
timestamp=1495706447426,
)
assert m2.timestamp == 1495706447426
assert m2.previous is m1
assert m2.sequence == 2
assert m2.signature == signature
m2.verify(signature)
assert m2.key == '%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256'
assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
def test_remote_no_signature(remote_feed):
def test_remote_no_signature(remote_feed: Feed) -> None: # pylint: disable=redefined-outer-name
"""Test remote feed without a signature"""
with pytest.raises(ValueError):
Message(remote_feed, OrderedDict([
('type', 'about'),
('about', remote_feed.id),
('name', 'neo'),
('description', 'The Chosen One')
]), None, timestamp=1495706260190)
Message(
remote_feed,
OrderedDict(
[("type", "about"), ("about", remote_feed.id), ("name", "neo"), ("description", "The Chosen One")]
),
None,
timestamp=1495706260190,
)
def test_serialize(local_feed):
m1 = LocalMessage(local_feed, OrderedDict([
('type', 'about'),
('about', local_feed.id),
('name', 'neo'),
('description', 'The Chosen One')
]), timestamp=1495706260190)
def test_serialize(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
"""Test feed serialization"""
m1 = LocalMessage(
local_feed,
OrderedDict([("type", "about"), ("about", local_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
timestamp=1495706260190,
)
assert m1.serialize() == SERIALIZED_M1
def test_parse(local_feed):
def test_parse(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
"""Test feed parsing"""
m1 = LocalMessage.parse(SERIALIZED_M1, local_feed)
assert m1.content == {
'type': 'about',
'about': local_feed.id,
'name': 'neo',
'description': 'The Chosen One'
}
assert m1.content == {"type": "about", "about": local_feed.id, "name": "neo", "description": "The Chosen One"}
assert m1.timestamp == 1495706260190

View File

@ -1,96 +1,140 @@
"""Tests for the packet stream"""
from asyncio import Event, ensure_future, gather
from asyncio.events import AbstractEventLoop
import json
from asyncio import ensure_future, gather, Event
from typing import AsyncGenerator, Awaitable, Callable, Generator, List
import pytest
from nacl.signing import SigningKey
from pytest_mock import MockerFixture
from secret_handshake.network import SHSDuplexStream
from ssb.packet_stream import PacketStream, PSMessageType
from ssb.packet_stream import PacketStream, PSMessage, PSMessageType
async def _collect_messages(generator):
async def _collect_messages(generator: AsyncGenerator[PSMessage, None]) -> List[PSMessage]:
results = []
async for msg in generator:
results.append(msg)
return results
MSG_BODY_1 = (b'{"previous":"%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":116,"timestamp":1496696699331,"hash":"sha256","content":{"type"'
b':"post","channel":"crypto","text":"Does anybody know any good resources (e.g. books) to learn cryptogra'
b'phy? I\'m not speaking of basic concepts (e.g. what\'s a private key) but the actual mathematics behind'
b' the whole thing.\\nI have a copy of the \\"Handbook of Applied Cryptography\\" on my bookshelf but I f'
b'ound it too long/hard to follow. Are there any better alternatives?","mentions":[]},"signature":"hqKePb'
b'bTXWxEi1njDnOWFsL0M0AoNoWyBFgNE6KXj//DThepaZSy9vRbygDHX5uNmCdyOrsQrwZsZhmUYKwtDQ==.sig.ed25519"}')
MSG_BODY_2 = (b'{"previous":"%iQRhPyqmNLpGaO1Tpm1I22jqnUEwRwkCTDbwAGtM+lY=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":103,"timestamp":1496674211806,"hash":"sha256","content":{"type"'
b':"post","channel":"git-ssb","text":"Is it only me or `git.scuttlebot.io` is timing out?\\n\\nE.g. try a'
b'ccessing %vZCTqraoqKBKNZeATErXEtnoEr+wnT3p8tT+vL+29I4=.sha256","mentions":[{"link":"%vZCTqraoqKBKNZeATE'
b'rXEtnoEr+wnT3p8tT+vL+29I4=.sha256"}]},"signature":"+i4U0HUGDDEyNoNr2NIROPnT3WQj3RuTaIhY5koWW8f0vwr4tZsY'
b'mAkqqMwFWfP+eBIbc7DZ835er6r6h9CwAg==.sig.ed25519"}')
MSG_BODY_1 = (
b'{"previous":"%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":116,"timestamp":1496696699331,"hash":"sha256","content":{"type"'
b':"post","channel":"crypto","text":"Does anybody know any good resources (e.g. books) to learn cryptogra'
b"phy? I'm not speaking of basic concepts (e.g. what's a private key) but the actual mathematics behind"
b' the whole thing.\\nI have a copy of the \\"Handbook of Applied Cryptography\\" on my bookshelf but I f'
b'ound it too long/hard to follow. Are there any better alternatives?","mentions":[]},"signature":"hqKePb'
b'bTXWxEi1njDnOWFsL0M0AoNoWyBFgNE6KXj//DThepaZSy9vRbygDHX5uNmCdyOrsQrwZsZhmUYKwtDQ==.sig.ed25519"}'
)
MSG_BODY_2 = (
b'{"previous":"%iQRhPyqmNLpGaO1Tpm1I22jqnUEwRwkCTDbwAGtM+lY=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":103,"timestamp":1496674211806,"hash":"sha256","content":{"type"'
b':"post","channel":"git-ssb","text":"Is it only me or `git.scuttlebot.io` is timing out?\\n\\nE.g. try a'
b'ccessing %vZCTqraoqKBKNZeATErXEtnoEr+wnT3p8tT+vL+29I4=.sha256","mentions":[{"link":"%vZCTqraoqKBKNZeATE'
b'rXEtnoEr+wnT3p8tT+vL+29I4=.sha256"}]},"signature":"+i4U0HUGDDEyNoNr2NIROPnT3WQj3RuTaIhY5koWW8f0vwr4tZsY'
b'mAkqqMwFWfP+eBIbc7DZ835er6r6h9CwAg==.sig.ed25519"}'
)
class MockSHSSocket(SHSDuplexStream):
def __init__(self, *args, **kwargs):
super(MockSHSSocket, self).__init__()
self.input = []
self.output = []
self.is_connected = False
self._on_connect = []
"""A mocked SHS socket"""
def __init__(self): # pylint: disable=unused-argument
super().__init__()
self.input: List[bytes] = []
self.output: List[bytes] = []
self.is_connected = False
self._on_connect: List[Callable[[SHSDuplexStream], Awaitable[None]]] = []
def on_connect(self, cb: Callable[[SHSDuplexStream], Awaitable[None]]) -> None:
"""Set the on_connect callback"""
def on_connect(self, cb):
self._on_connect.append(cb)
async def read(self):
async def read(self) -> bytes:
"""Read data from the socket"""
if not self.input:
raise StopAsyncIteration
return self.input.pop(0)
def write(self, data):
def write(self, data: bytes) -> None:
"""Write data to the socket"""
self.output.append(data)
def feed(self, input):
self.input += input
def feed(self, input_: List[bytes]) -> None:
"""Feed data into the connection"""
self.input += input_
def get_output(self) -> Generator[bytes, None, None]:
"""Get the output of a call"""
def get_output(self):
while True:
if not self.output:
break
yield self.output.pop(0)
def disconnect(self):
def disconnect(self) -> None:
"""Disconnect from the remote party"""
self.is_connected = False
class MockSHSClient(MockSHSSocket):
async def connect(self):
"""A mocked SHS client"""
async def connect(self) -> None:
"""Connect to a SHS server"""
self.is_connected = True
for cb in self._on_connect:
await cb()
await cb(self)
class MockSHSServer(MockSHSSocket):
def listen(self):
"""A mocked SHS server"""
def listen(self) -> None:
"""Listen for new connections"""
self.is_connected = True
for cb in self._on_connect:
ensure_future(cb())
ensure_future(cb(self))
@pytest.fixture
def ps_client(event_loop):
def ps_client(event_loop: AbstractEventLoop) -> MockSHSClient: # pylint: disable=unused-argument
"""Fixture to provide a mocked SHS client"""
return MockSHSClient()
@pytest.fixture
def ps_server(event_loop):
def ps_server(event_loop: AbstractEventLoop) -> MockSHSServer: # pylint: disable=unused-argument
"""Fixture to provide a mocked SHS server"""
return MockSHSServer()
@pytest.mark.asyncio
async def test_on_connect(ps_server):
async def test_on_connect(ps_server: MockSHSServer) -> None: # pylint: disable=redefined-outer-name
"""Test the on_connect callback functionality"""
called = Event()
async def _on_connect():
async def _on_connect(_: SHSDuplexStream) -> None:
called.set()
ps_server.on_connect(_on_connect)
@ -100,128 +144,132 @@ async def test_on_connect(ps_server):
@pytest.mark.asyncio
async def test_message_decoding(ps_client):
async def test_message_decoding(ps_client: MockSHSClient) -> None: # pylint: disable=redefined-outer-name
"""Test message decoding"""
await ps_client.connect()
ps = PacketStream(ps_client)
assert ps.is_connected
ps_client.feed([
b'\n\x00\x00\x00\x9a\x00\x00\x04\xfb',
b'{"name":["createHistoryStream"],"args":[{"id":"@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519",'
b'"seq":10,"live":true,"keys":false}],"type":"source"}'
])
ps_client.feed(
[
b"\n\x00\x00\x00\x9a\x00\x00\x04\xfb",
b'{"name":["createHistoryStream"],"args":[{"id":"@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519",'
b'"seq":10,"live":true,"keys":false}],"type":"source"}',
]
)
messages = (await _collect_messages(ps))
messages = await _collect_messages(ps)
assert len(messages) == 1
assert messages[0].type == PSMessageType.JSON
assert messages[0].body == {
'name': ['createHistoryStream'],
'args': [
{
'id': '@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519',
'seq': 10,
'live': True,
'keys': False
}
"name": ["createHistoryStream"],
"args": [
{"id": "@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519", "seq": 10, "live": True, "keys": False}
],
'type': 'source'
"type": "source",
}
@pytest.mark.asyncio
async def test_message_encoding(ps_client):
async def test_message_encoding(ps_client: MockSHSClient) -> None: # pylint: disable=redefined-outer-name
"""Test message encoding"""
await ps_client.connect()
ps = PacketStream(ps_client)
assert ps.is_connected
ps.send({
'name': ['createHistoryStream'],
'args': [{
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
'seq': 1,
'live': False,
'keys': False
}],
'type': 'source'
}, stream=True)
ps.send(
{
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
],
"type": "source",
},
stream=True,
)
header, body = list(ps_client.get_output())
assert header == b'\x0a\x00\x00\x00\xa6\x00\x00\x00\x01'
assert json.loads(body.decode('utf-8')) == {
assert header == b"\x0a\x00\x00\x00\xa6\x00\x00\x00\x01"
assert json.loads(body.decode("utf-8")) == {
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
],
"type": "source"
"type": "source",
}
@pytest.mark.asyncio
async def test_message_stream(ps_client, mocker):
async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture): # pylint: disable=redefined-outer-name
"""Test requesting a history stream"""
await ps_client.connect()
ps = PacketStream(ps_client)
mocker.patch.object(ps, 'register_handler', wraps=ps.register_handler)
mocker.patch.object(ps, "register_handler", wraps=ps.register_handler)
assert ps.is_connected
ps.send({
'name': ['createHistoryStream'],
'args': [{
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
'seq': 1,
'live': False,
'keys': False
}],
'type': 'source'
}, stream=True)
ps.send(
{
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
],
"type": "source",
},
stream=True,
)
assert ps.req_counter == 2
assert ps.register_handler.call_count == 1
handler = list(ps._event_map.values())[0][1]
mock_process = mocker.AsyncMock()
assert ps.register_handler.call_count == 1 # pylint: disable=no-member
handler = list(ps._event_map.values())[0][1] # pylint: disable=protected-access
mocker.patch.object(handler, 'process', mock_process)
mock_process = mocker.patch.object(handler, "process")
ps_client.feed([b'\n\x00\x00\x02\xc5\xff\xff\xff\xff', MSG_BODY_1])
ps_client.feed([b"\n\x00\x00\x02\xc5\xff\xff\xff\xff", MSG_BODY_1])
msg = await ps.read()
assert mock_process.await_count == 1
# responses have negative req
assert msg.req == -1
assert msg.body['previous'] == '%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256'
assert msg.body["previous"] == "%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256"
assert ps.req_counter == 2
stream_handler = ps.send({
'name': ['createHistoryStream'],
'args': [{
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
'seq': 1,
'live': False,
'keys': False
}],
'type': 'source'
}, stream=True)
stream_handler = ps.send(
{
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
],
"type": "source",
},
stream=True,
)
assert ps.req_counter == 3
assert ps.register_handler.call_count == 2
handler = list(ps._event_map.values())[1][1]
assert ps.register_handler.call_count == 2 # pylint: disable=no-member
handler = list(ps._event_map.values())[1][1] # pylint: disable=protected-access
mock_process = mocker.patch.object(handler, 'process', wraps=handler.process)
ps_client.feed([b'\n\x00\x00\x02\xc5\xff\xff\xff\xfe', MSG_BODY_1,
b'\x0e\x00\x00\x023\xff\xff\xff\xfe', MSG_BODY_2])
mock_process = mocker.patch.object(handler, "process", wraps=handler.process)
ps_client.feed(
[b"\n\x00\x00\x02\xc5\xff\xff\xff\xfe", MSG_BODY_1, b"\x0e\x00\x00\x023\xff\xff\xff\xfe", MSG_BODY_2]
)
# execute both message polling and response handling loops
collected, handled = await gather(_collect_messages(ps), _collect_messages(stream_handler))
# No messages collected, since they're all responses
assert collected == []
assert collected == [None, None]
assert mock_process.call_count == 2
@ -231,34 +279,39 @@ async def test_message_stream(ps_client, mocker):
@pytest.mark.asyncio
async def test_message_request(ps_server, mocker):
async def test_message_request(
ps_server: MockSHSServer, mocker: MockerFixture # pylint: disable=redefined-outer-name
) -> None:
"""Test message sending"""
ps_server.listen()
ps = PacketStream(ps_server)
mocker.patch.object(ps, 'register_handler', wraps=ps.register_handler)
mocker.patch.object(ps, "register_handler", wraps=ps.register_handler)
ps.send({
'name': ['whoami'],
'args': []
})
ps.send({"name": ["whoami"], "args": []})
header, body = list(ps_server.get_output())
assert header == b'\x02\x00\x00\x00 \x00\x00\x00\x01'
assert json.loads(body.decode('utf-8')) == {"name": ["whoami"], "args": []}
assert header == b"\x02\x00\x00\x00 \x00\x00\x00\x01"
assert json.loads(body.decode("utf-8")) == {"name": ["whoami"], "args": []}
assert ps.req_counter == 2
assert ps.register_handler.call_count == 1
handler = list(ps._event_map.values())[0][1]
mock_process = mocker.AsyncMock()
assert ps.register_handler.call_count == 1 # pylint: disable=no-member
handler = list(ps._event_map.values())[0][1] # pylint: disable=protected-access
mocker.patch.object(handler, 'process', mock_process)
ps_server.feed([b'\x02\x00\x00\x00>\xff\xff\xff\xff',
b'{"id":"@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"}'])
mock_process = mocker.patch.object(handler, "process")
ps_server.feed(
[
b"\x02\x00\x00\x00>\xff\xff\xff\xff",
b'{"id":"@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"}',
]
)
msg = await ps.read()
assert mock_process.await_count == 1
# responses have negative req
assert msg.req == -1
assert msg.body['id'] == '@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519'
assert msg.body["id"] == "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"
assert ps.req_counter == 2

View File

@ -1,10 +1,11 @@
"""Test for utility functions"""
from base64 import b64decode
from unittest.mock import mock_open, patch
import pytest
from ssb.util import load_ssb_secret, ConfigException
from ssb.util import ConfigException, load_ssb_secret
CONFIG_FILE = """
## Comments should be supported too
@ -16,21 +17,25 @@ CONFIG_FILE = """
}
"""
CONFIG_FILE_INVALID = CONFIG_FILE.replace('ed25519', 'foo')
CONFIG_FILE_INVALID = CONFIG_FILE.replace("ed25519", "foo")
def test_load_secret():
with patch('ssb.util.open', mock_open(read_data=CONFIG_FILE), create=True):
def test_load_secret() -> None:
"""Test loading the SSB secret from a file"""
with patch("ssb.util.open", mock_open(read_data=CONFIG_FILE), create=True):
secret = load_ssb_secret()
priv_key = b'\xfd\xba\x83\x04\x8f\xef\x18\xb0\xf9\xab-\xc6\xc4\xcb \x1cX\x18"\xba\xd8\xd3\xc2_O5\x1a\t\x84\xfa\xc7A'
assert secret['id'] == '@rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=.ed25519'
assert bytes(secret['keypair']) == priv_key
assert bytes(secret['keypair'].verify_key) == b64decode('rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=')
assert secret["id"] == "@rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=.ed25519"
assert bytes(secret["keypair"]) == priv_key
assert bytes(secret["keypair"].verify_key) == b64decode("rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=")
def test_load_exception():
def test_load_exception() -> None:
"""Test configuration loading if there is a problem with the file"""
with pytest.raises(ConfigException):
with patch('ssb.util.open', mock_open(read_data=CONFIG_FILE_INVALID), create=True):
with patch("ssb.util.open", mock_open(read_data=CONFIG_FILE_INVALID), create=True):
load_ssb_secret()