cmd/age: ensure TUI output goes all to the terminal

This commit is contained in:
Filippo Valsorda
2022-06-30 11:58:32 +02:00
parent de7c1fb565
commit 0ab5c738fb
3 changed files with 52 additions and 17 deletions

View File

@@ -288,12 +288,10 @@ func passphrasePromptForEncryption() (string, error) {
words = append(words, randomWord())
}
p = strings.Join(words, "-")
// It's somewhat unfortunate that the prompt comes through the terminal,
// while the autogenerated passphrase is printed to stderr. However,
// thinking about the terminal as a pinentry UI, it's better for the
// passphrase to stick around and be copy-pastable, than to show up in
// ephemeral UI.
printf("using autogenerated passphrase %q", p)
err := printfToTerminal("using autogenerated passphrase %q", p)
if err != nil {
return "", fmt.Errorf("could not print passphrase: %v", err)
}
} else {
confirm, err := readSecret("Confirm passphrase:")
if err != nil {

33
cmd/age/testdata/terminal.txt vendored Normal file
View File

@@ -0,0 +1,33 @@
[windows] skip # no pty support
# controlling terminal is used instead of stdin/stderr
pty terminal
age -p -o test.age input
! stderr .
# autogenerated passphrase is printed to terminal
pty empty
age -p -o test.age input
ptyout 'autogenerated passphrase'
! stderr .
# with no controlling terminal, stdin terminal is used
# TODO: enable once https://golang.org/issue/53601 is fixed
# and Noctty is added to testscript.
# pty -stdin terminal
# age -p -o test.age input
# ! stderr .
# no terminal causes an error
# TODO: enable once https://golang.org/issue/53601 is fixed
# and Noctty is added to testscript.
# ! age -p -o test.age input
# stderr 'standard input is not a terminal'
-- input --
test
-- terminal --
password
password
-- empty --

View File

@@ -88,29 +88,33 @@ func clearLine(out io.Writer) {
// withTerminal does not open a non-terminal stdin, so the caller does not need
// to check stdinInUse.
func withTerminal(f func(in, out *os.File) error) error {
var in, out *os.File
if runtime.GOOS == "windows" {
var err error
in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil {
return err
}
defer in.Close()
out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0)
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
if err != nil {
return err
}
defer out.Close()
return f(in, out)
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
in, out = tty, tty
return f(tty, tty)
} else if term.IsTerminal(int(os.Stdin.Fd())) {
return f(os.Stdin, os.Stdin)
} else {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
in, out = os.Stdin, os.Stderr
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
return f(in, out)
}
func printfToTerminal(format string, v ...interface{}) error {
return withTerminal(func(_, out *os.File) error {
_, err := fmt.Fprintf(out, "age: "+format, v...)
return err
})
}
// readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
@@ -124,7 +128,7 @@ func readSecret(prompt string) (s []byte, err error) {
return
}
// readSecret reads a single character from the terminal with no echo. The
// readCharacter reads a single character from the terminal with no echo. The
// prompt is ephemeral.
func readCharacter(prompt string) (c byte, err error) {
err = withTerminal(func(in, out *os.File) error {