Skip to content

Commit

Permalink
Add -f flag to gopass create
Browse files Browse the repository at this point in the history
This new flag allows overriding the default secret name
computation with a custom secret name passed as the first
argument.

Fixes #1811

RELEASE_NOTES=[ENHANCEMENT] Add -f flag to create

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
  • Loading branch information
dominikschulz committed Mar 22, 2021
1 parent 51595ce commit b18e348
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 59 deletions.
5 changes: 5 additions & 0 deletions internal/action/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ func (s *Action) GetCommands() []*cli.Command {
Aliases: []string{"s"},
Usage: "Which store to use",
},
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Force path selection",
},
},
},
{
Expand Down
147 changes: 92 additions & 55 deletions internal/action/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/pkg/clipboard"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/fsutil"
"github.com/gopasspw/gopass/pkg/gopass/secrets"
"github.com/gopasspw/gopass/pkg/pwgen"
Expand All @@ -23,13 +24,19 @@ import (

func fmtfn(d int, n string, t string) string {
strlen := 40 - d
return fmt.Sprintf("%"+strconv.Itoa(d)+"s%s %-"+strconv.Itoa(strlen)+"s", "", color.GreenString("["+n+"]"), color.CyanString(t))
// indent - [N] - text (trailing spaces)
fmtStr := "%" + strconv.Itoa(d) + "s%s %-" + strconv.Itoa(strlen) + "s"
debug.Log("d: %d, n: %q, t: %q, strlen: %d, fmtStr: %q", d, n, t, strlen, fmtStr)
return fmt.Sprintf(fmtStr, "", color.GreenString("["+n+"]"), t)
}

// Create displays the password creation wizard
func (s *Action) Create(c *cli.Context) error {
ctx := ctxutil.WithGlobalFlags(c)

out.Printf(ctx, "🌟 Welcome to the secret creation wizard (gopass create)!")
out.Printf(ctx, "🧪 Hint: Use 'gopass edit -c' for more control!")

acts := make(cui.Actions, 0, 5)
acts = append(acts, cui.Action{Name: "Website Login", Fn: s.createWebsite})
acts = append(acts, cui.Action{Name: "PIN Code (numerical)", Fn: s.createPIN})
Expand Down Expand Up @@ -69,17 +76,12 @@ func extractHostname(in string) string {

// createWebsite walks through the website credential creation wizard
func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error {
var (
urlStr = c.Args().Get(0)
username = c.Args().Get(1)
password string
comment string
store = c.String("store")
err error
genPw bool
)
out.Printf(ctx, "=> Creating Website login")
urlStr, err = termio.AskForString(ctx, fmtfn(2, "1", "URL"), urlStr)
name := c.Args().First()
store := c.String("store")
force := c.Bool("force")

out.Print(ctx, "🧪 Creating Website login")
urlStr, err := termio.AskForString(ctx, fmtfn(2, "1", "URL"), "")
if err != nil {
return err
}
Expand All @@ -89,16 +91,17 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error {
return ExitError(ExitUnknown, err, "Can not parse URL %q. Please use 'gopass edit' to manually create the secret", urlStr)
}

username, err = termio.AskForString(ctx, fmtfn(2, "2", "Login"), username)
username, err := termio.AskForString(ctx, fmtfn(2, "2", "Login"), "")
if err != nil {
return err
}

genPw, err = termio.AskForBool(ctx, fmtfn(2, "3", "Generate Password?"), true)
genPw, err := termio.AskForBool(ctx, fmtfn(2, "3", "Generate Password?"), true)
if err != nil {
return err
}

var password string
if genPw {
password, err = s.createGeneratePassword(ctx, hostname)
if err != nil {
Expand All @@ -110,7 +113,12 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error {
return err
}
}
comment, _ = termio.AskForString(ctx, fmtfn(2, "4", "Comments"), "")

comment, err := termio.AskForString(ctx, fmtfn(2, "4", "Comments"), "")
if err != nil {
debug.Log("failed to read comment input: %s", err)
// ignore the error, comments are considered optional
}

// select store
if store == "" {
Expand All @@ -122,14 +130,22 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error {
store += "/"
}

name := fmt.Sprintf("%swebsites/%s/%s", store, fsutil.CleanFilename(hostname), fsutil.CleanFilename(username))
if s.Store.Exists(ctx, name) {
name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists, please choose another path"), name)
// by default create will generate a name for the secret based on the user
// input. Only when the force flag is given it will accept a secrets path
// as the first argument.
if name == "" && !force {
name = fmt.Sprintf("%swebsites/%s/%s", store, fsutil.CleanFilename(hostname), fsutil.CleanFilename(username))
}

// force will also override the check for existing entries
if s.Store.Exists(ctx, name) && !force {
name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists. Choose another path or enter to overwrite"), name)
if err != nil {
return err
}
}

// populate a new secret with the gathered information
sec := secrets.New()
sec.SetPassword(password)
sec.Set("url", urlStr)
Expand All @@ -141,6 +157,7 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error {
if err := s.Store.Set(ctxutil.WithCommitMessage(ctx, "Created new entry"), name, sec); err != nil {
return ExitError(ExitEncrypt, err, "failed to set %q: %s", name, err)
}
out.OKf(ctx, "Credentials saved to %q", name)

return s.createPrintOrCopy(ctx, c, name, password, genPw)
}
Expand All @@ -152,11 +169,7 @@ func (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, pa
}

if c.Bool("print") {
fmt.Fprintf(
out.Stdout,
"The generated password for %s is:\n%s\n", name,
color.YellowString(password),
)
fmt.Fprintf(out.Stdout, "The generated password for %s is:\n%s\n", name, password)
return nil
}

Expand All @@ -168,34 +181,33 @@ func (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, pa

// createPIN will walk through the numerical password (PIN) wizard
func (s *Action) createPIN(ctx context.Context, c *cli.Context) error {
var (
authority = c.Args().Get(0)
application = c.Args().Get(1)
password string
comment string
store = c.String("store")
err error
genPw bool
)
out.Printf(ctx, "=> Creating numerical PIN ...")
authority, err = termio.AskForString(ctx, fmtfn(2, "1", "Authority"), authority)
name := c.Args().First()
store := c.String("store")
force := c.Bool("force")

out.Printf(ctx, "🧪 Creating numerical PIN ...")
authority, err := termio.AskForString(ctx, fmtfn(2, "1", "Authority"), "")
if err != nil {
return err
}
if authority == "" {
return ExitError(ExitUnknown, nil, "Authority must not be empty")
}
application, err = termio.AskForString(ctx, fmtfn(2, "2", "Entity"), application)

application, err := termio.AskForString(ctx, fmtfn(2, "2", "Entity"), "")
if err != nil {
return err
}
if application == "" {
return ExitError(ExitUnknown, nil, "Application must not be empty")
}
genPw, err = termio.AskForBool(ctx, fmtfn(2, "3", "Generate PIN?"), false)

genPw, err := termio.AskForBool(ctx, fmtfn(2, "3", "Generate PIN?"), false)
if err != nil {
return err
}

var password string
if genPw {
password, err = s.createGeneratePIN(ctx)
if err != nil {
Expand All @@ -207,7 +219,12 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error {
return err
}
}
comment, _ = termio.AskForString(ctx, fmtfn(2, "4", "Comments"), "")

comment, err := termio.AskForString(ctx, fmtfn(2, "4", "Comments"), "")
if err != nil {
debug.Log("failed to read comment input: %s", err)
// ignore the error, comments are considered optional
}

// select store
if store == "" {
Expand All @@ -218,45 +235,55 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error {
if store != "" {
store += "/"
}
name := fmt.Sprintf("%spins/%s/%s", store, fsutil.CleanFilename(authority), fsutil.CleanFilename(application))
if s.Store.Exists(ctx, name) {
name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists, please choose another path"), name)

// by default create will generate a name for the secret based on the user
// input. Only when the force flag is given it will accept a secrets path
// as the first argument.
if name == "" && !force {
name = fmt.Sprintf("%spins/%s/%s", store, fsutil.CleanFilename(authority), fsutil.CleanFilename(application))
}

// force will also override the check for existing entries
if s.Store.Exists(ctx, name) && !force {
name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists. Choose another path or enter to overwrite"), name)
if err != nil {
return err
}
}

sec := secrets.New()
sec.SetPassword(password)
sec.Set("application", application)
sec.Set("comment", comment)
if err := s.Store.Set(ctxutil.WithCommitMessage(ctx, "Created new entry"), name, sec); err != nil {
return ExitError(ExitEncrypt, err, "failed to set %q: %s", name, err)
}
out.OKf(ctx, "Credentials saved to %q", name)

return s.createPrintOrCopy(ctx, c, name, password, genPw)
}

// createGeneric will walk through the generic secret wizard
func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error {
var (
shortname = c.Args().Get(0)
password string
store = c.String("store")
err error
genPw bool
)
out.Printf(ctx, "=> Creating generic secret ...")
shortname, err = termio.AskForString(ctx, fmtfn(2, "1", "Name"), shortname)
name := c.Args().Get(0)
store := c.String("store")
force := c.Bool("force")

out.Printf(ctx, "🧪 Creating generic secret ...")
shortname, err := termio.AskForString(ctx, fmtfn(2, "1", "Name"), "")
if err != nil {
return err
}
if shortname == "" {
return ExitError(ExitUnknown, nil, "Name must not be empty")
}
genPw, err = termio.AskForBool(ctx, fmtfn(2, "2", "Generate password?"), true)

genPw, err := termio.AskForBool(ctx, fmtfn(2, "2", "Generate password?"), true)
if err != nil {
return err
}

var password string
if genPw {
password, err = s.createGeneratePassword(ctx, "")
if err != nil {
Expand All @@ -278,13 +305,22 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error {
if store != "" {
store += "/"
}
name := fmt.Sprintf("%smisc/%s", store, fsutil.CleanFilename(shortname))
if s.Store.Exists(ctx, name) {
name, err = termio.AskForString(ctx, "Secret already exists, please choose another path", name)

// by default create will generate a name for the secret based on the user
// input. Only when the force flag is given it will accept a secrets path
// as the first argument.
if name == "" && !force {
name = fmt.Sprintf("%smisc/%s/%s", store, fsutil.CleanFilename(shortname))
}

// force will also override the check for existing entries
if s.Store.Exists(ctx, name) && !force {
name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists. Choose another path or enter to overwrite"), name)
if err != nil {
return err
}
}

sec := secrets.New()
sec.SetPassword(password)
out.Printf(ctx, fmtfn(2, "3", "Enter zero or more key value pairs for this secret:"))
Expand All @@ -305,21 +341,22 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error {
if err := s.Store.Set(ctxutil.WithCommitMessage(ctx, "Created new entry"), name, sec); err != nil {
return ExitError(ExitEncrypt, err, "failed to set %q: %s", name, err)
}
out.OKf(ctx, "Credentials saved to %q", name)

return s.createPrintOrCopy(ctx, c, name, password, genPw)
}

// createGeneratePasssword will walk through the password generation steps
func (s *Action) createGeneratePassword(ctx context.Context, hostname string) (string, error) {
if _, found := pwrules.LookupRule(hostname); found {
out.Printf(ctx, "Using password rules for %s ...", hostname)
out.Noticef(ctx, "Using password rules for %s ...", hostname)
length, err := termio.AskForInt(ctx, fmtfn(4, "b", "How long?"), defaultLength)
if err != nil {
return "", err
}
return pwgen.NewCrypticForDomain(length, hostname).Password(), nil
}
xkcd, err := termio.AskForBool(ctx, fmtfn(4, "a", "Human-pronounceable passphrase? (see https://xkcd.com/936/)"), false)
xkcd, err := termio.AskForBool(ctx, fmtfn(4, "a", "Human-pronounceable passphrase?"), false)
if err != nil {
return "", err
}
Expand Down
6 changes: 3 additions & 3 deletions internal/action/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (s *Action) initGenerateIdentity(ctx context.Context, crypto backend.Crypto
return fmt.Errorf("failed to list private keys: %w", err)
}
if len(kl) > 1 {
out.Noticef(ctx, "More than one private key detected. Make sure to use the correct one!")
out.Notice(ctx, "More than one private key detected. Make sure to use the correct one!")
return nil
}
if len(kl) < 1 {
Expand All @@ -145,7 +145,7 @@ func (s *Action) initGenerateIdentity(ctx context.Context, crypto backend.Crypto
if err := s.initExportPublicKey(ctx, crypto, kl[0]); err != nil {
return err
}
out.OKf(ctx, "Key pair validated")
out.OK(ctx, "Key pair validated")
return nil
}

Expand Down Expand Up @@ -239,7 +239,7 @@ func (s *Action) initLocal(ctx context.Context) error {
return fmt.Errorf("failed to save config: %w", err)
}

out.OKf(ctx, "Configured")
out.OK(ctx, "Configured")
return nil
}

Expand Down
4 changes: 3 additions & 1 deletion internal/cui/cui.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"

"github.com/fatih/color"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/termio"
)
Expand All @@ -17,7 +18,8 @@ func GetSelection(ctx context.Context, prompt string, choices []string) (string,
}

for i, c := range choices {
fmt.Printf("[% d] %s\n", i, c)
fmt.Printf(color.GreenString("[% d]", i))
fmt.Printf(" %s\n", c)
}
fmt.Println()
var i int
Expand Down

0 comments on commit b18e348

Please sign in to comment.