From 57d572dbc10cfb1d14642598b0827d4119b26b64 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Tue, 14 Jul 2020 19:55:20 +0200 Subject: [PATCH] v2api: implement Getxattr, Setxattr, Removexattr, Listxattr gocryptfs/tests/xattr passes. --- internal/fusefrontend/node_api_check.go | 4 +- internal/fusefrontend/node_xattr.go | 86 ++++++++++++++++++++ internal/fusefrontend/node_xattr_darwin.go | 0 internal/fusefrontend/node_xattr_linux.go | 68 ++++++++++++++++ internal/fusefrontend/openbackingdir_test.go | 2 +- internal/fusefrontend/root_node.go | 52 ++++++++++++ internal/fusefrontend/xattr.go | 8 -- internal/fusefrontend/xattr_linux.go | 4 - internal/fusefrontend/xattr_unit_test.go | 19 ++--- 9 files changed, 217 insertions(+), 26 deletions(-) create mode 100644 internal/fusefrontend/node_xattr.go create mode 100644 internal/fusefrontend/node_xattr_darwin.go create mode 100644 internal/fusefrontend/node_xattr_linux.go diff --git a/internal/fusefrontend/node_api_check.go b/internal/fusefrontend/node_api_check.go index ac48341..0f60c74 100644 --- a/internal/fusefrontend/node_api_check.go +++ b/internal/fusefrontend/node_api_check.go @@ -21,11 +21,11 @@ var _ = (fs.NodeMknoder)((*Node)(nil)) var _ = (fs.NodeLinker)((*Node)(nil)) var _ = (fs.NodeSymlinker)((*Node)(nil)) var _ = (fs.NodeRenamer)((*Node)(nil)) - -/* TODO var _ = (fs.NodeGetxattrer)((*Node)(nil)) var _ = (fs.NodeSetxattrer)((*Node)(nil)) var _ = (fs.NodeRemovexattrer)((*Node)(nil)) var _ = (fs.NodeListxattrer)((*Node)(nil)) + +/* TODO var _ = (fs.NodeCopyFileRanger)((*Node)(nil)) */ diff --git a/internal/fusefrontend/node_xattr.go b/internal/fusefrontend/node_xattr.go new file mode 100644 index 0000000..de40915 --- /dev/null +++ b/internal/fusefrontend/node_xattr.go @@ -0,0 +1,86 @@ +// Package fusefrontend interfaces directly with the go-fuse library. +package fusefrontend + +import ( + "bytes" + "context" + "strings" + "syscall" + + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +// xattr names are encrypted like file names, but with a fixed IV. +// Padded with "_xx" for length 16. +var xattrNameIV = []byte("xattr_name_iv_xx") + +// We store encrypted xattrs under this prefix plus the base64-encoded +// encrypted original name. +var xattrStorePrefix = "user.gocryptfs." + +// 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() + cAttr := rn.encryptXattrName(attr) + cData, errno := n.getXAttr(cAttr) + if errno != 0 { + return 0, errno + } + data, err := rn.decryptXattrValue(cData) + if err != nil { + tlog.Warn.Printf("GetXAttr: %v", err) + return ^uint32(0), syscall.EIO + } + l := copy(dest, data) + return uint32(l), 0 +} + +// SetXAttr - FUSE call. Set extended attribute. +// +// This function is symlink-safe through Fsetxattr. +func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { + rn := n.rootNode() + flags = uint32(filterXattrSetFlags(int(flags))) + cAttr := rn.encryptXattrName(attr) + cData := rn.encryptXattrValue(data) + return n.setXAttr(cAttr, cData, flags) +} + +// RemoveXAttr - FUSE call. +// +// This function is symlink-safe through Fremovexattr. +func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { + rn := n.rootNode() + cAttr := rn.encryptXattrName(attr) + return n.removeXAttr(cAttr) +} + +// 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) { + cNames, errno := n.listXAttr() + if errno != 0 { + return 0, errno + } + rn := n.rootNode() + var buf bytes.Buffer + for _, curName := range cNames { + if !strings.HasPrefix(curName, xattrStorePrefix) { + continue + } + name, err := rn.decryptXattrName(curName) + if err != nil { + tlog.Warn.Printf("ListXAttr: invalid xattr name %q: %v", curName, err) + rn.reportMitigatedCorruption(curName) + continue + } + buf.WriteString(name + "\000") + } + if buf.Len() > len(dest) { + return ^uint32(0), syscall.ERANGE + } + return uint32(copy(dest, buf.Bytes())), 0 +} diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/fusefrontend/node_xattr_linux.go b/internal/fusefrontend/node_xattr_linux.go new file mode 100644 index 0000000..342bdf6 --- /dev/null +++ b/internal/fusefrontend/node_xattr_linux.go @@ -0,0 +1,68 @@ +package fusefrontend + +import ( + "fmt" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/internal/syscallcompat" +) + +func filterXattrSetFlags(flags int) int { + return flags +} + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + dirfd, cName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + cData, err := syscallcompat.Lgetxattr(procPath, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + return cData, 0 +} + +func (n *Node) setXAttr(cAttr string, cData []byte, flags uint32) (errno syscall.Errno) { + dirfd, cName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + return fs.ToErrno(unix.Lsetxattr(procPath, cAttr, cData, int(flags))) +} + +func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) { + dirfd, cName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + return fs.ToErrno(unix.Lremovexattr(procPath, cAttr)) +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + dirfd, cName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", dirfd, cName) + cNames, err := syscallcompat.Llistxattr(procPath) + if err != nil { + return nil, fs.ToErrno(err) + } + return cNames, 0 +} diff --git a/internal/fusefrontend/openbackingdir_test.go b/internal/fusefrontend/openbackingdir_test.go index 48c20d6..6d799b7 100644 --- a/internal/fusefrontend/openbackingdir_test.go +++ b/internal/fusefrontend/openbackingdir_test.go @@ -40,7 +40,7 @@ func TestOpenBackingDir(t *testing.T) { syscall.Close(dirfd) // Again, but populate the cache for "" by looking up a non-existing file - fs.Getattr(nil, "xyz1234", &fuse.AttrOut{}) + fs.Lookup(nil, "xyz1234", &fuse.EntryOut{}) dirfd, cName, err = fs.openBackingDir("") if err != nil { t.Fatal(err) diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index 72d5581..5870c97 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -265,3 +265,55 @@ func (rn *RootNode) encryptSymlinkTarget(data string) (cData64 string) { cData64 = rn.nameTransform.B64EncodeToString(cData) return cData64 } + +// 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) (cData []byte) { + if len(data) == 0 { + return []byte{} + } + return rn.contentEnc.EncryptBlock(data, 0, nil) +} + +// decryptXattrValue decrypts the xattr value "cData". +func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) { + if len(cData) == 0 { + return []byte{}, nil + } + data, err1 := rn.contentEnc.DecryptBlock([]byte(cData), 0, nil) + if err1 == nil { + return data, nil + } + // This backward compatibility is needed to support old + // file systems having xattr values base64-encoded. + cData, err2 := rn.nameTransform.B64DecodeString(string(cData)) + if err2 != nil { + // Looks like the value was not base64-encoded, but just corrupt. + // Return the original decryption error: err1 + return nil, err1 + } + return rn.contentEnc.DecryptBlock([]byte(cData), 0, nil) +} + +// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" +func (rn *RootNode) encryptXattrName(attr string) (cAttr string) { + // xattr names are encrypted like file names, but with a fixed IV. + cAttr = xattrStorePrefix + rn.nameTransform.EncryptName(attr, xattrNameIV) + return cAttr +} + +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.DecryptName(cAttr, xattrNameIV) + if err != nil { + return "", err + } + return attr, nil +} diff --git a/internal/fusefrontend/xattr.go b/internal/fusefrontend/xattr.go index d55de3e..6638d83 100644 --- a/internal/fusefrontend/xattr.go +++ b/internal/fusefrontend/xattr.go @@ -12,14 +12,6 @@ import ( const _EOPNOTSUPP = fuse.Status(syscall.EOPNOTSUPP) -// xattr names are encrypted like file names, but with a fixed IV. -// Padded with "_xx" for length 16. -var xattrNameIV = []byte("xattr_name_iv_xx") - -// We store encrypted xattrs under this prefix plus the base64-encoded -// encrypted original name. -var xattrStorePrefix = "user.gocryptfs." - // GetXAttr - FUSE call. Reads the value of extended attribute "attr". // // This function is symlink-safe through Fgetxattr. diff --git a/internal/fusefrontend/xattr_linux.go b/internal/fusefrontend/xattr_linux.go index a4d2710..5df0617 100644 --- a/internal/fusefrontend/xattr_linux.go +++ b/internal/fusefrontend/xattr_linux.go @@ -14,10 +14,6 @@ import ( "github.com/rfjakob/gocryptfs/internal/syscallcompat" ) -func filterXattrSetFlags(flags int) int { - return flags -} - func (fs *FS) getXAttr(relPath string, cAttr string, context *fuse.Context) ([]byte, fuse.Status) { dirfd, cName, err := fs.openBackingDir(relPath) if err != nil { diff --git a/internal/fusefrontend/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go index 0a5e14a..f6c0469 100644 --- a/internal/fusefrontend/xattr_unit_test.go +++ b/internal/fusefrontend/xattr_unit_test.go @@ -31,15 +31,12 @@ func newTestFS(args Args) *RootNode { } func TestEncryptDecryptXattrName(t *testing.T) { - t.Fatal("not yet implemented") - /* - fs := newTestFS(Args{}) - attr1 := "user.foo123456789" - cAttr := fs.encryptXattrName(attr1) - t.Logf("cAttr=%v", cAttr) - attr2, err := fs.decryptXattrName(cAttr) - if attr1 != attr2 || err != nil { - t.Fatalf("Decrypt mismatch: %v != %v", attr1, attr2) - } - */ + fs := newTestFS(Args{}) + attr1 := "user.foo123456789" + cAttr := fs.encryptXattrName(attr1) + t.Logf("cAttr=%v", cAttr) + attr2, err := fs.decryptXattrName(cAttr) + if attr1 != attr2 || err != nil { + t.Fatalf("Decrypt mismatch: %v != %v", attr1, attr2) + } }