Skip to content

Commit

Permalink
added new examples nip46 signer and client, other fixes to examples. …
Browse files Browse the repository at this point in the history
…In progress NIP46Signer
  • Loading branch information
monty committed Jun 8, 2024
1 parent c2ec126 commit e8b038d
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 27 deletions.
34 changes: 34 additions & 0 deletions examples/nip46_client_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import asyncio
import logging
from monstr.signing.nip46 import NIP46Signer
from monstr.client.client import Client
from monstr.event.event import Event

# url to relay used for talking to the signer
RELAY = 'ws://localhost:8080'


async def do_post(url, text):
"""
Example showing how to post a text note (Kind 1) to relay
using nip46 signer - we don't have access locally to the keys
"""

# bunker://... e.g. printed out by nip46_signer_service.py
con_str = input('connection string: ').strip()

# we'll make post and they'll be signed by the bunker at con_str
my_signer = NIP46Signer(connection=con_str)

# from here it's just a signer interface same as if we were using BasicKeySigner
async with Client(url) as c:
n_msg = await my_signer.ready_post(Event(kind=Event.KIND_TEXT_NOTE,
content=text))
c.publish(n_msg)


if __name__ == "__main__":
logging.getLogger().setLevel(logging.DEBUG)
text = 'hello using NIP46 signer'

asyncio.run(do_post(RELAY, text))
31 changes: 31 additions & 0 deletions examples/nip46_signer_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import asyncio
import logging
from monstr.encrypt import Keys
from monstr.signing.nip46 import NIP46ServerConnection
from monstr.signing.signing import BasicKeySigner

# url to relay used for talking to the signer
RELAY = 'ws://localhost:8080'


async def run_signer():
"""
Run a service that'll sign events via NIP46 for some random keys
"""
# rnd generate some keys
n_keys = Keys()

# create the signing service
my_signer = NIP46ServerConnection(signer=BasicKeySigner(n_keys),
relay=RELAY)

# output info needed for client to connect to the signer
print(await my_signer.bunker_url)

# wait forever...
while True:
await asyncio.sleep(0.1)

if __name__ == "__main__":
logging.getLogger().setLevel(logging.DEBUG)
asyncio.run(run_signer())
2 changes: 1 addition & 1 deletion examples/post_pow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from monstr.client.client import Client, ClientPool
from monstr.encrypt import Keys
from monstr.event.event import Event
from monstr.signing import BasicKeySigner
from monstr.signing.signing import BasicKeySigner


async def do_post(url, text, target):
Expand Down
2 changes: 0 additions & 2 deletions examples/post_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ async def do_post(url, text):
async with Client(url) as c:
n_msg = await my_signer.ready_post(Event(kind=Event.KIND_TEXT_NOTE,
content=text))
await my_signer.sign_event(n_msg)
c.publish(n_msg)
# await asyncio.sleep(1)

