diff options
| author | Jakob Unterwurzacher | 2020-05-17 19:31:04 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2020-05-17 19:31:04 +0200 | 
| commit | 416080203b4dd79de857eaf7c7cc97d050e00a9f (patch) | |
| tree | ed729c4cd365acc803a3d1e339eae8f1e8112f4c | |
| parent | ded4bbe6456dcfaa770f2c06df46d578fcbaa97e (diff) | |
main: accept multiple -passfile options
Each file will be read and then concatenated
for the effictive password. This can be used as a
kind of multi-factor authenticiton.
Fixes https://github.com/rfjakob/gocryptfs/issues/288
| -rw-r--r-- | Documentation/MANPAGE.md | 23 | ||||
| -rw-r--r-- | cli_args.go | 14 | ||||
| -rw-r--r-- | gocryptfs-xray/xray_main.go | 2 | ||||
| -rw-r--r-- | help.go | 2 | ||||
| -rw-r--r-- | init_dir.go | 2 | ||||
| -rw-r--r-- | internal/readpassword/extpass_test.go | 10 | ||||
| -rw-r--r-- | internal/readpassword/passfile.go | 12 | ||||
| -rw-r--r-- | internal/readpassword/passfile_test.go | 27 | ||||
| -rw-r--r-- | internal/readpassword/read.go | 14 | ||||
| -rw-r--r-- | main.go | 4 | ||||
| -rw-r--r-- | masterkey.go | 2 | ||||
| -rw-r--r-- | tests/cli/cli_test.go | 22 | 
12 files changed, 98 insertions, 36 deletions
| diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index e5a70b8..f9cf728 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -109,7 +109,7 @@ See also `-exclude`, `-exclude-wildcard` and the [EXCLUDING FILES](#excluding-fi  Enable (`-exec`) or disable (`-noexec`) executables in a gocryptfs mount  (default: `-exec`). If both are specified, `-noexec` takes precedence. -#### -extpass string +#### -extpass CMD [-extpass ARG1 ...]  Use an external program (like ssh-askpass) for the password prompt.  The program should return the password on stdout, a trailing newline is  stripped by gocryptfs. If you just want to read from a password file, see `-passfile`. @@ -302,14 +302,23 @@ built-in crypto is 4x slower unless your CPU has AES instructions and  you are using Go 1.6+. In mode "auto", gocrypts chooses the faster  option. -#### -passfile string -Read password from the specified file. A warning will be printed if there -is more than one line, and only the first line will be used. A single +#### -passfile FILE [-passfile FILE2 ...] +Read password from the specified plain text file. The file should contain exactly +one line (do not use binary files!). +A warning will be printed if there is more than one line, and only +the first line will be used. A single  trailing newline is allowed and does not cause a warning. -Before gocryptfs v1.7, using `-passfile` was equivant to writing -`-extpass="/bin/cat -- FILE"`. -gocryptfs v1.7 and later directly read the file without invoking `cat`. +Pass this option multiple times to read the first line from multiple +files. They are concatenated for the effective password. + +Example: + +    echo hello > hello.txt +    echo word > world.txt +    gocryptfs -passfile hello.txt -passfile world.txt + +The effective password will be "helloworld".  #### -passwd  Change the password. Will ask for the old password, check if it is diff --git a/cli_args.go b/cli_args.go index e5eebf8..e4073fa 100644 --- a/cli_args.go +++ b/cli_args.go @@ -32,9 +32,9 @@ type argContainer struct {  	// Mount options with opposites  	dev, nodev, suid, nosuid, exec, noexec, rw, ro bool  	masterkey, mountpoint, cipherdir, cpuprofile, -	memprofile, ko, passfile, ctlsock, fsname, force_owner, trace string -	// -extpass and -badname can be passed multiple times -	extpass, badname multipleStrings +	memprofile, ko, ctlsock, fsname, force_owner, trace string +	// -extpass, -badname, -passfile can be passed multiple times +	extpass, badname, passfile multipleStrings  	// For reverse mode, several ways to specify exclusions. All can be specified multiple times.  	exclude, excludeWildcard, excludeFrom multipleStrings  	// Configuration file name override @@ -184,7 +184,6 @@ func parseCliOpts() (args argContainer) {  	flagSet.StringVar(&args.cpuprofile, "cpuprofile", "", "Write cpu profile to specified file")  	flagSet.StringVar(&args.memprofile, "memprofile", "", "Write memory profile to specified file")  	flagSet.StringVar(&args.config, "config", "", "Use specified config file instead of CIPHERDIR/gocryptfs.conf") -	flagSet.StringVar(&args.passfile, "passfile", "", "Read password from file")  	flagSet.StringVar(&args.ko, "ko", "", "Pass additional options directly to the kernel, comma-separated list")  	flagSet.StringVar(&args.ctlsock, "ctlsock", "", "Create control socket at specified path")  	flagSet.StringVar(&args.fsname, "fsname", "", "Override the filesystem name") @@ -198,9 +197,10 @@ func parseCliOpts() (args argContainer) {  	flagSet.Var(&args.excludeWildcard, "exclude-wildcard", "Exclude path from reverse view, supporting wildcards")  	flagSet.Var(&args.excludeFrom, "exclude-from", "File from which to read exclusion patterns (with -exclude-wildcard syntax)") -	// -extpass +	// multipleStrings options ([]string)  	flagSet.Var(&args.extpass, "extpass", "Use external program for the password prompt")  	flagSet.Var(&args.badname, "badname", "Glob pattern invalid file names that should be shown") +	flagSet.Var(&args.passfile, "passfile", "Read password from file")  	flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+  		"successful mount - used internally for daemonization") @@ -267,11 +267,11 @@ func parseCliOpts() (args argContainer) {  		args.allow_other = false  		args.ko = "noexec"  	} -	if !args.extpass.Empty() && args.passfile != "" { +	if !args.extpass.Empty() && len(args.passfile) != 0 {  		tlog.Fatal.Printf("The options -extpass and -passfile cannot be used at the same time")  		os.Exit(exitcodes.Usage)  	} -	if args.passfile != "" && args.masterkey != "" { +	if len(args.passfile) != 0 && args.masterkey != "" {  		tlog.Fatal.Printf("The options -passfile and -masterkey cannot be used at the same time")  		os.Exit(exitcodes.Usage)  	} diff --git a/gocryptfs-xray/xray_main.go b/gocryptfs-xray/xray_main.go index ea000c0..7e928e7 100644 --- a/gocryptfs-xray/xray_main.go +++ b/gocryptfs-xray/xray_main.go @@ -105,7 +105,7 @@ func main() {  func dumpMasterKey(fn string) {  	tlog.Info.Enabled = false -	pw := readpassword.Once(nil, "", "") +	pw := readpassword.Once(nil, nil, "")  	masterkey, _, err := configfile.LoadAndDecrypt(fn, pw)  	if err != nil {  		fmt.Fprintln(os.Stderr, err) @@ -33,7 +33,7 @@ Common Options (use -hh to show all):    -masterkey         Mount with explicit master key instead of password    -nonempty          Allow mounting over non-empty directory    -nosyslog          Do not redirect log messages to syslog -  -passfile          Read password from file +  -passfile          Read password from plain text file(s)    -passwd            Change password    -plaintextnames    Do not encrypt file names (with -init)    -q, -quiet         Silence informational messages diff --git a/init_dir.go b/init_dir.go index 1bba59e..5939598 100644 --- a/init_dir.go +++ b/init_dir.go @@ -71,7 +71,7 @@ func initDir(args *argContainer) {  		tlog.Info.Printf("Choose a password for protecting your files.")  	}  	{ -		password := readpassword.Twice([]string(args.extpass), args.passfile) +		password := readpassword.Twice([]string(args.extpass), []string(args.passfile))  		creator := tlog.ProgramName + " " + GitVersion  		err = configfile.Create(args.config, password, args.plaintextnames,  			args.scryptn, creator, args.aessiv, args.devrandom) diff --git a/internal/readpassword/extpass_test.go b/internal/readpassword/extpass_test.go index b4ca8fa..9a643a5 100644 --- a/internal/readpassword/extpass_test.go +++ b/internal/readpassword/extpass_test.go @@ -26,7 +26,7 @@ func TestExtpass(t *testing.T) {  func TestOnceExtpass(t *testing.T) {  	p1 := "lkadsf0923rdfi48rqwhdsf" -	p2 := string(Once([]string{"echo " + p1}, "", "")) +	p2 := string(Once([]string{"echo " + p1}, nil, ""))  	if p1 != p2 {  		t.Errorf("p1=%q != p2=%q", p1, p2)  	} @@ -35,7 +35,7 @@ func TestOnceExtpass(t *testing.T) {  // extpass with two arguments  func TestOnceExtpass2(t *testing.T) {  	p1 := "foo" -	p2 := string(Once([]string{"echo", p1}, "", "")) +	p2 := string(Once([]string{"echo", p1}, nil, ""))  	if p1 != p2 {  		t.Errorf("p1=%q != p2=%q", p1, p2)  	} @@ -44,7 +44,7 @@ func TestOnceExtpass2(t *testing.T) {  // extpass with three arguments  func TestOnceExtpass3(t *testing.T) {  	p1 := "foo bar baz" -	p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, "", "")) +	p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, nil, ""))  	if p1 != p2 {  		t.Errorf("p1=%q != p2=%q", p1, p2)  	} @@ -52,7 +52,7 @@ func TestOnceExtpass3(t *testing.T) {  func TestOnceExtpassSpaces(t *testing.T) {  	p1 := "mypassword" -	p2 := string(Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, "", "")) +	p2 := string(Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, ""))  	if p1 != p2 {  		t.Errorf("p1=%q != p2=%q", p1, p2)  	} @@ -60,7 +60,7 @@ func TestOnceExtpassSpaces(t *testing.T) {  func TestTwiceExtpass(t *testing.T) {  	p1 := "w5w44t3wfe45srz434" -	p2 := string(Once([]string{"echo " + p1}, "", "")) +	p2 := string(Once([]string{"echo " + p1}, nil, ""))  	if p1 != p2 {  		t.Errorf("p1=%q != p2=%q", p1, p2)  	} diff --git a/internal/readpassword/passfile.go b/internal/readpassword/passfile.go index 73af279..df6cd4d 100644 --- a/internal/readpassword/passfile.go +++ b/internal/readpassword/passfile.go @@ -8,6 +8,16 @@ import (  	"github.com/rfjakob/gocryptfs/internal/tlog"  ) +// readPassFileConcatenate reads the first line from each file name and +// concatenates the results. The result does not contain any newlines. +func readPassFileConcatenate(passfileSlice []string) (result []byte) { +	for _, e := range passfileSlice { +		result = append(result, readPassFile(e)...) +	} +	return result +} + +// readPassFile reads the first line from the passed file name.  func readPassFile(passfile string) []byte {  	tlog.Info.Printf("passfile: reading from file %q", passfile)  	f, err := os.Open(passfile) @@ -36,7 +46,7 @@ func readPassFile(passfile string) []byte {  		os.Exit(exitcodes.ReadPassword)  	}  	if len(lines) > 1 && len(lines[1]) > 0 { -		tlog.Warn.Printf("passfile: ignoring trailing garbage (%d bytes) after first line", +		tlog.Warn.Printf("warning: passfile: ignoring trailing garbage (%d bytes) after first line",  			len(lines[1]))  	}  	return lines[0] diff --git a/internal/readpassword/passfile_test.go b/internal/readpassword/passfile_test.go index cb7fa44..dbfe159 100644 --- a/internal/readpassword/passfile_test.go +++ b/internal/readpassword/passfile_test.go @@ -21,13 +21,20 @@ func TestPassfile(t *testing.T) {  		if string(pw) != tc.want {  			t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)  		} +		// Calling readPassFileConcatenate with only one element should give the +		// same result +		pw = readPassFileConcatenate([]string{"passfile_test_files/" + tc.file}) +		if string(pw) != tc.want { +			t.Errorf("Wrong result: want=%q have=%q", tc.want, pw) +		}  	}  }  // readPassFile() should exit instead of returning an empty string.  //  // The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 . +// https://talks.golang.org/2014/testing.slide#23 , mirror: +// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23  func TestPassfileEmpty(t *testing.T) {  	if os.Getenv("TEST_SLAVE") == "1" {  		readPassFile("passfile_test_files/empty.txt") @@ -46,7 +53,8 @@ func TestPassfileEmpty(t *testing.T) {  // readPassFile() should exit instead of returning an empty string.  //  // The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 . +// https://talks.golang.org/2014/testing.slide#23 , mirror: +// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23  func TestPassfileNewline(t *testing.T) {  	if os.Getenv("TEST_SLAVE") == "1" {  		readPassFile("passfile_test_files/newline.txt") @@ -65,7 +73,8 @@ func TestPassfileNewline(t *testing.T) {  // readPassFile() should exit instead of returning an empty string.  //  // The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 . +// https://talks.golang.org/2014/testing.slide#23 , mirror: +// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23  func TestPassfileEmptyFirstLine(t *testing.T) {  	if os.Getenv("TEST_SLAVE") == "1" {  		readPassFile("passfile_test_files/empty_first_line.txt") @@ -79,3 +88,15 @@ func TestPassfileEmptyFirstLine(t *testing.T) {  	}  	t.Fatal("should have exited")  } + +// TestPassFileConcatenate tests readPassFileConcatenate +func TestPassFileConcatenate(t *testing.T) { +	files := []string{ +		"passfile_test_files/file with spaces.txt", +		"passfile_test_files/mypassword_garbage.txt", +	} +	res := string(readPassFileConcatenate(files)) +	if res != "mypasswordmypassword" { +		t.Errorf("wrong result: %q", res) +	} +} diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go index 92a0886..e116f0b 100644 --- a/internal/readpassword/read.go +++ b/internal/readpassword/read.go @@ -20,11 +20,11 @@ const (  	maxPasswordLen = 2048  ) -// Once tries to get a password from the user, either from the terminal, extpass +// Once tries to get a password from the user, either from the terminal, extpass, passfile  // or stdin. Leave "prompt" empty to use the default "Password: " prompt. -func Once(extpass []string, passfile string, prompt string) []byte { -	if passfile != "" { -		return readPassFile(passfile) +func Once(extpass []string, passfile []string, prompt string) []byte { +	if len(passfile) != 0 { +		return readPassFileConcatenate(passfile)  	}  	if len(extpass) != 0 {  		return readPasswordExtpass(extpass) @@ -40,9 +40,9 @@ func Once(extpass []string, passfile string, prompt string) []byte {  // Twice is the same as Once but will prompt twice if we get the password from  // the terminal. -func Twice(extpass []string, passfile string) []byte { -	if passfile != "" { -		return readPassFile(passfile) +func Twice(extpass []string, passfile []string) []byte { +	if len(passfile) != 0 { +		return readPassFileConcatenate(passfile)  	}  	if len(extpass) != 0 {  		return readPasswordExtpass(extpass) @@ -50,7 +50,7 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile,  	if masterkey != nil {  		return masterkey, cf, nil  	} -	pw := readpassword.Once([]string(args.extpass), args.passfile, "") +	pw := readpassword.Once([]string(args.extpass), []string(args.passfile), "")  	tlog.Info.Println("Decrypting master key")  	masterkey, err = cf.DecryptMasterKey(pw)  	for i := range pw { @@ -79,7 +79,7 @@ func changePassword(args *argContainer) {  			log.Panic("empty masterkey")  		}  		tlog.Info.Println("Please enter your new password.") -		newPw := readpassword.Twice([]string(args.extpass), args.passfile) +		newPw := readpassword.Twice([]string(args.extpass), []string(args.passfile))  		logN := confFile.ScryptObject.LogN()  		if args._explicitScryptn {  			logN = args.scryptn diff --git a/masterkey.go b/masterkey.go index 7b6779d..8d75c75 100644 --- a/masterkey.go +++ b/masterkey.go @@ -39,7 +39,7 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte {  func handleArgsMasterkey(args *argContainer) (masterkey []byte) {  	// "-masterkey=stdin"  	if args.masterkey == "stdin" { -		in := string(readpassword.Once(nil, "", "Masterkey")) +		in := string(readpassword.Once(nil, nil, "Masterkey"))  		return unhexMasterKey(in, true)  	}  	// "-masterkey=941a6029-3adc6a1c-..." diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index c6e86da..2484cf3 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -741,3 +741,25 @@ func TestBadname(t *testing.T) {  		t.Errorf("did not find invalid name %s in %v", invalid_file_name, names)  	}  } + +// TestPassfile tests the `-passfile` option +func TestPassfile(t *testing.T) { +	dir := test_helpers.InitFS(t) +	mnt := dir + ".mnt" +	passfile1 := mnt + ".1.txt" +	ioutil.WriteFile(passfile1, []byte("test"), 0600) +	test_helpers.MountOrFatal(t, dir, mnt, "-passfile="+passfile1) +	defer test_helpers.UnmountPanic(mnt) +} + +// TestPassfileX2 tests that the `-passfile` option can be passed twice +func TestPassfileX2(t *testing.T) { +	dir := test_helpers.InitFS(t) +	mnt := dir + ".mnt" +	passfile1 := mnt + ".1.txt" +	passfile2 := mnt + ".2.txt" +	ioutil.WriteFile(passfile1, []byte("te"), 0600) +	ioutil.WriteFile(passfile2, []byte("st"), 0600) +	test_helpers.MountOrFatal(t, dir, mnt, "-passfile="+passfile1, "-passfile="+passfile2) +	defer test_helpers.UnmountPanic(mnt) +} | 
