diff options
| author | Eduardo M KALINOWSKI | 2019-02-16 18:55:54 -0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2019-03-26 20:56:37 +0100 | 
| commit | 3bc100aeb3f0763f78c8b3a70165b9f8aaa52db5 (patch) | |
| tree | 19b1576b0ee8e74612c29b0ff4cec4f3cecf2b29 /internal | |
| parent | 73f9e2374dab47374dc479911a9be5cfebf89378 (diff) | |
reverse mode: support wildcard exclude (--exclude-wildcard)
This adds support for gitignore-like wildcards and exclude patters in
reverse mode. It (somewhat) fixes #273: no regexp support, but the
syntax should be powerful enough to satisfy most needs.
Also, since adding a lot of --exclude options can be tedious, it adds
the --exclude-from option to read patterns from a file (or files).
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/fusefrontend/args.go | 9 | ||||
| -rw-r--r-- | internal/fusefrontend/fs.go | 8 | ||||
| -rw-r--r-- | internal/fusefrontend/xattr.go | 2 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/excluder.go | 66 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/excluder_test.go | 84 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/isexcluded_test.go | 26 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/mocks_test.go | 32 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/reverse_longnames.go | 2 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/rfile.go | 11 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/rfs.go | 203 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/rfs_test.go | 124 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/rpath.go | 16 | ||||
| -rw-r--r-- | internal/nametransform/longnames.go | 7 | ||||
| -rw-r--r-- | internal/nametransform/longnames_test.go | 8 | ||||
| -rw-r--r-- | internal/nametransform/names.go | 21 | 
15 files changed, 484 insertions, 135 deletions
| diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 5fb72cd..e767f28 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -30,6 +30,13 @@ type Args struct {  	SerializeReads bool  	// Force decode even if integrity check fails (openSSL only)  	ForceDecode bool -	// Exclude is a list of paths to make inaccessible +	// Exclude is a list of paths to make inaccessible, starting match at +	// the filesystem root  	Exclude []string +	// ExcludeWildcards is a list of paths to make inaccessible, matched +	// anywhere, and supporting wildcards +	ExcludeWildcard []string +	// ExcludeFrom is a list of files from which to read exclusion patterns +	// (with wildcard syntax) +	ExcludeFrom []string  } diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index ff628e5..2c40942 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -35,7 +35,7 @@ type FS struct {  	// states  	dirIVLock sync.RWMutex  	// Filename encryption helper -	nameTransform *nametransform.NameTransform +	nameTransform nametransform.NameTransformer  	// Content encryption helper  	contentEnc *contentenc.ContentEnc  	// This lock is used by openWriteOnlyFile() to block concurrent opens while @@ -62,7 +62,7 @@ type FS struct {  //var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.  // NewFS returns a new encrypted FUSE overlay filesystem. -func NewFS(args Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *FS { +func NewFS(args Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *FS {  	if args.SerializeReads {  		serialize_reads.InitSerializer()  	} @@ -399,7 +399,7 @@ func (fs *FS) decryptSymlinkTarget(cData64 string) (string, error) {  	if cData64 == "" {  		return "", nil  	} -	cData, err := fs.nameTransform.B64.DecodeString(cData64) +	cData, err := fs.nameTransform.B64DecodeString(cData64)  	if err != nil {  		return "", err  	} @@ -472,7 +472,7 @@ func (fs *FS) encryptSymlinkTarget(data string) (cData64 string) {  		return ""  	}  	cData := fs.contentEnc.EncryptBlock([]byte(data), 0, nil) -	cData64 = fs.nameTransform.B64.EncodeToString(cData) +	cData64 = fs.nameTransform.B64EncodeToString(cData)  	return cData64  } diff --git a/internal/fusefrontend/xattr.go b/internal/fusefrontend/xattr.go index 1de9cac..20e8db7 100644 --- a/internal/fusefrontend/xattr.go +++ b/internal/fusefrontend/xattr.go @@ -150,7 +150,7 @@ func (fs *FS) decryptXattrValue(cData []byte) (data []byte, err error) {  	}  	// This backward compatibility is needed to support old  	// file systems having xattr values base64-encoded. -	cData, err2 := fs.nameTransform.B64.DecodeString(string(cData)) +	cData, err2 := fs.nameTransform.B64DecodeString(string(cData))  	if err2 != nil {  		// Looks like the value was not base64-encoded, but just corrupt.  		// Return the original decryption error: err1 diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go new file mode 100644 index 0000000..5c0941a --- /dev/null +++ b/internal/fusefrontend_reverse/excluder.go @@ -0,0 +1,66 @@ +package fusefrontend_reverse + +import ( +	"io/ioutil" +	"os" +	"strings" + +	"github.com/rfjakob/gocryptfs/internal/exitcodes" +	"github.com/rfjakob/gocryptfs/internal/fusefrontend" +	"github.com/rfjakob/gocryptfs/internal/tlog" + +	"github.com/sabhiram/go-gitignore" +) + +// prepareExcluder creates an object to check if paths are excluded +// based on the patterns specified in the command line. +func (rfs *ReverseFS) prepareExcluder(args fusefrontend.Args) { +	if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { +		excluder, err := ignore.CompileIgnoreLines(getExclusionPatterns(args)...) +		if err != nil { +			tlog.Fatal.Printf("Error compiling exclusion rules: %q", err) +			os.Exit(exitcodes.ExcludeError) +		} +		rfs.excluder = excluder +	} +} + +// getExclusionPatters prepares a list of patterns to be excluded. +// Patterns passed in the -exclude command line option are prefixed +// with a leading '/' to preserve backwards compatibility (before +// wildcard matching was implemented, exclusions always were matched +// agains the full path). +func getExclusionPatterns(args fusefrontend.Args) []string { +	patterns := make([]string, len(args.Exclude)+len(args.ExcludeWildcard)) +	// add -exclude +	for i, p := range args.Exclude { +		patterns[i] = "/" + p +	} +	// add -exclude-wildcard +	copy(patterns[len(args.Exclude):], args.ExcludeWildcard) +	// add -exclude-from +	for _, file := range args.ExcludeFrom { +		lines, err := getLines(file) +		if err != nil { +			tlog.Fatal.Printf("Error reading exclusion patterns: %q", err) +			os.Exit(exitcodes.ExcludeError) +		} +		patterns = append(patterns, lines...) +	} +	return patterns +} + +// getLines reads a file and splits it into lines +func getLines(file string) ([]string, error) { +	buffer, err := ioutil.ReadFile(file) +	if err != nil { +		return nil, err +	} +	return strings.Split(string(buffer), "\n"), nil +} + +// isExcludedPlain finds out if the plaintext path "pPath" is +// excluded (used when -exclude is passed by the user). +func (rfs *ReverseFS) isExcludedPlain(pPath string) bool { +	return rfs.excluder != nil && rfs.excluder.MatchesPath(pPath) +} diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go new file mode 100644 index 0000000..e19b99f --- /dev/null +++ b/internal/fusefrontend_reverse/excluder_test.go @@ -0,0 +1,84 @@ +package fusefrontend_reverse + +import ( +	"io/ioutil" +	"os" +	"reflect" +	"testing" + +	"github.com/rfjakob/gocryptfs/internal/fusefrontend" +) + +func TestShouldNoCreateExcluderIfNoPattersWereSpecified(t *testing.T) { +	var rfs ReverseFS +	var args fusefrontend.Args +	rfs.prepareExcluder(args) +	if rfs.excluder != nil { +		t.Error("Should not have created excluder") +	} +} + +func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) { +	var args fusefrontend.Args +	args.Exclude = []string{"file1", "dir1/file2.txt"} +	args.ExcludeWildcard = []string{"*~", "build/*.o"} + +	expected := []string{"/file1", "/dir1/file2.txt", "*~", "build/*.o"} + +	patterns := getExclusionPatterns(args) +	if !reflect.DeepEqual(patterns, expected) { +		t.Errorf("expected %q, got %q", expected, patterns) +	} +} + +func TestShouldReadExcludePatternsFromFiles(t *testing.T) { +	tmpfile1, err := ioutil.TempFile("", "excludetest") +	if err != nil { +		t.Fatal(err) +	} +	exclude1 := tmpfile1.Name() +	defer os.Remove(exclude1) +	defer tmpfile1.Close() + +	tmpfile2, err := ioutil.TempFile("", "excludetest") +	if err != nil { +		t.Fatal(err) +	} +	exclude2 := tmpfile2.Name() +	defer os.Remove(exclude2) +	defer tmpfile2.Close() + +	tmpfile1.WriteString("file1.1\n") +	tmpfile1.WriteString("file1.2\n") +	tmpfile2.WriteString("file2.1\n") +	tmpfile2.WriteString("file2.2\n") + +	var args fusefrontend.Args +	args.ExcludeWildcard = []string{"cmdline1"} +	args.ExcludeFrom = []string{exclude1, exclude2} + +	// An empty string is returned for the last empty line +	// It's ignored when the patterns are actually compiled +	expected := []string{"cmdline1", "file1.1", "file1.2", "", "file2.1", "file2.2", ""} + +	patterns := getExclusionPatterns(args) +	if !reflect.DeepEqual(patterns, expected) { +		t.Errorf("expected %q, got %q", expected, patterns) +	} +} + +func TestShouldReturnFalseIfThereAreNoExclusions(t *testing.T) { +	var rfs ReverseFS +	if rfs.isExcludedPlain("any/path") { +		t.Error("Should not exclude any path if no exclusions were specified") +	} +} + +func TestShouldCallIgnoreParserToCheckExclusion(t *testing.T) { +	rfs, ignorerMock := createRFSWithMocks() + +	rfs.isExcludedPlain("some/path") +	if ignorerMock.calledWith != "some/path" { +		t.Error("Failed to call IgnoreParser") +	} +} diff --git a/internal/fusefrontend_reverse/isexcluded_test.go b/internal/fusefrontend_reverse/isexcluded_test.go deleted file mode 100644 index fc3831a..0000000 --- a/internal/fusefrontend_reverse/isexcluded_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package fusefrontend_reverse - -import ( -	"testing" -) - -func verifyExcluded(t *testing.T, rfs *ReverseFS, paths []string) { -	for _, p := range paths { -		if !rfs.isExcluded(p) { -			t.Errorf("Path %q should be excluded, but is not", p) -		} -	} -	if t.Failed() { -		t.Logf("cExclude = %#v", rfs.cExclude) -	} -} - -// Note: See also the integration tests in -// tests/reverse/exclude_test.go -func TestIsExcluded(t *testing.T) { -	var rfs ReverseFS -	// If the root directory is excluded, all files and subdirs should be excluded -	// as well -	rfs.cExclude = []string{""} -	verifyExcluded(t, &rfs, []string{"", "foo", "foo/bar"}) -} diff --git a/internal/fusefrontend_reverse/mocks_test.go b/internal/fusefrontend_reverse/mocks_test.go new file mode 100644 index 0000000..fd2f940 --- /dev/null +++ b/internal/fusefrontend_reverse/mocks_test.go @@ -0,0 +1,32 @@ +package fusefrontend_reverse + +import ( +	"github.com/rfjakob/gocryptfs/internal/nametransform" +) + +type IgnoreParserMock struct { +	toExclude  string +	calledWith string +} + +func (parser *IgnoreParserMock) MatchesPath(f string) bool { +	parser.calledWith = f +	return f == parser.toExclude +} + +type NameTransformMock struct { +	nametransform.NameTransform +} + +func (n *NameTransformMock) DecryptName(cipherName string, iv []byte) (string, error) { +	return "mockdecrypt_" + cipherName, nil +} + +func createRFSWithMocks() (*ReverseFS, *IgnoreParserMock) { +	ignorerMock := &IgnoreParserMock{} +	nameTransformMock := &NameTransformMock{} +	var rfs ReverseFS +	rfs.excluder = ignorerMock +	rfs.nameTransform = nameTransformMock +	return &rfs, ignorerMock +} diff --git a/internal/fusefrontend_reverse/reverse_longnames.go b/internal/fusefrontend_reverse/reverse_longnames.go index a425e64..9f044e8 100644 --- a/internal/fusefrontend_reverse/reverse_longnames.go +++ b/internal/fusefrontend_reverse/reverse_longnames.go @@ -106,7 +106,7 @@ func (rfs *ReverseFS) findLongnameParent(dir string, dirIV []byte, longname stri  func (rfs *ReverseFS) newNameFile(relPath string) (nodefs.File, fuse.Status) {  	dotName := filepath.Base(relPath)                                    // gocryptfs.longname.XYZ.name -	longname := dotName[:len(dotName)-len(nametransform.LongNameSuffix)] // gocryptfs.longname.XYZ +	longname := nametransform.RemoveLongNameSuffix(dotName) // gocryptfs.longname.XYZ  	// cipher directory  	cDir := nametransform.Dir(relPath)  	// plain directory diff --git a/internal/fusefrontend_reverse/rfile.go b/internal/fusefrontend_reverse/rfile.go index 75932cb..9c31681 100644 --- a/internal/fusefrontend_reverse/rfile.go +++ b/internal/fusefrontend_reverse/rfile.go @@ -34,19 +34,16 @@ type reverseFile struct {  var inodeTable syncmap.Map -// newFile decrypts and opens the path "relPath" and returns a reverseFile +// newFile receives a ciphered path "relPath" and its corresponding +// decrypted path "pRelPath", opens it and returns a reverseFile  // object. The backing file descriptor is always read-only. -func (rfs *ReverseFS) newFile(relPath string) (*reverseFile, fuse.Status) { -	if rfs.isExcluded(relPath) { +func (rfs *ReverseFS) newFile(relPath string, pRelPath string) (*reverseFile, fuse.Status) { +	if rfs.isExcludedPlain(pRelPath) {  		// Excluded paths should have been filtered out beforehand. Better safe  		// than sorry.  		tlog.Warn.Printf("BUG: newFile: received excluded path %q. This should not happen.", relPath)  		return nil, fuse.ENOENT  	} -	pRelPath, err := rfs.decryptPath(relPath) -	if err != nil { -		return nil, fuse.ToStatus(err) -	}  	dir := filepath.Dir(pRelPath)  	dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, dir)  	if err != nil { diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go index 6ee10fc..01ed122 100644 --- a/internal/fusefrontend_reverse/rfs.go +++ b/internal/fusefrontend_reverse/rfs.go @@ -2,9 +2,7 @@ package fusefrontend_reverse  import (  	"fmt" -	"os"  	"path/filepath" -	"strings"  	"syscall"  	"golang.org/x/sys/unix" @@ -16,13 +14,13 @@ import (  	"github.com/rfjakob/gocryptfs/internal/configfile"  	"github.com/rfjakob/gocryptfs/internal/contentenc"  	"github.com/rfjakob/gocryptfs/internal/cryptocore" -	"github.com/rfjakob/gocryptfs/internal/ctlsock" -	"github.com/rfjakob/gocryptfs/internal/exitcodes"  	"github.com/rfjakob/gocryptfs/internal/fusefrontend"  	"github.com/rfjakob/gocryptfs/internal/nametransform"  	"github.com/rfjakob/gocryptfs/internal/pathiv"  	"github.com/rfjakob/gocryptfs/internal/syscallcompat"  	"github.com/rfjakob/gocryptfs/internal/tlog" + +	"github.com/sabhiram/go-gitignore"  )  // ReverseFS implements the pathfs.FileSystem interface and provides an @@ -35,12 +33,11 @@ type ReverseFS struct {  	// Stores configuration arguments  	args fusefrontend.Args  	// Filename encryption helper -	nameTransform *nametransform.NameTransform +	nameTransform nametransform.NameTransformer  	// Content encryption helper  	contentEnc *contentenc.ContentEnc -	// Relative ciphertext paths to exclude (hide) from the user. Used by -exclude. -	// With -plaintextnames, these are relative *plaintext* paths. -	cExclude []string +	// Tests wheter a path is excluded (hiden) from the user. Used by -exclude. +	excluder ignore.IgnoreParser  }  var _ pathfs.FileSystem = &ReverseFS{} @@ -48,7 +45,7 @@ var _ pathfs.FileSystem = &ReverseFS{}  // NewFS returns an encrypted FUSE overlay filesystem.  // In this case (reverse mode) the backing directory is plain-text and  // ReverseFS provides an encrypted view. -func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *ReverseFS { +func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *ReverseFS {  	initLongnameCache()  	fs := &ReverseFS{  		// pathfs.defaultFileSystem returns ENOSYS for all operations @@ -58,34 +55,7 @@ func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.Na  		nameTransform: n,  		contentEnc:    c,  	} -	if len(args.Exclude) > 0 { -		for _, dirty := range args.Exclude { -			clean := ctlsock.SanitizePath(dirty) -			if clean != dirty { -				tlog.Warn.Printf("-exclude: non-canonical path %q has been interpreted as %q", dirty, clean) -			} -			if clean == "" { -				tlog.Fatal.Printf("-exclude: excluding the root dir %q makes no sense", clean) -				os.Exit(exitcodes.ExcludeError) -			} -			cPath, err := fs.EncryptPath(clean) -			if err != nil { -				tlog.Fatal.Printf("-exclude: EncryptPath %q failed: %v", clean, err) -				os.Exit(exitcodes.ExcludeError) -			} -			fs.cExclude = append(fs.cExclude, cPath) -			if !fs.args.PlaintextNames { -				// If we exclude -				//   gocryptfs.longname.3vZ_r3eDPb1_fL3j5VA4rd_bcKWLKT9eaxOVIGK5HFA -				// we should also exclude -				//   gocryptfs.longname.3vZ_r3eDPb1_fL3j5VA4rd_bcKWLKT9eaxOVIGK5HFA.name -				if nametransform.IsLongContent(filepath.Base(cPath)) { -					fs.cExclude = append(fs.cExclude, cPath+nametransform.LongNameSuffix) -				} -			} -		} -		tlog.Debug.Printf("-exclude: %v -> %v", fs.args.Exclude, fs.cExclude) -	} +	fs.prepareExcluder(args)  	return fs  } @@ -100,24 +70,64 @@ func relDir(path string) string {  	return dir  } -// isExcluded finds out if relative ciphertext path "relPath" is excluded -// (used when -exclude is passed by the user) -func (rfs *ReverseFS) isExcluded(relPath string) bool { -	for _, e := range rfs.cExclude { -		// If the root dir is excluded, everything is excluded. -		if e == "" { -			return true -		} -		// This exact path is excluded -		if e == relPath { -			return true -		} -		// Files inside an excluded directory are also excluded -		if strings.HasPrefix(relPath, e+"/") { -			return true +// getFileInfo returns information on a ciphertext path "relPath": +// - ftype: file type (as returned by getFileType) +// - excluded: if the path is excluded +// - pPath: if it's not a special file, the decrypted path +// - err: non nil if any error happens +func (rfs *ReverseFS) getFileInfo(relPath string) (ftype fileType, excluded bool, pPath string, err error) { +	ftype = rfs.getFileType(relPath) +	if ftype == typeConfig { +		excluded, pPath, err = false, "", nil +		return +	} +	if ftype == typeDiriv { +		parentDir := nametransform.Dir(relPath) +		_, excluded, _, err = rfs.getFileInfo(parentDir) +		pPath = "" +		return +	} +	if ftype == typeName { +		parentDir := nametransform.Dir(relPath) +		var parentExcluded bool +		_, parentExcluded, _, err = rfs.getFileInfo(parentDir) +		if parentExcluded || err != nil { +			excluded, pPath = parentExcluded, "" +			return  		} +		relPath = nametransform.RemoveLongNameSuffix(relPath)  	} -	return false +	pPath, err = rfs.decryptPath(relPath) +	excluded = err == nil && rfs.isExcludedPlain(pPath) +	return +} + +type fileType int + +// Values returned by getFileType +const ( +	// A regular file/directory/symlink +	typeRegular fileType = iota +	// A DirIV (gocryptfs.diriv) file +	typeDiriv +	// A .name file for a file with a long name +	typeName +	// The config file +	typeConfig +) + +// getFileType returns the type of file. Only the name is checked +func (rfs *ReverseFS) getFileType(cPath string) fileType { +	if rfs.isDirIV(cPath) { +		return typeDiriv +	} +	if rfs.isNameFile(cPath) { +		return typeName +	} +	if rfs.isTranslatedConfig(cPath) { +		return typeConfig +	} +	return typeRegular  }  // isDirIV determines if the path points to a gocryptfs.diriv file @@ -155,14 +165,18 @@ func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool {  // GetAttr - FUSE call  // "relPath" is the relative ciphertext path  func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) { -	if rfs.isExcluded(relPath) { +	ftype, excluded, pPath, err := rfs.getFileInfo(relPath) +	if excluded {  		return nil, fuse.ENOENT  	} +	if err != nil { +		return nil, fuse.ToStatus(err) +	}  	// Handle "gocryptfs.conf" -	if rfs.isTranslatedConfig(relPath) { +	if ftype == typeConfig {  		absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil)  		var st syscall.Stat_t -		err := syscall.Lstat(absConfPath, &st) +		err = syscall.Lstat(absConfPath, &st)  		if err != nil {  			return nil, fuse.ToStatus(err)  		} @@ -177,11 +191,11 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr  	var f nodefs.File  	var status fuse.Status  	virtual := false -	if rfs.isDirIV(relPath) { +	if ftype == typeDiriv {  		virtual = true  		f, status = rfs.newDirIVFile(relPath)  	} -	if rfs.isNameFile(relPath) { +	if ftype == typeName {  		virtual = true  		f, status = rfs.newNameFile(relPath)  	} @@ -197,7 +211,7 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr  		}  		return &a, status  	} -	dirfd, name, err := rfs.openBackingDir(relPath) +	dirfd, name, err := rfs.openBackingDir(pPath)  	if err != nil {  		return nil, fuse.ToStatus(err)  	} @@ -239,10 +253,14 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr  // Access - FUSE call  func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status { -	if rfs.isExcluded(relPath) { +	ftype, excluded, pPath, err := rfs.getFileInfo(relPath) +	if excluded {  		return fuse.ENOENT  	} -	if rfs.isTranslatedConfig(relPath) || rfs.isDirIV(relPath) || rfs.isNameFile(relPath) { +	if err != nil { +		return fuse.ToStatus(err) +	} +	if ftype != typeRegular {  		// access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX.  		ROK := uint32(0x4)  		// Virtual files can always be read and never written @@ -251,7 +269,7 @@ func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context)  		}  		return fuse.EPERM  	} -	dirfd, name, err := rfs.openBackingDir(relPath) +	dirfd, name, err := rfs.openBackingDir(pPath)  	if err != nil {  		return fuse.ToStatus(err)  	} @@ -262,19 +280,23 @@ func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context)  // Open - FUSE call  func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { -	if rfs.isExcluded(relPath) { +	ftype, excluded, pPath, err := rfs.getFileInfo(relPath) +	if excluded {  		return nil, fuse.ENOENT  	} -	if rfs.isTranslatedConfig(relPath) { +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	if ftype == typeConfig {  		return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context)  	} -	if rfs.isDirIV(relPath) { +	if ftype == typeDiriv {  		return rfs.newDirIVFile(relPath)  	} -	if rfs.isNameFile(relPath) { +	if ftype == typeName {  		return rfs.newNameFile(relPath)  	} -	return rfs.newFile(relPath) +	return rfs.newFile(relPath, pPath)  }  func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) { @@ -304,13 +326,16 @@ func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEn  // OpenDir - FUSE readdir call  func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { -	if rfs.isExcluded(cipherPath) { +	ftype, excluded, relPath, err := rfs.getFileInfo(cipherPath) +	if excluded {  		return nil, fuse.ENOENT  	} -	relPath, err := rfs.decryptPath(cipherPath)  	if err != nil {  		return nil, fuse.ToStatus(err)  	} +	if ftype != typeRegular { +		return nil, fuse.ENOTDIR +	}  	// Read plaintext dir  	dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(relPath))  	if err != nil { @@ -332,9 +357,11 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.  		if !status.Ok() {  			return nil, status  		} -		entries = rfs.excludeDirEntries(cipherPath, entries) +		entries = rfs.excludeDirEntries(relPath, entries)  		return entries, fuse.OK  	} +	// Filter out excluded entries +	entries = rfs.excludeDirEntries(relPath, entries)  	// Allocate maximum possible number of virtual files.  	// If all files have long names we need a virtual ".name" file for each,  	// plus one for gocryptfs.diriv. @@ -370,24 +397,22 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.  	}  	// Add virtual files  	entries = append(entries, virtualFiles[:nVirtual]...) -	// Filter out excluded entries -	entries = rfs.excludeDirEntries(cipherPath, entries)  	return entries, fuse.OK  }  // excludeDirEntries filters out directory entries that are "-exclude"d. -// cDir is the relative ciphertext path to the directory these entries are -// from. -func (rfs *ReverseFS) excludeDirEntries(cDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { -	if rfs.cExclude == nil { +// pDir is the relative plaintext path to the directory these entries are +// from. The entries should be plaintext files. +func (rfs *ReverseFS) excludeDirEntries(pDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { +	if rfs.excluder == nil {  		return entries  	}  	filtered = make([]fuse.DirEntry, 0, len(entries))  	for _, entry := range entries { -		// filepath.Join handles the case of cipherPath="" correctly: -		// Join("", "foo") -> "foo". This does not: cipherPath + "/" + name" -		p := filepath.Join(cDir, entry.Name) -		if rfs.isExcluded(p) { +		// filepath.Join handles the case of pDir="" correctly: +		// Join("", "foo") -> "foo". This does not: pDir + "/" + name" +		p := filepath.Join(pDir, entry.Name) +		if rfs.isExcludedPlain(p) {  			// Skip file  			continue  		} @@ -402,11 +427,12 @@ func (rfs *ReverseFS) excludeDirEntries(cDir string, entries []fuse.DirEntry) (f  // it's worth, so we just ignore the path and always return info about the  // backing storage root dir.  func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut { -	if rfs.isExcluded(relPath) { +	_, excluded, _, err := rfs.getFileInfo(relPath) +	if excluded || err != nil {  		return nil  	}  	var s syscall.Statfs_t -	err := syscall.Statfs(rfs.args.Cipherdir, &s) +	err = syscall.Statfs(rfs.args.Cipherdir, &s)  	if err != nil {  		return nil  	} @@ -417,10 +443,17 @@ func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut {  // Readlink - FUSE call  func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) { -	if rfs.isExcluded(relPath) { +	ftype, excluded, pPath, err := rfs.getFileInfo(relPath) +	if excluded {  		return "", fuse.ENOENT  	} -	dirfd, name, err := rfs.openBackingDir(relPath) +	if err != nil { +		return "", fuse.ToStatus(err) +	} +	if ftype != typeRegular { +		return "", fuse.EINVAL +	} +	dirfd, name, err := rfs.openBackingDir(pPath)  	if err != nil {  		return "", fuse.ToStatus(err)  	} @@ -436,7 +469,7 @@ func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, f  	nonce := pathiv.Derive(relPath, pathiv.PurposeSymlinkIV)  	// Symlinks are encrypted like file contents and base64-encoded  	cBinTarget := rfs.contentEnc.EncryptBlockNonce([]byte(plainTarget), 0, nil, nonce) -	cTarget := rfs.nameTransform.B64.EncodeToString(cBinTarget) +	cTarget := rfs.nameTransform.B64EncodeToString(cBinTarget)  	// The kernel will reject a symlink target above 4096 chars and return  	// and I/O error to the user. Better emit the proper error ourselves.  	if len(cTarget) > syscallcompat.PATH_MAX { diff --git a/internal/fusefrontend_reverse/rfs_test.go b/internal/fusefrontend_reverse/rfs_test.go new file mode 100644 index 0000000..eccaab9 --- /dev/null +++ b/internal/fusefrontend_reverse/rfs_test.go @@ -0,0 +1,124 @@ +package fusefrontend_reverse + +import ( +	"testing" + +	"github.com/rfjakob/gocryptfs/internal/configfile" +	"github.com/rfjakob/gocryptfs/internal/nametransform" +) + +func TestShouldDetectDirIV(t *testing.T) { +	var rfs ReverseFS +	ftype := rfs.getFileType("some/path/" + nametransform.DirIVFilename) +	if ftype != typeDiriv { +		t.Errorf("Expecting %d, got %d\n", typeDiriv, ftype) +	} +} + +func TestShouldDetectNameFile(t *testing.T) { +	var rfs ReverseFS +	ftype := rfs.getFileType("dir1/dir2/gocryptfs.longname.URrM8kgxTKYMgCk4hKk7RO9Lcfr30XQof4L_5bD9Iro=" + nametransform.LongNameSuffix) +	if ftype != typeName { +		t.Errorf("Expecting %d, got %d\n", typeName, ftype) +	} +} + +func TestShouldDetectConfigFile(t *testing.T) { +	var rfs ReverseFS +	ftype := rfs.getFileType(configfile.ConfDefaultName) +	if ftype != typeConfig { +		t.Errorf("Expecting %d, got %d\n", typeConfig, ftype) +	} +} + +func TestShouldDetectRegularFile(t *testing.T) { +	var rfs ReverseFS +	ftype := rfs.getFileType("documents/text_file.txt") +	if ftype != typeRegular { +		t.Errorf("Expecting %d, got %d\n", typeRegular, ftype) +	} +} + +// Note: For path exclusion, see also the integration tests in +// tests/reverse/exclude_test.go +func TestShouldNotCallIgnoreParserForTranslatedConfig(t *testing.T) { +	rfs, ignorerMock := createRFSWithMocks() + +	ftype, excluded, _, err := rfs.getFileInfo(configfile.ConfDefaultName) +	if err != nil { +		t.Errorf("Unexpected error %q\n", err) +	} +	if ftype != typeConfig { +		t.Errorf("Wrong file type, expecting %d, got %d\n", typeConfig, ftype) +	} +	if excluded { +		t.Error("Should not exclude translated config") +	} +	if ignorerMock.calledWith != "" { +		t.Error("Should not call IgnoreParser for translated config") +	} +} + +func TestShouldCheckIfParentIsExcludedForDirIV(t *testing.T) { +	rfs, ignorerMock := createRFSWithMocks() +	path := "dir" +	ignorerMock.toExclude = "mockdecrypt_dir" +	dirIV := path + "/" + nametransform.DirIVFilename + +	ftype, excluded, _, err := rfs.getFileInfo(dirIV) +	if err != nil { +		t.Errorf("Unexpected error %q\n", err) +	} +	if ftype != typeDiriv { +		t.Errorf("Wrong file type, expecting %d, got %d\n", typeDiriv, ftype) +	} +	if !excluded { +		t.Error("Should have excluded DirIV based on parent") +	} +	if ignorerMock.calledWith != "mockdecrypt_dir" { +		t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) +	} +} + +func TestShouldCheckIfParentIsExcludedForLongName(t *testing.T) { +	rfs, ignorerMock := createRFSWithMocks() +	path := "parent" +	ignorerMock.toExclude = "mockdecrypt_parent" +	dirIV := path + "/" + "gocryptfs.longname.fake.name" + +	ftype, excluded, _, err := rfs.getFileInfo(dirIV) +	if err != nil { +		t.Errorf("Unexpected error %q\n", err) +	} +	if ftype != typeName { +		t.Errorf("Wrong file type, expecting %d, got %d\n", typeName, ftype) +	} +	if !excluded { +		t.Error("Should have excluded LongName based on parent") +	} +	if ignorerMock.calledWith != "mockdecrypt_parent" { +		t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) +	} +} + +func TestShouldDecryptPathAndReturnTrueForExcludedPath(t *testing.T) { +	rfs, ignorerMock := createRFSWithMocks() +	ignorerMock.toExclude = "mockdecrypt_file.txt" + +	ftype, excluded, pPath, err := rfs.getFileInfo("file.txt") +	if err != nil { +		t.Errorf("Unexpected error %q\n", err) +	} +	if ftype != typeRegular { +		t.Errorf("Wrong file type, expecting %d, got %d\n", typeRegular, ftype) +	} +	if !excluded { +		t.Error("Should have excluded") +	} +	if pPath != "mockdecrypt_file.txt" { +		t.Errorf("Wrong pPath returned, got %q\n", pPath) +	} +	if ignorerMock.calledWith != "mockdecrypt_file.txt" { +		t.Error("Didn't call IgnoreParser with decrypted path") +	} +} diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go index 7115426..2da8379 100644 --- a/internal/fusefrontend_reverse/rpath.go +++ b/internal/fusefrontend_reverse/rpath.go @@ -96,16 +96,12 @@ func (rfs *ReverseFS) decryptPath(relPath string) (string, error) {  	return pRelPath, nil  } -// openBackingDir decrypt the relative ciphertext path "cRelPath", opens -// the directory that contains the target file/dir and returns the fd to -// the directory and the decrypted name of the target file. -// The fd/name pair is intended for use with fchownat and friends. -func (rfs *ReverseFS) openBackingDir(cRelPath string) (dirfd int, pName string, err error) { -	// Decrypt relative path -	pRelPath, err := rfs.decryptPath(cRelPath) -	if err != nil { -		return -1, "", err -	} +// openBackingDir receives an already decrypted relative path +// "pRelPath", opens the directory that contains the target file/dir +// and returns the fd to the directory and the decrypted name of the +// target file. The fd/name pair is intended for use with fchownat and +// friends. +func (rfs *ReverseFS) openBackingDir(pRelPath string) (dirfd int, pName string, err error) {  	// Open directory, safe against symlink races  	pDir := filepath.Dir(pRelPath)  	dirfd, err = syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, pDir) diff --git a/internal/nametransform/longnames.go b/internal/nametransform/longnames.go index 7a6d413..1bbcbb6 100644 --- a/internal/nametransform/longnames.go +++ b/internal/nametransform/longnames.go @@ -69,6 +69,13 @@ func IsLongContent(cName string) bool {  	return NameType(cName) == LongNameContent  } +// RemoveLongNameSuffix removes the ".name" suffix from cName, returning the corresponding +// content file name. +// No check is made if cName actually is a LongNameFilename. +func RemoveLongNameSuffix(cName string) string { +	return cName[:len(cName)-len(LongNameSuffix)] +} +  // ReadLongName - read cName + ".name" from the directory opened as dirfd.  //  // Symlink-safe through Openat(). diff --git a/internal/nametransform/longnames_test.go b/internal/nametransform/longnames_test.go index 8fa19fe..4210492 100644 --- a/internal/nametransform/longnames_test.go +++ b/internal/nametransform/longnames_test.go @@ -20,3 +20,11 @@ func TestIsLongName(t *testing.T) {  		t.Errorf("False positive")  	}  } + +func TestRemoveLongNameSuffix(t *testing.T) { +	filename := "gocryptfs.longname.LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=.name" +	content := "gocryptfs.longname.LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=" +	if RemoveLongNameSuffix(filename) != content { +		t.Error(".name suffix not removed") +	} +} diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 20fbede..d5c2c8b 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -17,6 +17,17 @@ const (  	NameMax = 255  ) +// NameTransformer is an interface used to transform filenames. +type NameTransformer interface { +	DecryptName(cipherName string, iv []byte) (string, error) +	EncryptName(plainName string, iv []byte) string +	EncryptAndHashName(name string, iv []byte) (string, error) +	HashLongName(name string) string +	WriteLongNameAt(dirfd int, hashName string, plainName string) error +	B64EncodeToString(src []byte) string +	B64DecodeString(s string) ([]byte, error) +} +  // NameTransform is used to transform filenames.  type NameTransform struct {  	emeCipher *eme.EMECipher @@ -88,3 +99,13 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s  	cipherName64 = n.B64.EncodeToString(bin)  	return cipherName64  } + +// B64EncodeToString returns a Base64-encoded string +func (n *NameTransform) B64EncodeToString(src []byte) string { +	return n.B64.EncodeToString(src) +} + +// B64DecodeString decodes a Base64-encoded string +func (n *NameTransform) B64DecodeString(s string) ([]byte, error) { +	return n.B64.DecodeString(s) +} | 
