package nip46 import ( "context" "fmt" "net/url" "strconv" "sync" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip04" "fiatjaf.com/nostr/nip44" "github.com/mailru/easyjson" ) var _ Signer = (*StaticKeySigner)(nil) type StaticKeySigner struct { secretKey nostr.SecretKey sessions map[nostr.PubKey]*Session sync.Mutex AuthorizeRequest func(harmless bool, from nostr.PubKey, secret string) bool // used for switch_relays call DefaultRelays []string } func NewStaticKeySigner(secretKey [32]byte) StaticKeySigner { return StaticKeySigner{ secretKey: secretKey, sessions: make(map[nostr.PubKey]*Session), } } func (p *StaticKeySigner) getOrCreateSession(clientPubkey nostr.PubKey) (*Session, error) { p.Lock() defer p.Unlock() session, exists := p.sessions[clientPubkey] if exists { return session, nil } ck, err := nip44.GenerateConversationKey(clientPubkey, p.secretKey) if err != nil { return nil, fmt.Errorf("failed to compute shared secret: %w", err) } session = &Session{ PublicKey: p.secretKey.Public(), ConversationKey: ck, } // add to pool p.sessions[clientPubkey] = session return session, nil } // HandleNostrConnectURI works like HandleRequest, but takes a nostrconnect:// URI as input, as scanned/pasted // by the user, produced by the client. func (p *StaticKeySigner) HandleNostrConnectURI(ctx context.Context, uri *url.URL) ( resp Response, eventResponse nostr.Event, err error, ) { clientPublicKey, err := nostr.PubKeyFromHex(uri.Host) if err != nil { return resp, eventResponse, err } secret := uri.Query().Get("secret") // pretend they started with a request conversationKey, err := nip44.GenerateConversationKey(clientPublicKey, p.secretKey) if err != nil { return resp, eventResponse, err } reqj, _ := json.Marshal(Request{ ID: "nostrconnect-" + strconv.FormatInt(int64(nostr.Now()), 10), Method: "imagined-nostrconnect", Params: []string{clientPublicKey.Hex(), secret}, }) ciphertext, err := nip44.Encrypt(string(reqj), conversationKey) if err != nil { return resp, eventResponse, err } _, resp, eventResponse, err = p.HandleRequest(ctx, nostr.Event{ PubKey: clientPublicKey, Kind: nostr.KindNostrConnect, Content: ciphertext, }) return resp, eventResponse, err } func (p *StaticKeySigner) HandleRequest(_ context.Context, event nostr.Event) ( req Request, resp Response, eventResponse nostr.Event, err error, ) { if event.Kind != nostr.KindNostrConnect { return req, resp, eventResponse, fmt.Errorf("event kind is %d, but we expected %d", event.Kind, nostr.KindNostrConnect) } session, err := p.getOrCreateSession(event.PubKey) if err != nil { return req, resp, eventResponse, err } req, err = session.ParseRequest(event) if err != nil { return req, resp, eventResponse, fmt.Errorf("error parsing request: %w", err) } var secret string var harmless bool var result string var resultErr error switch req.Method { case "imagined-nostrconnect": // this is a fake request we pretend has existed, but was actually just we reading the nostrconnect:// uri if len(req.Params) < 2 || req.Params[1] == "" { resultErr = fmt.Errorf("needs a second argument 'secret'") break } result = req.Params[1] harmless = true case "connect": if len(req.Params) >= 2 { secret = req.Params[1] } result = "ack" harmless = true case "get_public_key": result = nostr.HexEncodeToString(session.PublicKey[:]) 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 } err = evt.Sign(p.secretKey) if err != nil { resultErr = fmt.Errorf("failed to sign event: %w", err) break } jrevt, _ := easyjson.Marshal(evt) result = string(jrevt) case "nip44_encrypt": if len(req.Params) != 2 { resultErr = fmt.Errorf("wrong number of arguments to 'nip44_encrypt'") break } thirdPartyPubkey, err := nostr.PubKeyFromHex(req.Params[0]) if err != nil { resultErr = fmt.Errorf("first argument to 'nip04_encrypt' is not a valid pubkey hex") break } plaintext := req.Params[1] sharedSecret, err := nip44.GenerateConversationKey(thirdPartyPubkey, p.secretKey) if err != nil { resultErr = fmt.Errorf("failed to compute shared secret: %w", err) break } ciphertext, err := nip44.Encrypt(plaintext, sharedSecret) if err != nil { resultErr = fmt.Errorf("failed to encrypt: %w", err) break } result = ciphertext case "nip44_decrypt": if len(req.Params) != 2 { resultErr = fmt.Errorf("wrong number of arguments to 'nip04_decrypt'") break } thirdPartyPubkey, err := nostr.PubKeyFromHex(req.Params[0]) if err != nil { resultErr = fmt.Errorf("first argument to 'nip04_decrypt' is not a valid pubkey hex") break } ciphertext := req.Params[1] sharedSecret, err := nip44.GenerateConversationKey(thirdPartyPubkey, p.secretKey) if err != nil { resultErr = fmt.Errorf("failed to compute shared secret: %w", err) break } plaintext, err := nip44.Decrypt(ciphertext, sharedSecret) if err != nil { resultErr = fmt.Errorf("failed to encrypt: %w", err) break } result = plaintext case "nip04_encrypt": if len(req.Params) != 2 { resultErr = fmt.Errorf("wrong number of arguments to 'nip04_encrypt'") break } thirdPartyPubkey, err := nostr.PubKeyFromHex(req.Params[0]) if err != nil { resultErr = fmt.Errorf("first argument to 'nip04_encrypt' is not a valid pubkey hex") break } plaintext := req.Params[1] sharedSecret, err := nip04.ComputeSharedSecret(thirdPartyPubkey, p.secretKey) 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, err := nostr.PubKeyFromHex(req.Params[0]) if err != nil { resultErr = fmt.Errorf("first argument to 'nip04_decrypt' is not a valid pubkey hex") break } ciphertext := req.Params[1] sharedSecret, err := nip04.ComputeSharedSecret(thirdPartyPubkey, p.secretKey) 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 case "ping": result = "pong" harmless = true case "switch_relays": j, _ := json.Marshal(p.DefaultRelays) result = string(j) default: return req, resp, eventResponse, fmt.Errorf("unknown method '%s'", req.Method) } if resultErr == nil && p.AuthorizeRequest != nil { if !p.AuthorizeRequest(harmless, event.PubKey, secret) { resultErr = fmt.Errorf("unauthorized") } } resp, eventResponse, err = session.MakeResponse(req.ID, event.PubKey, result, resultErr) if err != nil { return req, resp, eventResponse, err } err = eventResponse.Sign(p.secretKey) if err != nil { return req, resp, eventResponse, err } return req, resp, eventResponse, err }