summaryrefslogtreecommitdiff
path: root/internal/readpassword
diff options
context:
space:
mode:
authorJakob Unterwurzacher2016-06-15 22:43:31 +0200
committerJakob Unterwurzacher2016-06-15 22:44:24 +0200
commitc89455063cfd9c531c0a671251ccfcd46f09403d (patch)
tree1bd5330aad0ac7b16ecb5b35150a304e56271be3 /internal/readpassword
parent218bf83ce399832a0eccfbd025e5dd0399db6bed (diff)
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
Diffstat (limited to 'internal/readpassword')
-rw-r--r--internal/readpassword/extpass_test.go55
-rw-r--r--internal/readpassword/read.go133
-rw-r--r--internal/readpassword/stdin_test.go100
3 files changed, 288 insertions, 0 deletions
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")
+ }
+}