From fa5b575ceb6d4f64eb528e3a91cabac340a570d7 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Wed, 2 Jun 2021 10:58:45 +0200 Subject: [PATCH] cmd/age: use CONIN$/CONOUT$ on Windows for password prompts Fixes #128 Closes #274 Co-authored-by: codesoap --- cmd/age/age.go | 9 +++----- cmd/age/encrypted_keys.go | 48 +++++++++++++++++++++++++++------------ cmd/age/parse.go | 4 ++-- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/cmd/age/age.go b/cmd/age/age.go index f6dcf7a..1cda46d 100644 --- a/cmd/age/age.go +++ b/cmd/age/age.go @@ -225,8 +225,7 @@ func main() { } func passphrasePromptForEncryption() (string, error) { - fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ") - pass, err := readPassphrase() + pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):") if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } @@ -239,8 +238,7 @@ func passphrasePromptForEncryption() (string, error) { p = strings.Join(words, "-") fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p) } else { - fmt.Fprintf(os.Stderr, "Confirm passphrase: ") - confirm, err := readPassphrase() + confirm, err := readPassphrase("Confirm passphrase:") if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } @@ -345,8 +343,7 @@ func decrypt(keys []string, in io.Reader, out io.Writer) { } func passphrasePrompt() (string, error) { - fmt.Fprintf(os.Stderr, "Enter passphrase: ") - pass, err := readPassphrase() + pass, err := readPassphrase("Enter passphrase:") if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index ea9ee5b..9b31a63 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "os" + "runtime" "filippo.io/age" "golang.org/x/term" @@ -45,23 +46,40 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err return fileKey, err } -// readPassphrase reads a passphrase from the terminal. If stdin is not -// connected to a terminal, it tries /dev/tty and fails if that's not available. -// It does not read from a non-terminal stdin, so it does not check stdinInUse. -func readPassphrase() ([]byte, error) { - fd := int(os.Stdin.Fd()) - if !term.IsTerminal(fd) { - tty, err := os.Open("/dev/tty") +// readPassphrase reads a passphrase from the terminal. It does not read from a +// non-terminal stdin, so it does not check stdinInUse. +func readPassphrase(prompt string) ([]byte, error) { + var in, out *os.File + if runtime.GOOS == "windows" { + var err error + in, err = os.OpenFile("CONIN$", os.O_RDWR, 0) if err != nil { - return nil, fmt.Errorf("standard input is not a terminal, and opening /dev/tty failed: %v", err) + return nil, err + } + defer in.Close() + out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0) + if err != nil { + return nil, err + } + defer out.Close() + } else if _, err := os.Stat("/dev/tty"); err == nil { + tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) + if err != nil { + return nil, err } defer tty.Close() - fd = int(tty.Fd()) + in, out = tty, tty + } else { + if !term.IsTerminal(int(os.Stdin.Fd())) { + return nil, fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err) + } + in, out = os.Stdin, os.Stderr } - defer fmt.Fprintf(os.Stderr, "\n") - p, err := term.ReadPassword(fd) - if err != nil { - return nil, err - } - return p, nil + fmt.Fprintf(out, "%s ", prompt) + // Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$. + // Only when running a Windows binary from WSL2, the cursor would not go + // back to the start of the line with a simple LF. Honestly, it's impressive + // CONIN$ and CONOUT$ even work at all inside WSL2. + defer fmt.Fprintf(out, "\r\n") + return term.ReadPassword(int(in.Fd())) } diff --git a/cmd/age/parse.go b/cmd/age/parse.go index 607cdfb..addb7c7 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -168,8 +168,8 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) { } } passphrasePrompt := func() ([]byte, error) { - fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name) - pass, err := readPassphrase() + prompt := fmt.Sprintf("Enter passphrase for %q:", name) + pass, err := readPassphrase(prompt) if err != nil { return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err) }