diff --git a/internal/fusefrontend_reverse/node.go b/internal/fusefrontend_reverse/node.go index 6a1294a..a102a66 100644 --- a/internal/fusefrontend_reverse/node.go +++ b/internal/fusefrontend_reverse/node.go @@ -1,7 +1,15 @@ package fusefrontend_reverse import ( + "context" + "syscall" + + "golang.org/x/sys/unix" + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/syscallcompat" ) // Node is a file or directory in the filesystem tree @@ -9,3 +17,61 @@ import ( type Node struct { fs.Inode } + +// Lookup - FUSE call for discovering a file. +// TODO handle virtual files +func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) { + dirfd, cName, errno := n.prepareAtSyscall(name) + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + // Get device number and inode number into `st` + st, err := syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + return nil, fs.ToErrno(err) + } + + // Create new inode and fill `out` + ch = n.newChild(ctx, st, out) + + // Translate ciphertext size in `out.Attr.Size` to plaintext size + n.translateSize(dirfd, cName, &out.Attr) + + return ch, 0 +} + +// GetAttr - FUSE call for stat()ing a file. +// +// GetAttr is symlink-safe through use of openBackingDir() and Fstatat(). +func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) (errno syscall.Errno) { + // If the kernel gives us a file handle, use it. + if f != nil { + return f.(fs.FileGetattrer).Getattr(ctx, out) + } + + dirfd, cName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + st, err := syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + return fs.ToErrno(err) + } + + // Fix inode number + rn := n.rootNode() + rn.inoMap.TranslateStat(st) + out.Attr.FromStat(st) + + // Translate ciphertext size in `out.Attr.Size` to plaintext size + n.translateSize(dirfd, cName, &out.Attr) + + if rn.args.ForceOwner != nil { + out.Owner = *rn.args.ForceOwner + } + return 0 +} diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go new file mode 100644 index 0000000..a26ee81 --- /dev/null +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -0,0 +1,67 @@ +package fusefrontend_reverse + +import ( + "context" + "path/filepath" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +// translateSize translates the ciphertext size in `out` into plaintext size. +func (n *Node) translateSize(dirfd int, cName string, out *fuse.Attr) { + if out.IsRegular() { + rn := n.rootNode() + out.Size = rn.contentEnc.PlainSizeToCipherSize(out.Size) + } else if out.IsSymlink() { + panic("todo: call readlink once it is implemented") + } +} + +// Path returns the relative plaintext path of this node +func (n *Node) Path() string { + return n.Inode.Path(n.Root()) +} + +// rootNode returns the Root Node of the filesystem. +func (n *Node) rootNode() *RootNode { + return n.Root().Operations().(*RootNode) +} + +// prepareAtSyscall returns a (dirfd, cName) pair that can be used +// with the "___at" family of system calls (openat, fstatat, unlinkat...) to +// access the backing encrypted directory. +// +// If you pass a `child` file name, the (dirfd, cName) pair will refer to +// a child of this node. +// If `child` is empty, the (dirfd, cName) pair refers to this node itself. +func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) { + p := n.Path() + if child != "" { + p = filepath.Join(p, child) + } + rn := n.rootNode() + dirfd, cName, err := rn.openBackingDir(p) + if err != nil { + errno = fs.ToErrno(err) + } + return +} + +// newChild attaches a new child inode to n. +// The passed-in `st` will be modified to get a unique inode number. +func (n *Node) newChild(ctx context.Context, st *syscall.Stat_t, out *fuse.EntryOut) *fs.Inode { + // Get unique inode number + rn := n.rootNode() + rn.inoMap.TranslateStat(st) + out.Attr.FromStat(st) + // Create child node + id := fs.StableAttr{ + Mode: uint32(st.Mode), + Gen: 1, + Ino: st.Ino, + } + node := &Node{} + return n.NewInode(ctx, node, id) +} diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go new file mode 100644 index 0000000..ed37847 --- /dev/null +++ b/internal/fusefrontend_reverse/rpath.go @@ -0,0 +1,102 @@ +package fusefrontend_reverse + +import ( + "encoding/base64" + "path/filepath" + "strings" + "syscall" + + "github.com/rfjakob/gocryptfs/internal/nametransform" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +// abs basically returns storage dir + "/" + relPath. +// It takes an error parameter so it can directly wrap decryptPath like this: +// a, err := rfs.abs(rfs.decryptPath(relPath)) +// abs never generates an error on its own. In other words, abs(p, nil) never +// fails. +func (rfs *RootNode) abs(relPath string, err error) (string, error) { + if err != nil { + return "", err + } + return filepath.Join(rfs.args.Cipherdir, relPath), nil +} + +// rDecryptName decrypts the ciphertext name "cName", given the dirIV of the +// directory "cName" lies in. The relative plaintext path to the directory +// "pDir" is used if a "gocryptfs.longname.XYZ.name" must be resolved. +func (rfs *RootNode) rDecryptName(cName string, dirIV []byte, pDir string) (pName string, err error) { + nameType := nametransform.NameType(cName) + if nameType == nametransform.LongNameNone { + pName, err = rfs.nameTransform.DecryptName(cName, dirIV) + if err != nil { + // We get lots of decrypt requests for names like ".Trash" that + // are invalid base64. Convert them to ENOENT so the correct + // error gets returned to the user. + if _, ok := err.(base64.CorruptInputError); ok { + return "", syscall.ENOENT + } + // Stat attempts on the link target of encrypted symlinks. + // These are always valid base64 but the length is not a + // multiple of 16. + if err == syscall.EBADMSG { + return "", syscall.ENOENT + } + return "", err + } + } else if nameType == nametransform.LongNameContent { + panic("todo") + //pName, err = rfs.findLongnameParent(pDir, dirIV, cName) + if err != nil { + return "", err + } + } else { + // It makes no sense to decrypt a ".name" file. This is a virtual file + // that has no representation in the plaintext filesystem. ".name" + // files should have already been handled in virtualfile.go. + tlog.Warn.Printf("rDecryptName: cannot decrypt virtual file %q", cName) + return "", syscall.EINVAL + } + return pName, nil +} + +// decryptPath decrypts a relative ciphertext path to a relative plaintext +// path. +func (rn *RootNode) decryptPath(relPath string) (string, error) { + if rn.args.PlaintextNames || relPath == "" { + return relPath, nil + } + parts := strings.Split(relPath, "/") + var transformedParts []string + for i := range parts { + // Start at the top and recurse + currentCipherDir := filepath.Join(parts[:i]...) + currentPlainDir := filepath.Join(transformedParts[:i]...) + dirIV := pathiv.Derive(currentCipherDir, pathiv.PurposeDirIV) + transformedPart, err := rn.rDecryptName(parts[i], dirIV, currentPlainDir) + if err != nil { + return "", err + } + transformedParts = append(transformedParts, transformedPart) + } + pRelPath := filepath.Join(transformedParts...) + return pRelPath, nil +} + +// openBackingDir receives an already decrypted relative path +// "pRelPath", opens the directory that contains the target file/dir +// and returns the fd to the directory and the decrypted name of the +// target file. The fd/name pair is intended for use with fchownat and +// friends. +func (rn *RootNode) openBackingDir(pRelPath string) (dirfd int, pName string, err error) { + // Open directory, safe against symlink races + pDir := filepath.Dir(pRelPath) + dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, pDir) + if err != nil { + return -1, "", err + } + pName = filepath.Base(pRelPath) + return dirfd, pName, nil +}