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:
parent
218bf83ce3
commit
c89455063c
55
internal/readpassword/extpass_test.go
Normal file
55
internal/readpassword/extpass_test.go
Normal 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")
|
||||
}
|
133
internal/readpassword/read.go
Normal file
133
internal/readpassword/read.go
Normal 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)
|
||||
}
|
||||
}
|
100
internal/readpassword/stdin_test.go
Normal file
100
internal/readpassword/stdin_test.go
Normal 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
14
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 {
|
||||
|
64
password.go
64
password.go
@ -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
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user