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: