diff options
| author | Jakob Unterwurzacher | 2016-06-15 22:43:31 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2016-06-15 22:44:24 +0200 | 
| commit | c89455063cfd9c531c0a671251ccfcd46f09403d (patch) | |
| tree | 1bd5330aad0ac7b16ecb5b35150a304e56271be3 | |
| parent | 218bf83ce399832a0eccfbd025e5dd0399db6bed (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
| -rw-r--r-- | internal/readpassword/extpass_test.go | 55 | ||||
| -rw-r--r-- | internal/readpassword/read.go | 133 | ||||
| -rw-r--r-- | internal/readpassword/stdin_test.go | 100 | ||||
| -rw-r--r-- | main.go | 14 | ||||
| -rw-r--r-- | password.go | 64 | ||||
| -rw-r--r-- | tests/integration_tests/cli_test.go | 30 | 
6 files changed, 318 insertions, 78 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") +	} +} @@ -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 | 
