diff --git a/cmd/age/age.go b/cmd/age/age.go index cd46eea..6ad43b4 100644 --- a/cmd/age/age.go +++ b/cmd/age/age.go @@ -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 { diff --git a/cmd/age/testdata/terminal.txt b/cmd/age/testdata/terminal.txt new file mode 100644 index 0000000..47f8256 --- /dev/null +++ b/cmd/age/testdata/terminal.txt @@ -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 -- + diff --git a/cmd/age/tui.go b/cmd/age/tui.go index 6b8ae76..4a92bc6 100644 --- a/cmd/age/tui.go +++ b/cmd/age/tui.go @@ -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 {