From fd8b2b22b207682c2a40dbf4c42a59c47ae38001 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 9 Jan 2024 16:55:00 -0300 Subject: [PATCH] nip-46 dynamic and static signers. --- nip46/dynamic-signer.go | 218 ++++++++++++++++++++++ nip46/nip46.go | 79 ++++++++ nip46/{signer.go => static-key-signer.go} | 100 +++------- 3 files changed, 325 insertions(+), 72 deletions(-) create mode 100644 nip46/dynamic-signer.go create mode 100644 nip46/nip46.go rename nip46/{signer.go => static-key-signer.go} (68%) diff --git a/nip46/dynamic-signer.go b/nip46/dynamic-signer.go new file mode 100644 index 0000000..1e8ffaf --- /dev/null +++ b/nip46/dynamic-signer.go @@ -0,0 +1,218 @@ +package nip46 + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/mailru/easyjson" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" + "golang.org/x/exp/slices" +) + +var _ Signer = (*DynamicSigner)(nil) + +type DynamicSigner struct { + sessionKeys []string + sessions []Session + + sync.Mutex + + RelaysToAdvertise map[string]RelayReadWrite + + getPrivateKey func(pubkey string) (string, error) + authorizeSigning func(event nostr.Event) bool + onEventSigned func(event nostr.Event) + authorizeNIP04 func() bool +} + +func NewDynamicSigner( + getPrivateKey func(pubkey string) (string, error), + authorizeSigning func(event nostr.Event) bool, + onEventSigned func(event nostr.Event), + authorizeNIP04 func() bool, +) DynamicSigner { + return DynamicSigner{ + getPrivateKey: getPrivateKey, + authorizeSigning: authorizeSigning, + onEventSigned: onEventSigned, + authorizeNIP04: authorizeNIP04, + RelaysToAdvertise: make(map[string]RelayReadWrite), + } +} + +func (p *DynamicSigner) GetSession(clientPubkey string) (Session, bool) { + idx, exists := slices.BinarySearch(p.sessionKeys, clientPubkey) + if exists { + return p.sessions[idx], true + } + return Session{}, false +} + +func (p *DynamicSigner) setSession(clientPubkey string, session Session) { + p.Lock() + defer p.Unlock() + + idx, exists := slices.BinarySearch(p.sessionKeys, clientPubkey) + if exists { + return + } + + // add to pool + p.sessionKeys = append(p.sessionKeys, "") // bogus append just to increase the capacity + p.sessions = append(p.sessions, Session{}) + copy(p.sessionKeys[idx+1:], p.sessionKeys[idx:]) + copy(p.sessions[idx+1:], p.sessions[idx:]) + p.sessionKeys[idx] = clientPubkey + p.sessions[idx] = session +} + +func (p *DynamicSigner) HandleRequest(event *nostr.Event) ( + req Request, + resp Response, + eventResponse nostr.Event, + harmless bool, + err error, +) { + if event.Kind != nostr.KindNostrConnect { + return req, resp, eventResponse, false, + fmt.Errorf("event kind is %d, but we expected %d", event.Kind, nostr.KindNostrConnect) + } + + targetUser := event.Tags.GetFirst([]string{"p", ""}) + if targetUser == nil || !nostr.IsValidPublicKeyHex((*targetUser)[1]) { + return req, resp, eventResponse, false, fmt.Errorf("invalid \"p\" tag") + } + + targetPubkey := (*targetUser)[1] + + privateKey, err := p.getPrivateKey(targetPubkey) + if err != nil { + return req, resp, eventResponse, false, fmt.Errorf("no private key for %s: %w", targetPubkey, err) + } + + var session Session + idx, exists := slices.BinarySearch(p.sessionKeys, event.PubKey) + if exists { + session = p.sessions[idx] + } else { + session = Session{} + + session.SharedKey, err = nip04.ComputeSharedSecret(event.PubKey, privateKey) + if err != nil { + return req, resp, eventResponse, false, fmt.Errorf("failed to compute shared secret: %w", err) + } + p.setSession(event.PubKey, session) + + req, err = session.ParseRequest(event) + if err != nil { + return req, resp, eventResponse, false, fmt.Errorf("error parsing request: %w", err) + } + } + + var result string + var resultErr error + + switch req.Method { + case "connect": + result = "ack" + harmless = true + case "get_public_key": + result = targetPubkey + harmless = true + case "sign_event": + if len(req.Params) != 1 { + resultErr = fmt.Errorf("wrong number of arguments to 'sign_event'") + break + } + evt := nostr.Event{} + err = easyjson.Unmarshal([]byte(req.Params[0]), &evt) + if err != nil { + resultErr = fmt.Errorf("failed to decode event/2: %w", err) + break + } + if !p.authorizeSigning(evt) { + resultErr = fmt.Errorf("refusing to sign this event") + break + } + err = evt.Sign(privateKey) + if err != nil { + resultErr = fmt.Errorf("failed to sign event: %w", err) + break + } + jrevt, _ := easyjson.Marshal(evt) + result = string(jrevt) + case "get_relays": + jrelays, _ := json.Marshal(p.RelaysToAdvertise) + result = string(jrelays) + harmless = true + case "nip04_encrypt": + if len(req.Params) != 2 { + resultErr = fmt.Errorf("wrong number of arguments to 'nip04_encrypt'") + break + } + thirdPartyPubkey := req.Params[0] + if !nostr.IsValidPublicKeyHex(thirdPartyPubkey) { + resultErr = fmt.Errorf("first argument to 'nip04_encrypt' is not a pubkey string") + break + } + if !p.authorizeNIP04() { + resultErr = fmt.Errorf("refusing to encrypt") + break + } + plaintext := req.Params[1] + sharedSecret, err := nip04.ComputeSharedSecret(thirdPartyPubkey, privateKey) + if err != nil { + resultErr = fmt.Errorf("failed to compute shared secret: %w", err) + break + } + ciphertext, err := nip04.Encrypt(plaintext, sharedSecret) + if err != nil { + resultErr = fmt.Errorf("failed to encrypt: %w", err) + break + } + result = ciphertext + case "nip04_decrypt": + if len(req.Params) != 2 { + resultErr = fmt.Errorf("wrong number of arguments to 'nip04_decrypt'") + break + } + thirdPartyPubkey := req.Params[0] + if !nostr.IsValidPublicKeyHex(thirdPartyPubkey) { + resultErr = fmt.Errorf("first argument to 'nip04_decrypt' is not a pubkey string") + break + } + if !p.authorizeNIP04() { + resultErr = fmt.Errorf("refusing to decrypt") + break + } + ciphertext := req.Params[1] + sharedSecret, err := nip04.ComputeSharedSecret(thirdPartyPubkey, privateKey) + if err != nil { + resultErr = fmt.Errorf("failed to compute shared secret: %w", err) + break + } + plaintext, err := nip04.Decrypt(ciphertext, sharedSecret) + if err != nil { + resultErr = fmt.Errorf("failed to encrypt: %w", err) + break + } + result = plaintext + default: + return req, resp, eventResponse, false, + fmt.Errorf("unknown method '%s'", req.Method) + } + + resp, eventResponse, err = session.MakeResponse(req.ID, event.PubKey, result, resultErr) + if err != nil { + return req, resp, eventResponse, harmless, err + } + + err = eventResponse.Sign(privateKey) + if err != nil { + return req, resp, eventResponse, harmless, err + } + + return req, resp, eventResponse, harmless, err +} diff --git a/nip46/nip46.go b/nip46/nip46.go new file mode 100644 index 0000000..7d82f88 --- /dev/null +++ b/nip46/nip46.go @@ -0,0 +1,79 @@ +package nip46 + +import ( + "encoding/json" + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" +) + +type Request struct { + ID string `json:"id"` + Method string `json:"method"` + Params []string `json:"params"` +} + +type Response struct { + ID string `json:"id"` + Error string `json:"error,omitempty"` + Result string `json:"result,omitempty"` +} + +type Signer interface { + GetSession(clientPubkey string) (Session, bool) + HandleRequest(event *nostr.Event) (req Request, resp Response, eventResponse nostr.Event, harmless bool, err error) +} + +type Session struct { + SharedKey []byte +} + +type RelayReadWrite struct { + Read bool `json:"read"` + Write bool `json:"write"` +} + +func (s Session) ParseRequest(event *nostr.Event) (Request, error) { + var req Request + + plain, err := nip04.Decrypt(event.Content, s.SharedKey) + if err != nil { + return req, fmt.Errorf("failed to decrypt event from %s: %w", event.PubKey, err) + } + + err = json.Unmarshal([]byte(plain), &req) + return req, err +} + +func (s Session) MakeResponse( + id string, + requester string, + result string, + err error, +) (resp Response, evt nostr.Event, error error) { + if err != nil { + resp = Response{ + ID: id, + Error: err.Error(), + } + } else if result != "" { + resp = Response{ + ID: id, + Result: result, + } + } + + jresp, _ := json.Marshal(resp) + ciphertext, err := nip04.Encrypt(string(jresp), s.SharedKey) + if err != nil { + return resp, evt, fmt.Errorf("failed to encrypt result: %w", err) + } + evt.Content = ciphertext + + evt.CreatedAt = nostr.Now() + evt.Kind = nostr.KindNostrConnect + evt.Tags = nostr.Tags{nostr.Tag{"p", requester}} + + return resp, evt, nil +} diff --git a/nip46/signer.go b/nip46/static-key-signer.go similarity index 68% rename from nip46/signer.go rename to nip46/static-key-signer.go index 359aca6..78df95a 100644 --- a/nip46/signer.go +++ b/nip46/static-key-signer.go @@ -3,6 +3,7 @@ package nip46 import ( "encoding/json" "fmt" + "sync" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" @@ -10,89 +11,38 @@ import ( "golang.org/x/exp/slices" ) -type Request struct { - ID string `json:"id"` - Method string `json:"method"` - Params []string `json:"params"` -} +var _ Signer = (*StaticKeySigner)(nil) -type Response struct { - ID string `json:"id"` - Error string `json:"error,omitempty"` - Result string `json:"result,omitempty"` -} - -type Session struct { - SharedKey []byte -} - -func (s Session) ParseRequest(event *nostr.Event) (Request, error) { - var req Request - - plain, err := nip04.Decrypt(event.Content, s.SharedKey) - if err != nil { - return req, fmt.Errorf("failed to decrypt event from %s: %w", event.PubKey, err) - } - - err = json.Unmarshal([]byte(plain), &req) - return req, err -} - -func (s Session) MakeResponse( - id string, - requester string, - result string, - err error, -) (resp Response, evt nostr.Event, error error) { - if err != nil { - resp = Response{ - ID: id, - Error: err.Error(), - } - } else if result != "" { - resp = Response{ - ID: id, - Result: result, - } - } - - jresp, _ := json.Marshal(resp) - ciphertext, err := nip04.Encrypt(string(jresp), s.SharedKey) - if err != nil { - return resp, evt, fmt.Errorf("failed to encrypt result: %w", err) - } - evt.Content = ciphertext - - evt.CreatedAt = nostr.Now() - evt.Kind = nostr.KindNostrConnect - evt.Tags = nostr.Tags{nostr.Tag{"p", requester}} - - return resp, evt, nil -} - -type Signer struct { +type StaticKeySigner struct { secretKey string sessionKeys []string sessions []Session - RelaysToAdvertise map[string]relayReadWrite + sync.Mutex + + RelaysToAdvertise map[string]RelayReadWrite } -type relayReadWrite struct { - Read bool `json:"read"` - Write bool `json:"write"` +func NewStaticKeySigner(secretKey string) StaticKeySigner { + return StaticKeySigner{ + secretKey: secretKey, + RelaysToAdvertise: make(map[string]RelayReadWrite), + } } -func NewSigner(secretKey string) Signer { - return Signer{secretKey: secretKey} +func (p *StaticKeySigner) GetSession(clientPubkey string) (Session, bool) { + idx, exists := slices.BinarySearch(p.sessionKeys, clientPubkey) + if exists { + return p.sessions[idx], true + } + return Session{}, false } -func (p *Signer) AddRelayToAdvertise(url string, read bool, write bool) { - p.RelaysToAdvertise[url] = relayReadWrite{read, write} -} +func (p *StaticKeySigner) getOrCreateSession(clientPubkey string) (Session, error) { + p.Lock() + defer p.Unlock() -func (p *Signer) GetSession(clientPubkey string) (Session, error) { idx, exists := slices.BinarySearch(p.sessionKeys, clientPubkey) if exists { return p.sessions[idx], nil @@ -118,13 +68,19 @@ func (p *Signer) GetSession(clientPubkey string) (Session, error) { return session, nil } -func (p *Signer) HandleRequest(event *nostr.Event) (req Request, resp Response, eventResponse nostr.Event, harmless bool, err error) { +func (p *StaticKeySigner) HandleRequest(event *nostr.Event) ( + req Request, + resp Response, + eventResponse nostr.Event, + harmless bool, + err error, +) { if event.Kind != nostr.KindNostrConnect { return req, resp, eventResponse, false, fmt.Errorf("event kind is %d, but we expected %d", event.Kind, nostr.KindNostrConnect) } - session, err := p.GetSession(event.PubKey) + session, err := p.getOrCreateSession(event.PubKey) if err != nil { return req, resp, eventResponse, false, err }