aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend_reverse
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend_reverse')
-rw-r--r--internal/fusefrontend_reverse/excluder.go3
-rw-r--r--internal/fusefrontend_reverse/excluder_test.go5
-rw-r--r--internal/fusefrontend_reverse/node_api_check.go6
-rw-r--r--internal/fusefrontend_reverse/node_helpers.go19
-rw-r--r--internal/fusefrontend_reverse/node_xattr.go89
-rw-r--r--internal/fusefrontend_reverse/node_xattr_darwin.go55
-rw-r--r--internal/fusefrontend_reverse/node_xattr_freebsd.go17
-rw-r--r--internal/fusefrontend_reverse/node_xattr_linux.go43
-rw-r--r--internal/fusefrontend_reverse/root_node.go73
-rw-r--r--internal/fusefrontend_reverse/virtualnode.go9
10 files changed, 290 insertions, 29 deletions
diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go
index 0faadfa..1cb4b80 100644
--- a/internal/fusefrontend_reverse/excluder.go
+++ b/internal/fusefrontend_reverse/excluder.go
@@ -1,7 +1,6 @@
package fusefrontend_reverse
import (
- "io/ioutil"
"log"
"os"
"strings"
@@ -50,7 +49,7 @@ func getExclusionPatterns(args fusefrontend.Args) []string {
// getLines reads a file and splits it into lines
func getLines(file string) ([]string, error) {
- buffer, err := ioutil.ReadFile(file)
+ buffer, err := os.ReadFile(file)
if err != nil {
return nil, err
}
diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go
index bb041ce..b44ddce 100644
--- a/internal/fusefrontend_reverse/excluder_test.go
+++ b/internal/fusefrontend_reverse/excluder_test.go
@@ -1,7 +1,6 @@
package fusefrontend_reverse
import (
- "io/ioutil"
"os"
"reflect"
"testing"
@@ -23,7 +22,7 @@ func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) {
}
func TestShouldReadExcludePatternsFromFiles(t *testing.T) {
- tmpfile1, err := ioutil.TempFile("", "excludetest")
+ tmpfile1, err := os.CreateTemp("", "excludetest")
if err != nil {
t.Fatal(err)
}
@@ -31,7 +30,7 @@ func TestShouldReadExcludePatternsFromFiles(t *testing.T) {
defer os.Remove(exclude1)
defer tmpfile1.Close()
- tmpfile2, err := ioutil.TempFile("", "excludetest")
+ tmpfile2, err := os.CreateTemp("", "excludetest")
if err != nil {
t.Fatal(err)
}
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_helpers.go b/internal/fusefrontend_reverse/node_helpers.go
index 30361bc..dc8d928 100644
--- a/internal/fusefrontend_reverse/node_helpers.go
+++ b/internal/fusefrontend_reverse/node_helpers.go
@@ -17,14 +17,13 @@ import (
)
const (
- // File names are padded to 16-byte multiples, encrypted and
- // base64-encoded. We can encode at most 176 bytes to stay below the 255
- // bytes limit:
- // * base64(176 bytes) = 235 bytes
- // * base64(192 bytes) = 256 bytes (over 255!)
- // But the PKCS#7 padding is at least one byte. This means we can only use
- // 175 bytes for the file name.
- shortNameMax = 175
+// File names are padded to 16-byte multiples, encrypted and
+// base64-encoded. We can encode at most 176 bytes to stay below the 255
+// bytes limit:
+// * base64(176 bytes) = 235 bytes
+// * base64(192 bytes) = 256 bytes (over 255!)
+// But the PKCS#7 padding is at least one byte. This means we can only use
+// 175 bytes for the file name.
)
// translateSize translates the ciphertext size in `out` into plaintext size.
@@ -135,7 +134,7 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus
if errno != 0 {
return
}
- if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) {
+ if rn.isExcludedPlain(filepath.Join(d.pPath, pName)) {
errno = syscall.EPERM
return
}
@@ -175,7 +174,7 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod
errno = fs.ToErrno(err)
return
}
- content := pathiv.Derive(d.cPath, pathiv.PurposeDirIV)
+ content := rn.deriveDirIV(d.cPath)
var vf *VirtualMemNode
vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV)
if errno != 0 {
diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go
new file mode 100644
index 0000000..b940339
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr.go
@@ -0,0 +1,89 @@
+// 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()
+ // If -noxattr is enabled, return ENOATTR for all getxattr calls
+ if rn.args.NoXattr {
+ return 0, noSuchAttributeError
+ }
+ 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, noSuchAttributeError
+ }
+ 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) {
+ rn := n.rootNode()
+ // If -noxattr is enabled, return zero results for listxattr
+ if rn.args.NoXattr {
+ return 0, 0
+ }
+ pNames, errno := n.listXAttr()
+ if errno != 0 {
+ return 0, errno
+ }
+ 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..6816a18
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr_darwin.go
@@ -0,0 +1,55 @@
+package fusefrontend_reverse
+
+import (
+ "syscall"
+
+ "github.com/hanwen/go-fuse/v2/fs"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+)
+
+// On Darwin, ENOATTR is returned when an attribute is not found.
+const noSuchAttributeError = syscall.ENOATTR
+
+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_freebsd.go b/internal/fusefrontend_reverse/node_xattr_freebsd.go
new file mode 100644
index 0000000..949cf15
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr_freebsd.go
@@ -0,0 +1,17 @@
+package fusefrontend_reverse
+
+import (
+ "golang.org/x/sys/unix"
+)
+
+const noSuchAttributeError = unix.ENOATTR
+
+func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) {
+ // TODO
+ return nil, unix.EOPNOTSUPP
+}
+
+func (n *Node) listXAttr() (out []string, errno unix.Errno) {
+ // TODO
+ return nil, unix.EOPNOTSUPP
+}
diff --git a/internal/fusefrontend_reverse/node_xattr_linux.go b/internal/fusefrontend_reverse/node_xattr_linux.go
new file mode 100644
index 0000000..3c574f5
--- /dev/null
+++ b/internal/fusefrontend_reverse/node_xattr_linux.go
@@ -0,0 +1,43 @@
+package fusefrontend_reverse
+
+import (
+ "fmt"
+ "syscall"
+
+ "github.com/hanwen/go-fuse/v2/fs"
+
+ "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
+)
+
+// On Linux, ENODATA is returned when an attribute is not found.
+const noSuchAttributeError = syscall.ENODATA
+
+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 cb04151..461b25c 100644
--- a/internal/fusefrontend_reverse/root_node.go
+++ b/internal/fusefrontend_reverse/root_node.go
@@ -13,6 +13,7 @@ import (
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
+ "github.com/rfjakob/gocryptfs/v2/internal/configfile"
"github.com/rfjakob/gocryptfs/v2/internal/contentenc"
"github.com/rfjakob/gocryptfs/v2/internal/exitcodes"
"github.com/rfjakob/gocryptfs/v2/internal/fusefrontend"
@@ -21,7 +22,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
@@ -35,6 +36,11 @@ type RootNode struct {
contentEnc *contentenc.ContentEnc
// Tests whether a path is excluded (hidden) from the user. Used by -exclude.
excluder ignore.IgnoreParser
+ // configPlainPath is the path of a custom config file (-config), relative to
+ // Cipherdir, set only when that file is located inside Cipherdir. Such a file
+ // is hidden from the encrypted view, otherwise it would leak in encrypted
+ // form (https://github.com/rfjakob/gocryptfs/issues/1009).
+ configPlainPath string
// inoMap translates inode numbers from different devices to unique inode
// numbers.
inoMap *inomap.InoMap
@@ -50,7 +56,7 @@ type RootNode struct {
// makes go-fuse hand out separate FUSE Node IDs for each, and prevents
// bizarre problems when inode numbers are reused behind our back,
// like this one: https://github.com/rfjakob/gocryptfs/issues/802
- gen uint64
+ gen atomic.Uint64
// rootIno is the inode number that we report for the root node on mount
rootIno uint64
}
@@ -62,7 +68,7 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf
var rootDev uint64
var st syscall.Stat_t
var statErr error
- var shortNameMax int
+ var shortNameMax = syscall.NAME_MAX
if statErr = syscall.Stat(args.Cipherdir, &st); statErr != nil {
tlog.Warn.Printf("Could not stat backing directory %q: %v", args.Cipherdir, statErr)
if args.OneFileSystem {
@@ -73,8 +79,10 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf
rootDev = uint64(st.Dev)
}
- shortNameMax = n.GetLongNameMax() * 3 / 4
- shortNameMax = shortNameMax - shortNameMax%16 - 1
+ if !args.PlaintextNames {
+ shortNameMax = n.GetLongNameMax() * 3 / 4
+ shortNameMax = shortNameMax - shortNameMax%16 - 1
+ }
rn := &RootNode{
args: args,
@@ -91,6 +99,14 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf
if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 {
rn.excluder = prepareExcluder(args)
}
+ // A custom config file (-config) located inside Cipherdir must be hidden from
+ // the encrypted view, otherwise it leaks there in encrypted form (#1009).
+ if args.ConfigCustom && args.Config != "" {
+ if rel, err := filepath.Rel(args.Cipherdir, args.Config); err == nil &&
+ rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
+ rn.configPlainPath = rel
+ }
+ }
return rn
}
@@ -136,9 +152,15 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p
// excluded (used when -exclude is passed by the user).
func (rn *RootNode) isExcludedPlain(pPath string) bool {
// root dir can't be excluded
- if pPath == "" {
+ // Don't exclude gocryptfs.conf too
+ if pPath == "" || pPath == configfile.ConfReverseName {
return false
}
+ // A custom config file (-config) inside Cipherdir is hidden from the
+ // encrypted view (https://github.com/rfjakob/gocryptfs/issues/1009).
+ if rn.configPlainPath != "" && pPath == rn.configPlainPath {
+ return true
+ }
return rn.excluder != nil && rn.excluder.MatchesPath(pPath)
}
@@ -146,7 +168,7 @@ func (rn *RootNode) isExcludedPlain(pPath string) bool {
// pDir is the relative plaintext path to the directory these entries are
// from. The entries should be plaintext files.
func (rn *RootNode) excludeDirEntries(d *dirfdPlus, entries []fuse.DirEntry) (filtered []fuse.DirEntry) {
- if rn.excluder == nil {
+ if rn.excluder == nil && rn.configPlainPath == "" {
return entries
}
filtered = make([]fuse.DirEntry, 0, len(entries))
@@ -175,10 +197,45 @@ func (rn *RootNode) uniqueStableAttr(mode uint32, ino uint64) fs.StableAttr {
Ino: ino,
// Make each directory entry a unique node by using a unique generation
// value. Also see the comment at RootNode.gen for details.
- Gen: atomic.AddUint64(&rn.gen, 1),
+ Gen: rn.gen.Add(1),
}
}
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/fusefrontend_reverse/virtualnode.go b/internal/fusefrontend_reverse/virtualnode.go
index 732564a..95e71ab 100644
--- a/internal/fusefrontend_reverse/virtualnode.go
+++ b/internal/fusefrontend_reverse/virtualnode.go
@@ -100,6 +100,15 @@ func (n *Node) newVirtualMemNode(content []byte, parentStat *syscall.Stat_t, ino
st.Nlink = 1
var a fuse.Attr
a.FromStat(st)
+ // With inode number reuse and hard links, we could have returned
+ // wrong data for gocryptfs.diriv and gocryptfs.xyz.longname files, respectively
+ // (https://github.com/rfjakob/gocryptfs/issues/802).
+ //
+ // Now that this is fixed, ensure that rsync and similar tools pick up the new
+ // correct files by advancing mtime and ctime by 10 seconds, which should be more
+ // than any filesytems' timestamp granularity (FAT32 has 2 seconds).
+ a.Mtime += 10
+ a.Ctime += 10
if rn.args.ForceOwner != nil {
a.Owner = *rn.args.ForceOwner
}