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, robool
2828
masterkey, mountpoint, cipherdir, cpuprofile, extpass,
2929
memprofile, ko, passfile, ctlsock, fsname, force_owner, tracestring
30+
// For reverse mode, --exclude is available. It can be specified multiple times.
31+
excludemultipleStrings
3032
// Configuration file name override
3133
configstring
3234
notifypid, scryptnint
@@ -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
SerializeReadsbool
3131
// Force decode even if integrity check fails (openSSL only)
3232
ForceDecodebool
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
ifargs.SerializeReads {
5757
serialize_reads.InitSerializer()
5858
}
59+
iflen(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(relPathstring) (*reverseFile, fuse.Status) {
39+
ifrfs.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+
returnnil, fuse.ENOENT
44+
}
3945
pRelPath, err:=rfs.decryptPath(relPath)
4046
iferr!=nil {
4147
returnnil, 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
funcNewFS(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+
iflen(args.Exclude) >0 {
61+
for_, dirty:=rangeargs.Exclude {
62+
clean:=ctlsock.SanitizePath(dirty)
63+
ifclean!=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+
iferr!=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+
returnfs
5476
}
5577

5678
// relDir is identical to filepath.Dir excepts that it returns "" when
@@ -64,6 +86,21 @@ func relDir(path string) string {
6486
returndir
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(relPathstring) bool {
92+
for_, e:=rangerfs.cExclude {
93+
ife==relPath {
94+
returntrue
95+
}
96+
// Files inside an excluded directory are also excluded
97+
ifstrings.HasPrefix(relPath, e+"/") {
98+
returntrue
99+
}
100+
}
101+
returnfalse
102+
}
103+
67104
// isDirIV determines if the path points to a gocryptfs.diriv file
68105
func (rfs*ReverseFS) isDirIV(relPathstring) bool {
69106
ifrfs.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(relPathstring, context*fuse.Context) (*fuse.Attr, fuse.Status) {
139+
ifrfs.isExcluded(relPath) {
140+
returnnil, fuse.ENOENT
141+
}
102142
// Handle "gocryptfs.conf"
103143
ifrfs.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(relPathstring, modeuint32, context*fuse.Context) fuse.Status {
223+
ifrfs.isExcluded(relPath) {
224+
returnfuse.ENOENT
225+
}
183226
ifrfs.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(relPathstring, flagsuint32, context*fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
249+
ifrfs.isExcluded(relPath) {
250+
returnnil, fuse.ENOENT
251+
}
206252
ifrfs.isTranslatedConfig(relPath) {
207253
returnrfs.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(cipherPathstring, context*fuse.Context) ([]fuse.DirEntry, fuse.Status) {
291+
ifrfs.isExcluded(cipherPath) {
292+
returnnil, fuse.ENOENT
293+
}
245294
relPath, err:=rfs.decryptPath(cipherPath)
246295
iferr!=nil {
247296
returnnil, 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+
ifrfs.cExclude!=nil {
346+
filtered:=make([]fuse.DirEntry, 0, len(entries))
347+
for_, entry:=rangeentries {
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+
ifrfs.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
returnentries, 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(pathstring) *fuse.StatfsOut {
368+
func (rfs*ReverseFS) StatFs(relPathstring) *fuse.StatfsOut {
369+
ifrfs.isExcluded(relPath) {
370+
returnnil
371+
}
305372
vars syscall.Statfs_t
306373
err:=syscall.Statfs(rfs.args.Cipherdir, &s)
307374
iferr!=nil {
@@ -314,6 +381,9 @@ func (rfs *ReverseFS) StatFs(path string) *fuse.StatfsOut {
314381

315382
// Readlink - FUSE call
316383
func (rfs*ReverseFS) Readlink(relPathstring, context*fuse.Context) (string, fuse.Status) {
384+
ifrfs.isExcluded(relPath) {
385+
return"", fuse.ENOENT
386+
}
317387
dirfd, name, err:=rfs.openBackingDir(relPath)
318388
iferr!=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
ifargs.reverse {
214214
args.aessiv=true
215+
} else {
216+
ifargs.exclude!=nil {
217+
tlog.Fatal.Printf("-exclude only works in reverse mode")
218+
os.Exit(exitcodes.ExcludeError)
219+
}
215220
}
216221
// "-config"
217222
ifargs.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
ifconfFile!=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+
funcTestExcludeForward(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+
iferr==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+
constxxx="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+
funcctlsockEncryptPath(t*testing.T, sockstring, pathstring) string {
39+
req:= ctlsock.RequestStruct{EncryptPath: path}
40+
response:=test_helpers.QueryCtlSock(t, sock, req)
41+
ifresponse.ErrNo!=0 {
42+
t.Fatal(response)
43+
}
44+
returnresponse.Result
45+
}
46+
47+
funcTestExclude(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+
iferr!=nil {
72+
t.Fatal(err)
73+
}
74+
sock:=mnt+".sock"
75+
cliArgs:= []string{"-reverse", "-extpass", "echo test", "-ctlsock", sock}
76+
for_, v:=rangepExclude {
77+
cliArgs=append(cliArgs, "-exclude", v)
78+
}
79+
ifplaintextnames {
80+
cliArgs=append(cliArgs, "-config", "exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames")
81+
}
82+
test_helpers.MountOrFatal(t, "exclude_test_fs", mnt, cliArgs...)
83+
defertest_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+
fori, v:=rangepOk {
88+
cOk[i] =ctlsockEncryptPath(t, sock, v)
89+
}
90+
fori, v:=rangepExclude {
91+
cExclude[i] =ctlsockEncryptPath(t, sock, v)
92+
}
93+
// Check that "excluded" paths are not there and "ok" paths are there
94+
fori, v:=rangecExclude {
95+
iftest_helpers.VerifyExistence(mnt+"/"+v) {
96+
t.Errorf("File %q / %q is visible, but should be excluded", pExclude[i], v)
97+
}
98+
}
99+
fori, v:=rangecOk {
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)
close