diff --git a/common.go b/common.go deleted file mode 100644 index f4b3335..0000000 --- a/common.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import ( - "github.com/nbd-wtf/go-nostr/sdk" -) - -var sys = sdk.NewSystem() diff --git a/event.go b/event.go index d2aa549..2197abc 100644 --- a/event.go +++ b/event.go @@ -37,29 +37,7 @@ example: echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`, DisableSliceFlagSeparator: true, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "sec", - Usage: "secret key to sign the event, as nsec, ncryptsec or hex", - DefaultText: "the key '1'", - Category: CATEGORY_SIGNER, - }, - &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the event", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect", - Usage: "sign event using NIP-46, expects a bunker://... URL", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", - DefaultText: "a random key", - Category: CATEGORY_SIGNER, - }, + Flags: append(defaultKeyFlags, // ~ these args are only for the convoluted musig2 signing process // they will be generally copy-shared-pasted across some manual coordination method between participants &cli.UintFlag{ @@ -151,7 +129,7 @@ example: Value: nostr.Now(), Category: CATEGORY_EVENT_FIELDS, }, - }, + ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { // try to connect to the relays here @@ -170,10 +148,11 @@ example: } }() - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + kr, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } + sec, _, _ := gatherSecretKeyOrBunkerFromArguments(ctx, c) doAuth := c.Bool("auth") @@ -250,12 +229,7 @@ example: if difficulty := c.Uint("pow"); difficulty > 0 { // before doing pow we need the pubkey - if bunker != nil { - evt.PubKey, err = bunker.GetPublicKey(ctx) - if err != nil { - return fmt.Errorf("can't pow: failed to get public key from bunker: %w", err) - } - } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + if numSigners := c.Uint("musig"); numSigners > 1 { pubkeys := c.StringSlice("musig-pubkey") if int(numSigners) != len(pubkeys) { return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront") @@ -265,7 +239,7 @@ example: return err } } else { - evt.PubKey, _ = nostr.GetPublicKey(sec) + evt.PubKey = kr.GetPublicKey(ctx) } // try to generate work with this difficulty -- runs forever @@ -276,11 +250,7 @@ example: } if evt.Sig == "" || mustRehashAndResign { - if bunker != nil { - if err := bunker.SignEvent(ctx, &evt); err != nil { - return fmt.Errorf("failed to sign with bunker: %w", err) - } - } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { pubkeys := c.StringSlice("musig-pubkey") secNonce := c.String("musig-nonce-secret") pubNonces := c.StringSlice("musig-nonce") @@ -295,7 +265,7 @@ example: // instructions for what to do should have been printed by the performMusig() function return nil } - } else if err := evt.Sign(sec); err != nil { + } else if err := kr.SignEvent(ctx, &evt); err != nil { return fmt.Errorf("error signing with provided key: %w", err) } } @@ -332,21 +302,9 @@ example: // error publishing if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth { // if the relay is requesting auth and we can auth, let's do it - var pk string - if bunker != nil { - pk, err = bunker.GetPublicKey(ctx) - if err != nil { - return fmt.Errorf("failed to get public key from bunker: %w", err) - } - } else { - pk, _ = nostr.GetPublicKey(sec) - } - log("performing auth as %s... ", pk) - if err := relay.Auth(ctx, func(evt *nostr.Event) error { - if bunker != nil { - return bunker.SignEvent(ctx, evt) - } - return evt.Sign(sec) + log("performing auth as %s... ", kr.GetPublicKey(ctx)) + if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { + return kr.SignEvent(ctx, authEvent) }); err == nil { // try to publish again, but this time don't try to auth again doAuth = false diff --git a/go.mod b/go.mod index 9d43b24..5ae81bc 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.36.2 + github.com/nbd-wtf/go-nostr v0.36.3 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index 5b9e04d..c6a7a94 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.36.2 h1:c79JA5FOsNeVFdPUqP9dAA5xRw1qYcwaweKU/U8YyhE= -github.com/nbd-wtf/go-nostr v0.36.2/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= +github.com/nbd-wtf/go-nostr v0.36.3 h1:50fNFO8vQNMEIZ+6qUq0M5hlqEtA13WrtrKcz10eg9k= +github.com/nbd-wtf/go-nostr v0.36.3/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/helpers.go b/helpers.go index dfd9d93..8df8381 100644 --- a/helpers.go +++ b/helpers.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "encoding/hex" "fmt" "math/rand" "net/url" @@ -11,15 +10,14 @@ import ( "strings" "time" - "github.com/chzyer/readline" "github.com/fatih/color" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/go-nostr/nip46" - "github.com/nbd-wtf/go-nostr/nip49" + "github.com/nbd-wtf/go-nostr/sdk" ) +var sys = sdk.NewSystem() + const ( LINE_PROCESSING_ERROR = iota ) @@ -179,109 +177,6 @@ func exitIfLineProcessingError(ctx context.Context) { } } -func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { - var err error - - if bunkerURL := c.String("connect"); bunkerURL != "" { - clientKey := c.String("connect-as") - if clientKey != "" { - clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey - } else { - clientKey = nostr.GeneratePrivateKey() - } - bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { - fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) - }) - return "", bunker, err - } - - // take private from flags, environment variable or default to 1 - sec := c.String("sec") - if sec == "" { - if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok { - sec = key - } else { - sec = "0000000000000000000000000000000000000000000000000000000000000001" - } - } - - if c.Bool("prompt-sec") { - if isPiped() { - return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") - } - sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) - if err != nil { - return "", nil, fmt.Errorf("failed to get secret key: %w", err) - } - } - - if strings.HasPrefix(sec, "ncryptsec1") { - sec, err = promptDecrypt(sec) - if err != nil { - return "", nil, fmt.Errorf("failed to decrypt: %w", err) - } - } else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil { - sec = hex.EncodeToString(bsec) - } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { - return "", nil, fmt.Errorf("invalid nsec: %w", err) - } else if prefix == "nsec" { - sec = hexvalue.(string) - } - - if ok := nostr.IsValid32ByteHex(sec); !ok { - return "", nil, fmt.Errorf("invalid secret key") - } - - return sec, nil, nil -} - -func promptDecrypt(ncryptsec string) (string, error) { - for i := 1; i < 4; i++ { - var attemptStr string - if i > 1 { - attemptStr = fmt.Sprintf(" [%d/3]", i) - } - password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil) - if err != nil { - return "", err - } - sec, err := nip49.Decrypt(ncryptsec, password) - if err != nil { - continue - } - return sec, nil - } - return "", fmt.Errorf("couldn't decrypt private key") -} - -func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { - config := &readline.Config{ - Stdout: color.Error, - Prompt: color.YellowString(msg), - InterruptPrompt: "^C", - DisableAutoSaveHistory: true, - EnableMask: true, - MaskRune: '*', - } - - rl, err := readline.NewEx(config) - if err != nil { - return "", err - } - - for { - answer, err := rl.Readline() - if err != nil { - return "", err - } - answer = strings.TrimSpace(answer) - if shouldAskAgain != nil && shouldAskAgain(answer) { - continue - } - return answer, err - } -} - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randString(n int) string { diff --git a/helpers_key.go b/helpers_key.go new file mode 100644 index 0000000..2afb0f5 --- /dev/null +++ b/helpers_key.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "encoding/hex" + "fmt" + "os" + "strings" + + "github.com/chzyer/readline" + "github.com/fatih/color" + "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/keyer" + "github.com/nbd-wtf/go-nostr/nip19" + "github.com/nbd-wtf/go-nostr/nip46" + "github.com/nbd-wtf/go-nostr/nip49" +) + +var defaultKeyFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "sec", + Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL", + DefaultText: "the key '1'", + Aliases: []string{"connect"}, + Category: CATEGORY_SIGNER, + }, + &cli.BoolFlag{ + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + Category: CATEGORY_SIGNER, + }, + &cli.StringFlag{ + Name: "connect-as", + Usage: "private key to when communicating with the bunker given on --connect", + DefaultText: "a random key", + Category: CATEGORY_SIGNER, + }, +} + +func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (keyer.Keyer, error) { + key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + if err != nil { + return nil, err + } + + var kr keyer.Keyer + if bunker != nil { + kr = keyer.NewBunkerSignerFromBunkerClient(bunker) + } else { + kr = keyer.NewPlainKeySigner(key) + } + + return kr, nil +} + +func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { + var err error + + sec := c.String("sec") + if strings.HasPrefix(sec, "bunker://") { + // it's a bunker + bunkerURL := sec + clientKey := c.String("connect-as") + if clientKey != "" { + clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey + } else { + clientKey = nostr.GeneratePrivateKey() + } + bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { + fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) + }) + return "", bunker, err + } + + // take private from flags, environment variable or default to 1 + if sec == "" { + if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok { + sec = key + } else { + sec = "0000000000000000000000000000000000000000000000000000000000000001" + } + } + + if c.Bool("prompt-sec") { + if isPiped() { + return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") + } + sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) + if err != nil { + return "", nil, fmt.Errorf("failed to get secret key: %w", err) + } + } + + if strings.HasPrefix(sec, "ncryptsec1") { + sec, err = promptDecrypt(sec) + if err != nil { + return "", nil, fmt.Errorf("failed to decrypt: %w", err) + } + } else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil { + sec = hex.EncodeToString(bsec) + } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { + return "", nil, fmt.Errorf("invalid nsec: %w", err) + } else if prefix == "nsec" { + sec = hexvalue.(string) + } + + if ok := nostr.IsValid32ByteHex(sec); !ok { + return "", nil, fmt.Errorf("invalid secret key") + } + + return sec, nil, nil +} + +func promptDecrypt(ncryptsec string) (string, error) { + for i := 1; i < 4; i++ { + var attemptStr string + if i > 1 { + attemptStr = fmt.Sprintf(" [%d/3]", i) + } + password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil) + if err != nil { + return "", err + } + sec, err := nip49.Decrypt(ncryptsec, password) + if err != nil { + continue + } + return sec, nil + } + return "", fmt.Errorf("couldn't decrypt private key") +} + +func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { + config := &readline.Config{ + Stdout: color.Error, + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + EnableMask: true, + MaskRune: '*', + } + + rl, err := readline.NewEx(config) + if err != nil { + return "", err + } + + for { + answer, err := rl.Readline() + if err != nil { + return "", err + } + answer = strings.TrimSpace(answer) + if shouldAskAgain != nil && shouldAskAgain(answer) { + continue + } + return answer, err + } +} diff --git a/relay.go b/relay.go index 066e78c..3e8ce9f 100644 --- a/relay.go +++ b/relay.go @@ -74,26 +74,7 @@ var relay = &cli.Command{ flags[i] = declareFlag(argName) } - flags = append(flags, - &cli.StringFlag{ - Name: "sec", - Usage: "secret key to sign the event, as nsec, ncryptsec or hex", - DefaultText: "the key '1'", - }, - &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the event", - }, - &cli.StringFlag{ - Name: "connect", - Usage: "sign event using NIP-46, expects a bunker://... URL", - }, - &cli.StringFlag{ - Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", - DefaultText: "a random key", - }, - ) + flags = append(flags, defaultKeyFlags...) cmd := &cli.Command{ Name: def.method, @@ -114,7 +95,7 @@ var relay = &cli.Command{ return nil } - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + kr, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } @@ -131,7 +112,7 @@ var relay = &cli.Command{ // Authorization payloadHash := sha256.Sum256(reqj) - authEvent := nostr.Event{ + tokenEvent := nostr.Event{ Kind: 27235, CreatedAt: nostr.Now(), Tags: nostr.Tags{ @@ -140,14 +121,10 @@ var relay = &cli.Command{ {"payload", hex.EncodeToString(payloadHash[:])}, }, } - if bunker != nil { - if err := bunker.SignEvent(ctx, &authEvent); err != nil { - return fmt.Errorf("failed to sign with bunker: %w", err) - } - } else if err := authEvent.Sign(sec); err != nil { - return fmt.Errorf("error signing with provided key: %w", err) + if err := kr.SignEvent(ctx, &tokenEvent); err != nil { + return fmt.Errorf("failed to sign token event: %w", err) } - evtj, _ := json.Marshal(authEvent) + evtj, _ := json.Marshal(tokenEvent) req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) // Content-Type diff --git a/req.go b/req.go index 303367b..49f5d91 100644 --- a/req.go +++ b/req.go @@ -31,93 +31,67 @@ it can also take a filter from stdin, optionally modify it with flags and send i example: echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`, DisableSliceFlagSeparator: true, - Flags: append(reqFilterFlags, - &cli.BoolFlag{ - Name: "stream", - Usage: "keep the subscription open, printing all events as they are returned", - DefaultText: "false, will close on EOSE", - }, - &cli.BoolFlag{ - Name: "paginate", - Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met", - DefaultText: "false", - }, - &cli.DurationFlag{ - Name: "paginate-interval", - Usage: "time between queries when using --paginate", - }, - &cli.UintFlag{ - Name: "paginate-global-limit", - Usage: "global limit at which --paginate should stop", - DefaultText: "uses the value given by --limit/-l or infinite", - }, - &cli.BoolFlag{ - Name: "bare", - Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", - }, - &cli.BoolFlag{ - Name: "auth", - Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", - }, - &cli.BoolFlag{ - Name: "force-pre-auth", - Aliases: []string{"fpa"}, - Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "sec", - Usage: "secret key to sign the AUTH challenge, as hex or nsec", - DefaultText: "the key '1'", - Category: CATEGORY_SIGNER, - }, - &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect", - Usage: "sign AUTH using NIP-46, expects a bunker://... URL", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", - DefaultText: "a random key", - Category: CATEGORY_SIGNER, - }, + Flags: append(defaultKeyFlags, + append(reqFilterFlags, + &cli.BoolFlag{ + Name: "stream", + Usage: "keep the subscription open, printing all events as they are returned", + DefaultText: "false, will close on EOSE", + }, + &cli.BoolFlag{ + Name: "paginate", + Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met", + DefaultText: "false", + }, + &cli.DurationFlag{ + Name: "paginate-interval", + Usage: "time between queries when using --paginate", + }, + &cli.UintFlag{ + Name: "paginate-global-limit", + Usage: "global limit at which --paginate should stop", + DefaultText: "uses the value given by --limit/-l or infinite", + }, + &cli.BoolFlag{ + Name: "bare", + Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", + }, + &cli.BoolFlag{ + Name: "auth", + Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + }, + &cli.BoolFlag{ + Name: "force-pre-auth", + Aliases: []string{"fpa"}, + Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", + Category: CATEGORY_SIGNER, + }, + )..., ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.Args().Slice() if len(relayUrls) > 0 { - relays := connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error { - if !c.Bool("auth") && !c.Bool("force-pre-auth") { - return fmt.Errorf("auth not authorized") - } - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) - if err != nil { - return err - } + relays := connectToAllRelays(ctx, + relayUrls, + c.Bool("force-pre-auth"), + nostr.WithAuthHandler( + func(authEvent *nostr.Event) error { + if !c.Bool("auth") && !c.Bool("force-pre-auth") { + return fmt.Errorf("auth not authorized") + } + kr, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } - var pk string - if bunker != nil { - pk, err = bunker.GetPublicKey(ctx) - if err != nil { - return fmt.Errorf("failed to get public key from bunker: %w", err) - } - } else { - pk, _ = nostr.GetPublicKey(sec) - } - log("performing auth as %s... ", pk) + pk := kr.GetPublicKey(ctx) + log("performing auth as %s... ", pk) - if bunker != nil { - return bunker.SignEvent(ctx, evt) - } else { - return evt.Sign(sec) - } - })) + return kr.SignEvent(ctx, authEvent) + }, + ), + ) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3)