cmd/age: use CONIN$/CONOUT$ on Windows for password prompts

Fixes #128
Closes #274

Co-authored-by: codesoap <codesoap@mailbox.org>
This commit is contained in:
Filippo Valsorda
2021-06-02 10:58:45 +02:00
parent cde103daae
commit fa5b575ceb
3 changed files with 38 additions and 23 deletions

View File

@@ -225,8 +225,7 @@ func main() {
} }
func passphrasePromptForEncryption() (string, error) { func passphrasePromptForEncryption() (string, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ") pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):")
pass, err := readPassphrase()
if err != nil { if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err) return "", fmt.Errorf("could not read passphrase: %v", err)
} }
@@ -239,8 +238,7 @@ func passphrasePromptForEncryption() (string, error) {
p = strings.Join(words, "-") p = strings.Join(words, "-")
fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p) fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p)
} else { } else {
fmt.Fprintf(os.Stderr, "Confirm passphrase: ") confirm, err := readPassphrase("Confirm passphrase:")
confirm, err := readPassphrase()
if err != nil { if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err) 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) { func passphrasePrompt() (string, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase: ") pass, err := readPassphrase("Enter passphrase:")
pass, err := readPassphrase()
if err != nil { if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err) return "", fmt.Errorf("could not read passphrase: %v", err)
} }

View File

@@ -10,6 +10,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"runtime"
"filippo.io/age" "filippo.io/age"
"golang.org/x/term" "golang.org/x/term"
@@ -45,23 +46,40 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err
return fileKey, err return fileKey, err
} }
// readPassphrase reads a passphrase from the terminal. If stdin is not // readPassphrase reads a passphrase from the terminal. It does not read from a
// connected to a terminal, it tries /dev/tty and fails if that's not available. // non-terminal stdin, so it does not check stdinInUse.
// It does not read from a non-terminal stdin, so it does not check stdinInUse. func readPassphrase(prompt string) ([]byte, error) {
func readPassphrase() ([]byte, error) { var in, out *os.File
fd := int(os.Stdin.Fd()) if runtime.GOOS == "windows" {
if !term.IsTerminal(fd) { var err error
tty, err := os.Open("/dev/tty") in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil { 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() 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") fmt.Fprintf(out, "%s ", prompt)
p, err := term.ReadPassword(fd) // Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$.
if err != nil { // Only when running a Windows binary from WSL2, the cursor would not go
return nil, err // back to the start of the line with a simple LF. Honestly, it's impressive
} // CONIN$ and CONOUT$ even work at all inside WSL2.
return p, nil defer fmt.Fprintf(out, "\r\n")
return term.ReadPassword(int(in.Fd()))
} }

View File

@@ -168,8 +168,8 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
} }
} }
passphrasePrompt := func() ([]byte, error) { passphrasePrompt := func() ([]byte, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name) prompt := fmt.Sprintf("Enter passphrase for %q:", name)
pass, err := readPassphrase() pass, err := readPassphrase(prompt)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err) return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
} }