From 5db6b14ed44c98900c993a774e345311d7eba82d Mon Sep 17 00:00:00 2001 From: monty Date: Sat, 1 Jun 2024 14:41:50 +0100 Subject: [PATCH] added NIP46 server signer and moved signing into it's own folder --- examples/run_relay_tor.py | 3 +- src/__init__.py | 0 src/monstr/client/client.py | 2 +- src/monstr/event/event.py | 4 + src/monstr/giftwrap.py | 2 +- src/monstr/inbox.py | 2 +- src/monstr/signing/__init__.py | 0 src/monstr/signing/nip46.py | 282 ++++++++++++++++++++++++++++ src/monstr/{ => signing}/signing.py | 0 9 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/monstr/signing/__init__.py create mode 100644 src/monstr/signing/nip46.py rename src/monstr/{ => signing}/signing.py (100%) diff --git a/examples/run_relay_tor.py b/examples/run_relay_tor.py index 930d12e..921f69a 100644 --- a/examples/run_relay_tor.py +++ b/examples/run_relay_tor.py @@ -24,9 +24,10 @@ now you should be able to run without the torbrowser open if you pass the password(unhashed) in here """ + + async def run_relay(): port = 8081 - with TORService(relay_port=port, service_dir=None, password=None, diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/monstr/client/client.py b/src/monstr/client/client.py index c8a3f6d..90a0f38 100644 --- a/src/monstr/client/client.py +++ b/src/monstr/client/client.py @@ -21,7 +21,7 @@ from datetime import datetime from monstr.util import util_funcs from monstr.event.event import Event -from monstr.signing import SignerInterface, BasicKeySigner +from monstr.signing.signing import SignerInterface, BasicKeySigner from monstr.encrypt import Keys diff --git a/src/monstr/event/event.py b/src/monstr/event/event.py index 3076933..1394791 100644 --- a/src/monstr/event/event.py +++ b/src/monstr/event/event.py @@ -128,6 +128,10 @@ class Event: KIND_CHANNEL_HIDE = 43 KIND_CHANNEL_MUTE = 44 + # used for messaging in in remote signing as nip46, should be encryped as nip4 + # https://github.com/nostr-protocol/nips/blob/master/46.md + KIND_NIP46 = 24133 + # nip 98 http auth header event https://github.com/nostr-protocol/nips/blob/master/98.md KIND_HTTP_AUTH = 27235 diff --git a/src/monstr/giftwrap.py b/src/monstr/giftwrap.py index c3a6efb..9ca83e4 100644 --- a/src/monstr/giftwrap.py +++ b/src/monstr/giftwrap.py @@ -1,7 +1,7 @@ import json import random from datetime import datetime -from monstr.signing import SignerInterface, BasicKeySigner +from monstr.signing.signing import SignerInterface, BasicKeySigner from monstr.encrypt import Keys from monstr.event.event import Event from monstr.util import util_funcs diff --git a/src/monstr/inbox.py b/src/monstr/inbox.py index 79dde0d..c65c227 100644 --- a/src/monstr/inbox.py +++ b/src/monstr/inbox.py @@ -1,7 +1,7 @@ import hashlib import json from monstr.encrypt import Keys -from monstr.signing import SignerInterface +from monstr.signing.signing import SignerInterface from monstr.event.event import Event from monstr.util import util_funcs diff --git a/src/monstr/signing/__init__.py b/src/monstr/signing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/monstr/signing/nip46.py b/src/monstr/signing/nip46.py new file mode 100644 index 0000000..fbae9f8 --- /dev/null +++ b/src/monstr/signing/nip46.py @@ -0,0 +1,282 @@ +import logging +import asyncio +import json +from json import JSONDecodeError +from monstr.client.client import ClientPool, Client +from monstr.encrypt import Keys +from monstr.event.event import Event +from monstr.client.event_handlers import EventHandler, DeduplicateAcceptor +from monstr.signing.signing import SignerInterface +from monstr.util import util_funcs + + +class SignerException(Exception): + pass + + +class NIP46ServerConnection(EventHandler): + """ + this is the server side of a NIP46 signing process. + It'll listens and signs events on behalf of a client that requests it so that the + client never holds the keys itself. + + TODO: add hooks for funcs to allow/disallow event sign + also write the client side which will be implement our signing interface via NIP46 which + will allow us to replace anywhere that can use BasicKeySigner with a NIP46 version + + """ + def __init__(self, + signer: SignerInterface, + relay: [str], + comm_k: str = None): + + self._signer = signer + self._comm_k = comm_k + self._relay = relay + self._run = True + + # TODO - make client so that if it gets async for _on_connect, _do_event + # it'll automaticlly create as task + def _on_connect(my_client: Client): + asyncio.create_task(_aon_connect(my_client)) + + async def _aon_connect(my_client:Client): + my_client.subscribe( + handlers=[self], + filters={ + '#p': [await self._signer.get_public_key()], + 'kinds': [Event.KIND_NIP46] + } + ) + + self._client = ClientPool(self._relay, + on_connect=_on_connect) + + # events queued and dealt with serially as they come in + self._event_q: asyncio.Queue = asyncio.Queue() + # start a process to work on the queued events + self._event_process_task = asyncio.create_task(self._my_event_consumer()) + + asyncio.create_task(self._client.run()) + + super().__init__(event_acceptors=[ + DeduplicateAcceptor() + ]) + + async def describe(self): + await self._client.wait_connect() + sign_k = await self._signer.get_public_key() + + content = json.dumps({ + 'id': 'somerndstring', + 'result': ['describe', + 'get_public_key', + 'sign_event', + 'nip04_encrypt', + 'nip04_decrypt', + 'connect'], + 'error': None + }) + + content = await self._signer.nip4_encrypt(content, to_pub_k=self._comm_k) + + con_event = Event(pub_key=sign_k, + kind=Event.KIND_NIP46, + content=content, + tags=[ + ['p', self._comm_k] + ] + ) + + await self._signer.sign_event(con_event) + + self._client.publish(con_event) + + async def _get_msg_event(self, content: str) -> Event: + # returns encrypted and signed method for content + + # encrypt the content + content = await self._signer.nip4_encrypt(content, + to_pub_k=self._comm_k) + # make the event + ret = Event(pub_key=await self._signer.get_public_key(), + kind=Event.KIND_NIP46, + content=content, + tags=[ + ['p', self._comm_k] + ] + ) + # and sign it + await self._signer.sign_event(ret) + + return ret + + async def _do_response(self, result: str = None, error: str = None, id: str = None): + if result is None: + result = '' + if error is None: + error = '' + if id is None: + id = util_funcs.get_rnd_hex_str(8) + + evt = await self._get_msg_event(json.dumps({ + 'id': id, + 'result': result, + 'error': error + })) + + self._client.publish(evt) + + async def _do_command(self, method: str, params: [str]): + id = util_funcs.get_rnd_hex_str(8) + + evt = await self._get_msg_event(json.dumps({ + 'id': id, + 'method': method, + 'params': params + })) + + self._client.publish(evt) + return id + + async def request_connect(self): + await self._do_command('connect', [await self._signer.get_public_key()]) + + # async def do_ack(self): + # await self._do_response(result=) + + async def connect(self, id: str, params: [str]): + if self._comm_k is None: + self._comm_k = params[0] + + await self._do_response(result=await self._signer.get_public_key(), + id=id) + + # async def describe(self, id: str, params: [str]): + # await self._do_response(result=['describe', + # 'get_public_key', + # 'sign_event', + # 'nip04_decrypt', + # 'connect'], + # id=id) + + async def get_public_key(self, id: str, params: [str]): + await self._do_response(result=await self._signer.get_public_key(), + id=id) + + async def nip04_encrypt(self, id: str, params: [str]): + try: + n_params = len(params) + if n_params < 2: + raise SignerException( + f'Signer::nip04_encrypt: requires 2 params got {n_params} - from key and ciper text') + + to_k = params[0] + plain_text = params[1] + if not Keys.is_hex_key(to_k): + raise SignerException( + f'Signer::nip04_encrypt: from key is not valid {to_k}') + + ciper_text = await self._signer.nip4_encrypt(plain_text=plain_text, + to_pub_k=to_k) + + await self._do_response(result=ciper_text, + id=id) + + except SignerException as se: + await self._do_response(error=str(se), + id=id) + except Exception as e: + await self._do_response(error=f'Signer::nip04_encrypt:' + f'unable to decrypt as {await self._signer.get_public_key()}' + f' error - {str(e)}', + id=id) + + async def nip04_decrypt(self, id: str, params: [str]): + try: + n_params = len(params) + if n_params < 2: + raise SignerException( + f'Signer::nip04_decrypt: requires 2 params got {n_params} - from key and ciper text') + + from_k = params[0] + payload = params[1] + if not Keys.is_hex_key(from_k): + raise SignerException( + f'Signer::nip04_decrypt: from key is not valid {from_k}') + + plain_text = await self._signer.nip4_decrypt(payload=payload, + for_pub_k=from_k) + + await self._do_response(result=plain_text, + id=id) + + except SignerException as se: + await self._do_response(error=str(se), + id=id) + except Exception as e: + await self._do_response(error=f'Signer::nip04_decrypt:' + f'unable to decrypt as {await self._signer.get_public_key()}' + f' error - {str(e)}', + id=id) + + async def sign_event(self, id: str, params: [str]): + try: + evt_data = json.loads(params[0]) + event = Event.load(evt_data) + + await self._signer.sign_event(event) + await self._do_response(result=json.dumps(event.data()), + id=id) + + except JSONDecodeError as je: + await self._do_response(error=f'Signer::sign_event: bad event JSON - {je}', + id=id) + except Exception as e: + await self._do_response(error=f'Signer::sign_event: {e}', + id=id) + + async def _my_event_consumer(self): + while self._run: + try: + args = await self._event_q.get() + await self.ado_event(*args) + + except Exception as e: + logging.debug(f'NIP46ServerConnection::_my_event_consumer: {e}') + + def do_event(self, the_client: Client, sub_id, evt: Event): + if not self.accept_event(the_client=the_client, + sub_id=sub_id, + evt=evt): + return + # put events on a queue so we can deal with async + self._event_q.put_nowait( + (the_client, sub_id, evt) + ) + + async def ado_event(self, the_client: Client, sub_id, evt: Event): + decrypted_evt = await self._signer.nip4_decrypt_event(evt) + + try: + cmd_dict = json.loads(decrypted_evt.content) + if 'method' in cmd_dict: + id = cmd_dict['id'] + method = cmd_dict['method'] + params = cmd_dict['params'] + + if method in {'connect', + 'describe', + 'get_public_key', + 'nip04_decrypt', + 'nip04_encrypt', + 'sign_event'}: + + await getattr(self, method)(id, params) + + except Exception as e: + logging.debug(f'SignerConnection::ado_event {e}') + + def end(self): + self._run = False + self._client.end() \ No newline at end of file diff --git a/src/monstr/signing.py b/src/monstr/signing/signing.py similarity index 100% rename from src/monstr/signing.py rename to src/monstr/signing/signing.py