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
This commit is contained in:
Jakob Unterwurzacher 2020-05-17 19:31:04 +02:00
parent ded4bbe645
commit 416080203b
12 changed files with 98 additions and 36 deletions

View File

@ -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 Enable (`-exec`) or disable (`-noexec`) executables in a gocryptfs mount
(default: `-exec`). If both are specified, `-noexec` takes precedence. (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. Use an external program (like ssh-askpass) for the password prompt.
The program should return the password on stdout, a trailing newline is 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`. 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 you are using Go 1.6+. In mode "auto", gocrypts chooses the faster
option. option.
#### -passfile string #### -passfile FILE [-passfile FILE2 ...]
Read password from the specified file. A warning will be printed if there Read password from the specified plain text file. The file should contain exactly
is more than one line, and only the first line will be used. A single 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. trailing newline is allowed and does not cause a warning.
Before gocryptfs v1.7, using `-passfile` was equivant to writing Pass this option multiple times to read the first line from multiple
`-extpass="/bin/cat -- FILE"`. files. They are concatenated for the effective password.
gocryptfs v1.7 and later directly read the file without invoking `cat`.
Example:
echo hello > hello.txt
echo word > world.txt
gocryptfs -passfile hello.txt -passfile world.txt
The effective password will be "helloworld".
#### -passwd #### -passwd
Change the password. Will ask for the old password, check if it is Change the password. Will ask for the old password, check if it is

View File

@ -32,9 +32,9 @@ type argContainer struct {
// Mount options with opposites // Mount options with opposites
dev, nodev, suid, nosuid, exec, noexec, rw, ro bool dev, nodev, suid, nosuid, exec, noexec, rw, ro bool
masterkey, mountpoint, cipherdir, cpuprofile, masterkey, mountpoint, cipherdir, cpuprofile,
memprofile, ko, passfile, ctlsock, fsname, force_owner, trace string memprofile, ko, ctlsock, fsname, force_owner, trace string
// -extpass and -badname can be passed multiple times // -extpass, -badname, -passfile can be passed multiple times
extpass, badname multipleStrings extpass, badname, passfile multipleStrings
// For reverse mode, several ways to specify exclusions. All can be specified multiple times. // For reverse mode, several ways to specify exclusions. All can be specified multiple times.
exclude, excludeWildcard, excludeFrom multipleStrings exclude, excludeWildcard, excludeFrom multipleStrings
// Configuration file name override // 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.cpuprofile, "cpuprofile", "", "Write cpu profile to specified file")
flagSet.StringVar(&args.memprofile, "memprofile", "", "Write memory 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.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.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.ctlsock, "ctlsock", "", "Create control socket at specified path")
flagSet.StringVar(&args.fsname, "fsname", "", "Override the filesystem name") 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.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)") 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.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.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 "+ flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+
"successful mount - used internally for daemonization") "successful mount - used internally for daemonization")
@ -267,11 +267,11 @@ func parseCliOpts() (args argContainer) {
args.allow_other = false args.allow_other = false
args.ko = "noexec" 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") tlog.Fatal.Printf("The options -extpass and -passfile cannot be used at the same time")
os.Exit(exitcodes.Usage) 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") tlog.Fatal.Printf("The options -passfile and -masterkey cannot be used at the same time")
os.Exit(exitcodes.Usage) os.Exit(exitcodes.Usage)
} }

View File

@ -105,7 +105,7 @@ func main() {
func dumpMasterKey(fn string) { func dumpMasterKey(fn string) {
tlog.Info.Enabled = false tlog.Info.Enabled = false
pw := readpassword.Once(nil, "", "") pw := readpassword.Once(nil, nil, "")
masterkey, _, err := configfile.LoadAndDecrypt(fn, pw) masterkey, _, err := configfile.LoadAndDecrypt(fn, pw)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)

View File

@ -33,7 +33,7 @@ Common Options (use -hh to show all):
-masterkey Mount with explicit master key instead of password -masterkey Mount with explicit master key instead of password
-nonempty Allow mounting over non-empty directory -nonempty Allow mounting over non-empty directory
-nosyslog Do not redirect log messages to syslog -nosyslog Do not redirect log messages to syslog
-passfile Read password from file -passfile Read password from plain text file(s)
-passwd Change password -passwd Change password
-plaintextnames Do not encrypt file names (with -init) -plaintextnames Do not encrypt file names (with -init)
-q, -quiet Silence informational messages -q, -quiet Silence informational messages

View File

@ -71,7 +71,7 @@ func initDir(args *argContainer) {
tlog.Info.Printf("Choose a password for protecting your files.") 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 creator := tlog.ProgramName + " " + GitVersion
err = configfile.Create(args.config, password, args.plaintextnames, err = configfile.Create(args.config, password, args.plaintextnames,
args.scryptn, creator, args.aessiv, args.devrandom) args.scryptn, creator, args.aessiv, args.devrandom)

View File

@ -26,7 +26,7 @@ func TestExtpass(t *testing.T) {
func TestOnceExtpass(t *testing.T) { func TestOnceExtpass(t *testing.T) {
p1 := "lkadsf0923rdfi48rqwhdsf" p1 := "lkadsf0923rdfi48rqwhdsf"
p2 := string(Once([]string{"echo " + p1}, "", "")) p2 := string(Once([]string{"echo " + p1}, nil, ""))
if p1 != p2 { if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2) t.Errorf("p1=%q != p2=%q", p1, p2)
} }
@ -35,7 +35,7 @@ func TestOnceExtpass(t *testing.T) {
// extpass with two arguments // extpass with two arguments
func TestOnceExtpass2(t *testing.T) { func TestOnceExtpass2(t *testing.T) {
p1 := "foo" p1 := "foo"
p2 := string(Once([]string{"echo", p1}, "", "")) p2 := string(Once([]string{"echo", p1}, nil, ""))
if p1 != p2 { if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2) t.Errorf("p1=%q != p2=%q", p1, p2)
} }
@ -44,7 +44,7 @@ func TestOnceExtpass2(t *testing.T) {
// extpass with three arguments // extpass with three arguments
func TestOnceExtpass3(t *testing.T) { func TestOnceExtpass3(t *testing.T) {
p1 := "foo bar baz" p1 := "foo bar baz"
p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, "", "")) p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, nil, ""))
if p1 != p2 { if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2) t.Errorf("p1=%q != p2=%q", p1, p2)
} }
@ -52,7 +52,7 @@ func TestOnceExtpass3(t *testing.T) {
func TestOnceExtpassSpaces(t *testing.T) { func TestOnceExtpassSpaces(t *testing.T) {
p1 := "mypassword" 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 { if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2) t.Errorf("p1=%q != p2=%q", p1, p2)
} }
@ -60,7 +60,7 @@ func TestOnceExtpassSpaces(t *testing.T) {
func TestTwiceExtpass(t *testing.T) { func TestTwiceExtpass(t *testing.T) {
p1 := "w5w44t3wfe45srz434" p1 := "w5w44t3wfe45srz434"
p2 := string(Once([]string{"echo " + p1}, "", "")) p2 := string(Once([]string{"echo " + p1}, nil, ""))
if p1 != p2 { if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2) t.Errorf("p1=%q != p2=%q", p1, p2)
} }

View File

@ -8,6 +8,16 @@ import (
"github.com/rfjakob/gocryptfs/internal/tlog" "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 { func readPassFile(passfile string) []byte {
tlog.Info.Printf("passfile: reading from file %q", passfile) tlog.Info.Printf("passfile: reading from file %q", passfile)
f, err := os.Open(passfile) f, err := os.Open(passfile)
@ -36,7 +46,7 @@ func readPassFile(passfile string) []byte {
os.Exit(exitcodes.ReadPassword) os.Exit(exitcodes.ReadPassword)
} }
if len(lines) > 1 && len(lines[1]) > 0 { 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])) len(lines[1]))
} }
return lines[0] return lines[0]

View File

@ -21,13 +21,20 @@ func TestPassfile(t *testing.T) {
if string(pw) != tc.want { if string(pw) != tc.want {
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw) 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. // readPassFile() should exit instead of returning an empty string.
// //
// The TEST_SLAVE magic is explained at // 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) { func TestPassfileEmpty(t *testing.T) {
if os.Getenv("TEST_SLAVE") == "1" { if os.Getenv("TEST_SLAVE") == "1" {
readPassFile("passfile_test_files/empty.txt") readPassFile("passfile_test_files/empty.txt")
@ -46,7 +53,8 @@ func TestPassfileEmpty(t *testing.T) {
// readPassFile() should exit instead of returning an empty string. // readPassFile() should exit instead of returning an empty string.
// //
// The TEST_SLAVE magic is explained at // 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) { func TestPassfileNewline(t *testing.T) {
if os.Getenv("TEST_SLAVE") == "1" { if os.Getenv("TEST_SLAVE") == "1" {
readPassFile("passfile_test_files/newline.txt") readPassFile("passfile_test_files/newline.txt")
@ -65,7 +73,8 @@ func TestPassfileNewline(t *testing.T) {
// readPassFile() should exit instead of returning an empty string. // readPassFile() should exit instead of returning an empty string.
// //
// The TEST_SLAVE magic is explained at // 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) { func TestPassfileEmptyFirstLine(t *testing.T) {
if os.Getenv("TEST_SLAVE") == "1" { if os.Getenv("TEST_SLAVE") == "1" {
readPassFile("passfile_test_files/empty_first_line.txt") readPassFile("passfile_test_files/empty_first_line.txt")
@ -79,3 +88,15 @@ func TestPassfileEmptyFirstLine(t *testing.T) {
} }
t.Fatal("should have exited") 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)
}
}

View File

@ -20,11 +20,11 @@ const (
maxPasswordLen = 2048 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. // 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 != "" { if len(passfile) != 0 {
return readPassFile(passfile) return readPassFileConcatenate(passfile)
} }
if len(extpass) != 0 { if len(extpass) != 0 {
return readPasswordExtpass(extpass) 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 // Twice is the same as Once but will prompt twice if we get the password from
// the terminal. // the terminal.
func Twice(extpass []string, passfile string) []byte { func Twice(extpass []string, passfile []string) []byte {
if passfile != "" { if len(passfile) != 0 {
return readPassFile(passfile) return readPassFileConcatenate(passfile)
} }
if len(extpass) != 0 { if len(extpass) != 0 {
return readPasswordExtpass(extpass) return readPasswordExtpass(extpass)

View File

@ -50,7 +50,7 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile,
if masterkey != nil { if masterkey != nil {
return masterkey, cf, 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") tlog.Info.Println("Decrypting master key")
masterkey, err = cf.DecryptMasterKey(pw) masterkey, err = cf.DecryptMasterKey(pw)
for i := range pw { for i := range pw {
@ -79,7 +79,7 @@ func changePassword(args *argContainer) {
log.Panic("empty masterkey") log.Panic("empty masterkey")
} }
tlog.Info.Println("Please enter your new password.") 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() logN := confFile.ScryptObject.LogN()
if args._explicitScryptn { if args._explicitScryptn {
logN = args.scryptn logN = args.scryptn

View File

@ -39,7 +39,7 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte {
func handleArgsMasterkey(args *argContainer) (masterkey []byte) { func handleArgsMasterkey(args *argContainer) (masterkey []byte) {
// "-masterkey=stdin" // "-masterkey=stdin"
if args.masterkey == "stdin" { if args.masterkey == "stdin" {
in := string(readpassword.Once(nil, "", "Masterkey")) in := string(readpassword.Once(nil, nil, "Masterkey"))
return unhexMasterKey(in, true) return unhexMasterKey(in, true)
} }
// "-masterkey=941a6029-3adc6a1c-..." // "-masterkey=941a6029-3adc6a1c-..."

View File

@ -741,3 +741,25 @@ func TestBadname(t *testing.T) {
t.Errorf("did not find invalid name %s in %v", invalid_file_name, names) 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)
}