1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
|
package askpass
import (
"bufio"
"bytes"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"syscall"
"golang.org/x/sys/unix"
"golang.org/x/term"
)
// Most of this is copied from OpenSSH readpassphrase.
// State for the ReadPassphrase function
type ReadPassFlags uint8
const (
RP_ECHO = 1 << iota /* echo stuff or something 8 */
RP_ALLOW_STDIN /* Allow stdin and not askpass */
RP_ALLOW_EOF /* not used */
RP_USE_ASKPASS /* Use SSH_ASKPASS */
RP_ASK_PERMISSION /* Ask for permission, yes/no prompt */
RP_NEWLINE /* Print newline after answer. */
RPP_ECHO_OFF /* Turn off echo (default). */
RPP_ECHO_ON /* Leave echo on. */
RPP_REQUIRE_TTY /* Fail if there is no tty. */
RPP_FORCELOWER /* Force input to lower case. */
RPP_FORCEUPPER /* Force input to upper case. */
RPP_SEVENBIT /* Strip the high bit from input. */
RPP_STDIN /* Read from stdin, not /dev/tty */
)
var (
ErrNoAskpass = errors.New("system does not have an askpass program")
// Default ASKPASS programs
SSH_ASKPASS_DEFAULTS = []string{
"/usr/lib/ssh/x11-ssh-askpass",
"/usr/lib/ssh/gnome-ssh-askpass3",
"/usr/lib/ssh/gnome-ssh-askpass",
"/usr/libexec/openssh/gnome-ssh-askpass",
"/usr/bin/ksshaskpass",
"/usr/libexec/seahorse/ssh-askpass",
"/usr/lib/seahorse/ssh-askpass",
}
)
func findAskPass() (string, error) {
for _, s := range SSH_ASKPASS_DEFAULTS {
if _, err := os.Stat(s); errors.Is(err, os.ErrNotExist) {
continue
}
return s, nil
}
return "", ErrNoAskpass
}
func isTerminal(fd uintptr) bool {
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
return err == nil
}
func ReadPassphrase(prompt string, flags ReadPassFlags) ([]byte, error) {
var allow_askpass bool
var use_askpass bool
var askpass_hint string
if _, ok := os.LookupEnv("DISPLAY"); ok {
allow_askpass = true
} else if _, ok2 := os.LookupEnv("WAYLAND_DISPLAY"); ok2 {
allow_askpass = true
}
if s, ok := os.LookupEnv("SSH_ASKPASS_REQUIRE"); ok {
switch s {
case "force":
use_askpass = true
allow_askpass = true
case "prefer":
use_askpass = allow_askpass
case "never":
allow_askpass = false
}
}
if use_askpass {
slog.Debug("requested to askpass")
} else if (flags & RP_USE_ASKPASS) != 0 {
use_askpass = true
} else if (flags & RP_ALLOW_STDIN) != 0 {
if !isTerminal(os.Stdout.Fd()) {
slog.Debug("stdin is not a tty")
use_askpass = true
}
}
if use_askpass && allow_askpass {
if (flags & RP_ASK_PERMISSION) != 0 {
askpass_hint = "confirm"
}
return SshAskPass(prompt, askpass_hint)
}
// If we want to echo stuff, we read directly from stdin
// using bufio.NewReader.
if (flags & RPP_ECHO_ON) != 0 {
fmt.Printf("%s", prompt)
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return []byte(""), nil
}
return []byte(strings.TrimSpace(input)), nil
}
// Then we are defaulting to TTY prompt
fmt.Printf("%s", prompt)
pin, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return []byte{}, nil
}
if (flags & RP_NEWLINE) != 0 {
fmt.Println("")
}
return pin, nil
}
func SshAskPass(prompt, hint string) ([]byte, error) {
var askpass string
var err error
if s, ok := os.LookupEnv("SSH_ASKPASS"); ok {
askpass = s
} else if s, _ := exec.LookPath("ssh-askpass"); s != "" {
askpass = s
} else {
askpass, err = findAskPass()
if err != nil {
return nil, err
}
}
if hint != "" {
os.Setenv("SSH_ASKPASS_PROMPT", hint)
}
out, err := exec.Command(askpass, prompt).Output()
switch hint {
case "confirm":
// TODO: Ugly and needs a rework
var exerr *exec.ExitError
if errors.As(err, &exerr) {
if exerr.ExitCode() != 0 {
return []byte("no"), nil
}
} else {
return []byte("yes"), nil
}
}
if err != nil {
return []byte{}, err
}
return bytes.TrimSpace(out), nil
}
// AskPremission runs SSH_ASKPASS in with SSH_ASKPASS_PROMPT=confirm set as env
// it will expect exit code 0 or !0 and return 'yes' and 'no' respectively.
func AskPermission() (bool, error) {
a, err := ReadPassphrase("Confirm touch", RP_USE_ASKPASS|RP_ASK_PERMISSION)
if err != nil {
return false, err
}
if bytes.Equal(a, []byte("yes")) {
return true, nil
} else if bytes.Equal(a, []byte("no")) {
return false, nil
}
return false, nil
}
|