Skip to content

Commit

Permalink
Xattrs support in reverse mode
Browse files Browse the repository at this point in the history
Fixes #827
  • Loading branch information
Ratio2 committed Nov 18, 2024
1 parent 8689105 commit 411b4f9
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 23 deletions.
21 changes: 5 additions & 16 deletions internal/fusefrontend/node_xattr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
6 changes: 0 additions & 6 deletions internal/fusefrontend_reverse/node_api_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
81 changes: 81 additions & 0 deletions internal/fusefrontend_reverse/node_xattr.go
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 52 additions & 0 deletions internal/fusefrontend_reverse/node_xattr_darwin.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions internal/fusefrontend_reverse/node_xattr_linux.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 36 additions & 1 deletion internal/fusefrontend_reverse/root_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"

"github.com/sabhiram/go-gitignore"
ignore "github.com/sabhiram/go-gitignore"
)

// RootNode is the root directory in a `gocryptfs -reverse` mount
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions internal/pathiv/pathiv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 411b4f9

Please sign in to comment.