summaryrefslogtreecommitdiff
path: root/internal/fusefrontend_reverse/rfs.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend_reverse/rfs.go')
-rw-r--r--internal/fusefrontend_reverse/rfs.go472
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
-}