aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksey Vasenev2024-11-17 23:14:36 +0300
committerJakob Unterwurzacher2025-11-22 21:24:20 +0100
commited1c5e4a9f5ce1921f3ec03b32e591ce828ec5b9 (patch)
treea2e1f64e772e2af169df3ce4ac955fbf3ec8c83b
parentbe34b9822bea4ce3b717c1b9bf5076f1118427ec (diff)
Xattrs support in reverse mode
Fixes https://github.com/rfjakob/gocryptfs/issues/827
-rw-r--r--internal/fusefrontend/node_xattr.go21
-rw-r--r--internal/fusefrontend_reverse/node_api_check.go6
-rw-r--r--internal/fusefrontend_reverse/node_xattr.go81
-rw-r--r--internal/fusefrontend_reverse/node_xattr_darwin.go52
-rw-r--r--internal/fusefrontend_reverse/node_xattr_linux.go40
-rw-r--r--internal/fusefrontend_reverse/root_node.go35
-rw-r--r--internal/pathiv/pathiv.go2
-rw-r--r--tests/reverse/xattr_test.go6
8 files changed, 217 insertions, 26 deletions
diff --git a/internal/fusefrontend/node_xattr.go b/internal/fusefrontend/node_xattr.go
index 44bc502..8ff7bab 100644
--- a/internal/fusefrontend/node_xattr.go
+++ b/internal/fusefrontend/node_xattr.go
@@ -12,9 +12,6 @@ import (
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)
-// -1 as uint32
-const minus1 = ^uint32(0)
-
// We store encrypted xattrs under this prefix plus the base64-encoded
// encrypted original name.
var xattrStorePrefix = "user.gocryptfs."
@@ -50,13 +47,13 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32,
var errno syscall.Errno
data, errno = n.getXAttr(attr)
if errno != 0 {
- return minus1, errno
+ return 0, errno
}
} else {
// encrypted user xattr
cAttr, err := rn.encryptXattrName(attr)
if err != nil {
- return minus1, syscall.EIO
+ return 0, syscall.EIO
}
cData, errno := n.getXAttr(cAttr)
if errno != 0 {
@@ -65,15 +62,11 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32,
data, err = rn.decryptXattrValue(cData)
if err != nil {
tlog.Warn.Printf("GetXAttr: %v", err)
- return minus1, syscall.EIO
+ return 0, syscall.EIO
}
}
- // Caller passes size zero to find out how large their buffer should be
- if len(dest) == 0 {
- return uint32(len(data)), 0
- }
if len(dest) < len(data) {
- return minus1, syscall.ERANGE
+ return uint32(len(data)), syscall.ERANGE
}
l := copy(dest, data)
return uint32(l), 0
@@ -155,12 +148,8 @@ func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errn
}
buf.WriteString(name + "\000")
}
- // Caller passes size zero to find out how large their buffer should be
- if len(dest) == 0 {
- return uint32(buf.Len()), 0
- }
if buf.Len() > len(dest) {
- return minus1, syscall.ERANGE
+ return uint32(buf.Len()), syscall.ERANGE
}
return uint32(copy(dest, buf.Bytes())), 0
}
diff --git a/internal/fusefrontend_reverse/node_api_check.go b/internal/fusefrontend_reverse/node_api_check.go
index f8ec9ce..eb608f9 100644
--- a/internal/fusefrontend_reverse/node_api_check.go
+++ b/internal/fusefrontend_reverse/node_api_check.go
@@ -11,14 +11,8 @@ var _ = (fs.NodeReaddirer)((*Node)(nil))
var _ = (fs.NodeReadlinker)((*Node)(nil))
var _ = (fs.NodeOpener)((*Node)(nil))
var _ = (fs.NodeStatfser)((*Node)(nil))
-
-/*
-TODO but low prio. reverse mode in gocryptfs v1 did not have xattr support
-either.
-
var _ = (fs.NodeGetxattrer)((*Node)(nil))
var _ = (fs.NodeListxattrer)((*Node)(nil))
-*/
/* Not needed
var _ = (fs.NodeOpendirer)((*Node)(nil))
diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go
new file mode 100644
index 0000000..f4e3bda
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr.go
@@ -0,0 +1,81 @@
+// Package fusefrontend_reverse interfaces directly with the go-fuse library.
+package fusefrontend_reverse
+
+import (
+ "bytes"
+ "context"
+ "syscall"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/pathiv"
+)
+
+// We store encrypted xattrs under this prefix plus the base64-encoded
+// encrypted original name.
+var xattrStorePrefix = "user.gocryptfs."
+
+// isAcl returns true if the attribute name is for storing ACLs
+//
+// ACLs are passed through without encryption
+func isAcl(attr string) bool {
+ return attr == "system.posix_acl_access" || attr == "system.posix_acl_default"
+}
+
+// GetXAttr - FUSE call. Reads the value of extended attribute "attr".
+//
+// This function is symlink-safe through Fgetxattr.
+func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
+ rn := n.rootNode()
+ var data []byte
+ // ACLs are passed through without encryption
+ if isAcl(attr) {
+ var errno syscall.Errno
+ data, errno = n.getXAttr(attr)
+ if errno != 0 {
+ return 0, errno
+ }
+ } else {
+ pAttr, err := rn.decryptXattrName(attr)
+ if err != nil {
+ return 0, syscall.EINVAL
+ }
+ pData, errno := n.getXAttr(pAttr)
+ if errno != 0 {
+ return 0, errno
+ }
+ nonce := pathiv.Derive(n.Path()+"\000"+attr, pathiv.PurposeXattrIV)
+ data = rn.encryptXattrValue(pData, nonce)
+ }
+ if len(dest) < len(data) {
+ return uint32(len(data)), syscall.ERANGE
+ }
+ l := copy(dest, data)
+ return uint32(l), 0
+}
+
+// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath".
+//
+// This function is symlink-safe through Flistxattr.
+func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
+ pNames, errno := n.listXAttr()
+ if errno != 0 {
+ return 0, errno
+ }
+ rn := n.rootNode()
+ var buf bytes.Buffer
+ for _, pName := range pNames {
+ // ACLs are passed through without encryption
+ if isAcl(pName) {
+ buf.WriteString(pName + "\000")
+ continue
+ }
+ cName, err := rn.encryptXattrName(pName)
+ if err != nil {
+ continue
+ }
+ buf.WriteString(cName + "\000")
+ }
+ if buf.Len() > len(dest) {
+ return uint32(buf.Len()), syscall.ERANGE
+ }
+ return uint32(copy(dest, buf.Bytes())), 0
+}
diff --git a/internal/fusefrontend_reverse/node_xattr_darwin.go b/internal/fusefrontend_reverse/node_xattr_darwin.go
new file mode 100644
index 0000000..f5b58d9
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr_darwin.go
@@ -0,0 +1,52 @@
+package fusefrontend_reverse
+
+import (
+ "syscall"
+
+ "github.com/hanwen/go-fuse/v2/fs"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+)
+
+func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
+ d, errno := n.prepareAtSyscall("")
+ if errno != 0 {
+ return
+ }
+ defer syscall.Close(d.dirfd)
+
+ // O_NONBLOCK to not block on FIFOs.
+ fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ if err != nil {
+ return nil, fs.ToErrno(err)
+ }
+ defer syscall.Close(fd)
+
+ cData, err := syscallcompat.Fgetxattr(fd, cAttr)
+ if err != nil {
+ return nil, fs.ToErrno(err)
+ }
+
+ return cData, 0
+}
+
+func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
+ d, errno := n.prepareAtSyscall("")
+ if errno != 0 {
+ return
+ }
+ defer syscall.Close(d.dirfd)
+
+ // O_NONBLOCK to not block on FIFOs.
+ fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
+ if err != nil {
+ return nil, fs.ToErrno(err)
+ }
+ defer syscall.Close(fd)
+
+ pNames, err := syscallcompat.Flistxattr(fd)
+ if err != nil {
+ return nil, fs.ToErrno(err)
+ }
+ return pNames, 0
+}
diff --git a/internal/fusefrontend_reverse/node_xattr_linux.go b/internal/fusefrontend_reverse/node_xattr_linux.go
new file mode 100644
index 0000000..f6d04a5
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr_linux.go
@@ -0,0 +1,40 @@
+package fusefrontend_reverse
+
+import (
+ "fmt"
+ "syscall"
+
+ "github.com/hanwen/go-fuse/v2/fs"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+)
+
+func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
+ d, errno := n.prepareAtSyscall("")
+ if errno != 0 {
+ return
+ }
+ defer syscall.Close(d.dirfd)
+
+ procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName)
+ pData, err := syscallcompat.Lgetxattr(procPath, cAttr)
+ if err != nil {
+ return nil, fs.ToErrno(err)
+ }
+ return pData, 0
+}
+
+func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
+ d, errno := n.prepareAtSyscall("")
+ if errno != 0 {
+ return
+ }
+ defer syscall.Close(d.dirfd)
+
+ procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName)
+ pNames, err := syscallcompat.Llistxattr(procPath)
+ if err != nil {
+ return nil, fs.ToErrno(err)
+ }
+ return pNames, 0
+}
diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go
index 9c2de28..420ed22 100644
--- a/internal/fusefrontend_reverse/root_node.go
+++ b/internal/fusefrontend_reverse/root_node.go
@@ -182,3 +182,38 @@ func (rn *RootNode) uniqueStableAttr(mode uint32, ino uint64) fs.StableAttr {
func (rn *RootNode) RootIno() uint64 {
return rn.rootIno
}
+
+// encryptXattrValue encrypts the xattr value "data".
+// The data is encrypted like a file content block, but without binding it to
+// a file location (block number and file id are set to zero).
+// Special case: an empty value is encrypted to an empty value.
+func (rn *RootNode) encryptXattrValue(data []byte, nonce []byte) (cData []byte) {
+ if len(data) == 0 {
+ return []byte{}
+ }
+ return rn.contentEnc.EncryptBlockNonce(data, 0, nil, nonce)
+}
+
+// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
+func (rn *RootNode) encryptXattrName(attr string) (string, error) {
+ // xattr names are encrypted like file names, but with a fixed IV.
+ cAttr, err := rn.nameTransform.EncryptXattrName(attr)
+ if err != nil {
+ return "", err
+ }
+ return xattrStorePrefix + cAttr, nil
+}
+
+func (rn *RootNode) 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 = rn.nameTransform.DecryptXattrName(cAttr)
+ if err != nil {
+ return "", err
+ }
+ return attr, nil
+}
diff --git a/internal/pathiv/pathiv.go b/internal/pathiv/pathiv.go
index 48f8426..11a7189 100644
--- a/internal/pathiv/pathiv.go
+++ b/internal/pathiv/pathiv.go
@@ -20,6 +20,8 @@ const (
PurposeSymlinkIV Purpose = "SYMLINKIV"
// PurposeBlock0IV means the value will be used as the IV of ciphertext block #0.
PurposeBlock0IV Purpose = "BLOCK0IV"
+ // PurposeXattrIV means the value will be used as a xattr IV
+ PurposeXattrIV Purpose = "XATTRIV"
)
// Derive derives an IV from an encrypted path by hashing it with sha256
diff --git a/tests/reverse/xattr_test.go b/tests/reverse/xattr_test.go
index c70f623..df7724f 100644
--- a/tests/reverse/xattr_test.go
+++ b/tests/reverse/xattr_test.go
@@ -20,8 +20,6 @@ func xattrSupported(path string) bool {
}
func TestXattrList(t *testing.T) {
- t.Skip("TODO: not implemented yet in reverse mode")
-
if !xattrSupported(dirA) {
t.Skip()
}
@@ -32,7 +30,7 @@ func TestXattrList(t *testing.T) {
}
val := []byte("xxxxxxxxyyyyyyyyyyyyyyyzzzzzzzzzzzzz")
num := 20
- var namesA map[string]string
+ namesA := map[string]string{}
for i := 1; i <= num; i++ {
attr := fmt.Sprintf("user.TestXattrList.%02d", i)
err = xattr.LSet(fnA, attr, val)
@@ -46,7 +44,7 @@ func TestXattrList(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- var namesC map[string]string
+ namesC := map[string]string{}
for _, n := range tmp {
namesC[n] = string(val)
}