diff --git a/examples/nip46_client_post.py b/examples/nip46_client_post.py index cd530f3..8967103 100644 --- a/examples/nip46_client_post.py +++ b/examples/nip46_client_post.py @@ -5,7 +5,7 @@ from monstr.event.event import Event # url to relay used for talking to the signer -RELAY = 'ws://localhost:8080' +RELAY = 'ws://localhost:8081' async def do_post(url, text): @@ -22,10 +22,27 @@ async def do_post(url, text): # from here it's just a signer interface same as if we were using BasicKeySigner async with Client(url) as c: + # plain text n_msg = await my_signer.ready_post(Event(kind=Event.KIND_TEXT_NOTE, content=text)) c.publish(n_msg) + # nip4 encrypted to our self + enc_event = Event(kind=Event.KIND_TEXT_NOTE, + content=text+' - encrypted nip4') + enc_event = await my_signer.nip4_encrypt_event(enc_event, + to_pub_k=await my_signer.get_public_key()) + await my_signer.sign_event(enc_event) + c.publish(enc_event) + + # nip44 encrypted to our self + enc_event = Event(kind=Event.KIND_TEXT_NOTE, + content=text + ' - encrypted nip44') + enc_event = await my_signer.nip44_encrypt_event(enc_event, + to_pub_k=await my_signer.get_public_key()) + await my_signer.sign_event(enc_event) + c.publish(enc_event) + if __name__ == "__main__": logging.getLogger().setLevel(logging.DEBUG) diff --git a/examples/nip46_signer_service.py b/examples/nip46_signer_service.py index bd4c4e9..8b73a45 100644 --- a/examples/nip46_signer_service.py +++ b/examples/nip46_signer_service.py @@ -5,7 +5,7 @@ from monstr.signing.signing import BasicKeySigner # url to relay used for talking to the signer -RELAY = 'ws://localhost:8080' +RELAY = 'ws://localhost:8081' async def run_signer(): diff --git a/src/monstr/encrypt.py b/src/monstr/encrypt.py index eef7dff..cc10d0e 100644 --- a/src/monstr/encrypt.py +++ b/src/monstr/encrypt.py @@ -213,25 +213,30 @@ def __str__(self): return '\n'.join(ret) -class Encrypter(ABC): +class DecryptionException(Exception): + pass - def __init__(self, key: Keys | str): - if isinstance(key, str): - key = Keys(priv_k=key) - if key.private_key_hex() is None: - raise ValueError(f'{self.__class__.__name__}::__init__ a key that can sign is required') - self._key = key - self._priv_k = secp256k1.PrivateKey(bytes.fromhex(self._key.private_key_hex())) +class Encrypter(ABC): - @abstractmethod def encrypt(self, plain_text: str, to_pub_k: str) -> str: raise NotImplementedError - @abstractmethod def decrypt(self, payload: str, for_pub_k: str) -> str: raise NotImplementedError + def public_key_hex(self) -> str: + raise NotImplementedError + + async def aencrypt(self, plain_text: str, to_pub_k: str) -> str: + raise NotImplementedError + + async def adecrypt(self, payload: str, for_pub_k: str) -> str: + raise NotImplementedError + + async def apublic_key_hex(self) -> str: + raise NotImplementedError + """ util methods for basic nostr messaging if encrypt and decrypt functions have been declared (for basic 2 way event mesasge i.e use the p_tags) @@ -239,27 +244,44 @@ def decrypt(self, payload: str, for_pub_k: str) -> str: encrypt_event, content encrypted and p_tags set (append your own p_tags after) decrypt_event, content decrypted based on the p_tags """ - def encrypt_event(self, evt: Event, to_pub_k: str | Keys) -> Event: - if isinstance(to_pub_k, Keys): - to_pub_k = to_pub_k.public_key_hex() - elif not Keys.is_valid_key(to_pub_k): - raise ValueError(f'{self.__class__.__name__}::encrypt_event invalid to_pub_k - {to_pub_k}') - ret = Event.load(evt.data()) + def _get_to_pub_key_hex(self, to_pub_k: str) -> str: + ret = to_pub_k + if isinstance(ret, Keys): + ret = to_pub_k.public_key_hex() + elif not Keys.is_valid_key(ret): + raise ValueError(f'{self.__class__.__name__}::encrypt_event invalid to_pub_k - {ret}') + return ret + + def _make_encrypt_event(self, src_event: Event, to_k: str) -> Event: + to_k_hex = self._get_to_pub_key_hex(to_k) + + # copy the event + ret = Event.load(src_event.data()) + ret.tags = [['p', to_k_hex]] + return ret + def encrypt_event(self, evt: Event, to_pub_k: str | Keys) -> Event: + ret = self._make_encrypt_event(evt, to_pub_k) # the pub_k author must be us - ret.pub_key = self._key.public_key_hex() + ret.pub_key = self.public_key_hex() # change content to cipher_text - ret.content = self.encrypt(plain_text=evt.content, - to_pub_k=to_pub_k) - - ret.tags = [['p', to_pub_k]] + ret.content = self.encrypt(plain_text=ret.content, + to_pub_k=ret.tags.get_tag_value_pos('p')) + return ret + async def aencrypt_event(self, evt: Event, to_pub_k: str | Keys) -> Event: + ret = self._make_encrypt_event(evt, to_pub_k) + # the pub_k author must be us + ret.pub_key = await self.apublic_key_hex() + # change content to cipher_text + ret.content = await self.aencrypt(plain_text=ret.content, + to_pub_k=ret.tags.get_tag_value_pos('p')) return ret def decrypt_event(self, evt: Event) -> Event: pub_k = evt.pub_key - if pub_k == self._key.public_key_hex(): + if pub_k == self.public_key_hex(): pub_k = evt.p_tags[0] ret = Event.load(evt.data()) @@ -267,12 +289,35 @@ def decrypt_event(self, evt: Event) -> Event: for_pub_k=pub_k) return ret + async def adecrypt_event(self, evt: Event) -> Event: + pub_k = evt.pub_key + if pub_k == await self.apublic_key_hex(): + pub_k = evt.p_tags[0] + + # always a copy + ret = Event.load(evt.data()) -class DecryptionException(Exception): - pass + ret.content = await self.adecrypt(payload=evt.content, + for_pub_k=pub_k) + return ret + + +class KeyEncrypter(Encrypter): + + def __init__(self, key: Keys | str): + if isinstance(key, str): + key = Keys(priv_k=key) + if key.private_key_hex() is None: + raise ValueError(f'{self.__class__.__name__}::__init__ a key that can sign is required') + + self._key = key + self._priv_k = secp256k1.PrivateKey(bytes.fromhex(self._key.private_key_hex())) + + def public_key_hex(self) -> str: + return self._key.public_key_hex() -class NIP4Encrypt(Encrypter): +class NIP4Encrypt(KeyEncrypter): def __init__(self, key: Keys | str): super().__init__(key) @@ -342,7 +387,7 @@ def decrypt(self, payload: str, for_pub_k: str) -> str: for_pub_k=for_pub_k).decode('utf8') -class NIP44Encrypt(Encrypter): +class NIP44Encrypt(KeyEncrypter): """ base functionality for implementing NIP44 https://github.com/paulmillr/nip44 diff --git a/src/monstr/event/event.py b/src/monstr/event/event.py index 1394791..3765e41 100644 --- a/src/monstr/event/event.py +++ b/src/monstr/event/event.py @@ -560,6 +560,10 @@ def id(self): self._get_id() return self._id + @id.setter + def id(self, id): + self._id = id + @property def short_id(self): # shorter version of id for display, note id doesn't until signing @@ -605,6 +609,10 @@ def content(self, content): def sig(self): return self._sig + @sig.setter + def sig(self, sig): + self._sig = sig + def __str__(self): ret = super(Event, self).__str__() # hopefully id is set but it might not be if the event is being prepeped diff --git a/src/monstr/signing/nip46.py b/src/monstr/signing/nip46.py index 3b31a2f..0902306 100644 --- a/src/monstr/signing/nip46.py +++ b/src/monstr/signing/nip46.py @@ -5,13 +5,14 @@ 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.encrypt import Keys, Encrypter from monstr.event.event import Event from monstr.client.event_handlers import EventHandler, DeduplicateAcceptor from monstr.signing.signing import SignerInterface, BasicKeySigner from monstr.util import util_funcs + class SignerException(Exception): pass @@ -22,44 +23,43 @@ async def authorise(self, method: str, id: str, params: [str]) -> bool: 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. - """ +class NIP46Comm(EventHandler, ABC): + def __init__(self, - signer: SignerInterface, relay: [str], - comm_k: str = None, - authoriser: NIP46AuthoriseInterface = None): - - self._signer = signer - self._comm_k = comm_k - + on_command: callable = None, + on_response: callable = None, + comm_signer: SignerInterface = None): + + # I don't see why this needs to be the same key as we're signer for, but atleast with + # nostrudel as far as I can tell it does to get it to work.. else you end up signing in# + # as the comm key... + self._comm_signer = comm_signer + if comm_signer is None: + self._comm_signer = BasicKeySigner(Keys()) + + # relays we'll attach too self._relay = relay if isinstance(relay, str): self._relay = [relay] - self._authoriser = authoriser - - # 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): + # make client obj that will actually do the comm + async def aconnect(my_client: Client): + # sub any NIP46 events to our pub_k my_client.subscribe( handlers=[self], filters={ - '#p': [await self._signer.get_public_key()], + '#p': [await self._comm_signer.get_public_key()], 'kinds': [Event.KIND_NIP46] } ) + def on_connect(my_client: Client): + asyncio.create_task(aconnect(my_client)) + self._run = True self._client = ClientPool(self._relay, - on_connect=_on_connect) + on_connect=on_connect) # events queued and dealt with serially as they come in self._event_q: asyncio.Queue = asyncio.Queue() @@ -70,67 +70,91 @@ async def _aon_connect(my_client:Client): # not just start a task here... asyncio.create_task(self._client.run()) + # called when we see method events - most likely when we're acting as signer + self._on_command = on_command + + # called when we see response events - most likely when we're signer client + self._on_response = on_response + 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() - 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] - ] - ) + return f'bunker://{await self._comm_signer.get_public_key()}?relay={"&".join(self._relay)}' - await self._signer.sign_event(con_event) - - self._client.publish(con_event) + @property + async def bunker_key(self): + return await self._comm_signer.get_public_key() - async def _get_msg_event(self, content: str) -> Event: + async def _get_msg_event(self, content: str, to_k: str) -> Event: # returns encrypted and signed method for content - + print(content) # encrypt the content - content = await self._signer.nip4_encrypt(content, - to_pub_k=self._comm_k) + content = await self._comm_signer.nip4_encrypt(content, to_pub_k=to_k) + # make the event - ret = Event(pub_key=await self._signer.get_public_key(), + ret = Event(pub_key=await self._comm_signer.get_public_key(), kind=Event.KIND_NIP46, content=content, tags=[ - ['p', self._comm_k] + ['p', to_k] ] ) # and sign it - await self._signer.sign_event(ret) + await self._comm_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 = '' + async def _my_event_consumer(self): + # listen to event to us and call back the handler as we get commands + 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 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): + + # pull of events that were put on the queue bu do_event and deal with them + decrypted_evt = await self._comm_signer.nip4_decrypt_event(evt) + + try: + cmd_dict = json.loads(decrypted_evt.content) + if 'method' in cmd_dict and self._on_command: + id = cmd_dict['id'] + method = cmd_dict['method'] + params = cmd_dict['params'] + + result, err = await self._on_command(id, method, params, evt) + to_k = evt.pub_key + await self._do_response(result, err, to_k, id) + elif 'result' in cmd_dict and self._on_response: + await self._on_response(cmd_dict) + else: + logging.debug(f'NIP46Comm::ado_event - ignored event with contents - {cmd_dict}') + + except Exception as e: + logging.debug(f'NIP46Comm::ado_event {e}') + + async def _do_response(self, + result: str, + error: str, + to_k: str, + id: str = None): if id is None: id = util_funcs.get_rnd_hex_str(8) @@ -138,254 +162,243 @@ async def _do_response(self, result: str = None, error: str = None, id: str = No 'id': id, 'result': result, 'error': error - })) + }), to_k) self._client.publish(evt) - async def _do_command(self, method: str, params: [str]): - id = util_funcs.get_rnd_hex_str(8) + async def do_request(self, method: str, params: [str], to_k, id: str = None): + if id is None: + id = util_funcs.get_rnd_hex_str(8) - evt = await self._get_msg_event(json.dumps({ + content = json.dumps({ 'id': id, 'method': method, 'params': params - })) + }) + evt = await self._get_msg_event( + content=content, + to_k=to_k + ) self._client.publish(evt) + return id - async def request_connect(self): - await self._do_command('connect', [await self._signer.get_public_key()]) - async def connect(self, id: str, params: [str]): - if self._comm_k is None: - self._comm_k = params[0] + def end(self): + self._run = False + self._client.end() - await self._do_response(result=await self._signer.get_public_key(), - 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) +class NIP46ServerConnection: + """ + 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. + """ + def __init__(self, + signer: SignerInterface, + relay: [str], + authoriser: NIP46AuthoriseInterface = None): + + # this is the key we'll sign as + self._signer = signer + + # this is the chanel we sign over + self._comm = NIP46Comm(relay=relay, + comm_signer=signer, + on_command=self._do_command) - async def nip04_encrypt(self, id: str, params: [str]): + # called before we do any method + self._authoriser = authoriser + + # all keys that are connected + self._connections = set() + + @property + async def bunker_url(self): + # this will be different every time we're run + return await self._comm.bunker_url + + def _connected(self, connection_key: str) -> bool: + return connection_key in self._connections + + def _check_con(self, evt: Event, method: str): + connect_key = evt.pub_key + if not self._connected(connect_key): + raise SignerException(f'NIP46ServerConnection::{method}: not connected! {connect_key}') + + async def describe(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + self._check_con(evt, 'describe') + + return json.dumps(['describe', + 'get_public_key', + 'sign_event', + 'nip04_encrypt', + 'nip04_decrypt', + 'connect'] + ), '' + + # async def request_connect(self): + # await self._do_command('connect', [await self._signer.get_public_key()]) + + async def connect(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + if not params: + raise SignerException('NIP46ServerConnection::connect: connection key is required') + connect_key = params[0] + if not Keys.is_valid_key(connect_key): + raise SignerException(f'NIP46ServerConnection::connect: invalid key {connect_key}') + if self._connected(connect_key): + raise SignerException(f'NIP46ServerConnection::connect: already connected! {connect_key}') + + self._connections.add(connect_key) + return await self._signer.get_public_key(), '' + + async def get_public_key(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + self._check_con(evt, 'get_public_key') + return await self._signer.get_public_key(), '' + + async def _do_encrypt(self, id: str, params: [str], evt: Event, enc_name: str, enc_func: callable) -> tuple[str, str]: + self._check_con(evt, enc_name) 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') + f'Signer::{enc_name}: requires 2 params got {n_params} - to key and plain 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}') + f'Signer::{enc_name}: to key is not valid - {to_k}') - ciper_text = await self._signer.nip4_encrypt(plain_text=plain_text, - to_pub_k=to_k) + ciper_text = await enc_func(plain_text=plain_text, + to_pub_k=to_k) - await self._do_response(result=ciper_text, - id=id) + return ciper_text, '' except SignerException as se: - await self._do_response(error=str(se), - id=id) + return '', str(se) + 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) + return '', (f'Signer::{enc_name}: unable to encrypt as ' + f'{await self._signer.get_public_key()} ' + f'error - {str(e)}') - async def nip04_decrypt(self, id: str, params: [str]): + async def _do_decrypt(self, id: str, params: [str], evt: Event, dec_name: str, dec_func: callable) -> tuple[str, str]: + self._check_con(evt, dec_name) 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') + f'Signer::{dec_name}: 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}') + f'Signer::{dec_name}: from key is not valid - {from_k}') - plain_text = await self._signer.nip4_decrypt(payload=payload, - for_pub_k=from_k) + plain_text = await dec_func(payload=payload, + for_pub_k=from_k) - await self._do_response(result=plain_text, - id=id) + return plain_text, '' except SignerException as se: - await self._do_response(error=str(se), - id=id) + return '', str(se) 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) + return '', (f'Signer::{dec_name}: unable to decrypt as ' + f'{await self._signer.get_public_key()} ' + f'error - {str(e)}') - async def sign_event(self, id: str, params: [str]): + async def nip04_encrypt(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + return await self._do_encrypt(id, params, evt, 'nip04_encrypt', self._signer.nip4_encrypt) + + async def nip04_decrypt(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + return await self._do_decrypt(id, params, evt, 'nip04_decrypt', self._signer.nip4_decrypt) + + async def nip44_encrypt(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + return await self._do_encrypt(id, params, evt, 'nip44_encrypt', self._signer.nip44_encrypt) + + async def nip44_decrypt(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + return await self._do_decrypt(id, params, evt, 'nip44_decrypt', self._signer.nip44_decrypt) + + async def sign_event(self, id: str, params: [str], evt: Event) -> tuple[str, str]: + self._check_con(evt, 'sign_event') 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) + return json.dumps(event.data()), '' except JSONDecodeError as je: - await self._do_response(error=f'Signer::sign_event: bad event JSON - {je}', - id=id) + return '', f'Signer::sign_event: bad event JSON - {je}' 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) + return '', f'Signer::sign_event: {e}' + async def _do_command(self, id: str, method: str, params: [str], evt: Event) -> tuple[str, str]: 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'}: - logging.info(f'{method} - {params}') - - if self._authoriser: - if await self._authoriser.authorise(method, id, params): - await getattr(self, method)(id, params) - else: - await self._do_response(error='request not authorised!', - id=id) - else: - await getattr(self, method)(id, params) + if method in {'connect', + 'describe', + 'get_public_key', + 'nip04_decrypt', + 'nip04_encrypt', + 'nip44_encrypt', + 'nip44_decrypt', + 'sign_event'}: + logging.info(f'{method} - {params}') + + authorised = True + if self._authoriser: + authorised = await self._authoriser.authorise(method, id, params) + + if authorised: + ret = await getattr(self, method)(id, params, evt) + else: + ret = '', 'request not authorised!' + else: + ret = '', f'method not implemented! - {method}' except Exception as e: - logging.debug(f'SignerConnection::ado_event {e}') + logging.debug(f'SignerConnection::do_command {e}') + ret = '', str(e) - def end(self): - self._run = False - self._client.end() + return ret + def end(self): + self._comm.end() -class NIP46Comm(EventHandler): - def __init__(self, - client: str | Client | list[str | Client], - for_k: str, - signer: SignerInterface): +class NIP4SignerEncrypter(Encrypter): + def __init__(self, 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 apublic_key_hex(self) -> str: + return await self._signer.get_public_key() - async def do_command(self, method: str, params: [str]): - id = util_funcs.get_rnd_hex_str(8) + async def aencrypt(self, plain_text: str, to_pub_k: str) -> str: + return await self._signer.nip4_encrypt(plain_text, to_pub_k) - 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 - ) + async def adecrypt(self, payload: str, for_pub_k: str) -> str: + return await self._signer.nip4_decrypt(payload, for_pub_k) - self._client.publish(evt) - else: - print('something went wrong... maybe we did not connect yet') - return id +class NIP44SignerEncrypter(Encrypter): - def do_event(self, the_client: Client, sub_id, evt: Event): - pass + def __init__(self, signer: SignerInterface): + self._signer = signer - async def ado_event(self, the_client: Client, sub_id, evt: Event): - pass + async def apublic_key_hex(self) -> str: + return await self._signer.get_public_key() - async def _my_event_consumer(self): - while self._run: - try: - args = await self._event_q.get() - await self.ado_event(*args) + async def aencrypt(self, plain_text: str, to_pub_k: str) -> str: + return await self._signer.nip44_encrypt(plain_text, to_pub_k) - except Exception as e: - logging.debug(f'NIP46Comm::_my_event_consumer: {e}') - - def start(self): - asyncio.create_task(self._client.run()) + async def adecrypt(self, payload: str, for_pub_k: str) -> str: + return await self._signer.nip44_decrypt(payload, for_pub_k) - def end(self): - self._run = False - self._client.end() class NIP46Signer(SignerInterface): """ @@ -399,11 +412,14 @@ def __init__(self, connection: str): 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 + # this is the intial pk we send to to talk with the remote signer + signer_k = parsed.netloc + if not Keys.is_valid_key(signer_k): + raise SignerException(f'NIP46Signer::__init__: bad key {signer_k}') + self._signer_k = signer_k + + # this is the pub k we'll be signer as, it may be the same as signer_k + self._sign_as_k = None query_parsed = parse_qs(parsed.query) if 'relay' not in query_parsed: @@ -413,50 +429,104 @@ def __init__(self, connection: str): 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())) + self._comm = NIP46Comm(relay=self._relay, + on_response=self._do_response) + + # repsonses we waiting for key'd on id + self._responses = {} + + # max time we'll wait before failing a response + self._time_out = 5 - async def _get_comm_key(self): - await self._comm.do_command(method='connect', - params=[]) + async def _do_response(self, result: dict): + try: + self._responses[result['id']] = result + except Exception as e: + logging.debug(f'NIP46Signer::_do_response {e}') + + async def _wait_response(self, id:str, time_out: int = None): + if time_out is None: + time_out = self._time_out + + wait_time = 0.0 + while id not in self._responses and int(wait_time) < time_out: + await asyncio.sleep(0.1) + wait_time += 0.1 + + if id in self._responses: + response = self._responses[id] + if response['error']: + raise SignerException(f'NIP46Signer::_wait_response {response["error"]}') + else: + return response['result'] + else: + raise SignerException(f'NIP46Signer::_wait_response - time out waiting for response to {id}') - async def _do_method(self, method: str, args: list) -> Event: + async def _do_method(self, method: str, args: list) -> str: logging.debug(f'NIP46Signer::_do_method: {method} - {args}') + + to_k = self._sign_as_k + if method == 'connect' or to_k is None: + to_k = self._signer_k + # did we already get key to connect on? - await self._comm.connect() + return await self._comm.do_request(method=method, + params=args, + to_k=to_k) + + async def _get_connect(self): + if self._sign_as_k is None: + id = await self._do_method('connect', [await self._comm.bunker_key]) + result = await self._wait_response(id) + self._sign_as_k = result 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 + await self._get_connect() + return self._sign_as_k async def sign_event(self, evt: Event): - await self._do_method('sign_event', [json.dumps(evt.data())]) + await self._get_connect() + id = await self._do_method('sign_event', [json.dumps(evt.data())]) + sign_evt = Event.load(await self._wait_response(id)) + evt.id = sign_evt.id + evt.sig = sign_evt.sig 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 + await self._get_connect() + id = await self._do_method('nip04_encrypt', [to_pub_k, plain_text]) + return await self._wait_response(id) async def nip4_decrypt(self, payload: str, for_pub_k: str) -> str: - pass + await self._get_connect() + id = await self._do_method('nip04_decrypt', [for_pub_k, payload]) + return await self._wait_response(id) async def nip4_encrypt_event(self, evt: Event, to_pub_k: str) -> Event: - pass + self._enc = NIP4SignerEncrypter(self) + return await self._enc.aencrypt_event(evt, to_pub_k) async def nip4_decrypt_event(self, evt: Event) -> Event: - pass + self._enc = NIP4SignerEncrypter(self) + return await self._enc.adecrypt_event(evt) async def nip44_encrypt(self, plain_text: str, to_pub_k: str, version=2) -> str: - pass + await self._get_connect() + id = await self._do_method('nip44_encrypt', [to_pub_k, plain_text]) + return await self._wait_response(id) async def nip44_decrypt(self, payload: str, for_pub_k: str) -> str: - pass + await self._get_connect() + id = await self._do_method('nip44_decrypt', [for_pub_k, payload]) + return await self._wait_response(id) async def nip44_encrypt_event(self, evt: Event, to_pub_k: str) -> Event: - pass + self._enc = NIP44SignerEncrypter(self) + return await self._enc.aencrypt_event(evt, to_pub_k) async def nip44_decrypt_event(self, evt: Event) -> Event: - pass + self._enc = NIP44SignerEncrypter(self) + return await self._enc.adecrypt_event(evt) diff --git a/src/monstr/signing/signing.py b/src/monstr/signing/signing.py index 450d4e0..1dd1f7a 100644 --- a/src/monstr/signing/signing.py +++ b/src/monstr/signing/signing.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod -from monstr.event.event import Event, EventTags +from monstr.event.event import Event from monstr.encrypt import Keys -from monstr.encrypt import NIP44Encrypt, NIP4Encrypt, Encrypter +from monstr.encrypt import NIP44Encrypt, NIP4Encrypt class SignerInterface(ABC): @@ -71,44 +71,6 @@ def __init__(self, key: Keys): self._nip4_encrypt = NIP4Encrypt(key=self._keys) self._nip44_encrypt = NIP44Encrypt(key=self._keys) - """ - util methods for basic nostr messaging if encrypt and decrypt functions have been declared - (for basic 2 way event mesasge i.e use the p_tags) - both return new event: - encrypt_event, content encrypted and p_tags set (append your own p_tags after) - decrypt_event, content decrypted based on the p_tags - """ - def _encrypt_event(self, evt: Event, to_pub_k: str | Keys, enc: Encrypter) -> Event: - if isinstance(to_pub_k, Keys): - to_pub_k = to_pub_k.public_key_hex() - elif not Keys.is_valid_key(to_pub_k): - raise ValueError(f'{self.__class__.__name__}::encrypt_event invalid to_pub_k - {to_pub_k}') - - ret = Event.load(evt.data()) - - # the pub_k author must be us - ret.pub_key = self._keys.public_key_hex() - # change content to cipher_text - ret.content = enc.encrypt(plain_text=evt.content, - to_pub_k=to_pub_k) - - ret.tags = EventTags([ - ['p', to_pub_k] - ]) - - return ret - - def _decrypt_event(self, evt: Event, enc: Encrypter) -> Event: - pub_k = evt.pub_key - if pub_k == self._keys.public_key_hex(): - pub_k = evt.p_tags[0] - - ret = Event.load(evt.data()) - ret.content = enc.decrypt(payload=evt.content, - for_pub_k=pub_k) - return ret - - async def get_public_key(self) -> str: return self._keys.public_key_hex() diff --git a/tests/nip44.py b/tests/nip44.py index 3c30267..4cfa0bc 100644 --- a/tests/nip44.py +++ b/tests/nip44.py @@ -2,7 +2,7 @@ import os from monstr.encrypt import Keys, NIP44Encrypt from monstr.util import util_funcs -from monstr.signing import SignerInterface, BasicKeySigner +from monstr.signing.signing import SignerInterface, BasicKeySigner from monstr.event.event import Event # from tests.nip4 import TEST_TEXTS diff --git a/tests/nip59.py b/tests/nip59.py index f2578e1..6c69958 100644 --- a/tests/nip59.py +++ b/tests/nip59.py @@ -2,7 +2,7 @@ import json from monstr.giftwrap import GiftWrap from monstr.encrypt import Keys -from monstr.signing import BasicKeySigner +from monstr.signing.signing import BasicKeySigner from monstr.event.event import Event async def test_encrypt_decrypt():