diff options
Diffstat (limited to 'internal')
| -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 | 
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") +	} +} | 
