summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/fusefrontend/args.go9
-rw-r--r--internal/fusefrontend/fs.go8
-rw-r--r--internal/fusefrontend/xattr.go2
-rw-r--r--internal/fusefrontend_reverse/excluder.go66
-rw-r--r--internal/fusefrontend_reverse/excluder_test.go84
-rw-r--r--internal/fusefrontend_reverse/isexcluded_test.go26
-rw-r--r--internal/fusefrontend_reverse/mocks_test.go32
-rw-r--r--internal/fusefrontend_reverse/reverse_longnames.go2
-rw-r--r--internal/fusefrontend_reverse/rfile.go11
-rw-r--r--internal/fusefrontend_reverse/rfs.go203
-rw-r--r--internal/fusefrontend_reverse/rfs_test.go124
-rw-r--r--internal/fusefrontend_reverse/rpath.go16
-rw-r--r--internal/nametransform/longnames.go7
-rw-r--r--internal/nametransform/longnames_test.go8
-rw-r--r--internal/nametransform/names.go21
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)
+}