nip46: nostrconnect:// preliminary support (client-side).
This commit is contained in:
130
nip46/nostrconnect.go
Normal file
130
nip46/nostrconnect.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package nip46
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip44"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
var NoConnectionReceived = errors.New("relay connections ended without a bunker connection established")
|
||||
|
||||
// GenerateNostrConnectURL generates a nostrconnect:// URL that can be passed to the remote-signer
|
||||
// The remote-signer will then send a connect response to the client-pubkey via the specified relays
|
||||
func GenerateNostrConnectURL(
|
||||
ctx context.Context,
|
||||
clientSecretKey [32]byte,
|
||||
relayURLs []string,
|
||||
permissions []string,
|
||||
clientName, clientURL, clientImage string,
|
||||
) (string, error) {
|
||||
if len(relayURLs) == 0 {
|
||||
return "", fmt.Errorf("at least one relay URL is required")
|
||||
}
|
||||
|
||||
// generate random secret
|
||||
secretBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(secretBytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate secret: %w", err)
|
||||
}
|
||||
secret := fmt.Sprintf("%x", secretBytes)
|
||||
|
||||
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
|
||||
params := url.Values{}
|
||||
for _, relay := range relayURLs {
|
||||
params.Add("relay", relay)
|
||||
}
|
||||
params.Set("secret", secret)
|
||||
|
||||
if len(permissions) > 0 {
|
||||
params.Set("perms", strings.Join(permissions, ","))
|
||||
}
|
||||
if clientName != "" {
|
||||
params.Set("name", clientName)
|
||||
}
|
||||
if clientURL != "" {
|
||||
params.Set("url", clientURL)
|
||||
}
|
||||
if clientImage != "" {
|
||||
params.Set("image", clientImage)
|
||||
}
|
||||
|
||||
connectURL := fmt.Sprintf("nostrconnect://%s?%s", clientPublicKey.Hex(), params.Encode())
|
||||
return connectURL, nil
|
||||
}
|
||||
|
||||
// NewBunkerFromNostrConnect waits for a client to connect through our nostrconnect:// URL and returns a BunkerClient on success
|
||||
func NewBunkerFromNostrConnect(
|
||||
ctx context.Context,
|
||||
clientSecretKey [32]byte,
|
||||
relayURLs []string,
|
||||
secret string,
|
||||
pool *nostr.Pool,
|
||||
) (*BunkerClient, error) {
|
||||
if pool == nil {
|
||||
pool = nostr.NewPool(nostr.PoolOptions{})
|
||||
}
|
||||
|
||||
if len(relayURLs) == 0 {
|
||||
return nil, fmt.Errorf("at least one relay URL is required")
|
||||
}
|
||||
|
||||
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
|
||||
|
||||
// subscribe to events addressed to our client public key
|
||||
for ie := range pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
||||
Tags: nostr.TagMap{"p": []string{clientPublicKey.Hex()}},
|
||||
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
||||
Since: nostr.Now(),
|
||||
LimitZero: true,
|
||||
}, nostr.SubscriptionOptions{Label: "nostrconnect-client"}) {
|
||||
if ie.Kind != nostr.KindNostrConnect {
|
||||
continue
|
||||
}
|
||||
if ie.PubKey.String() == clientPublicKey.Hex() {
|
||||
continue
|
||||
}
|
||||
|
||||
// decrypt and process the connect response
|
||||
targetPublicKey := ie.PubKey
|
||||
conversationKey, err := nip44.GenerateConversationKey(targetPublicKey, clientSecretKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
plain, err := nip44.Decrypt(ie.Content, conversationKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var req Response // contradictorily the thing starts with a response
|
||||
if err := json.Unmarshal([]byte(plain), &req); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if req.Result != "" {
|
||||
if req.Result == secret {
|
||||
// secret validation passed - connection established
|
||||
return &BunkerClient{
|
||||
pool: pool,
|
||||
clientSecretKey: clientSecretKey,
|
||||
target: targetPublicKey,
|
||||
relays: relayURLs,
|
||||
conversationKey: conversationKey,
|
||||
listeners: xsync.NewMapOf[string, chan Response](),
|
||||
onAuth: func(string) {},
|
||||
idPrefix: "nl-" + strconv.Itoa(mrand.Intn(65536)),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, NoConnectionReceived
|
||||
}
|
||||
Reference in New Issue
Block a user