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
(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

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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]

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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-..."

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)
}
}
// 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)
}