if __name__ == "__main__":
logging.getLogger().setLevel(logging.DEBUG)
Expand Down
2 changes: 1 addition & 1 deletion src/monstr/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__='0.1.7'
__version__='0.1.8'
4 changes: 2 additions & 2 deletions src/monstr/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ class ClientPool:
"""

def __init__(self,
clients: str | Client,
clients: str | Client | list[str | Client],
on_connect: Callable = None,
on_status: Callable = None,
on_eose: Callable = None,
Expand Down Expand Up @@ -785,7 +785,7 @@ def __init__(self,
self._on_status = on_status

# for whatever reason using pool but only a single client handed in
if isinstance(clients, str):
if isinstance(clients, (str, Client)):
clients = [clients]

# ssl if disabled - will be disabled for all clients
Expand Down
1 change: 1 addition & 0 deletions src/monstr/encrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def decrypt_event(self, evt: Event) -> Event:
class DecryptionException(Exception):
pass


class NIP4Encrypt(Encrypter):

def __init__(self, key: Keys | str):
Expand Down
204 changes: 183 additions & 21 deletions src/monstr/signing/nip46.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import asyncio
import json
from json import JSONDecodeError
from urllib.parse import urlparse, parse_qs
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.signing.signing import SignerInterface, BasicKeySigner
from monstr.util import util_funcs


Expand All @@ -26,11 +27,6 @@ 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,
Expand All @@ -40,10 +36,12 @@ def __init__(self,

self._signer = signer
self._comm_k = comm_k

self._relay = relay
self._authoriser = authoriser
if isinstance(relay, str):
self._relay = [relay]

self._run = True
self._authoriser = authoriser

# TODO - make client so that if it gets async for _on_connect, _do_event
# it'll automaticlly create as task
Expand All @@ -59,6 +57,7 @@ async def _aon_connect(my_client:Client):
}
)

self._run = True
self._client = ClientPool(self._relay,
on_connect=_on_connect)

Expand All @@ -67,14 +66,20 @@ async def _aon_connect(my_client:Client):
# start a process to work on the queued events
self._event_process_task = asyncio.create_task(self._my_event_consumer())

# TODO: we should probably have start and end, etc as client
# not just start a task here...
asyncio.create_task(self._client.run())

super().__init__(event_acceptors=[
DeduplicateAcceptor()
])

@property
async def bunker_url(self):
return f'bunker://{await self._signer.get_public_key()}?relay={"&".join(self._relay)}'

async def describe(self):
await self._client.wait_connect()
# await self._client.wait_connect()
sign_k = await self._signer.get_public_key()

content = json.dumps({
Expand Down Expand Up @@ -152,24 +157,13 @@ async def _do_command(self, method: str, params: [str]):
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)
Expand Down Expand Up @@ -297,4 +291,172 @@ async def ado_event(self, the_client: Client, sub_id, evt: Event):

def end(self):
self._run = False
self._client.end()
self._client.end()


class NIP46Comm(EventHandler):

def __init__(self,
client: str | Client | list[str | Client],
for_k: str,
signer: SignerInterface):

self._signer = signer
self._comm_k = None
self._for_k = for_k

# 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())

self._client = ClientPool(client)
asyncio.create_task(self._client.run())
self._run = False

super().__init__(event_acceptors=[
DeduplicateAcceptor()
])

async def connect(self, timeout=5) -> bool:
if self._comm_k is None:
await self.do_command(method='connect',
params=[await self._signer.get_public_key()])

return self._comm_k is not None

async def _get_msg_event(self, content: str, to_k: str) -> Event:
# encrypt the content
content = await self._signer.nip4_encrypt(content,
to_pub_k=to_k)
# make the event
ret = Event(pub_key=await self._signer.get_public_key(),
kind=Event.KIND_NIP46,
content=content,
tags=[
['p', to_k]
]
)
# and sign it
await self._signer.sign_event(ret)

return ret

async def do_command(self, method: str, params: [str]):
id = util_funcs.get_rnd_hex_str(8)

if method == 'connect':
to_k = self._for_k
else:
to_k = self._comm_k
if to_k is not None:
evt = await self._get_msg_event(
content=json.dumps({
'id': id,
'method': method,
'params': params
}),
to_k=to_k
)

self._client.publish(evt)
else:
print('something went wrong... maybe we did not connect yet')

return id

def do_event(self, the_client: Client, sub_id, evt: Event):
pass

async def ado_event(self, the_client: Client, sub_id, evt: Event):
pass

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'NIP46Comm::_my_event_consumer: {e}')

def start(self):
asyncio.create_task(self._client.run())

def end(self):
self._run = False
self._client.end()

class NIP46Signer(SignerInterface):
"""
signer that proxies signing via NIP46 - it never has access to the keys itself
for now expects connection str, todo add where we initialise the connection
"""
def __init__(self, connection: str):
parsed = urlparse(connection)

if parsed.scheme != 'bunker':
raise SignerException(f'NIP46Signer::__init__: unknown connection scheme {parsed.scheme}')

# keys we'll be signing as
for_pub_k = parsed.netloc
if not Keys.is_valid_key(for_pub_k):
raise SignerException(f'NIP46Signer::__init__: bad key {for_pub_k}')
self._for_pub_k = for_pub_k

query_parsed = parse_qs(parsed.query)
if 'relay' not in query_parsed:
raise SignerException(f'NIP46Signer::__init__: relay not supplied')

# probably we should do more checks on the relay urls....
self._relay = query_parsed['relay']

# does the comm between us and the NIP46 server, we use ephemeral keys for signing
self._comm = NIP46Comm(client=self._relay,
for_k=for_pub_k,
signer=BasicKeySigner(Keys()))

async def _get_comm_key(self):
await self._comm.do_command(method='connect',
params=[])

async def _do_method(self, method: str, args: list) -> Event:
logging.debug(f'NIP46Signer::_do_method: {method} - {args}')
# did we already get key to connect on?
await self._comm.connect()

async def get_public_key(self) -> str:
# we know the pub k so won't bother calling signer service for this
return self._for_pub_k

async def sign_event(self, evt: Event):
await self._do_method('sign_event', [json.dumps(evt.data())])

async def echd_key(self, to_pub_k: str) -> str:
pass

async def nip4_encrypt(self, plain_text: str, to_pub_k: str) -> str:
pass

async def nip4_decrypt(self, payload: str, for_pub_k: str) -> str:
pass

async def nip4_encrypt_event(self, evt: Event, to_pub_k: str) -> Event:
pass

async def nip4_decrypt_event(self, evt: Event) -> Event:
pass

async def nip44_encrypt(self, plain_text: str, to_pub_k: str, version=2) -> str:
pass

async def nip44_decrypt(self, payload: str, for_pub_k: str) -> str:
pass

async def nip44_encrypt_event(self, evt: Event, to_pub_k: str) -> Event:
pass

async def nip44_decrypt_event(self, evt: Event) -> Event:
pass

0 comments on commit e8b038d

Please sign in to comment.