Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add -f flag to gopass create #1867

Merged
merged 1 commit into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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", 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.Print(color.GreenString("[% d]", i))
fmt.Printf(" %s\n", c)
}
fmt.Println()
var i int
Expand Down