diff --git a/go.mod b/go.mod index 4949687..4b72b35 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.21 toolchain go1.21.0 require ( - github.com/bgentry/speakeasy v0.1.0 + 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 @@ -18,7 +19,6 @@ 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/readline v0.0.0-20180603132655-2972be24d48e // 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 @@ -27,6 +27,8 @@ require ( github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.17.0 // indirect diff --git a/go.sum b/go.sum index 3b65c90..29a9176 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= @@ -44,6 +42,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -76,6 +76,11 @@ 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/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= @@ -133,6 +138,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/helpers.go b/helpers.go index 5cf95f7..ddd0bd1 100644 --- a/helpers.go +++ b/helpers.go @@ -3,14 +3,17 @@ package main import ( "bufio" "context" + "encoding/hex" "fmt" "net/url" "os" "strings" - "github.com/bgentry/speakeasy" + "github.com/chzyer/readline" + "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" + "github.com/nbd-wtf/go-nostr/nip49" "github.com/urfave/cli/v2" ) @@ -128,31 +131,90 @@ func exitIfLineProcessingError(c *cli.Context) { } func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { + var err error 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") } - var err error - sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ") + 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) } } - if strings.HasPrefix(sec, "nsec1") { - _, hex, err := nip19.Decode(sec) + if strings.HasPrefix(sec, "ncryptsec1") { + sec, err = promptDecrypt(sec) if err != nil { - return "", fmt.Errorf("invalid nsec: %w", err) + return "", fmt.Errorf("failed to decrypt: %w", err) } - sec = hex.(string) + } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { + return "", fmt.Errorf("invalid nsec: %w", err) + } else if prefix == "nsec" { + sec = hexvalue.(string) + } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { + sec = hex.EncodeToString(bsec) } - if len(sec) > 64 { - return "", fmt.Errorf("invalid secret key: too large") - } - sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad + if ok := nostr.IsValid32ByteHex(sec); !ok { return "", fmt.Errorf("invalid secret key") } - return sec, nil } + +func promptDecrypt(ncryptsec1 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(ncryptsec1, password) + if err != nil { + continue + } + return sec, nil + } + return "", fmt.Errorf("couldn't decrypt private key") +} + +func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { + return _ask(&readline.Config{ + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + }, msg, defaultValue, shouldAskAgain) +} + +func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { + config := &readline.Config{ + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + EnableMask: true, + MaskRune: '*', + } + return _ask(config, msg, "", shouldAskAgain) +} + +func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { + rl, err := readline.NewEx(config) + if err != nil { + return "", err + } + + rl.WriteStdin([]byte(defaultValue)) + for { + answer, err := rl.Readline() + if err != nil { + return "", err + } + answer = strings.TrimSpace(strings.ToLower(answer)) + if shouldAskAgain != nil && shouldAskAgain(answer) { + continue + } + return answer, err + } +}