From 460d10902f76c916fb54e23269958e5f394766ac Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 30 Sep 2025 20:38:44 +0530 Subject: [PATCH] Cleanup previous PR The overrides were not being passed to the askpass kitten. And we dont need to support backward compatibility for secrets with no backend, since this feature has never been released. --- docs/changelog.rst | 4 ++++ kittens/ssh/askpass.go | 16 +++++-------- kittens/ssh/config.go | 54 +++++++++++++++--------------------------- kittens/ssh/main.go | 25 ++++++++++++------- kittens/ssh/main.py | 2 +- 5 files changed, 46 insertions(+), 55 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 21742e455..911a991b5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -137,6 +137,10 @@ Detailed list of changes 0.43.1 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- ssh kitten: Allow specifying a password and/or TOTP authentication secret to + automate interactive logins in scenarios where public key authentication is + not supported (:pull:`9020`) + - macOS: Fix a bug where the color of a transparent titlebar was off when running in the release build versus the build from source. Also fix using a transparent titlebar causing the background opacity to be doubled. diff --git a/kittens/ssh/askpass.go b/kittens/ssh/askpass.go index bd5d3a117..ac28035ec 100644 --- a/kittens/ssh/askpass.go +++ b/kittens/ssh/askpass.go @@ -73,7 +73,7 @@ func generateTOTP(secret string, digits, period int64, t time.Time) (string, err off := sum[len(sum)-1] & 0x0f code := (uint32(sum[off])&0x7f)<<24 | (uint32(sum[off+1])&0xff)<<16 | (uint32(sum[off+2])&0xff)<<8 | (uint32(sum[off+3]) & 0xff) mod := uint32(1) - for i := int64(0); i < digits; i++ { + for range digits { mod *= 10 } val := code % mod @@ -96,15 +96,11 @@ func RunSSHAskpass() int { host := os.Getenv("KITTY_SSH_ASKPASS_HOST") user := os.Getenv("KITTY_SSH_ASKPASS_USER") if host != "" { - if cfg, bad_lines, err := load_config(host, user, nil); err == nil && cfg != nil { - for _, bl := range bad_lines { - if bl.Err != nil { - // Only fail for our secret backend errors to avoid - // unrelated ssh.conf issues breaking askpass. - if strings.Contains(bl.Err.Error(), "Unsupported secret backend") { - fatal(bl.Err) - } - } + var overrides []string + _ = json.Unmarshal([]byte(os.Getenv("KITTY_SSH_ASKPASS_OVERRIDES")), &overrides) + if cfg, _, err := load_config(host, user, overrides); err == nil && cfg != nil { + if err = resolve_secrets(cfg, false); err != nil { + return fatal(err) } // Password autofill if isPasswordPrompt(msg) && cfg.Password != "" { diff --git a/kittens/ssh/config.go b/kittens/ssh/config.go index cfbbe54a0..7cfd6cf22 100644 --- a/kittens/ssh/config.go +++ b/kittens/ssh/config.go @@ -24,10 +24,7 @@ import ( var _ = fmt.Print -// parseSecretSetting parses values of the form "backend:secret" for settings where -// only the "text" backend is currently supported. If no backend is specified, -// the value is treated as a plain text secret for backward compatibility. -func parseSecretSetting(settingKey, val string) (string, error) { +func resolve_secret(key, val string) (string, error) { v := strings.TrimSpace(val) if v == "" { return "", nil @@ -39,11 +36,25 @@ func parseSecretSetting(settingKey, val string) (string, error) { case "text": return s, nil default: - return "", fmt.Errorf("Unsupported secret backend %q for %s. Supported backends: text", b, settingKey) + return "", fmt.Errorf("Unsupported secret backend %s for %s. Supported backends: text", b, key) } } - // No backend specified; treat as text - return v, nil + return "", fmt.Errorf("No secret backend specified for: %s", key) +} + +func resolve_secrets(c *Config, only_syntax bool) error { + _ = only_syntax // this will be useful when using backends that require user interaction + if r, err := resolve_secret("password", c.Password); err != nil { + return err + } else { + c.Password = r + } + if r, err := resolve_secret("totp_secret", c.Totp_secret); err != nil { + return err + } else { + c.Password = r + } + return nil } type EnvInstruction struct { @@ -369,7 +380,7 @@ type ConfigSet struct { func config_for_hostname(hostname_to_match, username_to_match string, cs *ConfigSet) *Config { matcher := func(q *Config) bool { - for _, pat := range strings.Split(q.Hostname, " ") { + for pat := range strings.SplitSeq(q.Hostname, " ") { upat := "*" if strings.Contains(pat, "@") { upat, pat, _ = strings.Cut(pat, "@") @@ -401,22 +412,6 @@ func (self *ConfigSet) line_handler(key, val string) error { c = NewConfig() self.all_configs = append(self.all_configs, c) } - switch key { - case "password": - secret, err := parseSecretSetting("password", val) - if err != nil { - return err - } - c.Password = secret - return nil - case "totp_secret": - secret, err := parseSecretSetting("totp_secret", val) - if err != nil { - return err - } - c.Totp_secret = secret - return nil - } return c.Parse(key, val) } @@ -438,16 +433,5 @@ func load_config(hostname_to_match string, username_to_match string, overrides [ bad_lines = append(bad_lines, override_parser.BadLines()...) final_conf.Hostname = h } - // Normalize and validate secrets post-overrides as overrides bypass line_handler - if s, err := parseSecretSetting("password", final_conf.Password); err != nil { - bad_lines = append(bad_lines, config.ConfigLine{Src_file: "", Line: "password " + final_conf.Password, Line_number: 0, Err: err}) - } else { - final_conf.Password = s - } - if s, err := parseSecretSetting("totp_secret", final_conf.Totp_secret); err != nil { - bad_lines = append(bad_lines, config.ConfigLine{Src_file: "", Line: "totp_secret " + final_conf.Totp_secret, Line_number: 0, Err: err}) - } else { - final_conf.Totp_secret = s - } return final_conf, bad_lines, nil } diff --git a/kittens/ssh/main.go b/kittens/ssh/main.go index 1f1ae53e1..438c47910 100644 --- a/kittens/ssh/main.go +++ b/kittens/ssh/main.go @@ -145,7 +145,7 @@ func connection_sharing_args(kitty_pid int) ([]string, error) { }, nil } -func set_askpass() (need_to_request_data bool) { +func set_askpass(hostname_for_match, uname string, overrides []string) (need_to_request_data bool) { need_to_request_data = true sentinel := filepath.Join(utils.CacheDir(), "openssh-is-new-enough-for-askpass") _, err := os.Stat(sentinel) @@ -160,6 +160,11 @@ func set_askpass() (need_to_request_data bool) { if err == nil { os.Setenv("SSH_ASKPASS", exe) os.Setenv("KITTY_KITTEN_RUN_MODULE", "ssh_askpass") + // Provide data to askpass so it can lookup auth settings in ssh.conf + os.Setenv("KITTY_SSH_ASKPASS_HOST", hostname_for_match) + os.Setenv("KITTY_SSH_ASKPASS_USER", uname) + ov, _ := json.Marshal(overrides) + os.Setenv("KITTY_SSH_ASKPASS_OVERRIDES", string(ov)) if !need_to_request_data { os.Setenv("SSH_ASKPASS_REQUIRE", "force") } @@ -621,6 +626,11 @@ func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err erro if err != nil { return 1, err } + // check the secrets syntax here as askpass has no good way to report errors to + // the user + if err = resolve_secrets(host_opts, true); err != nil { + return 1, err + } if len(bad_lines) > 0 { for _, x := range bad_lines { fmt.Fprintf(os.Stderr, "Ignoring bad config line: %s:%d with error: %s", filepath.Base(x.Src_file), x.Line_number, x.Err) @@ -646,14 +656,11 @@ func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err erro } cmd = slices.Insert(cmd, insertion_point, control_master_args...) } - use_kitty_askpass := host_opts.Askpass == Askpass_native || (host_opts.Askpass == Askpass_unless_set && os.Getenv("SSH_ASKPASS") == "") - need_to_request_data := true - if use_kitty_askpass { - need_to_request_data = set_askpass() - // Provide host/user to askpass so it can lookup auth settings in ssh.conf - os.Setenv("KITTY_SSH_ASKPASS_HOST", hostname_for_match) - os.Setenv("KITTY_SSH_ASKPASS_USER", uname) - } + use_kitty_askpass := host_opts.Askpass == Askpass_native || (host_opts.Askpass == Askpass_unless_set && os.Getenv("SSH_ASKPASS") == "") + need_to_request_data := true + if use_kitty_askpass { + need_to_request_data = set_askpass(hostname_for_match, uname, overrides) + } master_is_functional := func() bool { if master_checked { return master_is_alive diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index d445784db..38d2272a8 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -223,7 +223,7 @@ egr() # }}} agr('askpass', 'Askpass automation') # {{{ opt('password', '', long_text=''' -Specify a secret to use when SSH prompts for a password. The value format is +Specify a password to use when SSH prompts for a password. The value format is "backend:secret". Currently, only the "text" backend is supported, which stores the secret in plain text in the config file. For example: