diff options
| author | Jakob Unterwurzacher | 2018-03-18 17:43:38 +0100 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2018-03-25 21:06:10 +0200 | 
| commit | 1ed3d51df1750d5472b1349222c352171f1e8d64 (patch) | |
| tree | b5f02aa633e8ebf1a71c4d1778a2e29310541d33 | |
| parent | f20974c4dad913a07ce5808005fe07084f87c95f (diff) | |
fusefrontend: add xattr support
At the moment, only for reverse mode.
https://github.com/rfjakob/gocryptfs/issues/217
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | internal/contentenc/content.go | 5 | ||||
| -rw-r--r-- | internal/fusefrontend/fs.go | 20 | ||||
| -rw-r--r-- | internal/fusefrontend/xattr.go | 157 | ||||
| -rw-r--r-- | internal/fusefrontend/xattr_unit_test.go | 41 | ||||
| -rw-r--r-- | tests/xattr/xattr_integration_test.go | 96 | 
6 files changed, 299 insertions, 22 deletions
| @@ -156,6 +156,8 @@ Changelog  vNEXT, in progress  * Add `-masterkey=stdin` functionality    ([#218](https://github.com/rfjakob/gocryptfs/issues/218)) +* Support extended attributes (xattr) in forward mode +  ([#217](https://github.com/rfjakob/gocryptfs/issues/217))  v1.4.4, 2018-03-18  * Overwrite secrets in memory with zeros as soon as possible diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go index e2a531c..c4ba7c9 100644 --- a/internal/contentenc/content.go +++ b/internal/contentenc/content.go @@ -131,10 +131,11 @@ func (be *ContentEnc) DecryptBlocks(ciphertext []byte, firstBlockNo uint64, file  // concatAD concatenates the block number and the file ID to a byte blob  // that can be passed to AES-GCM as associated data (AD). -// Result is: aData = blockNo.bigEndian + fileID. +// Result is: aData = [blockNo.bigEndian fileID].  func concatAD(blockNo uint64, fileID []byte) (aData []byte) {  	if fileID != nil && len(fileID) != headerIDLen { -		// fileID is nil when decrypting the master key from the config file +		// fileID is nil when decrypting the master key from the config file, +		// and for symlinks and xattrs.  		log.Panicf("wrong fileID length: %d", len(fileID))  	}  	const lenUint64 = 8 diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index 00361e8..738f113 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -592,23 +592,3 @@ func (fs *FS) Access(path string, mode uint32, context *fuse.Context) (code fuse  	}  	return fuse.ToStatus(syscall.Access(cPath, mode))  } - -// GetXAttr implements pathfs.Filesystem. -func (fs *FS) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) { -	return nil, fuse.ENOSYS -} - -// SetXAttr implements pathfs.Filesystem. -func (fs *FS) SetXAttr(name string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status { -	return fuse.ENOSYS -} - -// ListXAttr implements pathfs.Filesystem. -func (fs *FS) ListXAttr(name string, context *fuse.Context) ([]string, fuse.Status) { -	return nil, fuse.ENOSYS -} - -// RemoveXAttr implements pathfs.Filesystem. -func (fs *FS) RemoveXAttr(name string, attr string, context *fuse.Context) fuse.Status { -	return fuse.ENOSYS -} diff --git a/internal/fusefrontend/xattr.go b/internal/fusefrontend/xattr.go new file mode 100644 index 0000000..357e889 --- /dev/null +++ b/internal/fusefrontend/xattr.go @@ -0,0 +1,157 @@ +// Package fusefrontend interfaces directly with the go-fuse library. +package fusefrontend + +// FUSE operations on paths + +import ( +	"strings" +	"syscall" + +	"github.com/hanwen/go-fuse/fuse" +	xattr "github.com/rfjakob/pkg-xattr" + +	"github.com/rfjakob/gocryptfs/internal/tlog" +) + +// xattr names are encrypted like file names, but with a fixed IV. +var xattrNameIV = []byte("xattr_name_iv_xx") + +// Only allow the "user" namespace, block "trusted" and "security", as +// these may be interpreted by the system, and we don't want to cause +// trouble with our encrypted garbage. +var xattrUserPrefix = "user." + +// We store encrypted xattrs under this prefix plus the base64-encoded +// encrypted original name. +var xattrStorePrefix = "user.gocryptfs." + +// GetXAttr: read the value of extended attribute "attr". +// Implements pathfs.Filesystem. +func (fs *FS) GetXAttr(path string, attr string, context *fuse.Context) ([]byte, fuse.Status) { +	if fs.isFiltered(path) { +		return nil, fuse.EPERM +	} +	cAttr, err := fs.encryptXattrName(attr) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	cData64, err := xattr.Get(cPath, cAttr) +	if err != nil { +		return nil, unpackXattrErr(err) +	} +	// xattr data is decrypted like a symlink target +	data, err := fs.decryptSymlinkTarget(string(cData64)) +	if err != nil { +		tlog.Warn.Printf("GetXAttr: %v", err) +		return nil, fuse.EIO +	} +	return []byte(data), fuse.OK +} + +// SetXAttr implements pathfs.Filesystem. +func (fs *FS) SetXAttr(path string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	if flags != 0 { +		return fuse.EPERM +	} +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	cAttr, err := fs.encryptXattrName(attr) +	if err != nil { +		return fuse.ToStatus(err) +	} +	// xattr data is encrypted like a symlink target +	cData64 := []byte(fs.encryptSymlinkTarget(string(data))) +	return unpackXattrErr(xattr.Set(cPath, cAttr, cData64)) +} + +// RemoveXAttr implements pathfs.Filesystem. +func (fs *FS) RemoveXAttr(path string, attr string, context *fuse.Context) fuse.Status { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	cAttr, err := fs.encryptXattrName(attr) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return unpackXattrErr(xattr.Remove(cPath, cAttr)) +} + +// ListXAttr implements pathfs.Filesystem. +func (fs *FS) ListXAttr(path string, context *fuse.Context) ([]string, fuse.Status) { +	if fs.isFiltered(path) { +		return nil, fuse.EPERM +	} +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	cNames, err := xattr.List(cPath) +	if err != nil { +		return nil, unpackXattrErr(err) +	} +	names := make([]string, 0, len(cNames)) +	for _, curName := range cNames { +		if !strings.HasPrefix(curName, xattrStorePrefix) { +			continue +		} +		name, err := fs.decryptXattrName(curName) +		if err != nil { +			tlog.Warn.Printf("ListXAttr: invalid xattr name %q: %v", curName, err) +			continue +		} +		names = append(names, name) +	} +	return names, fuse.OK +} + +// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" +func (fs *FS) encryptXattrName(attr string) (cAttr string, err error) { +	// Reject anything that does not start with "user." +	if !strings.HasPrefix(attr, xattrUserPrefix) { +		return "", syscall.EPERM +	} +	// xattr names are encrypted like file names, but with a fixed IV. +	cAttr = xattrStorePrefix + fs.nameTransform.EncryptName(attr, xattrNameIV) +	return cAttr, nil +} + +func (fs *FS) decryptXattrName(cAttr string) (attr string, err error) { +	// Reject anything that does not start with "user.gocryptfs." +	if !strings.HasPrefix(cAttr, xattrStorePrefix) { +		return "", syscall.EINVAL +	} +	// Strip "user.gocryptfs." prefix +	cAttr = cAttr[len(xattrStorePrefix):] +	attr, err = fs.nameTransform.DecryptName(cAttr, xattrNameIV) +	if err != nil { +		return "", err +	} +	return attr, nil +} + +// unpackXattrErr unpacks an error value that we got from xattr.Get/Set/etc +// and converts it to a fuse status. +func unpackXattrErr(err error) fuse.Status { +	if err == nil { +		return fuse.OK +	} +	err2, ok := err.(*xattr.Error) +	if !ok { +		tlog.Warn.Printf("unpackXattrErr: cannot unpack err=%v", err) +		return fuse.EIO +	} +	return fuse.ToStatus(err2.Err) +} diff --git a/internal/fusefrontend/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go new file mode 100644 index 0000000..ea5d3bb --- /dev/null +++ b/internal/fusefrontend/xattr_unit_test.go @@ -0,0 +1,41 @@ +package fusefrontend + +// This file is named "xattr_unit_test.go" because there is also a +// "xattr_integration_test.go" in the test/xattr package. + +import ( +	"syscall" +	"testing" + +	"github.com/rfjakob/gocryptfs/internal/contentenc" +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +	"github.com/rfjakob/gocryptfs/internal/nametransform" +) + +func newTestFS() *FS { +	// Init crypto backend +	key := make([]byte, cryptocore.KeyLen) +	cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true, false) +	cEnc := contentenc.New(cCore, contentenc.DefaultBS, false) +	nameTransform := nametransform.New(cCore.EMECipher, true, true) +	args := Args{} +	return NewFS(args, cEnc, nameTransform) +} + +func TestEncryptDecryptXattrName(t *testing.T) { +	fs := newTestFS() +	_, err := fs.encryptXattrName("xxxx") +	if err != syscall.EPERM { +		t.Fatalf("Names that don't start with 'user.' should fail") +	} +	attr1 := "user.foo123456789" +	cAttr, err := fs.encryptXattrName(attr1) +	if err != nil { +		t.Fatal(err) +	} +	t.Logf("cAttr=%v", cAttr) +	attr2, err := fs.decryptXattrName(cAttr) +	if attr1 != attr2 { +		t.Fatalf("Decrypt mismatch: %v != %v", attr1, attr2) +	} +} diff --git a/tests/xattr/xattr_integration_test.go b/tests/xattr/xattr_integration_test.go new file mode 100644 index 0000000..1e081c2 --- /dev/null +++ b/tests/xattr/xattr_integration_test.go @@ -0,0 +1,96 @@ +package defaults + +import ( +	"bytes" +	"fmt" +	"io/ioutil" +	"os" +	"strings" +	"testing" + +	xattr "github.com/rfjakob/pkg-xattr" + +	"github.com/rfjakob/gocryptfs/tests/test_helpers" +) + +// On modern Linux distributions, /tmp may be on tmpfs, +// which does not support user xattrs. Try /var/tmp instead. +var alternateTestParentDir = "/var/tmp/gocryptfs-xattr-test-parent" + +func TestMain(m *testing.M) { +	if !xattr.Supported(test_helpers.TmpDir) { +		test_helpers.SwitchTestParentDir(alternateTestParentDir) +	} +	if !xattr.Supported(test_helpers.TmpDir) { +		fmt.Printf("xattrs not supported on %q", test_helpers.TmpDir) +		os.Exit(1) +	} +	test_helpers.ResetTmpDir(true) +	test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, "-zerokey") +	r := m.Run() +	test_helpers.UnmountPanic(test_helpers.DefaultPlainDir) +	os.RemoveAll(test_helpers.TmpDir) +	os.Exit(r) +} + +func TestXattrSetGetRm(t *testing.T) { +	attr := "user.foo" +	fn := test_helpers.DefaultPlainDir + "/TestXattrSetGetRm" +	err := ioutil.WriteFile(fn, nil, 0700) +	if err != nil { +		t.Fatalf("creating empty file failed: %v", err) +	} +	// Set +	val1 := []byte("123456789") +	err = xattr.Set(fn, attr, val1) +	if err != nil { +		t.Fatal(err) +	} +	// Read back +	val2, err := xattr.Get(fn, attr) +	if err != nil { +		t.Fatal(err) +	} +	if !bytes.Equal(val1, val2) { +		t.Fatalf("wrong readback value: %v != %v", val1, val2) +	} +	// Remove +	err = xattr.Remove(fn, attr) +	if err != nil { +		t.Fatal(err) +	} +	// Read back +	val3, err := xattr.Get(fn, attr) +	if err == nil { +		t.Fatalf("attr is still there after deletion!? val3=%v", val3) +	} +} + +func TestXattrList(t *testing.T) { +	fn := test_helpers.DefaultPlainDir + "/TestXattrList" +	err := ioutil.WriteFile(fn, nil, 0700) +	if err != nil { +		t.Fatalf("creating empty file failed: %v", err) +	} +	val := []byte("xxxxxxxxyyyyyyyyyyyyyyyzzzzzzzzzzzzz") +	num := 20 +	for i := 1; i <= num; i++ { +		attr := fmt.Sprintf("user.TestXattrList.%02d", i) +		err = xattr.Set(fn, attr, val) +		if err != nil { +			t.Fatal(err) +		} +	} +	names, err := xattr.List(fn) +	if err != nil { +		t.Fatal(err) +	} +	if len(names) != num { +		t.Errorf("wrong number of names, want=%d have=%d", num, len(names)) +	} +	for _, n := range names { +		if !strings.HasPrefix(n, "user.TestXattrList.") { +			t.Errorf("unexpected attr name: %q", n) +		} +	} +} | 
