From 751462990665087e7cedbe7f74ad5ff7f6cc873c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 10 Sep 2024 15:28:30 -0300 Subject: [PATCH] nip17 and nip59. --- nip17/nip17.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ nip59/nip59.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 nip17/nip17.go create mode 100644 nip59/nip59.go diff --git a/nip17/nip17.go b/nip17/nip17.go new file mode 100644 index 0000000..cd7d9b8 --- /dev/null +++ b/nip17/nip17.go @@ -0,0 +1,99 @@ +package nip17 + +import ( + "context" + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip59" +) + +func GetDMRelays(ctx context.Context, pubkey string, pool *nostr.SimplePool, relaysToQuery []string) []string { + ie := pool.QuerySingle(ctx, relaysToQuery, nostr.Filter{ + Authors: []string{pubkey}, + Kinds: []int{10050}, + }) + if ie == nil { + return nil + } + + res := make([]string, 0, 3) + for _, tag := range ie.Tags { + if len(tag) >= 2 && tag[0] == "relay" { + res = append(res, tag[1]) + if len(res) == 3 { + return res + } + } + } + + return res +} + +func PrepareMessage( + content string, + tags nostr.Tags, + ourPubkey string, + encrypt func(string) (string, error), + finalizeAndSign func(*nostr.Event) error, + recipientPubKey string, + modify func(*nostr.Event), +) (nostr.Event, error) { + rumor := nostr.Event{ + Kind: 14, + Content: content, + Tags: tags, + CreatedAt: nostr.Now(), + PubKey: ourPubkey, + } + rumor.ID = rumor.GetID() + + seal, err := nip59.Seal(rumor, encrypt) + if err != nil { + return nostr.Event{}, fmt.Errorf("failed to seal: %w", err) + } + + if err := finalizeAndSign(&seal); err != nil { + return nostr.Event{}, fmt.Errorf("finalizeAndSign failed: %w", err) + } + + return nip59.GiftWrap(seal, recipientPubKey, modify) +} + +// ListenForMessages returns a channel with the rumors already decrypted and checked +func ListenForMessages( + ctx context.Context, + pool *nostr.SimplePool, + relays []string, + ourPubkey string, + since nostr.Timestamp, + decrypt func(string) (string, error), +) chan nostr.Event { + ch := make(chan nostr.Event) + + go func() { + for ie := range pool.SubMany(ctx, relays, nostr.Filters{ + { + Kinds: []int{1059}, + Tags: nostr.TagMap{"p": []string{ourPubkey}}, + Since: &since, + }, + }) { + seal, err := nip59.GiftUnwrap(*ie.Event, decrypt) + if err != nil { + nostr.InfoLogger.Printf("[nip17] failed to unwrap received message: %s\n", err) + continue + } + + rumor, err := nip59.Unseal(seal, decrypt) + if err != nil { + nostr.InfoLogger.Printf("[nip17] failed to unseal received message: %s\n", err) + continue + } + + ch <- rumor + } + }() + + return ch +} diff --git a/nip59/nip59.go b/nip59/nip59.go new file mode 100644 index 0000000..935a023 --- /dev/null +++ b/nip59/nip59.go @@ -0,0 +1,91 @@ +package nip59 + +import ( + "fmt" + "math/rand" + + "github.com/mailru/easyjson" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip44" +) + +// Seal takes a rumor, encrypts it and returns an unsigned 'seal' event, the 'seal' must be signed +// afterwards. +func Seal(rumor nostr.Event, encrypt func(string) (string, error)) (nostr.Event, error) { + rumor.Sig = "" + ciphertext, err := encrypt(rumor.String()) + + return nostr.Event{ + Kind: 13, + Content: ciphertext, + CreatedAt: nostr.Now() - nostr.Timestamp(60*rand.Int63n(600) /* up to 6 hours in the past */), + Tags: make(nostr.Tags, 0), + }, err +} + +// Takes a signed 'seal' and gift-wraps it using a random key, returns it signed. +// +// modify is a function that takes the gift-wrap before signing, can be used to apply +// NIP-13 PoW or other things, otherwise can be nil. +func GiftWrap(seal nostr.Event, recipientPublicKey string, modify func(*nostr.Event)) (nostr.Event, error) { + nonceKey := nostr.GeneratePrivateKey() + temporaryConversationKey, err := nip44.GenerateConversationKey(recipientPublicKey, nonceKey) + if err != nil { + return nostr.Event{}, err + } + + ciphertext, err := nip44.Encrypt(seal.String(), temporaryConversationKey, nil) + if err != nil { + return nostr.Event{}, err + } + + gw := nostr.Event{ + Kind: 1059, + Content: ciphertext, + CreatedAt: nostr.Now() - nostr.Timestamp(60*rand.Int63n(600) /* up to 6 hours in the past */), + Tags: nostr.Tags{ + nostr.Tag{"p", recipientPublicKey}, + }, + } + + // apply POW if necessary + if modify != nil { + modify(&gw) + } + + err = gw.Sign(nonceKey) + return gw, nil +} + +func GiftUnwrap(gw nostr.Event, decrypt func(string) (string, error)) (seal nostr.Event, err error) { + jevt, err := decrypt(gw.Content) + if err != nil { + return seal, err + } + + err = easyjson.Unmarshal([]byte(jevt), &seal) + if err != nil { + return seal, err + } + + if ok, _ := seal.CheckSignature(); !ok { + return seal, fmt.Errorf("seal signature is invalid") + } + + return seal, nil +} + +func Unseal(seal nostr.Event, decrypt func(string) (string, error)) (rumor nostr.Event, err error) { + jevt, err := decrypt(seal.Content) + if err != nil { + return rumor, err + } + + err = easyjson.Unmarshal([]byte(jevt), &rumor) + if err != nil { + return rumor, err + } + + rumor.PubKey = seal.PubKey + return rumor, nil +}