Skip to content

Commit ec2fdc1

Browse files
committed
reverse mode: add --exclude option
#235
1 parent eaa5aec commit ec2fdc1

File tree

28 files changed

+258
-3
lines changed

28 files changed

+258
-3
lines changed

Documentation/MANPAGE.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,18 @@ Enable (`-dev`) or disable (`-nodev`) device files in a gocryptfs mount
7070
You need root permissions to use `-dev`.
7171

7272
#### -devrandom
73-
Use /dev/random for generating the master key instead of the default Go
73+
Use `/dev/random` for generating the master key instead of the default Go
7474
implementation. This is especially useful on embedded systems with Go versions
7575
prior to 1.9, which fall back to weak random data when the getrandom syscall
7676
is blocking. Using this option can block indefinitely when the kernel cannot
7777
harvest enough entropy.
7878

79+
#### -exclude PATH
80+
Only for reverse mode: exclude relative plaintext path from the encrypted
81+
view. Can be passed multiple times. Example:
82+
83+
gocryptfs -reverse -exclude Music -exclude Movies /home/user /mnt/user.encrypted
84+
7985
#### -exec, -noexec
8086
Enable (`-exec`) or disable (`-noexec`) executables in a gocryptfs mount
8187
(default: `-exec`). If both are specified, `-noexec` takes precedence.

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Changelog
155155
vNEXT, in progress
156156
* Fall back to buffered IO even when passed `O_DIRECT`
157157
([commit](https://github.com/rfjakob/gocryptfs/commit/893e41149ed353f355047003b89eeff456990e76))
158+
* Add `-exclude` option for reverse mode
158159

159160
v1.5, 2018-06-12
160161
* **Support extended attributes (xattr)** in forward mode

cli_args.go

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type argContainer struct {
2727
dev, nodev, suid, nosuid, exec, noexec, rw, ro bool
2828
masterkey, mountpoint, cipherdir, cpuprofile, extpass,
2929
memprofile, ko, passfile, ctlsock, fsname, force_owner, trace string
30+
// For reverse mode, --exclude is available. It can be specified multiple times.
31+
exclude multipleStrings
3032
// Configuration file name override
3133
config string
3234
notifypid, scryptn int
@@ -173,6 +175,9 @@ func parseCliOpts() (args argContainer) {
173175
flagSet.StringVar(&args.fsname, "fsname", "", "Override the filesystem name")
174176
flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership")
175177
flagSet.StringVar(&args.trace, "trace", "", "Write execution trace to file")
178+
179+
flagSet.Var(&args.exclude, "exclude", "Exclude relative path from reverse view")
180+
176181
flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+
177182
"successful mount - used internally for daemonization")
178183
flagSet.IntVar(&args.scryptn, "scryptn", configfile.ScryptDefaultLogN, "scrypt cost parameter logN. Possible values: 10-28. "+

internal/exitcodes/exitcodes.go

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ const (
6868
// TrezorError - an error was encountered while interacting with a Trezor
6969
// device
7070
TrezorError = 28
71+
// ExcludeError - an error occoured while processing "-exclude"
72+
ExcludeError = 29
7173
)
7274

7375
// Err wraps an error with an associated numeric exit code

internal/fusefrontend/args.go

+2
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ type Args struct {
3030
SerializeReads bool
3131
// Force decode even if integrity check fails (openSSL only)
3232
ForceDecode bool
33+
// Exclude is a list of paths to make inaccessible
34+
Exclude []string
3335
}

internal/fusefrontend/fs.go

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ func NewFS(args Args, c *contentenc.ContentEnc, n *nametransform.NameTransform)
5656
if args.SerializeReads {
5757
serialize_reads.InitSerializer()
5858
}
59+
if len(args.Exclude) > 0 {
60+
tlog.Warn.Printf("Forward mode does not support -exclude")
61+
}
5962
return &FS{
6063
FileSystem: pathfs.NewLoopbackFileSystem(args.Cipherdir),
6164
args: args,

internal/fusefrontend_reverse/rfile.go

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ var inodeTable syncmap.Map
3636
// newFile decrypts and opens the path "relPath" and returns a reverseFile
3737
// object. The backing file descriptor is always read-only.
3838
func (rfs *ReverseFS) newFile(relPath string) (*reverseFile, fuse.Status) {
39+
if rfs.isExcluded(relPath) {
40+
// Excluded paths should have been filtered out beforehand. Better safe
41+
// than sorry.
42+
tlog.Warn.Printf("BUG: newFile: received excluded path %q. This should not happen.", relPath)
43+
return nil, fuse.ENOENT
44+
}
3945
pRelPath, err := rfs.decryptPath(relPath)
4046
if err != nil {
4147
return nil, fuse.ToStatus(err)

internal/fusefrontend_reverse/rfs.go

+72-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package fusefrontend_reverse
22

33
import (
44
"fmt"
5+
"os"
56
"path/filepath"
7+
"strings"
68
"syscall"
79

810
"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 {
3438
nameTransform *nametransform.NameTransform
3539
// Content encryption helper
3640
contentEnc *contentenc.ContentEnc
41+
// Relative ciphertext paths to exclude (hide) from the user. Used by -exclude.
42+
cExclude []string
3743
}
3844

3945
var _ pathfs.FileSystem = &ReverseFS{}
@@ -43,14 +49,30 @@ var _ pathfs.FileSystem = &ReverseFS{}
4349
// ReverseFS provides an encrypted view.
4450
func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *ReverseFS {
4551
initLongnameCache()
46-
return &ReverseFS{
52+
fs := &ReverseFS{
4753
// pathfs.defaultFileSystem returns ENOSYS for all operations
4854
FileSystem: pathfs.NewDefaultFileSystem(),
4955
loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir),
5056
args: args,
5157
nameTransform: n,
5258
contentEnc: 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
}
5577

5678
// relDir is identical to filepath.Dir excepts that it returns "" when
@@ -64,6 +86,21 @@ func relDir(path string) string {
6486
return dir
6587
}
6688

89+
// 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
68105
func (rfs *ReverseFS) isDirIV(relPath string) bool {
69106
if 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
101138
func (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"
103143
if rfs.isTranslatedConfig(relPath) {
104144
absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil)
@@ -180,6 +220,9 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr
180220

181221
// Access - FUSE call
182222
func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status {
223+
if rfs.isExcluded(relPath) {
224+
return fuse.ENOENT
225+
}
183226
if 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.
185228
ROK := uint32(0x4)
@@ -203,6 +246,9 @@ func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context)
203246

204247
// Open - FUSE call
205248
func (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+
}
206252
if rfs.isTranslatedConfig(relPath) {
207253
return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context)
208254
}
@@ -242,6 +288,9 @@ func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEn
242288

243289
// OpenDir - FUSE readdir call
244290
func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
291+
if rfs.isExcluded(cipherPath) {
292+
return nil, fuse.ENOENT
293+
}
245294
relPath, err := rfs.decryptPath(cipherPath)
246295
if err != nil {
247296
return nil, fuse.ToStatus(err)
@@ -292,6 +341,21 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.
292341
}
293342
entries[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+
}
295359
entries = append(entries, virtualFiles[:nVirtual]...)
296360
return 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+
}
305372
var s syscall.Statfs_t
306373
err := syscall.Statfs(rfs.args.Cipherdir, &s)
307374
if err != nil {
@@ -314,6 +381,9 @@ func (rfs *ReverseFS) StatFs(path string) *fuse.StatfsOut {
314381

315382
// Readlink - FUSE call
316383
func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) {
384+
if rfs.isExcluded(relPath) {
385+
return "", fuse.ENOENT
386+
}
317387
dirfd, name, err := rfs.openBackingDir(relPath)
318388
if err != nil {
319389
return "", fuse.ToStatus(err)

main.go

+5
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ func main() {
212212
// "-reverse" implies "-aessiv"
213213
if args.reverse {
214214
args.aessiv = true
215+
} else {
216+
if args.exclude != nil {
217+
tlog.Fatal.Printf("-exclude only works in reverse mode")
218+
os.Exit(exitcodes.ExcludeError)
219+
}
215220
}
216221
// "-config"
217222
if args.config != "" {

mount.go

+1
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ func initFuseFrontend(args *argContainer) (pfs pathfs.FileSystem, wipeKeys func(
196196
SerializeReads: args.serialize_reads,
197197
ForceDecode: args.forcedecode,
198198
ForceOwner: args._forceOwner,
199+
Exclude: args.exclude,
199200
}
200201
// confFile is nil when "-zerokey" or "-masterkey" was used
201202
if confFile != nil {

tests/cli/cli_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,14 @@ func TestMissingOArg(t *testing.T) {
484484
exitcodes.Usage, exitCode)
485485
}
486486
}
487+
488+
// -exclude must return an error in forward mode
489+
func TestExcludeForward(t *testing.T) {
490+
dir := test_helpers.InitFS(t)
491+
mnt := dir + ".mnt"
492+
err := test_helpers.Mount(dir, mnt, false, "-extpass", "echo test", "-exclude", "foo")
493+
if err == nil {
494+
t.Errorf("-exclude in forward mode should fail")
495+
}
496+
t.Log(err)
497+
}

tests/reverse/exclude_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package reverse_test
2+
3+
import (
4+
"io/ioutil"
5+
"testing"
6+
7+
"github.com/rfjakob/gocryptfs/internal/ctlsock"
8+
"github.com/rfjakob/gocryptfs/tests/test_helpers"
9+
)
10+
11+
const xxx = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
12+
13+
/*
14+
tree exclude_test_fs
15+
exclude_test_fs/
16+
├── dir1
17+
│ ├── file1
18+
│ ├── file2
19+
│ ├── longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
20+
│ └── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
21+
├── dir2
22+
│ ├── file
23+
│ ├── longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
24+
│ │ └── file
25+
│ ├── longfile.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
26+
│ └── subdir
27+
│ └── file
28+
├── file1
29+
├── file2
30+
├── longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
31+
│ └── file
32+
├── longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
33+
│ └── file
34+
├── longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
35+
└── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
36+
*/
37+
38+
func ctlsockEncryptPath(t *testing.T, sock string, path string) string {
39+
req := ctlsock.RequestStruct{EncryptPath: path}
40+
response := test_helpers.QueryCtlSock(t, sock, req)
41+
if response.ErrNo != 0 {
42+
t.Fatal(response)
43+
}
44+
return response.Result
45+
}
46+
47+
func TestExclude(t *testing.T) {
48+
pOk := []string{
49+
"file2",
50+
"dir1/file1",
51+
"dir1/longfile1" + xxx,
52+
"longdir1" + xxx,
53+
"longdir1" + xxx + "/file",
54+
"longfile1" + xxx,
55+
}
56+
pExclude := []string{
57+
"file1",
58+
"dir1/file2",
59+
"dir1/longfile2" + xxx,
60+
"dir2",
61+
"dir2/file",
62+
"dir2/file/xxx",
63+
"dir2/subdir",
64+
"dir2/subdir/file",
65+
"dir2/longdir1" + xxx + "/file",
66+
"dir2/longfile." + xxx,
67+
"longfile2" + xxx,
68+
}
69+
// Mount reverse fs
70+
mnt, err := ioutil.TempDir(test_helpers.TmpDir, "TestExclude")
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
sock := mnt + ".sock"
75+
cliArgs := []string{"-reverse", "-extpass", "echo test", "-ctlsock", sock}
76+
for _, v := range pExclude {
77+
cliArgs = append(cliArgs, "-exclude", v)
78+
}
79+
if plaintextnames {
80+
cliArgs = append(cliArgs, "-config", "exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames")
81+
}
82+
test_helpers.MountOrFatal(t, "exclude_test_fs", mnt, cliArgs...)
83+
defer test_helpers.UnmountPanic(mnt)
84+
// Get encrypted version of "ok" and "excluded" paths
85+
cOk := make([]string, len(pOk))
86+
cExclude := make([]string, len(pExclude))
87+
for i, v := range pOk {
88+
cOk[i] = ctlsockEncryptPath(t, sock, v)
89+
}
90+
for i, v := range pExclude {
91+
cExclude[i] = ctlsockEncryptPath(t, sock, v)
92+
}
93+
// Check that "excluded" paths are not there and "ok" paths are there
94+
for i, v := range cExclude {
95+
if test_helpers.VerifyExistence(mnt + "/" + v) {
96+
t.Errorf("File %q / %q is visible, but should be excluded", pExclude[i], v)
97+
}
98+
}
99+
for i, v := range cOk {
100+
if !test_helpers.VerifyExistence(mnt + "/" + v) {
101+
t.Errorf("File %q / %q is hidden, but should be visible", pOk[i], v)
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)