diff options
Diffstat (limited to 'internal/fusefrontend_reverse/rfs.go')
-rw-r--r-- | internal/fusefrontend_reverse/rfs.go | 472 |
1 files changed, 0 insertions, 472 deletions
diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go deleted file mode 100644 index 9f7d1f6..0000000 --- a/internal/fusefrontend_reverse/rfs.go +++ /dev/null @@ -1,472 +0,0 @@ -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 -} |