Compare commits

...

6 Commits

Author SHA1 Message Date
fiatjaf
5dd5a7c699 bunker: better colors and prompts. 2024-02-12 15:39:13 -03:00
fiatjaf
347a82eaa9 verify: accept event to be verified as json argument. 2024-02-12 15:38:43 -03:00
fiatjaf
e89823b10e ensure at least one blank line will be emitted when piped. 2024-02-06 12:47:58 -03:00
fiatjaf
6626001dd2 --connect-as to specify client pubkey when using --connect to bunker 2024-02-06 12:47:46 -03:00
fiatjaf
b7a7e0504f --connect to use nip46 as a client to sign event and auth messages. 2024-02-06 00:58:26 -03:00
mattn
01e1f52a70 fix panic (#12)
* c.Args().Len() - 1 might be negative value

* fix getStdinLinesOrFirstArgument

* fix getStdinLinesOrFirstArgument
2024-02-03 10:03:32 -03:00
11 changed files with 154 additions and 71 deletions

View File

@@ -6,7 +6,7 @@ import (
"net/url"
"os"
"github.com/manifoldco/promptui"
"github.com/fatih/color"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46"
@@ -56,7 +56,7 @@ var bunker = &cli.Command{
}
// gather the secret key
sec, err := gatherSecretKeyFromArguments(c)
sec, _, err := gatherSecretKeyOrBunkerFromArguments(c)
if err != nil {
return err
}
@@ -65,12 +65,12 @@ var bunker = &cli.Command{
return err
}
npub, _ := nip19.EncodePublicKey(pubkey)
log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n %sbunker:%s %s\n\n",
BOLD_ON, relayURLs, BOLD_OFF,
BOLD_ON, BOLD_OFF, pubkey,
BOLD_ON, BOLD_OFF, npub,
BOLD_ON, BOLD_OFF, fmt.Sprintf("%s#secret?%s", npub, qs.Encode()),
BOLD_ON, BOLD_OFF, fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()),
bold := color.New(color.Bold).Sprint
log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n",
bold(relayURLs),
bold(pubkey),
bold(npub),
bold(fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())),
)
alwaysYes := c.Bool("yes")
@@ -93,18 +93,22 @@ var bunker = &cli.Command{
}
jreq, _ := json.MarshalIndent(req, " ", " ")
log("- got request from '%s': %s\n", ie.Event.PubKey, string(jreq))
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq))
jresp, _ := json.MarshalIndent(resp, " ", " ")
log("~ responding with %s\n", string(jresp))
if alwaysYes || harmless || askProceed(ie.Event.PubKey) {
if err := ie.Relay.Publish(c.Context, eventResponse); err == nil {
log("* sent response!\n")
for _, relayURL := range relayURLs {
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
if err := relay.Publish(c.Context, eventResponse); err == nil {
log("* sent response through %s\n", relay.URL)
} else {
log("* failed to send response: %s\n", err)
}
}
}
}
}
return nil
},
@@ -117,21 +121,23 @@ func askProceed(source string) bool {
return true
}
prompt := promptui.Select{
Label: "proceed?",
Items: []string{
"no",
"yes",
"always from this source",
},
}
n, _, _ := prompt.Run()
switch n {
case 0:
return false
case 1:
fmt.Fprintf(os.Stderr, "request from %s:\n", color.New(color.Bold, color.FgBlue).Sprint(source))
res, err := ask(" proceed to fulfill this request? (yes/no/always from this) (y/n/a): ", "",
func(answer string) bool {
if answer != "y" && answer != "n" && answer != "a" {
return true
case 2:
}
return false
})
if err != nil {
return false
}
switch res {
case "n":
return false
case "y":
return true
case "a":
allowedSources = append(allowedSources, source)
return true
}

View File

@@ -33,7 +33,7 @@ var decode = &cli.Command{
},
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
Action: func(c *cli.Context) error {
for input := range getStdinLinesOrFirstArgument(c) {
for input := range getStdinLinesOrFirstArgument(c.Args().First()) {
if strings.HasPrefix(input, "nostr:") {
input = input[6:]
}

View File

@@ -29,7 +29,7 @@ var encode = &cli.Command{
Name: "npub",
Usage: "encode a hex public key into bech32 'npub' format",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
if ok := nostr.IsValidPublicKey(target); !ok {
lineProcessingError(c, "invalid public key: %s", target)
continue
@@ -50,7 +50,7 @@ var encode = &cli.Command{
Name: "nsec",
Usage: "encode a hex private key into bech32 'nsec' format",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid private key: %s", target)
continue
@@ -78,7 +78,7 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid public key: %s", target)
continue
@@ -115,7 +115,7 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target)
continue
@@ -212,7 +212,7 @@ var encode = &cli.Command{
Name: "note",
Usage: "generate note1 event codes (not recommended)",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target)
continue

View File

@@ -44,6 +44,15 @@ example:
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",
},
&cli.BoolFlag{
Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
@@ -124,8 +133,7 @@ example:
}
}()
// gather the secret key
sec, err := gatherSecretKeyFromArguments(c)
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
if err != nil {
return err
}
@@ -215,7 +223,11 @@ example:
}
if evt.Sig == "" || mustRehashAndResign {
if err := evt.Sign(sec); err != nil {
if bunker != nil {
if err := bunker.SignEvent(c.Context, &evt); err != nil {
return fmt.Errorf("failed to sign with bunker: %w", err)
}
} else if err := evt.Sign(sec); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
}
}
@@ -252,11 +264,24 @@ example:
}
// error publishing
if strings.HasPrefix(err.Error(), "msg: auth-required:") && sec != "" && doAuth {
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
pk, _ := nostr.GetPublicKey(sec)
var pk string
if bunker != nil {
pk, err = bunker.GetPublicKey(c.Context)
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(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }); err == nil {
if err := relay.Auth(c.Context, func(evt *nostr.Event) error {
if bunker != nil {
return bunker.SignEvent(c.Context, evt)
}
return evt.Sign(sec)
}); err == nil {
// try to publish again, but this time don't try to auth again
doAuth = false
goto publish

View File

@@ -31,7 +31,7 @@ var fetch = &cli.Command{
})
}()
for code := range getStdinLinesOrFirstArgument(c) {
for code := range getStdinLinesOrFirstArgument(c.Args().First()) {
filter := nostr.Filter{}
prefix, value, err := nip19.Decode(code)

5
go.mod
View File

@@ -8,8 +8,7 @@ require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/fatih/color v1.16.0
github.com/mailru/easyjson v0.7.7
github.com/manifoldco/promptui v0.9.0
github.com/nbd-wtf/go-nostr v0.28.2
github.com/nbd-wtf/go-nostr v0.28.6
github.com/nbd-wtf/nostr-sdk v0.0.5
github.com/urfave/cli/v2 v2.25.7
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
@@ -19,6 +18,8 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect

7
go.sum
View File

@@ -74,15 +74,13 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
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.28.2 h1:KhpGcs6KMLBqYExzKoqt7vP5Re2f8Kpy9SavYZa2PTI=
github.com/nbd-wtf/go-nostr v0.28.2/go.mod h1:l9NRRaHPN+QwkqrjNKhnfYjQ0+nKP1xZrVxePPGUs+A=
github.com/nbd-wtf/go-nostr v0.28.6 h1:iOyzk+6ReG0lvyRAar7w7omFmUk5mnXDyFYkJ+zEjiw=
github.com/nbd-wtf/go-nostr v0.28.6/go.mod h1:aFcp8NO3erHg+glzBfh4wpaMrV1/ahcUPAgITdptxwA=
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -129,7 +127,6 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -13,15 +13,13 @@ import (
"github.com/fatih/color"
"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/urfave/cli/v2"
)
const (
LINE_PROCESSING_ERROR = iota
BOLD_ON = "\033[1m"
BOLD_OFF = "\033[21m"
)
var log = func(msg string, args ...any) {
@@ -47,12 +45,11 @@ func getStdinLinesOrBlank() chan string {
}
}
func getStdinLinesOrFirstArgument(c *cli.Context) chan string {
func getStdinLinesOrFirstArgument(arg string) chan string {
// try the first argument
target := c.Args().First()
if target != "" {
if arg != "" {
single := make(chan string, 1)
single <- target
single <- arg
close(single)
return single
}
@@ -69,8 +66,13 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
go func() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 16*1024), 256*1024)
hasEmittedAtLeastOne := false
for scanner.Scan() {
ch <- strings.TrimSpace(scanner.Text())
hasEmittedAtLeastOne = true
}
if !hasEmittedAtLeastOne {
ch <- ""
}
close(ch)
}()
@@ -130,35 +132,46 @@ func exitIfLineProcessingError(c *cli.Context) {
}
}
func gatherSecretKeyFromArguments(c *cli.Context) (string, error) {
func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (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(c.Context, clientKey, bunkerURL, nil)
return "", bunker, err
}
sec := c.String("sec")
if c.Bool("prompt-sec") {
if isPiped() {
return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
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 "", fmt.Errorf("failed to get secret key: %w", err)
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
}
}
if strings.HasPrefix(sec, "ncryptsec1") {
sec, err = promptDecrypt(sec)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
}
} else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil {
sec = hex.EncodeToString(bsec)
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
return "", fmt.Errorf("invalid nsec: %w", err)
return "", nil, fmt.Errorf("invalid nsec: %w", err)
} else if prefix == "nsec" {
sec = hexvalue.(string)
}
if ok := nostr.IsValid32ByteHex(sec); !ok {
return "", fmt.Errorf("invalid secret key")
return "", nil, fmt.Errorf("invalid secret key")
}
return sec, nil
return sec, nil, nil
}
func promptDecrypt(ncryptsec1 string) (string, error) {

32
key.go
View File

@@ -39,7 +39,7 @@ var public = &cli.Command{
Description: ``,
ArgsUsage: "[secret]",
Action: func(c *cli.Context) error {
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c) {
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, c.Args().First()) {
pubkey, err := nostr.GetPublicKey(sec)
if err != nil {
lineProcessingError(c, "failed to derive public key: %s", err)
@@ -65,11 +65,20 @@ var encrypt = &cli.Command{
},
},
Action: func(c *cli.Context) error {
password := c.Args().Get(c.Args().Len() - 1)
var content string
var password string
switch c.Args().Len() {
case 1:
content = ""
password = c.Args().Get(0)
case 2:
content = c.Args().Get(0)
password = c.Args().Get(1)
}
if password == "" {
return fmt.Errorf("no password given")
}
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c) {
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, content) {
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
if err != nil {
lineProcessingError(c, "failed to encrypt: %s", err)
@@ -87,11 +96,20 @@ var decrypt = &cli.Command{
Description: `uses the NIP-49 standard.`,
ArgsUsage: "<ncryptsec-code> <password>",
Action: func(c *cli.Context) error {
password := c.Args().Get(c.Args().Len() - 1)
var content string
var password string
switch c.Args().Len() {
case 1:
content = ""
password = c.Args().Get(0)
case 2:
content = c.Args().Get(0)
password = c.Args().Get(1)
}
if password == "" {
return fmt.Errorf("no password given")
}
for ncryptsec := range getStdinLinesOrFirstArgument(c) {
for ncryptsec := range getStdinLinesOrFirstArgument(content) {
sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil {
lineProcessingError(c, "failed to decrypt: %s", err)
@@ -104,10 +122,10 @@ var decrypt = &cli.Command{
},
}
func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context) chan string {
func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context, content string) chan string {
ch := make(chan string)
go func() {
for sec := range getStdinLinesOrFirstArgument(c) {
for sec := range getStdinLinesOrFirstArgument(content) {
if sec == "" {
continue
}

27
req.go
View File

@@ -113,6 +113,15 @@ example:
Name: "prompt-sec",
Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge",
},
&cli.StringFlag{
Name: "connect",
Usage: "sign AUTH 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",
},
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
@@ -124,13 +133,27 @@ example:
if !c.Bool("auth") {
return fmt.Errorf("auth not authorized")
}
sec, err := gatherSecretKeyFromArguments(c)
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
if err != nil {
return err
}
pk, _ := nostr.GetPublicKey(sec)
var pk string
if bunker != nil {
pk, err = bunker.GetPublicKey(c.Context)
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...\n", pk)
if bunker != nil {
return bunker.SignEvent(c.Context, evt)
} else {
return evt.Sign(sec)
}
}))
if len(relays) == 0 {
log("failed to connect to any of the given relays.\n")

View File

@@ -15,7 +15,7 @@ var verify = &cli.Command{
it outputs nothing if the verification is successful.`,
Action: func(c *cli.Context) error {
for stdinEvent := range getStdinLinesOrBlank() {
for stdinEvent := range getStdinLinesOrFirstArgument(c.Args().First()) {
evt := nostr.Event{}
if stdinEvent != "" {
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {