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:
private_key_hex = normalize_private_key(private_key_hex)
# Derive public key from private key
import coincurve
import secp256k1
privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex))
public_key_hex = privkey.public_key_xonly.format().hex()
privkey = secp256k1.PrivateKey(bytes.fromhex(private_key_hex), True)
public_key_hex = privkey.pubkey.serialize()[1:].hex()
settings.nostr_transport_public_key = public_key_hex
relay_urls = settings.nostr_transport_relays

View file

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

View file

@ -1,7 +1,7 @@
"""
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.
Also re-exports sign_event() and verify_event() from lnbits.utils.nostr.
@ -14,7 +14,7 @@ import os
import struct
from math import floor, log2
import coincurve
import secp256k1
from Cryptodome.Cipher import ChaCha20
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.
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)
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)
# coincurve's ecdh returns sha256(compressed_point) by default,
# but NIP-44 needs the unhashed x coordinate.
shared_point = pubkey.multiply(privkey.secret)
shared_x = shared_point.format(compressed=True)[1:] # strip 0x02/0x03 prefix
# ECDH: multiply pubkey by privkey scalar, get shared point
# tweak_mul multiplies the point by a scalar, returning a new PublicKey
shared_point = pubkey.tweak_mul(bytes.fromhex(private_key_hex))
# serialize(compressed=True) gives 0x02/0x03 + 32-byte x coordinate
# strip the prefix byte to get the raw x coordinate
shared_x = shared_point.serialize()[1:]
# HKDF-extract with salt="nip44-v2"
return _hkdf_extract(salt=_NIP44_SALT, ikm=shared_x)

View file

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