2024-02-06 00:45:36 -03:00
|
|
|
package nip46
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2024-02-11 11:07:01 -03:00
|
|
|
"math/rand"
|
2024-02-06 00:45:36 -03:00
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
|
|
"github.com/mailru/easyjson"
|
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
|
|
"github.com/nbd-wtf/go-nostr/nip04"
|
2024-10-26 19:37:36 -03:00
|
|
|
"github.com/nbd-wtf/go-nostr/nip44"
|
2024-02-06 00:45:36 -03:00
|
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
type BunkerClient struct {
|
|
|
|
serial atomic.Uint64
|
|
|
|
clientSecretKey string
|
|
|
|
pool *nostr.SimplePool
|
|
|
|
target string
|
|
|
|
relays []string
|
2024-10-26 19:37:36 -03:00
|
|
|
conversationKey [32]byte // nip44
|
2024-02-06 00:45:36 -03:00
|
|
|
listeners *xsync.MapOf[string, chan Response]
|
2024-02-29 20:37:16 -03:00
|
|
|
expectingAuth *xsync.MapOf[string, struct{}]
|
2024-02-11 11:07:01 -03:00
|
|
|
idPrefix string
|
2024-02-29 20:37:16 -03:00
|
|
|
onAuth func(string)
|
2024-02-06 00:45:36 -03:00
|
|
|
|
|
|
|
// memoized
|
|
|
|
getPublicKeyResponse string
|
2024-11-12 15:48:49 -03:00
|
|
|
|
|
|
|
// SkipSignatureCheck can be set if you don't want to double-check incoming signatures
|
|
|
|
SkipSignatureCheck bool
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
// ConnectBunker establishes an RPC connection to a NIP-46 signer using the relays and secret provided in the bunkerURL.
|
|
|
|
// pool can be passed to reuse an existing pool, otherwise a new pool will be created.
|
|
|
|
func ConnectBunker(
|
|
|
|
ctx context.Context,
|
|
|
|
clientSecretKey string,
|
2024-02-29 16:28:46 -03:00
|
|
|
bunkerURLOrNIP05 string,
|
2024-02-06 00:45:36 -03:00
|
|
|
pool *nostr.SimplePool,
|
2024-02-29 20:37:16 -03:00
|
|
|
onAuth func(string),
|
2024-02-06 00:45:36 -03:00
|
|
|
) (*BunkerClient, error) {
|
2024-02-29 16:28:46 -03:00
|
|
|
parsed, err := url.Parse(bunkerURLOrNIP05)
|
2024-02-06 00:45:36 -03:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid url: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-02-29 16:28:46 -03:00
|
|
|
// assume it's a bunker url (will fail later if not)
|
|
|
|
secret := parsed.Query().Get("secret")
|
|
|
|
relays := parsed.Query()["relay"]
|
2024-02-29 20:29:08 -03:00
|
|
|
targetPublicKey := parsed.Host
|
2024-02-29 16:28:46 -03:00
|
|
|
|
|
|
|
if parsed.Scheme == "" {
|
|
|
|
// could be a NIP-05
|
|
|
|
pubkey, relays_, err := queryWellKnownNostrJson(ctx, bunkerURLOrNIP05)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to query nip05: %w", err)
|
|
|
|
}
|
2024-02-29 20:29:08 -03:00
|
|
|
targetPublicKey = pubkey
|
2024-02-29 16:28:46 -03:00
|
|
|
relays = relays_
|
|
|
|
} else if parsed.Scheme == "bunker" {
|
|
|
|
// this is what we were expecting, so just move on
|
|
|
|
} else {
|
|
|
|
// otherwise fail here
|
2024-02-06 00:45:36 -03:00
|
|
|
return nil, fmt.Errorf("wrong scheme '%s', must be bunker://", parsed.Scheme)
|
|
|
|
}
|
2024-02-29 20:29:08 -03:00
|
|
|
if !nostr.IsValidPublicKey(targetPublicKey) {
|
|
|
|
return nil, fmt.Errorf("'%s' is not a valid public key hex", targetPublicKey)
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
|
2024-02-29 20:29:08 -03:00
|
|
|
bunker := NewBunker(
|
|
|
|
ctx,
|
|
|
|
clientSecretKey,
|
|
|
|
targetPublicKey,
|
|
|
|
relays,
|
|
|
|
pool,
|
2024-02-29 20:37:16 -03:00
|
|
|
onAuth,
|
2024-02-29 20:29:08 -03:00
|
|
|
)
|
2024-03-04 09:30:45 -03:00
|
|
|
_, err = bunker.RPC(ctx, "connect", []string{targetPublicKey, secret})
|
2024-02-29 20:29:08 -03:00
|
|
|
return bunker, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewBunker(
|
|
|
|
ctx context.Context,
|
|
|
|
clientSecretKey string,
|
|
|
|
targetPublicKey string,
|
|
|
|
relays []string,
|
|
|
|
pool *nostr.SimplePool,
|
2024-02-29 20:37:16 -03:00
|
|
|
onAuth func(string),
|
2024-02-29 20:29:08 -03:00
|
|
|
) *BunkerClient {
|
2024-02-06 00:45:36 -03:00
|
|
|
if pool == nil {
|
|
|
|
pool = nostr.NewSimplePool(ctx)
|
|
|
|
}
|
|
|
|
|
2024-02-29 20:29:08 -03:00
|
|
|
clientPublicKey, _ := nostr.GetPublicKey(clientSecretKey)
|
|
|
|
sharedSecret, _ := nip04.ComputeSharedSecret(targetPublicKey, clientSecretKey)
|
2024-10-26 19:37:36 -03:00
|
|
|
conversationKey, _ := nip44.GenerateConversationKey(targetPublicKey, clientSecretKey)
|
2024-02-06 00:45:36 -03:00
|
|
|
|
|
|
|
bunker := &BunkerClient{
|
2024-03-02 07:48:41 -03:00
|
|
|
pool: pool,
|
|
|
|
clientSecretKey: clientSecretKey,
|
|
|
|
target: targetPublicKey,
|
|
|
|
relays: relays,
|
2024-10-26 19:37:36 -03:00
|
|
|
conversationKey: conversationKey,
|
2024-03-02 07:48:41 -03:00
|
|
|
listeners: xsync.NewMapOf[string, chan Response](),
|
|
|
|
expectingAuth: xsync.NewMapOf[string, struct{}](),
|
|
|
|
onAuth: onAuth,
|
|
|
|
idPrefix: "gn-" + strconv.Itoa(rand.Intn(65536)),
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
2024-04-25 18:25:35 -03:00
|
|
|
now := nostr.Now()
|
2024-02-06 00:45:36 -03:00
|
|
|
events := pool.SubMany(ctx, relays, nostr.Filters{
|
|
|
|
{
|
2024-05-15 16:59:38 -03:00
|
|
|
Tags: nostr.TagMap{"p": []string{clientPublicKey}},
|
|
|
|
Kinds: []int{nostr.KindNostrConnect},
|
|
|
|
Since: &now,
|
|
|
|
LimitZero: true,
|
2024-02-06 00:45:36 -03:00
|
|
|
},
|
2024-12-17 11:05:27 -03:00
|
|
|
}, nostr.WithLabel("bunker46client"))
|
2024-02-06 00:45:36 -03:00
|
|
|
for ie := range events {
|
|
|
|
if ie.Kind != nostr.KindNostrConnect {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var resp Response
|
2024-10-26 19:37:36 -03:00
|
|
|
plain, err := nip44.Decrypt(ie.Content, conversationKey)
|
2024-02-06 00:45:36 -03:00
|
|
|
if err != nil {
|
2024-10-26 19:37:36 -03:00
|
|
|
plain, err = nip04.Decrypt(ie.Content, sharedSecret)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal([]byte(plain), &resp)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-02-29 20:37:16 -03:00
|
|
|
if resp.Result == "auth_url" {
|
|
|
|
// special case
|
|
|
|
authURL := resp.Error
|
|
|
|
if _, ok := bunker.expectingAuth.Load(resp.ID); ok {
|
|
|
|
bunker.onAuth(authURL)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-02-06 00:45:36 -03:00
|
|
|
if dispatcher, ok := bunker.listeners.Load(resp.ID); ok {
|
|
|
|
dispatcher <- resp
|
2024-12-17 11:05:27 -03:00
|
|
|
continue
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2024-02-29 20:29:08 -03:00
|
|
|
return bunker
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (bunker *BunkerClient) Ping(ctx context.Context) error {
|
|
|
|
_, err := bunker.RPC(ctx, "ping", []string{})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (string, error) {
|
|
|
|
if bunker.getPublicKeyResponse != "" {
|
|
|
|
return bunker.getPublicKeyResponse, nil
|
|
|
|
}
|
|
|
|
resp, err := bunker.RPC(ctx, "get_public_key", []string{})
|
|
|
|
bunker.getPublicKeyResponse = resp
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
|
|
|
resp, err := bunker.RPC(ctx, "sign_event", []string{evt.String()})
|
2024-11-12 15:48:49 -03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = easyjson.Unmarshal([]byte(resp), evt)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
2024-11-12 15:48:49 -03:00
|
|
|
|
|
|
|
if !bunker.SkipSignatureCheck {
|
|
|
|
if ok := evt.CheckID(); !ok {
|
|
|
|
return fmt.Errorf("sign_event response from bunker has invalid id")
|
|
|
|
}
|
|
|
|
if ok, _ := evt.CheckSignature(); !ok {
|
|
|
|
return fmt.Errorf("sign_event response from bunker has invalid signature")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
|
2024-09-10 15:28:18 -03:00
|
|
|
func (bunker *BunkerClient) NIP44Encrypt(
|
|
|
|
ctx context.Context,
|
|
|
|
targetPublicKey string,
|
|
|
|
plaintext string,
|
|
|
|
) (string, error) {
|
|
|
|
return bunker.RPC(ctx, "nip44_encrypt", []string{targetPublicKey, plaintext})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bunker *BunkerClient) NIP44Decrypt(
|
|
|
|
ctx context.Context,
|
|
|
|
targetPublicKey string,
|
|
|
|
ciphertext string,
|
|
|
|
) (string, error) {
|
|
|
|
return bunker.RPC(ctx, "nip44_decrypt", []string{targetPublicKey, ciphertext})
|
|
|
|
}
|
|
|
|
|
2024-09-17 11:25:13 -03:00
|
|
|
func (bunker *BunkerClient) NIP04Encrypt(
|
|
|
|
ctx context.Context,
|
|
|
|
targetPublicKey string,
|
|
|
|
plaintext string,
|
|
|
|
) (string, error) {
|
|
|
|
return bunker.RPC(ctx, "nip04_encrypt", []string{targetPublicKey, plaintext})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bunker *BunkerClient) NIP04Decrypt(
|
|
|
|
ctx context.Context,
|
|
|
|
targetPublicKey string,
|
|
|
|
ciphertext string,
|
|
|
|
) (string, error) {
|
|
|
|
return bunker.RPC(ctx, "nip04_decrypt", []string{targetPublicKey, ciphertext})
|
|
|
|
}
|
|
|
|
|
2024-02-06 00:45:36 -03:00
|
|
|
func (bunker *BunkerClient) RPC(ctx context.Context, method string, params []string) (string, error) {
|
2024-02-11 11:07:01 -03:00
|
|
|
id := bunker.idPrefix + "-" + strconv.FormatUint(bunker.serial.Add(1), 10)
|
2024-02-06 00:45:36 -03:00
|
|
|
req, err := json.Marshal(Request{
|
|
|
|
ID: id,
|
|
|
|
Method: method,
|
|
|
|
Params: params,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2024-10-27 15:03:07 -03:00
|
|
|
content, err := nip44.Encrypt(string(req), bunker.conversationKey)
|
2024-02-06 00:45:36 -03:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error encrypting request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
evt := nostr.Event{
|
|
|
|
Content: content,
|
|
|
|
CreatedAt: nostr.Now(),
|
|
|
|
Kind: nostr.KindNostrConnect,
|
|
|
|
Tags: nostr.Tags{{"p", bunker.target}},
|
|
|
|
}
|
|
|
|
if err := evt.Sign(bunker.clientSecretKey); err != nil {
|
|
|
|
return "", fmt.Errorf("failed to sign request event: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
respWaiter := make(chan Response)
|
|
|
|
bunker.listeners.Store(id, respWaiter)
|
2024-09-24 15:08:27 +09:00
|
|
|
defer func() {
|
|
|
|
bunker.listeners.Delete(id)
|
|
|
|
close(respWaiter)
|
|
|
|
}()
|
2024-08-29 17:49:51 -03:00
|
|
|
hasWorked := make(chan struct{})
|
|
|
|
|
|
|
|
for _, url := range bunker.relays {
|
|
|
|
go func(url string) {
|
|
|
|
relay, err := bunker.pool.EnsureRelay(url)
|
|
|
|
if err == nil {
|
|
|
|
select {
|
|
|
|
case hasWorked <- struct{}{}:
|
|
|
|
default:
|
|
|
|
}
|
2024-09-11 16:42:56 -03:00
|
|
|
relay.Publish(ctx, evt)
|
2024-08-29 17:49:51 -03:00
|
|
|
}
|
|
|
|
}(url)
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
2024-04-25 18:25:35 -03:00
|
|
|
|
2024-08-29 17:49:51 -03:00
|
|
|
select {
|
|
|
|
case <-hasWorked:
|
|
|
|
// continue
|
|
|
|
case <-ctx.Done():
|
2024-02-06 00:45:36 -03:00
|
|
|
return "", fmt.Errorf("couldn't connect to any relay")
|
|
|
|
}
|
|
|
|
|
2024-09-24 15:08:27 +09:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return "", fmt.Errorf("context canceled")
|
|
|
|
case resp := <-respWaiter:
|
|
|
|
if resp.Error != "" {
|
|
|
|
return "", fmt.Errorf("response error: %s", resp.Error)
|
|
|
|
}
|
|
|
|
return resp.Result, nil
|
2024-02-06 00:45:36 -03:00
|
|
|
}
|
|
|
|
}
|