feat: nostr transport layer for HTTP-free API access #3

Open
padreug wants to merge 2 commits from nostr-native-transport-forgejo into dev
4 changed files with 19 additions and 18 deletions
Showing only changes of commit bae881587c - Show all commits

View file

@ -51,10 +51,10 @@ async def start_nostr_transport():
else: else:
private_key_hex = normalize_private_key(private_key_hex) private_key_hex = normalize_private_key(private_key_hex)
# Derive public key from private key # Derive public key from private key
import coincurve import secp256k1
privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) privkey = secp256k1.PrivateKey(bytes.fromhex(private_key_hex), True)
public_key_hex = privkey.public_key_xonly.format().hex() public_key_hex = privkey.pubkey.serialize()[1:].hex()
settings.nostr_transport_public_key = public_key_hex settings.nostr_transport_public_key = public_key_hex
relay_urls = settings.nostr_transport_relays relay_urls = settings.nostr_transport_relays

View file

@ -59,8 +59,9 @@ async def resolve_nostr_auth(
extra=UserExtra(provider="nostr"), extra=UserExtra(provider="nostr"),
) )
if not account.activated: # TODO: upstream LNbits (dev) has account.activated field
raise PermissionError("Account is not activated.") # if not account.activated:
# raise PermissionError("Account is not activated.")
# Account-level operation (no wallet specified) # Account-level operation (no wallet specified)
if wallet_id is None: if wallet_id is None:

View file

@ -1,7 +1,7 @@
""" """
NIP-44 v2 encryption/decryption for nostr transport. NIP-44 v2 encryption/decryption for nostr transport.
Implements the NIP-44 v2 spec using coincurve (secp256k1 ECDH) and Implements the NIP-44 v2 spec using secp256k1 (ECDH) and
pycryptodome (ChaCha20, HMAC-SHA256, HKDF), all already in LNbits' deps. pycryptodome (ChaCha20, HMAC-SHA256, HKDF), all already in LNbits' deps.
Also re-exports sign_event() and verify_event() from lnbits.utils.nostr. Also re-exports sign_event() and verify_event() from lnbits.utils.nostr.
@ -14,7 +14,7 @@ import os
import struct import struct
from math import floor, log2 from math import floor, log2
import coincurve import secp256k1
from Cryptodome.Cipher import ChaCha20 from Cryptodome.Cipher import ChaCha20
from lnbits.utils.nostr import sign_event, verify_event # noqa: F401 from lnbits.utils.nostr import sign_event, verify_event # noqa: F401
@ -30,18 +30,18 @@ def get_conversation_key(
Compute NIP-44 v2 conversation key via ECDH + HKDF-extract. Compute NIP-44 v2 conversation key via ECDH + HKDF-extract.
The public key must be 32-byte x-only (as in nostr). We prepend 0x02 The public key must be 32-byte x-only (as in nostr). We prepend 0x02
to make it a valid compressed SEC1 point for coincurve. to make it a valid compressed SEC1 point for secp256k1.
""" """
privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex))
# x-only pubkey -> compressed pubkey (assume even y) # x-only pubkey -> compressed pubkey (assume even y)
pubkey_bytes = b"\x02" + bytes.fromhex(public_key_hex) pubkey_bytes = b"\x02" + bytes.fromhex(public_key_hex)
pubkey = coincurve.PublicKey(pubkey_bytes) pubkey = secp256k1.PublicKey(pubkey_bytes, True)
# ECDH: multiply pubkey by privkey, get raw x coordinate (32 bytes) # ECDH: multiply pubkey by privkey scalar, get shared point
# coincurve's ecdh returns sha256(compressed_point) by default, # tweak_mul multiplies the point by a scalar, returning a new PublicKey
# but NIP-44 needs the unhashed x coordinate. shared_point = pubkey.tweak_mul(bytes.fromhex(private_key_hex))
shared_point = pubkey.multiply(privkey.secret) # serialize(compressed=True) gives 0x02/0x03 + 32-byte x coordinate
shared_x = shared_point.format(compressed=True)[1:] # strip 0x02/0x03 prefix # strip the prefix byte to get the raw x coordinate
shared_x = shared_point.serialize()[1:]
# HKDF-extract with salt="nip44-v2" # HKDF-extract with salt="nip44-v2"
return _hkdf_extract(salt=_NIP44_SALT, ikm=shared_x) return _hkdf_extract(salt=_NIP44_SALT, ikm=shared_x)

View file

@ -14,7 +14,7 @@ import json
import time import time
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import coincurve import secp256k1
from loguru import logger from loguru import logger
from websockets import connect as ws_connect from websockets import connect as ws_connect
@ -49,8 +49,8 @@ class NostrTransportPool:
self.relay_urls = relay_urls self.relay_urls = relay_urls
self.event_callback = event_callback self.event_callback = event_callback
# coincurve key objects for signing # secp256k1 key objects for signing
self._privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) self._privkey = secp256k1.PrivateKey(bytes.fromhex(private_key_hex), True)
# Deduplication: event_id -> timestamp # Deduplication: event_id -> timestamp
self._handled_events: dict[str, float] = {} self._handled_events: dict[str, float] = {}