Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xattrs support in reverse mode #878

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}
Comment on lines -71 to -74
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior of the function is described in:
https://github.com/hanwen/go-fuse/blob/756578b6a0b03511fde147b2f23c5b97217ede27/fs/api.go#L312
It doesn't make sense, since the following condition will give the same result:
https://github.com/hanwen/go-fuse/blob/756578b6a0b03511fde147b2f23c5b97217ede27/fuse/opcode.go#L285

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
6 changes: 2 additions & 4 deletions tests/reverse/xattr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,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()
}
Expand All @@ -35,7 +33,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)
Expand All @@ -49,7 +47,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)
}
Expand Down
Loading