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
This commit is contained in:
Jakob Unterwurzacher 2016-06-15 22:43:31 +02:00
parent 218bf83ce3
commit c89455063c
6 changed files with 318 additions and 78 deletions

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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")
}
}

14
main.go
View File

@ -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 {

View File

@ -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
}

View File

@ -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