aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--internal/contentenc/content.go5
-rw-r--r--internal/fusefrontend/fs.go20
-rw-r--r--internal/fusefrontend/xattr.go157
-rw-r--r--internal/fusefrontend/xattr_unit_test.go41
-rw-r--r--tests/xattr/xattr_integration_test.go96
6 files changed, 299 insertions, 22 deletions
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)
+ }
+ }
+}