reverse mode: add --exclude option · rfjakob/gocryptfs@ec2fdc1
@@ -2,7 +2,9 @@ package fusefrontend_reverse
2233import (
44"fmt"
5+"os"
56"path/filepath"
7+"strings"
68"syscall"
79810"golang.org/x/sys/unix"
@@ -14,6 +16,8 @@ import (
1416"github.com/rfjakob/gocryptfs/internal/configfile"
1517"github.com/rfjakob/gocryptfs/internal/contentenc"
1618"github.com/rfjakob/gocryptfs/internal/cryptocore"
19+"github.com/rfjakob/gocryptfs/internal/ctlsock"
20+"github.com/rfjakob/gocryptfs/internal/exitcodes"
1721"github.com/rfjakob/gocryptfs/internal/fusefrontend"
1822"github.com/rfjakob/gocryptfs/internal/nametransform"
1923"github.com/rfjakob/gocryptfs/internal/pathiv"
@@ -34,6 +38,8 @@ type ReverseFS struct {
3438nameTransform *nametransform.NameTransform
3539// Content encryption helper
3640contentEnc *contentenc.ContentEnc
41+// Relative ciphertext paths to exclude (hide) from the user. Used by -exclude.
42+cExclude []string
3743}
38443945var _ pathfs.FileSystem = &ReverseFS{}
@@ -43,14 +49,30 @@ var _ pathfs.FileSystem = &ReverseFS{}
4349// ReverseFS provides an encrypted view.
4450func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *ReverseFS {
4551initLongnameCache()
46-return &ReverseFS{
52+fs := &ReverseFS{
4753// pathfs.defaultFileSystem returns ENOSYS for all operations
4854FileSystem: pathfs.NewDefaultFileSystem(),
4955loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir),
5056args: args,
5157nameTransform: n,
5258contentEnc: c,
5359 }
60+if len(args.Exclude) > 0 {
61+for _, dirty := range args.Exclude {
62+clean := ctlsock.SanitizePath(dirty)
63+if clean != dirty {
64+tlog.Warn.Printf("-exclude: non-canonical path %q has been interpreted as %q", dirty, clean)
65+ }
66+cPath, err := fs.EncryptPath(clean)
67+if err != nil {
68+tlog.Fatal.Printf("-exclude: EncryptPath %q failed: %v", clean, err)
69+os.Exit(exitcodes.ExcludeError)
70+ }
71+fs.cExclude = append(fs.cExclude, cPath)
72+ }
73+tlog.Debug.Printf("-exclude: %v -> %v", fs.args.Exclude, fs.cExclude)
74+ }
75+return fs
5476}
55775678// relDir is identical to filepath.Dir excepts that it returns "" when
@@ -64,6 +86,21 @@ func relDir(path string) string {
6486return dir
6587}
668889+// isExcluded finds out if relative ciphertext path "relPath" is excluded
90+// (used when -exclude is passed by the user)
91+func (rfs *ReverseFS) isExcluded(relPath string) bool {
92+for _, e := range rfs.cExclude {
93+if e == relPath {
94+return true
95+ }
96+// Files inside an excluded directory are also excluded
97+if strings.HasPrefix(relPath, e+"/") {
98+return true
99+ }
100+ }
101+return false
102+}
103+67104// isDirIV determines if the path points to a gocryptfs.diriv file
68105func (rfs *ReverseFS) isDirIV(relPath string) bool {
69106if rfs.args.PlaintextNames {
@@ -99,6 +136,9 @@ func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool {
99136// GetAttr - FUSE call
100137// "relPath" is the relative ciphertext path
101138func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
139+if rfs.isExcluded(relPath) {
140+return nil, fuse.ENOENT
141+ }
102142// Handle "gocryptfs.conf"
103143if rfs.isTranslatedConfig(relPath) {
104144absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil)
@@ -180,6 +220,9 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr
180220181221// Access - FUSE call
182222func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status {
223+if rfs.isExcluded(relPath) {
224+return fuse.ENOENT
225+ }
183226if rfs.isTranslatedConfig(relPath) || rfs.isDirIV(relPath) || rfs.isNameFile(relPath) {
184227// access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX.
185228ROK := uint32(0x4)
@@ -203,6 +246,9 @@ func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context)
203246204247// Open - FUSE call
205248func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
249+if rfs.isExcluded(relPath) {
250+return nil, fuse.ENOENT
251+ }
206252if rfs.isTranslatedConfig(relPath) {
207253return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context)
208254 }
@@ -242,6 +288,9 @@ func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEn
242288243289// OpenDir - FUSE readdir call
244290func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
291+if rfs.isExcluded(cipherPath) {
292+return nil, fuse.ENOENT
293+ }
245294relPath, err := rfs.decryptPath(cipherPath)
246295if err != nil {
247296return nil, fuse.ToStatus(err)
@@ -292,6 +341,21 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.
292341 }
293342entries[i].Name = cName
294343 }
344+// Filter out excluded entries
345+if rfs.cExclude != nil {
346+filtered := make([]fuse.DirEntry, 0, len(entries))
347+for _, entry := range entries {
348+// filepath.Join handles the case of cipherPath="" correctly:
349+// Join("", "foo") -> "foo". This does not: cipherPath + "/" + name"
350+p := filepath.Join(cipherPath, entry.Name)
351+if rfs.isExcluded(p) {
352+// Skip file
353+continue
354+ }
355+filtered = append(filtered, entry)
356+ }
357+entries = filtered
358+ }
295359entries = append(entries, virtualFiles[:nVirtual]...)
296360return entries, fuse.OK
297361}
@@ -301,7 +365,10 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.
301365// Securing statfs against symlink races seems to be more trouble than
302366// it's worth, so we just ignore the path and always return info about the
303367// backing storage root dir.
304-func (rfs *ReverseFS) StatFs(path string) *fuse.StatfsOut {
368+func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut {
369+if rfs.isExcluded(relPath) {
370+return nil
371+ }
305372var s syscall.Statfs_t
306373err := syscall.Statfs(rfs.args.Cipherdir, &s)
307374if err != nil {
@@ -314,6 +381,9 @@ func (rfs *ReverseFS) StatFs(path string) *fuse.StatfsOut {
314381315382// Readlink - FUSE call
316383func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) {
384+if rfs.isExcluded(relPath) {
385+return "", fuse.ENOENT
386+ }
317387dirfd, name, err := rfs.openBackingDir(relPath)
318388if err != nil {
319389return "", fuse.ToStatus(err)