summaryrefslogtreecommitdiff
path: root/internal/fusefrontend_reverse_v1api/rfs.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend_reverse_v1api/rfs.go')
-rw-r--r--internal/fusefrontend_reverse_v1api/rfs.go472
1 files changed, 472 insertions, 0 deletions
diff --git a/internal/fusefrontend_reverse_v1api/rfs.go b/internal/fusefrontend_reverse_v1api/rfs.go
new file mode 100644
index 0000000..9f7d1f6
--- /dev/null
+++ b/internal/fusefrontend_reverse_v1api/rfs.go
@@ -0,0 +1,472 @@
+package fusefrontend_reverse
+
+import (
+ "fmt"
+ "path/filepath"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+ "github.com/hanwen/go-fuse/v2/fuse/nodefs"
+ "github.com/hanwen/go-fuse/v2/fuse/pathfs"
+
+ "github.com/rfjakob/gocryptfs/internal/configfile"
+ "github.com/rfjakob/gocryptfs/internal/contentenc"
+ "github.com/rfjakob/gocryptfs/internal/cryptocore"
+ "github.com/rfjakob/gocryptfs/internal/fusefrontend"
+ "github.com/rfjakob/gocryptfs/internal/inomap"
+ "github.com/rfjakob/gocryptfs/internal/nametransform"
+ "github.com/rfjakob/gocryptfs/internal/pathiv"
+ "github.com/rfjakob/gocryptfs/internal/syscallcompat"
+ "github.com/rfjakob/gocryptfs/internal/tlog"
+
+ "github.com/sabhiram/go-gitignore"
+)
+
+// ReverseFS implements the pathfs.FileSystem interface and provides an
+// encrypted view of a plaintext directory.
+type ReverseFS struct {
+ // Embed pathfs.defaultFileSystem for a ENOSYS implementation of all methods
+ pathfs.FileSystem
+ // pathfs.loopbackFileSystem, see go-fuse/fuse/pathfs/loopback.go
+ loopbackfs pathfs.FileSystem
+ // Stores configuration arguments
+ args fusefrontend.Args
+ // Filename encryption helper
+ nameTransform nametransform.NameTransformer
+ // Content encryption helper
+ contentEnc *contentenc.ContentEnc
+ // Tests whether a path is excluded (hiden) from the user. Used by -exclude.
+ excluder ignore.IgnoreParser
+ // inoMap translates inode numbers from different devices to unique inode
+ // numbers.
+ inoMap *inomap.InoMap
+}
+
+var _ pathfs.FileSystem = &ReverseFS{}
+
+// NewFS returns an encrypted FUSE overlay filesystem.
+// In this case (reverse mode) the backing directory is plain-text and
+// ReverseFS provides an encrypted view.
+func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *ReverseFS {
+ initLongnameCache()
+ fs := &ReverseFS{
+ // pathfs.defaultFileSystem returns ENOSYS for all operations
+ FileSystem: pathfs.NewDefaultFileSystem(),
+ loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir),
+ args: args,
+ nameTransform: n,
+ contentEnc: c,
+ inoMap: inomap.New(),
+ }
+ fs.prepareExcluder(args)
+ return fs
+}
+
+// relDir is identical to filepath.Dir excepts that it returns "" when
+// filepath.Dir would return ".".
+// In the FUSE API, the root directory is called "", and we actually want that.
+func relDir(path string) string {
+ dir := filepath.Dir(path)
+ if dir == "." {
+ return ""
+ }
+ return dir
+}
+
+// getFileInfo returns information on a ciphertext path "relPath":
+// - ftype: file type (as returned by getFileType)
+// - excluded: if the path is excluded
+// - pPath: if it's not a special file, the decrypted path
+// - err: non nil if any error happens
+func (rfs *ReverseFS) getFileInfo(relPath string) (ftype fileType, excluded bool, pPath string, err error) {
+ ftype = rfs.getFileType(relPath)
+ if ftype == typeConfig {
+ excluded, pPath, err = false, "", nil
+ return
+ }
+ if ftype == typeDiriv {
+ parentDir := nametransform.Dir(relPath)
+ _, excluded, _, err = rfs.getFileInfo(parentDir)
+ pPath = ""
+ return
+ }
+ if ftype == typeName {
+ parentDir := nametransform.Dir(relPath)
+ var parentExcluded bool
+ _, parentExcluded, _, err = rfs.getFileInfo(parentDir)
+ if parentExcluded || err != nil {
+ excluded, pPath = parentExcluded, ""
+ return
+ }
+ relPath = nametransform.RemoveLongNameSuffix(relPath)
+ }
+ pPath, err = rfs.decryptPath(relPath)
+ excluded = err == nil && rfs.isExcludedPlain(pPath)
+ return
+}
+
+type fileType int
+
+// Values returned by getFileType
+const (
+ // A regular file/directory/symlink
+ typeRegular fileType = iota
+ // A DirIV (gocryptfs.diriv) file
+ typeDiriv
+ // A gocryptfs.longname.*.name file for a file with a long name
+ typeName
+ // The config file gocryptfs.conf
+ typeConfig
+)
+
+// getFileType returns the type of file (one of the fileType constants above).
+func (rfs *ReverseFS) getFileType(cPath string) fileType {
+ if !rfs.args.PlaintextNames {
+ cName := filepath.Base(cPath)
+ // Is it a gocryptfs.diriv file?
+ if cName == nametransform.DirIVFilename {
+ return typeDiriv
+ }
+ // Is it a gocryptfs.longname.*.name file?
+ if t := nametransform.NameType(cName); t == nametransform.LongNameFilename {
+ return typeName
+ }
+ }
+ if rfs.isTranslatedConfig(cPath) {
+ return typeConfig
+ }
+ return typeRegular
+}
+
+// isTranslatedConfig returns true if the default config file name is in use
+// and the ciphertext path is "gocryptfs.conf".
+// "gocryptfs.conf" then maps to ".gocryptfs.reverse.conf" in the plaintext
+// directory.
+func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool {
+ if rfs.args.ConfigCustom {
+ return false
+ }
+ if relPath == configfile.ConfDefaultName {
+ return true
+ }
+ return false
+}
+
+// GetAttr - FUSE call
+// "relPath" is the relative ciphertext path
+func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
+ ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
+ if excluded {
+ return nil, fuse.ENOENT
+ }
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ // Handle "gocryptfs.conf"
+ if ftype == typeConfig {
+ absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil)
+ var st syscall.Stat_t
+ err = syscall.Lstat(absConfPath, &st)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ rfs.inoMap.TranslateStat(&st)
+ var a fuse.Attr
+ a.FromStat(&st)
+ if rfs.args.ForceOwner != nil {
+ a.Owner = *rfs.args.ForceOwner
+ }
+ return &a, fuse.OK
+ }
+ // Handle virtual files (gocryptfs.diriv, *.name)
+ var f nodefs.File
+ var status fuse.Status
+ virtual := false
+ if ftype == typeDiriv {
+ virtual = true
+ f, status = rfs.newDirIVFile(relPath)
+ }
+ if ftype == typeName {
+ virtual = true
+ f, status = rfs.newNameFile(relPath)
+ }
+ if virtual {
+ if !status.Ok() {
+ tlog.Warn.Printf("GetAttr %q: newXFile failed: %v\n", relPath, status)
+ return nil, status
+ }
+ var a fuse.Attr
+ status = f.GetAttr(&a)
+ if rfs.args.ForceOwner != nil {
+ a.Owner = *rfs.args.ForceOwner
+ }
+ return &a, status
+ }
+ // Normal file / directory
+ dirfd, name, err := rfs.openBackingDir(pPath)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ // Stat the backing file/dir using Fstatat
+ var st syscall.Stat_t
+ {
+ var st2 unix.Stat_t
+ err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW)
+ syscall.Close(dirfd)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ st = syscallcompat.Unix2syscall(st2)
+ }
+ rfs.inoMap.TranslateStat(&st)
+ var a fuse.Attr
+ a.FromStat(&st)
+ // Calculate encrypted file size
+ if a.IsRegular() {
+ a.Size = rfs.contentEnc.PlainSizeToCipherSize(a.Size)
+ } else if a.IsSymlink() {
+ var linkTarget string
+ var readlinkStatus fuse.Status
+
+ linkTarget, readlinkStatus = rfs.Readlink(relPath, context)
+ if !readlinkStatus.Ok() {
+ return nil, readlinkStatus
+ }
+
+ a.Size = uint64(len(linkTarget))
+ }
+ if rfs.args.ForceOwner != nil {
+ a.Owner = *rfs.args.ForceOwner
+ }
+ return &a, fuse.OK
+}
+
+// Access - FUSE call
+func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status {
+ ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
+ if excluded {
+ return fuse.ENOENT
+ }
+ if err != nil {
+ return fuse.ToStatus(err)
+ }
+ if ftype != typeRegular {
+ // access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX.
+ ROK := uint32(0x4)
+ // Virtual files can always be read and never written
+ if mode == ROK || mode == 0 {
+ return fuse.OK
+ }
+ return fuse.EPERM
+ }
+ dirfd, name, err := rfs.openBackingDir(pPath)
+ if err != nil {
+ return fuse.ToStatus(err)
+ }
+ err = syscallcompat.Faccessat(dirfd, name, mode)
+ syscall.Close(dirfd)
+ return fuse.ToStatus(err)
+}
+
+// Open - FUSE call
+func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
+ ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
+ if excluded {
+ return nil, fuse.ENOENT
+ }
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ if ftype == typeConfig {
+ return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context)
+ }
+ if ftype == typeDiriv {
+ return rfs.newDirIVFile(relPath)
+ }
+ if ftype == typeName {
+ return rfs.newNameFile(relPath)
+ }
+ return rfs.newFile(relPath, pPath)
+}
+
+func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) {
+ if relPath != "" || rfs.args.ConfigCustom {
+ return entries, fuse.OK
+ }
+ // We are in the root dir and the default config file name
+ // ".gocryptfs.reverse.conf" is used. We map it to "gocryptfs.conf".
+ dupe := -1
+ status := fuse.OK
+ for i := range entries {
+ if entries[i].Name == configfile.ConfReverseName {
+ entries[i].Name = configfile.ConfDefaultName
+ } else if entries[i].Name == configfile.ConfDefaultName {
+ dupe = i
+ }
+ }
+ if dupe >= 0 {
+ // Warn the user loudly: The gocryptfs.conf_NAME_COLLISION file will
+ // throw ENOENT errors that are hard to miss.
+ tlog.Warn.Printf("The file %q is mapped to %q and shadows another file. Please rename %q in directory %q.",
+ configfile.ConfReverseName, configfile.ConfDefaultName, configfile.ConfDefaultName, rfs.args.Cipherdir)
+ entries[dupe].Name = "gocryptfs.conf_NAME_COLLISION_" + fmt.Sprintf("%d", cryptocore.RandUint64())
+ }
+ return entries, status
+}
+
+// OpenDir - FUSE readdir call
+func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
+ ftype, excluded, relPath, err := rfs.getFileInfo(cipherPath)
+ if excluded {
+ return nil, fuse.ENOENT
+ }
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ if ftype != typeRegular {
+ return nil, fuse.ENOTDIR
+ }
+ // Read plaintext dir
+ dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(relPath))
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ fd, err := syscallcompat.Openat(dirfd, filepath.Base(relPath), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
+ syscall.Close(dirfd)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ entries, err := syscallcompat.Getdents(fd)
+ syscall.Close(fd)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ if rfs.args.PlaintextNames {
+ var status fuse.Status
+ entries, status = rfs.openDirPlaintextnames(cipherPath, entries)
+ if !status.Ok() {
+ return nil, status
+ }
+ entries = rfs.excludeDirEntries(relPath, entries)
+ return entries, fuse.OK
+ }
+ // Filter out excluded entries
+ entries = rfs.excludeDirEntries(relPath, entries)
+ // Allocate maximum possible number of virtual files.
+ // If all files have long names we need a virtual ".name" file for each,
+ // plus one for gocryptfs.diriv.
+ virtualFiles := make([]fuse.DirEntry, len(entries)+1)
+ // Virtual gocryptfs.diriv file
+ virtualFiles[0] = fuse.DirEntry{
+ Mode: virtualFileMode,
+ Name: nametransform.DirIVFilename,
+ }
+ // Actually used entries
+ nVirtual := 1
+
+ // Encrypt names
+ dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV)
+ for i := range entries {
+ var cName string
+ // ".gocryptfs.reverse.conf" in the root directory is mapped to "gocryptfs.conf"
+ if cipherPath == "" && entries[i].Name == configfile.ConfReverseName &&
+ !rfs.args.ConfigCustom {
+ cName = configfile.ConfDefaultName
+ } else {
+ cName = rfs.nameTransform.EncryptName(entries[i].Name, dirIV)
+ if len(cName) > unix.NAME_MAX {
+ cName = rfs.nameTransform.HashLongName(cName)
+ dotNameFile := fuse.DirEntry{
+ Mode: virtualFileMode,
+ Name: cName + nametransform.LongNameSuffix,
+ }
+ virtualFiles[nVirtual] = dotNameFile
+ nVirtual++
+ }
+ }
+ entries[i].Name = cName
+ }
+ // Add virtual files
+ entries = append(entries, virtualFiles[:nVirtual]...)
+ return entries, fuse.OK
+}
+
+// excludeDirEntries filters out directory entries that are "-exclude"d.
+// pDir is the relative plaintext path to the directory these entries are
+// from. The entries should be plaintext files.
+func (rfs *ReverseFS) excludeDirEntries(pDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) {
+ if rfs.excluder == nil {
+ return entries
+ }
+ filtered = make([]fuse.DirEntry, 0, len(entries))
+ for _, entry := range entries {
+ // filepath.Join handles the case of pDir="" correctly:
+ // Join("", "foo") -> "foo". This does not: pDir + "/" + name"
+ p := filepath.Join(pDir, entry.Name)
+ if rfs.isExcludedPlain(p) {
+ // Skip file
+ continue
+ }
+ filtered = append(filtered, entry)
+ }
+ return filtered
+}
+
+// StatFs - FUSE call. Returns information about the filesystem (free space
+// etc).
+// Securing statfs against symlink races seems to be more trouble than
+// it's worth, so we just ignore the path and always return info about the
+// backing storage root dir.
+func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut {
+ _, excluded, _, err := rfs.getFileInfo(relPath)
+ if excluded || err != nil {
+ return nil
+ }
+ var s syscall.Statfs_t
+ err = syscall.Statfs(rfs.args.Cipherdir, &s)
+ if err != nil {
+ return nil
+ }
+ out := &fuse.StatfsOut{}
+ out.FromStatfsT(&s)
+ return out
+}
+
+// Readlink - FUSE call
+func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) {
+ ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
+ if excluded {
+ return "", fuse.ENOENT
+ }
+ if err != nil {
+ return "", fuse.ToStatus(err)
+ }
+ if ftype != typeRegular {
+ return "", fuse.EINVAL
+ }
+ dirfd, name, err := rfs.openBackingDir(pPath)
+ if err != nil {
+ return "", fuse.ToStatus(err)
+ }
+ // read the link target using Readlinkat
+ plainTarget, err := syscallcompat.Readlinkat(dirfd, name)
+ syscall.Close(dirfd)
+ if err != nil {
+ return "", fuse.ToStatus(err)
+ }
+ if rfs.args.PlaintextNames {
+ return plainTarget, fuse.OK
+ }
+ nonce := pathiv.Derive(relPath, pathiv.PurposeSymlinkIV)
+ // Symlinks are encrypted like file contents and base64-encoded
+ cBinTarget := rfs.contentEnc.EncryptBlockNonce([]byte(plainTarget), 0, nil, nonce)
+ cTarget := rfs.nameTransform.B64EncodeToString(cBinTarget)
+ // The kernel will reject a symlink target above 4096 chars and return
+ // and I/O error to the user. Better emit the proper error ourselves.
+ if len(cTarget) > syscallcompat.PATH_MAX {
+ return "", fuse.Status(syscall.ENAMETOOLONG)
+ }
+ return cTarget, fuse.OK
+}