Commit ec2fdc19 authored by Jakob Unterwurzacher's avatar Jakob Unterwurzacher

reverse mode: add --exclude option

https://github.com/rfjakob/gocryptfs/issues/235
parent eaa5aecd
......@@ -70,12 +70,18 @@ Enable (`-dev`) or disable (`-nodev`) device files in a gocryptfs mount
You need root permissions to use `-dev`.
#### -devrandom
Use /dev/random for generating the master key instead of the default Go
Use `/dev/random` for generating the master key instead of the default Go
implementation. This is especially useful on embedded systems with Go versions
prior to 1.9, which fall back to weak random data when the getrandom syscall
is blocking. Using this option can block indefinitely when the kernel cannot
harvest enough entropy.
#### -exclude PATH
Only for reverse mode: exclude relative plaintext path from the encrypted
view. Can be passed multiple times. Example:
gocryptfs -reverse -exclude Music -exclude Movies /home/user /mnt/user.encrypted
#### -exec, -noexec
Enable (`-exec`) or disable (`-noexec`) executables in a gocryptfs mount
(default: `-exec`). If both are specified, `-noexec` takes precedence.
......
......@@ -155,6 +155,7 @@ Changelog
vNEXT, in progress
* Fall back to buffered IO even when passed `O_DIRECT`
([commit](https://github.com/rfjakob/gocryptfs/commit/893e41149ed353f355047003b89eeff456990e76))
* Add `-exclude` option for reverse mode
v1.5, 2018-06-12
* **Support extended attributes (xattr)** in forward mode
......
......@@ -27,6 +27,8 @@ type argContainer struct {
dev, nodev, suid, nosuid, exec, noexec, rw, ro bool
masterkey, mountpoint, cipherdir, cpuprofile, extpass,
memprofile, ko, passfile, ctlsock, fsname, force_owner, trace string
// For reverse mode, --exclude is available. It can be specified multiple times.
exclude multipleStrings
// Configuration file name override
config string
notifypid, scryptn int
......@@ -173,6 +175,9 @@ func parseCliOpts() (args argContainer) {
flagSet.StringVar(&args.fsname, "fsname", "", "Override the filesystem name")
flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership")
flagSet.StringVar(&args.trace, "trace", "", "Write execution trace to file")
flagSet.Var(&args.exclude, "exclude", "Exclude relative path from reverse view")
flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+
"successful mount - used internally for daemonization")
flagSet.IntVar(&args.scryptn, "scryptn", configfile.ScryptDefaultLogN, "scrypt cost parameter logN. Possible values: 10-28. "+
......
......@@ -68,6 +68,8 @@ const (
// TrezorError - an error was encountered while interacting with a Trezor
// device
TrezorError = 28
// ExcludeError - an error occoured while processing "-exclude"
ExcludeError = 29
)
// Err wraps an error with an associated numeric exit code
......
......@@ -30,4 +30,6 @@ 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 []string
}
......@@ -56,6 +56,9 @@ func NewFS(args Args, c *contentenc.ContentEnc, n *nametransform.NameTransform)
if args.SerializeReads {
serialize_reads.InitSerializer()
}
if len(args.Exclude) > 0 {
tlog.Warn.Printf("Forward mode does not support -exclude")
}
return &FS{
FileSystem: pathfs.NewLoopbackFileSystem(args.Cipherdir),
args: args,
......
......@@ -36,6 +36,12 @@ var inodeTable syncmap.Map
// newFile decrypts and opens the path "relPath" 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) {
// 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)
......
......@@ -2,7 +2,9 @@ package fusefrontend_reverse
import (
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"golang.org/x/sys/unix"
......@@ -14,6 +16,8 @@ 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"
......@@ -34,6 +38,8 @@ type ReverseFS struct {
nameTransform *nametransform.NameTransform
// Content encryption helper
contentEnc *contentenc.ContentEnc
// Relative ciphertext paths to exclude (hide) from the user. Used by -exclude.
cExclude []string
}
var _ pathfs.FileSystem = &ReverseFS{}
......@@ -43,7 +49,7 @@ var _ pathfs.FileSystem = &ReverseFS{}
// ReverseFS provides an encrypted view.
func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *ReverseFS {
initLongnameCache()
return &ReverseFS{
fs := &ReverseFS{
// pathfs.defaultFileSystem returns ENOSYS for all operations
FileSystem: pathfs.NewDefaultFileSystem(),
loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir),
......@@ -51,6 +57,22 @@ 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)
}
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)
}
tlog.Debug.Printf("-exclude: %v -> %v", fs.args.Exclude, fs.cExclude)
}
return fs
}
// relDir is identical to filepath.Dir excepts that it returns "" when
......@@ -64,6 +86,21 @@ 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 e == relPath {
return true
}
// Files inside an excluded directory are also excluded
if strings.HasPrefix(relPath, e+"/") {
return true
}
}
return false
}
// isDirIV determines if the path points to a gocryptfs.diriv file
func (rfs *ReverseFS) isDirIV(relPath string) bool {
if rfs.args.PlaintextNames {
......@@ -99,6 +136,9 @@ 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) {
return nil, fuse.ENOENT
}
// Handle "gocryptfs.conf"
if rfs.isTranslatedConfig(relPath) {
absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil)
......@@ -180,6 +220,9 @@ 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) {
return fuse.ENOENT
}
if rfs.isTranslatedConfig(relPath) || rfs.isDirIV(relPath) || rfs.isNameFile(relPath) {
// access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX.
ROK := uint32(0x4)
......@@ -203,6 +246,9 @@ 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) {
return nil, fuse.ENOENT
}
if rfs.isTranslatedConfig(relPath) {
return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context)
}
......@@ -242,6 +288,9 @@ 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) {
return nil, fuse.ENOENT
}
relPath, err := rfs.decryptPath(cipherPath)
if err != nil {
return nil, fuse.ToStatus(err)
......@@ -292,6 +341,21 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.
}
entries[i].Name = cName
}
// Filter out excluded entries
if rfs.cExclude != nil {
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(cipherPath, entry.Name)
if rfs.isExcluded(p) {
// Skip file
continue
}
filtered = append(filtered, entry)
}
entries = filtered
}
entries = append(entries, virtualFiles[:nVirtual]...)
return entries, fuse.OK
}
......@@ -301,7 +365,10 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.
// Securing statfs against symlink races seems to be more trouble than
// it's worth, so we just ignore the path and always return info about the
// backing storage root dir.
func (rfs *ReverseFS) StatFs(path string) *fuse.StatfsOut {
func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut {
if rfs.isExcluded(relPath) {
return nil
}
var s syscall.Statfs_t
err := syscall.Statfs(rfs.args.Cipherdir, &s)
if err != nil {
......@@ -314,6 +381,9 @@ func (rfs *ReverseFS) StatFs(path string) *fuse.StatfsOut {
// Readlink - FUSE call
func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) {
if rfs.isExcluded(relPath) {
return "", fuse.ENOENT
}
dirfd, name, err := rfs.openBackingDir(relPath)
if err != nil {
return "", fuse.ToStatus(err)
......
......@@ -212,6 +212,11 @@ func main() {
// "-reverse" implies "-aessiv"
if args.reverse {
args.aessiv = true
} else {
if args.exclude != nil {
tlog.Fatal.Printf("-exclude only works in reverse mode")
os.Exit(exitcodes.ExcludeError)
}
}
// "-config"
if args.config != "" {
......
......@@ -196,6 +196,7 @@ func initFuseFrontend(args *argContainer) (pfs pathfs.FileSystem, wipeKeys func(
SerializeReads: args.serialize_reads,
ForceDecode: args.forcedecode,
ForceOwner: args._forceOwner,
Exclude: args.exclude,
}
// confFile is nil when "-zerokey" or "-masterkey" was used
if confFile != nil {
......
......@@ -484,3 +484,14 @@ func TestMissingOArg(t *testing.T) {
exitcodes.Usage, exitCode)
}
}
// -exclude must return an error in forward mode
func TestExcludeForward(t *testing.T) {
dir := test_helpers.InitFS(t)
mnt := dir + ".mnt"
err := test_helpers.Mount(dir, mnt, false, "-extpass", "echo test", "-exclude", "foo")
if err == nil {
t.Errorf("-exclude in forward mode should fail")
}
t.Log(err)
}
package reverse_test
import (
"io/ioutil"
"testing"
"github.com/rfjakob/gocryptfs/internal/ctlsock"
"github.com/rfjakob/gocryptfs/tests/test_helpers"
)
const xxx = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
/*
tree exclude_test_fs
exclude_test_fs/
├── dir1
│ ├── file1
│ ├── file2
│ ├── longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
│ └── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
├── dir2
│ ├── file
│ ├── longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
│ │ └── file
│ ├── longfile.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
│ └── subdir
│ └── file
├── file1
├── file2
├── longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
│ └── file
├── longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
│ └── file
├── longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
└── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
*/
func ctlsockEncryptPath(t *testing.T, sock string, path string) string {
req := ctlsock.RequestStruct{EncryptPath: path}
response := test_helpers.QueryCtlSock(t, sock, req)
if response.ErrNo != 0 {
t.Fatal(response)
}
return response.Result
}
func TestExclude(t *testing.T) {
pOk := []string{
"file2",
"dir1/file1",
"dir1/longfile1" + xxx,
"longdir1" + xxx,
"longdir1" + xxx + "/file",
"longfile1" + xxx,
}
pExclude := []string{
"file1",
"dir1/file2",
"dir1/longfile2" + xxx,
"dir2",
"dir2/file",
"dir2/file/xxx",
"dir2/subdir",
"dir2/subdir/file",
"dir2/longdir1" + xxx + "/file",
"dir2/longfile." + xxx,
"longfile2" + xxx,
}
// Mount reverse fs
mnt, err := ioutil.TempDir(test_helpers.TmpDir, "TestExclude")
if err != nil {
t.Fatal(err)
}
sock := mnt + ".sock"
cliArgs := []string{"-reverse", "-extpass", "echo test", "-ctlsock", sock}
for _, v := range pExclude {
cliArgs = append(cliArgs, "-exclude", v)
}
if plaintextnames {
cliArgs = append(cliArgs, "-config", "exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames")
}
test_helpers.MountOrFatal(t, "exclude_test_fs", mnt, cliArgs...)
defer test_helpers.UnmountPanic(mnt)
// Get encrypted version of "ok" and "excluded" paths
cOk := make([]string, len(pOk))
cExclude := make([]string, len(pExclude))
for i, v := range pOk {
cOk[i] = ctlsockEncryptPath(t, sock, v)
}
for i, v := range pExclude {
cExclude[i] = ctlsockEncryptPath(t, sock, v)
}
// Check that "excluded" paths are not there and "ok" paths are there
for i, v := range cExclude {
if test_helpers.VerifyExistence(mnt + "/" + v) {
t.Errorf("File %q / %q is visible, but should be excluded", pExclude[i], v)
}
}
for i, v := range cOk {
if !test_helpers.VerifyExistence(mnt + "/" + v) {
t.Errorf("File %q / %q is hidden, but should be visible", pOk[i], v)
}
}
}
{
"Creator": "gocryptfs v1.5-41-gf48b731-dirty",
"EncryptedKey": "FkACqloUeFZesem0UzRD3ezLXtPl8wIAxEHoIEfZxFdLMQeWOxqtw5xopJagDWE/GI1VFSUIrJIIIwwgMipmYA==",
"ScryptObject": {
"Salt": "UVfIgV31uj/voHWI4GqGwsTcbVKyYDOWvbleqJKhZbk=",
"N": 1024,
"R": 8,
"P": 1,
"KeyLen": 32
},
"Version": 2,
"FeatureFlags": [
"GCMIV128",
"HKDF",
"DirIV",
"EMENames",
"LongNames",
"Raw64",
"AESSIV"
]
}
{
"Creator": "gocryptfs v1.5-41-gf48b731-dirty",
"EncryptedKey": "wAmckZb7QsIv/GCdkhb5ep8TwJa44qhnswn5tbER6Tifk8TbUmkwBTceaTtYfHAnTQ48q9mnIlcN9cfbNe5oPw==",
"ScryptObject": {
"Salt": "o5XJ78TgG85zZXRnU55ZqHhKLbPge6jsyDiqrLvSqe0=",
"N": 1024,
"R": 8,
"P": 1,
"KeyLen": 32
},
"Version": 2,
"FeatureFlags": [
"GCMIV128",
"HKDF",
"PlaintextNames",
"AESSIV"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment