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/fusefrontend"
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||||
"github.com/rfjakob/gocryptfs/internal/prefer_openssl"
|
"github.com/rfjakob/gocryptfs/internal/prefer_openssl"
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/readpassword"
|
||||||
"github.com/rfjakob/gocryptfs/internal/toggledlog"
|
"github.com/rfjakob/gocryptfs/internal/toggledlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +39,6 @@ const (
|
|||||||
ERREXIT_CIPHERDIR = 6
|
ERREXIT_CIPHERDIR = 6
|
||||||
ERREXIT_INIT = 7
|
ERREXIT_INIT = 7
|
||||||
ERREXIT_LOADCONF = 8
|
ERREXIT_LOADCONF = 8
|
||||||
ERREXIT_PASSWORD = 9
|
|
||||||
ERREXIT_MOUNTPOINT = 10
|
ERREXIT_MOUNTPOINT = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ func initDir(args *argContainer) {
|
|||||||
} else {
|
} else {
|
||||||
toggledlog.Info.Printf("Using password provided via -extpass.")
|
toggledlog.Info.Printf("Using password provided via -extpass.")
|
||||||
}
|
}
|
||||||
password := readPasswordTwice(args.extpass)
|
password := readpassword.Twice(args.extpass)
|
||||||
creator := toggledlog.ProgramName + " " + GitVersion
|
creator := toggledlog.ProgramName + " " + GitVersion
|
||||||
err = configfile.CreateConfFile(args.config, password, args.plaintextnames, args.scryptn, creator)
|
err = configfile.CreateConfFile(args.config, password, args.plaintextnames, args.scryptn, creator)
|
||||||
if err != nil {
|
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)
|
toggledlog.Fatal.Printf(colorRed+"Config file not found: %v\n"+colorReset, err)
|
||||||
os.Exit(ERREXIT_LOADCONF)
|
os.Exit(ERREXIT_LOADCONF)
|
||||||
}
|
}
|
||||||
if args.extpass == "" {
|
pw := readpassword.Once(args.extpass)
|
||||||
fmt.Fprintf(os.Stderr, "Password: ")
|
toggledlog.Info.Println("Decrypting master key")
|
||||||
}
|
|
||||||
pw := readPassword(args.extpass)
|
|
||||||
toggledlog.Info.Printf("Decrypting master key... ")
|
|
||||||
masterkey, confFile, err = configfile.LoadConfFile(args.config, pw)
|
masterkey, confFile, err = configfile.LoadConfFile(args.config, pw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
toggledlog.Fatal.Println(colorRed + err.Error() + colorReset)
|
toggledlog.Fatal.Println(colorRed + err.Error() + colorReset)
|
||||||
os.Exit(ERREXIT_LOADCONF)
|
os.Exit(ERREXIT_LOADCONF)
|
||||||
}
|
}
|
||||||
toggledlog.Info.Printf("done.")
|
|
||||||
|
|
||||||
return masterkey, confFile
|
return masterkey, confFile
|
||||||
}
|
}
|
||||||
@ -140,7 +136,7 @@ func loadConfig(args *argContainer) (masterkey []byte, confFile *configfile.Conf
|
|||||||
func changePassword(args *argContainer) {
|
func changePassword(args *argContainer) {
|
||||||
masterkey, confFile := loadConfig(args)
|
masterkey, confFile := loadConfig(args)
|
||||||
toggledlog.Info.Println("Please enter your new password.")
|
toggledlog.Info.Println("Please enter your new password.")
|
||||||
newPw := readPasswordTwice(args.extpass)
|
newPw := readpassword.Twice(args.extpass)
|
||||||
confFile.EncryptKey(masterkey, newPw, confFile.ScryptObject.LogN())
|
confFile.EncryptKey(masterkey, newPw, confFile.ScryptObject.LogN())
|
||||||
err := confFile.WriteFile()
|
err := confFile.WriteFile()
|
||||||
if err != nil {
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Change password using "-extpass"
|
// Change password using "-extpass"
|
||||||
cmd2 := exec.Command(test_helpers.GocryptfsBinary, "-q", "-passwd", "-extpass", "echo test", dir)
|
cmd = exec.Command(test_helpers.GocryptfsBinary, "-q", "-passwd", "-extpass", "echo test", dir)
|
||||||
cmd2.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd2.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
err = cmd2.Run()
|
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 {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test -init & -config flag
|
// Test -init & -config flag
|
||||||
|
Loading…
Reference in New Issue
Block a user