From 416080203b4dd79de857eaf7c7cc97d050e00a9f Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 17 May 2020 19:31:04 +0200 Subject: 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 --- Documentation/MANPAGE.md | 23 ++++++++++++++++------- cli_args.go | 14 +++++++------- gocryptfs-xray/xray_main.go | 2 +- help.go | 2 +- init_dir.go | 2 +- internal/readpassword/extpass_test.go | 10 +++++----- internal/readpassword/passfile.go | 12 +++++++++++- internal/readpassword/passfile_test.go | 27 ++++++++++++++++++++++++--- internal/readpassword/read.go | 14 +++++++------- main.go | 4 ++-- masterkey.go | 2 +- 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) diff --git a/help.go b/help.go index 9df25b6..5cd35ac 100644 --- a/help.go +++ b/help.go @@ -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) diff --git a/main.go b/main.go index de627cb..11e15b2 100644 --- a/main.go +++ b/main.go @@ -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) +} -- cgit v1.2.3