From 1ed3d51df1750d5472b1349222c352171f1e8d64 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 18 Mar 2018 17:43:38 +0100 Subject: [PATCH] fusefrontend: add xattr support At the moment, only for reverse mode. https://github.com/rfjakob/gocryptfs/issues/217 --- README.md | 2 + internal/contentenc/content.go | 5 +- internal/fusefrontend/fs.go | 20 --- internal/fusefrontend/xattr.go | 157 +++++++++++++++++++++++ internal/fusefrontend/xattr_unit_test.go | 41 ++++++ tests/xattr/xattr_integration_test.go | 96 ++++++++++++++ 6 files changed, 299 insertions(+), 22 deletions(-) create mode 100644 internal/fusefrontend/xattr.go create mode 100644 internal/fusefrontend/xattr_unit_test.go create mode 100644 tests/xattr/xattr_integration_test.go diff --git a/README.md b/README.md index ad86894..5b53061 100644 --- a/README.md +++ b/README.md @@ -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) + } + } +}