diff options
| -rw-r--r-- | internal/fusefrontend/node_helpers.go | 10 | ||||
| -rw-r--r-- | internal/fusefrontend/root_node.go | 6 | ||||
| -rw-r--r-- | internal/nametransform/diriv.go | 50 | ||||
| -rw-r--r-- | internal/nametransform/names.go | 13 | ||||
| -rw-r--r-- | tests/cli/cli_test.go | 161 | 
5 files changed, 212 insertions, 28 deletions
| diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index b2f1d4a..f2d1e5e 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -121,7 +121,15 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy  		var iv []byte  		dirfd, iv = rn.dirCache.Lookup(n)  		if dirfd > 0 { -			cName, err := rn.nameTransform.EncryptAndHashName(child, iv) +			var cName string +			var err error +			if rn.nameTransform.HaveBadnamePatterns() { +				//BadName allowed, try to determine filenames +				cName, err = rn.nameTransform.EncryptAndHashBadName(child, iv, dirfd) +			} else { +				cName, err = rn.nameTransform.EncryptAndHashName(child, iv) +			} +  			if err != nil {  				return -1, "", fs.ToErrno(err)  			} diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index a830cc4..35b7be0 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -245,7 +245,11 @@ func (rn *RootNode) openBackingDir(relPath string) (dirfd int, cName string, err  			syscall.Close(dirfd)  			return -1, "", err  		} -		cName, err = rn.nameTransform.EncryptAndHashName(name, iv) +		if rn.nameTransform.HaveBadnamePatterns() { +			cName, err = rn.nameTransform.EncryptAndHashBadName(name, iv, dirfd) +		} else { +			cName, err = rn.nameTransform.EncryptAndHashName(name, iv) +		}  		if err != nil {  			syscall.Close(dirfd)  			return -1, "", err diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index 1d27aa5..d62b3fb 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -6,11 +6,13 @@ import (  	"io"  	"os"  	"path/filepath" +	"strings"  	"syscall"  	"github.com/rfjakob/gocryptfs/internal/cryptocore"  	"github.com/rfjakob/gocryptfs/internal/syscallcompat"  	"github.com/rfjakob/gocryptfs/internal/tlog" +	"golang.org/x/sys/unix"  )  const ( @@ -112,6 +114,54 @@ func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, err  	return cName, nil  } +// EncryptAndHashBadName tries to find the "name" substring, which (encrypted and hashed) +// leads to an unique existing file +// Returns ENOENT if cipher file does not exist or is not unique +func (be *NameTransform) EncryptAndHashBadName(name string, iv []byte, dirfd int) (cName string, err error) { +	var st unix.Stat_t +	var filesFound int +	lastFoundName, err := be.EncryptAndHashName(name, iv) +	if !strings.HasSuffix(name, BadNameFlag) || err != nil { +		//Default mode: same behaviour on error or no BadNameFlag on "name" +		return lastFoundName, err +	} +	//Default mode: Check if File extists without modifications +	err = syscallcompat.Fstatat(dirfd, lastFoundName, &st, unix.AT_SYMLINK_NOFOLLOW) +	if err == nil { +		//file found, return result +		return lastFoundName, nil +	} +	//BadName Mode: check if the name was tranformed without change (badname suffix and undecryptable cipher name) +	err = syscallcompat.Fstatat(dirfd, name[:len(name)-len(BadNameFlag)], &st, unix.AT_SYMLINK_NOFOLLOW) +	if err == nil { +		filesFound++ +		lastFoundName = name[:len(name)-len(BadNameFlag)] +	} +	// search for the longest badname pattern match +	for charpos := len(name) - len(BadNameFlag); charpos > 0; charpos-- { +		//only use original cipher name and append assumed suffix (without badname flag) +		cNamePart, err := be.EncryptName(name[:charpos], iv) +		if err != nil { +			//expand suffix on error +			continue +		} +		if be.longNames && len(cName) > NameMax { +			cNamePart = be.HashLongName(cName) +		} +		cNameBadReverse := cNamePart + name[charpos:len(name)-len(BadNameFlag)] +		err = syscallcompat.Fstatat(dirfd, cNameBadReverse, &st, unix.AT_SYMLINK_NOFOLLOW) +		if err == nil { +			filesFound++ +			lastFoundName = cNameBadReverse +		} +	} +	if filesFound == 1 { +		return lastFoundName, nil +	} +	// more than 1 possible file found, ignore +	return "", syscall.ENOENT +} +  // Dir is like filepath.Dir but returns "" instead of ".".  func Dir(path string) string {  	d := filepath.Dir(path) diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index ca28230..f730184 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -15,6 +15,8 @@ import (  const (  	// Like ext4, we allow at most 255 bytes for a file name.  	NameMax = 255 +	//BadNameFlag is appended to filenames in plain mode if a ciphername is inavlid but is shown +	BadNameFlag = " GOCRYPTFS_BAD_NAME"  )  // NameTransformer is an interface used to transform filenames. @@ -22,11 +24,13 @@ type NameTransformer interface {  	DecryptName(cipherName string, iv []byte) (string, error)  	EncryptName(plainName string, iv []byte) (string, error)  	EncryptAndHashName(name string, iv []byte) (string, error) +	EncryptAndHashBadName(name string, iv []byte, dirfd int) (string, error)  	// HashLongName - take the hash of a long string "name" and return  	// "gocryptfs.longname.[sha256]"  	//  	// This function does not do any I/O.  	HashLongName(name string) string +	HaveBadnamePatterns() bool  	WriteLongNameAt(dirfd int, hashName string, plainName string) error  	B64EncodeToString(src []byte) string  	B64DecodeString(s string) ([]byte, error) @@ -70,10 +74,10 @@ func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error  				for charpos := len(cipherName) - 1; charpos >= nameMin; charpos-- {  					res, err = n.decryptName(cipherName[:charpos], iv)  					if err == nil { -						return res + cipherName[charpos:] + " GOCRYPTFS_BAD_NAME", nil +						return res + cipherName[charpos:] + BadNameFlag, nil  					}  				} -				return cipherName + " GOCRYPTFS_BAD_NAME", nil +				return cipherName + BadNameFlag, nil  			}  		}  	} @@ -135,3 +139,8 @@ func (n *NameTransform) B64EncodeToString(src []byte) string {  func (n *NameTransform) B64DecodeString(s string) ([]byte, error) {  	return n.B64.DecodeString(s)  } + +// HaveBadnamePatterns returns true if BadName patterns were provided +func (n *NameTransform) HaveBadnamePatterns() bool { +	return len(n.BadnamePatterns) > 0 +} diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index 9248f5d..08c1b83 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -16,6 +16,7 @@ import (  	"github.com/rfjakob/gocryptfs/internal/configfile"  	"github.com/rfjakob/gocryptfs/internal/exitcodes" +	"github.com/rfjakob/gocryptfs/internal/nametransform"  	"github.com/rfjakob/gocryptfs/tests/test_helpers"  ) @@ -698,18 +699,29 @@ func TestSymlinkedCipherdir(t *testing.T) {  // TestBadname tests the `-badname` option  func TestBadname(t *testing.T) { +	//Supported structure of badname: <ciphername><badname pattern><badname suffix> +	//"Visible" shows the success of function DecryptName (cipher -> plain) +	//"Access" shows the success of function EncryptAndHashBadName (plain -> cipher) +	//Case    Visible  Access  Description +	//Case 1     x       x     Access file without BadName suffix (default mode) +	//Case 2     x       x     Access file with BadName suffix which has a valid cipher file (will only be possible if file was created without badname option) +	//Case 3                   Access file with valid ciphername + BadName suffix (impossible since this would not be produced by DecryptName) +	//Case 4     x       x     Access file with decryptable part of name and Badname suffix (default badname case) +	//Case 5     x       x     Access file with undecryptable name and BadName suffix (e. g. when part of the cipher name was cut) +	//Case 6     x             Access file with multiple possible matches. +	//Case 7                   Access file with BadName suffix and non-matching pattern +  	dir := test_helpers.InitFS(t)  	mnt := dir + ".mnt"  	validFileName := "file" -	invalidSuffix := ".invalid_file" - -	// use static suffix for testing -	test_helpers.MountOrFatal(t, dir, mnt, "-badname=*", "-extpass=echo test") -	defer test_helpers.UnmountPanic(mnt) +	invalidSuffix := "_invalid_file" +	var contentCipher [7][]byte +	//first mount without badname (see case 2) +	test_helpers.MountOrFatal(t, dir, mnt, "-extpass=echo test", "-wpanic=false") -	// write one valid filename (empty content)  	file := mnt + "/" + validFileName -	err := ioutil.WriteFile(file, nil, 0600) +	// Case 1: write one valid filename (empty content) +	err := ioutil.WriteFile(file, []byte("Content Case 1."), 0600)  	if err != nil {  		t.Fatal(err)  	} @@ -720,7 +732,6 @@ func TestBadname(t *testing.T) {  		t.Fatal(err)  	}  	defer fread.Close() -  	encryptedfilename := ""  	ciphernames, err := fread.Readdirnames(0)  	if err != nil { @@ -733,14 +744,64 @@ func TestBadname(t *testing.T) {  			break  		}  	} +	//Generate valid cipherdata for all cases +	for i := 0; i < len(contentCipher); i++ { +		err := ioutil.WriteFile(file, []byte(fmt.Sprintf("Content Case %d.", i+1)), 0600) +		if err != nil { +			t.Fatal(err) +		} +		//save the cipher data for file operations in cipher dir +		contentCipher[i], err = ioutil.ReadFile(dir + "/" + encryptedfilename) +		if err != nil { +			t.Fatal(err) +		} +	} -	// write invalid file which should be decodable -	err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, nil, 0600) +	//re-write content for case 1 +	err = ioutil.WriteFile(file, []byte("Content Case 1."), 0600)  	if err != nil {  		t.Fatal(err)  	} -	// write invalid file which is not decodable (cropping the encrpyted file name) -	err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix, nil, 0600) + +	// Case 2: File with invalid suffix in plain name but valid cipher file +	file = mnt + "/" + validFileName + nametransform.BadNameFlag +	err = ioutil.WriteFile(file, []byte("Content Case 2."), 0600) +	if err != nil { +		t.Fatal(err) +	} +	// unmount... +	test_helpers.UnmountPanic(mnt) + +	// ...and remount with -badname. +	test_helpers.MountOrFatal(t, dir, mnt, "-badname=*valid*", "-extpass=echo test", "-wpanic=false") +	defer test_helpers.UnmountPanic(mnt) + +	// Case 3 is impossible: only BadnameSuffix would mean the cipher name is valid + +	// Case 4: write invalid file which should be decodable +	err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, contentCipher[3], 0600) +	if err != nil { +		t.Fatal(err) +	} +	//Case 5: write invalid file which is not decodable (cropping the encrpyted file name) +	err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix, contentCipher[4], 0600) +	if err != nil { +		t.Fatal(err) +	} + +	// Case 6: Multiple possible matches +	// generate two files with invalid cipher names which can both match the badname pattern +	err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wPC"+invalidSuffix, contentCipher[5], 0600) +	if err != nil { +		t.Fatal(err) +	} +	err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wP"+invalidSuffix, contentCipher[5], 0600) +	if err != nil { +		t.Fatal(err) +	} + +	// Case 7: Non-Matching badname pattern +	err = ioutil.WriteFile(dir+"/"+encryptedfilename+"wrongPattern", contentCipher[6], 0600)  	if err != nil {  		t.Fatal(err)  	} @@ -755,22 +816,74 @@ func TestBadname(t *testing.T) {  	if err != nil {  		t.Fatal(err)  	} -	foundDecodable := false -	foundUndecodable := false + +	searchstrings := []string{ +		validFileName, +		validFileName + nametransform.BadNameFlag, +		"", +		validFileName + invalidSuffix + nametransform.BadNameFlag, +		encryptedfilename[:len(encryptedfilename)-2] + invalidSuffix + nametransform.BadNameFlag, +		"", +		validFileName + "wrongPattern" + nametransform.BadNameFlag} +	results := []bool{false, false, true, false, false, true, true} +	var filecontent string +	var filebytes []byte  	for _, name := range names { -		if strings.Contains(name, validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME") { -			foundDecodable = true -		} else if strings.Contains(name, encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix+" GOCRYPTFS_BAD_NAME") { -			foundUndecodable = true -		} -	} +		if name == searchstrings[0] { +			//Case 1: Test access +			filebytes, err = ioutil.ReadFile(mnt + "/" + name) +			if err != nil { +				t.Fatal(err) +			} +			filecontent = string(filebytes) +			if filecontent == "Content Case 1." { +				results[0] = true +			} -	if !foundDecodable { -		t.Errorf("did not find invalid name %s in %v", validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME", names) +		} else if name == searchstrings[1] { +			//Case 2: Test Access +			filebytes, err = ioutil.ReadFile(mnt + "/" + name) +			if err != nil { +				t.Fatal(err) +			} +			filecontent = string(filebytes) +			if filecontent == "Content Case 2." { +				results[1] = true +			} +		} else if name == searchstrings[3] { +			//Case 4: Test Access +			filebytes, err = ioutil.ReadFile(mnt + "/" + name) +			if err != nil { +				t.Fatal(err) +			} +			filecontent = string(filebytes) +			if filecontent == "Content Case 4." { +				results[3] = true +			} +		} else if name == searchstrings[4] { +			//Case 5: Test Access +			filebytes, err = ioutil.ReadFile(mnt + "/" + name) +			if err != nil { +				t.Fatal(err) +			} +			filecontent = string(filebytes) +			if filecontent == "Content Case 5." { +				results[4] = true +			} +		} else if name == searchstrings[6] { +			//Case 7 +			results[6] = false +		} +		//Case 3 is always passed +		//Case 6 is highly obscure: +		//The last part of a valid cipher name must match the badname pattern AND +		//the remaining cipher name must still be decryptable. Test case not programmable in a general case  	} -	if !foundUndecodable { -		t.Errorf("did not find invalid name %s in %v", encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix+" GOCRYPTFS_BAD_NAME", names) +	for i := 0; i < len(results); i++ { +		if !results[i] { +			t.Errorf("Case %d failed: '%s' in [%s]", i+1, searchstrings[i], strings.Join(names, ",")) +		}  	}  } | 
