diff options
Diffstat (limited to 'internal/fusefrontend_reverse')
| -rw-r--r-- | internal/fusefrontend_reverse/excluder.go | 3 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/excluder_test.go | 5 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_api_check.go | 6 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_helpers.go | 19 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_xattr.go | 89 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_xattr_darwin.go | 55 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_xattr_freebsd.go | 17 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/node_xattr_linux.go | 43 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/root_node.go | 73 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse/virtualnode.go | 9 |
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 } |
