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") + } +} diff --git a/main.go b/main.go index 6896f11..61b56e6 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( "github.com/rfjakob/gocryptfs/internal/fusefrontend" "github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/internal/prefer_openssl" + "github.com/rfjakob/gocryptfs/internal/readpassword" "github.com/rfjakob/gocryptfs/internal/toggledlog" ) @@ -38,7 +39,6 @@ const ( ERREXIT_CIPHERDIR = 6 ERREXIT_INIT = 7 ERREXIT_LOADCONF = 8 - ERREXIT_PASSWORD = 9 ERREXIT_MOUNTPOINT = 10 ) @@ -71,7 +71,7 @@ func initDir(args *argContainer) { } else { toggledlog.Info.Printf("Using password provided via -extpass.") } - password := readPasswordTwice(args.extpass) + password := readpassword.Twice(args.extpass) creator := toggledlog.ProgramName + " " + GitVersion err = configfile.CreateConfFile(args.config, password, args.plaintextnames, args.scryptn, creator) if err != nil { @@ -121,17 +121,13 @@ func loadConfig(args *argContainer) (masterkey []byte, confFile *configfile.Conf toggledlog.Fatal.Printf(colorRed+"Config file not found: %v\n"+colorReset, err) os.Exit(ERREXIT_LOADCONF) } - if args.extpass == "" { - fmt.Fprintf(os.Stderr, "Password: ") - } - pw := readPassword(args.extpass) - toggledlog.Info.Printf("Decrypting master key... ") + pw := readpassword.Once(args.extpass) + toggledlog.Info.Println("Decrypting master key") masterkey, confFile, err = configfile.LoadConfFile(args.config, pw) if err != nil { toggledlog.Fatal.Println(colorRed + err.Error() + colorReset) os.Exit(ERREXIT_LOADCONF) } - toggledlog.Info.Printf("done.") return masterkey, confFile } @@ -140,7 +136,7 @@ func loadConfig(args *argContainer) (masterkey []byte, confFile *configfile.Conf func changePassword(args *argContainer) { masterkey, confFile := loadConfig(args) toggledlog.Info.Println("Please enter your new password.") - newPw := readPasswordTwice(args.extpass) + newPw := readpassword.Twice(args.extpass) confFile.EncryptKey(masterkey, newPw, confFile.ScryptObject.LogN()) err := confFile.WriteFile() if err != nil { diff --git a/password.go b/password.go deleted file mode 100644 index 01c71a7..0000000 --- a/password.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "golang.org/x/crypto/ssh/terminal" - - "github.com/rfjakob/gocryptfs/internal/toggledlog" -) - -func readPasswordTwice(extpass string) string { - if extpass == "" { - fmt.Fprintf(os.Stderr, "Password: ") - p1 := readPassword("") - fmt.Fprintf(os.Stderr, "Repeat: ") - p2 := readPassword("") - if p1 != p2 { - toggledlog.Fatal.Println(colorRed + "Passwords do not match" + colorReset) - os.Exit(ERREXIT_PASSWORD) - } - return p1 - } else { - return readPassword(extpass) - } -} - -// readPassword - get password from terminal -// or from the "extpass" program -func readPassword(extpass string) string { - var password string - var err error - var output []byte - if extpass != "" { - parts := strings.Split(extpass, " ") - cmd := exec.Command(parts[0], parts[1:]...) - cmd.Stderr = os.Stderr - output, err = cmd.Output() - if err != nil { - toggledlog.Fatal.Printf(colorRed+"extpass program returned error: %v\n"+colorReset, err) - os.Exit(ERREXIT_PASSWORD) - } - // Trim trailing newline like terminal.ReadPassword() does - if output[len(output)-1] == '\n' { - output = output[:len(output)-1] - } - } else { - fd := int(os.Stdin.Fd()) - output, err = terminal.ReadPassword(fd) - if err != nil { - toggledlog.Fatal.Printf(colorRed+"Could not read password from terminal: %v\n"+colorReset, err) - os.Exit(ERREXIT_PASSWORD) - } - fmt.Fprintf(os.Stderr, "\n") - } - password = string(output) - if password == "" { - toggledlog.Fatal.Printf(colorRed + "Password is empty\n" + colorReset) - os.Exit(ERREXIT_PASSWORD) - } - return password -} diff --git a/tests/integration_tests/cli_test.go b/tests/integration_tests/cli_test.go index bc604bd..0e88581 100644 --- a/tests/integration_tests/cli_test.go +++ b/tests/integration_tests/cli_test.go @@ -49,14 +49,34 @@ func TestPasswd(t *testing.T) { t.Fatal(err) } // Change password using "-extpass" - cmd2 := exec.Command(test_helpers.GocryptfsBinary, "-q", "-passwd", "-extpass", "echo test", dir) - cmd2.Stdout = os.Stdout - cmd2.Stderr = os.Stderr - err = cmd2.Run() + cmd = exec.Command(test_helpers.GocryptfsBinary, "-q", "-passwd", "-extpass", "echo test", dir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + t.Error(err) + } + // Change password using stdin + cmd = exec.Command(test_helpers.GocryptfsBinary, "-q", "-passwd", dir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + p, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + err = cmd.Start() + if err != nil { + t.Error(err) + } + // Old password + p.Write([]byte("test\n")) + // New password + p.Write([]byte("newpasswd\n")) + p.Close() + err = cmd.Wait() if err != nil { t.Error(err) } - } // Test -init & -config flag