From c89455063cfd9c531c0a671251ccfcd46f09403d Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Wed, 15 Jun 2016 22:43:31 +0200 Subject: readpassword: create internal package for password reading * Supports stdin * Add tests for extpass and stdin As per user request at https://github.com/rfjakob/gocryptfs/issues/30 --- internal/readpassword/extpass_test.go | 55 ++++++++++++++ internal/readpassword/read.go | 133 ++++++++++++++++++++++++++++++++++ internal/readpassword/stdin_test.go | 100 +++++++++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 internal/readpassword/extpass_test.go create mode 100644 internal/readpassword/read.go create mode 100644 internal/readpassword/stdin_test.go (limited to 'internal') diff --git a/internal/readpassword/extpass_test.go b/internal/readpassword/extpass_test.go new file mode 100644 index 0000000..6eda142 --- /dev/null +++ b/internal/readpassword/extpass_test.go @@ -0,0 +1,55 @@ +package readpassword + +import ( + "os" + "os/exec" + "testing" + + "github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +func TestMain(m *testing.M) { + // Shut up info output + toggledlog.Info.Enabled = false + m.Run() +} + +func TestExtpass(t *testing.T) { + p1 := "ads2q4tw41reg52" + p2 := readPasswordExtpass("echo " + p1) + if p1 != p2 { + t.Errorf("p1=%q != p2=%q", p1, p2) + } +} + +func TestOnceExtpass(t *testing.T) { + p1 := "lkadsf0923rdfi48rqwhdsf" + p2 := Once("echo " + p1) + if p1 != p2 { + t.Errorf("p1=%q != p2=%q", p1, p2) + } +} + +func TestTwiceExtpass(t *testing.T) { + p1 := "w5w44t3wfe45srz434" + p2 := Once("echo " + p1) + if p1 != p2 { + t.Errorf("p1=%q != p2=%q", p1, p2) + } +} + +// When extpass returns an empty string, we should crash. +// https://talks.golang.org/2014/testing.slide#23 +func TestExtpassEmpty(t *testing.T) { + if os.Getenv("TEST_SLAVE") == "1" { + readPasswordExtpass("echo") + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestExtpassEmpty$") + cmd.Env = append(os.Environ(), "TEST_SLAVE=1") + err := cmd.Run() + if err != nil { + return + } + t.Fatal("empty password should have failed") +} diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go new file mode 100644 index 0000000..f316846 --- /dev/null +++ b/internal/readpassword/read.go @@ -0,0 +1,133 @@ +package readpassword + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +const ( + exitCode = 9 +) + +// TODO +var colorReset, colorRed string + +// Once() tries to get a password from the user, either from the terminal, +// extpass or stdin. +func Once(extpass string) string { + if extpass != "" { + return readPasswordExtpass(extpass) + } + if !terminal.IsTerminal(int(os.Stdin.Fd())) { + return readPasswordStdin() + } + return readPasswordTerminal("Password: ") +} + +// Twice() is the same as Once but will prompt twice if we get +// the password from the terminal. +func Twice(extpass string) string { + if extpass != "" { + return readPasswordExtpass(extpass) + } + if !terminal.IsTerminal(int(os.Stdin.Fd())) { + return readPasswordStdin() + } + p1 := readPasswordTerminal("Password: ") + p2 := readPasswordTerminal("Repeat: ") + if p1 != p2 { + toggledlog.Fatal.Println(colorRed + "Passwords do not match" + colorReset) + os.Exit(exitCode) + } + return p1 +} + +// readPasswordTerminal reads a line from the terminal. +// Exits on read error or empty result. +func readPasswordTerminal(prompt string) string { + fd := int(os.Stdin.Fd()) + fmt.Fprintf(os.Stderr, prompt) + // terminal.ReadPassword removes the trailing newline + p, err := terminal.ReadPassword(fd) + if err != nil { + toggledlog.Fatal.Printf(colorRed+"Could not read password from terminal: %v\n"+colorReset, err) + os.Exit(exitCode) + } + fmt.Fprintf(os.Stderr, "\n") + if len(p) == 0 { + toggledlog.Fatal.Println(colorRed + "Password is empty" + colorReset) + os.Exit(exitCode) + } + return string(p) +} + +// readPasswordStdin reads a line from stdin +// Exits on read error or empty result. +func readPasswordStdin() string { + toggledlog.Info.Println("Reading password from stdin") + p := readLineUnbuffered(os.Stdin) + if len(p) == 0 { + fmt.Fprintf(os.Stderr, "FOOOOOO\n") + toggledlog.Fatal.Println(colorRed + "Got empty password from stdin" + colorReset) + os.Exit(exitCode) + } + return p +} + +// readPasswordExtpass executes the "extpass" program and returns the first line +// of the output. +// Exits on read error or empty result. +func readPasswordExtpass(extpass string) string { + toggledlog.Info.Println("Reading password from extpass program") + parts := strings.Split(extpass, " ") + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Stderr = os.Stderr + pipe, err := cmd.StdoutPipe() + if err != nil { + toggledlog.Fatal.Printf(colorRed+"extpass pipe setup failed: %v\n"+colorReset, err) + os.Exit(exitCode) + } + err = cmd.Start() + if err != nil { + toggledlog.Fatal.Printf(colorRed+"extpass cmd start failed: %v\n"+colorReset, err) + os.Exit(exitCode) + } + p := readLineUnbuffered(pipe) + pipe.Close() + cmd.Wait() + if len(p) == 0 { + toggledlog.Fatal.Println(colorRed + "extpass: password is empty" + colorReset) + os.Exit(exitCode) + } + return p +} + +// readLineUnbuffered reads single bytes from "r" util it gets "\n" or EOF. +// The returned string does NOT contain the trailing "\n". +func readLineUnbuffered(r io.Reader) (l string) { + b := make([]byte, 1) + for { + n, err := r.Read(b) + if err == io.EOF { + return l + } + if err != nil { + toggledlog.Fatal.Printf(colorRed+"readLineUnbuffered: %v\n"+colorReset, err) + os.Exit(exitCode) + } + if n == 0 { + continue + } + if b[0] == '\n' { + return l + } + l = l + string(b) + } +} diff --git a/internal/readpassword/stdin_test.go b/internal/readpassword/stdin_test.go new file mode 100644 index 0000000..2d9f93f --- /dev/null +++ b/internal/readpassword/stdin_test.go @@ -0,0 +1,100 @@ +package readpassword + +import ( + "fmt" + "os" + "os/exec" + "testing" +) + +// Provide password via stdin, terminated by "\n". +func TestStdin(t *testing.T) { + p1 := "g55434t55wef" + if os.Getenv("TEST_SLAVE") == "1" { + p2 := readPasswordStdin() + if p1 != p2 { + fmt.Fprintf(os.Stderr, "%q != %q", p1, p2) + os.Exit(1) + } + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestStdin$") + cmd.Env = append(os.Environ(), "TEST_SLAVE=1") + cmd.Stderr = os.Stderr + pipe, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + n, err := pipe.Write([]byte(p1 + "\n")) + if n == 0 || err != nil { + t.Fatal(err) + } + err = cmd.Wait() + if err != nil { + t.Fatalf("slave failed with %v", err) + } +} + +// Provide password via stdin, terminated by EOF (pipe close). This should not +// hang. +func TestStdinEof(t *testing.T) { + p1 := "asd45as5f4a36" + if os.Getenv("TEST_SLAVE") == "1" { + p2 := readPasswordStdin() + if p1 != p2 { + fmt.Fprintf(os.Stderr, "%q != %q", p1, p2) + os.Exit(1) + } + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestStdinEof$") + cmd.Env = append(os.Environ(), "TEST_SLAVE=1") + cmd.Stderr = os.Stderr + pipe, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + _, err = pipe.Write([]byte(p1)) + if err != nil { + t.Fatal(err) + } + pipe.Close() + err = cmd.Wait() + if err != nil { + t.Fatalf("slave failed with %v", err) + } +} + +// Provide empty password via stdin +func TestStdinEmpty(t *testing.T) { + if os.Getenv("TEST_SLAVE") == "1" { + readPasswordStdin() + } + cmd := exec.Command(os.Args[0], "-test.run=TestStdinEmpty$") + cmd.Env = append(os.Environ(), "TEST_SLAVE=1") + pipe, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + _, err = pipe.Write([]byte("\n")) + if err != nil { + t.Fatal(err) + } + pipe.Close() + err = cmd.Wait() + if err == nil { + t.Fatalf("empty password should have failed") + } +} -- cgit v1.2.3