diff --git a/action/clihelper.go b/action/clihelper.go index 83f5a78f52..17f4ded4b7 100644 --- a/action/clihelper.go +++ b/action/clihelper.go @@ -28,6 +28,13 @@ func (s *Action) ConfirmRecipients(ctx context.Context, name string, recipients fmt.Printf("gopass: Encrypting %s for these recipients:\n", name) for _, r := range recipients { + // check for context cancelation + select { + case <-ctx.Done(): + return nil, errors.New("user aborted") + default: + } + kl, err := s.gpg.FindPublicKeys(ctx, r) if err != nil { fmt.Println(color.RedString("Failed to read public key for '%s': %s", name, err)) @@ -41,7 +48,7 @@ func (s *Action) ConfirmRecipients(ctx context.Context, name string, recipients } fmt.Println("") - yes, err := s.askForBool("Do you want to continue?", true) + yes, err := s.askForBool(ctx, "Do you want to continue?", true) if err != nil { return recipients, errors.Wrapf(err, "failed to read user input") } @@ -61,7 +68,7 @@ func (s *Action) AskForConfirmation(ctx context.Context, text string) bool { } for i := 0; i < maxTries; i++ { - if choice, err := s.askForBool(text, false); err == nil { + if choice, err := s.askForBool(ctx, text, false); err == nil { return choice } } @@ -71,13 +78,13 @@ func (s *Action) AskForConfirmation(ctx context.Context, text string) bool { // askForBool ask for a bool (yes or no) exactly once. // The empty answer uses the specified default, any other answer // is an error. -func (s *Action) askForBool(text string, def bool) (bool, error) { +func (s *Action) askForBool(ctx context.Context, text string, def bool) (bool, error) { choices := "y/N" if def { choices = "Y/n" } - str, err := s.askForString(text, choices) + str, err := s.askForString(ctx, text, choices) if err != nil { return false, errors.Wrapf(err, "failed to read user input") } @@ -101,7 +108,14 @@ func (s *Action) askForBool(text string, def bool) (bool, error) { // askForString asks for a string once, using the default if the // anser is empty. Errors are only returned on I/O errors -func (s *Action) askForString(text, def string) (string, error) { +func (s *Action) askForString(ctx context.Context, text, def string) (string, error) { + // check for context cancelation + select { + case <-ctx.Done(): + return "", errors.New("user aborted") + default: + } + reader := bufio.NewReader(os.Stdin) fmt.Printf("%s [%s]: ", text, def) @@ -118,8 +132,8 @@ func (s *Action) askForString(text, def string) (string, error) { // askForInt asks for an valid interger once. If the input // can not be converted to an int it returns an error -func (s *Action) askForInt(text string, def int) (int, error) { - str, err := s.askForString(text, strconv.Itoa(def)) +func (s *Action) askForInt(ctx context.Context, text string, def int) (int, error) { + str, err := s.askForString(ctx, text, strconv.Itoa(def)) if err != nil { return 0, err } @@ -143,6 +157,13 @@ func (s *Action) askForPassword(ctx context.Context, name string, askFn func(con askFn = s.promptPass } for i := 0; i < maxTries; i++ { + // check for context cancelation + select { + case <-ctx.Done(): + return "", errors.New("user aborted") + default: + } + pass, err := askFn(ctx, fmt.Sprintf("Enter password for %s", name)) if err != nil { return "", err @@ -171,7 +192,7 @@ func (s *Action) AskForKeyImport(ctx context.Context, key string) bool { return false } - ok, err := s.askForBool(fmt.Sprintf("Do you want to import the public key '%s' into your keyring?", key), false) + ok, err := s.askForBool(ctx, fmt.Sprintf("Do you want to import the public key '%s' into your keyring?", key), false) if err != nil { return false } @@ -196,12 +217,18 @@ func (s *Action) askForPrivateKey(ctx context.Context, prompt string) (string, e if ctxutil.IsAlwaysYes(ctx) { return kl[0].Fingerprint, nil } + // check for context cancelation + select { + case <-ctx.Done(): + return "", errors.New("user aborted") + default: + } fmt.Println(prompt) for i, k := range kl { fmt.Printf("[%d] %s\n", i, k.OneLine()) } - iv, err := s.askForInt(fmt.Sprintf("Please enter the number of a key (0-%d)", len(kl)-1), 0) + iv, err := s.askForInt(ctx, fmt.Sprintf("Please enter the number of a key (0-%d)", len(kl)-1), 0) if err != nil { continue } @@ -243,7 +270,8 @@ func (s *Action) askForGitConfigUser(ctx context.Context) (string, string, error } useCurrent, err = s.askForBool( - fmt.Sprintf("Use %s (%s) for password store git config?", identity.Name, identity.Email), false) + ctx, + fmt.Sprintf("Use %s (%s) for password store git config?", identity.Name, identity.Email), true) if err != nil { return "", "", err } diff --git a/action/create.go b/action/create.go index e7dcc07598..efc34f5a25 100644 --- a/action/create.go +++ b/action/create.go @@ -58,7 +58,7 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { err error genPw bool ) - urlStr, err = s.askForString("Please enter the URL", "") + urlStr, err = s.askForString(ctx, "Please enter the URL", "") if err != nil { return err } @@ -66,11 +66,11 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { if err != nil { return errors.Wrapf(err, "Can not parse URL. Please use 'gopass edit' to manually create the secret") } - username, err = s.askForString("Please enter the Username/Login", "") + username, err = s.askForString(ctx, "Please enter the Username/Login", "") if err != nil { return err } - genPw, err = s.askForBool("Do you want to generate a new password?", true) + genPw, err = s.askForBool(ctx, "Do you want to generate a new password?", true) if err != nil { return err } @@ -85,7 +85,7 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { return err } } - comment, _ = s.askForString("Comments (optional)", "") + comment, _ = s.askForString(ctx, "Comments (optional)", "") // select store stores := []string{""} stores = append(stores, s.Store.MountPoints()...) @@ -105,7 +105,7 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { } name := fmt.Sprintf("%swebsites/%s/%s", store, u.Hostname(), username) if s.Store.Exists(ctx, name) { - name, err = s.askForString("Secret already exists, please choose another path", name) + name, err = s.askForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } @@ -136,15 +136,15 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { err error genPw bool ) - authority, err = s.askForString("Please enter the authoriy (e.g. MyBank) this PIN is for", "") + authority, err = s.askForString(ctx, "Please enter the authoriy (e.g. MyBank) this PIN is for", "") if err != nil { return err } - application, err = s.askForString("Please enter the entity (e.g. Credit Card) this PIN is for", "") + application, err = s.askForString(ctx, "Please enter the entity (e.g. Credit Card) this PIN is for", "") if err != nil { return err } - genPw, err = s.askForBool("Do you want to generate a new PIN?", true) + genPw, err = s.askForBool(ctx, "Do you want to generate a new PIN?", true) if err != nil { return err } @@ -159,7 +159,7 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { return err } } - comment, _ = s.askForString("Comments (optional)", "") + comment, _ = s.askForString(ctx, "Comments (optional)", "") // select store stores := []string{""} stores = append(stores, s.Store.MountPoints()...) @@ -179,7 +179,7 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { } name := fmt.Sprintf("%spins/%s/%s", store, authority, application) if s.Store.Exists(ctx, name) { - name, err = s.askForString("Secret already exists, please choose another path", name) + name, err = s.askForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } @@ -209,15 +209,15 @@ func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { store string err error ) - account, err = s.askForString("Please enter the AWS Account this key belongs to", "") + account, err = s.askForString(ctx, "Please enter the AWS Account this key belongs to", "") if err != nil { return err } - username, err = s.askForString("Please enter the name of the AWS IAM User this key belongs to", "") + username, err = s.askForString(ctx, "Please enter the name of the AWS IAM User this key belongs to", "") if err != nil { return err } - accesskey, err = s.askForString("Please enter the Access Key ID (AWS_ACCESS_KEY_ID)", "") + accesskey, err = s.askForString(ctx, "Please enter the Access Key ID (AWS_ACCESS_KEY_ID)", "") if err != nil { return err } @@ -225,7 +225,7 @@ func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { if err != nil { return err } - region, _ = s.askForString("Please enter the default Region (AWS_DEFAULT_REGION) (optional)", "") + region, _ = s.askForString(ctx, "Please enter the default Region (AWS_DEFAULT_REGION) (optional)", "") // select store stores := []string{""} stores = append(stores, s.Store.MountPoints()...) @@ -245,7 +245,7 @@ func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { } name := fmt.Sprintf("%saws/iam/%s/%s", store, account, username) if s.Store.Exists(ctx, name) { - name, err = s.askForString("Secret already exists, please choose another path", name) + name, err = s.askForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } @@ -269,7 +269,7 @@ func (s *Action) createGCP(ctx context.Context, c *cli.Context) error { store string err error ) - svcaccfn, err = s.askForString("Please enter path to the Service Account JSON file", "") + svcaccfn, err = s.askForString(ctx, "Please enter path to the Service Account JSON file", "") if err != nil { return err } @@ -282,13 +282,13 @@ func (s *Action) createGCP(ctx context.Context, c *cli.Context) error { return err } if username == "" { - username, err = s.askForString("Please enter the name of this service account", "") + username, err = s.askForString(ctx, "Please enter the name of this service account", "") if err != nil { return err } } if project == "" { - project, err = s.askForString("Please enter the name of this GCP project", "") + project, err = s.askForString(ctx, "Please enter the name of this GCP project", "") if err != nil { return err } @@ -312,7 +312,7 @@ func (s *Action) createGCP(ctx context.Context, c *cli.Context) error { } name := fmt.Sprintf("%sgcp/iam/%s/%s", store, project, username) if s.Store.Exists(ctx, name) { - name, err = s.askForString("Secret already exists, please choose another path", name) + name, err = s.askForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } @@ -349,11 +349,11 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { err error genPw bool ) - shortname, err = s.askForString("Please enter a name for the secret", "") + shortname, err = s.askForString(ctx, "Please enter a name for the secret", "") if err != nil { return err } - genPw, err = s.askForBool("Do you want to generate a new password?", true) + genPw, err = s.askForBool(ctx, "Do you want to generate a new password?", true) if err != nil { return err } @@ -387,7 +387,7 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { } name := fmt.Sprintf("%smisc/%s", store, shortname) if s.Store.Exists(ctx, name) { - name, err = s.askForString("Secret already exists, please choose another path", name) + name, err = s.askForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } @@ -395,14 +395,14 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { sec := secret.New(password, "") fmt.Println("Enter zero or more key value pairs for this secret:") for { - key, err := s.askForString("Name for Key Value pair (enter to quit)", "") + key, err := s.askForString(ctx, "Name for Key Value pair (enter to quit)", "") if err != nil { return err } if key == "" { break } - val, err := s.askForString("Value for Key '"+key+"'", "") + val, err := s.askForString(ctx, "Value for Key '"+key+"'", "") if err != nil { return err } @@ -421,12 +421,12 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { } func (s *Action) createGeneratePassword(ctx context.Context) (string, error) { - xkcd, err := s.askForBool("Do you want an rememberable password?", true) + xkcd, err := s.askForBool(ctx, "Do you want an rememberable password?", true) if err != nil { return "", err } if xkcd { - length, err := s.askForInt("How many words should be cominbed into a passphrase?", 4) + length, err := s.askForInt(ctx, "How many words should be cominbed into a passphrase?", 4) if err != nil { return "", err } @@ -437,11 +437,11 @@ func (s *Action) createGeneratePassword(ctx context.Context) (string, error) { return string(g.GeneratePassword()), nil } - length, err := s.askForInt("How long should the password be?", defaultLength) + length, err := s.askForInt(ctx, "How long should the password be?", defaultLength) if err != nil { return "", err } - symbols, err := s.askForBool("Do you want to include symbols?", false) + symbols, err := s.askForBool(ctx, "Do you want to include symbols?", false) if err != nil { return "", err } @@ -449,7 +449,7 @@ func (s *Action) createGeneratePassword(ctx context.Context) (string, error) { } func (s *Action) createGeneratePIN(ctx context.Context) (string, error) { - length, err := s.askForInt("How long should the PIN be?", 4) + length, err := s.askForInt(ctx, "How long should the PIN be?", 4) if err != nil { return "", err } diff --git a/action/generate.go b/action/generate.go index a48eb0d06b..17775630a3 100644 --- a/action/generate.go +++ b/action/generate.go @@ -50,7 +50,7 @@ func (s *Action) Generate(ctx context.Context, c *cli.Context) error { if name == "" { var err error - name, err = s.askForString("Which name do you want to use?", "") + name, err = s.askForString(ctx, "Which name do you want to use?", "") if err != nil || name == "" { return s.exitError(ctx, ExitNoName, err, "please provide a password name") } @@ -70,7 +70,7 @@ func (s *Action) Generate(ctx context.Context, c *cli.Context) error { question = "How many words should be combined to a password?" } var err error - if length, err = s.askForString(question, string(candidateLength)); err != nil { + if length, err = s.askForString(ctx, question, string(candidateLength)); err != nil { panic(err) // panic on i/o error only, string -> int conversion is done below } } diff --git a/action/git.go b/action/git.go index f599355420..da5d8c590f 100644 --- a/action/git.go +++ b/action/git.go @@ -50,14 +50,14 @@ func (s *Action) gitInit(ctx context.Context, store, sk string) error { if userName == "" { var err error - userName, err = s.askForString(color.CyanString("Please enter a user name for password store git config"), userName) + userName, err = s.askForString(ctx, color.CyanString("Please enter a user name for password store git config"), userName) if err != nil { return errors.Wrapf(err, "failed to ask for user input") } } if userEmail == "" { var err error - userEmail, err = s.askForString(color.CyanString("Please enter an email address for password store git config"), userEmail) + userEmail, err = s.askForString(ctx, color.CyanString("Please enter an email address for password store git config"), userEmail) if err != nil { return errors.Wrapf(err, "failed to ask for user input") } diff --git a/action/init.go b/action/init.go index f82b728176..008440a8e8 100644 --- a/action/init.go +++ b/action/init.go @@ -17,7 +17,7 @@ import ( func (s *Action) Initialized(ctx context.Context, c *cli.Context) error { if !s.Store.Initialized() { if ctxutil.IsInteractive(ctx) { - if ok, err := s.askForBool("It seems you are new to gopass. Do you want to run the onboarding wizard?", true); err == nil && ok { + if ok, err := s.askForBool(ctx, "It seems you are new to gopass. Do you want to run the onboarding wizard?", true); err == nil && ok { return s.initOnboarding(ctx, c) } } @@ -127,10 +127,10 @@ func (s *Action) initOBLocal(ctx context.Context, c *cli.Context) error { return err } fmt.Println("Configuring your local store") - if want, err := s.askForBool("Do you want to automatically push any changes to the git remote (if any)?", true); err == nil { + if want, err := s.askForBool(ctx, "Do you want to automatically push any changes to the git remote (if any)?", true); err == nil { s.cfg.Root.AutoSync = want } - if want, err := s.askForBool("Do you want to always confirm recipients when encrypting?", false); err == nil { + if want, err := s.askForBool(ctx, "Do you want to always confirm recipients when encrypting?", false); err == nil { s.cfg.Root.NoConfirm = !want } if err := s.cfg.Save(); err != nil { @@ -145,7 +145,7 @@ func (s *Action) initOBCreateTeam(ctx context.Context, c *cli.Context) error { if err := s.initOBLocal(ctx, c); err != nil { return errors.Wrapf(err, "failed to create local store") } - team, err := s.askForString("Please enter the name of your team (may contain slashes)", "") + team, err := s.askForString(ctx, "Please enter the name of your team (may contain slashes)", "") if err != nil { return err } @@ -154,7 +154,7 @@ func (s *Action) initOBCreateTeam(ctx context.Context, c *cli.Context) error { return err } fmt.Println("3.) Configuring the remote for ", team) - remote, err := s.askForString("Please enter the git remote for your shared store", "") + remote, err := s.askForString(ctx, "Please enter the git remote for your shared store", "") if err != nil { return err } @@ -172,12 +172,12 @@ func (s *Action) initOBJoinTeam(ctx context.Context, c *cli.Context) error { if err := s.initOBLocal(ctx, c); err != nil { return errors.Wrapf(err, "failed to create local store") } - team, err := s.askForString("Please enter the name of your team (may contain slashes)", "") + team, err := s.askForString(ctx, "Please enter the name of your team (may contain slashes)", "") if err != nil { return err } fmt.Println("2.) Cloning from the remote for ", team) - remote, err := s.askForString("Please enter the git remote for your shared store", "") + remote, err := s.askForString(ctx, "Please enter the git remote for your shared store", "") if err != nil { return err } diff --git a/action/insert.go b/action/insert.go index 1db396989f..71fda682e2 100644 --- a/action/insert.go +++ b/action/insert.go @@ -49,7 +49,7 @@ func (s *Action) Insert(ctx context.Context, c *cli.Context) error { // update to a single YAML entry if key != "" { if ctxutil.IsInteractive(ctx) { - pw, err := s.askForString(name+":"+key, "") + pw, err := s.askForString(ctx, name+":"+key, "") if err != nil { return s.exitError(ctx, ExitIO, err, "failed to ask for user input: %s", err) } @@ -122,7 +122,7 @@ func (s *Action) Insert(ctx context.Context, c *cli.Context) error { var promptFn func(context.Context, string) (string, error) if echo { promptFn = func(ctx context.Context, prompt string) (string, error) { - return s.askForString(prompt, "") + return s.askForString(ctx, prompt, "") } } diff --git a/action/jsonapi.go b/action/jsonapi.go index 34715cf2e1..df55850f5a 100644 --- a/action/jsonapi.go +++ b/action/jsonapi.go @@ -25,22 +25,22 @@ func (s *Action) JSONAPI(ctx context.Context, c *cli.Context) error { // SetupNativeMessaging sets up manifest for gopass as native messaging host func (s *Action) SetupNativeMessaging(ctx context.Context, c *cli.Context) error { - browser, err := s.getBrowser(c) + browser, err := s.getBrowser(ctx, c) if err != nil { return err } - globalInstall, err := s.getGlobalInstall(c) + globalInstall, err := s.getGlobalInstall(ctx, c) if err != nil { return err } - libpath, err := s.getLibPath(c, browser, globalInstall) + libpath, err := s.getLibPath(ctx, c, browser, globalInstall) if err != nil { return err } - wrapperPath, err := s.getWrapperPath(c) + wrapperPath, err := s.getWrapperPath(ctx, c) if err != nil { return err } @@ -53,17 +53,17 @@ func (s *Action) SetupNativeMessaging(ctx context.Context, c *cli.Context) error return nil } - install, err := s.askForBool(color.BlueString("Install manifest and wrapper?"), true) + install, err := s.askForBool(ctx, color.BlueString("Install manifest and wrapper?"), true) if install && err == nil { return manifest.SetUp(browser, wrapperPath, libpath, globalInstall) } return err } -func (s *Action) getBrowser(c *cli.Context) (browser string, err error) { +func (s *Action) getBrowser(ctx context.Context, c *cli.Context) (browser string, err error) { browser = c.String("browser") if browser == "" { - browser, err = s.askForString(color.BlueString("For which browser do you want to install gopass native messaging? [%s]", strings.Join(manifest.ValidBrowsers[:], ",")), manifest.DefaultBrowser) + browser, err = s.askForString(ctx, color.BlueString("For which browser do you want to install gopass native messaging? [%s]", strings.Join(manifest.ValidBrowsers[:], ",")), manifest.DefaultBrowser) if err != nil { return "", errors.Wrapf(err, "failed to ask for user input") } @@ -74,24 +74,24 @@ func (s *Action) getBrowser(c *cli.Context) (browser string, err error) { return } -func (s *Action) getGlobalInstall(c *cli.Context) (bool, error) { +func (s *Action) getGlobalInstall(ctx context.Context, c *cli.Context) (bool, error) { if !c.IsSet("global") { - return s.askForBool(color.BlueString("Install for all users? (might require sudo gopass)"), false) + return s.askForBool(ctx, color.BlueString("Install for all users? (might require sudo gopass)"), false) } return c.Bool("global"), nil } -func (s *Action) getLibPath(c *cli.Context, browser string, global bool) (string, error) { +func (s *Action) getLibPath(ctx context.Context, c *cli.Context, browser string, global bool) (string, error) { if !c.IsSet("libpath") && runtime.GOOS == "linux" && browser == "firefox" && global { - return s.askForString(color.BlueString("What is your lib path?"), "/usr/lib") + return s.askForString(ctx, color.BlueString("What is your lib path?"), "/usr/lib") } return c.String("libpath"), nil } -func (s *Action) getWrapperPath(c *cli.Context) (path string, err error) { +func (s *Action) getWrapperPath(ctx context.Context, c *cli.Context) (path string, err error) { path = c.String("path") if path == "" { - path, err = s.askForString(color.BlueString("In which path should gopass_wrapper.sh be installed?"), manifest.DefaultWrapperPath) + path, err = s.askForString(ctx, color.BlueString("In which path should gopass_wrapper.sh be installed?"), manifest.DefaultWrapperPath) if err != nil { return "", errors.Wrapf(err, "failed to ask for user input") } diff --git a/utils/termwiz/selection.go b/utils/termwiz/selection.go index 7f2ed870a2..078f77371f 100644 --- a/utils/termwiz/selection.go +++ b/utils/termwiz/selection.go @@ -34,6 +34,12 @@ func GetSelection(ctx context.Context, prompt, usage string, choices []string) ( cur := 0 for { + // check for context cancelation + select { + case <-ctx.Done(): + return "aborted", cur + default: + } _ = termbox.Clear(coldef, coldef) tbprint(0, 0, coldef, coldef, prompt+"Please select:") _, h := termbox.Size()