diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 5ebb0b4..5802fb3 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -89,8 +89,19 @@ Enable (`-exec`) or disable (`-noexec`) executables in a gocryptfs mount #### -extpass string 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. Using something like "cat /mypassword.txt" allows -one to mount the gocryptfs filesystem without user interaction. +stripped by gocryptfs. If you just want to read from a password file, see `-passfile`. + +When `-extpass` is specified once, the string argument will be split on spaces. +For example, `-extpass "md5sum my password.txt"` will be executed as +`"md5sum" "my" "password.txt"`, which is NOT what you want. + +Specify `-extpass` twice or more to use the string arguments as-is. +For example, you DO want to call `md5sum` like this: +`-extpass "md5sum" -extpass "my password.txt"`. + +If you want to prevent splitting on spaces but don't want to pass arguments +to your program, use `"--"`, which is accepted by most programs: +`-extpass "my program" -extpass "--"` #### -fg, -f Stay in the foreground instead of forking away. Implies "-nosyslog". diff --git a/README.md b/README.md index 3a002cb..f61d49e 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,8 @@ v1.7, in progress (v1.7-beta1: 2019-01-03, v1.7-rc1: 2019-01-04) ([#320](https://github.com/rfjakob/gocryptfs/issues/320)). Prevents trouble in the unlikely case that gocryptfs is called with stdin,stdout and/or stderr closed. +* `-extpass` now can be specified multiple times to support arguments containing spaces + ([#289](https://github.com/rfjakob/gocryptfs/issues/289)) v1.6.1, 2018-12-12 * Fix "Operation not supported" chmod errors on Go 1.11 diff --git a/cli_args.go b/cli_args.go index a4da85c..425730a 100644 --- a/cli_args.go +++ b/cli_args.go @@ -32,9 +32,11 @@ type argContainer struct { sharedstorage, devrandom, fsck, trezor bool // Mount options with opposites dev, nodev, suid, nosuid, exec, noexec, rw, ro bool - masterkey, mountpoint, cipherdir, cpuprofile, extpass, + masterkey, mountpoint, cipherdir, cpuprofile, memprofile, ko, passfile, ctlsock, fsname, force_owner, trace string - // For reverse mode, --exclude is available. It can be specified multiple times. + // -extpass can be passed multiple times + extpass multipleStrings + // For reverse mode, -exclude is available. It can be specified multiple times. exclude multipleStrings // Configuration file name override config string @@ -62,6 +64,11 @@ func (s *multipleStrings) Set(val string) error { return nil } +func (s *multipleStrings) Empty() bool { + s2 := []string(*s) + return len(s2) == 0 +} + var flagSet *flag.FlagSet // prefixOArgs transform options passed via "-o foo,bar" into regular options @@ -179,7 +186,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.extpass, "extpass", "", "Use external program for the password prompt") 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") @@ -190,6 +196,8 @@ func parseCliOpts() (args argContainer) { // -e, --exclude flagSet.Var(&args.exclude, "e", "Alias for -exclude") flagSet.Var(&args.exclude, "exclude", "Exclude relative path from reverse view") + // -extpass + flagSet.Var(&args.extpass, "extpass", "Use external program for the password prompt") flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+ "successful mount - used internally for daemonization") @@ -248,7 +256,7 @@ func parseCliOpts() (args argContainer) { args.allow_other = false args.ko = "noexec" } - if args.extpass != "" && args.passfile != "" { + if !args.extpass.Empty() && args.passfile != "" { tlog.Fatal.Printf("The options -extpass and -passfile cannot be used at the same time") os.Exit(exitcodes.Usage) } @@ -256,11 +264,11 @@ func parseCliOpts() (args argContainer) { tlog.Fatal.Printf("The options -passfile and -masterkey cannot be used at the same time") os.Exit(exitcodes.Usage) } - if args.extpass != "" && args.masterkey != "" { + if !args.extpass.Empty() && args.masterkey != "" { tlog.Fatal.Printf("The options -extpass and -masterkey cannot be used at the same time") os.Exit(exitcodes.Usage) } - if args.extpass != "" && args.trezor { + if !args.extpass.Empty() && args.trezor { tlog.Fatal.Printf("The options -extpass and -trezor 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 73b1e18..df92f2d 100644 --- a/gocryptfs-xray/xray_main.go +++ b/gocryptfs-xray/xray_main.go @@ -68,7 +68,7 @@ func main() { func dumpMasterKey(fn string) { tlog.Info.Enabled = false - pw := readpassword.Once("", "", "") + pw := readpassword.Once(nil, "", "") masterkey, _, err := configfile.LoadAndDecrypt(fn, pw) if err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/init_dir.go b/init_dir.go index ecfec9d..c3aa4b5 100644 --- a/init_dir.go +++ b/init_dir.go @@ -68,7 +68,7 @@ func initDir(args *argContainer) { } } // Choose password for config file - if args.extpass == "" { + if args.extpass.Empty() { tlog.Info.Printf("Choose a password for protecting your files.") } { @@ -80,7 +80,7 @@ func initDir(args *argContainer) { password = readpassword.Trezor(trezorPayload) } else { // Normal password entry - password = readpassword.Twice(args.extpass, args.passfile) + password = readpassword.Twice([]string(args.extpass), args.passfile) readpassword.CheckTrailingGarbage() } creator := tlog.ProgramName + " " + GitVersion diff --git a/internal/readpassword/extpass_test.go b/internal/readpassword/extpass_test.go index 037d111..b4ca8fa 100644 --- a/internal/readpassword/extpass_test.go +++ b/internal/readpassword/extpass_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { func TestExtpass(t *testing.T) { p1 := "ads2q4tw41reg52" - p2 := string(readPasswordExtpass("echo " + p1)) + p2 := string(readPasswordExtpass([]string{"echo " + p1})) if p1 != p2 { t.Errorf("p1=%q != p2=%q", p1, p2) } @@ -26,7 +26,33 @@ func TestExtpass(t *testing.T) { func TestOnceExtpass(t *testing.T) { p1 := "lkadsf0923rdfi48rqwhdsf" - p2 := string(Once("echo "+p1, "", "")) + p2 := string(Once([]string{"echo " + p1}, "", "")) + if p1 != p2 { + t.Errorf("p1=%q != p2=%q", p1, p2) + } +} + +// extpass with two arguments +func TestOnceExtpass2(t *testing.T) { + p1 := "foo" + p2 := string(Once([]string{"echo", p1}, "", "")) + if p1 != p2 { + t.Errorf("p1=%q != p2=%q", p1, p2) + } +} + +// extpass with three arguments +func TestOnceExtpass3(t *testing.T) { + p1 := "foo bar baz" + p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, "", "")) + if p1 != p2 { + t.Errorf("p1=%q != p2=%q", p1, p2) + } +} + +func TestOnceExtpassSpaces(t *testing.T) { + p1 := "mypassword" + p2 := string(Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, "", "")) if p1 != p2 { t.Errorf("p1=%q != p2=%q", p1, p2) } @@ -34,7 +60,7 @@ func TestOnceExtpass(t *testing.T) { func TestTwiceExtpass(t *testing.T) { p1 := "w5w44t3wfe45srz434" - p2 := string(Once("echo "+p1, "", "")) + p2 := string(Once([]string{"echo " + p1}, "", "")) if p1 != p2 { t.Errorf("p1=%q != p2=%q", p1, p2) } @@ -46,7 +72,7 @@ func TestTwiceExtpass(t *testing.T) { // https://talks.golang.org/2014/testing.slide#23 . func TestExtpassEmpty(t *testing.T) { if os.Getenv("TEST_SLAVE") == "1" { - readPasswordExtpass("echo") + readPasswordExtpass([]string{"echo"}) return } cmd := exec.Command(os.Args[0], "-test.run=TestExtpassEmpty$") diff --git a/internal/readpassword/passfile_test.go b/internal/readpassword/passfile_test.go index 30f976f..cb7fa44 100644 --- a/internal/readpassword/passfile_test.go +++ b/internal/readpassword/passfile_test.go @@ -14,6 +14,7 @@ func TestPassfile(t *testing.T) { {"mypassword.txt", "mypassword"}, {"mypassword_garbage.txt", "mypassword"}, {"mypassword_missing_newline.txt", "mypassword"}, + {"file with spaces.txt", "mypassword"}, } for _, tc := range testcases { pw := readPassFile("passfile_test_files/" + tc.file) diff --git a/internal/readpassword/passfile_test_files/file with spaces.txt b/internal/readpassword/passfile_test_files/file with spaces.txt new file mode 100644 index 0000000..48d23cf --- /dev/null +++ b/internal/readpassword/passfile_test_files/file with spaces.txt @@ -0,0 +1 @@ +mypassword diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go index 0378e53..060100b 100644 --- a/internal/readpassword/read.go +++ b/internal/readpassword/read.go @@ -24,11 +24,11 @@ const ( // Once tries to get a password from the user, either from the terminal, extpass // or stdin. Leave "prompt" empty to use the default "Password: " prompt. -func Once(extpass string, passfile string, prompt string) []byte { +func Once(extpass []string, passfile string, prompt string) []byte { if passfile != "" { return readPassFile(passfile) } - if extpass != "" { + if len(extpass) != 0 { return readPasswordExtpass(extpass) } if prompt == "" { @@ -42,11 +42,11 @@ 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 { +func Twice(extpass []string, passfile string) []byte { if passfile != "" { return readPassFile(passfile) } - if extpass != "" { + if len(extpass) != 0 { return readPasswordExtpass(extpass) } if !terminal.IsTerminal(int(os.Stdin.Fd())) { @@ -99,9 +99,14 @@ func readPasswordStdin(prompt string) []byte { // readPasswordExtpass executes the "extpass" program and returns the first line // of the output. // Exits on read error or empty result. -func readPasswordExtpass(extpass string) []byte { - tlog.Info.Println("Reading password from extpass program") - parts := strings.Split(extpass, " ") +func readPasswordExtpass(extpass []string) []byte { + var parts []string + if len(extpass) == 1 { + parts = strings.Split(extpass[0], " ") + } else { + parts = extpass + } + tlog.Info.Printf("Reading password from extpass program %q, arguments: %q\n", parts[0], parts[1:]) cmd := exec.Command(parts[0], parts[1:]...) cmd.Stderr = os.Stderr pipe, err := cmd.StdoutPipe() diff --git a/main.go b/main.go index a376356..09c8ed7 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile, pw = readpassword.Trezor(cf.TrezorPayload) } else { // Normal password entry - pw = readpassword.Once(args.extpass, args.passfile, "") + pw = readpassword.Once([]string(args.extpass), args.passfile, "") } tlog.Info.Println("Decrypting master key") masterkey, err = cf.DecryptMasterKey(pw) @@ -93,7 +93,7 @@ func changePassword(args *argContainer) { log.Panic("empty masterkey") } tlog.Info.Println("Please enter your new password.") - newPw := readpassword.Twice(args.extpass, args.passfile) + newPw := readpassword.Twice([]string(args.extpass), args.passfile) readpassword.CheckTrailingGarbage() confFile.EncryptKey(masterkey, newPw, confFile.ScryptObject.LogN()) for i := range newPw { diff --git a/masterkey.go b/masterkey.go index 332a673..8392bc6 100644 --- a/masterkey.go +++ b/masterkey.go @@ -43,7 +43,7 @@ func getMasterKey(args *argContainer) (masterkey []byte, confFile *configfile.Co masterkeyFromStdin := false // "-masterkey=stdin" if args.masterkey == "stdin" { - args.masterkey = string(readpassword.Once("", "", "Masterkey")) + args.masterkey = string(readpassword.Once(nil, "", "Masterkey")) masterkeyFromStdin = true } // "-masterkey=941a6029-3adc6a1c-..."